mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
* 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
112 lines
4.3 KiB
Python
112 lines
4.3 KiB
Python
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()
|