mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Allow to bulk-edit devices (#2583)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -39,6 +39,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
@@ -268,6 +269,69 @@ class DeviceForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceBulkEditForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
organizer = kwargs.pop('organizer')
|
||||
self.mixed_values = kwargs.pop('mixed_values')
|
||||
self.queryset = kwargs.pop('queryset')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['limit_events'].queryset = organizer.events.all().order_by(
|
||||
'-has_subevents', '-date_from'
|
||||
)
|
||||
self.fields['gate'].queryset = organizer.gates.all()
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if self.prefix + '__events' in self.data.getlist('_bulk') and not d['all_events'] and not d['limit_events']:
|
||||
raise ValidationError(_('Your device will not have access to anything, please select some events.'))
|
||||
|
||||
return d
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['all_events', 'limit_events', 'security_profile', 'gate']
|
||||
widgets = {
|
||||
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_events',
|
||||
'class': 'scrolling-multiple-choice scrolling-multiple-choice-large',
|
||||
}),
|
||||
}
|
||||
field_classes = {
|
||||
'limit_events': SafeEventMultipleChoiceField
|
||||
}
|
||||
|
||||
def save(self, commit=True):
|
||||
objs = list(self.queryset)
|
||||
fields = set()
|
||||
|
||||
check_map = {
|
||||
'all_events': '__events',
|
||||
'limit_events': '__events',
|
||||
}
|
||||
for k in self.fields:
|
||||
cb_val = self.prefix + check_map.get(k, k)
|
||||
if cb_val not in self.data.getlist('_bulk'):
|
||||
continue
|
||||
|
||||
fields.add(k)
|
||||
for obj in objs:
|
||||
if k == 'limit_events':
|
||||
getattr(obj, k).set(self.cleaned_data[k])
|
||||
else:
|
||||
setattr(obj, k, self.cleaned_data[k])
|
||||
|
||||
if fields:
|
||||
Device.objects.bulk_update(objs, [f for f in fields if f != 'limit_events'], 200)
|
||||
|
||||
def full_clean(self):
|
||||
if len(self.data) == 0:
|
||||
# form wasn't submitted
|
||||
self._errors = ErrorDict()
|
||||
return
|
||||
super().full_clean()
|
||||
|
||||
|
||||
class OrganizerSettingsForm(SettingsForm):
|
||||
timezone = forms.ChoiceField(
|
||||
choices=((a, a) for a in common_timezones),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Change multiple devices" %}
|
||||
<small>
|
||||
{% blocktrans trimmed with number=devices.count %}
|
||||
{{ number }} selected
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="hidden">
|
||||
{% for d in devices %}
|
||||
<input type="hidden" name="device" value="{{ d.pk }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
<div class="bulk-edit-field-group">
|
||||
<label class="field-toggle">
|
||||
<input type="checkbox" name="_bulk" value="{{ form.prefix }}__events" {% if form.prefix|add:"__events" in bulk_selected %}checked{% endif %}>
|
||||
{% trans "change" context "form_bulk" %}
|
||||
</label>
|
||||
<div class="field-content">
|
||||
{% bootstrap_field form.all_events layout="control" %}
|
||||
{% bootstrap_field form.limit_events layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p> </p>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.security_profile layout="bulkedit" %}
|
||||
{% bootstrap_field form.gate layout="bulkedit" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -21,7 +21,7 @@
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
@@ -53,101 +53,139 @@
|
||||
</div>
|
||||
<p>
|
||||
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Device ID" %}
|
||||
<a href="?{% url_replace request 'ordering' '-device_id' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'device_id' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>{% trans "Name" %}
|
||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>{% trans "Hardware model" %}</th>
|
||||
<th>{% trans "Software" %}</th>
|
||||
<th>{% trans "Setup date" %}
|
||||
<a href="?{% url_replace request 'ordering' '-initialized' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'initialized' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>{% trans "Events" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr {% if d.revoked %}class="text-muted"{% endif %}>
|
||||
<td>
|
||||
{{ d.device_id }}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.revoked %}<del>{% endif %}
|
||||
{{ d.name }}
|
||||
{% if d.revoked %}</del>{% endif %}
|
||||
{% if d.gate %}
|
||||
<br>
|
||||
<small class="text-muted">{{ d.gate.name }}</small>
|
||||
{% endif %}
|
||||
<br>
|
||||
<small class="text-muted">{{ d.unique_serial }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ d.hardware_brand|default_if_none:"" }} {{ d.hardware_model|default_if_none:"" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.initialized %}
|
||||
{{ d.initialized|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
<em>{% trans "Not yet initialized" %}</em>
|
||||
{% endif %}
|
||||
{% if d.revoked %}
|
||||
<span class="label label-danger">{% trans "Revoked" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.all_events %}
|
||||
{% trans "All" %}
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for e in d.limit_events.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.index" organizer=request.organizer.slug event=e.slug %}">
|
||||
{{ e }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if not d.initialized %}
|
||||
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
||||
{% trans "Connect" %}</a>
|
||||
{% elif d.api_token %}
|
||||
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm">
|
||||
{% trans "Revoke access" %}</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm">
|
||||
<span class="fa fa-list-alt"></span>
|
||||
{% trans "Logs" %}
|
||||
</a>
|
||||
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="hidden">
|
||||
{{ filter_form.as_p }}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
</th>
|
||||
<th>{% trans "Device ID" %}
|
||||
<a href="?{% url_replace request 'ordering' '-device_id' %}"><i
|
||||
class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'device_id' %}"><i
|
||||
class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Name" %}
|
||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Hardware model" %}</th>
|
||||
<th>{% trans "Software" %}</th>
|
||||
<th>{% trans "Setup date" %}
|
||||
<a href="?{% url_replace request 'ordering' '-initialized' %}"><i
|
||||
class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'initialized' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Events" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<tr class="table-select-all warning hidden">
|
||||
<td>
|
||||
<input type="checkbox" name="__ALL" id="__all"
|
||||
data-results-total="{{ page_obj.paginator.count }}">
|
||||
</td>
|
||||
<td colspan="7">
|
||||
<label for="__all">
|
||||
{% trans "Select all results on other pages as well" %}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr {% if d.revoked %}class="text-muted"{% endif %}>
|
||||
<td>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" name="device"
|
||||
class="batch-select-checkbox"
|
||||
value="{{ d.pk }}"/></label>
|
||||
</td>
|
||||
<td>
|
||||
{{ d.device_id }}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.revoked %}
|
||||
<del>{% endif %}
|
||||
{{ d.name }}
|
||||
{% if d.revoked %}</del>{% endif %}
|
||||
{% if d.gate %}
|
||||
<br>
|
||||
<small class="text-muted">{{ d.gate.name }}</small>
|
||||
{% endif %}
|
||||
<br>
|
||||
<small class="text-muted">{{ d.unique_serial }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ d.hardware_brand|default_if_none:"" }} {{ d.hardware_model|default_if_none:"" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.initialized %}
|
||||
{{ d.initialized|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
<em>{% trans "Not yet initialized" %}</em>
|
||||
{% endif %}
|
||||
{% if d.revoked %}
|
||||
<span class="label label-danger">{% trans "Revoked" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.all_events %}
|
||||
{% trans "All" %}
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for e in d.limit_events.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.index" organizer=request.organizer.slug event=e.slug %}">
|
||||
{{ e }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if not d.initialized %}
|
||||
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
||||
{% trans "Connect" %}</a>
|
||||
{% elif d.api_token %}
|
||||
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm">
|
||||
{% trans "Revoke access" %}</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm">
|
||||
<span class="fa fa-list-alt"></span>
|
||||
{% trans "Logs" %}
|
||||
</a>
|
||||
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="batch-select-actions">
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit">
|
||||
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
|
||||
<i class="fa fa-trash"></i>{% trans "Delete selected" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="disable"
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit"
|
||||
formaction="{% url "control:event.subevents.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||
</button>
|
||||
|
||||
@@ -164,6 +164,8 @@ urlpatterns = [
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/add$', organizer.DeviceCreateView.as_view(),
|
||||
name='organizer.device.add'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/bulk_edit$', organizer.DeviceBulkUpdateView.as_view(),
|
||||
name='organizer.device.bulk_edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/edit$', organizer.DeviceUpdateView.as_view(),
|
||||
name='organizer.device.edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/connect$', organizer.DeviceConnectView.as_view(),
|
||||
|
||||
@@ -42,14 +42,14 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db import connections, transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, IntegerField, Max, Min, OuterRef, Prefetch, ProtectedError,
|
||||
Q, Subquery, Sum,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.forms import DecimalField
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -89,11 +89,11 @@ from pretix.control.forms.filter import (
|
||||
)
|
||||
from pretix.control.forms.orders import ExporterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
CustomerCreateForm, CustomerUpdateForm, DeviceForm, EventMetaPropertyForm,
|
||||
GateForm, GiftCardCreateForm, GiftCardUpdateForm, MailSettingsForm,
|
||||
MembershipTypeForm, MembershipUpdateForm, OrganizerDeleteForm,
|
||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
||||
WebHookForm,
|
||||
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
|
||||
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm,
|
||||
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
||||
OrganizerDeleteForm, OrganizerForm, OrganizerSettingsForm,
|
||||
OrganizerUpdateForm, TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
from pretix.control.permissions import (
|
||||
@@ -102,6 +102,7 @@ from pretix.control.permissions import (
|
||||
from pretix.control.signals import nav_organizer
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.control.views.mailsetup import MailSettingsSetupView
|
||||
from pretix.helpers import GroupConcat
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
@@ -819,11 +820,17 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
})
|
||||
|
||||
|
||||
class DeviceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = Device
|
||||
template_name = 'pretixcontrol/organizers/devices.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'devices'
|
||||
class DeviceQueryMixin:
|
||||
|
||||
@cached_property
|
||||
def request_data(self):
|
||||
if self.request.method == "POST":
|
||||
return self.request.POST
|
||||
return self.request.GET
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return DeviceFilterForm(data=self.request.GET, request=self.request)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.devices.prefetch_related(
|
||||
@@ -831,17 +838,27 @@ class DeviceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
).order_by('revoked', '-device_id')
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
if 'device' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
id__in=self.request_data.getlist('device')
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = Device
|
||||
template_name = 'pretixcontrol/organizers/devices.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'devices'
|
||||
paginate_by = 100
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return DeviceFilterForm(data=self.request.GET, request=self.request)
|
||||
|
||||
|
||||
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
model = Device
|
||||
@@ -935,6 +952,125 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
||||
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'device'
|
||||
form_class = DeviceBulkEditForm
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related(None).order_by()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse(status=405)
|
||||
|
||||
@cached_property
|
||||
def is_submitted(self):
|
||||
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
|
||||
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
|
||||
# that behaviour
|
||||
return '_bulk' in self.request.POST
|
||||
|
||||
def get_form_kwargs(self):
|
||||
initial = {}
|
||||
mixed_values = set()
|
||||
qs = self.get_queryset().annotate(
|
||||
limit_events_list=Subquery(
|
||||
Device.limit_events.through.objects.filter(
|
||||
device_id=OuterRef('pk')
|
||||
).order_by('device_id', 'event_id').values('device_id').annotate(
|
||||
g=GroupConcat('event_id', separator=',')
|
||||
).values('g')
|
||||
)
|
||||
)
|
||||
|
||||
fields = {
|
||||
'all_events': 'all_events',
|
||||
'limit_events': 'limit_events_list',
|
||||
'security_profile': 'security_profile',
|
||||
'gate': 'gate',
|
||||
}
|
||||
for k, f in fields.items():
|
||||
existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
|
||||
if len(existing_values) == 1:
|
||||
if k == 'limit_events':
|
||||
if existing_values[0][f]:
|
||||
initial[k] = self.request.organizer.events.filter(id__in=existing_values[0][f].split(","))
|
||||
else:
|
||||
initial[k] = []
|
||||
else:
|
||||
initial[k] = existing_values[0][f]
|
||||
elif len(existing_values) > 1:
|
||||
mixed_values.add(k)
|
||||
initial[k] = None
|
||||
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
kwargs['prefix'] = 'bulkedit'
|
||||
kwargs['initial'] = initial
|
||||
kwargs['queryset'] = self.get_queryset()
|
||||
kwargs['mixed_values'] = mixed_values
|
||||
if not self.is_submitted:
|
||||
kwargs['data'] = None
|
||||
kwargs['files'] = None
|
||||
return kwargs
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.devices', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form):
|
||||
log_entries = []
|
||||
|
||||
# Main form
|
||||
form.save()
|
||||
data = {
|
||||
k: (v if k != 'limit_events' else [e.id for e in v])
|
||||
for k, v in form.cleaned_data.items()
|
||||
if k in form.changed_data
|
||||
}
|
||||
data['_raw_bulk_data'] = self.request.POST.dict()
|
||||
for obj in self.get_queryset():
|
||||
log_entries.append(
|
||||
obj.log_action('pretix.device.changed', data=data, user=self.request.user, save=False)
|
||||
)
|
||||
|
||||
if connections['default'].features.can_return_rows_from_bulk_insert:
|
||||
LogEntry.objects.bulk_create(log_entries, batch_size=200)
|
||||
LogEntry.bulk_postprocess(log_entries)
|
||||
else:
|
||||
for le in log_entries:
|
||||
le.save()
|
||||
LogEntry.bulk_postprocess(log_entries)
|
||||
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['devices'] = self.get_queryset()
|
||||
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
is_valid = (
|
||||
self.is_submitted and
|
||||
form.is_valid()
|
||||
)
|
||||
if is_valid:
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
if self.is_submitted:
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||
model = Device
|
||||
template_name = 'pretixcontrol/organizers/device_connect.html'
|
||||
|
||||
@@ -43,7 +43,7 @@ def event(organizer):
|
||||
|
||||
@pytest.fixture
|
||||
def device(organizer):
|
||||
return organizer.devices.create(name='Cashdesk')
|
||||
return organizer.devices.create(name='Cashdesk', all_events=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -108,3 +108,19 @@ def test_revoke_device(event, admin_user, admin_team, device, client):
|
||||
client.post('/control/organizer/dummy/device/{}/revoke'.format(device.pk), {}, follow=True)
|
||||
device.refresh_from_db()
|
||||
assert device.revoked
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_bulk_update_device(event, admin_user, admin_team, device, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
client.post('/control/organizer/dummy/device/bulk_edit', {
|
||||
'device': str(device.pk),
|
||||
'bulkedit-limit_events': str(event.pk),
|
||||
'_bulk': ['bulkedit__events', 'bulkeditsecurity_profile'],
|
||||
'bulkedit-security_profile': 'full',
|
||||
}, follow=True)
|
||||
device.refresh_from_db()
|
||||
assert device.security_profile == 'full'
|
||||
assert not device.all_events
|
||||
with scopes_disabled():
|
||||
assert list(device.limit_events.all()) == [event]
|
||||
|
||||
@@ -181,6 +181,7 @@ organizer_urls = [
|
||||
'organizer/abc/team/add',
|
||||
'organizer/abc/devices',
|
||||
'organizer/abc/device/add',
|
||||
'organizer/abc/device/bulk_edit',
|
||||
'organizer/abc/device/1/edit',
|
||||
'organizer/abc/device/1/connect',
|
||||
'organizer/abc/device/1/revoke',
|
||||
|
||||
Reference in New Issue
Block a user