forked from CGM_Public/pretix_original
Add support for reserved seating (#1228)
* Initial work on seating * Add seat guids * Add product_list_top * CartAdd: Ignore item when a seat is passed * Cart display * product_list_top → render_seating_plan * Render seating plan in voucher redemption * Fix failing tests * Add tests for extending cart positions with seats * Add subevent_forms to docs * Update schema, migrations * Dealing with expired orders * steps to order change * Change order positions * Allow to add seats * tests for ocm * Fix things after rebase * Seating plans API * Add more tests for cart behaviour * Widget support * Adjust widget tests * Re-enable CSP * Update schema * Api: position.seat * Add guid to word list * API: (sub)event.seating_plan * Vali fixes * Fix api * Fix reference in test * Fix test for real
This commit is contained in:
111
src/pretix/base/models/seating.py
Normal file
111
src/pretix/base/models/seating.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import json
|
||||
from collections import namedtuple
|
||||
|
||||
import jsonschema
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
|
||||
|
||||
|
||||
@deconstructible
|
||||
class SeatingPlanLayoutValidator:
|
||||
def __call__(self, value):
|
||||
if not isinstance(value, dict):
|
||||
try:
|
||||
val = json.loads(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_('Your layout file is not a valid JSON file.'))
|
||||
else:
|
||||
val = value
|
||||
with open(finders.find('seating/seating-plan.schema.json'), 'r') as f:
|
||||
schema = json.loads(f.read())
|
||||
try:
|
||||
jsonschema.validate(val, schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(str(e)))
|
||||
|
||||
|
||||
class SeatingPlan(LoggedModel):
|
||||
"""
|
||||
Represents an abstract seating plan, without relation to any event.
|
||||
"""
|
||||
name = models.CharField(max_length=190, verbose_name=_('Name'))
|
||||
organizer = models.ForeignKey(Organizer, related_name='seating_plans', on_delete=models.CASCADE)
|
||||
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
|
||||
|
||||
Category = namedtuple('Categrory', 'name')
|
||||
RawSeat = namedtuple('Seat', 'name guid number row category')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def layout_data(self):
|
||||
return json.loads(self.layout)
|
||||
|
||||
@layout_data.setter
|
||||
def layout_data(self, v):
|
||||
self.layout = json.dumps(v)
|
||||
|
||||
def get_categories(self):
|
||||
return [
|
||||
self.Category(name=c['name'])
|
||||
for c in self.layout_data['categories']
|
||||
]
|
||||
|
||||
def iter_all_seats(self):
|
||||
for z in self.layout_data['zones']:
|
||||
for r in z['rows']:
|
||||
for s in r['seats']:
|
||||
yield self.RawSeat(
|
||||
number=s['seat_number'],
|
||||
guid=s['seat_guid'],
|
||||
name='{} {}'.format(r['row_number'], s['seat_number']), # TODO: Zone? Variable scheme?
|
||||
row=r['row_number'],
|
||||
category=s['category']
|
||||
)
|
||||
|
||||
|
||||
class SeatCategoryMapping(models.Model):
|
||||
"""
|
||||
Input seating plans have abstract "categories", such as "Balcony seat", etc. This model maps them to actual
|
||||
pretix product on a per-(sub)event level.
|
||||
"""
|
||||
event = models.ForeignKey(Event, related_name='seat_category_mappings', on_delete=models.CASCADE)
|
||||
subevent = models.ForeignKey(SubEvent, null=True, blank=True, related_name='seat_category_mappings', on_delete=models.CASCADE)
|
||||
layout_category = models.CharField(max_length=190)
|
||||
product = models.ForeignKey(Item, related_name='seat_category_mappings', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Seat(models.Model):
|
||||
"""
|
||||
This model is used to represent every single specific seat within an (sub)event that can be selected. It's mainly
|
||||
used for internal bookkeeping and not to be modified by users directly.
|
||||
"""
|
||||
event = models.ForeignKey(Event, related_name='seats', on_delete=models.CASCADE)
|
||||
subevent = models.ForeignKey(SubEvent, null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=190)
|
||||
seat_guid = models.CharField(max_length=190, db_index=True)
|
||||
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
|
||||
blocked = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None):
|
||||
from .orders import Order
|
||||
|
||||
if self.blocked:
|
||||
return False
|
||||
opqs = self.orderposition_set.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||
cpqs = self.cartposition_set.filter(expires__gte=now())
|
||||
if ignore_cart:
|
||||
cpqs = cpqs.exclude(pk=ignore_cart.pk)
|
||||
if ignore_orderpos:
|
||||
opqs = opqs.exclude(pk=ignore_orderpos.pk)
|
||||
return not opqs.exists() and not cpqs.exists()
|
||||
Reference in New Issue
Block a user