fix-timezone-handling #13

Merged
tiradoe merged 2 commits from fix-timezone-handling into main 2025-07-03 05:08:43 +00:00
13 changed files with 107 additions and 98 deletions

3
.gitignore vendored
View file

@ -175,3 +175,6 @@ cython_debug/
# django # django
static static
# JetBrains
.idea

8
.idea/.gitignore generated vendored
View file

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View file

@ -1,8 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>mpaa</w>
<w>viewset</w>
</words>
</dictionary>
</component>

View file

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

11
.idea/misc.xml generated
View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="enabledOnReformat" value="true" />
<option name="enabledOnSave" value="true" />
<option name="executionMode" value="BINARY" />
<option name="pathToExecutable" value="$USER_HOME$/.local/bin/black" />
<option name="sdkName" value="Python 3.13 virtualenv at ~/Projects/MovieNight/movie-night-api/.venv" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 virtualenv at ~/Projects/MovieNight/movie-night-api/.venv" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/movie-night-py.iml" filepath="$PROJECT_DIR$/.idea/movie-night-py.iml" />
</modules>
</component>
</project>

View file

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="movienight/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/.venv1" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 virtualenv at ~/Projects/MovieNight/movie-night-api/.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
</component>
</module>

6
.idea/vcs.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -1,6 +1,6 @@
from django.utils import timezone
from gunicorn.config import User from gunicorn.config import User
from rest_framework import serializers from rest_framework import serializers
from movie_manager.models import Movie, MovieList, Schedule, Showing from movie_manager.models import Movie, MovieList, Schedule, Showing
@ -28,6 +28,7 @@ class MovieSerializer(serializers.ModelSerializer):
def get_has_been_scheduled(self, obj): def get_has_been_scheduled(self, obj):
return Showing.objects.filter(movie_id=obj.id).exists() return Showing.objects.filter(movie_id=obj.id).exists()
class MovieListListSerializer(serializers.ModelSerializer): class MovieListListSerializer(serializers.ModelSerializer):
movie_count = serializers.SerializerMethodField() movie_count = serializers.SerializerMethodField()
@ -45,17 +46,13 @@ class MovieListSerializer(serializers.ModelSerializer):
owner = serializers.PrimaryKeyRelatedField(read_only=True) owner = serializers.PrimaryKeyRelatedField(read_only=True)
def get_queryset(self): def get_queryset(self):
return MovieList.objects.prefetch_related( return MovieList.objects.prefetch_related("movies", "movies__showing_set")
"movies",
"movies__showing_set"
)
class Meta: class Meta:
model = MovieList model = MovieList
fields = ["id", "name", "owner", "public", "movies"] fields = ["id", "name", "owner", "public", "movies"]
class UserSerializer(serializers.Serializer): class UserSerializer(serializers.Serializer):
class Meta: class Meta:
model = User model = User
@ -69,19 +66,18 @@ class ShowingSerializer(serializers.ModelSerializer):
model = Showing model = Showing
fields = ["id", "public", "showtime", "movie", "owner"] fields = ["id", "public", "showtime", "movie", "owner"]
def to_internal_value(self, data): # def to_internal_value(self, data):
validated_data = super().to_internal_value(data) # validated_data = super().to_internal_value(data)
if "showtime" in validated_data and timezone.is_naive( # if "showtime" in validated_data and timezone.is_naive(
validated_data["showtime"] # validated_data["showtime"]
): # ):
validated_data["showtime"] = timezone.make_aware(validated_data["showtime"]) # validated_data["showtime"] = timezone.make_aware(validated_data["showtime"])
return validated_data # return validated_data
class ScheduleSerializer(serializers.ModelSerializer): class ScheduleSerializer(serializers.ModelSerializer):
name = serializers.CharField(read_only=True)
showings = ShowingSerializer(source="showing_set", read_only=True, many=True) showings = ShowingSerializer(source="showing_set", read_only=True, many=True)
class Meta: class Meta:

View file

@ -1,3 +1,89 @@
from django.test import TestCase import json
# Create your tests here. 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-02 23:59:00", tz_offset=-5)
def test_it_returns_active_showings(self):
self.client.force_authenticate(user=self.schedule.owner)
showtimes_america_chicago_utc = [
"2025-07-03T04:00:59.000Z", # 7/2/25 11:59pm
"2025-07-01T04:00:59.000Z", # 6/30/25 11:59pm
"2025-07-08T04:00:59.000Z", # 7/7/25 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")

View file

@ -1,6 +1,7 @@
import datetime import datetime
from django.http import JsonResponse from django.http import JsonResponse
from django.utils import timezone
from knox.auth import TokenAuthentication from knox.auth import TokenAuthentication
from rest_framework import viewsets, permissions from rest_framework import viewsets, permissions
@ -23,15 +24,14 @@ class ScheduleViewset(viewsets.ModelViewSet):
def retrieve(self, request, pk=None, *args, **kwargs): def retrieve(self, request, pk=None, *args, **kwargs):
# Get the schedule instance # Get the schedule instance
instance = self.get_object() instance = self.get_object()
now = datetime.datetime.now() now = timezone.now()
# get time from start of day # get time from start of day
today = datetime.datetime(now.year, now.month, now.day) today = timezone.make_aware(datetime.datetime(now.year, now.month, now.day))
upcoming_showings = Showing.objects.filter( upcoming_showings = Showing.objects.filter(
showtime__gte=today, schedule=instance showtime__gte=today, schedule=instance
) )
# Create a serialized response
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
data = serializer.data data = serializer.data

View file

@ -33,4 +33,4 @@ class ShowingViewset(viewsets.ModelViewSet):
owner=request.user, owner=request.user,
) )
return JsonResponse(ShowingSerializer(showing).data) return JsonResponse(ShowingSerializer(showing).data, status=201)

View file

@ -6,4 +6,5 @@ django-filter
gunicorn==23.0.0 gunicorn==23.0.0
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
django-cors-headers==4.7.0 django-cors-headers==4.7.0
requests==2.32.3 requests==2.32.3
freezegun==1.5.2