Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

django challeng task #7

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
venv
*.pyc
local_settings.py
**/migrations/**
!**/migrations
!**/migrations/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions selling_tickets/booking/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.contrib import admin
from booking.models import Stadium, StadiumPlace, PlaceSeats, Match, Team
# Register your models here.
admin.site.register(Stadium)
admin.site.register(StadiumPlace)
admin.site.register(PlaceSeats)
admin.site.register(Match)
admin.site.register(Team)
admin.site.register(Ticket)
admin.site.register(Invoice)
6 changes: 6 additions & 0 deletions selling_tickets/booking/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class BookingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'booking'
Empty file.
143 changes: 143 additions & 0 deletions selling_tickets/booking/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from django.db import models, transaction
import uuid

# each stadium is consist of defining statdium and its places and the nuumber of row and seats
# in each place, so in adding stadium action we should define all these models (Stadium, StadiumPlace, PlaceSeats)
class Stadium(models.Model):
name = models.CharField(max_length=50, verbose_name='Stadium Name')
city = models.CharField(max_length=50, blank=True, verbose_name='Stadium City')
address = models.TextField(verbose_name='Stadium Address')

def __str__(self) -> str:
return self.name

class StadiumPlace(models.Model):
"""
each stadium has multiple places such as vip, north , south and ...
in this model we define these palces and number of rows in each place
"""
stadium = models.ForeignKey('Stadium', on_delete=models.CASCADE, related_name='places')
name = models.CharField(max_length=50, verbose_name='Place Name', unique=True)
number_of_rows = models.PositiveSmallIntegerField()

def __str__(self) -> str:
return ("stadium: %s place: %s") % (self.stadium, self.name)


class PlaceSeats(models.Model):
"""
each place in stadium has muultiple rows and each row has different number of saets
so we define this model to define this feature
"""
place = models.ForeignKey('StadiumPlace', on_delete=models.CASCADE, related_name='place_seats')
row_number = models.IntegerField()
number_of_seats_per_row = models.PositiveSmallIntegerField()

def __str__(self) -> str:
return ("%s row_number: %s") % (self.place, self.row_number)

class Meta:
unique_together = ['place', 'row_number']

# in definattion of matches, we need to add teams too, so for adding a new match we should hav att least two teams

class Team(models.Model):
name = models.CharField(max_length=50, verbose_name='Team Name', unique=True)

def __str__(self) -> str:
return self.name


class Match(models.Model):
stadium = models.ForeignKey('Stadium', on_delete=models.CASCADE, related_name='matches')
host_team = models.ForeignKey('Team', on_delete=models.PROTECT, related_name='host_team')
guest_team = models.ForeignKey('Team', on_delete=models.PROTECT, related_name='guest_team')
start_time = models.DateTimeField()

def __str__(self) -> str:
return "%s VS %s, Date: %s" % (self.host_team, self.guest_team, self.start_time)

class Meta:
unique_together = ['stadium', 'start_time']


# by selecting first ticket , we create the invoice for that user
# invoice has many to many relation with ticket model
# so each invoice has multiple tickets and each ticket can assign to many invoice
# before calling the payment method we change the state of tickets to reserved
# at this moment nobody can buy those tickets anymore until payment is done
# reservation of selected tickets only occured that user going to pay the invoice amount

class Invoice(models.Model):
START = 0 # creation of invoice
IN_PROGGRESS = 1 # user is in payment section
SUCCESS = 2 # payment is successfull
FAILED = 3 # payment is faieled or canceled
INVOICE_STATUSES = [
(START, 'START'),
(IN_PROGGRESS, 'IN_PROGGRESS'),
(SUCCESS, 'SUCCESS'),
(FAILED, 'FAILED'),
]
status = models.IntegerField(default=START, choices=INVOICE_STATUSES)
user = models.ForeignKey('user.CustomUser', on_delete=models.CASCADE, related_name='reservations')
creation_date = models.DateTimeField(auto_now_add=True)
payment_date_time = models.DateTimeField(blank=True, null=True)
payment_description = models.TextField(blank=True)
payment_number = models.CharField(max_length=50, blank=True)

def __str__(self) -> str:
return ("user: %s created_date: %s") % (self.user, self.creation_date)

