From 1f314f1c20890949b89190086e70612832ad2e37 Mon Sep 17 00:00:00 2001 From: "Edward Tirado Jr." Date: Tue, 8 Jul 2025 00:39:58 -0500 Subject: [PATCH] added support for user profiles --- movie_manager/models.py | 3 +- movienight/settings.py | 12 ++----- movienight/urls.py | 33 +++++++++---------- users/migrations/0001_initial.py | 25 +++++++++++++++ users/models.py | 9 ++++++ users/permissions.py | 6 ++++ users/serializers.py | 27 ++++++++++++++-- users/viewsets/user.py | 54 ++++++++++++++++++++++++++++++-- 8 files changed, 137 insertions(+), 32 deletions(-) create mode 100644 users/migrations/0001_initial.py create mode 100644 users/permissions.py diff --git a/movie_manager/models.py b/movie_manager/models.py index f401dde..fa39298 100644 --- a/movie_manager/models.py +++ b/movie_manager/models.py @@ -1,7 +1,6 @@ -from django.db import models from django.contrib.auth.models import User +from django.db import models from django.db.models import SET_NULL -import datetime class Movie(models.Model): diff --git a/movienight/settings.py b/movienight/settings.py index 74f4921..eb9878d 100644 --- a/movienight/settings.py +++ b/movienight/settings.py @@ -10,14 +10,12 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ -from pathlib import Path import os - +from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -30,13 +28,13 @@ DEBUG = bool(os.environ.get("DEBUG", default=0)) ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1").split(",") CORS_ALLOWED_ORIGINS = ["http://localhost:3000"] - # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", + "corsheaders", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", @@ -44,7 +42,7 @@ INSTALLED_APPS = [ "rest_framework.authtoken", "knox", "movie_manager", - "corsheaders", + "users", ] MIDDLEWARE = [ @@ -78,7 +76,6 @@ TEMPLATES = [ WSGI_APPLICATION = "movienight.wsgi.application" - # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases @@ -95,7 +92,6 @@ DATABASES = { } } - # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators @@ -114,7 +110,6 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] - # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ @@ -131,7 +126,6 @@ OMDB_API_KEY = os.environ.get("OMDB_API_KEY") # Django Rest Framework REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ - #'rest_framework.authentication.SessionAuthentication', "knox.auth.TokenAuthentication", ], } diff --git a/movienight/urls.py b/movienight/urls.py index c07c5b5..e36e1b5 100644 --- a/movienight/urls.py +++ b/movienight/urls.py @@ -2,14 +2,14 @@ URL configuration for movienight project. """ -import knox -from knox import views as knox_views +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include -from django.conf.urls.static import static -from django.conf import settings +from rest_framework.authtoken.views import obtain_auth_token from rest_framework.routers import DefaultRouter +from movie_db import views as movie_db_views from movie_manager.viewsets import ( MovieViewset, MovieListViewset, @@ -17,11 +17,8 @@ from movie_manager.viewsets import ( ShowingViewset, ) from users import views as user_views -from movie_db import views as movie_db_views -from rest_framework.authtoken.views import obtain_auth_token - -from users.viewsets.user import register from users.viewsets import UserViewSet, GroupViewSet +from users.viewsets.user import register, UserProfileViewSet router = DefaultRouter() router.register(r"v1/users", UserViewSet) @@ -30,13 +27,17 @@ router.register(r"v1/movies", MovieViewset) router.register(r"v1/lists", MovieListViewset) router.register(r"v1/schedules", ScheduleViewset) router.register(r"v1/showings", ShowingViewset) +router.register(r"v1/users/profiles/", UserProfileViewSet) urlpatterns = [ - path("", include(router.urls)), - path("admin/", admin.site.urls), - path(r"v1/auth/token/", obtain_auth_token), - path(r"v1/auth/login/", user_views.LoginView.as_view(), name="knox_login"), - path(r"v1/auth/register/", register, name="register"), - path(r"v1/movies/search", movie_db_views.omdb_search, name="omdb_search"), - path(r"v1/auth/", include("knox.urls")), -] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + path("", include(router.urls)), + path("admin/", admin.site.urls), + path(r"v1/auth/token/", obtain_auth_token), + path(r"v1/auth/login/", user_views.LoginView.as_view(), name="knox_login"), + path(r"v1/auth/register/", register, name="register"), + path(r"v1/movies/search", movie_db_views.omdb_search, name="omdb_search"), + path(r"v1/auth/", include("knox.urls")), + path('v1/users/profile', UserProfileViewSet.as_view({"get": "current_user_profile"}), + name="current_user_profile"), + path('v1/users/profiles//', UserProfileViewSet.as_view({"get": "retrieve"})) + ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..8b80e3f --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.2 on 2025-07-08 02:37 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=100, null=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/users/models.py b/users/models.py index 71a8362..416811d 100644 --- a/users/models.py +++ b/users/models.py @@ -1,3 +1,12 @@ +from django.contrib.auth.models import User from django.db import models + # Create your models here. +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + name = models.CharField(max_length=100, null=True, blank=True) + + @property + def lists(self): + return self.user.movielist_set.all() diff --git a/users/permissions.py b/users/permissions.py new file mode 100644 index 0000000..3dae833 --- /dev/null +++ b/users/permissions.py @@ -0,0 +1,6 @@ +from rest_framework import permissions +from rest_framework.permissions import SAFE_METHODS + +class ReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + return request.method in SAFE_METHODS diff --git a/users/serializers.py b/users/serializers.py index a269baa..33f9a3f 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,12 +1,35 @@ from django.contrib.auth import authenticate -from rest_framework import serializers from django.contrib.auth.models import User, Group +from rest_framework import serializers + +from movie_manager.serializers import MovieListSerializer +from users.models import UserProfile class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User - fields = ["url", "username", "email", "password", "groups"] + fields = ["url", "username", "email", "groups"] + + +class UserProfileSerializer(serializers.HyperlinkedModelSerializer): + name = serializers.SerializerMethodField() + username = serializers.SerializerMethodField() + date_joined = serializers.SerializerMethodField() + lists = MovieListSerializer(many=True, read_only=True) + + class Meta: + model = UserProfile + fields = ["name", "username", "date_joined", "lists"] + + def get_name(self, obj): + return obj.name or "" + + def get_username(self, obj): + return obj.user.username + + def get_date_joined(self, obj): + return obj.user.date_joined class GroupSerializer(serializers.HyperlinkedModelSerializer): diff --git a/users/viewsets/user.py b/users/viewsets/user.py index c2e2063..e7856d3 100644 --- a/users/viewsets/user.py +++ b/users/viewsets/user.py @@ -1,10 +1,13 @@ -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import User +from django.http import JsonResponse from knox.auth import TokenAuthentication from rest_framework import viewsets, permissions, status -from rest_framework.decorators import api_view +from rest_framework.decorators import api_view, action from rest_framework.response import Response -from users.serializers import UserSerializer, GroupSerializer +from users.models import UserProfile +from users.permissions import ReadOnly +from users.serializers import UserSerializer, UserProfileSerializer class UserViewSet(viewsets.ModelViewSet): @@ -15,6 +18,51 @@ class UserViewSet(viewsets.ModelViewSet): serializer_class = UserSerializer +class UserProfileViewSet(viewsets.ModelViewSet): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated | ReadOnly] + + queryset = UserProfile.objects.all() + serializer_class = UserProfileSerializer + lookup_field = "user__username" + + @action(detail=False) + def current_user_profile(self, request, *args, **kwargs): + try: + user = request.user + except User.DoesNotExist: + return Response([], status=status.HTTP_404_NOT_FOUND) + + try: + user_profile = UserProfile.objects.get(user=user) + except UserProfile.DoesNotExist: + user_profile = UserProfile( + user=user, + ) + + user_profile.save() + + return JsonResponse(UserProfileSerializer(user_profile).data) + + def retrieve(self, request, pk=None, *args, **kwargs): + try: + username = kwargs.get('user__username') + user = User.objects.get(username=username) + except User.DoesNotExist: + return Response([], status=status.HTTP_404_NOT_FOUND) + + try: + user_profile = UserProfile.objects.get(user=user) + except UserProfile.DoesNotExist: + user_profile = UserProfile( + user=user, + ) + + user_profile.save() + + return JsonResponse(UserProfileSerializer(user_profile).data) + + @api_view(["POST"]) def register(request): user_data = UserSerializer(data=request.data)