import os from decimal import Decimal from itertools import chain from django import forms from django.core.exceptions import ValidationError from django.db.models import Count, Prefetch, Q from django.utils.encoding import force_text from django.utils.formats import number_format from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from pretix.base.decimal import round_decimal from pretix.base.models import ItemVariation, Question from pretix.base.models.orders import InvoiceAddress, OrderPosition from pretix.base.templatetags.rich_text import rich_text from pretix.multidomain.urlreverse import eventreverse from pretix.presale.signals import contact_form_fields, question_form_fields class ContactForm(forms.Form): required_css_class = 'required' email = forms.EmailField(label=_('E-mail'), help_text=_('Make sure to enter a valid email address. We will send you an order ' 'confirmation including a link that you need in case you want to make ' 'modifications to your order or download your ticket later.'), widget=forms.EmailInput(attrs={'data-typocheck-target': '1'})) def __init__(self, *args, **kwargs): self.event = kwargs.pop('event') super().__init__(*args, **kwargs) if self.event.settings.order_email_asked_twice: self.fields['email_repeat'] = forms.EmailField( label=_('E-mail address (repeated)'), help_text=_('Please enter the same email address again to make sure you typed it correctly.') ) responses = contact_form_fields.send(self.event) for r, response in sorted(responses, key=lambda r: str(r[0])): for key, value in response.items(): # We need to be this explicit, since OrderedDict.update does not retain ordering self.fields[key] = value def clean(self): if self.event.settings.order_email_asked_twice: if self.cleaned_data['email'] != self.cleaned_data['email_repeat']: raise ValidationError(_('Please enter the same email address twice.')) class BusinessBooleanRadio(forms.RadioSelect): def __init__(self, attrs=None): choices = ( ('individual', _('Individual customer')), ('business', _('Business customer')), ) super().__init__(attrs, choices) def format_value(self, value): try: return {True: 'business', False: 'individual'}[value] except KeyError: return 'individual' def value_from_datadict(self, data, files, name): value = data.get(name) return { 'business': True, True: True, 'True': True, 'individual': False, 'False': False, False: False, }.get(value) class InvoiceAddressForm(forms.ModelForm): required_css_class = 'required' class Meta: model = InvoiceAddress fields = ('is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id') widgets = { 'is_business': BusinessBooleanRadio, 'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}), 'company': forms.TextInput(attrs={'data-typocheck-source': '1', 'data-display-dependency': '#id_is_business_1', 'data-required-if': '#id_is_business_1'}), 'name': forms.TextInput(attrs={'data-typocheck-source': '1', 'data-required-if': '#id_is_business_0'}), 'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}), } labels = { 'is_business': '' } def __init__(self, *args, **kwargs): self.event = event = kwargs.pop('event') super().__init__(*args, **kwargs) if not event.settings.invoice_address_vatid: del self.fields['vat_id'] if not event.settings.invoice_address_required: for k, f in self.fields.items(): f.required = False f.widget.is_required = False if 'required' in f.widget.attrs: del f.widget.attrs['required'] def clean(self): data = self.cleaned_data if not data['name'] and not data['company'] and self.event.settings.invoice_address_required: raise ValidationError(_('You need to provide either a company name or your name.')) class UploadedFileWidget(forms.ClearableFileInput): def __init__(self, *args, **kwargs): self.position = kwargs.pop('position') self.event = kwargs.pop('event') self.answer = kwargs.pop('answer') super().__init__(*args, **kwargs) class FakeFile: def __init__(self, file, position, event, answer): self.file = file self.position = position self.event = event self.answer = answer def __str__(self): return os.path.basename(self.file.name).split('.', 1)[-1] @property def url(self): if isinstance(self.position, OrderPosition): return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={ 'order': self.position.order.code, 'secret': self.position.order.secret, 'answer': self.answer.pk, }) else: return eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={ 'answer': self.answer.pk, }) def format_value(self, value): if self.is_initial(value): return self.FakeFile(value, self.position, self.event, self.answer) class QuestionsForm(forms.Form): """ This form class is responsible for asking order-related questions. This includes the attendee name for admission tickets, if the corresponding setting is enabled, as well as additional questions defined by the organizer. """ required_css_class = 'required' def __init__(self, *args, **kwargs): """ Takes two additional keyword arguments: :param cartpos: The cart position the form should be for :param event: The event this belongs to """ cartpos = self.cartpos = kwargs.pop('cartpos', None) orderpos = self.orderpos = kwargs.pop('orderpos', None) pos = cartpos or orderpos item = pos.item questions = list(item.questions.all()) event = kwargs.pop('event') super().__init__(*args, **kwargs) if item.admission and event.settings.attendee_names_asked: self.fields['attendee_name'] = forms.CharField( max_length=255, required=event.settings.attendee_names_required, label=_('Attendee name'), initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name), widget=forms.TextInput(attrs={'data-typocheck-source': '1'}), ) if item.admission and event.settings.attendee_emails_asked: self.fields['attendee_email'] = forms.EmailField( required=event.settings.attendee_emails_required, label=_('Attendee email'), initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email) ) for q in questions: # Do we already have an answer? Provide it as the initial value answers = [ a for a in (cartpos.answers.all() if cartpos else orderpos.answers.all()) if a.question_id == q.id ] if answers: initial = answers[0] else: initial = None if q.type == Question.TYPE_BOOLEAN: if q.required: # For some reason, django-bootstrap3 does not set the required attribute # itself. widget = forms.CheckboxInput(attrs={'required': 'required'}) else: widget = forms.CheckboxInput() if initial: initialbool = (initial.answer == "True") else: initialbool = False field = forms.BooleanField( label=q.question, required=q.required, initial=initialbool, widget=widget ) elif q.type == Question.TYPE_NUMBER: field = forms.DecimalField( label=q.question, required=q.required, initial=initial.answer if initial else None, min_value=Decimal('0.00') ) elif q.type == Question.TYPE_STRING: field = forms.CharField( label=q.question, required=q.required, initial=initial.answer if initial else None, ) elif q.type == Question.TYPE_TEXT: field = forms.CharField( label=q.question, required=q.required, widget=forms.Textarea, initial=initial.answer if initial else None, ) elif q.type == Question.TYPE_CHOICE: field = forms.ModelChoiceField( queryset=q.options.all(), label=q.question, required=q.required, widget=forms.RadioSelect, initial=initial.options.first() if initial else None, ) elif q.type == Question.TYPE_CHOICE_MULTIPLE: field = forms.ModelMultipleChoiceField( queryset=q.options.all(), label=q.question, required=q.required, widget=forms.CheckboxSelectMultiple, initial=initial.options.all() if initial else None, ) elif q.type == Question.TYPE_FILE: field = forms.FileField( label=q.question, required=q.required, initial=initial.file if initial else None, widget=UploadedFileWidget(position=pos, event=event, answer=initial) ) field.question = q if answers: # Cache the answer object for later use field.answer = answers[0] self.fields['question_%s' % q.id] = field responses = question_form_fields.send(sender=event, position=pos) data = pos.meta_info_data for r, response in sorted(responses, key=lambda r: str(r[0])): for key, value in response.items(): # We need to be this explicit, since OrderedDict.update does not retain ordering self.fields[key] = value value.initial = data.get('question_form_data', {}).get(key) class AddOnRadioSelect(forms.RadioSelect): option_template_name = 'pretixpresale/forms/addon_choice_option.html' def optgroups(self, name, value, attrs=None): attrs = attrs or {} groups = [] has_selected = False for index, (option_value, option_label, option_desc) in enumerate(chain(self.choices)): if option_value is None: option_value = '' if isinstance(option_label, (list, tuple)): raise TypeError('Choice groups are not supported here') group_name = None subgroup = [] groups.append((group_name, subgroup, index)) selected = ( force_text(option_value) in value and (has_selected is False or self.allow_multiple_selected) ) if selected is True and has_selected is False: has_selected = True attrs['description'] = option_desc subgroup.append(self.create_option( name, option_value, option_label, selected, index, subindex=None, attrs=attrs, )) return groups class AddOnVariationField(forms.ChoiceField): def valid_value(self, value): text_value = force_text(value) for k, v, d in self.choices: if value == k or text_value == force_text(k): return True return False class AddOnsForm(forms.Form): """ This form class is responsible for selecting add-ons to a product in the cart. """ def _label(self, event, item_or_variation, avail, override_price=None): if isinstance(item_or_variation, ItemVariation): variation = item_or_variation item = item_or_variation.item price = variation.price price_net = variation.net_price label = variation.value else: item = item_or_variation price = item.default_price price_net = item.default_price_net label = item.name if override_price: price = override_price tax_value = round_decimal(price * (1 - 100 / (100 + item.tax_rate))) price_net = price - tax_value if self.price_included: price = Decimal('0.00') if not price: n = '{name}'.format( name=label ) elif not item.tax_rate: n = _('{name} (+ {currency} {price})').format( name=label, currency=event.currency, price=number_format(price) ) elif event.settings.display_net_prices: n = _('{name} (+ {currency} {price} plus {taxes}% taxes)').format( name=label, currency=event.currency, price=number_format(price_net), taxes=number_format(item.tax_rate) ) else: n = _('{name} (+ {currency} {price} incl. {taxes}% taxes)').format( name=label, currency=event.currency, price=number_format(price), taxes=number_format(item.tax_rate) ) if avail[0] < 20: n += ' – {}'.format(_('SOLD OUT')) elif avail[0] < 100: n += ' – {}'.format(_('Currently unavailable')) return n def __init__(self, *args, **kwargs): """ Takes additional keyword arguments: :param category: The category to choose from :param event: The event this belongs to :param subevent: The event the parent cart position belongs to :param initial: The current set of add-ons :param quota_cache: A shared dictionary for quota caching :param item_cache: A shared dictionary for item/category caching """ category = kwargs.pop('category') event = kwargs.pop('event') subevent = kwargs.pop('subevent') current_addons = kwargs.pop('initial') quota_cache = kwargs.pop('quota_cache') item_cache = kwargs.pop('item_cache') self.price_included = kwargs.pop('price_included') super().__init__(*args, **kwargs) if subevent: item_price_override = subevent.item_price_overrides var_price_override = subevent.var_price_overrides else: item_price_override = {} var_price_override = {} ckey = '{}-{}'.format(subevent.pk if subevent else 0, category.pk) if ckey not in item_cache: # Get all items to possibly show items = category.items.filter( Q(active=True) & Q(Q(available_from__isnull=True) | Q(available_from__lte=now())) & Q(Q(available_until__isnull=True) | Q(available_until__gte=now())) & Q(hide_without_voucher=False) ).prefetch_related( Prefetch('quotas', to_attr='_subevent_quotas', queryset=event.quotas.filter(subevent=subevent)), Prefetch('variations', to_attr='available_variations', queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).prefetch_related( Prefetch('quotas', to_attr='_subevent_quotas', queryset=event.quotas.filter(subevent=subevent)) ).distinct()), ).annotate( quotac=Count('quotas'), has_variations=Count('variations') ).filter( quotac__gt=0 ).order_by('category__position', 'category_id', 'position', 'name') item_cache[ckey] = items else: items = item_cache[ckey] for i in items: if i.has_variations: choices = [('', _('no selection'), '')] for v in i.available_variations: cached_availability = v.check_quotas(subevent=subevent, _cache=quota_cache) if v._subevent_quotas: choices.append( (v.pk, self._label(event, v, cached_availability, override_price=var_price_override.get(v.pk)), v.description) ) field = AddOnVariationField( choices=choices, label=i.name, required=False, widget=AddOnRadioSelect, help_text=rich_text(str(i.description)), initial=current_addons.get(i.pk), ) if len(choices) > 1: self.fields['item_%s' % i.pk] = field else: if not i._subevent_quotas: continue cached_availability = i.check_quotas(subevent=subevent, _cache=quota_cache) field = forms.BooleanField( label=self._label(event, i, cached_availability, override_price=item_price_override.get(i.pk)), required=False, initial=i.pk in current_addons, help_text=rich_text(str(i.description)), ) self.fields['item_%s' % i.pk] = field