PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/ticketshop/tickets/models.py

https://gitlab.com/fscons/ticketshop
Python | 220 lines | 195 code | 6 blank | 19 comment | 1 complexity | 18cedded44cd2087f1281e54288fffaf MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. from datetime import datetime
  4. import uuid
  5. from django.core.validators import RegexValidator
  6. from django.db import models
  7. from django.db.models import Count, Q, F
  8. from django.dispatch import Signal
  9. from .mails import (
  10. send_payment_confirmation_email,
  11. send_cancellation_email)
  12. from .utils import randrefnumber
  13. class TicketTypeManager(models.Manager):
  14. """
  15. Create a custom manager with an extra method
  16. that only returns available ticket types
  17. """
  18. def available(self):
  19. """
  20. Custom method that return a queryset with only
  21. available tickets
  22. """
  23. return super(TicketTypeManager, self).get_query_set() \
  24. .annotate(num_sold=Count('ticket')) \
  25. .filter(Q(quantity=None) | Q(num_sold__lt=F('quantity')))
  26. class TicketType(models.Model):
  27. """
  28. A type of ticket is for instance "regular", "early bird", etc.
  29. """
  30. name = models.CharField(
  31. max_length=200,
  32. help_text="How do you call this kind of ticket? E.g. \"Early bird\"")
  33. description = models.TextField(
  34. blank=True,
  35. help_text="What does this ticket gives access to and who can by it.")
  36. price = models.IntegerField(help_text="Price in SEK.")
  37. sales_end = models.DateField(
  38. help_text="After this, it won't be possible to buy this ticket type.")
  39. quantity = models.IntegerField(
  40. null=True,
  41. blank=True,
  42. help_text="Maximum number of ticket of this type.")
  43. # Add our custom manager
  44. objects = TicketTypeManager()
  45. def __unicode__(self):
  46. return u"%s" % (self.name)
  47. def remaining(self):
  48. """ Return the number of tickets remaining of this type """
  49. return self.quantity
  50. def available(self, n=1):
  51. if self.quantity is None:
  52. return True
  53. else:
  54. return self.ticket_set.count() + n <= self.quantity
  55. class Meta:
  56. ordering = ['-price']
  57. def default_ticket_type():
  58. Q = TicketType.objects.order_by('-price')
  59. if Q.count() > 0:
  60. return Q[0]
  61. else:
  62. return None
  63. class Ticket(models.Model):
  64. ticket_type = models.ForeignKey(TicketType, default=default_ticket_type)
  65. name_on_badge = models.CharField(max_length=200)
  66. # Gender distribution, as required by the manifesto.
  67. # For a discussion on more inclusive gender question, see
  68. # http://itspronouncedmetrosexual.com/2012/06/how-can-i-make-the-gender-question-on-an-application-form-more-inclusive/
  69. gender = models.CharField(
  70. max_length=1,
  71. blank=True,
  72. verbose_name="I identify my gender as…",
  73. help_text="Why are we asking this? We ask you for your gender in \
  74. order to evaluate our effort in reaching a fair and \
  75. reasonable, within 60/40, gender distribution among \
  76. participants.",
  77. choices=(('F', 'Woman'), ('M', 'Man'), ('T', 'Trans*'),
  78. ('?', 'Prefer not to disclose'))
  79. )
  80. returning_visitor = models.NullBooleanField(
  81. blank=True,
  82. verbose_name="I have been to FSCONS before",
  83. choices=((True, 'Yes'), (False, 'No')),
  84. )
  85. purchase = models.ForeignKey("TicketPurchase", related_name="tickets")
  86. class Meta:
  87. permissions = (("view_reportk", "Can see the ticket report"),)
  88. def __unicode__(self):
  89. return u"{0.name_on_badge} ({0.ticket_type})".format(self)
  90. class PreBooking(models.Model):
  91. """
  92. A pre-booking is a time-limited hold on a set of tickets.
  93. When a user start a new registration by selecting how many tickets they
  94. want to buy, we create a new pre-booking to hold those tickets for a given
  95. number of minutes. When the registration is completed and a new
  96. TicketPurchase object is created in the database, the pre-booking should be
  97. deleted. Alternatively, after the decided amount of time, the pre-booking
  98. is considered expired and the held-up tickets available again.
  99. Note that the natural key is the combination of session_id and ticket_type
  100. but since django doesn't support compound keys, there is also a surrogate
  101. key.
  102. """
  103. session = models.CharField(max_length=40)
  104. ticket_type = models.ForeignKey(TicketType)
  105. quantity = models.IntegerField()
  106. creation_date = models.DateTimeField(auto_now_add=True)
  107. expiration_date = models.DateTimeField()
  108. class Meta:
  109. unique_together = (('session', 'ticket_type'),)
  110. def __unicode__(self):
  111. return "{0.quantity} x {0.ticket_type}, {0.session}".format(self)
  112. class TicketPurchase(models.Model):
  113. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  114. # Contact details
  115. contact_first_name = models.CharField(
  116. verbose_name="First name",
  117. max_length=200)
  118. contact_surname = models.CharField(
  119. verbose_name="Surname",
  120. max_length=200)
  121. contact_email = models.EmailField(
  122. verbose_name="E-mail")
  123. contact_subscribe = models.BooleanField(
  124. default=False,
  125. verbose_name="I would like to get emails about future conferences")
  126. additional_information = models.TextField(
  127. blank=True,
  128. help_text="Do not hesitate to let us know if you have specific \
  129. requirements or comments about the registration.")
  130. paid = models.BooleanField(default=False, editable=False)
  131. # Administrativia
  132. creation_date = models.DateTimeField(editable=False, auto_now_add=True)
  133. ref = models.CharField(max_length=10, unique=True,
  134. default=randrefnumber,
  135. validators=[RegexValidator(regex=r"^\d{10}$")])
  136. cancelled = models.BooleanField(default=False, editable=False)
  137. cancelled_by = models.CharField(
  138. max_length=5, choices=(('USER', 'User'), ('ADMIN', 'Admin')),
  139. null=True, blank=True, editable=False)
  140. cancellation_date = models.DateTimeField(editable=False, null=True)
  141. def __unicode__(self):
  142. return u"%s %s (%d ticket(s))" % (
  143. self.contact_first_name,
  144. self.contact_surname,
  145. self.number_of_tickets())
  146. def price(self):
  147. p = 0
  148. for ticket in self.tickets.all():
  149. p += ticket.ticket_type.price
  150. return p
  151. def contact_full_name(self):
  152. return u"%s %s" % (self.contact_first_name, self.contact_surname)
  153. def get_contact_mailbox(self):
  154. """ Get the contact 'mailbox', in the form
  155. First Last <Address>
  156. suitable to use when sending emails.
  157. """
  158. return (u"{} <{}>"
  159. .format(self.contact_full_name(), self.contact_email))
  160. def number_of_tickets(self):
  161. return self.tickets.count()
  162. def mark_as_paid(self):
  163. """
  164. Mark an unpaid purchase as paid and send a purchase_paid signal
  165. """
  166. if not self.paid:
  167. self.paid = True
  168. self.save(update_fields=['paid'])
  169. purchase_paid.send(sender=self, purchase=self)
  170. def cancel(self, by):
  171. assert by in ['USER', 'ADMIN']
  172. self.cancelled = True
  173. self.cancellation_date = datetime.now()
  174. self.cancelled_by = by
  175. if by == "USER":
  176. send_cancellation_email(self)
  177. # ~~~ Custom signals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  178. purchase_paid = Signal(providing_args=["purchase"])
  179. # ~~~ Signal handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  180. def send_mail_on_paiement(sender, purchase, **kwargs):
  181. send_payment_confirmation_email(purchase)
  182. purchase_paid.connect(send_mail_on_paiement)