Skip to content

Commit

Permalink
Use a proxy model for the Subject model
Browse files Browse the repository at this point in the history
This is easier compared to the previous approach since it means we don't have to manually manage our tokens(at least for now). Less code to write, less code to maintain!

- disable the subject auth backend for now.
  • Loading branch information
trevor-james-nangosha committed Nov 8, 2024
1 parent 0255ca7 commit fde4c78
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 124 deletions.
54 changes: 27 additions & 27 deletions api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,37 @@
Disease,
Result, Location)

class SubjectAdmin(admin.ModelAdmin):
# control which columns appear in the list view
list_display = ('email', 'name', 'name')
# class SubjectAdmin(admin.ModelAdmin):
# # control which columns appear in the list view
# list_display = ('email', 'name', 'name')

# Fields for when adding a user
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'name',
'email',
'password',
)}
),
)
# # Fields for when adding a user
# add_fieldsets = (
# (None, {
# 'classes': ('wide',),
# 'fields': (
# 'name',
# 'email',
# 'password',
# )}
# ),
# )

# Fields for editing existing users, grouped in fieldsets
# fieldsets = (
# (None, {'fields': ('email', 'password')}),
# ('Personal info', {'fields': ('name',)}),
# ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
# ('Important dates', {'fields': ('last_login', 'date_joined')}),
# )
# # Fields for editing existing users, grouped in fieldsets
# # fieldsets = (
# # (None, {'fields': ('email', 'password')}),
# # ('Personal info', {'fields': ('name',)}),
# # ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
# # ('Important dates', {'fields': ('last_login', 'date_joined')}),
# # )

# Fields you can search by
search_fields = ('email', 'name')
# # Fields you can search by
# search_fields = ('email', 'name')

# Fields you can use to order the user list
ordering = ('email', 'name')
# # Fields you can use to order the user list
# ordering = ('email', 'name')

readonly_fields = ('date_archived', 'date_deleted', 'created_at', 'modified_at')
# readonly_fields = ('date_archived', 'date_deleted', 'created_at', 'modified_at')

class TestAdmin(admin.ModelAdmin):
list_display = ('disease_id', 'subject')
Expand Down Expand Up @@ -100,7 +100,7 @@ class LocationAdmin(admin.ModelAdmin):

readonly_fields = ('created_at', 'modified_at')

admin.site.register(Subject, SubjectAdmin)
# admin.site.register(Subject, SubjectAdmin)
admin.site.register(Test, TestAdmin)
admin.site.register(Disease, DiseaseAdmin)
admin.site.register(Result, ResultAdmin)
Expand Down
57 changes: 10 additions & 47 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin

class CustomUserManager(BaseUserManager):
def create_user(self, email, name, password=None):
def create_user(self, email, name, password=None, user_role=None):
if not email:
raise ValueError('Users must have an email address')

user = self.model(email=self.normalize_email(email), name=name)
user.set_password(password)

if user_role:
user.user_role = user_role

user.save(using=self._db)
return user

Expand All @@ -26,6 +31,7 @@ class UserRole(models.TextChoices):
LAB_TECHINICIAN = 'lab_tech', 'Lab_tech'
USER = 'user', 'User'
ADMIN = 'admin', 'Admin'
SUBJECT = 'subject', 'Subject'

class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
Expand Down Expand Up @@ -57,52 +63,9 @@ def delete(self, *args, **kwargs):
def __str__(self):
return self.name

class Subject(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=255)
email = models.EmailField(unique=True)
password = models.CharField(max_length=128)
date_archived = models.DateTimeField(null=True, blank=True)
date_deleted = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)

objects = CustomUserManager()

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name']

groups = models.ManyToManyField(
'auth.Group',
related_name='subject_set',
blank=True,
verbose_name=('groups'),
help_text=(
'The groups this subject belongs to. A subject will get all permissions '
'granted to each of their groups.'
),
)

user_permissions = models.ManyToManyField(
'auth.Permission',
related_name='subject_set',
blank=True,
verbose_name=('user permissions'),
help_text=('Specific permissions for this subject.'),
)

def set_password(self, raw_password):
self.password = make_password(raw_password)

def check_password(self, raw_password):
return check_password(raw_password, self.password)

def delete(self, *args, **kwargs):
self.date_deleted = timezone.now()
self.save()

def __str__(self):
return self.name
class Subject(User):
class Meta:
proxy = True

