Refs #96 -- Allow anonymous orders

This commit is contained in:
Raphael Michel
2015-09-17 22:10:25 +02:00
parent 7def097dcd
commit 9d625198bd
21 changed files with 245 additions and 111 deletions

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View File

@@ -1442,6 +1442,14 @@ class Order(Versionable):
verbose_name=_("User"), verbose_name=_("User"),
related_name="orders" 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) secret = models.CharField(max_length=32, default=generate_secret)
datetime = models.DateTimeField( datetime = models.DateTimeField(
verbose_name=_("Date") verbose_name=_("Date")
@@ -1590,6 +1598,18 @@ class Order(Versionable):
return error_messages['busy'] return error_messages['busy']
return True 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): class CachedTicket(models.Model):
order = VersionedForeignKey(Order, on_delete=models.CASCADE) order = VersionedForeignKey(Order, on_delete=models.CASCADE)

View File

@@ -14,6 +14,7 @@ from pretix.base.models import CartPosition, Order
from pretix.base.services.orders import mark_order_paid from pretix.base.services.orders import mark_order_paid
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from pretix.presale.views import user_cart_q
class BasePaymentProvider: class BasePaymentProvider:
@@ -441,7 +442,7 @@ class FreeOrderProvider(BasePaymentProvider):
def is_allowed(self, request: HttpRequest) -> bool: def is_allowed(self, request: HttpRequest) -> bool:
return CartPosition.objects.current.filter( 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 ).aggregate(sum=Sum('price'))['sum'] == 0

View File

@@ -13,7 +13,7 @@ from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger('pretix.base.mail') 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. 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 the email has been sent, just that it has been queued by the e-mail
backend. backend.
""" """
if not user.email:
return False
_lng = translation.get_language() _lng = translation.get_language()
translation.activate(user.locale or settings.LANGUAGE_CODE) translation.activate(locale or settings.LANGUAGE_CODE)
if isinstance(template, LazyI18nString): if isinstance(template, LazyI18nString):
body = str(template) body = str(template)
@@ -66,7 +63,7 @@ def mail(user: User, subject: str, template: str, context: dict=None, event: Eve
) )
body += "\r\n" body += "\r\n"
try: try:
return mail_send([user.email], subject, body, sender) return mail_send([email], subject, body, sender)
finally: finally:
translation.activate(_lng) translation.activate(_lng)

View File

@@ -4,13 +4,16 @@ from django.db import transaction
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ 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.services.mail import mail
from pretix.base.signals import order_paid, order_placed from pretix.base.signals import order_paid, order_placed
from pretix.helpers.urls import build_absolute_uri 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, Marks an order as paid. This clones the order object, sets the payment provider,
info and date and returns the cloned order object. 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 from pretix.base.services.mail import mail
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', 'pretixpresale/email/order_paid.txt',
{ {
'user': order.user,
'order': order, 'order': order,
'event': order.event, 'event': order.event,
'url': build_absolute_uri('presale:event.order', kwargs={ 'url': build_absolute_uri('presale:event.order', kwargs={
'event': order.event.slug, 'event': order.event.slug,
'organizer': order.event.organizer.slug, 'organizer': order.event.organizer.slug,
'order': order.code, 'order': order.code,
}), }) + '?order_secret=' + order.secret,
'downloads': order.event.settings.get('ticket_download', as_type=bool) 'downloads': order.event.settings.get('ticket_download', as_type=bool)
}, },
order.event order.event, locale=order.locale
) )
return order return order
@@ -66,7 +68,7 @@ class OrderError(Exception):
pass pass
def check_positions(event, dt, positions): def check_positions(event: Event, dt: datetime, positions: list):
error_messages = { error_messages = {
'unavailable': _('Some of the products you selected were no longer available. ' 'unavailable': _('Some of the products you selected were no longer available. '
'Please see below for details.'), 'Please see below for details.'),
@@ -117,7 +119,8 @@ def check_positions(event, dt, positions):
raise OrderError(err) 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 = { error_messages = {
'busy': _('We were not able to process your request completely as the ' 'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'), 'server was too busy. Please try again.'),
@@ -127,21 +130,22 @@ def perform_order(event, user, payment_provider, positions):
try: try:
with event.lock(): with event.lock():
check_positions(event, dt, positions) 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( mail(
user, _('Your order: %(code)s') % {'code': order.code}, order.email, _('Your order: %(code)s') % {'code': order.code},
'pretixpresale/email/order_placed.txt', 'pretixpresale/email/order_placed.txt',
{ {
'user': user, 'order': order, 'order': order,
'event': event, 'event': event,
'url': build_absolute_uri('presale:event.order', kwargs={ 'url': build_absolute_uri('presale:event.order', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'order': order.code, 'order': order.code,
}), }) + '?order_secret=' + order.secret,
'payment': payment_provider.order_pending_mail_render(order) 'payment': payment_provider.order_pending_mail_render(order)
}, },
event event, locale=order.locale
) )
return order return order
except EventLock.LockTimeoutException: except EventLock.LockTimeoutException:
@@ -151,7 +155,8 @@ def perform_order(event, user, payment_provider, positions):
@transaction.atomic() @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]) total = sum([c.price for c in positions])
payment_fee = payment_provider.calculate_fee(total) payment_fee = payment_provider.calculate_fee(total)
total += payment_fee total += payment_fee
@@ -162,8 +167,10 @@ def place_order(event, user, positions, dt, payment_provider):
status=Order.STATUS_PENDING, status=Order.STATUS_PENDING,
event=event, event=event,
user=user, user=user,
guest_email=email,
datetime=dt, datetime=dt,
expires=min(expires), expires=min(expires),
locale=locale,
total=total, total=total,
payment_fee=payment_fee, payment_fee=payment_fee,
payment_provider=payment_provider.identifier, payment_provider=payment_provider.identifier,

