diff --git a/doc/development/api/restriction.rst b/doc/development/api/restriction.rst index 5e54f0c88..34569eda7 100644 --- a/doc/development/api/restriction.rst +++ b/doc/development/api/restriction.rst @@ -247,7 +247,7 @@ Our time restriction example looks like this:: from pretix.control.signals import restriction_formset from pretix.base.models import Item - from pretix.control.views.forms import ( + from pretix.control.forms import ( VariationsField, RestrictionInlineFormset, RestrictionForm ) diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 2c71a9e29..71de53ba2 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -4,8 +4,8 @@ import copy import uuid import random import time -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse from django.db import models from django.conf import settings from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin @@ -20,7 +20,6 @@ from pretix.base.settings import SettingsProxy import six from versions.models import Versionable as BaseVersionable from versions.models import VersionedForeignKey, VersionedManyToManyField, get_utc_now - from .types import VariationDict @@ -1177,7 +1176,7 @@ class VariationsField(VersionedManyToManyField): """ def formfield(self, **kwargs): - from pretix.control.views.forms import VariationsField as FVariationsField + from pretix.control.forms import VariationsField as FVariationsField from django.db.models.fields.related import RelatedField defaults = { 'form_class': FVariationsField, diff --git a/src/pretix/control/views/forms.py b/src/pretix/control/forms/__init__.py similarity index 100% rename from src/pretix/control/views/forms.py rename to src/pretix/control/forms/__init__.py diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py new file mode 100644 index 000000000..f1a5b3463 --- /dev/null +++ b/src/pretix/control/forms/event.py @@ -0,0 +1,208 @@ +from django.conf import settings +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from pytz import common_timezones + +from pretix.base.forms import VersionedModelForm, SettingsForm +from pretix.base.models import Event + + +class EventCreateForm(VersionedModelForm): + error_messages = { + 'duplicate_slug': _("You already used this slug for a different event. Please choose a new one."), + } + + class Meta: + model = Event + fields = [ + 'name', + 'slug', + 'currency', + 'date_from', + 'date_to', + 'presale_start', + 'presale_end' + ] + + def __init__(self, *args, **kwargs): + self.organizer = kwargs.pop('organizer') + super().__init__(*args, **kwargs) + + def clean_slug(self): + slug = self.cleaned_data['slug'] + if Event.objects.filter(slug=slug, organizer=self.organizer).exists(): + raise forms.ValidationError( + self.error_messages['duplicate_slug'], + code='duplicate_slug', + ) + return slug + + +class EventUpdateForm(VersionedModelForm): + def clean_slug(self): + return self.instance.slug + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['slug'].widget.attrs['readonly'] = 'readonly' + + class Meta: + model = Event + localized_fields = '__all__' + fields = [ + 'name', + 'slug', + 'currency', + 'date_from', + 'date_to', + 'presale_start', + 'presale_end', + ] + + +class EventSettingsForm(SettingsForm): + show_date_to = forms.BooleanField( + label=_("Show event end date"), + help_text=_("If disabled, only event's start date will be displayed to the public."), + required=False + ) + show_times = forms.BooleanField( + label=_("Show dates with time"), + help_text=_("If disabled, the event's start and end date will be displayed without the time of day."), + required=False + ) + payment_term_days = forms.IntegerField( + label=_('Payment term in days'), + help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."), + ) + show_items_outside_presale_period = forms.BooleanField( + label=_("Show items outside presale period"), + help_text=_("Show item details before presale has started and after presale has ended"), + required=False + ) + presale_start_show_date = forms.BooleanField( + label=_("Show start date"), + help_text=_("Show the presale start date before presale has started"), + required=False + ) + payment_term_last = forms.DateTimeField( + label=_('Last date of payments'), + help_text=_("The last date any payments are accepted. This has precedence over the number of " + "days configured above."), + required=False + ) + payment_term_accept_late = forms.BooleanField( + label=_('Accept late payments'), + help_text=_("Accept payments that come after the end of the order's payment term. " + "Payments will only be accepted if the regarding quotas have remaining " + "capacity. No payments will be accepted after the 'Last date of payments' " + "configured above."), + required=False + ) + last_order_modification_date = forms.DateTimeField( + label=_('Last date of modifications'), + help_text=_("The last date users can modify details of their orders, such as attendee names or " + "answers to questions."), + required=False + ) + timezone = forms.ChoiceField( + choices=((a, a) for a in common_timezones), + label=_("Default timezone"), + ) + locales = forms.MultipleChoiceField( + choices=settings.LANGUAGES, + label=_("Available langauges"), + ) + locale = forms.ChoiceField( + choices=settings.LANGUAGES, + label=_("Default language"), + ) + user_mail_required = forms.BooleanField( + label=_("Require e-mail adresses"), + help_text=_("Require all customers to provide an e-mail address."), + required=False + ) + attendee_names_asked = forms.BooleanField( + label=_("Ask for attendee names"), + help_text=_("Ask for a name for all tickets which include admission to the event."), + required=False + ) + attendee_names_required = forms.BooleanField( + label=_("Require attendee names"), + help_text=_("Require customers to fill in the names of all attendees."), + required=False + ) + max_items_per_order = forms.IntegerField( + min_value=1, + label=_("Maximum number of items per order") + ) + reservation_time = forms.IntegerField( + min_value=0, + label=_("Reservation period"), + help_text=_("The number of minutes the items in a user's card are reserved for this user."), + ) + mail_from = forms.EmailField( + label=_("Sender address"), + help_text=_("Sender address for outgoing e-mails") + ) + + +class ProviderForm(SettingsForm): + """ + This is a SettingsForm, but if fields are set to required=True, validation + errors are only raised if the payment method is enabled. + """ + + def __init__(self, *args, **kwargs): + self.settingspref = kwargs.pop('settingspref') + super().__init__(*args, **kwargs) + + def prepare_fields(self): + for k, v in self.fields.items(): + v._required = v.required + v.required = False + v.widget.is_required = False + + def clean(self): + cleaned_data = super().clean() + enabled = cleaned_data.get(self.settingspref + '_enabled') == 'True' + if not enabled: + return + for k, v in self.fields.items(): + val = cleaned_data.get(k) + if v._required and (val is None or val == ""): + print(enabled, k, v) + self.add_error(k, _('This field is required.')) + + +class TicketSettingsForm(SettingsForm): + ticket_download = forms.BooleanField( + label=_("Use feature"), + help_text=_("Use pretix to generate tickets for the user to download and print out."), + required=False + ) + ticket_download_date = forms.DateTimeField( + label=_("Download date"), + help_text=_("Ticket download will be offered after this date."), + required=True + ) + + def prepare_fields(self): + # See clean() + for k, v in self.fields.items(): + v._required = v.required + v.required = False + v.widget.is_required = False + + def clean(self): + # required=True files should only be required if the feature is enabled + cleaned_data = super().clean() + enabled = cleaned_data.get('ticket_download') == 'True' + if not enabled: + return + for k, v in self.fields.items(): + val = cleaned_data.get(k) + if v._required and (val is None or val == ""): + print(enabled, k, v) + self.add_error(k, _('This field is required.')) diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py new file mode 100644 index 000000000..8b486a750 --- /dev/null +++ b/src/pretix/control/forms/item.py @@ -0,0 +1,122 @@ +from django.forms import BooleanField +from django.utils.translation import ugettext_lazy as _ +from pretix.base.forms import VersionedModelForm, I18nModelForm +from pretix.base.models import ( + Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota, + Versionable) +from pretix.control.forms import TolerantFormsetModelForm, VariationsField + + +class CategoryForm(VersionedModelForm): + class Meta: + model = ItemCategory + localized_fields = '__all__' + fields = [ + 'name' + ] + + +class PropertyForm(VersionedModelForm): + class Meta: + model = Property + localized_fields = '__all__' + fields = [ + 'name', + ] + + +class PropertyValueForm(TolerantFormsetModelForm): + class Meta: + model = PropertyValue + localized_fields = '__all__' + fields = [ + 'value', + ] + + +class QuestionForm(VersionedModelForm): + class Meta: + model = Question + localized_fields = '__all__' + fields = [ + 'question', + 'type', + 'required', + ] + + +class QuotaForm(I18nModelForm): + def __init__(self, **kwargs): + items = kwargs['items'] + del kwargs['items'] + super().__init__(**kwargs) + + if hasattr(self, 'instance'): + active_items = set(self.instance.items.all()) + active_variations = set(self.instance.variations.all()) + else: + active_items = set() + active_variations = set() + + for item in items: + if len(item.properties.all()) > 0: + self.fields['item_%s' % item.identity] = VariationsField( + item, label=_("Activate for"), + required=False, + initial=active_variations + ) + self.fields['item_%s' % item.identity].set_item(item) + else: + self.fields['item_%s' % item.identity] = BooleanField( + label=_("Activate"), + required=False, + initial=(item in active_items) + ) + + def save(self, commit=True): + if self.instance.pk is not None and isinstance(self.instance, Versionable): + if self.has_changed(): + self.instance = self.instance.clone_shallow() + return super().save(commit) + + class Meta: + model = Quota + localized_fields = '__all__' + fields = [ + 'name', + 'size', + ] + + +class ItemFormGeneral(VersionedModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['category'].queryset = self.instance.event.categories.current.all() + self.fields['properties'].queryset = self.instance.event.properties.current.all() + self.fields['questions'].queryset = self.instance.event.questions.current.all() + + class Meta: + model = Item + localized_fields = '__all__' + fields = [ + 'category', + 'name', + 'active', + 'admission', + 'short_description', + 'long_description', + 'default_price', + 'tax_rate', + 'properties', + 'questions', + ] + + +class ItemVariationForm(VersionedModelForm): + class Meta: + model = ItemVariation + localized_fields = '__all__' + fields = [ + 'active', + 'default_price', + ] diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py new file mode 100644 index 000000000..4dfbd06d7 --- /dev/null +++ b/src/pretix/control/forms/orders.py @@ -0,0 +1,9 @@ +from pretix.base.forms import VersionedModelForm + +from pretix.base.models import Order + + +class ExtendForm(VersionedModelForm): + class Meta: + model = Order + fields = ['expires'] diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py new file mode 100644 index 000000000..45c5633d8 --- /dev/null +++ b/src/pretix/control/forms/organizer.py @@ -0,0 +1,33 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from pretix.base.forms import VersionedModelForm + +from pretix.base.models import Organizer + + +class OrganizerForm(VersionedModelForm): + error_messages = { + 'duplicate_slug': _("This slug is already in use. Please choose a different one."), + } + + class Meta: + model = Organizer + fields = ['name', 'slug'] + + def clean_slug(self): + slug = self.cleaned_data['slug'] + if Organizer.objects.filter(slug=slug).exists(): + raise forms.ValidationError( + self.error_messages['duplicate_slug'], + code='duplicate_slug', + ) + return slug + + +class OrganizerUpdateForm(OrganizerForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['slug'].widget.attrs['disabled'] = 'disabled' + + def clean_slug(self): + return self.instance.slug diff --git a/src/pretix/control/forms/user.py b/src/pretix/control/forms/user.py new file mode 100644 index 000000000..df56dc799 --- /dev/null +++ b/src/pretix/control/forms/user.py @@ -0,0 +1,88 @@ +from django import forms +from django.contrib.auth.hashers import check_password +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ +from pytz import common_timezones + +from pretix.base.models import User + + +class UserSettingsForm(forms.ModelForm): + error_messages = { + 'duplicate_identifier': _("There already is an account associated with this e-mail address. " + "Please choose a different one."), + 'pw_current': _("Please enter your current password if you want to change your e-mail " + "address or password."), + 'pw_current_wrong': _("The current password you entered was not correct."), + 'pw_mismatch': _("Please enter the same password twice"), + } + + old_pw = forms.CharField(max_length=255, + required=False, + label=_("Your current password"), + widget=forms.PasswordInput()) + new_pw = forms.CharField(max_length=255, + required=False, + label=_("New password"), + widget=forms.PasswordInput()) + new_pw_repeat = forms.CharField(max_length=255, + required=False, + label=_("Repeat new password"), + widget=forms.PasswordInput()) + timezone = forms.ChoiceField( + choices=((a, a) for a in common_timezones), + label=_("Default timezone"), + ) + + class Meta: + model = User + fields = [ + 'givenname', 'familyname', 'locale', 'timezone', 'email' + ] + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user') + super().__init__(*args, **kwargs) + + def clean_old_pw(self): + old_pw = self.cleaned_data.get('old_pw') + if old_pw and not check_password(old_pw, self.user.password): + raise forms.ValidationError( + self.error_messages['pw_current_wrong'], + code='pw_current_wrong', + ) + return old_pw + + def clean_email(self): + email = self.cleaned_data['email'] + if User.objects.filter(Q(identifier=email) & ~Q(pk=self.instance.pk)).exists(): + raise forms.ValidationError( + self.error_messages['duplicate_identifier'], + code='duplicate_identifier', + ) + return email + + def clean(self): + password1 = self.cleaned_data.get('new_pw') + password2 = self.cleaned_data.get('new_pw_repeat') + old_pw = self.cleaned_data.get('old_pw') + email = self.cleaned_data.get('email') + + if (password1 or email != self.user.email) and not old_pw: + raise forms.ValidationError( + self.error_messages['pw_current'], + code='pw_current', + ) + + if password1 and password1 != password2: + raise forms.ValidationError( + self.error_messages['pw_mismatch'], + code='pw_mismatch', + ) + + if password1: + self.instance.set_password(password1) + + self.instance.identifier = email + + return self.cleaned_data diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 317a8ab14..c65055333 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -1,134 +1,20 @@ from collections import OrderedDict -from django.conf import settings + from django.contrib import messages from django.shortcuts import render, redirect from django.utils.functional import cached_property from django.views.generic import FormView from django.views.generic.base import TemplateView from django.views.generic.detail import SingleObjectMixin -from django import forms from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse - -from pytz import common_timezones - -from pretix.base.forms import VersionedModelForm, SettingsForm +from pretix.control.forms.event import ProviderForm, TicketSettingsForm, EventSettingsForm, EventUpdateForm from pretix.base.models import Event from pretix.base.signals import register_payment_providers, register_ticket_outputs from pretix.control.permissions import EventPermissionRequiredMixin from . import UpdateView -class EventUpdateForm(VersionedModelForm): - - def clean_slug(self): - return self.instance.slug - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['slug'].widget.attrs['readonly'] = 'readonly' - - class Meta: - model = Event - localized_fields = '__all__' - fields = [ - 'name', - 'slug', - 'currency', - 'date_from', - 'date_to', - 'presale_start', - 'presale_end', - ] - - -class EventSettingsForm(SettingsForm): - show_date_to = forms.BooleanField( - label=_("Show event end date"), - help_text=_("If disabled, only event's start date will be displayed to the public."), - required=False - ) - show_times = forms.BooleanField( - label=_("Show dates with time"), - help_text=_("If disabled, the event's start and end date will be displayed without the time of day."), - required=False - ) - payment_term_days = forms.IntegerField( - label=_('Payment term in days'), - help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."), - ) - show_items_outside_presale_period = forms.BooleanField( - label=_("Show items outside presale period"), - help_text=_("Show item details before presale has started and after presale has ended"), - required=False - ) - presale_start_show_date = forms.BooleanField( - label=_("Show start date"), - help_text=_("Show the presale start date before presale has started"), - required=False - ) - payment_term_last = forms.DateTimeField( - label=_('Last date of payments'), - help_text=_("The last date any payments are accepted. This has precedence over the number of " - "days configured above."), - required=False - ) - payment_term_accept_late = forms.BooleanField( - label=_('Accept late payments'), - help_text=_("Accept payments that come after the end of the order's payment term. " - "Payments will only be accepted if the regarding quotas have remaining " - "capacity. No payments will be accepted after the 'Last date of payments' " - "configured above."), - required=False - ) - last_order_modification_date = forms.DateTimeField( - label=_('Last date of modifications'), - help_text=_("The last date users can modify details of their orders, such as attendee names or " - "answers to questions."), - required=False - ) - timezone = forms.ChoiceField( - choices=((a, a) for a in common_timezones), - label=_("Default timezone"), - ) - locales = forms.MultipleChoiceField( - choices=settings.LANGUAGES, - label=_("Available langauges"), - ) - locale = forms.ChoiceField( - choices=settings.LANGUAGES, - label=_("Default language"), - ) - user_mail_required = forms.BooleanField( - label=_("Require e-mail adresses"), - help_text=_("Require all customers to provide an e-mail address."), - required=False - ) - attendee_names_asked = forms.BooleanField( - label=_("Ask for attendee names"), - help_text=_("Ask for a name for all tickets which include admission to the event."), - required=False - ) - attendee_names_required = forms.BooleanField( - label=_("Require attendee names"), - help_text=_("Require customers to fill in the names of all attendees."), - required=False - ) - max_items_per_order = forms.IntegerField( - min_value=1, - label=_("Maximum number of items per order") - ) - reservation_time = forms.IntegerField( - min_value=0, - label=_("Reservation period"), - help_text=_("The number of minutes the items in a user's card are reserved for this user."), - ) - mail_from = forms.EmailField( - label=_("Sender address"), - help_text=_("Sender address for outgoing e-mails") - ) - - class EventUpdate(EventPermissionRequiredMixin, UpdateView): model = Event form_class = EventUpdateForm @@ -217,34 +103,6 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin }) + '?success=true' -class ProviderForm(SettingsForm): - """ - This is a SettingsForm, but if fields are set to required=True, validation - errors are only raised if the payment method is enabled. - """ - - def __init__(self, *args, **kwargs): - self.settingspref = kwargs.pop('settingspref') - super().__init__(*args, **kwargs) - - def prepare_fields(self): - for k, v in self.fields.items(): - v._required = v.required - v.required = False - v.widget.is_required = False - - def clean(self): - cleaned_data = super().clean() - enabled = cleaned_data.get(self.settingspref + '_enabled') == 'True' - if not enabled: - return - for k, v in self.fields.items(): - val = cleaned_data.get(k) - if v._required and (val is None or val == ""): - print(enabled, k, v) - self.add_error(k, _('This field is required.')) - - class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin): model = Event context_object_name = 'event' @@ -306,38 +164,6 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi }) + '?success=true' -class TicketSettingsForm(SettingsForm): - ticket_download = forms.BooleanField( - label=_("Use feature"), - help_text=_("Use pretix to generate tickets for the user to download and print out."), - required=False - ) - ticket_download_date = forms.DateTimeField( - label=_("Download date"), - help_text=_("Ticket download will be offered after this date."), - required=True - ) - - def prepare_fields(self): - # See clean() - for k, v in self.fields.items(): - v._required = v.required - v.required = False - v.widget.is_required = False - - def clean(self): - # required=True files should only be required if the feature is enabled - cleaned_data = super().clean() - enabled = cleaned_data.get('ticket_download') == 'True' - if not enabled: - return - for k, v in self.fields.items(): - val = cleaned_data.get(k) - if v._required and (val is None or val == ""): - print(enabled, k, v) - self.add_error(k, _('This field is required.')) - - class TicketSettings(EventPermissionRequiredMixin, FormView): model = Event form_class = TicketSettingsForm diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index bb3a91ff1..983a73b8a 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -1,9 +1,8 @@ from itertools import product + from django.contrib import messages from django.db import transaction -from django.forms import BooleanField from django.utils.functional import cached_property - from django.views.generic import ListView from django.views.generic.edit import DeleteView from django.views.generic.base import TemplateView @@ -13,13 +12,13 @@ from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404 from django.shortcuts import redirect from django.forms.models import inlineformset_factory from django.utils.translation import ugettext_lazy as _ -from pretix.base.forms import VersionedModelForm, I18nModelForm - from pretix.base.models import ( - Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota, - Versionable) + Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota) +from pretix.control.forms.item import ItemVariationForm, QuotaForm, QuestionForm, PropertyForm, PropertyValueForm, \ + CategoryForm +from pretix.control.forms.item import ItemFormGeneral from pretix.control.permissions import EventPermissionRequiredMixin, event_permission_required -from pretix.control.views.forms import TolerantFormsetModelForm, VariationsField, I18nInlineFormSet +from pretix.control.forms import VariationsField, I18nInlineFormSet from pretix.control.signals import restriction_formset from . import UpdateView, CreateView @@ -81,16 +80,6 @@ def item_move_down(request, organizer, event, item): event=request.event.slug) -class CategoryForm(VersionedModelForm): - - class Meta: - model = ItemCategory - localized_fields = '__all__' - fields = [ - 'name' - ] - - class CategoryDelete(EventPermissionRequiredMixin, DeleteView): model = ItemCategory form_class = CategoryForm @@ -235,24 +224,6 @@ class PropertyList(ListView): ) -class PropertyForm(VersionedModelForm): - class Meta: - model = Property - localized_fields = '__all__' - fields = [ - 'name', - ] - - -class PropertyValueForm(TolerantFormsetModelForm): - class Meta: - model = PropertyValue - localized_fields = '__all__' - fields = [ - 'value', - ] - - class PropertyUpdate(EventPermissionRequiredMixin, UpdateView): model = Property form_class = PropertyForm @@ -422,18 +393,6 @@ class QuestionList(ListView): return self.request.event.questions.current.all() -class QuestionForm(VersionedModelForm): - - class Meta: - model = Question - localized_fields = '__all__' - fields = [ - 'question', - 'type', - 'required', - ] - - class QuestionDelete(EventPermissionRequiredMixin, DeleteView): model = Question template_name = 'pretixcontrol/items/question_delete.html' @@ -524,52 +483,7 @@ class QuotaList(ListView): ).prefetch_related("items") -class QuotaForm(I18nModelForm): - - def __init__(self, **kwargs): - items = kwargs['items'] - del kwargs['items'] - super().__init__(**kwargs) - - if hasattr(self, 'instance'): - active_items = set(self.instance.items.all()) - active_variations = set(self.instance.variations.all()) - else: - active_items = set() - active_variations = set() - - for item in items: - if len(item.properties.all()) > 0: - self.fields['item_%s' % item.identity] = VariationsField( - item, label=_("Activate for"), - required=False, - initial=active_variations - ) - self.fields['item_%s' % item.identity].set_item(item) - else: - self.fields['item_%s' % item.identity] = BooleanField( - label=_("Activate"), - required=False, - initial=(item in active_items) - ) - - def save(self, commit=True): - if self.instance.pk is not None and isinstance(self.instance, Versionable): - if self.has_changed(): - self.instance = self.instance.clone_shallow() - return super().save(commit) - - class Meta: - model = Quota - localized_fields = '__all__' - fields = [ - 'name', - 'size', - ] - - class QuotaEditorMixin: - @cached_property def items(self) -> "List[Item]": return list(self.request.event.items.all().prefetch_related("properties", "variations")) @@ -606,8 +520,8 @@ class QuotaEditorMixin: selected_variations.append(v) if data: # and item not in items: self.object.items.add(item) - # elif not data and item in items: - # self.object.items.remove(item) + # elif not data and item in items: + # self.object.items.remove(item) self.object.variations.add(*[v for v in selected_variations]) # if v not in variations]) # self.object.variations.remove(*[v for v in variations if v not in selected_variations]) @@ -708,31 +622,6 @@ class ItemDetailMixin(SingleObjectMixin): raise Http404(_("The requested item does not exist.")) -class ItemFormGeneral(VersionedModelForm): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['category'].queryset = self.instance.event.categories.current.all() - self.fields['properties'].queryset = self.instance.event.properties.current.all() - self.fields['questions'].queryset = self.instance.event.questions.current.all() - - class Meta: - model = Item - localized_fields = '__all__' - fields = [ - 'category', - 'name', - 'active', - 'admission', - 'short_description', - 'long_description', - 'default_price', - 'tax_rate', - 'properties', - 'questions', - ] - - class ItemCreate(EventPermissionRequiredMixin, CreateView): form_class = ItemFormGeneral template_name = 'pretixcontrol/item/index.html' @@ -776,19 +665,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie return super().form_valid(form) -class ItemVariationForm(VersionedModelForm): - - class Meta: - model = ItemVariation - localized_fields = '__all__' - fields = [ - 'active', - 'default_price', - ] - - class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView): - permission = 'can_change_items' def __init__(self, *args, **kwargs): @@ -952,7 +829,6 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView): - permission = 'can_change_items' template_name = 'pretixcontrol/item/restrictions.html' diff --git a/src/pretix/control/views/main.py b/src/pretix/control/views/main.py index d8f6c05ed..cacc18d0a 100644 --- a/src/pretix/control/views/main.py +++ b/src/pretix/control/views/main.py @@ -1,12 +1,11 @@ -from django import forms from django.contrib import messages from django.core.urlresolvers import reverse from django.shortcuts import render from django.views.generic import ListView, CreateView, TemplateView from django.utils.translation import ugettext_lazy as _ -from pretix.base.forms import VersionedModelForm -from pretix.base.models import Event, EventPermission, Organizer, OrganizerPermission +from pretix.base.models import Event, EventPermission, OrganizerPermission +from pretix.control.forms.event import EventCreateForm from pretix.control.permissions import OrganizerPermissionRequiredMixin @@ -28,37 +27,6 @@ def index(request): return render(request, 'pretixcontrol/base.html', {}) -class EventForm(VersionedModelForm): - error_messages = { - 'duplicate_slug': _("You already used this slug for a different event. Please choose a new one."), - } - - class Meta: - model = Event - fields = [ - 'name', - 'slug', - 'currency', - 'date_from', - 'date_to', - 'presale_start', - 'presale_end' - ] - - def __init__(self, *args, **kwargs): - self.organizer = kwargs.pop('organizer') - super().__init__(*args, **kwargs) - - def clean_slug(self): - slug = self.cleaned_data['slug'] - if Event.objects.filter(slug=slug, organizer=self.organizer).exists(): - raise forms.ValidationError( - self.error_messages['duplicate_slug'], - code='duplicate_slug', - ) - return slug - - class EventCreateStart(TemplateView): template_name = 'pretixcontrol/events/start.html' @@ -74,7 +42,7 @@ class EventCreateStart(TemplateView): class EventCreate(OrganizerPermissionRequiredMixin, CreateView): model = Event - form_class = EventForm + form_class = EventCreateForm template_name = 'pretixcontrol/events/create.html' context_object_name = 'event' permission = 'can_create_events' diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index b74eb7fa0..7b756fcee 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1,6 +1,6 @@ from itertools import groupby + from django.contrib import messages -from django.core.urlresolvers import reverse from django.db.models import Count from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -8,10 +8,9 @@ from django.http import HttpResponse from django.shortcuts import redirect, render from django.utils.functional import cached_property from django.views.generic import ListView, DetailView, TemplateView -from pretix.base.forms import VersionedModelForm - from pretix.base.models import Order, Quota, OrderPosition from pretix.base.signals import register_payment_providers +from pretix.control.forms.orders import ExtendForm from pretix.control.permissions import EventPermissionRequiredMixin @@ -51,12 +50,6 @@ class OrderView(EventPermissionRequiredMixin, DetailView): return provider -class ExtendForm(VersionedModelForm): - class Meta: - model = Order - fields = ['expires'] - - class OrderDetail(OrderView): template_name = 'pretixcontrol/order/index.html' permission = 'can_view_orders' @@ -84,8 +77,8 @@ class OrderDetail(OrderView): # We do this by list manipulations instead of a GROUP BY query, as # Django is unable to join related models in a .values() query def keyfunc(pos): - if ((pos.item.admission and self.request.event.settings.attendee_names_asked) - or pos.item.questions.all()): + if (pos.item.admission and self.request.event.settings.attendee_names_asked) \ + or pos.item.questions.all(): return pos.id, "", "", "" return "", pos.item_id, pos.variation_id, pos.price @@ -212,33 +205,38 @@ class OverView(EventPermissionRequiredMixin, TemplateView): num_total = { (p['item'], p['variation']): p['cnt'] - for p in OrderPosition.objects.current.filter( - order__event=self.request.event - ).values('item', 'variation').annotate(cnt=Count('id')) + for p in + OrderPosition.objects.current.filter(order__event=self.request.event).values('item', 'variation').annotate( + cnt=Count('id')) } num_cancelled = { (p['item'], p['variation']): p['cnt'] - for p in OrderPosition.objects.current.filter( - order__event=self.request.event, order__status=Order.STATUS_CANCELLED - ).values('item', 'variation').annotate(cnt=Count('id')) + for p in (OrderPosition.objects.current + .filter(order__event=self.request.event, order__status=Order.STATUS_CANCELLED) + .values('item', 'variation') + .annotate(cnt=Count('id'))) } num_refunded = { (p['item'], p['variation']): p['cnt'] - for p in OrderPosition.objects.current.filter( - order__event=self.request.event, order__status=Order.STATUS_REFUNDED - ).values('item', 'variation').annotate(cnt=Count('id')) + for p in (OrderPosition.objects.current + .filter(order__event=self.request.event, order__status=Order.STATUS_REFUNDED) + .values('item', 'variation') + .annotate(cnt=Count('id'))) } num_pending = { (p['item'], p['variation']): p['cnt'] - for p in OrderPosition.objects.current.filter( - order__event=self.request.event, order__status__in=(Order.STATUS_PENDING, Order.STATUS_EXPIRED) - ).values('item', 'variation').annotate(cnt=Count('id')) + for p in (OrderPosition.objects.current + .filter(order__event=self.request.event, + order__status__in=(Order.STATUS_PENDING, Order.STATUS_EXPIRED)) + .values('item', 'variation') + .annotate(cnt=Count('id'))) } num_paid = { (p['item'], p['variation']): p['cnt'] - for p in OrderPosition.objects.current.filter( - order__event=self.request.event, order__status=Order.STATUS_PAID - ).values('item', 'variation').annotate(cnt=Count('id')) + for p in (OrderPosition.objects.current + .filter(order__event=self.request.event, order__status=Order.STATUS_PAID) + .values('item', 'variation') + .annotate(cnt=Count('id'))) } for item in items: @@ -260,12 +258,16 @@ class OverView(EventPermissionRequiredMixin, TemplateView): item.num_paid = sum(var.num_paid for var in item.all_variations) # Regroup those by category - ctx['items_by_category'] = sorted([ - # a group is a tuple of a category and a list of items - (cat, [i for i in items if i.category == cat]) - for cat in set([i.category for i in items]) # insert categories into a set for uniqueness - # a set is unsorted, so sort again by category - ], key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "")) + ctx['items_by_category'] = sorted( + [ + # a group is a tuple of a category and a list of items + (cat, [i for i in items if i.category == cat]) + for cat in set([i.category for i in items]) + # insert categories into a set for uniqueness + # a set is unsorted, so sort again by category + ], + key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "") + ) for c in ctx['items_by_category']: c[0].num_total = sum(item.num_total for item in c[1]) c[0].num_pending = sum(item.num_pending for item in c[1]) diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index 2c4327f18..53250be2c 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -1,12 +1,11 @@ -from django import forms from django.contrib import messages from django.core.urlresolvers import reverse from django.http import HttpResponseForbidden from django.utils.translation import ugettext_lazy as _ from django.views.generic import ListView, UpdateView, CreateView -from pretix.base.forms import VersionedModelForm from pretix.base.models import Organizer, OrganizerPermission +from pretix.control.forms.organizer import OrganizerUpdateForm, OrganizerForm from pretix.control.permissions import OrganizerPermissionRequiredMixin @@ -25,35 +24,6 @@ class OrganizerList(ListView): ) -class OrganizerForm(VersionedModelForm): - error_messages = { - 'duplicate_slug': _("This slug is already in use. Please choose a different one."), - } - - class Meta: - model = Organizer - fields = ['name', 'slug'] - - def clean_slug(self): - slug = self.cleaned_data['slug'] - if Organizer.objects.filter(slug=slug).exists(): - raise forms.ValidationError( - self.error_messages['duplicate_slug'], - code='duplicate_slug', - ) - return slug - - -class OrganizerUpdateForm(OrganizerForm): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['slug'].widget.attrs['disabled'] = 'disabled' - - def clean_slug(self): - return self.instance.slug - - class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView): model = Organizer form_class = OrganizerUpdateForm diff --git a/src/pretix/control/views/user.py b/src/pretix/control/views/user.py index 7a80b0ead..5c1fdb02c 100644 --- a/src/pretix/control/views/user.py +++ b/src/pretix/control/views/user.py @@ -1,100 +1,13 @@ -from django import forms from django.contrib import messages from django.contrib.auth import update_session_auth_hash -from django.contrib.auth.hashers import check_password from django.core.urlresolvers import reverse -from django.db.models import Q from django.views.generic import UpdateView from django.utils.translation import ugettext_lazy as _ -from pytz import common_timezones +from pretix.control.forms.user import UserSettingsForm from pretix.base.models import User -class UserSettingsForm(forms.ModelForm): - error_messages = { - 'duplicate_identifier': _("There already is an account associated with this e-mail address. " - "Please choose a different one."), - 'pw_current': _("Please enter your current password if you want to change your e-mail " - "address or password."), - 'pw_current_wrong': _("The current password you entered was not correct."), - 'pw_mismatch': _("Please enter the same password twice"), - } - - old_pw = forms.CharField(max_length=255, - required=False, - label=_("Your current password"), - widget=forms.PasswordInput()) - new_pw = forms.CharField(max_length=255, - required=False, - label=_("New password"), - widget=forms.PasswordInput()) - new_pw_repeat = forms.CharField(max_length=255, - required=False, - label=_("Repeat new password"), - widget=forms.PasswordInput()) - timezone = forms.ChoiceField( - choices=((a, a) for a in common_timezones), - label=_("Default timezone"), - ) - - class Meta: - model = User - fields = [ - 'givenname', 'familyname', 'locale', 'timezone', 'email' - ] - - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') - super().__init__(*args, **kwargs) - - def clean_old_pw(self): - old_pw = self.cleaned_data.get('old_pw') - if old_pw and not check_password(old_pw, self.user.password): - raise forms.ValidationError( - self.error_messages['pw_current_wrong'], - code='pw_current_wrong', - ) - return old_pw - - def clean_email(self): - email = self.cleaned_data['email'] - if User.objects.filter( - Q(identifier=email) - & ~Q(pk=self.instance.pk) - ).exists(): - raise forms.ValidationError( - self.error_messages['duplicate_identifier'], - code='duplicate_identifier', - ) - return email - - def clean(self): - password1 = self.cleaned_data.get('new_pw') - password2 = self.cleaned_data.get('new_pw_repeat') - old_pw = self.cleaned_data.get('old_pw') - email = self.cleaned_data.get('email') - - if (password1 or email != self.user.email) and not old_pw: - raise forms.ValidationError( - self.error_messages['pw_current'], - code='pw_current', - ) - - if password1 and password1 != password2: - raise forms.ValidationError( - self.error_messages['pw_mismatch'], - code='pw_mismatch', - ) - - if password1: - self.instance.set_password(password1) - - self.instance.identifier = email - - return self.cleaned_data - - class UserSettings(UpdateView): model = User form_class = UserSettingsForm diff --git a/src/pretix/plugins/timerestriction/signals.py b/src/pretix/plugins/timerestriction/signals.py index 53501c38d..760aa095e 100644 --- a/src/pretix/plugins/timerestriction/signals.py +++ b/src/pretix/plugins/timerestriction/signals.py @@ -5,7 +5,7 @@ from django.forms.models import inlineformset_factory from pretix.base.signals import determine_availability from pretix.base.models import Item -from pretix.control.views.forms import RestrictionInlineFormset, RestrictionForm +from pretix.control.forms import RestrictionInlineFormset, RestrictionForm from pretix.control.signals import restriction_formset from .models import TimeRestriction diff --git a/src/pretix/presale/forms/__init__.py b/src/pretix/presale/forms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pretix/presale/forms/auth.py b/src/pretix/presale/forms/auth.py new file mode 100644 index 000000000..a94dc00da --- /dev/null +++ b/src/pretix/presale/forms/auth.py @@ -0,0 +1,220 @@ +from django.contrib.auth import authenticate +from django.core.validators import RegexValidator +from django import forms +from django.forms import Form +from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from pretix.base.models import User + + +class LoginForm(BaseAuthenticationForm): + username = forms.CharField( + label=_('Username'), + help_text=( + _('If you registered for multiple events, your username is your email address.') + if settings.PRETIX_GLOBAL_REGISTRATION + else None + ) + ) + password = forms.CharField( + label=_('Password'), + widget=forms.PasswordInput + ) + + error_messages = { + 'invalid_login': _("Please enter a correct username and password."), + 'inactive': _("This account is inactive."), + } + + def __init__(self, request=None, *args, **kwargs): + self.request = request + self.user_cache = None + super(forms.Form, self).__init__(*args, **kwargs) + + def clean(self): + username = self.cleaned_data.get('username') + password = self.cleaned_data.get('password') + + if username and password: + if '@' in username: + identifier = username.lower() + else: + identifier = "%s@%s.event.pretix" % (username, self.request.event.identity) + self.user_cache = authenticate(identifier=identifier, + password=password) + if self.user_cache is None: + raise forms.ValidationError( + self.error_messages['invalid_login'], + code='invalid_login', + ) + else: + self.confirm_login_allowed(self.user_cache) + + return self.cleaned_data + + +class GlobalRegistrationForm(forms.Form): + error_messages = { + 'duplicate_email': _("You already registered with that e-mail address, please use the login form."), + 'pw_mismatch': _("Please enter the same password twice"), + } + email = forms.EmailField( + label=_('Email address'), + required=True + ) + password = forms.CharField( + label=_('Password'), + widget=forms.PasswordInput, + required=True + ) + password_repeat = forms.CharField( + label=_('Repeat password'), + widget=forms.PasswordInput + ) + + def clean(self): + password1 = self.cleaned_data.get('password') + password2 = self.cleaned_data.get('password_repeat') + + if password1 and password1 != password2: + raise forms.ValidationError( + self.error_messages['pw_mismatch'], + code='pw_mismatch', + ) + + return self.cleaned_data + + def clean_email(self): + email = self.cleaned_data['email'] + if User.objects.filter(identifier=email).exists(): + raise forms.ValidationError( + self.error_messages['duplicate_email'], + code='duplicate_email', + ) + return email + + +class LocalRegistrationForm(forms.Form): + error_messages = { + 'invalid_username': _("Please only use characters, numbers or ./+/-/_ in your username."), + 'duplicate_username': _("This username is already taken. Please choose a different one."), + 'pw_mismatch': _("Please enter the same password twice"), + } + username = forms.CharField( + label=_('Username'), + validators=[ + RegexValidator( + regex='^[a-zA-Z0-9\.+\-_]*$', + code='invalid_username', + message=error_messages['invalid_username'] + ), + ], + required=True + ) + email = forms.EmailField( + label=_('E-mail address'), + required=False + ) + password = forms.CharField( + label=_('Password'), + widget=forms.PasswordInput, + required=True + ) + password_repeat = forms.CharField( + label=_('Repeat password'), + widget=forms.PasswordInput + ) + + def __init__(self, request, *args, **kwargs): + super().__init__(*args, **kwargs) + self.request = request + self.fields['email'].required = request.event.settings.user_mail_required + + def clean(self): + password1 = self.cleaned_data.get('password') + password2 = self.cleaned_data.get('password_repeat') + + if password1 and password1 != password2: + raise forms.ValidationError( + self.error_messages['pw_mismatch'], + code='pw_mismatch', + ) + + return self.cleaned_data + + def clean_username(self): + username = self.cleaned_data['username'] + if User.objects.filter(event=self.request.event, username=username).exists(): + raise forms.ValidationError( + self.error_messages['duplicate_username'], + code='duplicate_username', + ) + return username + + +class PasswordRecoverForm(Form): + error_messages = { + 'pw_mismatch': _("Please enter the same password twice"), + } + password = forms.CharField( + label=_('Password'), + widget=forms.PasswordInput, + required=True + ) + password_repeat = forms.CharField( + label=_('Repeat password'), + widget=forms.PasswordInput + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def clean(self): + password1 = self.cleaned_data.get('password') + password2 = self.cleaned_data.get('password_repeat') + + if password1 and password1 != password2: + raise forms.ValidationError( + self.error_messages['pw_mismatch'], + code='pw_mismatch', + ) + + return self.cleaned_data + + +class PasswordForgotForm(Form): + username = forms.CharField( + label=_('Username or E-mail'), + ) + + def __init__(self, event, *args, **kwargs): + self.event = event + super().__init__(*args, **kwargs) + + def clean_username(self): + username = self.cleaned_data['username'] + try: + self.cleaned_data['user'] = User.objects.get( + identifier=username, event__isnull=True + ) + return username + except User.DoesNotExist: + pass + try: + self.cleaned_data['user'] = User.objects.get( + username=username, event=self.event + ) + return username + except User.DoesNotExist: + pass + try: + self.cleaned_data['user'] = User.objects.get( + email=username, event=self.event + ) + return username + except: + raise forms.ValidationError( + _("We are unable to find a user matching the data you provided."), + code='unknown_user', + ) diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py new file mode 100644 index 000000000..d278825b8 --- /dev/null +++ b/src/pretix/presale/forms/checkout.py @@ -0,0 +1,71 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from pretix.base.models import Question + + +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. + """ + + 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 = kwargs.pop('cartpos', None) + orderpos = kwargs.pop('orderpos', None) + item = cartpos.item if cartpos else orderpos.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) + ) + + 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.identity + ] + if answers: + initial = answers[0].answer + else: + initial = None + if q.type == Question.TYPE_BOOLEAN: + field = forms.BooleanField( + label=q.question, required=q.required, + initial=initial + ) + elif q.type == Question.TYPE_NUMBER: + field = forms.DecimalField( + label=q.question, required=q.required, + initial=initial + ) + elif q.type == Question.TYPE_STRING: + field = forms.CharField( + label=q.question, required=q.required, + initial=initial + ) + elif q.type == Question.TYPE_TEXT: + field = forms.CharField( + label=q.question, required=q.required, + widget=forms.Textarea, + initial=initial + ) + field.question = q + if answers: + # Cache the answer object for later use + field.answer = answers[0] + self.fields['question_%s' % q.identity] = field diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py index 584e9906d..b6664f7bf 100644 --- a/src/pretix/presale/views/checkout.py +++ b/src/pretix/presale/views/checkout.py @@ -1,9 +1,9 @@ from datetime import timedelta, datetime + from django.contrib import messages from django.core.urlresolvers import reverse from django.db import transaction from django.db.models import Q, Sum -from django import forms from django.http import HttpRequest from django.shortcuts import redirect from django.utils.functional import cached_property @@ -11,82 +11,13 @@ from django.utils.timezone import now from django.views.generic import TemplateView from django.utils.translation import ugettext_lazy as _ from pretix.base.mail import mail -from pretix.base.models import CartPosition, Question, QuestionAnswer, Quota, Order, OrderPosition +from pretix.base.models import CartPosition, QuestionAnswer, Quota, Order, OrderPosition from pretix.base.signals import register_payment_providers - +from pretix.presale.forms.checkout import QuestionsForm from pretix.presale.views import EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin -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. - """ - - 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 = kwargs.pop('cartpos', None) - orderpos = kwargs.pop('orderpos', None) - item = cartpos.item if cartpos else orderpos.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) - ) - - 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.identity - ] - if answers: - initial = answers[0].answer - else: - initial = None - if q.type == Question.TYPE_BOOLEAN: - field = forms.BooleanField( - label=q.question, required=q.required, - initial=initial - ) - elif q.type == Question.TYPE_NUMBER: - field = forms.DecimalField( - label=q.question, required=q.required, - initial=initial - ) - elif q.type == Question.TYPE_STRING: - field = forms.CharField( - label=q.question, required=q.required, - initial=initial - ) - elif q.type == Question.TYPE_TEXT: - field = forms.CharField( - label=q.question, required=q.required, - widget=forms.Textarea, - initial=initial - ) - field.question = q - if answers: - # Cache the answer object for later use - field.answer = answers[0] - self.fields['question_%s' % q.identity] = field - - class CheckoutView(TemplateView): - def get_payment_url(self): return reverse('presale:event.checkout.payment', kwargs={ 'event': self.request.event.slug, @@ -120,7 +51,6 @@ class CheckoutView(TemplateView): class QuestionsViewMixin: - @cached_property def forms(self): """ @@ -358,7 +288,8 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch if not self.request.event.presale_end or now() < self.request.event.presale_end: cp = cp.clone() cartpos[i] = cp - cp.expires = now() + timedelta(minutes=self.request.event.settings.get('reservation_time', as_type=int)) + cp.expires = now() + timedelta( + minutes=self.request.event.settings.get('reservation_time', as_type=int)) cp.save() else: cp.delete() # Sorry! diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 7714258e2..eeca529dd 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -1,23 +1,22 @@ import json + from django.contrib import messages from django.contrib.auth import authenticate, logout from django.core import signing from django.core.signing import SignatureExpired, BadSignature from django.core.urlresolvers import reverse -from django.core.validators import RegexValidator from django.db.models import Count -from django import forms -from django.forms import Form from django.shortcuts import redirect from django.utils.functional import cached_property -from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm from django.contrib.auth import login from django.views.generic import TemplateView, View from django.utils.translation import ugettext_lazy as _ from django.conf import settings from pretix.base.mail import mail from pretix.base.models import User - +from pretix.presale.forms.auth import GlobalRegistrationForm, LocalRegistrationForm, PasswordForgotForm, \ + PasswordRecoverForm +from pretix.presale.forms.auth import LoginForm from pretix.presale.views import EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin from pretix.presale.views.cart import CartAdd @@ -59,162 +58,21 @@ class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): items = [item for item in items if len(item.available_variations) > 0] # Regroup those by category - context['items_by_category'] = sorted([ - # a group is a tuple of a category and a list of items - (cat, [i for i in items if i.category == cat]) - for cat in set([i.category for i in items]) # insert categories into a set for uniqueness - # a set is unsorted, so sort again by category - ], key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "")) + context['items_by_category'] = sorted( + [ + # a group is a tuple of a category and a list of items + (cat, [i for i in items if i.category == cat]) + for cat in set([i.category for i in items]) + # insert categories into a set for uniqueness + # a set is unsorted, so sort again by category + ], + key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "") + ) context['cart'] = self.get_cart() if self.request.user.is_authenticated() else None return context -class LoginForm(BaseAuthenticationForm): - username = forms.CharField( - label=_('Username'), - help_text=( - _('If you registered for multiple events, your username is your email address.') - if settings.PRETIX_GLOBAL_REGISTRATION - else None - ) - ) - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput - ) - - error_messages = { - 'invalid_login': _("Please enter a correct username and password."), - 'inactive': _("This account is inactive."), - } - - def __init__(self, request=None, *args, **kwargs): - self.request = request - self.user_cache = None - super(forms.Form, self).__init__(*args, **kwargs) - - def clean(self): - username = self.cleaned_data.get('username') - password = self.cleaned_data.get('password') - - if username and password: - if '@' in username: - identifier = username.lower() - else: - identifier = "%s@%s.event.pretix" % (username, self.request.event.identity) - self.user_cache = authenticate(identifier=identifier, - password=password) - if self.user_cache is None: - raise forms.ValidationError( - self.error_messages['invalid_login'], - code='invalid_login', - ) - else: - self.confirm_login_allowed(self.user_cache) - - return self.cleaned_data - - -class GlobalRegistrationForm(forms.Form): - error_messages = { - 'duplicate_email': _("You already registered with that e-mail address, please use the login form."), - 'pw_mismatch': _("Please enter the same password twice"), - } - email = forms.EmailField( - label=_('Email address'), - required=True - ) - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput, - required=True - ) - password_repeat = forms.CharField( - label=_('Repeat password'), - widget=forms.PasswordInput - ) - - def clean(self): - password1 = self.cleaned_data.get('password') - password2 = self.cleaned_data.get('password_repeat') - - if password1 and password1 != password2: - raise forms.ValidationError( - self.error_messages['pw_mismatch'], - code='pw_mismatch', - ) - - return self.cleaned_data - - def clean_email(self): - email = self.cleaned_data['email'] - if User.objects.filter(identifier=email).exists(): - raise forms.ValidationError( - self.error_messages['duplicate_email'], - code='duplicate_email', - ) - return email - - -class LocalRegistrationForm(forms.Form): - error_messages = { - 'invalid_username': _("Please only use characters, numbers or ./+/-/_ in your username."), - 'duplicate_username': _("This username is already taken. Please choose a different one."), - 'pw_mismatch': _("Please enter the same password twice"), - } - username = forms.CharField( - label=_('Username'), - validators=[ - RegexValidator( - regex='^[a-zA-Z0-9\.+\-_]*$', - code='invalid_username', - message=error_messages['invalid_username'] - ), - ], - required=True - ) - email = forms.EmailField( - label=_('E-mail address'), - required=False - ) - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput, - required=True - ) - password_repeat = forms.CharField( - label=_('Repeat password'), - widget=forms.PasswordInput - ) - - def __init__(self, request, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - self.fields['email'].required = request.event.settings.user_mail_required - - def clean(self): - password1 = self.cleaned_data.get('password') - password2 = self.cleaned_data.get('password_repeat') - - if password1 and password1 != password2: - raise forms.ValidationError( - self.error_messages['pw_mismatch'], - code='pw_mismatch', - ) - - return self.cleaned_data - - def clean_username(self): - username = self.cleaned_data['username'] - if User.objects.filter(event=self.request.event, username=username).exists(): - raise forms.ValidationError( - self.error_messages['duplicate_username'], - code='duplicate_username', - ) - return username - - class EventLogin(EventViewMixin, TemplateView): template_name = 'pretixpresale/event/login.html' @@ -301,73 +159,6 @@ class EventLogin(EventViewMixin, TemplateView): return context -class PasswordRecoverForm(Form): - error_messages = { - 'pw_mismatch': _("Please enter the same password twice"), - } - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput, - required=True - ) - password_repeat = forms.CharField( - label=_('Repeat password'), - widget=forms.PasswordInput - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def clean(self): - password1 = self.cleaned_data.get('password') - password2 = self.cleaned_data.get('password_repeat') - - if password1 and password1 != password2: - raise forms.ValidationError( - self.error_messages['pw_mismatch'], - code='pw_mismatch', - ) - - return self.cleaned_data - - -class PasswordForgotForm(Form): - username = forms.CharField( - label=_('Username or E-mail'), - ) - - def __init__(self, event, *args, **kwargs): - self.event = event - super().__init__(*args, **kwargs) - - def clean_username(self): - username = self.cleaned_data['username'] - try: - self.cleaned_data['user'] = User.objects.get( - identifier=username, event__isnull=True - ) - return username - except User.DoesNotExist: - pass - try: - self.cleaned_data['user'] = User.objects.get( - username=username, event=self.event - ) - return username - except User.DoesNotExist: - pass - try: - self.cleaned_data['user'] = User.objects.get( - email=username, event=self.event - ) - return username - except: - raise forms.ValidationError( - _("We are unable to find a user matching the data you provided."), - code='unknown_user', - ) - - class EventForgot(EventViewMixin, TemplateView): template_name = 'pretixpresale/event/forgot.html' @@ -398,8 +189,8 @@ class EventForgot(EventViewMixin, TemplateView): 'url': settings.SITE_URL + reverse('presale:event.forgot.recover', kwargs={ 'event': self.request.event.slug, 'organizer': self.request.event.organizer.slug, - }) + '?token=' + self.generate_token(user), - }, + }) + '?token=' + self.generate_token(user), + }, self.request.event ) messages.success(request, _('We sent you an e-mail containing further instructions.')) diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index be434848a..358cce82f 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -1,4 +1,3 @@ -from io import BytesIO from django.contrib import messages from django.core.urlresolvers import reverse from django.shortcuts import redirect @@ -6,16 +5,14 @@ from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django.utils.functional import cached_property from django.views.generic import TemplateView, View -from django.http import HttpResponseNotFound, HttpResponseForbidden, HttpResponse +from django.http import HttpResponseNotFound, HttpResponseForbidden from pretix.base.models import Order, OrderPosition from pretix.base.signals import register_payment_providers, register_ticket_outputs from pretix.presale.views import EventViewMixin, EventLoginRequiredMixin, CartDisplayMixin from pretix.presale.views.checkout import QuestionsViewMixin -from django.contrib.staticfiles import finders class OrderDetailMixin: - @cached_property def order(self): try: @@ -163,7 +160,6 @@ class OrderCancel(EventViewMixin, EventLoginRequiredMixin, OrderDetailMixin, class OrderDownload(EventViewMixin, EventLoginRequiredMixin, OrderDetailMixin, View): - def get_order_url(self): return reverse('presale:event.order', kwargs={ 'event': self.request.event.slug,