Skip to content

Commit

Permalink
Merge pull request #7 from osmanmakhtoom/feat/backend-stadiums-app
Browse files Browse the repository at this point in the history
Feat/backend stadiums app
  • Loading branch information
osmanmakhtoom authored Sep 27, 2024
2 parents dd9ce2e + 0859b61 commit 32ae1dd
Show file tree
Hide file tree
Showing 28 changed files with 407 additions and 12 deletions.
5 changes: 5 additions & 0 deletions backend/apps/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,8 @@
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

TEST_RUNNER = "redgreenunittest.django.runner.RedGreenDiscoverRunner"

MATCHES_QUERYSET_CACHE_KEY = "MATCHES_QUERYSET_CACHE_KEY"
MATCHES_QUERYSET_CACHE_TIMEOUT = 60 * 60 * 24 * 7
AVAILABLE_SEATS_CACHE_KEY = "AVAILABLE_SEATS_CACHE_KEY"
AVAILABLE_SEATS_CACHE_TIMEOUT = 60 * 60 * 24 * 7
1 change: 1 addition & 0 deletions backend/apps/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
path("admin/", admin.site.urls),
path("accounts/", include("apps.accounts.api.urls"), name="accounts"),
path("stadiums/", include("apps.stadiums.api.urls"), name="stadiums"),
path("auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("auth/logout/", TokenBlacklistView.as_view(), name="logout"),
Expand Down
44 changes: 44 additions & 0 deletions backend/apps/stadiums/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from django.contrib import admin

from apps.stadiums.models import Stadium, Match, Seat, Ticket
from apps.stadiums.tasks import update_cache_match_info_task, update_cache_seats_task


@admin.register(Stadium)
class StadiumAdmin(admin.ModelAdmin):
list_display = ('name', 'location', 'description', 'is_active', 'created_at', 'updated_at')
list_filter = ('is_active', 'location')
search_fields = ('name', 'description', 'location', 'uuid')
date_hierarchy = 'created_at'


@admin.register(Match)
class MatchAdmin(admin.ModelAdmin):
list_display = ('stadium', 'home_team', 'away_team', 'is_active', 'created_at', 'updated_at')
list_filter = ('is_active', 'home_team', 'away_team')
search_fields = ('stadium__name', 'home_team', 'away_team', 'uuid')
date_hierarchy = 'created_at'

def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
update_cache_match_info_task.delay()


@admin.register(Seat)
class SeatAdmin(admin.ModelAdmin):
list_display = ('seat_number', 'match', 'is_reserved', 'price', 'is_active', 'created_at', 'updated_at')
list_filter = ('is_reserved', 'match', 'price')
search_fields = ('seat_number', 'match__home_team', 'match__away_team', 'price', 'uuid')
date_hierarchy = 'created_at'

def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
update_cache_seats_task.delay()


@admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin):
list_display = ('user', 'seat', 'is_active', 'created_at', 'updated_at')
list_filter = ('is_active', 'seat', 'user')
search_fields = ('user__email', 'seat__price', 'uuid')
date_hierarchy = 'created_at'
6 changes: 6 additions & 0 deletions backend/apps/stadiums/api/serializers/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from apps.stadiums.api.serializers.v1.stadium_serializer import StadiumSerializer
from apps.stadiums.api.serializers.v1.match_serializer import MatchSerializer
from apps.stadiums.api.serializers.v1.seat_serializer import SeatSerializer
from apps.stadiums.api.serializers.v1.ticket_serializer import TicketSerializer

__all__ = ['StadiumSerializer', 'MatchSerializer', 'SeatSerializer', 'TicketSerializer']
19 changes: 19 additions & 0 deletions backend/apps/stadiums/api/serializers/v1/match_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.core.exceptions import ImproperlyConfigured

from rest_framework import serializers

from apps.stadiums.models import Match


class MatchSerializer(serializers.ModelSerializer):
stadium = serializers.SlugRelatedField(slug_field='uuid', read_only=True)

class Meta:
model = Match
exclude = ('id',)

def create(self, validated_data):
raise ImproperlyConfigured('You cant\'t create Match instances.')

def update(self, instance, validated_data):
raise ImproperlyConfigured('You cant\'t update Match instances.')
19 changes: 19 additions & 0 deletions backend/apps/stadiums/api/serializers/v1/seat_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.core.exceptions import ImproperlyConfigured

from rest_framework import serializers

from apps.stadiums.models import Seat


class SeatSerializer(serializers.ModelSerializer):
match = serializers.SlugRelatedField(slug_field='uuid', read_only=True)

class Meta:
model = Seat
exclude = ('id',)

def create(self, validated_data):
raise ImproperlyConfigured('You cant\'t create Seat instances.')

def update(self, instance, validated_data):
raise ImproperlyConfigured('You cant\'t update Seat instances.')
17 changes: 17 additions & 0 deletions backend/apps/stadiums/api/serializers/v1/stadium_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.core.exceptions import ImproperlyConfigured

