1

I'm really struggling to update the quantity of a ticket. My website (project) mimics a e-commerce site which sells tickets to for music events.

When a user buys ticket, the ticket quantity should update. Example: 100 tickets available and 5 are bought, the db should say 95 remaining. But if the tickets goes below 0 then a warning should appear "No more tickets for this event".

Can somebody help me implement this? This is the last bit I need to do then I've finished!! Cheers. Code:

The capacity in the Venue class is what I'm referring to when I say it needs to be updated.

Models.py

class Genre(models.Model):
    genre = models.CharField(max_length=30, default=None, unique=True)

    def __str__(self):
        return self.genre

    class Meta:
        ordering = [ "genre" ]
        verbose_name_plural = "genres"

class CardChoices(models.Model):
    payment_type = models.CharField(max_length=30, default=None, unique=True)

    def __str__(self):
        return self.payment_type

    class Meta:
        ordering = [ "payment_type" ]
        verbose_name_plural = "payment types"
### Start
class Venue(models.Model):
    """The 'Venue' model represents a collection of different venues."""
    name = models.CharField(max_length=30, default=None)
    address = models.CharField(max_length=255, default=None)
    capacity = models.PositiveIntegerField(default=0)
    website = models.URLField(max_length=50, null=True)
    phone_number = models.CharField(validators=[
        RegexValidator(regex='^\d{11}$', message='Length has to be 11',
            code='Invalid number')], blank=True, null=True, max_length=11)
    #phone_number = models.IntegerField(max_length=11, unique=True, validators=[RegexValidator(regex='^\d{10}$', message='Length has to be 11', code='Invalid number')], default=None)
    description = models.TextField()
    slug = models.SlugField(unique=True, default=None)

    # Returns the name of a category
    def __str__(self):
        return self.name

    # Call the slugify method and update the slug field when the name is changed
    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        super(Venue, self).save(*args, **kwargs)

    class Meta:
        # Orders the categories using the name field in ascending order
        ordering = [ "name" ]
        verbose_name_plural = "venues"    

class Artist(models.Model):
    """The 'Artist' model represents a collection of different artists."""
    name = models.CharField(max_length=30, default=None)
    description = models.CharField(max_length=255)
    image = models.ImageField(upload_to='artist/images')
    genre = models.ForeignKey('Genre', related_name='genres', default=None)
    slug = models.SlugField(unique=True, default=None)

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        super(Artist, self).save(*args, **kwargs)

    class Meta:
        ordering = [ "name" ]
        verbose_name_plural = "artists"

class Event(models.Model):
    """The 'Event' model represents a collection of different events."""
    artists = models.ManyToManyField(Artist)
    venue = models.ForeignKey(Venue)
    name = models.CharField(max_length=255, default=None)
    quantity = models.IntegerField()
    price = models.IntegerField() # Needs to be repeated so that django-carton can convert the value (dirty hack needs fixing)
    #price = PriceField('Price', currency='GBP', max_digits=10, decimal_places=2)
    date = models.DateField()
    start_time = models.TimeField()
    end_time = models.TimeField()
    curfew_time = models.TimeField()
    description = models.TextField()
    slug = models.SlugField(unique=True, default=None)

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        super(Event, self).save(*args, **kwargs)

    class Meta:
        ordering = [ "name" ]
        verbose_name_plural = "events"

class UserAccount(models.Model): # needs to be associated with INDIVIDUAL purchases, a user can hold more than one purchase
    user = models.OneToOneField(User) # Links user account with a Django User model instance
    #event = models.ForeignKey(Event, blank=True)
    name = models.CharField(max_length=30)    
    address = models.CharField(max_length=255)
    phone_number = models.IntegerField(max_length=11, unique=True, validators=[RegexValidator(regex='^\d{10}$', message='Length has to be 11', code='Invalid number')])
    email = models.EmailField(validators=[validate_email])

    def __str__(self):
        return self.user.email

class Purchase(models.Model): # needs to be associated with a user account
    cash_only = models.BooleanField('Cash only', default=False)
    payment_type = models.ForeignKey('CardChoices', related_name='Payment Types')
    card_name = models.CharField(max_length=26, default=None, validators=[alphanumeric_RE]) #done
    card_number = models.CharField(max_length=19, default=None) # OVERWRITTEN
    security_code = models.IntegerField(max_length=3, default=None) # OVERWRITTEN
    expiry_date = models.DateField(default=datetime.now) # OVERWRITTEN
    date = models.DateField(editable=True, auto_now_add=True, default=datetime.now)
    delivery_option = models.BooleanField('Is Collecting Ticket', default=True)
    reference_number = models.CharField(max_length=LENGTH, unique=True, default=None)
    temp_session_key = models.CharField(null=True, editable=False, max_length=255, default=None)

def __str__(self):
    return self.card_name

def save(self, *args, **kwargs): # Thanks to workmajj for providing the logic!
    """
    Upon saving, generate a code by randomly picking LENGTH number of
    characters from CHARSET and concatenating them. If code has already
    been used, repeat until a unique code is found, or fail after trying
    MAX_TRIES number of times. (This will work reliably for even modest
    values of LENGTH and MAX_TRIES, but do check for the exception.)
    Discussion of method: http://stackoverflow.com/questions/2076838/
    """
    loop_num = 0
    unique = False
    while not unique:
        if loop_num < MAX_TRIES:
            new_code = ''
            for i in range(LENGTH):
                new_code += CHARSET[randrange(0, len(CHARSET))]
            if not Purchase.objects.filter(reference_number=new_code):
                self.reference_number = new_code
                unique = True
            loop_num += 1
        else:
            raise ValueError("Couldn't generate a unique code.")
    super(Purchase, self).save(*args, **kwargs)

