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..2ae60fe 100644 --- a/.gitignore +++ b/.gitignore @@ -173,8 +173,5 @@ cython_debug/ # PyPI configuration file .pypirc -# django +# 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/docker-compose.yml b/docker-compose.yml index 20f5b0e..34ef508 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,18 +5,15 @@ services: environment: POSTGRES_DB: ${DATABASE_NAME} POSTGRES_USER: ${DATABASE_USERNAME} - PGUSER: ${DATABASE_USERNAME} POSTGRES_PASSWORD: ${DATABASE_PASSWORD} env_file: - .env ports: - "5432:5432" - user: postgres volumes: - movienight_data:/var/lib/postgresql/data - - ./init-scripts:/docker-entrypoint-initdb.d healthcheck: - test: ["CMD-SHELL", "pg_isready -U $DATABASE_USERNAME"] + test: ["CMD-SHELL", "pg_isready", "-d", "$DATABASE_NAME"] interval: 30s timeout: 60s retries: 5 @@ -43,7 +40,6 @@ services: DATABASE_PASSWORD: ${DATABASE_PASSWORD} DATABASE_HOST: ${DATABASE_HOST} DATABASE_PORT: ${DATABASE_PORT} - OMDB_API_KEY: ${OMDB_API_KEY} env_file: - .env volumes: diff --git a/init-scripts/01-create-db.sh b/init-scripts/01-create-db.sh deleted file mode 100644 index 13d3573..0000000 --- a/init-scripts/01-create-db.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -e - -# Create a postgres database for the user set in the .env file. -# Everything works fine without this, but this prevents a FATAL -# error from spamming the logs -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL - CREATE DATABASE "$POSTGRES_USER"; - GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_USER" TO "$POSTGRES_USER"; -EOSQL \ No newline at end of file diff --git a/movie_db/__init__.py b/movie_db/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/movie_db/admin.py b/movie_db/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/movie_db/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/movie_db/apps.py b/movie_db/apps.py deleted file mode 100644 index d9aa88e..0000000 --- a/movie_db/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class MoviedbConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'movie_db' diff --git a/movie_db/db_providers/omdb.py b/movie_db/db_providers/omdb.py deleted file mode 100644 index 2041126..0000000 --- a/movie_db/db_providers/omdb.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - -from django.conf import settings - -from movie_db.movie_db import MovieDB -import requests - -from movie_db.serializers import MovieSerializer, MovieResultSerializer - - -class OMDb(MovieDB): - def __init__(self): - self.api_key = settings.OMDB_API_KEY - self.base_url = "https://www.omdbapi.com/?apikey=" + self.api_key - super().__init__() - - def search(self, query, options=None): - if options["type"] == "imdb_id": - return self.search_by_imdb_id(query) - elif options["type"] == "title": - return self.search_by_title(query) - else: - return self.search_by_term(query) - - def search_by_title(self, title): - response = requests.get(self.base_url + "&t=" + title).json() - return MovieSerializer(response).data - - def search_by_imdb_id(self, imdb_id): - response = requests.get(self.base_url + "&i=" + imdb_id).json() - return MovieSerializer(response).data - - def search_by_term(self, term): - response = requests.get(self.base_url + "&s=" + term).json() - try: - return MovieResultSerializer(response["Search"], many=True).data - except KeyError: - return {"error": response} diff --git a/movie_db/migrations/__init__.py b/movie_db/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/movie_db/models.py b/movie_db/models.py deleted file mode 100644 index 71a8362..0000000 --- a/movie_db/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/movie_db/movie_db.py b/movie_db/movie_db.py deleted file mode 100644 index d65a019..0000000 --- a/movie_db/movie_db.py +++ /dev/null @@ -1,8 +0,0 @@ -from abc import ABC - -class MovieDB(ABC): - def __init__(self): - pass - - def search(self, query, options=None): - pass \ No newline at end of file diff --git a/movie_db/serializers.py b/movie_db/serializers.py deleted file mode 100644 index 077d771..0000000 --- a/movie_db/serializers.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework import serializers - - -class MovieSerializer(serializers.Serializer): - actors = serializers.CharField(source="Actors") - director = serializers.CharField(source="Director") - genre = serializers.CharField(source="Genre") - imdb_id = serializers.CharField(source="imdbID") - critic_scores = serializers.CharField(source="Ratings") - mpaa_rating = serializers.CharField(source="Rated") - media_type = serializers.CharField(source="Type") - plot = serializers.CharField(source="Plot") - poster = serializers.CharField(source="Poster") - runtime = serializers.CharField(source="Runtime") - title = serializers.CharField(source="Title") - year = serializers.CharField(source="Year") - - -class MovieResultSerializer(serializers.Serializer): - title = serializers.CharField(source="Title") - year = serializers.CharField(source="Year") - imdb_id = serializers.CharField(source="imdbID") - media_type = serializers.CharField(source="Type") - poster = serializers.CharField(source="Poster") diff --git a/movie_db/tests.py b/movie_db/tests.py deleted file mode 100644 index 6cb44f7..0000000 --- a/movie_db/tests.py +++ /dev/null @@ -1,6 +0,0 @@ -from unittest import TestCase - - -class OmdbTestCase(TestCase): - def test_movie_db(self): - self.assertTrue(True) diff --git a/movie_db/views.py b/movie_db/views.py deleted file mode 100644 index 536c6e5..0000000 --- a/movie_db/views.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.http import JsonResponse -from movie_db.db_providers.omdb import OMDb - - -def omdb_search(request): - query = request.GET.get("q") - if not query: - return JsonResponse({"Error": "Missing query"}, status=400) - - 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 diff --git a/movie_manager/admin.py b/movie_manager/admin.py index 7773cfb..8c38f3f 100644 --- a/movie_manager/admin.py +++ b/movie_manager/admin.py @@ -1,38 +1,3 @@ -from zoneinfo import ZoneInfo - from django.contrib import admin -from django.utils import timezone - -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"] - - -@admin.register(MovieList) -class MovieListAdmin(admin.ModelAdmin): - list_display = ["name", "owner"] - - -@admin.register(Schedule) -class ScheduleAdmin(admin.ModelAdmin): - list_display = ["name", "owner"] - - -@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" diff --git a/movie_manager/migrations/0001_initial.py b/movie_manager/migrations/0001_initial.py index 19195fa..8b7d822 100644 --- a/movie_manager/migrations/0001_initial.py +++ b/movie_manager/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2025-04-21 00:50 +# Generated by Django 5.1.4 on 2025-03-31 04:04 import django.db.models.deletion from django.conf import settings @@ -20,22 +20,18 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=100)), ('imdb_id', models.CharField(max_length=100)), - ('year', models.IntegerField(blank=True, null=True)), - ('director', models.CharField(blank=True, max_length=500, null=True)), - ('actors', models.TextField(blank=True, null=True)), - ('plot', models.TextField(blank=True, null=True)), - ('genre', models.CharField(blank=True, max_length=100, null=True)), - ('mpaa_rating', models.CharField(blank=True, max_length=20, null=True)), - ('critic_scores', models.TextField(blank=True, null=True)), - ('poster', models.TextField(blank=True, null=True)), + ('year', models.IntegerField()), + ('critic_score', models.CharField(max_length=500)), + ('genre', models.CharField(max_length=100)), + ('director', models.CharField(max_length=500)), + ('actors', models.CharField(max_length=500)), + ('plot', models.CharField(max_length=500)), + ('poster', models.CharField(max_length=500)), + ('last_watched', models.DateTimeField()), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('added_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), ], - options={ - 'ordering': ['title'], - }, ), migrations.CreateModel( name='MovieList', @@ -46,40 +42,7 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('movies', models.ManyToManyField(to='movie_manager.movie')), ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], - options={ - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='Schedule', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('public', models.BooleanField(default=False)), - ('slug', models.SlugField(default='', max_length=100)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Showing', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('public', models.BooleanField(default=False)), - ('showtime', models.DateTimeField()), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('movie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='movie_manager.movie')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['showtime'], - }, ), ] diff --git a/movie_manager/migrations/0002_alter_movie_options_alter_movielist_options_and_more.py b/movie_manager/migrations/0002_alter_movie_options_alter_movielist_options_and_more.py new file mode 100644 index 0000000..b0f1663 --- /dev/null +++ b/movie_manager/migrations/0002_alter_movie_options_alter_movielist_options_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.4 on 2025-04-07 05:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('movie_manager', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='movie', + options={'ordering': ['title']}, + ), + migrations.AlterModelOptions( + name='movielist', + options={'ordering': ['name']}, + ), + migrations.AddField( + model_name='movielist', + name='movies', + field=models.ManyToManyField(to='movie_manager.movie'), + ), + ] diff --git a/movie_manager/migrations/0002_showing_schedule.py b/movie_manager/migrations/0002_showing_schedule.py deleted file mode 100644 index b081684..0000000 --- a/movie_manager/migrations/0002_showing_schedule.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.4 on 2025-04-21 01:01 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('movie_manager', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='showing', - name='schedule', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='movie_manager.schedule'), - preserve_default=False, - ), - ] diff --git a/movie_manager/migrations/0003_alter_schedule_options.py b/movie_manager/migrations/0003_alter_schedule_options.py deleted file mode 100644 index 08ee78e..0000000 --- a/movie_manager/migrations/0003_alter_schedule_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.1.4 on 2025-04-21 03:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('movie_manager', '0002_showing_schedule'), - ] - - operations = [ - migrations.AlterModelOptions( - name='schedule', - options={'ordering': ['name']}, - ), - ] diff --git a/movie_manager/migrations/0003_movie_added_by.py b/movie_manager/migrations/0003_movie_added_by.py new file mode 100644 index 0000000..94fee8e --- /dev/null +++ b/movie_manager/migrations/0003_movie_added_by.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.4 on 2025-04-08 00:18 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('movie_manager', '0002_alter_movie_options_alter_movielist_options_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='movie', + name='added_by', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/movie_manager/migrations/0004_alter_movie_last_watched_showing_schedule.py b/movie_manager/migrations/0004_alter_movie_last_watched_showing_schedule.py new file mode 100644 index 0000000..9895ec9 --- /dev/null +++ b/movie_manager/migrations/0004_alter_movie_last_watched_showing_schedule.py @@ -0,0 +1,50 @@ +# Generated by Django 5.1.4 on 2025-04-08 03:39 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('movie_manager', '0003_movie_added_by'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='movie', + name='last_watched', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.CreateModel( + name='Showing', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('public', models.BooleanField(default=False)), + ('showtime', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('movie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='movie_manager.movie')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['showtime'], + }, + ), + migrations.CreateModel( + name='Schedule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('public', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('showings', models.ManyToManyField(to='movie_manager.showing')), + ], + ), + ] diff --git a/movie_manager/migrations/0005_showing_slug.py b/movie_manager/migrations/0005_showing_slug.py new file mode 100644 index 0000000..4d5c0b1 --- /dev/null +++ b/movie_manager/migrations/0005_showing_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-04-08 03:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('movie_manager', '0004_alter_movie_last_watched_showing_schedule'), + ] + + operations = [ + migrations.AddField( + model_name='showing', + name='slug', + field=models.SlugField(default='', max_length=100), + ), + ] diff --git a/movie_manager/migrations/0006_remove_showing_slug_schedule_slug.py b/movie_manager/migrations/0006_remove_showing_slug_schedule_slug.py new file mode 100644 index 0000000..dfe8b61 --- /dev/null +++ b/movie_manager/migrations/0006_remove_showing_slug_schedule_slug.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.4 on 2025-04-08 04:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('movie_manager', '0005_showing_slug'), + ] + + operations = [ + migrations.RemoveField( + model_name='showing', + name='slug', + ), + migrations.AddField( + model_name='schedule', + name='slug', + field=models.SlugField(default='', max_length=100), + ), + ] diff --git a/movie_manager/models.py b/movie_manager/models.py index ccbb950..171e9e2 100644 --- a/movie_manager/models.py +++ b/movie_manager/models.py @@ -1,20 +1,18 @@ -from django.contrib.auth.models import User from django.db import models -from django.db.models import SET_NULL - +from django.contrib.auth.models import User class Movie(models.Model): title = models.CharField(max_length=100) - imdb_id = models.CharField(max_length=100, db_index=True, unique=True) - year = models.IntegerField(null=True, blank=True) - director = models.CharField(max_length=500, null=True, blank=True) - actors = models.TextField(null=True, blank=True) - plot = models.TextField(null=True, blank=True) - genre = models.CharField(max_length=100, null=True, blank=True) - mpaa_rating = models.CharField(max_length=20, null=True, blank=True) - critic_scores = models.TextField(null=True, blank=True) - poster = models.TextField(null=True, blank=True) - added_by = models.ForeignKey(User, on_delete=SET_NULL, null=True) + imdb_id = models.CharField(max_length=100) + year = models.IntegerField() + critic_score = models.CharField(max_length=500) + genre = models.CharField(max_length=100) + director = models.CharField(max_length=500) + actors = models.CharField(max_length=500) + plot = models.CharField(max_length=500) + poster = models.CharField(max_length=500) + last_watched = models.DateTimeField(null=True, blank=True) + added_by = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) deleted_at = models.DateTimeField(null=True, blank=True) @@ -27,8 +25,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,34 +35,23 @@ class MovieList(models.Model): class Meta: ordering = ["name"] - indexes = [ - models.Index(fields=["public", "owner"]), - ] def __str__(self): return self.name - 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) + showings = models.ManyToManyField("Showing") + 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) - class Meta: - ordering = ["name"] - - def __str__(self): - return self.name - - class Showing(models.Model): movie = models.ForeignKey(Movie, on_delete=models.CASCADE) owner = models.ForeignKey(User, on_delete=models.CASCADE) - schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE) public = models.BooleanField(default=False) showtime = models.DateTimeField() created_at = models.DateTimeField(auto_now_add=True) @@ -73,7 +60,3 @@ class Showing(models.Model): class Meta: ordering = ["showtime"] - - def __str__(self): - showtime = self.showtime.strftime("%Y-%m-%d %H:%M") - return showtime 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..bebebb0 100644 --- a/movie_manager/serializers.py +++ b/movie_manager/serializers.py @@ -1,58 +1,26 @@ 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): class Meta: model = User @@ -64,22 +32,14 @@ class ShowingSerializer(serializers.ModelSerializer): class Meta: model = Showing - fields = ["id", "public", "showtime", "movie", "owner"] - - # 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"]) - - # return validated_data + fields = ["public", "showtime", "movie", "owner"] class ScheduleSerializer(serializers.ModelSerializer): - showings = ShowingSerializer(source="showing_set", read_only=True, many=True) + name = serializers.CharField(read_only=True) + showings = ShowingSerializer(read_only=True, many=True) class Meta: model = Schedule - fields = ["name", "owner", "public", "slug", "showings"] + fields = ["name", "owner","public","slug", "showings"] + 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..e08d86e 100644 --- a/movie_manager/views.py +++ b/movie_manager/views.py @@ -0,0 +1,138 @@ +import datetime + +from django.http import JsonResponse +from django.contrib.auth.models import User +from rest_framework import permissions, viewsets +from knox.auth import TokenAuthentication +from rest_framework.decorators import action +from rest_framework.exceptions import NotFound + +from movie_manager.models import Movie, MovieList, Schedule, Showing +from movie_manager.serializers import MovieListSerializer, MovieSerializer, ScheduleSerializer, ShowingSerializer + + +# 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] + + serializer_class = MovieListSerializer + + + def retrieve(self, request, pk=None, *args, **kwargs): + movie_list = MovieList.objects.get(pk=pk) + 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/(?P[0-9]+)') + def add_movie(self, request, pk=None, movie_id=None, *args, **kwargs): + if request.method == 'DELETE': + return self.remove_movie(request, pk, movie_id) + + movie_list = MovieList.objects.get(pk=pk) + movie = Movie.objects.get(pk=movie_id) + movie_list.movies.add(movie) + + return JsonResponse(MovieListSerializer(movie_list).data) + + def remove_movie(self, request, pk=None, movie_id=None, *args, **kwargs): + movie = Movie.objects.get(pk=movie_id) + + 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] + + 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 = instance.showings.filter(showtime__gte=today) + + # 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 = instance.showings.filter(showtime__lt=today) + + # 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] + + 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) + + showing = Showing.objects.create( + movie=movie, + schedule=schedule, + showtime=request.data.get("showtime"), + public=request.data.get("public"), + owner=User.objects.get(pk=request.data.get("owner")) + ) + + schedule.showings.add(showing) + + 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..6889ee7 100644 --- a/movienight/urls.py +++ b/movienight/urls.py @@ -2,42 +2,31 @@ 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 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"api/users", user_views.UserViewSet) +router.register(r"api/groups", user_views.GroupViewSet) +router.register(r"api/movies", movie_views.MovieViewset) +router.register(r"api/lists", movie_views.MovieListViewset) +router.register(r"api/schedules", movie_views.ScheduleViewset) +router.register(r"api/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"api/auth/token/", obtain_auth_token), + path(r"api/auth/login/", user_views.LoginView.as_view(), name="knox_login"), + path(r"api/auth/register/", user_views.register, name="register"), + path(r"api/auth/", include("knox.urls")), +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/requirements.txt b/requirements.txt index 73606df..7c9b3b9 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,3 @@ 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 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)