from django.db import transaction from django.db.models import Prefetch from django.utils.timezone import now from django.utils.translation import ugettext as _ from pretix.base.models import ( Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption, ) class CheckInError(Exception): def __init__(self, msg, code): self.msg = msg self.code = code super().__init__(msg) class RequiredQuestionsError(Exception): def __init__(self, msg, code, questions): self.msg = msg self.code = code self.questions = questions super().__init__(msg) def _save_answers(op, answers, given_answers): for q, a in given_answers.items(): if not a: if q in answers: answers[q].delete() else: continue if isinstance(a, QuestionOption): if q in answers: qa = answers[q] qa.answer = str(a.answer) qa.save() qa.options.clear() else: qa = op.answers.create(question=q, answer=str(a.answer)) qa.options.add(a) elif isinstance(a, list): if q in answers: qa = answers[q] qa.answer = ", ".join([str(o) for o in a]) qa.save() qa.options.clear() else: qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a])) qa.options.add(*a) else: if q in answers: qa = answers[q] qa.answer = str(a) qa.save() else: op.answers.create(question=q, answer=str(a)) @transaction.atomic def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False, ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True): """ Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is not valid at this time. :param op: The order position to check in :param clist: The order position to check in :param given_answers: A dictionary of questions mapped to validated, given answers :param force: When set to True, this will succeed even when the position is already checked in or when required questions are not filled out. :param ignore_unpaid: When set to True, this will succeed even when the order is unpaid. :param questions_supported: When set to False, questions are ignored :param nonce: A random nonce to prevent race conditions. :param datetime: The datetime of the checkin, defaults to now. """ dt = datetime or now() # Fetch order position with related objects op = OrderPosition.objects.select_related( 'item', 'variation', 'order', 'addon_to' ).prefetch_related( 'item__questions', Prefetch( 'item__questions', queryset=Question.objects.filter(ask_during_checkin=True), to_attr='checkin_questions' ), 'answers' ).get(pk=op.pk) answers = {a.question: a for a in op.answers.all()} require_answers = [] for q in op.item.checkin_questions: if q not in given_answers: require_answers.append(q) _save_answers(op, answers, given_answers) if not clist.all_products and op.item_id not in [i.pk for i in clist.limit_products.all()]: raise CheckInError( _('This order position has an invalid product for this check-in list.'), 'product' ) elif op.order.status != Order.STATUS_PAID and not force and not ( ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING ): raise CheckInError( _('This order is not marked as paid.'), 'unpaid' ) elif require_answers and not force and questions_supported: raise RequiredQuestionsError( _('You need to answer questions to complete this check-in.'), 'incomplete', require_answers ) else: ci, created = Checkin.objects.get_or_create(position=op, list=clist, defaults={ 'datetime': dt, 'nonce': nonce, }) if created or (nonce and nonce == ci.nonce): if created: op.order.log_action('pretix.event.checkin', data={ 'position': op.id, 'positionid': op.positionid, 'first': True, 'forced': op.order.status != Order.STATUS_PAID, 'datetime': dt, 'list': clist.pk }) else: if not force: raise CheckInError( _('This ticket has already been redeemed.'), 'already_redeemed', ) op.order.log_action('pretix.event.checkin', data={ 'position': op.id, 'positionid': op.positionid, 'first': False, 'forced': force, 'datetime': dt, 'list': clist.pk })