diff --git a/.env.example b/.env.example index 128f832..8a2d2bb 100644 --- a/.env.example +++ b/.env.example @@ -11,10 +11,5 @@ DATABASE_HOST=movienight-db DATABASE_NAME=movienight DATABASE_USERNAME=admin DATABASE_PASSWORD=super_secret_password - -# Django key generator: https://djecrety.ir/ SECRET_KEY=your_django_secret_key DJANGO_SECRET_KEY=your_django_secret_key - -# You can get a free key here: https://www.omdbapi.com/apikey.aspx -OMDB_API_KEY=your_omdb_api_key diff --git a/.gitignore b/.gitignore index 41818d1..6bd33c1 100644 --- a/.gitignore +++ b/.gitignore @@ -175,6 +175,3 @@ cython_debug/ # django static - -# JetBrains -.idea diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9f60641 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..60235a5 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/movie-night-py.iml b/.idea/movie-night-py.iml new file mode 100644 index 0000000..f13645b --- /dev/null +++ b/.idea/movie-night-py.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e6d5f42..e94a7e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,4 +35,4 @@ USER web EXPOSE 8000 -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] \ No newline at end of file +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "movienight.wsgi:application"] diff --git a/movie_db/db_providers/omdb.py b/movie_db/db_providers/omdb.py index 2041126..7db8f1e 100644 --- a/movie_db/db_providers/omdb.py +++ b/movie_db/db_providers/omdb.py @@ -1,7 +1,5 @@ import os -from django.conf import settings - from movie_db.movie_db import MovieDB import requests @@ -10,7 +8,8 @@ from movie_db.serializers import MovieSerializer, MovieResultSerializer class OMDb(MovieDB): def __init__(self): - self.api_key = settings.OMDB_API_KEY + api_key = os.getenv("OMDB_API_KEY") + self.api_key = f"{api_key}" self.base_url = "https://www.omdbapi.com/?apikey=" + self.api_key super().__init__() diff --git a/movie_db/tests.py b/movie_db/tests.py index 6cb44f7..7ce503c 100644 --- a/movie_db/tests.py +++ b/movie_db/tests.py @@ -1,6 +1,3 @@ -from unittest import TestCase +from django.test import TestCase - -class OmdbTestCase(TestCase): - def test_movie_db(self): - self.assertTrue(True) +# Create your tests here. diff --git a/movie_db/views.py b/movie_db/views.py index 536c6e5..602e418 100644 --- a/movie_db/views.py +++ b/movie_db/views.py @@ -9,17 +9,4 @@ def omdb_search(request): search_type = request.GET.get("type") omdb = OMDb() - - results = omdb.search(query, {"type": search_type}) - if "error" in results: - return parse_error(results) - - return JsonResponse(results, safe=False) - - -def parse_error(results): - error_json = results["error"] - if "Error" in error_json and error_json["Error"] == "Movie not found!": - return JsonResponse({}, status=404) - else: - return JsonResponse("Error while searching for movie.", status=500) \ No newline at end of file + return JsonResponse(omdb.search(query, {"type": search_type}), safe=False) diff --git a/movie_manager/admin.py b/movie_manager/admin.py index 7773cfb..b199e47 100644 --- a/movie_manager/admin.py +++ b/movie_manager/admin.py @@ -1,7 +1,4 @@ -from zoneinfo import ZoneInfo - from django.contrib import admin -from django.utils import timezone from movie_manager.models import Movie, MovieList, Schedule, Showing @@ -9,30 +6,19 @@ from movie_manager.models import Movie, MovieList, Schedule, Showing # Register your models here. @admin.register(Movie) class MovieAdmin(admin.ModelAdmin): - list_display = ["title", "imdb_id", "added_by"] + pass @admin.register(MovieList) class MovieListAdmin(admin.ModelAdmin): - list_display = ["name", "owner"] + pass @admin.register(Schedule) class ScheduleAdmin(admin.ModelAdmin): - list_display = ["name", "owner"] + pass @admin.register(Showing) class ShowingAdmin(admin.ModelAdmin): - list_display = ["local_showtime", "showtime", "movie"] - - def local_showtime(self, obj): - if obj.showtime: - target_tz = ZoneInfo("America/Chicago") - with timezone.override(target_tz): - local_time = timezone.localtime(obj.showtime) - return local_time.strftime("%Y-%m-%d %H:%M") - return "Invalid datetime" - - local_showtime.short_description = "Showtime (Local)" - local_showtime.admin_order_field = "showtime" + pass diff --git a/movie_manager/models.py b/movie_manager/models.py index ccbb950..f401dde 100644 --- a/movie_manager/models.py +++ b/movie_manager/models.py @@ -1,11 +1,12 @@ -from django.contrib.auth.models import User from django.db import models +from django.contrib.auth.models import User from django.db.models import SET_NULL +import datetime class Movie(models.Model): title = models.CharField(max_length=100) - imdb_id = models.CharField(max_length=100, db_index=True, unique=True) + imdb_id = models.CharField(max_length=100) year = models.IntegerField(null=True, blank=True) director = models.CharField(max_length=500, null=True, blank=True) actors = models.TextField(null=True, blank=True) @@ -27,8 +28,8 @@ class Movie(models.Model): class MovieList(models.Model): - name = models.CharField(max_length=100, db_index=True) - public = models.BooleanField(default=False, db_index=True) + name = models.CharField(max_length=100) + public = models.BooleanField(default=False) owner = models.ForeignKey(User, on_delete=models.CASCADE) movies = models.ManyToManyField(Movie) created_at = models.DateTimeField(auto_now_add=True) @@ -37,9 +38,6 @@ class MovieList(models.Model): class Meta: ordering = ["name"] - indexes = [ - models.Index(fields=["public", "owner"]), - ] def __str__(self): return self.name @@ -49,7 +47,7 @@ class Schedule(models.Model): name = models.CharField(max_length=100) owner = models.ForeignKey(User, on_delete=models.CASCADE) public = models.BooleanField(default=False) - slug = models.SlugField(max_length=100, default="", db_index=True) + slug = models.SlugField(max_length=100, default="") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) deleted_at = models.DateTimeField(null=True, blank=True) diff --git a/movie_manager/permissions.py b/movie_manager/permissions.py deleted file mode 100644 index 3dae833..0000000 --- a/movie_manager/permissions.py +++ /dev/null @@ -1,6 +0,0 @@ -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/movie_manager/serializers.py b/movie_manager/serializers.py index c9bb829..75d451c 100644 --- a/movie_manager/serializers.py +++ b/movie_manager/serializers.py @@ -1,56 +1,25 @@ +from django.utils import timezone from gunicorn.config import User from rest_framework import serializers - from movie_manager.models import Movie, MovieList, Schedule, Showing class MovieSerializer(serializers.ModelSerializer): - has_been_scheduled = serializers.SerializerMethodField() - class Meta: model = Movie - fields = [ - "id", - "title", - "imdb_id", - "year", - "director", - "actors", - "plot", - "genre", - "mpaa_rating", - "critic_scores", - "poster", - "added_by_id", - "has_been_scheduled", - ] - - def get_has_been_scheduled(self, obj): - return Showing.objects.filter(movie_id=obj.id).exists() - - -class MovieListListSerializer(serializers.ModelSerializer): - movie_count = serializers.SerializerMethodField() - - class Meta: - model = MovieList - fields = ["id", "name", "owner", "public", "movie_count"] - - def get_movie_count(self, obj): - return obj.movies.count() + fields = "__all__" class MovieListSerializer(serializers.ModelSerializer): + movie_count = serializers.SerializerMethodField() movies = MovieSerializer(read_only=True, many=True) - serializer_class = MovieSerializer - owner = serializers.PrimaryKeyRelatedField(read_only=True) - - def get_queryset(self): - return MovieList.objects.prefetch_related("movies", "movies__showing_set") class Meta: model = MovieList - fields = ["id", "name", "owner", "public", "movies"] + fields = ["id", "name", "owner", "public", "movies", "movie_count"] + + def get_movie_count(self, obj): + return len(obj.movies.all()) class UserSerializer(serializers.Serializer): @@ -66,18 +35,19 @@ class ShowingSerializer(serializers.ModelSerializer): model = Showing fields = ["id", "public", "showtime", "movie", "owner"] - # def to_internal_value(self, data): - # validated_data = super().to_internal_value(data) + def to_internal_value(self, data): + validated_data = super().to_internal_value(data) - # if "showtime" in validated_data and timezone.is_naive( - # validated_data["showtime"] - # ): - # validated_data["showtime"] = timezone.make_aware(validated_data["showtime"]) + if "showtime" in validated_data and timezone.is_naive( + validated_data["showtime"] + ): + validated_data["showtime"] = timezone.make_aware(validated_data["showtime"]) - # return validated_data + return validated_data class ScheduleSerializer(serializers.ModelSerializer): + name = serializers.CharField(read_only=True) showings = ShowingSerializer(source="showing_set", read_only=True, many=True) class Meta: diff --git a/movie_manager/tests.py b/movie_manager/tests.py index 09fcbb8..7ce503c 100644 --- a/movie_manager/tests.py +++ b/movie_manager/tests.py @@ -1,90 +1,3 @@ -import json +from django.test import TestCase -from django.contrib.auth.models import User -from django.utils import timezone -from freezegun import freeze_time -from rest_framework import status -from rest_framework.test import APITestCase, APIClient - -from movie_manager.models import Movie, Schedule, Showing - - -class ShowingViewsetTestCase(APITestCase): - def setUp(self): - self.client: APIClient = APIClient() - self.movie: Movie = Movie.objects.create(title="Test Movie") - self.owner: User = User.objects.create(id=1, username="test_user") - self.schedule: Schedule = Schedule.objects.create( - owner=self.owner, name="Test Schedule" - ) - - def test_it_creates_a_new_showing(self): - self.client.force_authenticate(user=self.owner) - - showing_time = timezone.now().isoformat().replace("+00:00", "Z") - response = self.client.post( - "/v1/showings/", - { - "movie": self.movie.id, - "public": True, - "schedule": self.schedule.id, - "showtime": showing_time, - }, - ) - - response_data = json.loads(response.content) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response_data.get("showtime"), showing_time) - self.assertEqual(response_data.get("movie").get("title"), "Test Movie") - - @freeze_time("2025-07-03 04:59:00") # 2025-07-02 23:59 CDT in UTC - def test_it_returns_active_showings(self): - self.client.force_authenticate(user=self.schedule.owner) - - showtimes_america_chicago_utc = [ - "2025-07-08T04:59:59.000Z", # 2025-07-07 11:59pm - "2025-07-03T04:59:59.000Z", # 2025-07-02 11:59pm - "2025-07-03T04:00:00.000Z", # 2025-07-02 11:00pm - "2025-07-01T04:59:00.000Z", # 2025-06-30 11:59pm - ] - - for showtime in showtimes_america_chicago_utc: - Showing.objects.create( - movie=self.movie, - schedule=self.schedule, - showtime=showtime, - public=True, - owner=self.schedule.owner, - ) - - response = self.client.get(f"/v1/schedules/{self.schedule.id}/") - parsed_schedule = json.loads(response.content) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(parsed_schedule.get("showings")), 2) - - -class ScheduleViewsetTestCase(APITestCase): - def setUp(self): - self.client: APIClient = APIClient() - self.test_user: User = User.objects.create(id=1, username="test_user") - - def test_it_creates_a_new_schedule(self): - self.client.force_authenticate(user=self.test_user) - response = self.client.post( - "/v1/schedules/", - { - "name": "Test Schedule", - "owner": self.test_user.id, - "public": True, - "slug": "test-schedule", - }, - ) - - response_data = json.loads(response.content) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response_data.get("name"), "Test Schedule") - self.assertEqual(response_data.get("owner"), 1) - self.assertEqual(response_data.get("public"), True) - self.assertEqual(response_data.get("slug"), "test-schedule") +# Create your tests here. diff --git a/movie_manager/views.py b/movie_manager/views.py index e69de29..e7b961b 100644 --- a/movie_manager/views.py +++ b/movie_manager/views.py @@ -0,0 +1,183 @@ +import datetime +import json + +from django.http import JsonResponse +from django.contrib.auth.models import User +from django.utils.dateparse import parse_datetime +from rest_framework import permissions, viewsets +from knox.auth import TokenAuthentication +from rest_framework.decorators import action, api_view +from rest_framework.exceptions import NotFound +from rest_framework.permissions import AllowAny, SAFE_METHODS + +from movie_db.db_providers.omdb import OMDb +from movie_manager.models import Movie, MovieList, Schedule, Showing +from movie_manager.serializers import ( + MovieListSerializer, + MovieSerializer, + ScheduleSerializer, + ShowingSerializer, +) + + +class ReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + return request.method in SAFE_METHODS + + +# Create your views here. +class MovieViewset(viewsets.ModelViewSet): + queryset = Movie.objects.all().order_by("title") + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + serializer_class = MovieSerializer + + +class MovieListViewset(viewsets.ModelViewSet): + queryset = MovieList.objects.all().order_by("name") + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated | ReadOnly] + + serializer_class = MovieListSerializer + + def create(self, request, *args, **kwargs): + movie_list = MovieList.objects.create( + name=request.data.get("name"), + owner=request.user, + ) + + return JsonResponse(MovieListSerializer(movie_list).data) + + def update(self, request, pk=None, *args, **kwargs): + movie_list = MovieList.objects.get(pk=pk) + movie_list.name = request.data.get("name") + movie_list.owner = User.objects.get(pk=request.data.get("owner")) + + if request.data.get("movies"): + movie_ids = request.data.get("movies") + for movie_id in movie_ids: + try: + movie = Movie.objects.get(pk=movie_id) + movie_list.movies.add(movie) + except Movie.DoesNotExist: + raise NotFound(f"Movie {movie_id} does not exist") + + removed_movies = Movie.objects.exclude(id__in=movie_ids) + for removed_movie in removed_movies: + removed_movie.delete() + + movie_list.save() + + return JsonResponse(MovieListSerializer(movie_list).data) + + @action( + detail=True, methods=["put", "delete"], url_path="movie/(?Ptt[0-9]+)" + ) + def add_movie(self, request, pk=None, imdb_id=None, *args, **kwargs): + if request.method == "DELETE": + return self.remove_movie(request, pk, imdb_id) + + movie_list = MovieList.objects.get(pk=pk) + try: + new_movie = Movie.objects.get(imdb_id=imdb_id) + except Movie.DoesNotExist: + omdb = OMDb() + movie = omdb.search(imdb_id, {"type": "imdb_id"}) + + new_movie = Movie.objects.create( + title=movie["title"], + actors=movie["actors"], + year=movie["year"], + imdb_id=movie["imdb_id"], + poster=movie["poster"], + plot=movie["plot"], + genre=movie["genre"], + critic_scores=movie["critic_scores"], + mpaa_rating=movie["mpaa_rating"], + director=movie["director"], + added_by_id=request.user.id, + ) + + movie_list.movies.add(new_movie) + + return JsonResponse(MovieListSerializer(movie_list).data) + + def remove_movie(self, request, pk=None, imdb_id=None, *args, **kwargs): + movie = Movie.objects.filter(imdb_id=imdb_id).first() + + movie_list = MovieList.objects.get(pk=pk) + movie_list.movies.remove(movie) + + return JsonResponse(MovieListSerializer(movie_list).data) + + +class ScheduleViewset(viewsets.ModelViewSet): + queryset = Schedule.objects.all().order_by("name") + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated | ReadOnly] + + serializer_class = ScheduleSerializer + + def retrieve(self, request, pk=None, *args, **kwargs): + # Get the schedule instance + instance = self.get_object() + today = datetime.datetime.now() + + upcoming_showings = Showing.objects.filter( + showtime__gte=today, schedule=instance + ) + + # Create a serialized response + serializer = self.get_serializer(instance) + data = serializer.data + + # Replace all showings with only future showings + data["showings"] = ShowingSerializer(upcoming_showings, many=True).data + + if request.GET.get("past_showings") == "true": + past_showings = Showing.objects.filter( + showtime__lt=today, schedule=instance + ) + + # Add both to the response + data["past_showings"] = [ + { + "id": showing.id, + "showtime": showing.showtime.isoformat(), + "movie": MovieSerializer(showing.movie).data, + } + for showing in past_showings + ] + else: + data["past_showings"] = [] + + return JsonResponse(data) + + +class ShowingViewset(viewsets.ModelViewSet): + queryset = Showing.objects.all().order_by("showtime") + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated | ReadOnly] + + serializer_class = ShowingSerializer + + def create(self, request, *args, **kwargs): + movie_id = request.data.get("movie") + movie = Movie.objects.get(pk=movie_id) + + schedule_id = request.data.get("schedule") + schedule = Schedule.objects.get(pk=schedule_id) + + showtime_str = request.data.get("showtime") + showtime = parse_datetime(showtime_str) + + showing = Showing.objects.create( + movie=movie, + schedule=schedule, + showtime=showtime, + public=request.data.get("public"), + owner=request.user, + ) + + return JsonResponse(ShowingSerializer(showing).data) diff --git a/movie_manager/viewsets/__init__.py b/movie_manager/viewsets/__init__.py deleted file mode 100644 index da282d0..0000000 --- a/movie_manager/viewsets/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .movie import MovieViewset -from .movie_list import MovieListViewset -from .schedule import ScheduleViewset -from .showing import ShowingViewset - -__all__ = [ - "MovieViewset", - "MovieListViewset", - "ScheduleViewset", - "ShowingViewset", -] diff --git a/movie_manager/viewsets/movie.py b/movie_manager/viewsets/movie.py deleted file mode 100644 index c7b6977..0000000 --- a/movie_manager/viewsets/movie.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.http import JsonResponse -from rest_framework import permissions, viewsets - -from movie_db.db_providers.omdb import OMDb -from movie_manager.models import Movie - -from knox.auth import TokenAuthentication - -from movie_manager.serializers import MovieSerializer - - -class MovieViewset(viewsets.ModelViewSet): - queryset = Movie.objects.all().order_by("title") - authentication_classes = [TokenAuthentication] - permission_classes = [permissions.IsAuthenticated] - - serializer_class = MovieSerializer - - def update(self, request, pk=None, *args, **kwargs): - omdb = OMDb() - updated_movie = omdb.search(request.data.get("imdb_id"), {"type": "imdb_id"}) - - movie = Movie.objects.get(pk=pk) - - movie.title = updated_movie["title"] - movie.actors = updated_movie["actors"] - movie.year = updated_movie["year"] - movie.critic_scores = updated_movie["critic_scores"] - movie.mpaa_rating = updated_movie["mpaa_rating"] - movie.director = updated_movie["director"] - movie.poster = updated_movie["poster"] - movie.plot = updated_movie["plot"] - movie.genre = updated_movie["genre"] - - movie.save() - - return JsonResponse(MovieSerializer(movie).data) diff --git a/movie_manager/viewsets/movie_list.py b/movie_manager/viewsets/movie_list.py deleted file mode 100644 index 75730b6..0000000 --- a/movie_manager/viewsets/movie_list.py +++ /dev/null @@ -1,120 +0,0 @@ -from django.http import JsonResponse -from django.db import models -from django.contrib.auth.models import User -from rest_framework import permissions, viewsets -from rest_framework.decorators import action -from rest_framework.exceptions import NotFound - -from movie_db.db_providers.omdb import OMDb -from movie_manager.models import MovieList, Movie - -from knox.auth import TokenAuthentication - -from movie_manager.permissions import ReadOnly -from movie_manager.serializers import MovieListSerializer, MovieListListSerializer - - -class MovieListViewset(viewsets.ModelViewSet): - queryset = MovieList.objects.all() - authentication_classes = [TokenAuthentication] - permission_classes = [permissions.IsAuthenticated | ReadOnly] - - def get_serializer_class(self): - if self.action == "list": - return MovieListListSerializer - else: - return MovieListSerializer - - def get_queryset(self): - base_qs = MovieList.objects.all() - - if self.action == "list": - if self.request.user.is_authenticated: - return base_qs.filter( - models.Q(public=True) | models.Q(owner=self.request.user) - ).order_by("name") - - return base_qs.filter(public=True).order_by("name") - else: - return MovieList.objects.prefetch_related( - "movies", "movies__showing_set" - ).order_by("name") - - def perform_create(self, serializer): - serializer.save(owner=self.request.user) - - def get_permissions(self): - if self.action in ["update", "partial_update", "destroy"]: - self.permission_classes = [permissions.IsAuthenticated] - return super().get_permissions() - - def create(self, request, *args, **kwargs): - movie_list = MovieList.objects.create( - name=request.data.get("name"), - owner=request.user, - ) - - return JsonResponse(MovieListSerializer(movie_list).data) - - def update(self, request, pk=None, *args, **kwargs): - movie_list = MovieList.objects.get(pk=pk) - movie_list.name = request.data.get("name") - movie_list.owner = User.objects.get(pk=request.data.get("owner")) - - if request.data.get("movies"): - movie_ids = request.data.get("movies") - for movie_id in movie_ids: - try: - movie = Movie.objects.get(pk=movie_id) - movie_list.movies.add(movie) - except Movie.DoesNotExist: - raise NotFound(f"Movie {movie_id} does not exist") - - removed_movies = Movie.objects.exclude(id__in=movie_ids) - for removed_movie in removed_movies: - removed_movie.delete() - - movie_list.save() - - return JsonResponse(MovieListSerializer(movie_list).data) - - @action( - detail=True, methods=["put", "delete"], url_path="movie/(?Ptt[0-9]+)" - ) - def add_movie(self, request, pk=None, imdb_id=None, *args, **kwargs): - if request.method == "DELETE": - return self.remove_movie(request, pk, imdb_id) - - movie_list = MovieList.objects.get(pk=pk) - try: - new_movie = Movie.objects.get(imdb_id=imdb_id) - except Movie.DoesNotExist: - omdb = OMDb() - movie = omdb.search(imdb_id, {"type": "imdb_id"}) - - new_movie = Movie.objects.create( - title=movie["title"], - actors=movie["actors"], - year=movie["year"], - imdb_id=movie["imdb_id"], - poster=movie["poster"], - plot=movie["plot"], - genre=movie["genre"], - critic_scores=movie["critic_scores"], - mpaa_rating=movie["mpaa_rating"], - director=movie["director"], - added_by_id=request.user.id, - ) - - movie_list.movies.add(new_movie) - - return JsonResponse(MovieListSerializer(movie_list).data) - - @staticmethod - def remove_movie(request, pk=None, imdb_id=None, *args, **kwargs): - movie = Movie.objects.filter(imdb_id=imdb_id).first() - - movie_list = MovieList.objects.get(pk=pk) - movie_list.movies.remove(movie) - - return JsonResponse(MovieListSerializer(movie_list).data) diff --git a/movie_manager/viewsets/schedule.py b/movie_manager/viewsets/schedule.py deleted file mode 100644 index 41d7364..0000000 --- a/movie_manager/viewsets/schedule.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.http import JsonResponse -from django.utils import timezone -from knox.auth import TokenAuthentication -from rest_framework import viewsets, permissions - -from movie_manager.models import Schedule, Showing -from movie_manager.permissions import ReadOnly -from movie_manager.serializers import ( - ScheduleSerializer, - ShowingSerializer, - MovieSerializer, -) - - -class ScheduleViewset(viewsets.ModelViewSet): - queryset = Schedule.objects.all().order_by("name") - authentication_classes = [TokenAuthentication] - permission_classes = [permissions.IsAuthenticated | ReadOnly] - - serializer_class = ScheduleSerializer - - def retrieve(self, request, pk=None, *args, **kwargs): - # Get the schedule instance - instance = self.get_object() - now = timezone.now() - - upcoming_showings = Showing.objects.filter(showtime__gte=now, schedule=instance) - - serializer = self.get_serializer(instance) - data = serializer.data - - # Replace all showings with only future showings - data["showings"] = ShowingSerializer(upcoming_showings, many=True).data - - if request.GET.get("past_showings") == "true": - past_showings = Showing.objects.filter(showtime__lt=now, schedule=instance) - - # Add both to the response - data["past_showings"] = [ - { - "id": past_showing.id, - "showtime": past_showing.showtime.isoformat(), - "movie": MovieSerializer(past_showing.movie).data, - } - for past_showing in past_showings - ] - else: - data["past_showings"] = [] - - return JsonResponse(data) diff --git a/movie_manager/viewsets/showing.py b/movie_manager/viewsets/showing.py deleted file mode 100644 index a52f7c1..0000000 --- a/movie_manager/viewsets/showing.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.http import JsonResponse -from django.utils.dateparse import parse_datetime -from knox.auth import TokenAuthentication -from rest_framework import viewsets, permissions - -from movie_manager.models import Showing, Movie, Schedule -from movie_manager.permissions import ReadOnly -from movie_manager.serializers import ShowingSerializer - - -class ShowingViewset(viewsets.ModelViewSet): - queryset = Showing.objects.all().order_by("showtime") - authentication_classes = [TokenAuthentication] - permission_classes = [permissions.IsAuthenticated | ReadOnly] - - serializer_class = ShowingSerializer - - def create(self, request, *args, **kwargs): - movie_id = request.data.get("movie") - movie = Movie.objects.get(pk=movie_id) - - schedule_id = request.data.get("schedule") - schedule = Schedule.objects.get(pk=schedule_id) - - showtime_str = request.data.get("showtime") - showtime = parse_datetime(showtime_str) - - showing = Showing.objects.create( - movie=movie, - schedule=schedule, - showtime=showtime, - public=request.data.get("public"), - owner=request.user, - ) - - return JsonResponse(ShowingSerializer(showing).data, status=201) diff --git a/movienight/settings.py b/movienight/settings.py index dc59df7..4fcd523 100644 --- a/movienight/settings.py +++ b/movienight/settings.py @@ -10,12 +10,14 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ -import os from pathlib import Path +import os + # 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/ @@ -26,7 +28,8 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") DEBUG = bool(os.environ.get("DEBUG", default=0)) ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1").split(",") -CORS_ALLOWED_ORIGINS = os.environ.get("DJANGO_ALLOWED_ORIGINS", "http://localhost:3000").split(",") +CORS_ALLOWED_ORIGINS = ["http://localhost:3000"] + # Application definition @@ -34,7 +37,6 @@ INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", - "corsheaders", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", @@ -42,7 +44,7 @@ INSTALLED_APPS = [ "rest_framework.authtoken", "knox", "movie_manager", - "users", + "corsheaders", ] MIDDLEWARE = [ @@ -76,6 +78,7 @@ TEMPLATES = [ WSGI_APPLICATION = "movienight.wsgi.application" + # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases @@ -92,6 +95,7 @@ DATABASES = { } } + # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators @@ -110,6 +114,7 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] + # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ @@ -121,11 +126,10 @@ USE_I18N = True USE_TZ = True -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 e36e1b5..22a5c6a 100644 --- a/movienight/urls.py +++ b/movienight/urls.py @@ -2,42 +2,33 @@ URL configuration for movienight project. """ -from django.conf import settings -from django.conf.urls.static import static +import knox +from knox import views as knox_views from django.contrib import admin from django.urls import path, include -from rest_framework.authtoken.views import obtain_auth_token +from django.conf.urls.static import static +from django.conf import settings from rest_framework.routers import DefaultRouter -from movie_db import views as movie_db_views -from movie_manager.viewsets import ( - MovieViewset, - MovieListViewset, - ScheduleViewset, - ShowingViewset, -) from users import views as user_views -from users.viewsets import UserViewSet, GroupViewSet -from users.viewsets.user import register, UserProfileViewSet +from movie_manager import views as movie_views +from movie_db import views as movie_db_views +from rest_framework.authtoken.views import obtain_auth_token router = DefaultRouter() -router.register(r"v1/users", UserViewSet) -router.register(r"v1/groups", GroupViewSet) -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) +router.register(r"v1/users", user_views.UserViewSet) +router.register(r"v1/groups", user_views.GroupViewSet) +router.register(r"v1/movies", movie_views.MovieViewset) +router.register(r"v1/lists", movie_views.MovieListViewset) +router.register(r"v1/schedules", movie_views.ScheduleViewset) +router.register(r"v1/showings", movie_views.ShowingViewset) 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")), - 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) + 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/", user_views.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) diff --git a/requirements.txt b/requirements.txt index 73606df..456b8f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -django==5.2.2 +django==5.1.4 djangorestframework django-rest-knox markdown @@ -6,6 +6,4 @@ django-filter gunicorn==23.0.0 psycopg2-binary==2.9.10 django-cors-headers==4.7.0 -requests==2.32.3 -freezegun==1.5.2 -coverage==7.9.1 +requests==2.32.3 \ No newline at end of file diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py deleted file mode 100644 index 8b80e3f..0000000 --- a/users/migrations/0001_initial.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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 416811d..71a8362 100644 --- a/users/models.py +++ b/users/models.py @@ -1,12 +1,3 @@ -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 deleted file mode 100644 index 3dae833..0000000 --- a/users/permissions.py +++ /dev/null @@ -1,6 +0,0 @@ -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 33f9a3f..a269baa 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,35 +1,12 @@ from django.contrib.auth import authenticate -from django.contrib.auth.models import User, Group from rest_framework import serializers - -from movie_manager.serializers import MovieListSerializer -from users.models import UserProfile +from django.contrib.auth.models import User, Group class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User - 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 + fields = ["url", "username", "email", "password", "groups"] class GroupSerializer(serializers.HyperlinkedModelSerializer): diff --git a/users/views.py b/users/views.py index f231eee..bcc7f13 100644 --- a/users/views.py +++ b/users/views.py @@ -1,9 +1,40 @@ from django.contrib.auth import login -from rest_framework import permissions +from django.contrib.auth.models import Group, User, AnonymousUser +from rest_framework import permissions, viewsets, status +from knox.auth import TokenAuthentication from knox.views import LoginView as KnoxLoginView from rest_framework.authtoken.serializers import AuthTokenSerializer +from rest_framework.response import Response +from rest_framework.decorators import api_view -from users.serializers import UserSerializer +from users.serializers import GroupSerializer, UserSerializer + + +class UserViewSet(viewsets.ModelViewSet): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + queryset = User.objects.all().order_by("-date_joined") + serializer_class = UserSerializer + + +class GroupViewSet(viewsets.ModelViewSet): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + queryset = Group.objects.all().order_by("name") + serializer_class = GroupSerializer + + +@api_view(["POST"]) +def register(request): + user_data = UserSerializer(data=request.data) + + if user_data.is_valid(): + User.objects.create_user(**user_data.validated_data) + return Response(request.data, status=status.HTTP_201_CREATED) + else: + return Response([], status=status.HTTP_400_BAD_REQUEST) class LoginView(KnoxLoginView): diff --git a/users/viewsets/__init__.py b/users/viewsets/__init__.py deleted file mode 100644 index a3d0f22..0000000 --- a/users/viewsets/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .group import GroupViewSet -from .user import UserViewSet - -__all__ = ["GroupViewSet", "UserViewSet"] diff --git a/users/viewsets/group.py b/users/viewsets/group.py deleted file mode 100644 index 7bbc91d..0000000 --- a/users/viewsets/group.py +++ /dev/null @@ -1,13 +0,0 @@ -from knox.auth import TokenAuthentication -from django.contrib.auth.models import Group -from rest_framework import viewsets, permissions, status - -from users.serializers import GroupSerializer - - -class GroupViewSet(viewsets.ModelViewSet): - authentication_classes = [TokenAuthentication] - permission_classes = [permissions.IsAuthenticated] - - queryset = Group.objects.all().order_by("name") - serializer_class = GroupSerializer diff --git a/users/viewsets/user.py b/users/viewsets/user.py deleted file mode 100644 index 881f612..0000000 --- a/users/viewsets/user.py +++ /dev/null @@ -1,74 +0,0 @@ -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, action -from rest_framework.response import Response - -from users.models import UserProfile -from users.permissions import ReadOnly -from users.serializers import UserSerializer, UserProfileSerializer - - -class UserViewSet(viewsets.ModelViewSet): - authentication_classes = [TokenAuthentication] - permission_classes = [permissions.IsAuthenticated] - - queryset = User.objects.all().order_by("-date_joined") - 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" - - 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) - - @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) - - -@api_view(["POST"]) -def register(request): - user_data = UserSerializer(data=request.data) - - if user_data.is_valid(): - User.objects.create_user(**user_data.validated_data) - return Response(request.data, status=status.HTTP_201_CREATED) - else: - return Response([], status=status.HTTP_400_BAD_REQUEST)