class Meta:
    ordering = [ "date" ]

Forms.py

class ContactForm(forms.Form):
    """Form for the contact page containing relevant fields and appropriate attributes."""
    name = forms.CharField(required=True, max_length=100, label='Name')
    email = forms.EmailField(required=True, label='E-mail Address', validators=[validate_email])
    subject = forms.CharField(required=True, label='Subject')
    query = forms.CharField(required=True, widget=forms.Textarea)

class PurchaseForm(forms.ModelForm):
    # Custom fields overwrite the ones in the Purchase model
    payment_type = forms.ModelChoiceField(queryset=CardChoices.objects.all())
    card_name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': "Card Holder's Name"}))
    card_number = CreditCardField(required=True, widget=forms.TextInput(attrs={'placeholder': 'Credit / Debit card number'}))
    security_code = VerificationValueField(required=True)
    expiry_date = ExpiryDateField(required=True)
    DELIVERY_CHOICES = (
        (True, 'Collect from Venue'),
        (False, 'Print Ticket'),
    )
    delivery_option = forms.ChoiceField(choices=DELIVERY_CHOICES, widget=forms.RadioSelect())
    email_receipt = forms.BooleanField(required=False, label='Tick this box to receive an e-mail receipt')
    email = forms.EmailField(required=False, label='E-mail address', validators=[validate_email])

    class Meta:
        model = Purchase
        fields = ("payment_type", "card_name", "card_number", "security_code", "expiry_date", "delivery_option", "email_receipt", "email")

class UserForm(forms.ModelForm):
    password = forms.CharField(widget=forms.PasswordInput())

    class Meta:
        model = User
        fields = ("username", "email", "password")

class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserAccount
        fields = ("name", "address", "phone_number")
  • To be honest it depends whather a user can purchase more that 1 ticket with the same transaction or not. If a user can only purchase one ticket at a time and 2 users are trying to purchase the last ticket then you should begin reserving the last ticket for the user that started the transaction first with an expiring session.If the user buys the ticket then no more tickets should be available. – Fanis Despoudis May 07 '15 at 19:14
  • If a user can only purchase more that one ticket you should allow only a maximum of lets say 5 at the time per transaction and it should be reserved the same way. You can release unbought tickets after the session has expired. – Fanis Despoudis May 07 '15 at 19:16
  • The number of available tickets should be hardcoded or saved in a file or a database field. Lets say there are 100 tickets. In your code there should be an atomic transaction that will save the Purchase and subtract the number of tickets as long as they are available. In case of a IntegrityError it should rollback the changes – Fanis Despoudis May 07 '15 at 19:23
  • shall i show you my views? @FanisDespoudis –  May 07 '15 at 19:30

2 Answers2

1

On your view after form validations you should do something like this :

try:
   with transaction.atomic(): 
      x=get_number_of_tickets   
      venue=Venue.objects.get(id=5)   
      venue.capacity -= x  
      venue.update()   
      Purchase.save() 
except IntegrityError:
      raise Error

check the django transactions documentation for more info as its more complete: https://docs.djangoproject.com/en/1.8/topics/db/transactions/

Fanis Despoudis
  • 362
  • 1
  • 7
0

You'd want to avoid race conditions. Transactions are only a first step in this.

The first thing you'd want to do in your transaction is to acquire a lock on the row you're changing. This prevents that you read a value that is outdated by the time you write to the database. This can be done with an update statement, that will lock the row from further updates until the transaction is committed or rolled back:

with transaction.atomic():
    Event.objects.filter(id=5).update(quantity=F('quantity') - x)

When you have the lock, and done the update, check if data integrity is still intact (i.e. the capacity isn't lower than 0). If it is, continue, otherwise, rollback the transaction by raising an exception:

event = Event.objects.get(id=5)
if event.capacity < 0:
    raise ValueError("Capacity exceeded")

If there are enough places left, you'll exit the atomic() block at this point, the transaction is committed, and other requests can acquire the lock and try to update the value. If there isn't enough capacity, the transaction is rolled back, and other requests will never have known the value has changed, as they've been waiting to get the lock.

Now, to put it all together:

from django.db import transaction
from django.db.models import F

def get_tickets(event_id, num_tickets):
    with transaction.atomic():
        Event.objects.filter(id=event_id).update(quantity=F('quantity') - num_tickets)
        if Event.objects.get(id=venue_id).quantity < 0:
            raise ValueError("Capacity exceeded: less than %s tickets left." % num_tickets)
knbk
  • 52,111
  • 9
  • 124
  • 122
  • I would probably put it as a method on the model, but there are really no restrictions. To take a quote from the docs out of context: "This is your chance to really flaunt your individualism." – knbk May 07 '15 at 20:47
  • @knbk Would it work with mongoDB which does not support transactions?? – Ekwinder Saini Oct 05 '15 at 08:35
  • @EkwinderSaini The code as-is, without transactions, would not prevent that the quantity can drop below 0, since the `update()` won't be rolled back on an error. – knbk Oct 05 '15 at 09:51
  • @knbk You missed my point, I simply mean would it work with mongoDB? or it would only work with an SQL based database? – Ekwinder Saini Oct 05 '15 at 13:56
  • @EkwinderSaini I've never used MongoDB, so I have no idea how to properly implement this. – knbk Oct 05 '15 at 14:02