def set_in_progress(self):
self.status = Invoice.IN_PROGGRESS
self.save()

def set_payment_number(self):
self.payment_number = uuid.uuid4().hex[:10].upper()
self.save()

# after we create Stadium and its places, we can add tickets for each seat in stadium accordiing to each match policy
# sometimes we can not define tickets for all of seats in stadium

class Ticket(models.Model):
EMPTY = 'EMPTY' #first state of Tticket
RESERVED = 'RESERVED' # user is in payment section and the tickets are reserved for that user
SOLD = 'SOLD' # user pay the invoice amount successfully and get the reservation number
STATUS_CHOICES = [
(EMPTY, 'EMPTY'),
(RESERVED, 'RESERVED'),
(SOLD, 'SOLD'),
]
match = models.ForeignKey('Match', on_delete=models.RESTRICT, related_name='tickets')
stadium = models.ForeignKey('Stadium', on_delete=models.RESTRICT, related_name='tickets')
status = models.CharField(default=EMPTY, choices=STATUS_CHOICES, max_length=8)
last_update_time = models.DateTimeField(auto_now=True)
place = models.ForeignKey('StadiumPlace', on_delete=models.CASCADE, related_name='tickets')
row_number = models.PositiveSmallIntegerField()
seat_number = models.PositiveSmallIntegerField()
price= models.DecimalField(max_digits=10, decimal_places=2)
invoices = models.ManyToManyField(Invoice, blank=True, related_name='tickets')

def __str__(self) -> str:
return "%s, %s row: %s seat: %s" % (self.match, self.place, self.row_number, self.seat_number)

class AlreadyReserved(Exception):
pass

def get_queryset(self):
return self.__class__.objects.filter(id=self.id)

def reserve_ticket(self):
if self.status == Ticket.RESERVED: # if status of ticket is already reserved we raise an error
raise Ticket.AlreadyReserved
with transaction.atomic(): # the action of reservation of ticket is doing by transaction that nobody can get the rows concurrency
ticket = self.get_queryset().select_for_update().get()
if ticket.status == Ticket.RESERVED:
raise Ticket.AlreadyReserved
self._reserve_ticket()

def _reserve_ticket(self):
self.status = Ticket.RESERVED
self.save()

120 changes: 120 additions & 0 deletions selling_tickets/booking/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from django.db.models import Q
from rest_framework import serializers
from booking.models import Stadium, StadiumPlace, PlaceSeats, Team, Match, Ticket, Invoice


class MatchSerializer(serializers.ModelSerializer):
start_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
tickets = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='ticket_detail')
class Meta:
model = Match
fields = '__all__'


class PlaceSeatsSerializer(serializers.ModelSerializer):
class Meta:
model = PlaceSeats
fields = ('id', 'number_of_seats_per_row', 'row_number', 'place',)

def create(self, validated_data):
place = validated_data.get('place')
place_number_of_rows_limitation = place.number_of_rows
if validated_data.get('row_number') > place_number_of_rows_limitation: # validate that selected row is existed in range of rows limiation
raise serializers.ValidationError("seledted row is not existed for this place")
return super().create(validated_data)


class StadiumPlaceSerializer(serializers.ModelSerializer):
place_seats = PlaceSeatsSerializer(many=True, read_only=True)

class Meta:
model = StadiumPlace
fields = ('id', 'name', 'number_of_rows', 'stadium', 'place_seats',)


class StadiumSerializer(serializers.ModelSerializer):
places = StadiumPlaceSerializer(many=True, read_only=True)

class Meta:
model = Stadium
fields = ('id', 'name', 'city', 'address', 'places', 'matches',)


class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ('id', 'name',)


class TicketSerializer(serializers.ModelSerializer):
status = serializers.CharField(read_only=True)
last_update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True)

class Meta:
model = Ticket
fields = ('id', 'stadium', 'match', 'status', 'last_update_time', 'row_number', 'seat_number', 'place', 'price')

def create(self, validated_data):
selected_row_number = validated_data.get('row_number')
place_seats = PlaceSeats.objects.get(row_number=selected_row_number)
number_of_row_seats_limitation = place_seats.number_of_seats_per_row
if validated_data.get('seat_number') > number_of_row_seats_limitation: # validate that the seat number is existed in range of number of seats limitation
raise serializers.ValidationError("seledted seats is not existed for this row")
return super().create(validated_data)