from rest_framework import serializers

from apps.stadiums.models import Stadium


class StadiumSerializer(serializers.ModelSerializer):
class Meta:
model = Stadium
exclude = ('id',)

def create(self, validated_data):
raise ImproperlyConfigured('You cant\'t create Stadium instances.')

def update(self, instance, validated_data):
raise ImproperlyConfigured('You cant\'t update Stadium instances.')
29 changes: 29 additions & 0 deletions backend/apps/stadiums/api/serializers/v1/ticket_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.core.exceptions import ImproperlyConfigured

from rest_framework import serializers

from apps.stadiums.models import Ticket
from apps.stadiums.tasks import reserve_seat_task


class TicketSerializer(serializers.ModelSerializer):
user = serializers.SlugRelatedField(slug_field='uuid', read_only=True)
seat = serializers.SlugRelatedField(slug_field='uuid', read_only=True)

class Meta:
model = Ticket
exclude = ('id',)
read_only_fields = ('uuid', 'is_active', 'created_at', 'updated_at')
extra_kwargs = {
'user': {'required': False},
}

def create(self, validated_data):
user_uuid = self.context['request'].user.uuid
seat_uuid = validated_data['seat'].uuid
task = reserve_seat_task.delay(user_uuid, seat_uuid)
result = task.get(timeout=10)
return result

def update(self, instance, validated_data):
raise ImproperlyConfigured('You cant\'t update Ticket instances.')
19 changes: 19 additions & 0 deletions backend/apps/stadiums/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.urls import path

from rest_framework.routers import DefaultRouter

from apps.stadiums.api.views.v1 import (
StadiumView as StadiumViewV1,
MatchView as MatchViewV1,
SeatView as SeatViewV1,
TicketViewSet as TicketViewSetV1,
)

router = DefaultRouter(trailing_slash=False)
router.register('v1/tickets', TicketViewSetV1, basename='tickets')

urlpatterns = router.urls + [
path('v1/stadiums', StadiumViewV1.as_view(), name='stadiums'),
path('v1/matches', MatchViewV1.as_view(), name='matches'),
path('v1/seats', SeatViewV1.as_view(), name='seats'),
]
6 changes: 6 additions & 0 deletions backend/apps/stadiums/api/views/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from apps.stadiums.api.views.v1.stadium_view import StadiumView
from apps.stadiums.api.views.v1.match_view import MatchView
from apps.stadiums.api.views.v1.seat_view import SeatView
from apps.stadiums.api.views.v1.ticket_viewset import TicketViewSet

__all__ = ['StadiumView', 'MatchView', 'SeatView', 'TicketViewSet']
28 changes: 28 additions & 0 deletions backend/apps/stadiums/api/views/v1/match_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.conf import settings

from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.generics import ListAPIView, RetrieveAPIView

from apps.stadiums.api.serializers.v1 import MatchSerializer
from apps.stadiums.models import Match
from apps.utils.helpers import CacheManager


class MatchView(ListAPIView, RetrieveAPIView):
serializer_class = MatchSerializer
permission_classes = (IsAuthenticatedOrReadOnly,)
lookup_field = 'uuid'

def get_queryset(self):
"""
Return cached queryset if is not expired else hit to database cache it and return.
This approach do not any break in system because we do update cached queryset every time updated table data
"""
cached_queryset = CacheManager(settings.MATCHES_QUERYSET_CACHE_KEY)
if not cached_queryset.is_expired:
return cached_queryset.value
matches = Match.objects.filter_active().select_related('stadium')
cached_queryset.period = settings.MATCHES_QUERYSET_CACHE_TIMEOUT
cached_queryset.value = matches

return matches
28 changes: 28 additions & 0 deletions backend/apps/stadiums/api/views/v1/seat_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.conf import settings

from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.generics import ListAPIView, RetrieveAPIView

from apps.stadiums.api.serializers.v1 import SeatSerializer
from apps.stadiums.models import Seat
from apps.utils.helpers import CacheManager


class SeatView(ListAPIView, RetrieveAPIView):
serializer_class = SeatSerializer
permission_classes = (IsAuthenticatedOrReadOnly,)
lookup_field = 'uuid'

def get_queryset(self):
"""
Return cached queryset if is not expired else hit to database cache it and return.
This approach do not any break in system because we do update cached queryset every time updated table data
"""
cached_queryset = CacheManager(settings.AVAILABLE_SEATS_CACHE_KEY)
if not cached_queryset.is_expired:
return cached_queryset.value
seats = Seat.objects.filter_active(is_reserved=False).select_related('match')
cached_queryset.period = settings.AVAILABLE_SEATS_CACHE_TIMEOUT
cached_queryset.value = seats

return seats
16 changes: 16 additions & 0 deletions backend/apps/stadiums/api/views/v1/stadium_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator

