mirror of
https://github.com/pretix/pretix.git
synced 2026-08-28 13:34:40 +00:00
Restructure our python module. A lot.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PretixControlConfig(AppConfig):
|
||||
name = 'pretix.control'
|
||||
label = 'pretixcontrol'
|
||||
|
||||
default_app_config = 'pretix.control.PretixControlConfig'
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import resolve
|
||||
|
||||
|
||||
def contextprocessor(request):
|
||||
"""
|
||||
Adds data to all template contexts
|
||||
"""
|
||||
ctx = {
|
||||
'url_name': resolve(request.path_info).url_name,
|
||||
'settings': settings,
|
||||
}
|
||||
|
||||
return ctx
|
||||
@@ -0,0 +1,59 @@
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import resolve
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.six.moves.urllib.parse import urlparse
|
||||
from django.shortcuts import resolve_url
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.models import Event
|
||||
|
||||
|
||||
class PermissionMiddleware:
|
||||
|
||||
"""
|
||||
This middleware enforces all requests to the control app to require login.
|
||||
Additionally, it enforces all requests to "control:event." URLs
|
||||
to be for an event the user has basic access to.
|
||||
"""
|
||||
|
||||
EXCEPTIONS = (
|
||||
"auth.login"
|
||||
)
|
||||
|
||||
def process_request(self, request):
|
||||
url = resolve(request.path_info)
|
||||
url_namespace = url.namespace
|
||||
url_name = url.url_name
|
||||
if url_namespace != 'control' or url_name in self.EXCEPTIONS:
|
||||
return
|
||||
if not request.user.is_authenticated():
|
||||
# Taken from django/contrib/auth/decorators.py
|
||||
path = request.build_absolute_uri()
|
||||
# urlparse chokes on lazy objects in Python 3, force to str
|
||||
resolved_login_url = force_str(
|
||||
resolve_url(settings.LOGIN_URL_CONTROL))
|
||||
# If the login url is the same scheme and net location then just
|
||||
# use the path as the "next" url.
|
||||
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
|
||||
current_scheme, current_netloc = urlparse(path)[:2]
|
||||
if ((not login_scheme or login_scheme == current_scheme) and
|
||||
(not login_netloc or login_netloc == current_netloc)):
|
||||
path = request.get_full_path()
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
return redirect_to_login(
|
||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
|
||||
request.user.events_cache = request.user.events.current.order_by(
|
||||
"organizer", "date_from").prefetch_related("organizer")
|
||||
if 'event.' in url_name and 'event' in url.kwargs:
|
||||
try:
|
||||
request.event = Event.objects.current.filter(
|
||||
slug=url.kwargs['event'],
|
||||
permitted__id__exact=request.user.id,
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer')[0]
|
||||
except IndexError:
|
||||
return HttpResponseNotFound(_("The selected event was not found or you "
|
||||
"have no permission to administrate it."))
|
||||
@@ -0,0 +1,43 @@
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.models import EventPermission
|
||||
|
||||
|
||||
def event_permission_required(permission):
|
||||
"""
|
||||
This view decorator rejects all requests with a 403 response which are not from
|
||||
users having the given permission for the event the request is associated with.
|
||||
"""
|
||||
def decorator(function):
|
||||
def wrapper(request, *args, **kw):
|
||||
if not request.user.is_authenticated(): # NOQA
|
||||
# just a double check, should not ever happen
|
||||
return HttpResponseForbidden()
|
||||
perm = EventPermission.objects.get(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
allowed = False
|
||||
try:
|
||||
allowed = getattr(perm, permission)
|
||||
except AttributeError:
|
||||
pass
|
||||
if allowed:
|
||||
return function(request, *args, **kw)
|
||||
return HttpResponseForbidden(_('You do not have permission to view this content.'))
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class EventPermissionRequiredMixin:
|
||||
"""
|
||||
This mixin is equivalent to the event_permission_required view decorator but
|
||||
is in a form suitable for class-based views.
|
||||
"""
|
||||
permission = ''
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
view = super(EventPermissionRequiredMixin, cls).as_view(**initkwargs)
|
||||
return event_permission_required(cls.permission)(view)
|
||||
@@ -0,0 +1,10 @@
|
||||
from pretix.base.signals import EventPluginSignal
|
||||
|
||||
|
||||
"""
|
||||
This signal is sent out to build configuration forms for all restriction formsets
|
||||
(see plugin API documentation for details).
|
||||
"""
|
||||
restriction_formset = EventPluginSignal(
|
||||
providing_args=["item"]
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
$(function () {
|
||||
$("[data-formset]").formset({
|
||||
animateForms: true,
|
||||
reorderMode: 'animate'
|
||||
});
|
||||
$(document).on("click", ".variations .variations-select-all", function (e) {
|
||||
$(this).parent().parent().find("input[type=checkbox]").prop("checked", true).change();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
$(document).on("click", ".variations .variations-select-none", function (e) {
|
||||
$(this).parent().parent().find("input[type=checkbox]").prop("checked", false).change();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
if ($(".items-on-quota").length) {
|
||||
$(".items-on-quota .panel").each(function () {
|
||||
var $panel = $(this);
|
||||
$panel.toggleClass("panel-success", $panel.find("input:checked").length > 0);
|
||||
$(this).find("input").change(function () {
|
||||
$panel.toggleClass("panel-success", $panel.find("input:checked").length > 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
$('.collapse').collapse();
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
@import "../../../../base/static/bootstrap/less/bootstrap.less";
|
||||
|
||||
body {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.form-signin {
|
||||
.well;
|
||||
|
||||
max-width: 330px;
|
||||
margin: auto;
|
||||
margin-top: 10%;
|
||||
padding-bottom: 0;
|
||||
|
||||
.control-label {
|
||||
.sr-only;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
td > .form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
td > .errorlist {
|
||||
color: #a94442;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
td > .form-group > .checkbox {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.has-success .form-control {
|
||||
border-color: #cccccc;
|
||||
}
|
||||
.form-horizontal [data-formset] .form-group {
|
||||
width: 100%;
|
||||
}
|
||||
[data-formset] .form-group:not([data-formset-form-deleted]):last-of-type [data-formset-move-down-button],
|
||||
[data-formset] .form-group:not([data-formset-form-deleted]):first-of-type [data-formset-move-up-button] {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none; // Future-proof disabling of clicks
|
||||
.opacity(.65);
|
||||
.box-shadow(none);
|
||||
}
|
||||
|
||||
.form-plugins .panel-title {
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
.restriction-formset .variations label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.submit-group {
|
||||
margin: 15px 0 0 0 !important;
|
||||
padding: 15px;
|
||||
background: #eeeeee;
|
||||
text-align: right;
|
||||
.btn-save {
|
||||
.btn-lg;
|
||||
}
|
||||
.btn-cancel {
|
||||
.pull-left;
|
||||
.btn-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.panel .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.container ul.nav-pills {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.variation-matrix {
|
||||
td .form-group, .checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-quotas td ul {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-sm-min) {
|
||||
.variation-matrix > tbody > tr > td {
|
||||
line-height: 34px;
|
||||
|
||||
input[type=checkbox] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@import "../../../../base/static/bootstrap/less/bootstrap.less";
|
||||
@import "../../../../base/static/fontawesome/less/font-awesome.less";
|
||||
@fa-font-path: "../../fontawesome/fonts";
|
||||
@import "forms.less";
|
||||
@@ -0,0 +1,25 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/less" href="{% static "pretixcontrol/less/auth.less" %}" />
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<footer>
|
||||
{% with "href='http://pretix.de'" as a_attr %}
|
||||
{% blocktrans trimmed %}
|
||||
powered by <a {{ a_attr }}>pretix</a>
|
||||
{% endblocktrans %}
|
||||
{% endwith %}
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "pretixcontrol/auth/base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<form class="form-signin" action="" method="post">
|
||||
{% bootstrap_form_errors form type='all' layout='inline' %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.email %}
|
||||
{% bootstrap_field form.password %}
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% trans "Log in" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,50 @@
|
||||
{% load compress %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}{% if url_name != "index" %} :: {% endif %}
|
||||
{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/less" href="{% static "pretixcontrol/less/main.less" %}" />
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "bootstrap/dist/js/bootstrap.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar navbar-default navbar-static-top" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">{% trans "Toggle navigation" %}</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="#">{{ settings.PRETIX_INSTANCE_NAME }}</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
{% block nav %}
|
||||
<li {% if url_name == "index" %}class="active"{% endif %}><a href="{% url 'control:index' %}">{% trans "Dashboard" %}</a></li>
|
||||
<li {% if "events" in url_name %}class="active"{% endif %}><a href="{% url 'control:events' %}">{% trans "Events" %}</a></li>
|
||||
{% endblock %}
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="#"><i class="fa fa-user"></i> {{ request.user.get_full_name }}</a></li>
|
||||
<li><a href="{% url 'control:auth.logout' %}" title="{% trans "Log out" %}"><i class="fa fa-sign-out"></i><span class="visible-xs-inline">{% trans "Log out" %}</span></a></li>
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block nav %}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-calendar"></i> {{ request.event.slug }} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="{% url "control:events" %}">{% trans "Event overview" %}</a></li>
|
||||
{% regroup request.user.events_cache by organizer as event_list %}
|
||||
{% for g in event_list %}
|
||||
<li class="dropdown-header">{{ g.grouper }}</li>
|
||||
{% for e in g.list %}
|
||||
<li><a href="{% url "control:event.index" organizer=g.grouper.slug event=e.slug %}">{{ e.name }}</a></li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
<li {% if url_name == "event.index" %}class="active"{% endif %}><a href="{% url 'control:event.index' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Dashboard" %}</a></li>
|
||||
<li {% if "event.settings" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Settings" %}</a></li>
|
||||
<li {% if "event.item" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Items" %}</a></li>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ request.event.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ request.event.name }}</h1>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
<form action="" method="post" class="form-horizontal form-plugins">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Installed plugins" %}</legend>
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for plugin in plugins %}
|
||||
<div class="panel panel-{% if plugin.module in plugins_active %}success{% else %}default{% endif %}">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-sm-10">
|
||||
<h3 class="panel-title">{{ plugin.name }}</h3>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
{% if plugin.module in plugins_active %}
|
||||
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-primary btn-block" name="plugin:{{ plugin.module }}" value="enable">{% trans "Enable" %}</button>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="meta">{% blocktrans %}Version {{ plugin.version }} by <em>{{ plugin.author }}</em>{% endblocktrans %}</p>
|
||||
<p>{{ plugin.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,44 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.slug layout="horizontal" %}
|
||||
{% bootstrap_field form.date_from layout="horizontal" %}
|
||||
{% bootstrap_field form.date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.currency layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Display settings" %}</legend>
|
||||
{% bootstrap_field form.locale layout="horizontal" %}
|
||||
{% bootstrap_field form.timezone layout="horizontal" %}
|
||||
{% bootstrap_field form.show_date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.show_times layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Presale settings" %}</legend>
|
||||
{% bootstrap_field form.presale_start layout="horizontal" %}
|
||||
{% bootstrap_field form.presale_end layout="horizontal" %}
|
||||
{% bootstrap_field form.max_items_per_order layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Payment settings" %}</legend>
|
||||
{% bootstrap_field form.payment_term_days layout="horizontal" %}
|
||||
{% bootstrap_field form.payment_term_last layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{{ request.event.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Settings" %}</h1>
|
||||
<ul class="nav nav-pills">
|
||||
<li {% if "event.settings" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "General settings" %}</a></li>
|
||||
<li {% if "event.settings.plugins" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.settings.plugins' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Plugins" %}</a></li>
|
||||
</ul>
|
||||
{% block inside %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Events" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Events" %}</h1>
|
||||
<p>{% trans "The list below shows all events you have administrative access to. Click on the event name to access event details." %}</p>
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Event name" %}</th>
|
||||
<th>{% trans "Organizer" %}</th>
|
||||
<th>{% trans "Start date" %}</th>
|
||||
<th>{% trans "End date" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in events %}
|
||||
<tr>
|
||||
<td><strong><a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong></td>
|
||||
<td>{{ e.organizer }}</td>
|
||||
<td>{{ e.get_date_from_display }}</td>
|
||||
<td>{{ e.get_date_to_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ item.name }} :: {% trans "Item" %}{% endblock %}
|
||||
{% block content %}
|
||||
{% if item.identity %}
|
||||
<h1>{% trans "Modify item:" %} {{ item.name }}</h1>
|
||||
<ul class="nav nav-pills">
|
||||
<li {% if "event.item" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item' organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}">{% trans "General information" %}</a></li>
|
||||
<li {% if "event.item.variations" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.variations' organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}">{% trans "Variations" %}</a></li>
|
||||
<li {% if "event.item.restrictions" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.restrictions' organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}">{% trans "Restrictions" %}</a></li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<h1>{% trans "Create item" %}</h1>
|
||||
<p>{% blocktrans trimmed %}
|
||||
You will be able to adjust further settings in the next step.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% if item.identity and not item.quotas.exists %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
Please note, that your item will <strong>not</strong> be available for sale until you added your item
|
||||
to an existing or newly created quota.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block inside %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.active layout="horizontal" %}
|
||||
{% bootstrap_field form.category layout="horizontal" %}
|
||||
{% bootstrap_field form.short_description layout="horizontal" %}
|
||||
{% bootstrap_field form.long_description layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Price settings" %}</legend>
|
||||
{% bootstrap_field form.default_price layout="horizontal" %}
|
||||
{% bootstrap_field form.tax_rate layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.properties layout="horizontal" %}
|
||||
{% bootstrap_field form.questions layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% block inside %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
In this area, you can choose of a set of "restriction types" to restrict the availability of your item with
|
||||
certain conditions.
|
||||
{% endblocktrans %}</p>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% for set in formsets %}
|
||||
<fieldset>
|
||||
<legend>{{ set.title }}</legend>
|
||||
<p>{{ set.description }}</p>
|
||||
<div data-formset class="restriction-formset" data-formset-prefix="{{ set.formset.prefix }}">
|
||||
<div data-formset-body class="panel-group collapse" id="accordion_{{ set.formset.prefix }}">
|
||||
{{ set.formset.management_form }}
|
||||
{% for f in set.formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion"
|
||||
href="#collapse{{ f.prefix }}">
|
||||
{{ set.title }}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse{{ f.prefix }}" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_form f layout="horizontal" field_class="col-md-10" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapse__prefix__">
|
||||
{% trans "New restriction" %}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse__prefix__" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_form set.formset.initialized_empty_form layout="horizontal" field_class="col-md-10" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<button type="button" class="btn btn-default" data-formset-add><i
|
||||
class="fa fa-plus"></i> {% trans "Add a new restriction" %}</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
<em>{% trans "You have to define and select propreties to be able to configure variations." %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ properties.0 }}</th>
|
||||
<th>{% trans "Active" %}</th>
|
||||
<th>{% trans "Price" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for form in forms %}
|
||||
{% bootstrap_form_errors form type='all' layout='inline' %}
|
||||
<tr>
|
||||
<td>{{ form.values.0 }}</td>
|
||||
<td>{% bootstrap_field form.active layout='inline' %}</td>
|
||||
<td>{% bootstrap_field form.default_price layout='inline' %} {{ form.default_price.errors }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% for major in forms %}
|
||||
{% if major.row %}
|
||||
<h3>{{ major.row }}</h3>
|
||||
{% endif %}
|
||||
<table class="table variation-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for val in properties.1.values.all %}
|
||||
<th>{{ val.value }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sub in major.forms %}
|
||||
<tr>
|
||||
<td>{{ sub.row.value }}</td>
|
||||
{% for form in sub.forms %}
|
||||
<td>
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
{% bootstrap_field form.active layout='inline' %}
|
||||
</div>
|
||||
<div class="col-sm-7">
|
||||
{% bootstrap_field form.default_price layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
{{ form.default_price.errors }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Items" %}{% endblock %}
|
||||
{% block content %}
|
||||
<ul class="nav nav-pills">
|
||||
<li {% if "event.items" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Items" %}</a></li>
|
||||
<li {% if "event.items.quotas" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.quotas' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Quotas" %}</a></li>
|
||||
<li {% if "event.items.categories" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.categories' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Categories" %}</a></li>
|
||||
<li {% if "event.items.properties" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.properties' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Properties" %}</a></li>
|
||||
<li {% if "event.items.questions" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.questions' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Questions" %}</a></li>
|
||||
</ul>
|
||||
{% block inside %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Item categories" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Item categories" %}</h1>
|
||||
{% if "updated" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% elif "created" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "A new category has been created." %}
|
||||
</div>
|
||||
{% elif "deleted" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "The category has been deleted." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new category" %}</a>
|
||||
</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Item categories" %}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in categories %}
|
||||
<tr>
|
||||
<td><strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.identity %}">{{ c.name }}</a></strong></td>
|
||||
<td>
|
||||
<a href="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.identity %}" class="btn btn-default btn-sm {% if forloop.counter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-up"></i></a>
|
||||
<a href="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.identity %}" class="btn btn-default btn-sm {% if forloop.revcounter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-down"></i></a>
|
||||
</td>
|
||||
<td class="text-right"><a href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Item category" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Item category" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Delete item category" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Delete item category" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete the category <strong>{{ category.name }}</strong>?{% endblocktrans %}</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.items.categories" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Items" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Items" %}</h1>
|
||||
<p>
|
||||
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new item" %}</a>
|
||||
</p>
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Item name" %}</th>
|
||||
<th>{% trans "Category" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in items %}
|
||||
<tr>
|
||||
<td><strong><a href="
|
||||
{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.identity %}">{{ i.name }}</a></strong></td>
|
||||
<td>{% if i.category %}{{ i.category }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Item properties" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Item properties" %}</h1>
|
||||
{% if "updated" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% elif "created" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "A new property has been created." %}
|
||||
</div>
|
||||
{% elif "deleted" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "The property has been deleted." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.properties.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new property" %}</a>
|
||||
</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Item properties" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in properties %}
|
||||
<tr>
|
||||
<td><strong><a href="
|
||||
{% url "control:event.items.properties.edit" organizer=request.event.organizer.slug event=request.event.slug property=p.identity %}">{{ p.name }}</a></strong></td>
|
||||
<td class="text-right"><a href="
|
||||
{% url "control:event.items.properties.delete" organizer=request.event.organizer.slug event=request.event.slug property=p.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% block title %}{% trans "Item property" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Item property" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Values" %}</legend>
|
||||
<div data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
<div data-formset-body>
|
||||
{{ formset.management_form }}
|
||||
{% for f in formset %}
|
||||
<div class="form-group" data-formset-form>
|
||||
{{ f.id }}
|
||||
<div class="col-sm-9">
|
||||
{% bootstrap_field f.value form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="sr-only">
|
||||
{% bootstrap_field f.ORDER form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field f.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-sm-3 text-right">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button><i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button><i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button><i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="form-group" data-formset-form>
|
||||
{{ formset.empty_form.id }}
|
||||
<div class="col-sm-9">
|
||||
{% bootstrap_field formset.empty_form.value form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="sr-only">
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-sm-3 text-right">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button><i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button><i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button><i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<button type="button" class="btn btn-default" data-formset-add><i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Delete item property" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Delete item property" %}</h1>
|
||||
{% if not possible %}
|
||||
<p>{% blocktrans %}You can not delete the property <strong>{{ property }}</strong> as long as the following items use it:{% endblocktrans %}</p>
|
||||
<ul>
|
||||
{% for item in dependent %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to the property <strong>{{ property }}</strong>?{% endblocktrans %}</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.items.properties" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Question" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Question" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.question layout="horizontal" %}
|
||||
{% bootstrap_field form.type layout="horizontal" %}
|
||||
{% bootstrap_field form.required layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Delete question" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Delete question" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to the question <strong>{{ question }}</strong>?{% endblocktrans %}</p>
|
||||
{% if dependent|length > 0 %}
|
||||
<p>{% blocktrans %}All answers to the question given by the buyers of the following tickets will be <strong>lost</strong>.{% endblocktrans %}</p>
|
||||
{% for item in dependent %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item.name }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Questions" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Questions" %}</h1>
|
||||
{% if "updated" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% elif "created" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "A new question has been created." %}
|
||||
</div>
|
||||
{% elif "deleted" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "The question has been deleted." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new question" %}</a>
|
||||
</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Question" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in questions %}
|
||||
<tr>
|
||||
<td><strong><a href="
|
||||
{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.identity %}">{{ q.question }}</a></strong></td>
|
||||
<td>{{ q.get_type_display }}</td>
|
||||
<td class="text-right"><a href="
|
||||
{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,54 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Quota" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Quota" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.size layout="horizontal" %}
|
||||
<legend>{% trans "Items" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Please select the items or item variations this quota should be applied to. If you apply two
|
||||
quotas to the same items, it will only be available if <strong>both</strong> quotas have capacity
|
||||
left.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="panel-group items-on-quota">
|
||||
{% for item in items %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion"
|
||||
href="#collapse{{ item.identity }}">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse{{ item.identity }}" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_field item.field layout="horizontal" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Delete quota" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Delete quota" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete the quota <strong>{{ quota }}</strong>?{% endblocktrans %}</p>
|
||||
{% if dependent|length > 0 %}
|
||||
<p>{% blocktrans %}The following items might be no longer available for sale:{% endblocktrans %}</p>
|
||||
{% for item in dependent %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item.name }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,52 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Quotas" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Quotas" %}</h1>
|
||||
{% if "updated" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% elif "created" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "A new quota has been created." %}
|
||||
</div>
|
||||
{% elif "deleted" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "The quota has been deleted." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}</a>
|
||||
</p>
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Quota name" %}</th>
|
||||
<th>{% trans "Items" %}</th>
|
||||
<th>{% trans "Total capacity" %}</th>
|
||||
<th>{% trans "Capacity left" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in quotas %}
|
||||
<tr>
|
||||
<td><strong><a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.identity %}">{{ q.name }}</a></strong></td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for item in q.items.all %}
|
||||
<li><a href="
|
||||
{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}"
|
||||
>{{ item.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
<td>{{ q.size }}</td>
|
||||
<td></td>
|
||||
<td class="text-right"><a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,108 @@
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.base.tests import BrowserTest, on_platforms
|
||||
|
||||
|
||||
@on_platforms()
|
||||
class LoginFormBrowserTest(BrowserTest):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
|
||||
|
||||
def test_login(self):
|
||||
self.driver.implicitly_wait(10)
|
||||
self.driver.get('%s%s' % (self.live_server_url, '/control/login'))
|
||||
username_input = self.driver.find_element_by_name("email")
|
||||
username_input.send_keys('dummy@dummy.dummy')
|
||||
password_input = self.driver.find_element_by_name("password")
|
||||
password_input.send_keys('dummy')
|
||||
self.driver.find_element_by_css_selector('button[type="submit"]').click()
|
||||
self.driver.find_element_by_class_name("navbar-right")
|
||||
|
||||
def test_login_fail(self):
|
||||
self.driver.implicitly_wait(10)
|
||||
self.driver.get('%s%s' % (self.live_server_url, '/control/login'))
|
||||
username_input = self.driver.find_element_by_name("email")
|
||||
username_input.send_keys('dummy@dummy.dummy')
|
||||
password_input = self.driver.find_element_by_name("password")
|
||||
password_input.send_keys('wrong')
|
||||
self.driver.find_element_by_css_selector('button[type="submit"]').click()
|
||||
self.driver.find_element_by_class_name("alert-danger")
|
||||
|
||||
|
||||
class LoginFormTest(TestCase):
|
||||
"""
|
||||
This test case tests various methods around the properties /
|
||||
variations concept.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
|
||||
|
||||
def test_wrong_credentials(self):
|
||||
c = Client()
|
||||
response = c.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'foo',
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_correct_credentials(self):
|
||||
c = Client()
|
||||
response = c.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_inactive_account(self):
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
|
||||
c = Client()
|
||||
response = c.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_redirect(self):
|
||||
c = Client()
|
||||
response = c.post('/control/login?next=/control/events/', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/events/', response['Location'])
|
||||
|
||||
def test_logged_in(self):
|
||||
c = Client()
|
||||
response = c.post('/control/login?next=/control/events/', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/events/', response['Location'])
|
||||
|
||||
response = c.get('/control/login')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = c.get('/control/login?next=/control/events/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/events/', response['Location'])
|
||||
|
||||
def test_logout(self):
|
||||
c = Client()
|
||||
response = c.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = c.get('/control/logout')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = c.get('/control/login')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -0,0 +1,61 @@
|
||||
import datetime
|
||||
from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission
|
||||
from pretix.base.tests import BrowserTest, on_platforms
|
||||
|
||||
|
||||
@on_platforms()
|
||||
class EventsTest(BrowserTest):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
|
||||
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
|
||||
self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')
|
||||
self.event1 = Event.objects.create(
|
||||
organizer=self.orga1, name='30C3', slug='30c3',
|
||||
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
|
||||
)
|
||||
self.event2 = Event.objects.create(
|
||||
organizer=self.orga1, name='31C3', slug='31c3',
|
||||
date_from=datetime.datetime(2014, 12, 26, tzinfo=datetime.timezone.utc),
|
||||
)
|
||||
self.event3 = Event.objects.create(
|
||||
organizer=self.orga2, name='MRMCD14', slug='mrmcd14',
|
||||
date_from=datetime.datetime(2014, 9, 5, tzinfo=datetime.timezone.utc),
|
||||
)
|
||||
OrganizerPermission.objects.create(organizer=self.orga1, user=self.user)
|
||||
EventPermission.objects.create(event=self.event1, user=self.user, can_change_items=True,
|
||||
can_change_settings=True)
|
||||
self.driver.implicitly_wait(10)
|
||||
self.driver.get('%s%s' % (self.live_server_url, '/control/login'))
|
||||
username_input = self.driver.find_element_by_name("email")
|
||||
username_input.send_keys('dummy@dummy.dummy')
|
||||
password_input = self.driver.find_element_by_name("password")
|
||||
password_input.send_keys('dummy')
|
||||
self.driver.find_element_by_css_selector('button[type="submit"]').click()
|
||||
self.driver.find_element_by_class_name("navbar-right")
|
||||
|
||||
def test_event_list(self):
|
||||
self.driver.get('%s%s' % (self.live_server_url, '/control/events/'))
|
||||
tabletext = self.driver.find_element_by_css_selector(".container .table").text
|
||||
self.assertIn("30C3", tabletext)
|
||||
self.assertNotIn("31C3", tabletext)
|
||||
self.assertNotIn("MRMCD14", tabletext)
|
||||
|
||||
def test_settings(self):
|
||||
self.driver.get('%s/control/event/%s/%s/settings/' % (self.live_server_url, self.orga1.slug,
|
||||
self.event1.slug))
|
||||
self.driver.find_element_by_name("date_to").send_keys("2013-12-30 17:00:00")
|
||||
self.driver.find_element_by_class_name("btn-save").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertIn("2013-12-30 17:00:00", self.driver.find_element_by_name("date_to").get_attribute("value"))
|
||||
|
||||
def test_plugins(self):
|
||||
self.driver.get('%s/control/event/%s/%s/settings/plugins' % (self.live_server_url, self.orga1.slug,
|
||||
self.event1.slug))
|
||||
self.assertIn("Restriction by time", self.driver.find_element_by_class_name("form-plugins").text)
|
||||
self.assertIn("Enable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)
|
||||
self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").click()
|
||||
self.assertIn("Disable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)
|
||||
self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").click()
|
||||
self.assertIn("Enable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)
|
||||
@@ -0,0 +1,241 @@
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
from django.utils import unittest
|
||||
from selenium.webdriver.support.select import Select
|
||||
from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission, ItemCategory, Property, \
|
||||
PropertyValue, Question, Quota, Item
|
||||
from pretix.base.tests import BrowserTest, on_platforms
|
||||
|
||||
|
||||
class ItemFormTest(BrowserTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.driver.set_window_size(1920, 1080)
|
||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
|
||||
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
|
||||
self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')
|
||||
self.event1 = Event.objects.create(
|
||||
organizer=self.orga1, name='30C3', slug='30c3',
|
||||
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
|
||||
)
|
||||
OrganizerPermission.objects.create(organizer=self.orga1, user=self.user)
|
||||
EventPermission.objects.create(event=self.event1, user=self.user, can_change_items=True,
|
||||
can_change_settings=True)
|
||||
self.driver.implicitly_wait(10)
|
||||
self.driver.get('%s%s' % (self.live_server_url, '/control/login'))
|
||||
username_input = self.driver.find_element_by_name("email")
|
||||
username_input.send_keys('dummy@dummy.dummy')
|
||||
password_input = self.driver.find_element_by_name("password")
|
||||
password_input.send_keys('dummy')
|
||||
self.driver.find_element_by_css_selector('button[type="submit"]').click()
|
||||
self.driver.find_element_by_class_name("navbar-right")
|
||||
|
||||
def scroll_into_view(self, element):
|
||||
"""Scroll element into view"""
|
||||
y = element.location['y']
|
||||
self.driver.execute_script('window.scrollTo(0, {0})'.format(y))
|
||||
|
||||
def scroll_and_click(self, element):
|
||||
self.scroll_into_view(element)
|
||||
time.sleep(0.5)
|
||||
element.click()
|
||||
|
||||
|
||||
@on_platforms()
|
||||
class CategoriesTest(ItemFormTest):
|
||||
|
||||
def test_create(self):
|
||||
self.driver.get('%s/control/event/%s/%s/categories/add' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug
|
||||
))
|
||||
self.driver.find_element_by_name("name").send_keys('Entry tickets')
|
||||
self.driver.find_element_by_class_name("btn-save").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertIn("Entry tickets", self.driver.find_element_by_css_selector(".container table").text)
|
||||
|
||||
def test_update(self):
|
||||
c = ItemCategory.objects.create(event=self.event1, name="Entry tickets")
|
||||
self.driver.get('%s/control/event/%s/%s/categories/%s/' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
|
||||
))
|
||||
self.driver.find_element_by_name("name").clear()
|
||||
self.driver.find_element_by_name("name").send_keys('T-Shirts')
|
||||
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertIn("T-Shirts", self.driver.find_element_by_css_selector(".container table").text)
|
||||
self.assertNotIn("Entry tickets", self.driver.find_element_by_css_selector(".container table").text)
|
||||
|
||||
@unittest.skipIf('TRAVIS' in os.environ, 'See docstring for details.')
|
||||
def test_sort(self):
|
||||
"""
|
||||
For unknown reasons, the first scoll_and_click() call sometimes results in the following exception
|
||||
|
||||
selenium.common.exceptions.ElementNotVisibleException:
|
||||
Message: {"errorMessage":"Element is not currently visible and may not be manipulated", …}
|
||||
|
||||
This exception does not occur on either of my machines, but only when being run in Travis CI.
|
||||
|
||||
– Raphael Michel, 2015-02-08
|
||||
"""
|
||||
ItemCategory.objects.create(event=self.event1, name="Entry tickets", position=0)
|
||||
ItemCategory.objects.create(event=self.event1, name="T-Shirts", position=1)
|
||||
self.driver.get('%s/control/event/%s/%s/categories/' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug
|
||||
))
|
||||
self.assertIn("Entry tickets",
|
||||
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(1)").text)
|
||||
self.assertIn("T-Shirts",
|
||||
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(2)").text)
|
||||
self.scroll_and_click(self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(1) a[href*='down']"))
|
||||
time.sleep(1)
|
||||
self.assertIn("Entry tickets",
|
||||
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(2)").text)
|
||||
self.assertIn("T-Shirts",
|
||||
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(1)").text)
|
||||
self.scroll_and_click(self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(2) a[href*='up']"))
|
||||
time.sleep(1)
|
||||
self.assertIn("Entry tickets",
|
||||
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(1)").text)
|
||||
self.assertIn("T-Shirts",
|
||||
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(2)").text)
|
||||
|
||||
def test_delete(self):
|
||||
c = ItemCategory.objects.create(event=self.event1, name="Entry tickets")
|
||||
self.driver.get('%s/control/event/%s/%s/categories/%s/delete' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
|
||||
))
|
||||
self.driver.find_element_by_class_name("btn-danger").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertNotIn("Entry tickets", self.driver.find_element_by_css_selector(".container table").text)
|
||||
|
||||
|
||||
@on_platforms()
|
||||
class PropertiesTest(ItemFormTest):
|
||||
|
||||
def test_create(self):
|
||||
self.driver.get('%s/control/event/%s/%s/properties/add' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug
|
||||
))
|
||||
self.driver.find_element_by_css_selector("#id_name").send_keys('Size')
|
||||
self.driver.find_element_by_name("values-0-value").send_keys('S')
|
||||
self.driver.find_element_by_name("values-1-value").send_keys('M')
|
||||
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertIn("Size", self.driver.find_element_by_css_selector(".container table").text)
|
||||
self.driver.find_element_by_partial_link_text("Size").click()
|
||||
self.assertEqual("S", self.driver.find_element_by_name("values-0-value").get_attribute("value"))
|
||||
self.assertEqual("M", self.driver.find_element_by_name("values-1-value").get_attribute("value"))
|
||||
|
||||
def test_update(self):
|
||||
c = Property.objects.create(event=self.event1, name="Size")
|
||||
PropertyValue.objects.create(prop=c, position=0, value="S")
|
||||
PropertyValue.objects.create(prop=c, position=1, value="M")
|
||||
self.driver.get('%s/control/event/%s/%s/properties/%s/' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
|
||||
))
|
||||
self.driver.find_element_by_css_selector("#id_name").clear()
|
||||
self.driver.find_element_by_css_selector("#id_name").send_keys('Color')
|
||||
self.driver.find_elements_by_css_selector("div.form-group button.btn-danger")[0].click()
|
||||
self.scroll_into_view(self.driver.find_element_by_name("values-1-value"))
|
||||
self.driver.find_element_by_name("values-1-value").clear()
|
||||
self.driver.find_element_by_name("values-1-value").send_keys('red')
|
||||
self.driver.find_element_by_css_selector("button[data-formset-add]").click()
|
||||
self.driver.find_element_by_name("values-2-value").send_keys('blue')
|
||||
self.driver.find_element_by_class_name("btn-save").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertEqual("red", self.driver.find_element_by_name("values-0-value").get_attribute("value"))
|
||||
self.assertEqual("blue", self.driver.find_element_by_name("values-1-value").get_attribute("value"))
|
||||
|
||||
def test_delete(self):
|
||||
c = Property.objects.create(event=self.event1, name="Size")
|
||||
self.driver.get('%s/control/event/%s/%s/properties/%s/delete' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
|
||||
))
|
||||
self.driver.find_element_by_class_name("btn-danger").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertNotIn("Size", self.driver.find_element_by_css_selector(".container table").text)
|
||||
|
||||
|
||||
@on_platforms()
|
||||
class QuestionsTest(ItemFormTest):
|
||||
|
||||
def test_create(self):
|
||||
self.driver.get('%s/control/event/%s/%s/questions/add' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug
|
||||
))
|
||||
self.driver.find_element_by_name("question").send_keys('What is your shoe size?')
|
||||
Select(self.driver.find_element_by_name("type")).select_by_value('N')
|
||||
self.driver.find_element_by_class_name("btn-save").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertIn("shoe size", self.driver.find_element_by_css_selector(".container table").text)
|
||||
|
||||
def test_update(self):
|
||||
c = Question.objects.create(event=self.event1, question="What is your shoe size?", type="N", required=True)
|
||||
self.driver.get('%s/control/event/%s/%s/questions/%s/' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
|
||||
))
|
||||
self.driver.find_element_by_name("question").clear()
|
||||
self.driver.find_element_by_name("question").send_keys('How old are you?')
|
||||
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertIn("How old", self.driver.find_element_by_css_selector(".container table").text)
|
||||
self.assertNotIn("shoe size", self.driver.find_element_by_css_selector(".container table").text)
|
||||
c = Question.objects.current.get(identity=c.identity)
|
||||
self.assertTrue(c.required)
|
||||
|
||||
def test_delete(self):
|
||||
c = Question.objects.create(event=self.event1, question="What is your shoe size?", type="N", required=True)
|
||||
self.driver.get('%s/control/event/%s/%s/questions/%s/delete' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
|
||||
))
|
||||
self.driver.find_element_by_class_name("btn-danger").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertNotIn("shoe size", self.driver.find_element_by_css_selector(".container table").text)
|
||||
|
||||
|
||||
@on_platforms()
|
||||
class QuotaTest(ItemFormTest):
|
||||
|
||||
def test_create(self):
|
||||
self.driver.get('%s/control/event/%s/%s/quotas/add' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug
|
||||
))
|
||||
self.driver.find_element_by_name("name").send_keys('Full house')
|
||||
self.driver.find_element_by_name("size").send_keys('500')
|
||||
self.driver.find_element_by_class_name("btn-save").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertIn("Full house", self.driver.find_element_by_css_selector(".container table").text)
|
||||
|
||||
def test_update(self):
|
||||
c = Quota.objects.create(event=self.event1, name="Full house", size=500)
|
||||
item1 = Item.objects.create(event=self.event1, name="Standard")
|
||||
item2 = Item.objects.create(event=self.event1, name="Business")
|
||||
prop1 = Property.objects.create(event=self.event1, name="Level")
|
||||
item2.properties.add(prop1)
|
||||
PropertyValue.objects.create(prop=prop1, value="Silver")
|
||||
PropertyValue.objects.create(prop=prop1, value="Gold")
|
||||
self.driver.get('%s/control/event/%s/%s/quotas/%s/' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
|
||||
))
|
||||
self.driver.find_element_by_name("size").clear()
|
||||
self.driver.find_element_by_name("size").send_keys('350')
|
||||
self.scroll_and_click(self.driver.find_element_by_css_selector('.panel-group .panel:nth-child(1) .panel-title a'))
|
||||
time.sleep(1)
|
||||
self.scroll_and_click(self.driver.find_element_by_name("item_%s" % item1.identity))
|
||||
self.driver.find_element_by_css_selector('.panel-group .panel:nth-child(2) .panel-title a').click()
|
||||
time.sleep(1)
|
||||
self.scroll_and_click(self.driver.find_elements_by_css_selector("input[name=item_%s]" % item2.identity)[1])
|
||||
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertIn("350", self.driver.find_element_by_css_selector(".container table").text)
|
||||
self.assertNotIn("500", self.driver.find_element_by_css_selector(".container table").text)
|
||||
|
||||
def test_delete(self):
|
||||
c = Quota.objects.create(event=self.event1, name="Full house", size=500)
|
||||
self.driver.get('%s/control/event/%s/%s/quotas/%s/delete' % (
|
||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
|
||||
))
|
||||
self.driver.find_element_by_class_name("btn-danger").click()
|
||||
self.driver.find_element_by_class_name("alert-success")
|
||||
self.assertNotIn("Full house", self.driver.find_element_by_css_selector(".container table").text)
|
||||
@@ -0,0 +1,69 @@
|
||||
from django.test import TestCase, Client
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models import Event, Organizer, User, EventPermission
|
||||
|
||||
|
||||
class PermissionMiddlewareTest(TestCase):
|
||||
"""
|
||||
This test case tests various methods around the properties /
|
||||
variations concept.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
self.event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now(),
|
||||
)
|
||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
|
||||
|
||||
def test_logged_out(self):
|
||||
c = Client()
|
||||
response = c.get('/control/login')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = c.get('/control/events/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_wrong_event(self):
|
||||
c = Client()
|
||||
response = c.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = c.get('/control/event/dummy/dummy/settings/')
|
||||
self.assertIn(response.status_code, (403, 404))
|
||||
|
||||
def test_wrong_event_permission(self):
|
||||
EventPermission.objects.create(
|
||||
event=self.event, user=self.user,
|
||||
can_change_settings=False,
|
||||
can_change_items=True,
|
||||
)
|
||||
c = Client()
|
||||
response = c.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = c.get('/control/event/dummy/dummy/settings/')
|
||||
self.assertIn(response.status_code, (403, 404))
|
||||
|
||||
def test_correct(self):
|
||||
EventPermission.objects.create(
|
||||
event=self.event, user=self.user,
|
||||
can_change_settings=True,
|
||||
can_change_items=True,
|
||||
)
|
||||
c = Client()
|
||||
response = c.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = c.get('/control/event/dummy/dummy/settings/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -0,0 +1,58 @@
|
||||
from django.conf.urls import patterns, url, include
|
||||
from pretix.control.views import main, event, item
|
||||
|
||||
urlpatterns = patterns('',)
|
||||
urlpatterns += patterns(
|
||||
'pretix.control.views.auth',
|
||||
url(r'^logout$', 'logout', name='auth.logout'),
|
||||
url(r'^login$', 'login', name='auth.login'),
|
||||
)
|
||||
urlpatterns += patterns(
|
||||
'pretix.control.views.main',
|
||||
url(r'^$', 'index', name='index'),
|
||||
url(r'^events/$', main.EventList.as_view(), name='events'),
|
||||
)
|
||||
urlpatterns += patterns(
|
||||
'pretix.control.views.event',
|
||||
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include(
|
||||
patterns(
|
||||
'pretix.control.views',
|
||||
url(r'^$', 'event.index', name='event.index'),
|
||||
url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'),
|
||||
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'),
|
||||
url(r'^items/$', item.ItemList.as_view(), name='event.items'),
|
||||
url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/variations$', item.ItemVariations.as_view(),
|
||||
name='event.item.variations'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/restrictions$', item.ItemRestrictions.as_view(),
|
||||
name='event.item.restrictions'),
|
||||
url(r'^categories/$', item.CategoryList.as_view(), name='event.items.categories'),
|
||||
url(r'^categories/(?P<category>[0-9a-f-]+)/delete$', item.CategoryDelete.as_view(),
|
||||
name='event.items.categories.delete'),
|
||||
url(r'^categories/(?P<category>[0-9a-f-]+)/up$', item.category_move_up, name='event.items.categories.up'),
|
||||
url(r'^categories/(?P<category>[0-9a-f-]+)/down$', item.category_move_down,
|
||||
name='event.items.categories.down'),
|
||||
url(r'^categories/(?P<category>[0-9a-f-]+)/$', item.CategoryUpdate.as_view(),
|
||||
name='event.items.categories.edit'),
|
||||
url(r'^categories/add$', item.CategoryCreate.as_view(), name='event.items.categories.add'),
|
||||
url(r'^questions/$', item.QuestionList.as_view(), name='event.items.questions'),
|
||||
url(r'^questions/(?P<question>[0-9a-f-]+)/delete$', item.QuestionDelete.as_view(),
|
||||
name='event.items.questions.delete'),
|
||||
url(r'^questions/(?P<question>[0-9a-f-]+)/$', item.QuestionUpdate.as_view(),
|
||||
name='event.items.questions.edit'),
|
||||
url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
|
||||
url(r'^properties/$', item.PropertyList.as_view(), name='event.items.properties'),
|
||||
url(r'^properties/(?P<property>[0-9a-f-]+)/$', item.PropertyUpdate.as_view(),
|
||||
name='event.items.properties.edit'),
|
||||
url(r'^properties/(?P<property>[0-9a-f-]+)/delete$', item.PropertyDelete.as_view(),
|
||||
name='event.items.properties.delete'),
|
||||
url(r'^properties/add$', item.PropertyCreate.as_view(), name='event.items.properties.add'),
|
||||
url(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
|
||||
url(r'^quotas/(?P<quota>[0-9a-f-]+)/$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'),
|
||||
url(r'^quotas/(?P<quota>[0-9a-f-]+)/delete$', item.QuotaDelete.as_view(),
|
||||
name='event.items.quotas.delete'),
|
||||
url(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'),
|
||||
)
|
||||
))
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth import login as auth_login
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
|
||||
|
||||
class AuthenticationForm(BaseAuthenticationForm):
|
||||
"""
|
||||
The login form, providing an email and password field. The form already implements
|
||||
validation for correct user data.
|
||||
"""
|
||||
email = forms.EmailField(label=_("E-mail address"), max_length=254)
|
||||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||
username = None
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("Please enter a correct e-mail address and password."),
|
||||
'inactive': _("This account is inactive."),
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
super(forms.Form, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if email and password:
|
||||
self.user_cache = authenticate(identifier=email.lower(),
|
||||
password=password)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login',
|
||||
)
|
||||
else:
|
||||
self.confirm_login_allowed(self.user_cache)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
def login(request):
|
||||
"""
|
||||
Render and process a most basic login form. Takes an URL as GET
|
||||
parameter "next" for redirection after successful login
|
||||
"""
|
||||
ctx = {}
|
||||
if request.user.is_authenticated():
|
||||
if "next" in request.GET:
|
||||
return redirect(request.GET.get("next", 'control:index'))
|
||||
return redirect('control:index')
|
||||
if request.method == 'POST':
|
||||
form = AuthenticationForm(data=request.POST)
|
||||
if form.is_valid() and form.user_cache:
|
||||
auth_login(request, form.user_cache)
|
||||
if "next" in request.GET:
|
||||
return redirect(request.GET.get("next", 'control:index'))
|
||||
return redirect('control:index')
|
||||
else:
|
||||
form = AuthenticationForm()
|
||||
ctx['form'] = form
|
||||
return render(request, 'pretixcontrol/auth/login.html', ctx)
|
||||
|
||||
|
||||
def logout(request):
|
||||
"""
|
||||
Log the user out of the current session, then redirect to login page.
|
||||
"""
|
||||
auth_logout(request)
|
||||
return redirect('control:auth.login')
|
||||
@@ -0,0 +1,111 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.views.generic.edit import UpdateView
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from pytz import common_timezones
|
||||
from pretix.base.forms import VersionedModelForm
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
|
||||
|
||||
class EventUpdateForm(VersionedModelForm):
|
||||
|
||||
timezone = forms.ChoiceField(
|
||||
choices=((a, a) for a in common_timezones),
|
||||
label=_("Default timezone"),
|
||||
)
|
||||
|
||||
def clean_slug(self):
|
||||
return self.instance.slug
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name',
|
||||
'slug',
|
||||
'locale',
|
||||
'timezone',
|
||||
'currency',
|
||||
'date_from',
|
||||
'date_to',
|
||||
'show_date_to',
|
||||
'show_times',
|
||||
'presale_start',
|
||||
'presale_end',
|
||||
'payment_term_days',
|
||||
'payment_term_last',
|
||||
'max_items_per_order'
|
||||
]
|
||||
|
||||
|
||||
class EventUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = Event
|
||||
form_class = EventUpdateForm
|
||||
template_name = 'pretixcontrol/event/settings.html'
|
||||
permission = 'can_change_settings'
|
||||
|
||||
def get_object(self, queryset=None) -> Event:
|
||||
return self.request.event
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings', kwargs={
|
||||
'organizer': self.get_object().organizer.slug,
|
||||
'event': self.get_object().slug,
|
||||
}) + '?success=true'
|
||||
|
||||
|
||||
class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
||||
|
||||
model = Event
|
||||
context_object_name = 'event'
|
||||
permission = 'can_change_settings'
|
||||
template_name = 'pretixcontrol/event/plugins.html'
|
||||
|
||||
def get_object(self, queryset=None) -> Event:
|
||||
return self.request.event
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['plugins'] = [p for p in get_all_plugins() if not p.name.startswith('.')]
|
||||
context['plugins_active'] = self.object.get_plugins()
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
context = self.get_context_data(object=self.object)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
plugins_active = self.object.get_plugins()
|
||||
for key, value in request.POST.items():
|
||||
if key.startswith("plugin:"):
|
||||
module = key.split(":")[1]
|
||||
if value == "enable":
|
||||
plugins_active.append(module)
|
||||
else:
|
||||
plugins_active.remove(module)
|
||||
self.object.plugins = ",".join(plugins_active)
|
||||
self.object.save()
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.plugins', kwargs={
|
||||
'organizer': self.get_object().organizer.slug,
|
||||
'event': self.get_object().slug,
|
||||
}) + '?success=true'
|
||||
|
||||
|
||||
def index(request, organizer, event):
|
||||
return render(request, 'pretixcontrol/event/index.html', {})
|
||||
@@ -0,0 +1,389 @@
|
||||
from itertools import product
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.forms.widgets import flatatt
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pretix.base.forms import VersionedModelForm
|
||||
|
||||
from pretix.base.models import ItemVariation, PropertyValue, Item
|
||||
|
||||
|
||||
class TolerantFormsetModelForm(VersionedModelForm):
|
||||
"""
|
||||
This is equivalent to a normal VersionedModelForm, but works around a problem that
|
||||
arises when the form is used inside a FormSet with can_order=True and django-formset-js
|
||||
enabled. In this configuration, even empty "extra" forms might have an ORDER value
|
||||
sent and Django marks the form as empty and raises validation errors because the other
|
||||
fields have not been filled.
|
||||
"""
|
||||
|
||||
def has_changed(self) -> bool:
|
||||
"""
|
||||
Returns True if data differs from initial. Contrary to the default
|
||||
implementation, the ORDER field is being ignored.
|
||||
"""
|
||||
for name, field in self.fields.items():
|
||||
if name == 'ORDER' or name == 'id':
|
||||
continue
|
||||
prefixed_name = self.add_prefix(name)
|
||||
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
|
||||
if not field.show_hidden_initial:
|
||||
initial_value = self.initial.get(name, field.initial)
|
||||
if callable(initial_value):
|
||||
initial_value = initial_value()
|
||||
else:
|
||||
initial_prefixed_name = self.add_initial_prefix(name)
|
||||
hidden_widget = field.hidden_widget()
|
||||
try:
|
||||
initial_value = field.to_python(hidden_widget.value_from_datadict(
|
||||
self.data, self.files, initial_prefixed_name))
|
||||
except forms.ValidationError:
|
||||
# Always assume data has changed if validation fails.
|
||||
self._changed_data.append(name)
|
||||
continue
|
||||
# We're using a private API of Django here. This is not nice, but no problem as it seems
|
||||
# like this will become a public API in future Django.
|
||||
if field._has_changed(initial_value, data_value):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class RestrictionForm(TolerantFormsetModelForm):
|
||||
"""
|
||||
The restriction form provides useful functionality for all forms
|
||||
representing a restriction instance. To be concret, this form does
|
||||
the necessary magic to make the 'variations' field work correctly
|
||||
and look beautiful.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'item' in kwargs:
|
||||
self.item = kwargs['item']
|
||||
del kwargs['item']
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'variations' in self.fields and isinstance(self.fields['variations'], VariationsField):
|
||||
self.fields['variations'].set_item(self.item)
|
||||
|
||||
|
||||
class RestrictionInlineFormset(forms.BaseInlineFormSet):
|
||||
"""
|
||||
This is the base class you should use for any formset you return
|
||||
from a ``restriction_formset`` signal receiver that contains
|
||||
RestrictionForm objects as its forms, as it correcly handles the
|
||||
necessary item parameter for the RestrictionForm. While this could
|
||||
be achieved with a regular formset, this also adds a
|
||||
``initialized_empty_form`` method which is the only way to correctly
|
||||
render a working empty form for a JavaScript-enabled restriction formset.
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, files=None, instance=None,
|
||||
save_as_new=False, prefix=None, queryset=None, **kwargs):
|
||||
super().__init__(
|
||||
data, files, instance, save_as_new, prefix, queryset, **kwargs
|
||||
)
|
||||
if isinstance(self.instance, Item):
|
||||
self.queryset = self.queryset.as_of().prefetch_related("variations")
|
||||
|
||||
def initialized_empty_form(self):
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
item=self.instance
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['item'] = self.instance
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
class Meta:
|
||||
exclude = ['item']
|
||||
|
||||
|
||||
class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer):
|
||||
"""
|
||||
This is the default renderer for a VariationsField. Based on the choice input class
|
||||
this renders a list or a matrix of checkboxes/radio buttons/...
|
||||
"""
|
||||
|
||||
def __init__(self, name, value, attrs, choices):
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.attrs = attrs
|
||||
self.choices = choices
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Outputs a grid for this set of choice fields.
|
||||
"""
|
||||
if len(self.choices) == 0:
|
||||
raise ValueError("Can't handle empty lists")
|
||||
|
||||
variations = []
|
||||
for key, value in self.choices:
|
||||
value['key'] = key
|
||||
variations.append(value)
|
||||
|
||||
properties = [v.prop for v in variations[0].relevant_values()]
|
||||
dimension = len(properties)
|
||||
|
||||
id_ = self.attrs.get('id', None)
|
||||
start_tag = format_html('<div class="variations" id="{0}">', id_) if id_ else '<div class="variations">'
|
||||
output = [start_tag]
|
||||
|
||||
# TODO: This is very duplicate to pretixcontrol.views.item.ItemVariations.get_forms()
|
||||
# Find a common abstraction to avoid the repetition.
|
||||
if dimension == 0:
|
||||
output.append(format_html('<em>{0}</em>', _("not applicable")))
|
||||
elif dimension == 1:
|
||||
output.append('<ul>')
|
||||
for i, variation in enumerate(variations):
|
||||
final_attrs = dict(
|
||||
self.attrs.copy(), type=self.choice_input_class.input_type,
|
||||
name=self.name, value=variation['key']
|
||||
)
|
||||
if variation['key'] in self.value:
|
||||
final_attrs['checked'] = 'checked'
|
||||
w = self.choice_input_class(
|
||||
self.name, self.value, self.attrs.copy(),
|
||||
(variation['key'], variation[properties[0].identity].value),
|
||||
i
|
||||
)
|
||||
output.append(format_html('<li>{0}</li>', force_text(w)))
|
||||
output.append('</ul>')
|
||||
|
||||
elif dimension >= 2:
|
||||
# prop1 is the property on all the grid's y-axes
|
||||
prop1 = properties[0]
|
||||
prop1v = list(prop1.values.current.all())
|
||||
# prop2 is the property on all the grid's x-axes
|
||||
prop2 = properties[1]
|
||||
prop2v = list(prop2.values.current.all())
|
||||
|
||||
def selector(values):
|
||||
# Given an iterable of PropertyValue objects, this will return a
|
||||
# list of their primary keys, ordered by the primary keys of the
|
||||
# properties they belong to EXCEPT the value for the property prop2.
|
||||
# We'll see later why we need this.
|
||||
return [
|
||||
v.identity for v in sorted(values, key=lambda v: v.prop.identity)
|
||||
if v.prop.identity != prop2.identity
|
||||
]
|
||||
|
||||
def sort(v):
|
||||
# Given a list of variations, this will sort them by their position
|
||||
# on the x-axis
|
||||
return v[prop2.identity].identity
|
||||
|
||||
# We now iterate over the cartesian product of all the other
|
||||
# properties which are NOT on the axes of the grid because we
|
||||
# create one grid for any combination of them.
|
||||
for gridrow in product(*[prop.values.current.all() for prop in properties[2:]]):
|
||||
if len(gridrow) > 0:
|
||||
output.append('<strong>')
|
||||
output.append(", ".join([value.value for value in gridrow]))
|
||||
output.append('</strong>')
|
||||
output.append('<table class="table"><thead><tr><th></th>')
|
||||
for val2 in prop2v:
|
||||
output.append(format_html('<th>{0}</th>', val2.value))
|
||||
output.append('</thead><tbody>')
|
||||
for val1 in prop1v:
|
||||
output.append(format_html('<tr><th>{0}</th>', val1.value))
|
||||
# We are now inside one of the rows of the grid and have to
|
||||
# select the variations to display in this row. In order to
|
||||
# achieve this, we use the 'selector' lambda defined above.
|
||||
# It gives us a normalized, comparable version of a set of
|
||||
# PropertyValue objects. In this case, we compute the
|
||||
# selector of our row as the selector of the sum of the
|
||||
# values defining our grind and the value defining our row.
|
||||
selection = selector(gridrow + (val1,))
|
||||
# We now iterate over all variations who generate the same
|
||||
# selector as 'selection'.
|
||||
filtered = [v for v in variations if selector(v.relevant_values()) == selection]
|
||||
for variation in sorted(filtered, key=sort):
|
||||
final_attrs = dict(
|
||||
self.attrs.copy(), type=self.choice_input_class.input_type,
|
||||
name=self.name, value=variation['key']
|
||||
)
|
||||
if variation['key'] in self.value:
|
||||
final_attrs['checked'] = 'checked'
|
||||
output.append(format_html('<td><label><input{0} /></label></td>', flatatt(final_attrs)))
|
||||
output.append('</td>')
|
||||
output.append('</tbody></table>')
|
||||
output.append(
|
||||
('<div class="help-block"><a href="#" class="variations-select-all">{0}</a> · '
|
||||
'<a href="#" class="variations-select-none">{1}</a></div></div>').format(
|
||||
_("Select all"),
|
||||
_("Deselect all")
|
||||
)
|
||||
)
|
||||
return mark_safe('\n'.join(output))
|
||||
|
||||
|
||||
class VariationsCheckboxRenderer(VariationsFieldRenderer):
|
||||
"""
|
||||
This is the same as VariationsFieldRenderer but with the choice input class
|
||||
forced to checkboxes
|
||||
"""
|
||||
choice_input_class = forms.widgets.CheckboxChoiceInput
|
||||
|
||||
|
||||
class VariationsSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
"""
|
||||
This is the default widget for a VariationsField
|
||||
"""
|
||||
renderer = VariationsCheckboxRenderer
|
||||
_empty_value = []
|
||||
|
||||
|
||||
class VariationsField(forms.ModelMultipleChoiceField):
|
||||
"""
|
||||
This form field is intended to be used to let the user select a
|
||||
variation of a certain item, for example in a restriction plugin.
|
||||
|
||||
As this field expects the non-standard keyword parameter ``item``
|
||||
at initialization time, this is field is normally named ``variations``
|
||||
and lives inside a ``pretixcontrol.views.forms.RestrictionForm``, which
|
||||
does some magic to provide this parameter.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, item=None, **kwargs):
|
||||
self.item = item
|
||||
if 'widget' not in args or kwargs['widget'] is None:
|
||||
kwargs['widget'] = VariationsSelectMultiple
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def set_item(self, item: Item):
|
||||
assert isinstance(item, Item)
|
||||
self.item = item
|
||||
self._set_choices(self._get_choices())
|
||||
|
||||
def _get_choices(self) -> "list[(str, VariationDict)]":
|
||||
"""
|
||||
We can't use a normal QuerySet as there theoretically might be
|
||||
two types of variations: Some who already have a ItemVariation
|
||||
object associated with them and some who don't. We therefore use
|
||||
the item's ``get_all_variations`` method. In the first case, we
|
||||
use the ItemVariation objects primary key as our choice, key,
|
||||
in the latter case we use a string constructed from the values
|
||||
(see VariationDict.key() for implementation details).
|
||||
"""
|
||||
if self.item is None:
|
||||
return ()
|
||||
variations = self.item.get_all_variations(use_cache=True)
|
||||
return (
|
||||
(
|
||||
v['variation'].identity if 'variation' in v else v.key(),
|
||||
v
|
||||
) for v in variations
|
||||
)
|
||||
|
||||
def clean(self, value: "list[int]"):
|
||||
"""
|
||||
At cleaning time, we have to clean up the mess we produced with our
|
||||
_get_choices implementation. In the case of ItemVariation object ids
|
||||
we don't to anything to them, but if one of the selected items is a
|
||||
list of PropertyValue objects (see _get_choices), we need to create
|
||||
a new ItemVariation object for this combination and then add this to
|
||||
our list of selected items.
|
||||
"""
|
||||
if self.item is None:
|
||||
raise ValueError(
|
||||
"VariationsField object was not properly initialized. Please"
|
||||
"use a pretixcontrol.views.forms.RestrictionForm form instead of"
|
||||
"a plain Django ModelForm"
|
||||
)
|
||||
|
||||
# Standard validation foo
|
||||
if self.required and not value:
|
||||
raise ValidationError(self.error_messages['required'], code='required')
|
||||
elif not self.required and not value:
|
||||
return []
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise ValidationError(self.error_messages['list'], code='list')
|
||||
|
||||
# Build up a cache of variations having an ItemVariation object
|
||||
# For implementation details, see ItemVariation.get_all_variations()
|
||||
# which uses a very similar method
|
||||
all_variations = self.item.variations.all().prefetch_related("values")
|
||||
variations_cache = {}
|
||||
for var in all_variations:
|
||||
key = []
|
||||
for v in var.values.all():
|
||||
key.append((v.prop_id, v.identity))
|
||||
key = tuple(sorted(key))
|
||||
variations_cache[key] = var.identity
|
||||
|
||||
cleaned_value = []
|
||||
|
||||
# Wrap this in a transaction to prevent strange database state if we
|
||||
# get a ValidationError half-way through
|
||||
with transaction.atomic():
|
||||
for pk in value:
|
||||
if ":" in pk:
|
||||
# A combination of PropertyValues was given
|
||||
|
||||
# Hash the combination in the same way as in our cache above
|
||||
key = []
|
||||
for pair in pk.split(","):
|
||||
key.append(tuple([i for i in pair.split(":")]))
|
||||
key = tuple(sorted(key))
|
||||
|
||||
if key in variations_cache:
|
||||
# An ItemVariation object already exists for this variation,
|
||||
# so use this. (This might occur if the variation object was
|
||||
# created _after_ the user loaded the form but _before_ he
|
||||
# submitted it.)
|
||||
cleaned_value.append(str(variations_cache[key]))
|
||||
continue
|
||||
|
||||
# No ItemVariation present, create one!
|
||||
var = ItemVariation()
|
||||
var.item_id = self.item.identity
|
||||
var.save()
|
||||
# Add the values to the ItemVariation object
|
||||
for pair in pk.split(","):
|
||||
prop, value = pair.split(":")
|
||||
try:
|
||||
var.values.add(
|
||||
PropertyValue.objects.current.get(
|
||||
identity=value,
|
||||
prop_id=prop
|
||||
)
|
||||
)
|
||||
except PropertyValue.DoesNotExist:
|
||||
raise ValidationError(
|
||||
self.error_messages['invalid_pk_value'],
|
||||
code='invalid_pk_value',
|
||||
params={'pk': value},
|
||||
)
|
||||
variations_cache[key] = var.identity
|
||||
cleaned_value.append(str(var.identity))
|
||||
else:
|
||||
# An ItemVariation id was given
|
||||
cleaned_value.append(pk)
|
||||
|
||||
qs = self.item.variations.current.filter(identity__in=cleaned_value)
|
||||
|
||||
# Re-check for consistency
|
||||
pks = set(force_text(getattr(o, "identity")) for o in qs)
|
||||
for val in cleaned_value:
|
||||
if force_text(val) not in pks:
|
||||
raise ValidationError(
|
||||
self.error_messages['invalid_choice'],
|
||||
code='invalid_choice',
|
||||
params={'value': val},
|
||||
)
|
||||
|
||||
# Since this overrides the inherited ModelChoiceField.clean
|
||||
# we run custom validators here
|
||||
self.run_validators(cleaned_value)
|
||||
return qs
|
||||
|
||||
choices = property(_get_choices, forms.ChoiceField._set_choices)
|
||||
@@ -0,0 +1,899 @@
|
||||
from itertools import product
|
||||
from django.db import transaction
|
||||
from django.forms import BooleanField, ModelForm
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.http import HttpResponseRedirect, HttpResponseForbidden
|
||||
from django.shortcuts import redirect
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pretix.base.forms import VersionedModelForm
|
||||
|
||||
from pretix.base.models import (
|
||||
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota,
|
||||
Versionable)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin, event_permission_required
|
||||
from pretix.control.views.forms import TolerantFormsetModelForm, VariationsField
|
||||
from pretix.control.signals import restriction_formset
|
||||
|
||||
|
||||
class ItemList(ListView):
|
||||
model = Item
|
||||
context_object_name = 'items'
|
||||
template_name = 'pretixcontrol/items/index.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return Item.objects.current.filter(
|
||||
event=self.request.event
|
||||
).prefetch_related("category")
|
||||
|
||||
|
||||
class CategoryForm(VersionedModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ItemCategory
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name'
|
||||
]
|
||||
|
||||
|
||||
class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = ItemCategory
|
||||
form_class = CategoryForm
|
||||
template_name = 'pretixcontrol/items/category_delete.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'category'
|
||||
|
||||
def get_object(self, queryset=None) -> ItemCategory:
|
||||
url = resolve(self.request.path_info)
|
||||
return self.request.event.categories.current.get(
|
||||
identity=url.kwargs['category']
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
for item in self.object.items.current.all():
|
||||
# TODO: Clone!?
|
||||
item.category = None
|
||||
item.save()
|
||||
success_url = self.get_success_url()
|
||||
self.object.delete()
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.categories', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?deleted=true'
|
||||
|
||||
|
||||
class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = ItemCategory
|
||||
form_class = CategoryForm
|
||||
template_name = 'pretixcontrol/items/category.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'category'
|
||||
|
||||
def get_object(self, queryset=None) -> ItemCategory:
|
||||
url = resolve(self.request.path_info)
|
||||
return self.request.event.categories.current.get(
|
||||
identity=url.kwargs['category']
|
||||
)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.categories', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?updated=true'
|
||||
|
||||
|
||||
class CategoryCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = ItemCategory
|
||||
form_class = CategoryForm
|
||||
template_name = 'pretixcontrol/items/category.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'category'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.categories', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?created=true'
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.event = self.request.event
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class CategoryList(ListView):
|
||||
model = ItemCategory
|
||||
context_object_name = 'categories'
|
||||
template_name = 'pretixcontrol/items/categories.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.categories.current.all()
|
||||
|
||||
|
||||
def category_move(request, category, up=True):
|
||||
"""
|
||||
This is a helper function to avoid duplicating code in category_move_up and
|
||||
category_move_down. It takes a category and a direction and then tries to bring
|
||||
all categories for this event in a new order.
|
||||
"""
|
||||
category = request.event.categories.current.get(
|
||||
identity=category
|
||||
)
|
||||
categories = list(request.event.categories.current.order_by("position"))
|
||||
|
||||
index = categories.index(category)
|
||||
if index != 0 and up:
|
||||
categories[index - 1], categories[index] = categories[index], categories[index - 1]
|
||||
elif index != len(categories) - 1 and not up:
|
||||
categories[index + 1], categories[index] = categories[index], categories[index + 1]
|
||||
|
||||
for i, cat in enumerate(categories):
|
||||
if cat.position != i:
|
||||
cat.position = i
|
||||
cat.save() # TODO: Clone or document sloppiness?
|
||||
|
||||
|
||||
@event_permission_required("can_change_items")
|
||||
def category_move_up(request, organizer, event, category):
|
||||
category_move(request, category, up=True)
|
||||
return redirect(reverse('control:event.items.categories', kwargs={
|
||||
'organizer': request.event.organizer.slug,
|
||||
'event': request.event.slug,
|
||||
}) + '?ordered=true')
|
||||
|
||||
|
||||
@event_permission_required("can_change_items")
|
||||
def category_move_down(request, organizer, event, category):
|
||||
category_move(request, category, up=False)
|
||||
return redirect(reverse('control:event.items.categories', kwargs={
|
||||
'organizer': request.event.organizer.slug,
|
||||
'event': request.event.slug,
|
||||
}) + '?ordered=true')
|
||||
|
||||
|
||||
class PropertyList(ListView):
|
||||
model = Property
|
||||
context_object_name = 'properties'
|
||||
template_name = 'pretixcontrol/items/properties.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return Property.objects.current.filter(
|
||||
event=self.request.event
|
||||
)
|
||||
|
||||
|
||||
class PropertyForm(VersionedModelForm):
|
||||
class Meta:
|
||||
model = Property
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name',
|
||||
]
|
||||
|
||||
|
||||
class PropertyValueForm(TolerantFormsetModelForm):
|
||||
class Meta:
|
||||
model = PropertyValue
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'value',
|
||||
]
|
||||
|
||||
|
||||
class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = Property
|
||||
form_class = PropertyForm
|
||||
template_name = 'pretixcontrol/items/property.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'property'
|
||||
|
||||
def get_object(self, queryset=None) -> Property:
|
||||
url = resolve(self.request.path_info)
|
||||
return self.request.event.properties.current.get(
|
||||
identity=url.kwargs['property']
|
||||
)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
url = resolve(self.request.path_info)
|
||||
return reverse('control:event.items.properties.edit', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'property': url.kwargs['property']
|
||||
}) + '?success=true'
|
||||
|
||||
def get_formset(self):
|
||||
formsetclass = inlineformset_factory(
|
||||
Property, PropertyValue,
|
||||
form=PropertyValueForm,
|
||||
can_order=True,
|
||||
extra=0,
|
||||
)
|
||||
kwargs = self.get_form_kwargs()
|
||||
kwargs['queryset'] = self.object.values.current.all()
|
||||
formset = formsetclass(**kwargs)
|
||||
return formset
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['formset'] = self.get_formset()
|
||||
return context
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form, formset):
|
||||
for f in formset.deleted_forms:
|
||||
f.instance.delete()
|
||||
f.instance.pk = None
|
||||
|
||||
for i, f in enumerate(formset.ordered_forms):
|
||||
if f.instance.pk is not None:
|
||||
f.instance = f.instance.clone()
|
||||
f.instance.position = i
|
||||
f.instance.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form_class = self.get_form_class()
|
||||
form = self.get_form(form_class)
|
||||
formset = self.get_formset()
|
||||
if form.is_valid() and formset.is_valid():
|
||||
return self.form_valid(form, formset)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class PropertyCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = Property
|
||||
form_class = PropertyForm
|
||||
template_name = 'pretixcontrol/items/property.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'property'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.properties', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?created=true'
|
||||
|
||||
def get_formset(self):
|
||||
formsetclass = inlineformset_factory(
|
||||
Property, PropertyValue,
|
||||
form=PropertyValueForm,
|
||||
can_order=True,
|
||||
extra=3,
|
||||
)
|
||||
formset = formsetclass(**self.get_form_kwargs())
|
||||
return formset
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
self.object = None
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['formset'] = self.get_formset()
|
||||
return context
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form, formset):
|
||||
form.instance.event = self.request.event
|
||||
resp = super().form_valid(form)
|
||||
for i, f in enumerate(formset.ordered_forms):
|
||||
f.instance.position = i
|
||||
f.instance.prop = form.instance
|
||||
f.instance.save()
|
||||
return resp
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form_class = self.get_form_class()
|
||||
form = self.get_form(form_class)
|
||||
formset = self.get_formset()
|
||||
if form.is_valid() and formset.is_valid():
|
||||
return self.form_valid(form, formset)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class PropertyDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Property
|
||||
form_class = PropertyForm
|
||||
template_name = 'pretixcontrol/items/property_delete.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'property'
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['dependent'] = self.get_object().items.current.all()
|
||||
context['possible'] = self.is_allowed()
|
||||
return context
|
||||
|
||||
def is_allowed(self) -> bool:
|
||||
return self.get_object().items.current.count() == 0
|
||||
|
||||
def get_object(self, queryset=None) -> Property:
|
||||
if not hasattr(self, 'object') or not self.object:
|
||||
url = resolve(self.request.path_info)
|
||||
self.object = self.request.event.properties.current.get(
|
||||
identity=url.kwargs['property']
|
||||
)
|
||||
return self.object
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
if self.is_allowed():
|
||||
success_url = self.get_success_url()
|
||||
self.get_object().delete()
|
||||
return HttpResponseRedirect(success_url)
|
||||
else:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.properties', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?deleted=true'
|
||||
|
||||
|
||||
class QuestionList(ListView):
|
||||
model = Question
|
||||
context_object_name = 'questions'
|
||||
template_name = 'pretixcontrol/items/questions.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.current.all()
|
||||
|
||||
|
||||
class QuestionForm(VersionedModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'question',
|
||||
'type',
|
||||
'required',
|
||||
]
|
||||
|
||||
|
||||
class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Question
|
||||
template_name = 'pretixcontrol/items/question_delete.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'question'
|
||||
|
||||
def get_object(self, queryset=None) -> Question:
|
||||
url = resolve(self.request.path_info)
|
||||
return self.request.event.questions.current.get(
|
||||
identity=url.kwargs['question']
|
||||
)
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['dependent'] = list(self.get_object().items.current.all())
|
||||
return context
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
success_url = self.get_success_url()
|
||||
self.object.delete()
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.questions', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?deleted=true'
|
||||
|
||||
|
||||
class QuestionUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = Question
|
||||
form_class = QuestionForm
|
||||
template_name = 'pretixcontrol/items/question.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'question'
|
||||
|
||||
def get_object(self, queryset=None) -> Question:
|
||||
url = resolve(self.request.path_info)
|
||||
return self.request.event.questions.current.get(
|
||||
identity=url.kwargs['question']
|
||||
)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.questions', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?updated=true'
|
||||
|
||||
|
||||
class QuestionCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = Question
|
||||
form_class = QuestionForm
|
||||
template_name = 'pretixcontrol/items/question.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'question'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.questions', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?created=true'
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.event = self.request.event
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class QuotaList(ListView):
|
||||
model = Quota
|
||||
context_object_name = 'quotas'
|
||||
template_name = 'pretixcontrol/items/quotas.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return Quota.objects.current.filter(
|
||||
event=self.request.event
|
||||
).prefetch_related("items")
|
||||
|
||||
|
||||
class QuotaForm(ModelForm):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
items = kwargs['items']
|
||||
del kwargs['items']
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if hasattr(self, 'instance'):
|
||||
active_items = set(self.instance.items.all())
|
||||
active_variations = set(self.instance.variations.all())
|
||||
else:
|
||||
active_items = set()
|
||||
active_variations = set()
|
||||
|
||||
for item in items:
|
||||
if len(item.properties.all()) > 0:
|
||||
self.fields['item_%s' % item.identity] = VariationsField(
|
||||
item, label=_("Activate for"),
|
||||
required=False,
|
||||
initial=active_variations
|
||||
)
|
||||
self.fields['item_%s' % item.identity].set_item(item)
|
||||
else:
|
||||
self.fields['item_%s' % item.identity] = BooleanField(
|
||||
label=_("Activate"),
|
||||
required=False,
|
||||
initial=(item in active_items)
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
if self.instance.pk is not None and isinstance(self.instance, Versionable):
|
||||
if self.has_changed():
|
||||
self.instance = self.instance.clone_shallow()
|
||||
# TODO: order_cache, lock_cache are emptied by that but you'll have
|
||||
# to rebuild them anyway
|
||||
return super().save(commit)
|
||||
|
||||
class Meta:
|
||||
model = Quota
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name',
|
||||
'size',
|
||||
]
|
||||
|
||||
|
||||
class QuotaEditorMixin:
|
||||
|
||||
@cached_property
|
||||
def items(self) -> "List[Item]":
|
||||
return list(self.request.event.items.all().prefetch_related("properties", "variations"))
|
||||
|
||||
def get_form(self, form_class):
|
||||
if not hasattr(self, '_form'):
|
||||
kwargs = self.get_form_kwargs()
|
||||
kwargs['items'] = self.items
|
||||
self._form = form_class(**kwargs)
|
||||
return self._form
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['items'] = self.items
|
||||
for item in context['items']:
|
||||
item.field = self.get_form(QuotaForm)['item_%s' % item.identity]
|
||||
return context
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form):
|
||||
res = super().form_valid(form)
|
||||
# The following commented-out checks are not necessary as both self.object.items
|
||||
# and self.object.variations can be expected empty due to the performance
|
||||
# optimization of pretixbase.models.Versionable.clone_shallow()
|
||||
# items = self.object.items.all()
|
||||
# variations = self.object.variations.all()
|
||||
self.object = form.instance
|
||||
for item in self.items:
|
||||
field = form.fields['item_%s' % item.identity]
|
||||
data = form.cleaned_data['item_%s' % item.identity]
|
||||
if isinstance(field, VariationsField):
|
||||
self.object.variations.add(*data)
|
||||
# for v in data:
|
||||
# if v not in variations:
|
||||
# self.object.variations.add(v)
|
||||
# for v in variations:
|
||||
# if v not in data:
|
||||
# self.object.variations.remove(v)
|
||||
if data: # and item not in items:
|
||||
self.object.items.add(item)
|
||||
# elif not data and item in items:
|
||||
# self.object.items.remove(item)
|
||||
return res
|
||||
|
||||
|
||||
class QuotaCreate(EventPermissionRequiredMixin, QuotaEditorMixin, CreateView):
|
||||
model = Quota
|
||||
form_class = QuotaForm
|
||||
template_name = 'pretixcontrol/items/quota.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'quota'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.quotas', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?created=true'
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.event = self.request.event
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
|
||||
model = Quota
|
||||
form_class = QuotaForm
|
||||
template_name = 'pretixcontrol/items/quota.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'quota'
|
||||
|
||||
def get_object(self, queryset=None) -> Quota:
|
||||
url = resolve(self.request.path_info)
|
||||
return self.request.event.quotas.current.get(
|
||||
identity=url.kwargs['quota']
|
||||
)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.quotas', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?updated=true'
|
||||
|
||||
|
||||
class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Quota
|
||||
template_name = 'pretixcontrol/items/quota_delete.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'quota'
|
||||
|
||||
def get_object(self, queryset=None) -> Quota:
|
||||
url = resolve(self.request.path_info)
|
||||
return self.request.event.quotas.current.get(
|
||||
identity=url.kwargs['quota']
|
||||
)
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['dependent'] = list(self.get_object().items.current.all())
|
||||
return context
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
success_url = self.get_success_url()
|
||||
self.object.delete()
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.quotas', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?deleted=true'
|
||||
|
||||
|
||||
class ItemDetailMixin(SingleObjectMixin):
|
||||
model = Item
|
||||
context_object_name = 'item'
|
||||
|
||||
def get_object(self, queryset=None) -> Item:
|
||||
if not hasattr(self, 'object') or not self.object:
|
||||
url = resolve(self.request.path_info)
|
||||
self.item = self.request.event.items.current.get(
|
||||
identity=url.kwargs['item']
|
||||
)
|
||||
self.object = self.item
|
||||
return self.object
|
||||
|
||||
|
||||
class ItemFormGeneral(VersionedModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['category'].queryset = self.instance.event.categories.current.all()
|
||||
self.fields['properties'].queryset = self.instance.event.properties.current.all()
|
||||
self.fields['questions'].queryset = self.instance.event.questions.current.all()
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'category',
|
||||
'name',
|
||||
'active',
|
||||
'short_description',
|
||||
'long_description',
|
||||
'default_price',
|
||||
'tax_rate',
|
||||
'properties',
|
||||
'questions',
|
||||
]
|
||||
|
||||
|
||||
class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
||||
form_class = ItemFormGeneral
|
||||
template_name = 'pretixcontrol/item/index.html'
|
||||
permission = 'can_change_items'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.item', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'item': self.object.identity,
|
||||
}) + '?success=true'
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""
|
||||
Returns the keyword arguments for instantiating the form.
|
||||
"""
|
||||
newinst = Item(event=self.request.event)
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs.update({'instance': newinst})
|
||||
return kwargs
|
||||
|
||||
|
||||
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateView):
|
||||
form_class = ItemFormGeneral
|
||||
template_name = 'pretixcontrol/item/index.html'
|
||||
permission = 'can_change_items'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.item', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'item': self.get_object().identity,
|
||||
}) + '?success=true'
|
||||
|
||||
|
||||
class ItemVariationForm(VersionedModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'active',
|
||||
'default_price',
|
||||
]
|
||||
|
||||
|
||||
class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
permission = 'can_change_items'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.item = None
|
||||
|
||||
def get_form(self, variation, data=None) -> ItemVariationForm:
|
||||
"""
|
||||
Return the dict for one given variation. Variations are expected to be
|
||||
dictionaries in the format of Item.get_all_variations()
|
||||
"""
|
||||
# Values are all dictionary ite
|
||||
values = [i[1] for i in sorted([it for it in variation.items() if it[0] != 'variation'])]
|
||||
if 'variation' in variation:
|
||||
form = ItemVariationForm(
|
||||
data,
|
||||
instance=variation['variation'],
|
||||
prefix=",".join([str(i.identity) for i in values]),
|
||||
)
|
||||
else:
|
||||
inst = ItemVariation(item=self.object)
|
||||
inst.item_id = self.object.identity
|
||||
inst.creation = True
|
||||
form = ItemVariationForm(
|
||||
data,
|
||||
instance=inst,
|
||||
prefix=",".join([str(i.identity) for i in values]),
|
||||
)
|
||||
form.values = values
|
||||
return form
|
||||
|
||||
def get_forms(self) -> tuple:
|
||||
"""
|
||||
Returns one form per possible item variation. The forms are returned
|
||||
twice: The first entry in the returned tuple contains a 1-, 2- or
|
||||
3-dimensional list, depending on the number of properties associated
|
||||
with this item (this is being used for form display), the second
|
||||
contains all forms in one single list (this is used for processing).
|
||||
|
||||
The first, hierarchical list, is a list of dicts on all levels but the
|
||||
last one, where the dict contains the two entries 'row' containing a
|
||||
string describing this layer and 'forms' which contains the forms or
|
||||
the next list of dicts.
|
||||
"""
|
||||
forms = []
|
||||
forms_flat = []
|
||||
variations = self.object.get_all_variations()
|
||||
data = self.request.POST if self.request.method == 'POST' else None
|
||||
|
||||
if self.dimension == 1:
|
||||
# For one-dimensional structures we just have a list of forms
|
||||
for variation in variations:
|
||||
form = self.get_form(variation, data)
|
||||
forms.append(form)
|
||||
forms_flat = forms
|
||||
|
||||
elif self.dimension >= 2:
|
||||
# For 2 or more dimensional structures we display a list of grids
|
||||
# of forms
|
||||
|
||||
# prop1 is the property on all the grid's y-axes
|
||||
prop1 = self.properties[0]
|
||||
# prop2 is the property on all the grid's x-axes
|
||||
prop2 = self.properties[1]
|
||||
|
||||
def selector(values):
|
||||
# Given an iterable of PropertyValue objects, this will return a
|
||||
# list of their primary keys, ordered by the primary keys of the
|
||||
# properties they belong to EXCEPT the value for the property prop2.
|
||||
# We'll see later why we need this.
|
||||
return [
|
||||
v.identity for v in sorted(values, key=lambda v: v.prop.identity)
|
||||
if v.prop.identity != prop2.identity
|
||||
]
|
||||
|
||||
def sort(v):
|
||||
# Given a list of variations, this will sort them by their position
|
||||
# on the x-axis
|
||||
return v[prop2.identity].identity
|
||||
|
||||
# We now iterate over the cartesian product of all the other
|
||||
# properties which are NOT on the axes of the grid because we
|
||||
# create one grid for any combination of them.
|
||||
for gridrow in product(*[prop.values.current.all() for prop in self.properties[2:]]):
|
||||
grids = []
|
||||
for val1 in prop1.values.current.all():
|
||||
formrow = []
|
||||
# We are now inside one of the rows of the grid and have to
|
||||
# select the variations to display in this row. In order to
|
||||
# achieve this, we use the 'selector' lambda defined above.
|
||||
# It gives us a normalized, comparable version of a set of
|
||||
# PropertyValue objects. In this case, we compute the
|
||||
# selector of our row as the selector of the sum of the
|
||||
# values defining our grind and the value defining our row.
|
||||
selection = selector(gridrow + (val1,))
|
||||
# We now iterate over all variations who generate the same
|
||||
# selector as 'selection'.
|
||||
filtered = [v for v in variations if selector(v.relevant_values()) == selection]
|
||||
for variation in sorted(filtered, key=sort):
|
||||
form = self.get_form(variation, data)
|
||||
formrow.append(form)
|
||||
forms_flat.append(form)
|
||||
|
||||
grids.append({'row': val1, 'forms': formrow})
|
||||
|
||||
forms.append({'row': ", ".join([value.value for value in gridrow]), 'forms': grids})
|
||||
|
||||
return forms, forms_flat
|
||||
|
||||
def main(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.properties = list(self.object.properties.current.all().prefetch_related("values"))
|
||||
self.dimension = len(self.properties)
|
||||
self.forms, self.forms_flat = self.get_forms()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.main(request, *args, **kwargs)
|
||||
context = self.get_context_data(object=self.object)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.main(request, *args, **kwargs)
|
||||
context = self.get_context_data(object=self.object)
|
||||
with transaction.atomic():
|
||||
for form in self.forms_flat:
|
||||
if form.is_valid() and form.has_changed():
|
||||
form.save()
|
||||
if hasattr(form.instance, 'creation') and form.instance.creation:
|
||||
# We need this special 'creation' field set to true in get_form
|
||||
# for newly created items as cleanerversion does already set the
|
||||
# primary key in its post_init hook
|
||||
form.instance.values.add(*form.values)
|
||||
# TODO: Redirect to success message
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_template_names(self) -> "List[str]":
|
||||
if self.dimension == 0:
|
||||
return ['pretixcontrol/item/variations_0d.html']
|
||||
elif self.dimension == 1:
|
||||
return ['pretixcontrol/item/variations_1d.html']
|
||||
elif self.dimension >= 2:
|
||||
return ['pretixcontrol/item/variations_nd.html']
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['forms'] = self.forms
|
||||
context['properties'] = self.properties
|
||||
return context
|
||||
|
||||
|
||||
class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
permission = 'can_change_items'
|
||||
template_name = 'pretixcontrol/item/restrictions.html'
|
||||
|
||||
def get_formsets(self):
|
||||
responses = restriction_formset.send(self.object.event, item=self.object)
|
||||
formsets = []
|
||||
for receiver, response in responses:
|
||||
response['formset'] = response['formsetclass'](
|
||||
self.request.POST if self.request.method == 'POST' else None,
|
||||
instance=self.object,
|
||||
prefix=response['prefix'],
|
||||
)
|
||||
formsets.append(response)
|
||||
return formsets
|
||||
|
||||
def main(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.request = request
|
||||
self.formsets = self.get_formsets()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.main(request, *args, **kwargs)
|
||||
context = self.get_context_data(object=self.object)
|
||||
return self.render_to_response(context)
|
||||
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.main(request, *args, **kwargs)
|
||||
valid = True
|
||||
for f in self.formsets:
|
||||
valid &= f['formset'].is_valid()
|
||||
if valid:
|
||||
for f in self.formsets:
|
||||
for form in f['formset']:
|
||||
if 'DELETE' in form.cleaned_data and form.cleaned_data['DELETE'] is True:
|
||||
if form.instance.pk is None:
|
||||
continue
|
||||
form.instance.delete()
|
||||
else:
|
||||
form.instance.event = request.event
|
||||
form.instance.item = self.object
|
||||
form.save()
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
context = self.get_context_data(object=self.object)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['formsets'] = self.formsets
|
||||
return context
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.item.restrictions', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'item': self.object.identity
|
||||
}) + '?success=true'
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import ListView
|
||||
|
||||
from pretix.base.models import Event
|
||||
|
||||
|
||||
class EventList(ListView):
|
||||
model = Event
|
||||
context_object_name = 'events'
|
||||
template_name = 'pretixcontrol/events/index.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return Event.objects.current.filter(
|
||||
permitted__id__exact=self.request.user.pk
|
||||
).prefetch_related(
|
||||
"organizer",
|
||||
)
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, 'pretixcontrol/base.html', {})
|
||||
Reference in New Issue
Block a user