forked from CGM_Public/pretix_original
Add a simple test mode (#1181)
- [x] Provide data model and configuration toggle - [x] Allow to delete individual test orders - [x] Add tests - [x] Add a prominent warning message to the backend if test mode orders exist (even though test mode is off), as this leads to wrong statistics - [x] Decide if and how to generate invoices for test orders as invoice numbers cannot be repeated or should not have gaps. - [x] Decide if and how we expose test orders through the API, since our difference pull mechanism relies on the fact that orders cannot be deleted. - [x] Decide if and how we want to couple test modes of payment providers? - [ ] pretix.eu: Ignore test orders for billing - [ ] Adjust payment providers: Mollie, bitpay, cash, fakepayment, sepadebit 
This commit is contained in:
@@ -48,7 +48,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'currency', 'date_from',
|
||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'has_subevents', 'meta_data', 'plugins')
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
fields = ('code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel')
|
||||
|
||||
@@ -413,7 +413,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
|
||||
@@ -16,7 +16,7 @@ from rest_framework.exceptions import (
|
||||
APIException, NotFound, PermissionDenied, ValidationError,
|
||||
)
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
@@ -51,10 +51,10 @@ class OrderFilter(FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale', 'require_approval']
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||
|
||||
|
||||
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderSerializer
|
||||
queryset = Order.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
@@ -378,6 +378,13 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.testmode:
|
||||
raise PermissionDenied('Only test mode orders can be deleted.')
|
||||
|
||||
with transaction.atomic():
|
||||
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
|
||||
|
||||
class OrderPositionFilter(FilterSet):
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
|
||||
@@ -9,7 +9,7 @@ import vat_moss.exchange_rates
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import pgettext
|
||||
from django.utils.translation import pgettext, ugettext
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
@@ -267,6 +267,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
canvas.saveState()
|
||||
canvas.setFont(self.font_regular, 8)
|
||||
|
||||
if self.invoice.order.testmode:
|
||||
canvas.saveState()
|
||||
canvas.setFont('OpenSansBd', 30)
|
||||
canvas.setFillColorRGB(32, 0, 0)
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
|
||||
canvas.restoreState()
|
||||
|
||||
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
|
||||
|
||||
|
||||
27
src/pretix/base/migrations/0111_auto_20190219_0949.py
Normal file
27
src/pretix/base/migrations/0111_auto_20190219_0949.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 2.1.5 on 2019-02-19 09:49
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0110_auto_20190219_1245'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='testmode',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='testmode',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -242,6 +242,8 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
:param organizer: The organizer this event belongs to
|
||||
:type organizer: Organizer
|
||||
:param testmode: This event is in test mode
|
||||
:type testmode: bool
|
||||
:param name: This event's full title
|
||||
:type name: str
|
||||
:param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
|
||||
@@ -271,6 +273,7 @@ class Event(EventMixin, LoggedModel):
|
||||
settings_namespace = 'event'
|
||||
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
|
||||
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
|
||||
testmode = models.BooleanField(default=False)
|
||||
name = I18nCharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Event name"),
|
||||
|
||||
@@ -150,6 +150,8 @@ class Invoice(models.Model):
|
||||
if not self.prefix:
|
||||
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
|
||||
if not self.invoice_no:
|
||||
if self.order.testmode:
|
||||
self.prefix += 'TEST-'
|
||||
for i in range(10):
|
||||
if self.event.settings.get('invoice_numbers_consecutive'):
|
||||
self.invoice_no = self._get_numeric_invoice_number()
|
||||
|
||||
@@ -76,6 +76,8 @@ class Order(LockModel, LoggedModel):
|
||||
:type event: Event
|
||||
:param email: The email of the person who ordered this
|
||||
:type email: str
|
||||
:param testmode: Whether this is a test mode order
|
||||
:type testmode: bool
|
||||
:param locale: The locale of this order
|
||||
:type locale: str
|
||||
:param secret: A secret string that is required to modify the order
|
||||
@@ -121,6 +123,7 @@ class Order(LockModel, LoggedModel):
|
||||
verbose_name=_("Status"),
|
||||
db_index=True
|
||||
)
|
||||
testmode = models.BooleanField(default=False)
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
verbose_name=_("Event"),
|
||||
@@ -185,6 +188,23 @@ class Order(LockModel, LoggedModel):
|
||||
def __str__(self):
|
||||
return self.full_code
|
||||
|
||||
def gracefully_delete(self, user=None, auth=None):
|
||||
if not self.testmode:
|
||||
raise TypeError("Only test mode orders can be deleted.")
|
||||
self.event.log_action(
|
||||
'pretix.event.order.deleted', user=user, auth=auth,
|
||||
data={
|
||||
'code': self.code,
|
||||
}
|
||||
)
|
||||
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
|
||||
OrderPosition.all.filter(order=self).delete()
|
||||
OrderFee.all.filter(order=self).delete()
|
||||
self.refunds.all().delete()
|
||||
self.payments.all().delete()
|
||||
self.event.cache.delete('complain_testmode_orders')
|
||||
self.delete()
|
||||
|
||||
@property
|
||||
def fees(self):
|
||||
"""
|
||||
@@ -490,6 +510,10 @@ class Order(LockModel, LoggedModel):
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
while True:
|
||||
code = get_random_string(length=settings.ENTROPY['order_code'], allowed_chars=charset)
|
||||
if self.testmode:
|
||||
# Subtle way to recognize test orders while debugging: They all contain a 0 at the second place,
|
||||
# even though zeros are not used outside test mode.
|
||||
code = code[0] + "0" + code[2:]
|
||||
if not Order.objects.filter(event__organizer=self.event.organizer, code=code).exists():
|
||||
self.code = code
|
||||
return
|
||||
|
||||
@@ -88,6 +88,18 @@ class BasePaymentProvider:
|
||||
"""
|
||||
return self.settings.get('_enabled', as_type=bool)
|
||||
|
||||
@property
|
||||
def test_mode_message(self) -> str:
|
||||
"""
|
||||
If this property is set to a string, this will be displayed when this payment provider is selected
|
||||
while the event is in test mode. You should use it to explain to your user how your plugin behaves,
|
||||
e.g. if it falls back to a test mode automatically as well or if actual payments will be performed.
|
||||
|
||||
If you do not set this (or, return ``None``), pretix will show a default message warning the user
|
||||
that this plugin does not support test mode payments.
|
||||
"""
|
||||
return None
|
||||
|
||||
def calculate_fee(self, price: Decimal) -> Decimal:
|
||||
"""
|
||||
Calculate the fee for this payment provider which will be added to
|
||||
@@ -713,6 +725,11 @@ class ManualPayment(BasePaymentProvider):
|
||||
identifier = 'manual'
|
||||
verbose_name = _('Manual payment')
|
||||
|
||||
@property
|
||||
def test_mode_message(self):
|
||||
return _('In test mode, you can just manually mark this order as paid in the backend after it has been '
|
||||
'created.')
|
||||
|
||||
@property
|
||||
def is_implicit(self):
|
||||
return 'pretix.plugins.manualpayment' not in self.event.plugins
|
||||
|
||||
@@ -130,6 +130,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
if order:
|
||||
if order.testmode:
|
||||
subject = "[TESTMODE] " + subject
|
||||
body_plain += _(
|
||||
"You are receiving this email because you placed an order for {event}."
|
||||
).format(event=event.name)
|
||||
|
||||
@@ -533,6 +533,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
datetime=now_dt,
|
||||
locale=locale,
|
||||
total=total,
|
||||
testmode=event.testmode,
|
||||
meta_info=json.dumps(meta_info or {}),
|
||||
require_approval=any(p.item.require_approval for p in positions),
|
||||
sales_channel=sales_channel
|
||||
|
||||
@@ -52,6 +52,15 @@ def contextprocessor(request):
|
||||
|
||||
ctx['has_domain'] = request.event.organizer.domains.exists()
|
||||
|
||||
if not request.event.testmode:
|
||||
complain_testmode_orders = request.event.cache.get('complain_testmode_orders')
|
||||
if complain_testmode_orders is None:
|
||||
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
|
||||
request.event.cache.set('complain_testmode_orders', complain_testmode_orders, 30)
|
||||
ctx['complain_testmode_orders'] = complain_testmode_orders
|
||||
else:
|
||||
ctx['complain_testmode_orders'] = False
|
||||
|
||||
if not request.event.live and ctx['has_domain']:
|
||||
child_sess = request.session.get('child_session_{}'.format(request.event.pk))
|
||||
s = SessionStore()
|
||||
|
||||
@@ -212,6 +212,7 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
('overpaid', _('Overpaid')),
|
||||
('underpaid', _('Underpaid')),
|
||||
('pendingpaid', _('Pending (but fully paid)')),
|
||||
('testmode', _('Test mode')),
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
@@ -298,6 +299,10 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=True
|
||||
)
|
||||
elif fdata.get('status') == 'testmode':
|
||||
qs = qs.filter(
|
||||
testmode=True
|
||||
)
|
||||
elif fdata.get('status') == 'cp':
|
||||
s = OrderPosition.objects.filter(
|
||||
order=OuterRef('pk')
|
||||
|
||||
@@ -172,6 +172,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.paid': _('The order has been marked as paid.'),
|
||||
'pretix.event.order.refunded': _('The order has been refunded.'),
|
||||
'pretix.event.order.canceled': _('The order has been canceled.'),
|
||||
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
|
||||
'pretix.event.order.placed': _('The order has been created.'),
|
||||
'pretix.event.order.placed.require_approval': _('The order requires approval before it can continue to be processed.'),
|
||||
'pretix.event.order.approved': _('The order has been approved.'),
|
||||
@@ -268,6 +269,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.plugins.disabled': _('A plugin has been disabled.'),
|
||||
'pretix.event.live.activated': _('The shop has been taken live.'),
|
||||
'pretix.event.live.deactivated': _('The shop has been taken offline.'),
|
||||
'pretix.event.testmode.activated': _('The shop has been taken into test mode.'),
|
||||
'pretix.event.testmode.deactivated': _('The test mode has been disabled.'),
|
||||
'pretix.event.added': _('The event has been created.'),
|
||||
'pretix.event.changed': _('The event settings have been changed.'),
|
||||
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
|
||||
|
||||
@@ -235,6 +235,14 @@
|
||||
</ul>
|
||||
</div>
|
||||
<ul class="nav" id="side-menu">
|
||||
{% if request.event and request.event.testmode %}
|
||||
<li class="testmode">
|
||||
<a href="{% url "control:event.live" event=request.event.slug organizer=request.organizer.slug %}">
|
||||
<i class="fa fa-warning fa-fw"></i>
|
||||
{% trans "TEST MODE" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% block nav %}
|
||||
{% for nav in nav_items %}
|
||||
<li>
|
||||
@@ -317,6 +325,20 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if complain_testmode_orders %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
Your event contains <strong>test mode orders</strong> even though <strong>test mode has been disabled</strong>.
|
||||
You should delete those orders to make sure they do not show up in your reports and statistics and block people from
|
||||
actually buying tickets.
|
||||
{% endblocktrans %}
|
||||
<strong>
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.organizer.slug %}?status=testmode">
|
||||
{% trans "Show all test mode orders" %}
|
||||
</a>
|
||||
</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if warning_update_check_active %}
|
||||
<div class="alert alert-info">
|
||||
<a href="{% url "control:global.update" %}">
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
{% if e.order.status == "n" %}
|
||||
<span class="label label-warning">{% trans "unpaid" %}</span>
|
||||
{% endif %}
|
||||
{% if e.order.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ e.item }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
||||
<td>{{ e.order.email }}</td>
|
||||
|
||||
@@ -3,49 +3,113 @@
|
||||
{% load bootstrap3 %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Shop status" %}</h1>
|
||||
{% if request.event.live %}
|
||||
<p>
|
||||
{% trans "Your shop is currently live. If you take it down, it will only be visible to you and your team." %}
|
||||
</p>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="live" value="false">
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Go offline" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans "Your ticket shop is currently not live. It is thus only visible to you and your team, not to any visitors." %}
|
||||
</p>
|
||||
{% if issues|length > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% trans "Shop visibility" %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if request.event.live %}
|
||||
<p>
|
||||
{% trans "To publish your ticket shop, you first need to resolve the following issues:" %}
|
||||
{% trans "Your shop is currently live. If you take it down, it will only be visible to you and your team." %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
<li>{{ issue|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans "If you want to, you can publish your ticket shop now." %}
|
||||
</p>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="live" value="true">
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Go live" %}
|
||||
<form action="" method="post" class="text-right">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="live" value="false">
|
||||
<button type="submit" class="btn btn-lg btn-danger btn-save">
|
||||
{% trans "Go offline" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% else %}
|
||||
{% if issues|length > 0 %}
|
||||
<p>
|
||||
{% trans "Your ticket shop is currently not live. It is thus only visible to you and your team, not to any visitors." %}
|
||||
</p>
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
{% trans "To publish your ticket shop, you first need to resolve the following issues:" %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
<li>{{ issue|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="test-right">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-save" disabled>
|
||||
{% trans "Go live" %}
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans "Your ticket shop is currently not live. It is thus only visible to you and your team, not to any visitors." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "If you want to, you can publish your ticket shop now." %}
|
||||
</p>
|
||||
<form action="" method="post" class="text-right">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="live" value="true">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-save">
|
||||
{% trans "Go live" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% trans "Test mode" %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if request.event.testmode %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="testmode" value="false">
|
||||
<p>
|
||||
{% trans "Your shop is currently in test mode. All orders are not persistant and can be deleted at any point." %}
|
||||
</p>
|
||||
<div class="form-inline">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="delete" value="yes" />
|
||||
{% trans "Permanently delete all orders created in test mode" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn btn-lg btn-primary btn-save">
|
||||
{% trans "Disable test mode" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans "Your shop is currently in production mode." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "If you want to do some test orders, you can enable test mode for your shop. As long as the shop is in test mode, all orders that are created are marked as test orders and can be deleted again." %}
|
||||
<strong>
|
||||
{% trans "Please note that test orders still count into your quotas, actually use vouchers and might perform actual payments. The only difference is that you can delete test orders. Use at your own risk!" %}
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Also, test mode only covers the main web shop. Orders created through other sales channels such as the box office or resellers module are still created as production orders." %}
|
||||
</p>
|
||||
{% if actual_orders %}
|
||||
<div class="alert alert-danger">
|
||||
{% trans "It looks like you already have some real orders in your shop. We do not recommend enabling test mode if your customers already know your shop, as it will confuse them." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="text-right">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="testmode" value="true">
|
||||
<button type="submit" class="btn btn-danger btn-lg btn-save">
|
||||
{% trans "Enable test mode" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
31
src/pretix/control/templates/pretixcontrol/order/delete.html
Normal file
31
src/pretix/control/templates/pretixcontrol/order/delete.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{% trans "Delete order" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Delete order" %}
|
||||
</h1>
|
||||
<p>{% blocktrans trimmed %}
|
||||
Do you really want to delete this order? <strong>You really cannot revert this action and we can't either.</strong>
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% trans "No, take me back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<button class="btn btn-block btn-danger btn-lg" type="submit">
|
||||
{% trans "Yes, delete order" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -16,6 +16,9 @@
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Order details: {{ code }}
|
||||
{% endblocktrans %}
|
||||
{% if order.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
{% include "pretixcontrol/orders/fragment_order_status.html" with order=order class="pull-right" %}
|
||||
</h1>
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
@@ -24,6 +27,13 @@
|
||||
{% csrf_token %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
{% if order.testmode %}
|
||||
<a href="{% url "control:event.order.delete" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
|
||||
class="btn btn-danger">
|
||||
<span class="fa fa-trash"></span>
|
||||
{% trans "Delete" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if order.require_approval and order.status == 'n' %}
|
||||
<a href="{% url "control:event.order.approve" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
|
||||
class="btn btn-primary">
|
||||
|
||||
@@ -114,9 +114,11 @@
|
||||
<strong>
|
||||
<a
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
||||
{{ o.code }}
|
||||
</a>
|
||||
{{ o.code }}</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.email|default_if_none:"" }}
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code %}">
|
||||
{{ r.order.code }}</a>-R-{{ r.local_id }}
|
||||
</strong>
|
||||
{% if r.order.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ r.payment_provider.verbose_name }}
|
||||
|
||||
@@ -59,9 +59,11 @@
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% url "control:event.order" event=o.event.slug organizer=o.event.organizer.slug code=o.code %}">
|
||||
{{ o.event.slug|upper }}-{{ o.code }}
|
||||
</a>
|
||||
{{ o.event.slug|upper }}-{{ o.code }}</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ o.event.name }}</td>
|
||||
<td>
|
||||
|
||||
@@ -222,6 +222,8 @@ urlpatterns = [
|
||||
name='event.order.approve'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/deny$', orders.OrderDeny.as_view(),
|
||||
name='event.order.deny'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/delete$', orders.OrderDelete.as_view(),
|
||||
name='event.order.delete'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/info', orders.OrderModifyInformation.as_view(),
|
||||
name='event.order.info'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/sendmail$', orders.OrderSendMail.as_view(),
|
||||
|
||||
@@ -183,8 +183,20 @@ def shop_state_widget(sender, **kwargs):
|
||||
'priority': 1000,
|
||||
'content': '<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>'.format(
|
||||
t1=_('Your ticket shop is'), t2=_('Click here to change'),
|
||||
state=_('live') if sender.live else _('not yet public'),
|
||||
icon='fa-check-circle' if sender.live else 'fa-times-circle',
|
||||
state=_('live') if sender.live and not sender.testmode else (
|
||||
_('live and in test mode') if sender.live else (
|
||||
_('not yet public') if not sender.testmode else (
|
||||
_('in private test mode')
|
||||
)
|
||||
)
|
||||
),
|
||||
icon='fa-check-circle' if sender.live and not sender.testmode else (
|
||||
'fa-warning' if sender.live else (
|
||||
'fa-times-circle' if not sender.testmode else (
|
||||
'fa-times-circle'
|
||||
)
|
||||
)
|
||||
),
|
||||
cls='live' if sender.live else 'off'
|
||||
),
|
||||
'url': reverse('control:event.live', kwargs={
|
||||
|
||||
@@ -854,23 +854,55 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['issues'] = self.request.event.live_issues
|
||||
ctx['actual_orders'] = self.request.event.orders.filter(testmode=False).exists()
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get("live") == "true" and not self.request.event.live_issues:
|
||||
request.event.live = True
|
||||
request.event.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.live.activated', user=self.request.user, data={}
|
||||
)
|
||||
with transaction.atomic():
|
||||
request.event.live = True
|
||||
request.event.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.live.activated', user=self.request.user, data={}
|
||||
)
|
||||
messages.success(self.request, _('Your shop is live now!'))
|
||||
elif request.POST.get("live") == "false":
|
||||
request.event.live = False
|
||||
request.event.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.live.deactivated', user=self.request.user, data={}
|
||||
)
|
||||
with transaction.atomic():
|
||||
request.event.live = False
|
||||
request.event.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.live.deactivated', user=self.request.user, data={}
|
||||
)
|
||||
messages.success(self.request, _('We\'ve taken your shop down. You can re-enable it whenever you want!'))
|
||||
elif request.POST.get("testmode") == "true":
|
||||
with transaction.atomic():
|
||||
request.event.testmode = True
|
||||
request.event.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.testmode.activated', user=self.request.user, data={}
|
||||
)
|
||||
messages.success(self.request, _('Your shop is now in test mode!'))
|
||||
elif request.POST.get("testmode") == "false":
|
||||
with transaction.atomic():
|
||||
request.event.testmode = False
|
||||
request.event.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.testmode.deactivated', user=self.request.user, data={
|
||||
'delete': (request.POST.get("delete") == "yes")
|
||||
}
|
||||
)
|
||||
request.event.cache.delete('complain_testmode_orders')
|
||||
if request.POST.get("delete") == "yes":
|
||||
try:
|
||||
with transaction.atomic():
|
||||
for order in request.event.orders.filter(testmode=True):
|
||||
order.gracefully_delete(user=self.request.user)
|
||||
except ProtectedError:
|
||||
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
|
||||
'created by plug-ins) do not allow it.'))
|
||||
else:
|
||||
request.event.cache.set('complain_testmode_orders', False, 30)
|
||||
messages.success(self.request, _('We\'ve disabled test mode for you. Let\'s sell some real tickets!'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
|
||||
@@ -11,7 +11,9 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, IntegerField, OuterRef, Subquery
|
||||
from django.db.models import (
|
||||
Count, IntegerField, OuterRef, ProtectedError, Subquery,
|
||||
)
|
||||
from django.http import (
|
||||
FileResponse, Http404, HttpResponseNotAllowed, JsonResponse,
|
||||
)
|
||||
@@ -398,6 +400,35 @@ class OrderApprove(OrderView):
|
||||
})
|
||||
|
||||
|
||||
class OrderDelete(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.order.testmode:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
self.order.gracefully_delete(user=self.request.user)
|
||||
messages.success(self.request, _('The order has been deleted.'))
|
||||
return redirect(reverse('control:event.orders', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.organizer.slug,
|
||||
}))
|
||||
except ProtectedError:
|
||||
messages.error(self.request, _('The order could not be deleted as some constraints (e.g. data created '
|
||||
'by plug-ins) do not allow it.'))
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
if not self.order.testmode:
|
||||
messages.error(self.request, _('Only orders created in test mode can be deleted.'))
|
||||
return redirect(self.get_order_url())
|
||||
return render(self.request, 'pretixcontrol/order/delete.html', {
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
class OrderDeny(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class OrderSearch(PaginationMixin, ListView):
|
||||
"""
|
||||
return qs.only(
|
||||
'id', 'invoice_address__name_cached', 'invoice_address__name_parts', 'code', 'event', 'email',
|
||||
'datetime', 'total', 'status', 'require_approval'
|
||||
'datetime', 'total', 'status', 'require_approval', 'testmode'
|
||||
).prefetch_related(
|
||||
'event', 'event__organizer'
|
||||
).select_related('invoice_address')
|
||||
|
||||
@@ -101,6 +101,11 @@ class BankTransfer(BasePaymentProvider):
|
||||
def public_name(self):
|
||||
return str(self.settings.get('public_name', as_type=LazyI18nString) or self.verbose_name)
|
||||
|
||||
@property
|
||||
def test_mode_message(self):
|
||||
return _('In test mode, you can just manually mark this order as paid in the backend after it has been '
|
||||
'created.')
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
d = OrderedDict(
|
||||
|
||||
@@ -36,6 +36,18 @@ class Paypal(BasePaymentProvider):
|
||||
super().__init__(event)
|
||||
self.settings = SettingsSandbox('payment', 'paypal', event)
|
||||
|
||||
@property
|
||||
def test_mode_message(self):
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
# in OAuth mode, sandbox mode needs to be set global
|
||||
is_sandbox = self.settings.connect_endpoint == 'sandbox'
|
||||
else:
|
||||
is_sandbox = self.settings.get('endpoint') == 'sandbox'
|
||||
if is_sandbox:
|
||||
return _('The PayPal sandbox is being used, you can test without actually sending money but you will need a '
|
||||
'PayPal sandbox user to log in.')
|
||||
return None
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
</p>
|
||||
<h2>{% trans "3. Start scanning tickets" %}</h2>
|
||||
<script type="text/javascript" src="{% static "pretixplugins/pretixdroid/pretixdroid.js" %}"></script>
|
||||
{% if request.event.testmode %}
|
||||
<div class="alert-info">
|
||||
{% trans "Test mode orders will only be scanned if you scan online. If you scan in asynchronous mode, test mode orders won't be there." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ class ApiDownloadView(ApiView):
|
||||
order__event=self.event,
|
||||
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if self.config.list.include_pending else
|
||||
[]),
|
||||
order__testmode=False,
|
||||
subevent=self.config.list.subevent
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.http import urlquote
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext, ugettext, ugettext_lazy as _
|
||||
from django_countries import countries
|
||||
@@ -111,6 +112,8 @@ class StripeSettingsHolder(BasePaymentProvider):
|
||||
('live', pgettext('stripe', 'Live')),
|
||||
('test', pgettext('stripe', 'Testing')),
|
||||
),
|
||||
help_text=_('If your event is in test mode, we will always use Stripe\'s test API, '
|
||||
'regardless of this setting.')
|
||||
)),
|
||||
]
|
||||
else:
|
||||
@@ -230,6 +233,21 @@ class StripeMethod(BasePaymentProvider):
|
||||
super().__init__(event)
|
||||
self.settings = SettingsSandbox('payment', 'stripe', event)
|
||||
|
||||
@property
|
||||
def test_mode_message(self):
|
||||
if self.settings.connect_client_id and not self.settings.secret_key:
|
||||
is_testmode = True
|
||||
else:
|
||||
is_testmode = '_test_' in self.settings.secret_key
|
||||
if is_testmode:
|
||||
return mark_safe(
|
||||
_('The Stripe plugin is operating in test mode. You can use one of <a {args}>many test '
|
||||
'cards</a> to perform a transaction. No money will actually be transferred.').format(
|
||||
args='href="https://stripe.com/docs/testing#cards" target="_blank"'
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
return {}
|
||||
@@ -262,7 +280,7 @@ class StripeMethod(BasePaymentProvider):
|
||||
@property
|
||||
def api_kwargs(self):
|
||||
if self.settings.connect_client_id and self.settings.connect_user_id:
|
||||
if self.settings.get('endpoint', 'live') == 'live':
|
||||
if self.settings.get('endpoint', 'live') == 'live' and not self.event.testmode:
|
||||
kwargs = {
|
||||
'api_key': self.settings.connect_secret_key,
|
||||
'stripe_account': self.settings.connect_user_id
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% endblock %}
|
||||
{% block page %}
|
||||
<div class="page-header">
|
||||
{% if request.event.settings.organizer_link_back %}
|
||||
{% if request.event.settings.organizer_link_back %}
|
||||
<p>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.index" %}">
|
||||
« {% blocktrans trimmed with name=request.organizer.name %}
|
||||
@@ -64,6 +64,13 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% if request.event.testmode %}
|
||||
<div class="alert alert-warning">
|
||||
<strong>
|
||||
{% trans "This ticket shop is currently in test mode. Please do not perform any real purchases as your order might be deleted without notice." %}
|
||||
</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }}">
|
||||
@@ -73,6 +80,13 @@
|
||||
{% endif %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% if request.event.testmode %}
|
||||
<div class="alert alert-testmode alert-warning">
|
||||
<strong>
|
||||
{% trans "This ticket shop is currently in test mode. Please do not perform any real purchases as your order might be deleted without notice." %}
|
||||
</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block footer %}
|
||||
{% if request.event.settings.contact_mail %}
|
||||
|
||||
@@ -31,6 +31,20 @@
|
||||
<div id="payment_{{ p.provider.identifier }}"
|
||||
class="panel-collapse collapsed {% if selected == p.provider.identifier %}in{% endif %}">
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if request.event.testmode %}
|
||||
{% if p.provider.test_mode_message %}
|
||||
<div class="alert alert-info">
|
||||
{{ p.provider.test_mode_message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "This payment provider does not provide support for testmode." %}
|
||||
<strong>
|
||||
{% trans "If you continue, actual money might be transferred." %}
|
||||
</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ p.form }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Your order: {{ code }}
|
||||
{% endblocktrans %}
|
||||
{% if order.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
{% include "pretixpresale/event/fragment_order_status.html" with order=order class="pull-right" %}
|
||||
<div class="clearfix"></div>
|
||||
</h2>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
.sidebar-nav li > a > .fa {
|
||||
color: $navbar-inverse-bg;
|
||||
}
|
||||
.nav .testmode a {
|
||||
background: $brand-warning;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
&:hover, &:active, &:focus {
|
||||
background: $btn-warning-border;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-width: 0px;
|
||||
|
||||
@@ -604,3 +604,7 @@ details summary {
|
||||
color: $brand-danger;
|
||||
text-decoration: line-through !important;
|
||||
}
|
||||
|
||||
h1 .label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -281,6 +281,12 @@ details summary {
|
||||
form.download-btn-form {
|
||||
display: inline;
|
||||
}
|
||||
.alert-testmode {
|
||||
margin-top: 20px;
|
||||
}
|
||||
h2 .label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@import "_iframe.scss";
|
||||
@import "_a11y.scss";
|
||||
|
||||
Reference in New Issue
Block a user