diff --git a/src/pretix/base/migrations/0016_order_guest_email.py b/src/pretix/base/migrations/0016_order_guest_email.py new file mode 100644 index 0000000000..fef35d8b06 --- /dev/null +++ b/src/pretix/base/migrations/0016_order_guest_email.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0015_auto_20150916_2219'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='guest_email', + field=models.EmailField(max_length=254, verbose_name='E-mail', blank=True, null=True), + ), + ] diff --git a/src/pretix/base/migrations/0017_order_guest_locale.py b/src/pretix/base/migrations/0017_order_guest_locale.py new file mode 100644 index 0000000000..79a63f7e2b --- /dev/null +++ b/src/pretix/base/migrations/0017_order_guest_locale.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0016_order_guest_email'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='guest_locale', + field=models.CharField(max_length=32, null=True, blank=True, verbose_name='Locale'), + ), + ] diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 52678fdf72..7478e1602c 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -1442,6 +1442,14 @@ class Order(Versionable): verbose_name=_("User"), related_name="orders" ) + guest_email = models.EmailField( + null=True, blank=True, + verbose_name=_('E-mail') + ) + guest_locale = models.CharField( + null=True, blank=True, max_length=32, + verbose_name=_('Locale') + ) secret = models.CharField(max_length=32, default=generate_secret) datetime = models.DateTimeField( verbose_name=_("Date") @@ -1590,6 +1598,18 @@ class Order(Versionable): return error_messages['busy'] return True + @property + def locale(self): + if self.user: + return self.user.locale + return self.guest_locale + + @property + def email(self): + if self.user: + return self.user.email + return self.guest_email + class CachedTicket(models.Model): order = VersionedForeignKey(Order, on_delete=models.CASCADE) diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 626e4917b1..060549bc6f 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -14,6 +14,7 @@ from pretix.base.models import CartPosition, Order from pretix.base.services.orders import mark_order_paid from pretix.base.settings import SettingsSandbox from pretix.base.signals import register_payment_providers +from pretix.presale.views import user_cart_q class BasePaymentProvider: @@ -441,7 +442,7 @@ class FreeOrderProvider(BasePaymentProvider): def is_allowed(self, request: HttpRequest) -> bool: return CartPosition.objects.current.filter( - Q(user=request.user) & Q(event=request.event) + user_cart_q(request) & Q(event=request.event) ).aggregate(sum=Sum('price'))['sum'] == 0 diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 37ec41f867..8893ebff38 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -13,7 +13,7 @@ from pretix.helpers.urls import build_absolute_uri logger = logging.getLogger('pretix.base.mail') -def mail(user: User, subject: str, template: str, context: dict=None, event: Event=None): +def mail(email: str, subject: str, template: str, context: dict=None, event: Event=None, locale: str=None): """ Sends out an email to a user. @@ -30,11 +30,8 @@ def mail(user: User, subject: str, template: str, context: dict=None, event: Eve the email has been sent, just that it has been queued by the e-mail backend. """ - if not user.email: - return False - _lng = translation.get_language() - translation.activate(user.locale or settings.LANGUAGE_CODE) + translation.activate(locale or settings.LANGUAGE_CODE) if isinstance(template, LazyI18nString): body = str(template) @@ -66,7 +63,7 @@ def mail(user: User, subject: str, template: str, context: dict=None, event: Eve ) body += "\r\n" try: - return mail_send([user.email], subject, body, sender) + return mail_send([email], subject, body, sender) finally: translation.activate(_lng) diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index a998adfa5f..0683d56b66 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -4,13 +4,16 @@ from django.db import transaction from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ -from pretix.base.models import EventLock, Order, OrderPosition, Quota +from pretix.base.models import ( + Event, EventLock, Order, OrderPosition, Quota, User, +) from pretix.base.services.mail import mail from pretix.base.signals import order_paid, order_placed from pretix.helpers.urls import build_absolute_uri -def mark_order_paid(order, provider=None, info=None, date=None, manual=None, force=False): +def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None, + force: bool=False): """ Marks an order as paid. This clones the order object, sets the payment provider, info and date and returns the cloned order object. @@ -44,20 +47,19 @@ def mark_order_paid(order, provider=None, info=None, date=None, manual=None, for from pretix.base.services.mail import mail mail( - order.user, _('Payment received for your order: %(code)s') % {'code': order.code}, + order.email, _('Payment received for your order: %(code)s') % {'code': order.code}, 'pretixpresale/email/order_paid.txt', { - 'user': order.user, 'order': order, 'event': order.event, 'url': build_absolute_uri('presale:event.order', kwargs={ 'event': order.event.slug, 'organizer': order.event.organizer.slug, 'order': order.code, - }), + }) + '?order_secret=' + order.secret, 'downloads': order.event.settings.get('ticket_download', as_type=bool) }, - order.event + order.event, locale=order.locale ) return order @@ -66,7 +68,7 @@ class OrderError(Exception): pass -def check_positions(event, dt, positions): +def check_positions(event: Event, dt: datetime, positions: list): error_messages = { 'unavailable': _('Some of the products you selected were no longer available. ' 'Please see below for details.'), @@ -117,7 +119,8 @@ def check_positions(event, dt, positions): raise OrderError(err) -def perform_order(event, user, payment_provider, positions): +def perform_order(event: Event, payment_provider: str, positions: list, user: User=None, email: str=None, + locale: str=None): error_messages = { 'busy': _('We were not able to process your request completely as the ' 'server was too busy. Please try again.'), @@ -127,21 +130,22 @@ def perform_order(event, user, payment_provider, positions): try: with event.lock(): check_positions(event, dt, positions) - order = place_order(event, user, positions, dt, payment_provider) + order = place_order(event, user, email if user is None else None, positions, dt, payment_provider, + locale=locale) mail( - user, _('Your order: %(code)s') % {'code': order.code}, + order.email, _('Your order: %(code)s') % {'code': order.code}, 'pretixpresale/email/order_placed.txt', { - 'user': user, 'order': order, + 'order': order, 'event': event, 'url': build_absolute_uri('presale:event.order', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug, 'order': order.code, - }), + }) + '?order_secret=' + order.secret, 'payment': payment_provider.order_pending_mail_render(order) }, - event + event, locale=order.locale ) return order except EventLock.LockTimeoutException: @@ -151,7 +155,8 @@ def perform_order(event, user, payment_provider, positions): @transaction.atomic() -def place_order(event, user, positions, dt, payment_provider): +def place_order(event: Event, user: User, email: str, positions: list, dt: datetime, payment_provider: str, + locale: str=None): total = sum([c.price for c in positions]) payment_fee = payment_provider.calculate_fee(total) total += payment_fee @@ -162,8 +167,10 @@ def place_order(event, user, positions, dt, payment_provider): status=Order.STATUS_PENDING, event=event, user=user, + guest_email=email, datetime=dt, expires=min(expires), + locale=locale, total=total, payment_fee=payment_fee, payment_provider=payment_provider.identifier, diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 51ee169f44..b36f78a8df 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -66,8 +66,8 @@
{% trans "Expiry date" %}
{{ order.expires }}
{% endif %} -
{% trans "Username" %}
-
{{ order.user }}
+
{% trans "User" %}
+
{{ order.user|default:order.guest_email }}
diff --git a/src/pretix/plugins/sendmail/views.py b/src/pretix/plugins/sendmail/views.py index 03cc47317a..3e1c1acd37 100644 --- a/src/pretix/plugins/sendmail/views.py +++ b/src/pretix/plugins/sendmail/views.py @@ -31,8 +31,8 @@ class SenderView(EventPermissionRequiredMixin, FormView): users = set([o.user for o in orders]) for u in users: - mail(u, form.cleaned_data['subject'], form.cleaned_data['message'], - None, self.request.event) + mail(u.email, form.cleaned_data['subject'], form.cleaned_data['message'], + None, self.request.event, locale=u.locale) messages.success(self.request, _('Your message will be sent to the selected users.')) diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py index 4b8660fa63..321973510f 100644 --- a/src/pretix/presale/forms/checkout.py +++ b/src/pretix/presale/forms/checkout.py @@ -4,6 +4,10 @@ from django.utils.translation import ugettext_lazy as _ from pretix.base.models import Question +class GuestForm(forms.Form): + email = forms.EmailField(label=_('E-mail')) + + class QuestionsForm(forms.Form): """ This form class is responsible for asking order-related questions. This includes diff --git a/src/pretix/presale/middleware.py b/src/pretix/presale/middleware.py index a674e39a2f..bb2e7419fc 100644 --- a/src/pretix/presale/middleware.py +++ b/src/pretix/presale/middleware.py @@ -5,13 +5,24 @@ from pretix.base.models import Event class EventMiddleware: - def process_request(self, request): url = resolve(request.path_info) url_namespace = url.namespace url_name = url.url_name if url_namespace != 'presale': return + + if 'order_secrets' not in request.session: + request.session['order_secrets'] = [] + if 'order_secret' in request.GET and request.GET.get('order_secret') not in request.session['order_secrets']: + # We can't use append here, because this would not trigger __setitem__ + # on the session store and would not be saved + request.session['order_secrets'] = request.session['order_secrets'] + [request.GET.get('order_secret')] + # Removal of the secret from the URL has been disabled so people can bookmark it + # g = request.GET.copy() + # del g['order_secret'] + # return redirect(request.path + '?' + g.urlencode()) + if 'event.' in url_name and 'event' in url.kwargs: try: request.event = Event.objects.current.filter( diff --git a/src/pretix/presale/templates/pretixpresale/email/order_paid.txt b/src/pretix/presale/templates/pretixpresale/email/order_paid.txt index f4e4d8951e..40fafb648c 100644 --- a/src/pretix/presale/templates/pretixpresale/email/order_paid.txt +++ b/src/pretix/presale/templates/pretixpresale/email/order_paid.txt @@ -11,7 +11,7 @@ Your {{ event }} team we successfully received your payment for {{ event }}. Thank you! -You can view the status of your order at +You can change your order details and view the status of your order at {{ url }} Best regards, diff --git a/src/pretix/presale/templates/pretixpresale/email/order_placed.txt b/src/pretix/presale/templates/pretixpresale/email/order_placed.txt index cb577edc31..64238275c9 100644 --- a/src/pretix/presale/templates/pretixpresale/email/order_placed.txt +++ b/src/pretix/presale/templates/pretixpresale/email/order_placed.txt @@ -4,7 +4,7 @@ we successfully received your order for {{ event }} with a total value of {{ total }} {{ currency }}. Please complete your payment before {{ date }}. {{ paymentinfo }} -You can view the status of your order at +You can change your order details and view the status of your order at {{ url }} diff --git a/src/pretix/presale/templates/pretixpresale/event/forgot.html b/src/pretix/presale/templates/pretixpresale/event/forgot.html index 88b5faa7c2..87d0ac24d9 100644 --- a/src/pretix/presale/templates/pretixpresale/event/forgot.html +++ b/src/pretix/presale/templates/pretixpresale/event/forgot.html @@ -10,7 +10,7 @@ {% bootstrap_field form.email layout="horizontal" %}
-
+
diff --git a/src/pretix/presale/templates/pretixpresale/event/login.html b/src/pretix/presale/templates/pretixpresale/event/login.html index 27aafb3e9a..cca01a4393 100644 --- a/src/pretix/presale/templates/pretixpresale/event/login.html +++ b/src/pretix/presale/templates/pretixpresale/event/login.html @@ -23,13 +23,13 @@ {% bootstrap_field login_form.password layout="horizontal" %}
- @@ -47,7 +47,19 @@ @@ -71,7 +83,7 @@ {% bootstrap_field registration_form.password_repeat layout="horizontal" %}
-
+
diff --git a/src/pretix/presale/templates/pretixpresale/event/recover.html b/src/pretix/presale/templates/pretixpresale/event/recover.html index 7610ab0fc9..8749c21b09 100644 --- a/src/pretix/presale/templates/pretixpresale/event/recover.html +++ b/src/pretix/presale/templates/pretixpresale/event/recover.html @@ -11,7 +11,7 @@ {% bootstrap_field form.password_repeat layout="horizontal" %}
-
+
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py index bb44e2646b..da2bd95ac9 100644 --- a/src/pretix/presale/views/__init__.py +++ b/src/pretix/presale/views/__init__.py @@ -1,7 +1,6 @@ from datetime import timedelta from itertools import groupby -from django.contrib.auth.decorators import login_required from django.contrib.auth.views import redirect_to_login from django.core.urlresolvers import reverse from django.db.models import Q @@ -12,14 +11,54 @@ from pretix.base.models import CartPosition from pretix.base.signals import register_payment_providers -class LoginRequiredMixin: +def login_required(view_func): + def _wrapped_view(request, *args, **kwargs): + if request.user.is_authenticated(): + return view_func(request, *args, **kwargs) + path = request.path + return redirect_to_login( + path, reverse('presale:event.checkout.login', kwargs={ + 'organizer': request.event.organizer.slug, + 'event': request.event.slug, + }), 'next' + ) + return _wrapped_view + +def login_or_guest_required(view_func): + def _wrapped_view(request, *args, **kwargs): + if request.user.is_authenticated() or 'guest_email' in request.session: + return view_func(request, *args, **kwargs) + path = request.path + return redirect_to_login( + path, reverse('presale:event.checkout.login', kwargs={ + 'organizer': request.event.organizer.slug, + 'event': request.event.slug, + }), 'next' + ) + return _wrapped_view + + +class LoginRequiredMixin: @classmethod def as_view(cls, **initkwargs): view = super().as_view(**initkwargs) return login_required(view) +class LoginOrGuestRequiredMixin: + @classmethod + def as_view(cls, **initkwargs): + view = super().as_view(**initkwargs) + return login_or_guest_required(view) + + +def user_cart_q(request): + if request.user.is_authenticated(): + return Q(Q(user=request.user) | Q(session=request.session.session_key)) + return Q(Q(user__isnull=True) & Q(session=request.session.session_key)) + + class CartDisplayMixin: @cached_property @@ -28,7 +67,7 @@ class CartDisplayMixin: A list of this users cart position """ return list(CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) + user_cart_q(self.request) & Q(event=self.request.event) ).order_by( 'item', 'variation' ).select_related( @@ -40,7 +79,7 @@ class CartDisplayMixin: def get_cart(self, answers=False, queryset=None, payment_fee=None): queryset = queryset or CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) + user_cart_q(self.request) & Q(event=self.request.event) ) prefetch = ['variation__values', 'variation__values__prop'] @@ -106,7 +145,6 @@ class CartDisplayMixin: class EventViewMixin: - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['event'] = self.request.event diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 20a3d8bc88..b157449585 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -13,7 +13,9 @@ from django.views.generic import View from pretix.base.models import ( CartPosition, EventLock, Item, ItemVariation, Quota, ) -from pretix.presale.views import EventViewMixin, LoginRequiredMixin +from pretix.presale.views import ( + EventViewMixin, LoginOrGuestRequiredMixin, user_cart_q, +) class CartActionMixin: @@ -62,13 +64,13 @@ class CartActionMixin: return items -class CartRemove(EventViewMixin, CartActionMixin, LoginRequiredMixin, View): +class CartRemove(EventViewMixin, CartActionMixin, LoginOrGuestRequiredMixin, View): def post(self, *args, **kwargs): items = self._items_from_post_data() if not items: return redirect(self.get_failure_url()) - qw = Q(user=self.request.user) + qw = user_cart_q(self.request) for item, variation, cnt in items: cw = qw & Q(item_id=item) @@ -112,7 +114,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # We do not use LoginRequiredMixin here, as we want to store stuff into the # session before redirecting to login - if not request.user.is_authenticated(): + if not request.user.is_authenticated() and 'guest_email' not in request.session: request.session['cart_tmp'] = json.dumps(self.items) return redirect_to_login( self.get_success_url(), reverse('presale:event.checkout.login', kwargs={ @@ -121,7 +123,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): }), 'next' ) - existing = CartPosition.objects.current.filter(user=self.request.user, event=self.request.event).count() + existing = CartPosition.objects.current.filter(user_cart_q(self.request) & Q(event=self.request.event)).count() if sum(i[2] for i in self.items) + existing > int(self.request.event.settings.max_items_per_order): # TODO: i18n plurals self.error_message(self.error_messages['max_items'] % self.request.event.settings.max_items_per_order) @@ -142,7 +144,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # For items that are already expired, we have to delete and re-add them, as they might # be no longer available or prices might have changed. Sorry! for cp in CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__lte=now()) + user_cart_q(self.request) & Q(event=self.request.event) & Q(expires__lte=now()) ): self._re_add_position(cp) positions.add(cp) @@ -153,7 +155,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # cart expire at the same time # We can extend the reservation of items which are not yet expired without risk CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__gt=now()) + user_cart_q(self.request) & Q(event=self.request.event) & Q(expires__gt=now()) ).update(expires=expiry) def _delete_expired(self): @@ -237,14 +239,18 @@ class CartAdd(EventViewMixin, CartActionMixin, View): cp.price = price cp.save() else: - CartPosition.objects.create( + cp = CartPosition( event=self.request.event, - user=self.request.user, item=item, variation=variation, price=price, expires=expiry ) + if self.request.user.is_authenticated(): + cp.user = self.request.user + else: + cp.session = self.request.session.session_key + cp.save() self._delete_expired() diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py index f1eb094979..dbac1510aa 100644 --- a/src/pretix/presale/views/checkout.py +++ b/src/pretix/presale/views/checkout.py @@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse from django.db.models import Q, Sum from django.http import HttpRequest from django.shortcuts import redirect +from django.utils import translation from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from django.views.generic import TemplateView @@ -12,11 +13,13 @@ from pretix.base.services.orders import OrderError, perform_order from pretix.base.signals import register_payment_providers from pretix.presale.forms.checkout import QuestionsForm from pretix.presale.views import ( - CartDisplayMixin, EventViewMixin, LoginRequiredMixin, + CartDisplayMixin, EventViewMixin, LoginOrGuestRequiredMixin, + LoginRequiredMixin, user_cart_q, ) class CheckoutView(TemplateView): + def get_payment_url(self): return reverse('presale:event.checkout.payment', kwargs={ 'event': self.request.event.slug, @@ -41,12 +44,12 @@ class CheckoutView(TemplateView): 'organizer': self.request.event.organizer.slug }) - def get_order_url(self, order): + def get_order_url(self, order, add_secret): return reverse('presale:event.order', kwargs={ 'event': self.request.event.slug, 'organizer': self.request.event.organizer.slug, 'order': order.code, - }) + }) + '?thanks=yes' + ('&order_secret=' + order.secret if add_secret else '') class QuestionsViewMixin: @@ -106,7 +109,7 @@ class QuestionsViewMixin: return not failed -class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, +class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginOrGuestRequiredMixin, QuestionsViewMixin, CheckoutView): template_name = "pretixpresale/event/checkout_questions.html" @@ -138,13 +141,13 @@ class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, return ctx -class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, CheckoutView): +class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginOrGuestRequiredMixin, CheckoutView): template_name = "pretixpresale/event/checkout_payment.html" @cached_property def _total_order_value(self): return CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) + user_cart_q(self.request) & Q(event=self.request.event) ).aggregate(sum=Sum('price'))['sum'] @cached_property @@ -194,7 +197,7 @@ class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, Check return self.get_questions_url() + "?back=true" -class OrderConfirm(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, CheckoutView): +class OrderConfirm(EventViewMixin, CartDisplayMixin, LoginOrGuestRequiredMixin, CheckoutView): template_name = "pretixpresale/event/checkout_confirm.html" def __init__(self, *args, **kwargs): @@ -256,14 +259,18 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, Checkou def perform_order(self, request: HttpRequest): try: - order = perform_order(self.request.event, self.request.user, self.payment_provider, self.positions) + order = perform_order(self.request.event, self.payment_provider, self.positions, + user=request.user if request.user.is_authenticated() else None, + email=request.session.get('guest_email', None), + locale=translation.get_language()) except OrderError as e: messages.error(request, str(e)) return redirect(self.get_confirm_url()) else: - messages.success(request, _('Your order has been placed.')) + # Message is delivered via GET parameter + # messages.success(request, _('Your order has been placed.')) resp = self.payment_provider.payment_perform(request, order) - return redirect(resp or self.get_order_url(order)) + return redirect(resp or self.get_order_url(order, not request.user.is_authenticated())) def get_previous_url(self): if self.payment_provider.identifier != "free": diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 91d114de9f..f01f5b11f8 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -21,6 +21,7 @@ from pretix.base.forms.user import UserSettingsForm from pretix.base.models import User from pretix.base.services.mail import mail from pretix.helpers.urls import build_absolute_uri +from pretix.presale.forms.checkout import GuestForm from pretix.presale.views import ( CartDisplayMixin, EventViewMixin, LoginRequiredMixin, ) @@ -78,7 +79,7 @@ class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): 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 + context['cart'] = self.get_cart() return context @@ -111,6 +112,11 @@ class EventLogin(EventViewMixin, TemplateView): if form.is_valid() and form.user_cache: login(request, form.user_cache) return self.redirect_to_next() + elif request.POST.get('form') == 'guest': + form = self.guest_form + if form.is_valid(): + request.session['guest_email'] = form.cleaned_data['email'] + return self.redirect_to_next() elif request.POST.get('form') == 'registration': form = self.registration_form if form.is_valid(): @@ -131,6 +137,12 @@ class EventLogin(EventViewMixin, TemplateView): data=self.request.POST if self.request.POST.get('form', '') == 'login' else None ) + @cached_property + def guest_form(self): + return GuestForm( + data=self.request.POST if self.request.POST.get('form', '') == 'guest' else None + ) + @cached_property def registration_form(self): return RegistrationForm( @@ -141,6 +153,7 @@ class EventLogin(EventViewMixin, TemplateView): context = super().get_context_data(**kwargs) context['login_form'] = self.login_form context['registration_form'] = self.registration_form + context['guest_form'] = self.guest_form return context @@ -163,24 +176,19 @@ class EventForgot(EventViewMixin, TemplateView): def post(self, request, *args, **kwargs): if self.form.is_valid(): user = self.form.cleaned_data['user'] - if user.email: - mail( - user, _('Password recovery'), - 'pretixpresale/email/forgot.txt', - { - 'user': user, - 'event': self.request.event, - 'url': build_absolute_uri('presale:event.forgot.recover', kwargs={ - 'event': self.request.event.slug, - 'organizer': self.request.event.organizer.slug, - }) + '?token=' + self.generate_token(user), - }, - self.request.event - ) - messages.success(request, _('We sent you an e-mail containing further instructions.')) - else: - messages.success(request, _('We are unable to send you a new password, as you did not enter an e-mail ' - 'address at your registration.')) + mail( + user.email, _('Password recovery'), 'pretixpresale/email/forgot.txt', + { + 'user': user, + 'event': self.request.event, + 'url': build_absolute_uri('presale:event.forgot.recover', kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + }) + '?token=' + self.generate_token(user), + }, + self.request.event, locale=user.locale + ) + messages.success(request, _('We sent you an e-mail containing further instructions.')) return redirect('presale:event.forgot', organizer=self.request.event.organizer.slug, event=self.request.event.slug) diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index db2a498462..4534675e3c 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -2,34 +2,28 @@ from datetime import timedelta from django.contrib import messages from django.core.urlresolvers import reverse +from django.db.models import Q from django.http import HttpResponseForbidden, HttpResponseNotFound from django.shortcuts import redirect from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django.views.generic import TemplateView, View - from pretix.base.models import CachedFile, CachedTicket, Order, OrderPosition from pretix.base.services.tickets import generate -from pretix.base.signals import ( - register_payment_providers, register_ticket_outputs, -) -from pretix.presale.views import ( - CartDisplayMixin, EventViewMixin, LoginRequiredMixin, -) +from pretix.base.signals import register_payment_providers, register_ticket_outputs +from pretix.presale.views import CartDisplayMixin, EventViewMixin from pretix.presale.views.checkout import QuestionsViewMixin class OrderDetailMixin: - @cached_property def order(self): try: - return Order.objects.current.get( - user=self.request.user, - event=self.request.event, - code=self.kwargs['order'], - ) + q = Q(Q(secret__isnull=False) & Q(secret__in=self.request.session['order_secrets'])) + if self.request.user.is_authenticated(): + q |= Q(user=self.request.user) + return Order.objects.current.get(q & Q(event=self.request.event) & Q(code=self.kwargs['order'])) except Order.DoesNotExist: return None @@ -49,8 +43,7 @@ class OrderDetailMixin: }) -class OrderDetails(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, - CartDisplayMixin, TemplateView): +class OrderDetails(EventViewMixin, OrderDetailMixin, CartDisplayMixin, TemplateView): template_name = "pretixpresale/event/order.html" def get(self, request, *args, **kwargs): @@ -102,7 +95,7 @@ class OrderDetails(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, return ctx -class OrderPay(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateView): +class OrderPay(EventViewMixin, OrderDetailMixin, TemplateView): template_name = "pretixpresale/event/order_pay.html" def dispatch(self, request, *args, **kwargs): @@ -145,7 +138,7 @@ class OrderPay(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateVie }) -class OrderPayDo(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateView): +class OrderPayDo(EventViewMixin, OrderDetailMixin, TemplateView): template_name = "pretixpresale/event/order_pay_confirm.html" def dispatch(self, request, *args, **kwargs): @@ -185,8 +178,7 @@ class OrderPayDo(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateV }) -class OrderModify(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, - QuestionsViewMixin, TemplateView): +class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView): template_name = "pretixpresale/event/order_modify.html" @cached_property @@ -227,8 +219,7 @@ class OrderModify(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, return ctx -class OrderCancel(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, - TemplateView): +class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView): template_name = "pretixpresale/event/order_cancel.html" def dispatch(self, request, *args, **kwargs): @@ -255,9 +246,7 @@ class OrderCancel(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, return ctx -class OrderDownload(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, - View): - +class OrderDownload(EventViewMixin, OrderDetailMixin, View): @cached_property def output(self): responses = register_ticket_outputs.send(self.request.event) diff --git a/src/tests/base/test_mail.py b/src/tests/base/test_mail.py index 672984753d..4f4092fab8 100644 --- a/src/tests/base/test_mail.py +++ b/src/tests/base/test_mail.py @@ -26,8 +26,7 @@ def test_send_mail_with_prefix(client, env): djmail.outbox = [] event, user, organizer = env event.settings.set('mail_prefix', 'test') - mail(user, 'Test subject', - 'mailtest.txt', {}, event) + mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event) assert len(djmail.outbox) == 1 assert djmail.outbox[0].to == [user.email] @@ -39,8 +38,7 @@ def test_send_mail_with_event_sender(client, env): djmail.outbox = [] event, user, organizer = env event.settings.set('mail_from', 'foo@bar') - mail(user, 'Test subject', - 'mailtest.txt', {}, event) + mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event) assert len(djmail.outbox) == 1 assert djmail.outbox[0].to == [user.email] @@ -52,8 +50,7 @@ def test_send_mail_with_event_sender(client, env): def test_send_mail_with_default_sender(client, env): djmail.outbox = [] event, user, organizer = env - mail(user, 'Test subject', - 'mailtest.txt', {}, event) + mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event) del event.settings['mail_from'] assert len(djmail.outbox) == 1 @@ -68,8 +65,7 @@ def test_send_mail_with_user_locale(client, env): event, user, organizer = env user.locale = 'de' user.save() - mail(user, _('User'), - 'mailtest.txt', {}, event) + mail('dummy@dummy.dummy', _('User'), 'mailtest.txt', {}, event, locale=user.locale) del event.settings['mail_from'] assert len(djmail.outbox) == 1