diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index ef02c26339..8110031382 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -159,7 +159,7 @@ class ReactivateOrderForm(ForceQuotaConfirmationForm): pass -class CancelForm(ForceQuotaConfirmationForm): +class CancelForm(forms.Form): send_email = forms.BooleanField( required=False, label=_('Notify customer by email'), @@ -188,6 +188,7 @@ class CancelForm(ForceQuotaConfirmationForm): ) def __init__(self, *args, **kwargs): + self.instance = kwargs.pop("instance") super().__init__(*args, **kwargs) change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency) self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat( @@ -205,6 +206,20 @@ class CancelForm(ForceQuotaConfirmationForm): return val +class DenyForm(forms.Form): + send_email = forms.BooleanField( + required=False, + label=_('Notify customer by email'), + initial=True + ) + comment = forms.CharField( + label=_('Comment (will be sent to the user)'), + help_text=_('Will be included in the notification email when the respective placeholder is present in the ' + 'configured email text.'), + required=False, + ) + + class MarkPaidForm(ConfirmPaymentForm): send_email = forms.BooleanField( required=False, diff --git a/src/pretix/control/templates/pretixcontrol/order/deny.html b/src/pretix/control/templates/pretixcontrol/order/deny.html index a139909ea0..3c02c6f73b 100644 --- a/src/pretix/control/templates/pretixcontrol/order/deny.html +++ b/src/pretix/control/templates/pretixcontrol/order/deny.html @@ -1,5 +1,6 @@ {% extends "pretixcontrol/event/base.html" %} {% load i18n %} +{% load bootstrap3 %} {% block title %} {% trans "Deny order" %} {% endblock %} @@ -13,16 +14,7 @@
{% csrf_token %} -
- -
-

- - -

+ {% bootstrap_form form %}
{% trans "Modify orders" %} + + {% csrf_token %} +
+ {% blocktrans trimmed with label=label allowed=allowed.count total=total %} + The operation {{ label }} can be applied to {{ allowed }} of the + selected {{ total }} orders. + {% endblocktrans %} +
+ {% if allowed %} +
+ + + + + + + + + + + + {% for o in allowed|slice:":50" %} + + + + + + + + {% endfor %} + {% if allowed.count > 50 %} + + + + + + + + {% endif %} + +
{% trans "Order code" %}{% trans "User" %}{% trans "Order date" %}{% trans "Order total" %}{% trans "Status" %}
+ + + {{ o.code }} + + {% if o.testmode %} + {% trans "TEST MODE" %} + {% endif %} + + {{ o.email|default_if_none:"" }} + {% if o.invoice_address.name %} +
{{ o.invoice_address.name }} + {% endif %} +
+ + {{ o.datetime|date:"SHORT_DATETIME_FORMAT" }} + + {{ o.total|money:request.event.currency }} + {% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}
+
+

+ {% blocktrans trimmed %} + Do you want to continue? + {% endblocktrans %} +

+
+ + {% blocktrans trimmed %} + This operation cannot be reversed. + {% endblocktrans %} + +
+ {% for k, l in request.POST.lists %} + {% if "bulkactionform" not in k %} + {% for v in l %} + + {% endfor %} + {% endif %} + {% endfor %} + {% bootstrap_form form layout="control" %} + {% endif %} +
+ + {% trans "Cancel" %} + + {% if allowed %} + + {% endif %} +
+ +{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/orders/index.html b/src/pretix/control/templates/pretixcontrol/orders/index.html index e8ea05e3d5..21dcf344e5 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/index.html +++ b/src/pretix/control/templates/pretixcontrol/orders/index.html @@ -17,11 +17,12 @@ {% if not request.event.live %} + class="btn btn-primary btn-lg"> {% trans "Take your shop live" %} {% else %} - + {% trans "Go to the ticket shop" %} {% endif %} @@ -40,9 +41,10 @@

{% else %}
+ action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}">

- + @@ -82,7 +84,8 @@ {% endif %}

- + {% trans "Advanced search" %}
+
+
+ + +
+
+ {% include "pretixcontrol/pagination.html" %} {% endif %} {% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 03816e54c1..b0dbfc9137 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -414,6 +414,10 @@ urlpatterns = [ re_path(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'), re_path(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'), re_path(r'^orders/$', orders.OrderList.as_view(), name='event.orders'), + re_path(r'^orders/bulk/approve$', orders.OrderApproveBulkActionView.as_view(), name='event.orders.bulk.approve'), + re_path(r'^orders/bulk/deny$', orders.OrderDenyBulkActionView.as_view(), name='event.orders.bulk.deny'), + re_path(r'^orders/bulk/expire$', orders.OrderExpireBulkActionView.as_view(), name='event.orders.bulk.expire'), + re_path(r'^orders/bulk/delete$', orders.OrderDeleteBulkActionView.as_view(), name='event.orders.bulk.delete'), re_path(r'^orders/search$', orders.OrderSearch.as_view(), name='event.orders.search'), re_path(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'), re_path(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'), diff --git a/src/pretix/control/views/checkin.py b/src/pretix/control/views/checkin.py index ed36656a74..5de7c0d341 100644 --- a/src/pretix/control/views/checkin.py +++ b/src/pretix/control/views/checkin.py @@ -192,7 +192,6 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView): - template_name = 'pretixcontrol/organizers/device_bulk_edit.html' permission = ('can_change_orders', 'can_checkin_orders') context_object_name = 'device' diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 4fa571b5f5..8eb840906c 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -45,12 +45,12 @@ from urllib.parse import quote, urlencode from django import forms from django.conf import settings from django.contrib import messages -from django.core.exceptions import ValidationError +from django.core.exceptions import PermissionDenied, ValidationError from django.core.files import File from django.db import transaction from django.db.models import ( Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q, - Subquery, Sum, + QuerySet, Subquery, Sum, ) from django.forms import formset_factory from django.http import ( @@ -111,16 +111,16 @@ from pretix.base.signals import ( from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.views.mixins import OrderQuestionsViewMixin -from pretix.base.views.tasks import AsyncAction +from pretix.base.views.tasks import AsyncAction, AsyncFormView from pretix.control.forms.exports import ScheduledEventExportForm from pretix.control.forms.filter import ( EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm, RefundFilterForm, ) from pretix.control.forms.orders import ( - CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm, - ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm, - OrderLocaleForm, OrderMailForm, OrderPositionAddForm, + CancelForm, CommentForm, ConfirmPaymentForm, DenyForm, EventCancelForm, + ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm, + OrderFeeChangeForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm, OrderRefundForm, OtherOperationsForm, ReactivateOrderForm, ) @@ -137,10 +137,17 @@ logger = logging.getLogger(__name__) class OrderSearchMixin: + + @cached_property + def request_data(self): + if self.request.method == "POST": + return self.request.POST + return self.request.GET + def get_forms(self): f = [ EventOrderExpertFilterForm( - data=self.request.GET, + data=self.request_data, event=self.request.event, prefix='expert', ) @@ -160,6 +167,167 @@ class OrderSearch(OrderSearchMixin, EventPermissionRequiredMixin, TemplateView): return ctx +class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, AsyncFormView): + template_name = 'pretixcontrol/orders/bulk_action.html' + permission = 'can_change_orders' + form_class = forms.Form + + def get_queryset(self): + qs = Order.objects.filter( + event=self.request.event + ).select_related('invoice_address') + + if self.filter_form.is_valid(): + qs = self.filter_form.filter_qs(qs) + + for f in self.get_forms(): + if any(k.startswith(f.prefix) for k in self.request.POST.keys()): + if not f.is_valid(): + raise PermissionDenied("Invalid query") # better safe than sorry with this one + qs = f.filter_qs(qs) + + if 'order' in self.request_data and '__ALL' not in self.request_data: + qs = qs.filter( + id__in=self.request_data.getlist('order') + ) + elif '__ALL' not in self.request_data: + raise PermissionDenied("Invalid query") # better safe than sorry with this one + + return qs + + @cached_property + def filter_form(self): + return EventOrderFilterForm(data=self.request.POST, event=self.request.event) + + @property + def label(self) -> str: + raise NotImplementedError() + + def allowed_for(self, queryset: QuerySet) -> QuerySet: + raise NotImplementedError() + + def execute_single(self, instance, form: forms.Form): + raise NotImplementedError() + + def execute_bulk(self, queryset: QuerySet, form: forms.Form): + qs = self.allowed_for(self.allowed_for(self.get_queryset())) + total = qs.count() + for i, o in enumerate(qs): + self.execute_single(o, form) + if i % 100 == 0: + self.async_set_progress(i / total * 100) + + def get_error_url(self): + return self.get_success_url(None) + + def get(self, request, *args, **kwargs): + if 'async_id' in request.GET and settings.HAS_CELERY: + return self.get_result(request) + return render(request, self.template_name, self.get_context_data()) + + def get_success_url(self, value): + return reverse('control:event.orders', kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + }) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx['total'] = self.get_queryset().count() + ctx['allowed'] = self.allowed_for(self.get_queryset()) + ctx['label'] = self.label + ctx['form'] = self.get_form() + return ctx + + def get_form_kwargs(self): + kwargs = { + "initial": self.get_initial(), + "prefix": self.get_prefix(), + } + + if self.request.method in ("POST", "PUT") and self.request.POST.get("operation") == "confirm": + kwargs.update( + { + "data": self.request.POST, + "files": self.request.FILES, + } + ) + return kwargs + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + form = self.get_form() + if self.request.POST.get("operation") == "confirm" and form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def get_prefix(self): + return "bulkactionform" + + @transaction.atomic() + def async_form_valid(self, task, form): + self.execute_bulk(self.allowed_for(self.get_queryset()), form) + + +class OrderApproveBulkActionView(BaseOrderBulkActionView): + label = _("Approve") + + def allowed_for(self, queryset): + return queryset.filter( + status=Order.STATUS_PENDING, + require_approval=True, + ) + + def execute_single(self, instance, form: forms.Form): + approve_order(instance, user=self.request.user) + + +class OrderDenyBulkActionView(BaseOrderBulkActionView): + label = _("Deny") + form_class = DenyForm + + def allowed_for(self, queryset): + return queryset.filter( + status=Order.STATUS_PENDING, + require_approval=True, + ) + + def execute_single(self, instance, form: forms.Form): + deny_order(instance, user=self.request.user, + comment=form.cleaned_data.get('comment') or None, + send_mail=form.cleaned_data['send_email']) + + +class OrderExpireBulkActionView(BaseOrderBulkActionView): + label = _("Mark as expired if overdue") + + def allowed_for(self, queryset): + return queryset.filter( + status=Order.STATUS_PENDING, + require_approval=False, + expires__lt=now(), + ) + + def execute_single(self, instance, form: forms.Form): + mark_order_expired(instance, user=self.request.user) + + +class OrderDeleteBulkActionView(BaseOrderBulkActionView): + label = _("Delete") + + def allowed_for(self, queryset): + return queryset.filter( + testmode=True, + ) + + def execute_single(self, instance, form: forms.Form): + instance.gracefully_delete(user=self.request.user) + + class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView): model = Order context_object_name = 'orders' @@ -183,6 +351,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['filter_form'] = self.filter_form + ctx['filter_forms'] = self.get_forms() ctx['filter_strings'] = [] for f in self.get_forms(): @@ -607,21 +776,26 @@ class OrderDelete(OrderView): class OrderDeny(OrderView): permission = 'can_change_orders' - def post(self, *args, **kwargs): + def post(self, request, *args, **kwargs): if self.order.require_approval: - try: - deny_order(self.order, user=self.request.user, - comment=self.request.POST.get('comment'), - send_mail=self.request.POST.get('send_email') == 'on') - except OrderError as e: - messages.error(self.request, str(e)) + form = DenyForm(self.request.POST if self.request.method == "POST" else None) + if form.is_valid(): + try: + deny_order(self.order, user=self.request.user, + comment=self.request.POST.get('comment'), + send_mail=self.request.POST.get('send_email') == 'on') + except OrderError as e: + messages.error(self.request, str(e)) + else: + messages.success(self.request, _('The order has been denied and is therefore now canceled.')) else: - messages.success(self.request, _('The order has been denied and is therefore now canceled.')) + return self.get(request, *args, **kwargs) return redirect(self.get_order_url()) def get(self, *args, **kwargs): return render(self.request, 'pretixcontrol/order/deny.html', { 'order': self.order, + 'form': DenyForm(self.request.POST if self.request.method == "POST" else None) }) diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index 2ecf8c5c9c..9c846b2565 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -784,7 +784,7 @@ function setup_basics(el) { el.find("input[data-toggle-table]").each(function (ev) { var $toggle = $(this); var $actionButtons = $(".batch-select-actions button", this.form); - var countLabels = $("").appendTo($actionButtons); + var countLabels = $("").appendTo($actionButtons.filter(function () { return !$(this).closest(".dropdown-menu").length })); var $table = $toggle.closest("table"); var $selectAll = $table.find(".table-select-all"); var $rows = $table.find("tbody tr"); diff --git a/src/pretix/static/pretixcontrol/scss/_forms.scss b/src/pretix/static/pretixcontrol/scss/_forms.scss index f266232e10..9f4b3909c3 100644 --- a/src/pretix/static/pretixcontrol/scss/_forms.scss +++ b/src/pretix/static/pretixcontrol/scss/_forms.scss @@ -788,12 +788,32 @@ table td > .checkbox input[type="checkbox"] { padding-bottom: 5px; } - .batch-select-label { display: block; width: 100%; height: 1.5em; cursor: pointer; + margin: 0; +} + +.dropdown-menu > li > button.btn { + display: block; + padding: 3px 20px; + clear: both; + font-weight: 400; + line-height: $line-height-base; + color: $dropdown-link-color; + white-space: nowrap; + background: inherit; + border: 0; + width: 100%; + text-align: left; + + &:hover, &:focus { + color: $dropdown-link-hover-color; + text-decoration: none; + background-color: $dropdown-link-hover-bg; + } } .bulk-edit-field-group { diff --git a/src/tests/control/test_orders_bulk.py b/src/tests/control/test_orders_bulk.py new file mode 100644 index 0000000000..0d582b005e --- /dev/null +++ b/src/tests/control/test_orders_bulk.py @@ -0,0 +1,279 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-2021 rami.io GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# + +# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of +# the Apache License 2.0 can be obtained at . +# +# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A +# full history of changes and contributors is available at . +# +# This file contains Apache-licensed contributions copyrighted by: Daniel, Flavia Bastos, Jahongir +# +# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under the License. + +from datetime import timedelta +from decimal import Decimal + +import pytest +from bs4 import BeautifulSoup +from django.core import mail +from django.utils.timezone import now +from django_scopes import scopes_disabled +from tests.base import extract_form_fields + +from pretix.base.models import ( + Event, Item, Order, OrderPayment, OrderPosition, Organizer, Team, User, +) + + +@pytest.fixture +def env(): + o = Organizer.objects.create(name='Dummy', slug='dummy') + event = Event.objects.create( + organizer=o, name='Dummy', slug='dummy', + date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.stripe,tests.testdummy' + ) + event.settings.set('ticketoutput_testdummy__enabled', True) + user = User.objects.create_user('dummy@dummy.dummy', 'dummy') + t = Team.objects.create(organizer=o, can_view_orders=True, can_change_orders=True, can_manage_customers=True) + t.members.add(user) + t.limit_events.add(event) + ticket = Item.objects.create(event=event, name='Early-bird ticket', + category=None, default_price=23, + admission=True, personalized=True) + event.settings.set('attendee_names_asked', True) + event.settings.set('locales', ['en', 'de']) + return event, user, ticket + + +@pytest.fixture +def order1(env): + o = Order.objects.create( + code='FOO', event=env[0], email='foo@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() + timedelta(days=10), + total=14, locale='en' + ) + o.payments.create( + amount=o.total, provider='banktransfer', state=OrderPayment.PAYMENT_STATE_PENDING + ) + OrderPosition.objects.create( + order=o, + item=env[2], + variation=None, + price=Decimal("14"), + attendee_name_parts={'full_name': "Peter", "_scheme": "full"} + ) + return o + + +@pytest.fixture +def order2(env): + o = Order.objects.create( + code='BAR', event=env[0], email='bar@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() + timedelta(days=10), + total=14, locale='en' + ) + o.payments.create( + amount=o.total, provider='banktransfer', state=OrderPayment.PAYMENT_STATE_PENDING + ) + OrderPosition.objects.create( + order=o, + item=env[2], + variation=None, + price=Decimal("14"), + attendee_name_parts={'full_name': "Peter", "_scheme": "full"} + ) + return o + + +def _run_bulk_action(client, urlname, filter_post_data, submit_post_data): + resp = client.post(f'/control/event/dummy/dummy/orders/bulk/{urlname}', filter_post_data) + doc = BeautifulSoup(resp.content, "lxml") + data = extract_form_fields(doc.select('.container-fluid form')[0]) + for k, v in submit_post_data.items(): + if v is None: + data.pop(k, None) + else: + data[k] = v + resp = client.post(f'/control/event/dummy/dummy/orders/bulk/{urlname}', data, follow=True) + assert b'alert-success' in resp.content + + +@pytest.mark.django_db +def test_order_bulk_approve_explicit_id(client, env, order1, order2): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + order1.require_approval = True + order1.save() + order2.require_approval = True + order2.save() + + _run_bulk_action(client, 'approve', {'order': order1.pk}, {'operation': 'confirm'}) + + order1.refresh_from_db() + order2.refresh_from_db() + assert not order1.require_approval + assert order1.status == Order.STATUS_PENDING + assert order2.require_approval + + +@pytest.mark.django_db +def test_order_bulk_approve_search_form_all(client, env, order1, order2): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + order1.require_approval = True + order1.save() + order2.require_approval = True + order2.save() + + _run_bulk_action(client, 'approve', {'query': 'FOO', '__ALL': 'on'}, {'operation': 'confirm'}) + + order1.refresh_from_db() + order2.refresh_from_db() + assert not order1.require_approval + assert order1.status == Order.STATUS_PENDING + assert order2.require_approval + + +@pytest.mark.django_db +def test_order_bulk_approve_expert_search_form_all(client, env, order1, order2): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + order1.require_approval = True + order1.save() + order2.require_approval = True + order2.save() + + _run_bulk_action(client, 'approve', {'expert-email': 'foo@dummy.test', '__ALL': 'on'}, {'operation': 'confirm'}) + + order1.refresh_from_db() + order2.refresh_from_db() + assert not order1.require_approval + assert order1.status == Order.STATUS_PENDING + assert order2.require_approval + + +@pytest.mark.django_db +def test_order_bulk_approve_ignore_wrong_state(client, env, order1, order2): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + order1.require_approval = True + order1.save() + order2.require_approval = True + order2.status = Order.STATUS_CANCELED + order2.save() + + resp = client.post('/control/event/dummy/dummy/orders/bulk/approve', {'__ALL': 'on'}) + assert b'FOO' in resp.content + assert b'BAR' not in resp.content + + _run_bulk_action(client, 'approve', {'__ALL': 'on'}, {'operation': 'confirm'}) + + order1.refresh_from_db() + order2.refresh_from_db() + assert not order1.require_approval + assert order2.require_approval + + +@pytest.mark.django_db +def test_order_bulk_deny_ignore_wrong_state(client, env, order1, order2): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + order1.require_approval = True + order1.save() + order2.require_approval = False + order2.save() + mail.outbox = [] + + resp = client.post('/control/event/dummy/dummy/orders/bulk/deny', {'__ALL': 'on'}) + assert b'FOO' in resp.content + assert b'BAR' not in resp.content + + _run_bulk_action(client, 'deny', {'__ALL': 'on'}, {'operation': 'confirm', 'bulkactionform-send_email': 'on'}) + assert len(mail.outbox) == 1 + + order1.refresh_from_db() + order2.refresh_from_db() + assert order1.require_approval + assert order1.status == Order.STATUS_CANCELED + assert not order2.require_approval + assert order2.status == Order.STATUS_PENDING + + +@pytest.mark.django_db +def test_order_bulk_deny_send_no_mail(client, env, order1, order2): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + order1.require_approval = True + order1.save() + order2.require_approval = False + order2.save() + + mail.outbox = [] + _run_bulk_action(client, 'deny', {'__ALL': 'on'}, {'operation': 'confirm', 'bulkactionform-send_email': None}) + assert len(mail.outbox) == 0 + + +@pytest.mark.django_db +def test_order_bulk_expire_ignore_wrong_state(client, env, order1, order2): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + order1.expires = now() - timedelta(days=1) + order1.save() + order2.expires = now() + timedelta(days=1) + order2.save() + mail.outbox = [] + + resp = client.post('/control/event/dummy/dummy/orders/bulk/expire', {'__ALL': 'on'}) + assert b'FOO' in resp.content + assert b'BAR' not in resp.content + + _run_bulk_action(client, 'expire', {'__ALL': 'on'}, {'operation': 'confirm'}) + + order1.refresh_from_db() + order2.refresh_from_db() + assert order1.status == Order.STATUS_EXPIRED + assert order2.status == Order.STATUS_PENDING + + +@pytest.mark.django_db +def test_order_bulk_delete_ignore_wrong_state(client, env, order1, order2): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + order1.testmode = True + order1.save() + order2.testmode = False + order2.save() + mail.outbox = [] + + resp = client.post('/control/event/dummy/dummy/orders/bulk/delete', {'__ALL': 'on'}) + assert b'FOO' in resp.content + assert b'BAR' not in resp.content + + _run_bulk_action(client, 'delete', {'__ALL': 'on'}, {'operation': 'confirm'}) + + with scopes_disabled(): + assert Order.objects.get() == order2