View File

@@ -66,8 +66,8 @@
<dt>{% trans "Expiry date" %}</dt> <dt>{% trans "Expiry date" %}</dt>
<dd>{{ order.expires }}</dd> <dd>{{ order.expires }}</dd>
{% endif %} {% endif %}
<dt>{% trans "Username" %}</dt> <dt>{% trans "User" %}</dt>
<dd>{{ order.user }}</dd> <dd>{{ order.user|default:order.guest_email }}</dd>
</dl> </dl>
</div> </div>
</div> </div>

View File

@@ -31,8 +31,8 @@ class SenderView(EventPermissionRequiredMixin, FormView):
users = set([o.user for o in orders]) users = set([o.user for o in orders])
for u in users: for u in users:
mail(u, form.cleaned_data['subject'], form.cleaned_data['message'], mail(u.email, form.cleaned_data['subject'], form.cleaned_data['message'],
None, self.request.event) None, self.request.event, locale=u.locale)
messages.success(self.request, _('Your message will be sent to the selected users.')) messages.success(self.request, _('Your message will be sent to the selected users.'))

View File

@@ -4,6 +4,10 @@ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Question from pretix.base.models import Question
class GuestForm(forms.Form):
email = forms.EmailField(label=_('E-mail'))
class QuestionsForm(forms.Form): class QuestionsForm(forms.Form):
""" """
This form class is responsible for asking order-related questions. This includes This form class is responsible for asking order-related questions. This includes

View File

@@ -5,13 +5,24 @@ from pretix.base.models import Event
class EventMiddleware: class EventMiddleware:
def process_request(self, request): def process_request(self, request):
url = resolve(request.path_info) url = resolve(request.path_info)
url_namespace = url.namespace url_namespace = url.namespace
url_name = url.url_name url_name = url.url_name
if url_namespace != 'presale': if url_namespace != 'presale':
return 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: if 'event.' in url_name and 'event' in url.kwargs:
try: try:
request.event = Event.objects.current.filter( request.event = Event.objects.current.filter(

View File

@@ -11,7 +11,7 @@ Your {{ event }} team
we successfully received your payment for {{ event }}. Thank you! 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 }} {{ url }}
Best regards, Best regards,

View File

@@ -4,7 +4,7 @@ we successfully received your order for {{ event }} with a total value
of {{ total }} {{ currency }}. Please complete your payment before {{ date }}. of {{ total }} {{ currency }}. Please complete your payment before {{ date }}.
{{ paymentinfo }} {{ 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 }} {{ url }}

View File