class InvoiceSerilizer(serializers.ModelSerializer):
tickets = serializers.PrimaryKeyRelatedField(many=True, queryset=Ticket.objects.filter(~Q(status=Ticket.RESERVED)))

class Meta:
model = Invoice
fields = ('id', 'tickets')

def create(self, validated_data):
tickets = validated_data.get('tickets')
for t in tickets:
if t.status == Ticket.RESERVED:
raise serializers.ValidationError("selected ticket %s is reserved" % (t.id))
return super().create(validated_data)


def update(self, instance, validated_data):
tickets = validated_data.get('tickets')
for t in tickets:
if t.status == Ticket.RESERVED:
raise serializers.ValidationError("selected ticket %s is reserved" % (t.id))
return super().update(instance, validated_data)


class RetrieveInvoiceSerilizer(serializers.ModelSerializer):
tickets = TicketSerializer(many=True)
tickets_total_price = serializers.SerializerMethodField()

class Meta:
model = Invoice
fields = ('id', 'tickets', 'tickets_total_price')

def get_tickets_total_price(self, obj):
return sum([t.price for t in obj.tickets.all()])


class BuyInvoiceSerilizer(serializers.ModelSerializer):
class Meta:
model = Invoice
fields = ('id',)

def update(self, instance, validated_data):
# meaning do the payment but before that
# get invoice instance , get invoice tickets, check status of tickets
# if they are not reserved we will change their status to reservered ,
# change status of invoice to in progress, generate payment_id and payment_date
# and then call payment method
# payment gateway should call my callback method for alerting the result #TODO: callback method is not implemented
# TODO: we should handle reserverd tickets after 15 min (default duration for payment) in another job (such as celery job)
tickets = instance.tickets.all()
for t in tickets:
t.reserve_ticket()
instance.set_in_progress()
instance.set_payment_number()

return super().update(instance, validated_data)
3 changes: 3 additions & 0 deletions selling_tickets/booking/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
24 changes: 24 additions & 0 deletions selling_tickets/booking/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.urls import path, include
from booking.views import StadiumListCreateAPI, StadiumRetrieveUpdateDestroyAPI, StadiumPlaceListCreateAPI,\
StadiumPlaceRetrieveUpdateDestroyAPI, PlaceSeatsListCreateAPI, PlaceSeatsRetrieveUpdateDestroyAPI,\
TeamListCreateAPI, TeamRetrieveUpdateDestroyAPI, MatchListCreateAPI, MatchRetrieveUpdateDestroyAPI,\
TicketListCreateAPIView, TicketRetrieveUpdateDestroyAPI, CreateInvoiceAPI, RetrieveUpdateInvoiceAPI,\
BuyInvoiceAPI

urlpatterns = [
path('stadium', StadiumListCreateAPI.as_view()),
path('stadium/<int:pk>', StadiumRetrieveUpdateDestroyAPI.as_view(), name='stadium_detail'),
path('stadium-place', StadiumPlaceListCreateAPI.as_view()),
path('stadium-place/<int:pk>', StadiumPlaceRetrieveUpdateDestroyAPI.as_view(), name='stadium_place_detail'),
path('place-seats', PlaceSeatsListCreateAPI.as_view()),
path('place-seats/<int:pk>', PlaceSeatsRetrieveUpdateDestroyAPI.as_view(), name='place_seats_detail'),
path('team', TeamListCreateAPI.as_view()),
path('team/<int:pk>', TeamRetrieveUpdateDestroyAPI.as_view(), name='team_detail'),
path('match', MatchListCreateAPI.as_view()),
path('match/<int:pk>', MatchRetrieveUpdateDestroyAPI.as_view(), name='match_detail'),
path('ticket', TicketListCreateAPIView.as_view()),
path('ticket/<int:pk>', TicketRetrieveUpdateDestroyAPI.as_view(), name='ticket_detail'),
path('invoice', CreateInvoiceAPI.as_view()),
path('invoice/<int:pk>', RetrieveUpdateInvoiceAPI.as_view()),
path('invoice/<int:pk>/buy', BuyInvoiceAPI.as_view()),
]
Loading