from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.generics import ListAPIView, RetrieveAPIView

from apps.stadiums.api.serializers.v1 import StadiumSerializer
from apps.stadiums.models import Stadium


@method_decorator(cache_page(60 * 10), name='dispatch')
class StadiumView(ListAPIView, RetrieveAPIView):
queryset = Stadium.objects.filter_active()
serializer_class = StadiumSerializer
permission_classes = (IsAuthenticatedOrReadOnly,)
lookup_field = 'uuid'
18 changes: 18 additions & 0 deletions backend/apps/stadiums/api/views/v1/ticket_viewset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet

from apps.stadiums.api.serializers.v1 import TicketSerializer
from apps.stadiums.models import Ticket


@method_decorator(cache_page(60 * 10), name='dispatch')
class TicketViewSet(ModelViewSet):
serializer_class = TicketSerializer
permission_classes = (IsAuthenticated,)
lookup_field = 'uuid'

def get_queryset(self):
return Ticket.objects.filter(user=self.request.user).select_related('user', 'seat')
6 changes: 6 additions & 0 deletions backend/apps/stadiums/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from apps.stadiums.models.stadium import Stadium
from apps.stadiums.models.match import Match
from apps.stadiums.models.seat import Seat
from apps.stadiums.models.ticket import Ticket

__all__ = ['Stadium', 'Match', 'Seat', 'Ticket']
21 changes: 21 additions & 0 deletions backend/apps/stadiums/models/match.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import models

from apps.stadiums.models import Stadium

from apps.utils.model_mixins import IsActiveMixin, TimestampedMixin, UUIDMixin
from apps.utils.managers import FilterActiveManager


class Match(TimestampedMixin, IsActiveMixin, UUIDMixin):
stadium = models.ForeignKey(Stadium, on_delete=models.PROTECT, related_name='matches')
home_team = models.CharField(max_length=255)
away_team = models.CharField(max_length=255)
match_date = models.DateTimeField()

objects = FilterActiveManager()

def __str__(self):
return f'{self.home_team} vs {self.away_team} @ {self.stadium.name} on {self.match_date}'

def __repr__(self):
return f'<Match: home={self.home_team}, away={self.away_team}, date={self.match_date}, stadium={self.stadium.name}>'
21 changes: 21 additions & 0 deletions backend/apps/stadiums/models/seat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import models

from apps.utils.model_mixins import IsActiveMixin, TimestampedMixin, UUIDMixin
from apps.utils.managers import FilterActiveManager

from apps.stadiums.models import Match


class Seat(TimestampedMixin, IsActiveMixin, UUIDMixin):
match = models.ForeignKey(Match, on_delete=models.PROTECT, related_name='seats')
seat_number = models.CharField(max_length=10)
is_reserved = models.BooleanField(default=False)
price = models.IntegerField(default=0)

objects = FilterActiveManager()

def __str__(self):
return f'{self.seat_number} for {self.match}'

def __repr__(self):
return f'<Seat: number={self.seat_number}, match={self.match}>'
19 changes: 19 additions & 0 deletions backend/apps/stadiums/models/stadium.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.db import models

from apps.utils.model_mixins import IsActiveMixin, TimestampedMixin, UUIDMixin
from apps.utils.managers import FilterActiveManager


class Stadium(TimestampedMixin, IsActiveMixin, UUIDMixin):
name = models.CharField(max_length=255)
location = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
capacity = models.IntegerField(default=0)

objects = FilterActiveManager()

def __str__(self):
return self.name

def __repr__(self):
return f'<Stadium: name={self.name}, capacity={self.capacity}>'
20 changes: 20 additions & 0 deletions backend/apps/stadiums/models/ticket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.contrib.auth import get_user_model
from django.db import models

from apps.utils.model_mixins import IsActiveMixin, TimestampedMixin, UUIDMixin
from apps.utils.managers import FilterActiveManager

from apps.stadiums.models import Seat


class Ticket(TimestampedMixin, IsActiveMixin, UUIDMixin):
seat = models.ForeignKey(Seat, on_delete=models.PROTECT, related_name='tickets')
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT, related_name='tickets')

objects = FilterActiveManager()

def __str__(self):
return f'Ticket for {self.user.uuid} - {self.seat}'

def __repr__(self):
return f'<Ticket: user={self.user.uuid}, {self.seat}, {self.created_at}>'
5 changes: 5 additions & 0 deletions backend/apps/stadiums/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from apps.stadiums.tasks.reserve_seat_task import reserve_seat_task
from apps.stadiums.tasks.update_cache_match_info_task import update_cache_match_info_task
from apps.stadiums.tasks.update_cache_seats_task import update_cache_seats_task

__all__ = ['reserve_seat_task', 'update_cache_match_info_task', 'update_cache_seats_task']
Loading

0 comments on commit 32ae1dd

Please sign in to comment.