class Location(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
Expand Down
18 changes: 14 additions & 4 deletions api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from rest_framework import serializers
from .models import Subject, User, Location, Test, Disease, Result, Hotspot, InfectionRate

from .models import (
Subject,
User,
Location,
Test,
Disease,
Result,
Hotspot,
InfectionRate,
UserRole
)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'email', 'password', 'user_role']
fields = ['id', 'name', 'email', 'password']
extra_kwargs = {
'password': {'write_only': True},
}
Expand Down Expand Up @@ -35,7 +44,8 @@ def create(self, validated_data):
subject = Subject.objects.create_user(
email=validated_data['email'],
name=validated_data['name'],
password=validated_data['password']
password=validated_data['password'],
user_role=UserRole.SUBJECT
)
return subject

Expand Down
55 changes: 11 additions & 44 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
from django.db.models import F, Subquery, OuterRef, Count
from django.core.paginator import Paginator
from django.db.models.functions import TruncDate
from django.contrib.auth import authenticate
from django.core.exceptions import PermissionDenied

from rest_framework import viewsets, status, serializers
from rest_framework.response import Response
Expand All @@ -19,9 +17,7 @@
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

from api.auth.backends import SubjectAuthBackend

from .models import Subject, User, Location, Test, Disease, Result, Hotspot, InfectionRate
from .models import Subject, User, Location, Test, Disease, Result, Hotspot, InfectionRate, UserRole
from .serializers import (
SubjectSerializer,
UserSerializer,
Expand All @@ -36,45 +32,18 @@
class TheaUserTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
data = super().validate(attrs)

# Add custom claims...

data['user'] = UserSerializer(self.user).data
return data

class TheaUserTokenObtainPairView(TokenObtainPairView):
serializer_class = TheaUserTokenObtainPairSerializer

class SubjectRefreshToken(RefreshToken):
@classmethod
def for_user(cls, subject):
token = cls()
return token

class TheaSubjectTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, subject: Subject):
return SubjectRefreshToken.for_user(subject)

def validate(self, attrs):
authenticate_kwargs = {
self.username_field: attrs[self.username_field],
"password": attrs["password"],
}

subject_auth_backend = SubjectAuthBackend()
self.subject = subject_auth_backend.authenticate(**authenticate_kwargs)

if self.subject:
refresh = self.get_token(self.subject)
data = {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
data['subject'] = SubjectSerializer(self.subject).data

return data
else:
raise PermissionDenied("Invalid credentials")
def validate(self, attrs):
data = super().validate(attrs)
data['subject'] = SubjectSerializer(self.user).data
return data

class TheaSubjectTokenObtainPairView(TokenObtainPairView):
serializer_class = TheaSubjectTokenObtainPairSerializer
Expand Down Expand Up @@ -110,8 +79,6 @@ def post(self, request):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class LogoutView(APIView):
# i think this view could work for both users and subjects!

def post(self, request):
try:
refresh_token = request.data["refresh_token"]
Expand All @@ -124,7 +91,7 @@ def post(self, request):

class SubjectViewSet(viewsets.ModelViewSet):
serializer_class = SubjectSerializer
queryset = Subject.objects.filter(date_deleted__isnull=True)
queryset = Subject.objects.filter(date_deleted__isnull=True, user_role=UserRole.SUBJECT)

def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
Expand Down Expand Up @@ -202,11 +169,11 @@ class LocationViewSet(viewsets.ModelViewSet):

def list(self, request, *args, **kwargs):
# get locations that a particular user has been to
user = request.query_params.get('user')
if user:
queryset = Location.objects.filter(date_deleted__isnull=True, user_id=user)
subject = request.query_params.get('subject')
if subject:
queryset = Location.objects.filter(date_deleted__isnull=True, subject=subject)
else:
return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST)
return Response({'error': 'subject is required'}, status=status.HTTP_400_BAD_REQUEST)
serializer = LocationSerializer(queryset, many=True)
return Response(serializer.data)

Expand Down
2 changes: 1 addition & 1 deletion thea/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,6 @@
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

AUTHENTICATION_BACKENDS = [
'api.auth.backends.SubjectAuthBackend',
# 'api.auth.backends.SubjectAuthBackend', # disable this for now - may come in handy when we start enabling different auth/register methods for subject roles
"django.contrib.auth.backends.ModelBackend"
]
2 changes: 1 addition & 1 deletion thea/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

path('register/user/', UserRegisterView.as_view(), name='user_register'),
path('login/user/', TheaUserTokenObtainPairView.as_view(), name='user_token_obtain_pair'),
path('login/user/refresh/', TokenRefreshView.as_view(), name='user_token_refresh'), # client must pass the refresh token!
path('login/user/refresh/', TokenRefreshView.as_view(), name='user_token_refresh'),
path('logout/user/', LogoutView.as_view(), name='user_logout'),

path('register/subject/', SubjectRegisterView.as_view(), name='subject_register'),
Expand Down

0 comments on commit fde4c78

Please sign in to comment.