@@ -10,7 +10,7 @@
{% bootstrap_field form.email layout="horizontal" %} {% bootstrap_field form.email layout="horizontal" %}
<input type="hidden" name="form" value="login" /> <input type="hidden" name="form" value="login" />
<div class="form-group"> <div class="form-group">
<div class="submit-group col-md-offset-2 col-md-4 text-right"> <div class="submit-group col-md-offset-3 col-md-4">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Send recovery information" %} {% trans "Send recovery information" %}
</button> </button>

View File

@@ -23,13 +23,13 @@
{% bootstrap_field login_form.password layout="horizontal" %} {% bootstrap_field login_form.password layout="horizontal" %}
<input type="hidden" name="form" value="login" /> <input type="hidden" name="form" value="login" />
<div class="form-group"> <div class="form-group">
<div class="submit-group col-md-offset-2 col-md-4 text-right"> <div class="submit-group col-md-offset-3 col-md-4">
<a href="{% url "presale:event.forgot" event=request.event.slug organizer=request.event.organizer.slug %}" class="btn btn-link">
{% trans "Lost password?" %}
</a>
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Login" %} {% trans "Login" %}
</button> </button>
<a href="{% url "presale:event.forgot" event=request.event.slug organizer=request.event.organizer.slug %}" class="btn btn-link">
{% trans "Lost password?" %}
</a>
</div> </div>
</div> </div>
</form> </form>
@@ -47,7 +47,19 @@
<div id="guestForm" class="panel-collapse collapsed {% if request.POST.form == 'guest' %}in{% endif %}"> <div id="guestForm" class="panel-collapse collapsed {% if request.POST.form == 'guest' %}in{% endif %}">
<div class="panel-body"> <div class="panel-body">
<div class="panel-body"> <div class="panel-body">
Coming soon. <form class="form-horizontal" method="post">
{% csrf_token %}
{% bootstrap_form_errors guest_form type='all' layout='inline' %}
{% bootstrap_field guest_form.email layout="horizontal" %}
<input type="hidden" name="form" value="guest" />
<div class="form-group">
<div class="submit-group col-md-offset-3 col-md-4">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
</button>
</div>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
@@ -71,7 +83,7 @@
{% bootstrap_field registration_form.password_repeat layout="horizontal" %} {% bootstrap_field registration_form.password_repeat layout="horizontal" %}
<input type="hidden" name="form" value="registration" /> <input type="hidden" name="form" value="registration" />
<div class="form-group"> <div class="form-group">
<div class="submit-group col-md-offset-2 col-md-4 text-right"> <div class="submit-group col-md-offset-3 col-md-4">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Register" %} {% trans "Register" %}
</button> </button>

View File

@@ -11,7 +11,7 @@
{% bootstrap_field form.password_repeat layout="horizontal" %} {% bootstrap_field form.password_repeat layout="horizontal" %}
<input type="hidden" name="form" value="login" /> <input type="hidden" name="form" value="login" />
<div class="form-group"> <div class="form-group">
<div class="submit-group col-md-offset-2 col-md-4 text-right"> <div class="submit-group col-md-offset-3 col-md-4">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Set new password" %} {% trans "Set new password" %}
</button> </button>

View File

@@ -1,7 +1,6 @@
from datetime import timedelta from datetime import timedelta
from itertools import groupby from itertools import groupby
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Q from django.db.models import Q
@@ -12,14 +11,54 @@ from pretix.base.models import CartPosition
from pretix.base.signals import register_payment_providers 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 @classmethod
def as_view(cls, **initkwargs): def as_view(cls, **initkwargs):
view = super().as_view(**initkwargs) view = super().as_view(**initkwargs)
return login_required(view) 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: class CartDisplayMixin:
@cached_property @cached_property
@@ -28,7 +67,7 @@ class CartDisplayMixin:
A list of this users cart position A list of this users cart position
""" """
return list(CartPosition.objects.current.filter( 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( ).order_by(
'item', 'variation' 'item', 'variation'
).select_related( ).select_related(
@@ -40,7 +79,7 @@ class CartDisplayMixin:
def get_cart(self, answers=False, queryset=None, payment_fee=None): def get_cart(self, answers=False, queryset=None, payment_fee=None):
queryset = queryset or CartPosition.objects.current.filter( 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'] prefetch = ['variation__values', 'variation__values__prop']
@@ -106,7 +145,6 @@ class CartDisplayMixin:
class EventViewMixin: class EventViewMixin:
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['event'] = self.request.event context['event'] = self.request.event

View File

@@ -13,7 +13,9 @@ from django.views.generic import View
from pretix.base.models import ( from pretix.base.models import (
CartPosition, EventLock, Item, ItemVariation, Quota, CartPosition, EventLock, Item, ItemVariation, Quota,
) )
from pretix.presale.views import EventViewMixin, LoginRequiredMixin from pretix.presale.views import (
EventViewMixin, LoginOrGuestRequiredMixin, user_cart_q,
)
class CartActionMixin: class CartActionMixin:
@@ -62,13 +64,13 @@ class CartActionMixin:
return items return items
class CartRemove(EventViewMixin, CartActionMixin, LoginRequiredMixin, View): class CartRemove(EventViewMixin, CartActionMixin, LoginOrGuestRequiredMixin, View):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
items = self._items_from_post_data() items = self._items_from_post_data()
if not items: if not items:
return redirect(self.get_failure_url()) return redirect(self.get_failure_url())
qw = Q(user=self.request.user) qw = user_cart_q(self.request)
for item, variation, cnt in items: for item, variation, cnt in items:
cw = qw & Q(item_id=item) 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 # We do not use LoginRequiredMixin here, as we want to store stuff into the
# session before redirecting to login # 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) request.session['cart_tmp'] = json.dumps(self.items)
return redirect_to_login( return redirect_to_login(
self.get_success_url(), reverse('presale:event.checkout.login', kwargs={ self.get_success_url(), reverse('presale:event.checkout.login', kwargs={
@@ -121,7 +123,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
}), 'next' }), '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): if sum(i[2] for i in self.items) + existing > int(self.request.event.settings.max_items_per_order):
# TODO: i18n plurals # TODO: i18n plurals
self.error_message(self.error_messages['max_items'] % self.request.event.settings.max_items_per_order) 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 # 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! # be no longer available or prices might have changed. Sorry!
for cp in CartPosition.objects.current.filter( 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) self._re_add_position(cp)
positions.add(cp) positions.add(cp)
@@ -153,7 +155,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
# cart expire at the same time # cart expire at the same time
# We can extend the reservation of items which are not yet expired without risk # We can extend the reservation of items which are not yet expired without risk
CartPosition.objects.current.filter( 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) ).update(expires=expiry)
def _delete_expired(self): def _delete_expired(self):
@@ -237,14 +239,18 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
cp.price = price cp.price = price
cp.save() cp.save()
else: else:
CartPosition.objects.create( cp = CartPosition(
event=self.request.event, event=self.request.event,
user=self.request.user,
item=item, item=item,
variation=variation, variation=variation,
price=price, price=price,
expires=expiry 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() self._delete_expired()

View File

@@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse
from django.db.models import Q, Sum from django.db.models import Q, Sum
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils import translation
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView 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.base.signals import register_payment_providers
from pretix.presale.forms.checkout import QuestionsForm from pretix.presale.forms.checkout import QuestionsForm
from pretix.presale.views import ( from pretix.presale.views import (
CartDisplayMixin, EventViewMixin, LoginRequiredMixin, CartDisplayMixin, EventViewMixin, LoginOrGuestRequiredMixin,
LoginRequiredMixin, user_cart_q,
) )
class CheckoutView(TemplateView): class CheckoutView(TemplateView):
def get_payment_url(self): def get_payment_url(self):
return reverse('presale:event.checkout.payment', kwargs={ return reverse('presale:event.checkout.payment', kwargs={
'event': self.request.event.slug, 'event': self.request.event.slug,
@@ -41,12 +44,12 @@ class CheckoutView(TemplateView):
'organizer': self.request.event.organizer.slug '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={ return reverse('presale:event.order', kwargs={
'event': self.request.event.slug, 'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
'order': order.code, 'order': order.code,
}) }) + '?thanks=yes' + ('&order_secret=' + order.secret if add_secret else '')
class QuestionsViewMixin: class QuestionsViewMixin:
@@ -106,7 +109,7 @@ class QuestionsViewMixin:
return not failed return not failed
class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginOrGuestRequiredMixin,
QuestionsViewMixin, CheckoutView): QuestionsViewMixin, CheckoutView):
template_name = "pretixpresale/event/checkout_questions.html" template_name = "pretixpresale/event/checkout_questions.html"
@@ -138,13 +141,13 @@ class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginRequiredMixin,
return ctx return ctx
class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, CheckoutView): class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginOrGuestRequiredMixin, CheckoutView):
template_name = "pretixpresale/event/checkout_payment.html" template_name = "pretixpresale/event/checkout_payment.html"
@cached_property @cached_property
def _total_order_value(self): def _total_order_value(self):
return CartPosition.objects.current.filter( 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'] ).aggregate(sum=Sum('price'))['sum']
@cached_property @cached_property
@@ -194,7 +197,7 @@ class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, Check
return self.get_questions_url() + "?back=true" 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" template_name = "pretixpresale/event/checkout_confirm.html"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -256,14 +259,18 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, Checkou
def perform_order(self, request: HttpRequest): def perform_order(self, request: HttpRequest):
try: 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: except OrderError as e:
messages.error(request, str(e)) messages.error(request, str(e))
return redirect(self.get_confirm_url()) return redirect(self.get_confirm_url())
else: 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) 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): def get_previous_url(self):
if self.payment_provider.identifier != "free": if self.payment_provider.identifier != "free":

View File

@@ -21,6 +21,7 @@ from pretix.base.forms.user import UserSettingsForm
from pretix.base.models import User from pretix.base.models import User
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import build_absolute_uri
from pretix.presale.forms.checkout import GuestForm
from pretix.presale.views import ( from pretix.presale.views import (
CartDisplayMixin, EventViewMixin, LoginRequiredMixin, 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, "") 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 return context
@@ -111,6 +112,11 @@ class EventLogin(EventViewMixin, TemplateView):
if form.is_valid() and form.user_cache: if form.is_valid() and form.user_cache:
login(request, form.user_cache) login(request, form.user_cache)
return self.redirect_to_next() 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': elif request.POST.get('form') == 'registration':
form = self.registration_form form = self.registration_form
if form.is_valid(): 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 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 @cached_property
def registration_form(self): def registration_form(self):
return RegistrationForm( return RegistrationForm(
@@ -141,6 +153,7 @@ class EventLogin(EventViewMixin, TemplateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['login_form'] = self.login_form context['login_form'] = self.login_form
context['registration_form'] = self.registration_form context['registration_form'] = self.registration_form
context['guest_form'] = self.guest_form
return context return context
@@ -163,24 +176,19 @@ class EventForgot(EventViewMixin, TemplateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if self.form.is_valid(): if self.form.is_valid():
user = self.form.cleaned_data['user'] user = self.form.cleaned_data['user']
if user.email: mail(
mail( user.email, _('Password recovery'), 'pretixpresale/email/forgot.txt',
user, _('Password recovery'), {
'pretixpresale/email/forgot.txt', 'user': user,
{ 'event': self.request.event,
'user': user, 'url': build_absolute_uri('presale:event.forgot.recover', kwargs={
'event': self.request.event, 'event': self.request.event.slug,
'url': build_absolute_uri('presale:event.forgot.recover', kwargs={ 'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug, }) + '?token=' + self.generate_token(user),
'organizer': self.request.event.organizer.slug, },
}) + '?token=' + self.generate_token(user), self.request.event, locale=user.locale
}, )
self.request.event messages.success(request, _('We sent you an e-mail containing further instructions.'))
)
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.'))
return redirect('presale:event.forgot', return redirect('presale:event.forgot',
organizer=self.request.event.organizer.slug, organizer=self.request.event.organizer.slug,
event=self.request.event.slug) event=self.request.event.slug)

View File

@@ -2,34 +2,28 @@ from datetime import timedelta
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import HttpResponseForbidden, HttpResponseNotFound from django.http import HttpResponseForbidden, HttpResponseNotFound
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from pretix.base.models import CachedFile, CachedTicket, Order, OrderPosition from pretix.base.models import CachedFile, CachedTicket, Order, OrderPosition
from pretix.base.services.tickets import generate from pretix.base.services.tickets import generate
from pretix.base.signals import ( from pretix.base.signals import register_payment_providers, register_ticket_outputs
register_payment_providers, register_ticket_outputs, from pretix.presale.views import CartDisplayMixin, EventViewMixin
)
from pretix.presale.views import (
CartDisplayMixin, EventViewMixin, LoginRequiredMixin,
)
from pretix.presale.views.checkout import QuestionsViewMixin from pretix.presale.views.checkout import QuestionsViewMixin
class OrderDetailMixin: class OrderDetailMixin:
@cached_property @cached_property
def order(self): def order(self):
try: try:
return Order.objects.current.get( q = Q(Q(secret__isnull=False) & Q(secret__in=self.request.session['order_secrets']))
user=self.request.user, if self.request.user.is_authenticated():
event=self.request.event, q |= Q(user=self.request.user)
code=self.kwargs['order'], return Order.objects.current.get(q & Q(event=self.request.event) & Q(code=self.kwargs['order']))
)
except Order.DoesNotExist: except Order.DoesNotExist:
return None return None
@@ -49,8 +43,7 @@ class OrderDetailMixin:
}) })
class OrderDetails(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, class OrderDetails(EventViewMixin, OrderDetailMixin, CartDisplayMixin, TemplateView):
CartDisplayMixin, TemplateView):
template_name = "pretixpresale/event/order.html" template_name = "pretixpresale/event/order.html"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@@ -102,7 +95,7 @@ class OrderDetails(EventViewMixin, LoginRequiredMixin, OrderDetailMixin,
return ctx return ctx
class OrderPay(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateView): class OrderPay(EventViewMixin, OrderDetailMixin, TemplateView):
template_name = "pretixpresale/event/order_pay.html" template_name = "pretixpresale/event/order_pay.html"
def dispatch(self, request, *args, **kwargs): 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" template_name = "pretixpresale/event/order_pay_confirm.html"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@@ -185,8 +178,7 @@ class OrderPayDo(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateV
}) })
class OrderModify(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView):
QuestionsViewMixin, TemplateView):
template_name = "pretixpresale/event/order_modify.html" template_name = "pretixpresale/event/order_modify.html"
@cached_property @cached_property
@@ -227,8 +219,7 @@ class OrderModify(EventViewMixin, LoginRequiredMixin, OrderDetailMixin,
return ctx return ctx
class OrderCancel(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
TemplateView):
template_name = "pretixpresale/event/order_cancel.html" template_name = "pretixpresale/event/order_cancel.html"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@@ -255,9 +246,7 @@ class OrderCancel(EventViewMixin, LoginRequiredMixin, OrderDetailMixin,
return ctx return ctx
class OrderDownload(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, class OrderDownload(EventViewMixin, OrderDetailMixin, View):
View):
@cached_property @cached_property
def output(self): def output(self):
responses = register_ticket_outputs.send(self.request.event) responses = register_ticket_outputs.send(self.request.event)

View File

@@ -26,8 +26,7 @@ def test_send_mail_with_prefix(client, env):
djmail.outbox = [] djmail.outbox = []
event, user, organizer = env event, user, organizer = env
event.settings.set('mail_prefix', 'test') event.settings.set('mail_prefix', 'test')
mail(user, 'Test subject', mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event)
'mailtest.txt', {}, event)
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email] assert djmail.outbox[0].to == [user.email]
@@ -39,8 +38,7 @@ def test_send_mail_with_event_sender(client, env):
djmail.outbox = [] djmail.outbox = []
event, user, organizer = env event, user, organizer = env
event.settings.set('mail_from', 'foo@bar') event.settings.set('mail_from', 'foo@bar')
mail(user, 'Test subject', mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event)
'mailtest.txt', {}, event)
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email] 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): def test_send_mail_with_default_sender(client, env):
djmail.outbox = [] djmail.outbox = []
event, user, organizer = env event, user, organizer = env
mail(user, 'Test subject', mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event)
'mailtest.txt', {}, event)
del event.settings['mail_from'] del event.settings['mail_from']
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
@@ -68,8 +65,7 @@ def test_send_mail_with_user_locale(client, env):
event, user, organizer = env event, user, organizer = env
user.locale = 'de' user.locale = 'de'
user.save() user.save()
mail(user, _('User'), mail('dummy@dummy.dummy', _('User'), 'mailtest.txt', {}, event, locale=user.locale)
'mailtest.txt', {}, event)
del event.settings['mail_from'] del event.settings['mail_from']
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1