forked from CGM_Public/pretix_original
Compare commits
15 Commits
fix-cond-d
...
release/1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33382681f3 | ||
|
|
340f7aaba4 | ||
|
|
f96e525cb5 | ||
|
|
a87fe32a77 | ||
|
|
55feddc239 | ||
|
|
28e146f2ad | ||
|
|
e7d6265b22 | ||
|
|
2603d321bb | ||
|
|
88f35cb361 | ||
|
|
aed6515971 | ||
|
|
a0be1b8389 | ||
|
|
17eb0a3817 | ||
|
|
a87cb75ed5 | ||
|
|
f70eb95346 | ||
|
|
9abdc6a3cc |
@@ -1 +1 @@
|
||||
__version__ = "1.6.0"
|
||||
__version__ = "1.6.2"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import csv
|
||||
import io
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
import pytz
|
||||
from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import receiver
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.core.urlresolvers import get_script_prefix
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||
from django.utils.translation.trans_real import (
|
||||
@@ -165,6 +166,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'/api/v1/docs/',
|
||||
)
|
||||
|
||||
def process_request(self, request):
|
||||
request.csp_nonce = get_random_string(length=32)
|
||||
|
||||
def process_response(self, request, resp):
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||
@@ -179,9 +183,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# frame-src is deprecated but kept for compatibility with CSP 1.0 browsers, e.g. Safari 9
|
||||
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'style-src': ["{static}"],
|
||||
'style-src': ["{static}", "'nonce-{nonce}'"],
|
||||
'connect-src': ["{dynamic}", "https://checkout.stripe.com"],
|
||||
'img-src': ["{static}", "data:", "https://*.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
# form-actions redirect to. In the context of e.g. payment providers or
|
||||
# single-sign-on this can be nearly anything so we cannot really restrict
|
||||
@@ -193,6 +197,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
|
||||
staticdomain = "'self'"
|
||||
dynamicdomain = "'self'"
|
||||
mediadomain = "'self'"
|
||||
if settings.MEDIA_URL.startswith('http'):
|
||||
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
|
||||
if settings.STATIC_URL.startswith('http'):
|
||||
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
|
||||
if settings.SITE_URL.startswith('http'):
|
||||
@@ -212,5 +219,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
dynamicdomain += " " + domain
|
||||
|
||||
if request.path not in self.CSP_EXEMPT:
|
||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain)
|
||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
|
||||
media=mediadomain, nonce=request.csp_nonce)
|
||||
return resp
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models.event import SubEvent
|
||||
@@ -68,7 +69,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'code': co.code
|
||||
}),
|
||||
'val': co.code,
|
||||
'val': escape(co.code),
|
||||
}
|
||||
elif isinstance(co, Voucher):
|
||||
a_text = _('Voucher {val}…')
|
||||
@@ -78,7 +79,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'voucher': co.id
|
||||
}),
|
||||
'val': co.code[:6],
|
||||
'val': escape(co.code[:6]),
|
||||
}
|
||||
elif isinstance(co, Item):
|
||||
a_text = _('Product {val}')
|
||||
@@ -88,7 +89,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'item': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, SubEvent):
|
||||
a_text = pgettext_lazy('subevent', 'Date {val}')
|
||||
@@ -98,7 +99,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'subevent': co.id
|
||||
}),
|
||||
'val': str(co)
|
||||
'val': escape(str(co))
|
||||
}
|
||||
elif isinstance(co, Quota):
|
||||
a_text = _('Quota {val}')
|
||||
@@ -108,7 +109,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'quota': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, ItemCategory):
|
||||
a_text = _('Category {val}')
|
||||
@@ -118,7 +119,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'category': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, Question):
|
||||
a_text = _('Question {val}')
|
||||
@@ -128,7 +129,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'question': co.id
|
||||
}),
|
||||
'val': co.question,
|
||||
'val': escape(co.question),
|
||||
}
|
||||
|
||||
if a_text and a_map:
|
||||
|
||||
@@ -12,11 +12,10 @@ from django.db import models
|
||||
from django.db.models import F, Sum
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import escape_uri_path
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_countries.fields import CountryField
|
||||
@@ -487,7 +486,19 @@ class QuestionAnswer(models.Model):
|
||||
)
|
||||
|
||||
@property
|
||||
def file_link(self):
|
||||
def backend_file_url(self):
|
||||
if self.file:
|
||||
if self.orderposition:
|
||||
return reverse('control:event.order.download.answer', kwargs={
|
||||
'code': self.orderposition.order.code,
|
||||
'event': self.orderposition.order.event.slug,
|
||||
'organizer': self.orderposition.order.event.organizer.slug,
|
||||
'answer': self.pk,
|
||||
})
|
||||
return ""
|
||||
|
||||
@property
|
||||
def frontend_file_url(self):
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
|
||||
if self.file:
|
||||
@@ -502,12 +513,13 @@ class QuestionAnswer(models.Model):
|
||||
'answer': self.pk,
|
||||
})
|
||||
|
||||
return mark_safe("<a href='{}'>{}</a>".format(
|
||||
url,
|
||||
escape(self.file.name.split('.', 1)[-1])
|
||||
))
|
||||
return url
|
||||
return ""
|
||||
|
||||
@property
|
||||
def file_name(self):
|
||||
return self.file.name.split('.', 1)[-1]
|
||||
|
||||
def __str__(self):
|
||||
if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True":
|
||||
return str(_("Yes"))
|
||||
|
||||
13
src/pretix/base/templatetags/escapejson.py
Normal file
13
src/pretix/base/templatetags/escapejson.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django import template
|
||||
from django.template.defaultfilters import stringfilter
|
||||
|
||||
from pretix.helpers.escapejson import escapejson
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter("escapejson")
|
||||
@stringfilter
|
||||
def escapejs_filter(value):
|
||||
"""Hex encodes characters for use in a application/json type script."""
|
||||
return escapejson(value)
|
||||
@@ -1,6 +1,12 @@
|
||||
import urllib.parse
|
||||
|
||||
import bleach
|
||||
import markdown
|
||||
from bleach import DEFAULT_CALLBACKS
|
||||
from django import template
|
||||
from django.core import signing
|
||||
from django.urls import reverse
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
register = template.Library()
|
||||
@@ -48,6 +54,15 @@ ALLOWED_ATTRIBUTES = {
|
||||
}
|
||||
|
||||
|
||||
def safelink_callback(attrs, new=False):
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
if not is_safe_url(url):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
attrs[None, 'target'] = '_blank'
|
||||
return attrs
|
||||
|
||||
|
||||
@register.filter
|
||||
def rich_text(text: str, **kwargs):
|
||||
"""
|
||||
@@ -58,5 +73,5 @@ def rich_text(text: str, **kwargs):
|
||||
markdown.markdown(text),
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
))
|
||||
), callbacks=DEFAULT_CALLBACKS + [safelink_callback])
|
||||
return mark_safe(body_md)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from django import template
|
||||
|
||||
from pretix.helpers.safedownload import get_token
|
||||
|
||||
from ..views.redirect import safelink as sl
|
||||
|
||||
register = template.Library()
|
||||
@@ -8,3 +10,8 @@ register = template.Library()
|
||||
@register.simple_tag
|
||||
def safelink(url):
|
||||
return sl(url)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def answer_token(request, answer):
|
||||
return get_token(request, answer)
|
||||
|
||||
@@ -690,7 +690,7 @@ class DisplaySettingsForm(SettingsForm):
|
||||
)
|
||||
logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
help_text=_('If you provide a logo image, we will by default not show your events name and date '
|
||||
'in the page header. We will show your logo with a maximal height of 120 pixels.')
|
||||
|
||||
@@ -195,7 +195,7 @@ class ItemCreateForm(I18nModelForm):
|
||||
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
if not self.event.has_subevents:
|
||||
if not self.event.has_subevents and not self.cleaned_data.get('has_variations'):
|
||||
if self.cleaned_data.get('quota_option') == self.EXISTING and self.cleaned_data.get('quota_add_existing') is not None:
|
||||
quota = self.cleaned_data.get('quota_add_existing')
|
||||
quota.items.add(self.instance)
|
||||
|
||||
@@ -128,7 +128,7 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
|
||||
organizer_logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. We will show your logo with a maximal height of 120 pixels.')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% load formset_tags %}
|
||||
{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
|
||||
{% block inside %}
|
||||
@@ -58,7 +59,7 @@
|
||||
<div class="chart" id="question_chart" data-type="{{ question.type }}">
|
||||
|
||||
</div>
|
||||
<script type="application/json" id="question-chart-data">{{ stats_json|safe }}</script>
|
||||
<script type="application/json" id="question-chart-data">{{ stats_json|escapejson }}</script>
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<table class="table table-bordered table-hover">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% load eventsignal %}
|
||||
{% block title %}{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}{% endblock %}
|
||||
{% block inside %}
|
||||
@@ -25,7 +26,7 @@
|
||||
<div class="chart" id="quota_chart">
|
||||
|
||||
</div>
|
||||
<script type="application/json" id="quota-chart-data">{{ quota_chart_data|safe }}</script>
|
||||
<script type="application/json" id="quota-chart-data">{{ quota_chart_data|escapejson }}</script>
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<legend>{% trans "Availability calculation" %}</legend>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load eventurl %}
|
||||
{% load safelink %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Order details: {{ code }}
|
||||
@@ -210,7 +211,14 @@
|
||||
<dd>
|
||||
{% if q.answer %}
|
||||
{% if q.answer.file %}
|
||||
<span class="fa fa-file"></span> {{ q.answer.file_link }}
|
||||
<span class="fa fa-file"></span>
|
||||
<a href="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}">
|
||||
{{ q.answer.file_name }}
|
||||
</a>
|
||||
<span class="label label-danger" data-toggle="tooltip"
|
||||
title="{% trans "This file has been uploaded by a user and could contain viruses or other malicious content." %}">
|
||||
{% trans "UNSAFE" %}
|
||||
</span>
|
||||
{% else %}
|
||||
{{ q.answer|linebreaksbr }}
|
||||
{% endif %}
|
||||
|
||||
@@ -128,6 +128,9 @@ urlpatterns = [
|
||||
name='event.order.regeninvoice'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/invoices/(?P<id>\d+)/reissue$', orders.OrderInvoiceReissue.as_view(),
|
||||
name='event.order.reissueinvoice'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/answer/(?P<answer>[^/]+)/$',
|
||||
orders.AnswerDownload.as_view(),
|
||||
name='event.order.download.answer'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/extend$', orders.OrderExtend.as_view(),
|
||||
name='event.order.extend'),
|
||||
url(r'^orders/(?P<code>[0-9A-Z]+)/contact$', orders.OrderContactChange.as_view(),
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.shortcuts import render
|
||||
from django.template.loader import get_template
|
||||
from django.utils import formats
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import (
|
||||
@@ -136,7 +137,7 @@ def quota_widgets(sender, **kwargs):
|
||||
status, left = q.availability()
|
||||
widgets.append({
|
||||
'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
|
||||
text=_('{quota} left').format(quota=q.name)),
|
||||
text=_('{quota} left').format(quota=escape(q.name))),
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
'url': reverse('control:event.items.quotas.show', kwargs={
|
||||
@@ -258,7 +259,8 @@ def user_event_widgets(**kwargs):
|
||||
for event in events:
|
||||
widgets.append({
|
||||
'content': '<div class="event">{event}<span class="from">{df}</span><span class="to">{dt}</span></div>'.format(
|
||||
event=event.name, df=date_format(event.date_from, 'SHORT_DATE_FORMAT') if event.date_from else '',
|
||||
event=escape(event.name),
|
||||
df=date_format(event.date_from, 'SHORT_DATE_FORMAT') if event.date_from else '',
|
||||
dt=date_format(event.date_to, 'SHORT_DATE_FORMAT') if event.date_to else ''
|
||||
),
|
||||
'display_size': 'small',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import mimetypes
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
import pytz
|
||||
@@ -6,7 +8,7 @@ from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count
|
||||
from django.http import FileResponse, Http404, HttpResponseNotAllowed
|
||||
from django.shortcuts import redirect, render
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
@@ -19,7 +21,8 @@ from i18nfield.strings import LazyI18nString
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, CachedTicket, Invoice, InvoiceAddress, Item, ItemVariation,
|
||||
LogEntry, Order, Quota, generate_position_secret, generate_secret,
|
||||
LogEntry, Order, QuestionAnswer, Quota, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.export import export
|
||||
@@ -41,6 +44,7 @@ from pretix.control.forms.orders import (
|
||||
OrderMailForm, OrderPositionAddForm, OrderPositionChangeForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.helpers.safedownload import check_token
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.signals import question_form_fields
|
||||
|
||||
@@ -739,6 +743,29 @@ class OrderEmailHistory(EventPermissionRequiredMixin, OrderViewMixin, ListView):
|
||||
return qs
|
||||
|
||||
|
||||
class AnswerDownload(EventPermissionRequiredMixin, OrderViewMixin, ListView):
|
||||
permission = 'can_view_orders'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
answid = kwargs.get('answer')
|
||||
token = request.GET.get('token', '')
|
||||
|
||||
answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid)
|
||||
if not answer.file:
|
||||
raise Http404()
|
||||
if not check_token(request, answer, token):
|
||||
raise Http404(_("This link is no longer valid. Please go back, refresh the page, and try again."))
|
||||
|
||||
ftype, ignored = mimetypes.guess_type(answer.file.name)
|
||||
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code,
|
||||
answer.orderposition.positionid,
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
class OverView(EventPermissionRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/orders/overview.html'
|
||||
permission = 'can_view_orders'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import csv
|
||||
import io
|
||||
|
||||
from defusedcsv import csv
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
|
||||
16
src/pretix/helpers/escapejson.py
Normal file
16
src/pretix/helpers/escapejson.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import keep_lazy
|
||||
from django.utils.safestring import SafeText, mark_safe
|
||||
|
||||
_json_escapes = {
|
||||
ord('>'): '\\u003E',
|
||||
ord('<'): '\\u003C',
|
||||
ord('&'): '\\u0026',
|
||||
}
|
||||
|
||||
|
||||
@keep_lazy(six.text_type, SafeText)
|
||||
def escapejson(value):
|
||||
"""Hex encodes characters for use in a application/json type script."""
|
||||
return mark_safe(force_text(value).translate(_json_escapes))
|
||||
18
src/pretix/helpers/safedownload.py
Normal file
18
src/pretix/helpers/safedownload.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import hashlib
|
||||
|
||||
from django.core.signing import BadSignature, TimestampSigner
|
||||
|
||||
|
||||
def get_token(request, answer):
|
||||
payload = '{}:{}'.format(request.session.session_key, answer.pk)
|
||||
signer = TimestampSigner()
|
||||
return signer.sign(hashlib.sha1(payload.encode()).hexdigest())
|
||||
|
||||
|
||||
def check_token(request, answer, token):
|
||||
payload = hashlib.sha1('{}:{}'.format(request.session.session_key, answer.pk).encode()).hexdigest()
|
||||
signer = TimestampSigner()
|
||||
try:
|
||||
return payload == signer.unsign(token, max_age=3600 * 24)
|
||||
except BadSignature:
|
||||
return False
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block inner %}
|
||||
<h2>{% trans "Import result" %}</h2>
|
||||
{% if job.state == "running" or job.state == "pending" %}
|
||||
<div class="empty-collection" data-job-waiting data-job-waiting-url="{% url "plugins:banktransfer:import.job" event=request.event.slug organizer=request.event.organizer.slug job=job.pk %}?ajax=1">
|
||||
<div class="empty-collection" data-job-waiting data-job-waiting-url="{% if request.event %}{% url "plugins:banktransfer:import.job" event=request.event.slug organizer=request.event.organizer.slug job=job.pk %}{% else %}{% url "plugins:banktransfer:import.job" organizer=request.organizer.slug job=job.pk %}{% endif %}?ajax=1">
|
||||
<p>
|
||||
<span class="fa big-grey-icon fa-cog fa big-rotating-icon"></span>
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import csv
|
||||
import io
|
||||
from collections import OrderedDict
|
||||
|
||||
from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.translation import (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load compress %}
|
||||
{% load staticfiles %}
|
||||
{% load escapejson %}
|
||||
{% block title %}{% trans "Statistics" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Statistics" %}</h1>
|
||||
@@ -30,9 +31,9 @@
|
||||
<div id="obp_chart" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/json" id="obd-data">{{ obd_data|safe }}</script>
|
||||
<script type="application/json" id="rev-data">{{ rev_data|safe }}</script>
|
||||
<script type="application/json" id="obp-data">{{ obp_data|safe }}</script>
|
||||
<script type="application/json" id="obd-data">{{ obd_data|escapejson }}</script>
|
||||
<script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script>
|
||||
<script type="application/json" id="obp-data">{{ obp_data|escapejson }}</script>
|
||||
<script type="application/text" id="currency">{{ request.event.currency }}</script>
|
||||
<script type="application/javascript" src="{% static "pretixplugins/statistics/statistics.js" %}"></script>
|
||||
{% else %}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
|
||||
import django_libsass
|
||||
import sass
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.templatetags.static import static as _static
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.services.async import ProfiledTask
|
||||
from pretix.celery_app import app
|
||||
from pretix.multidomain.urlreverse import get_domain
|
||||
|
||||
logger = logging.getLogger('pretix.presale.style')
|
||||
|
||||
@@ -25,10 +28,25 @@ def regenerate_css(event_id: int):
|
||||
sassrules.append('$brand-primary: {};'.format(event.settings.get('primary_color')))
|
||||
sassrules.append('@import "main.scss";')
|
||||
|
||||
def static(path):
|
||||
sp = _static(path)
|
||||
if not settings.MEDIA_URL.startswith("/") and sp.startswith("/"):
|
||||
domain = get_domain(event.organizer)
|
||||
if domain:
|
||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||
sp = urljoin('%s://%s' % (siteurlsplit.scheme, domain), sp)
|
||||
else:
|
||||
sp = urljoin(settings.SITE_URL, sp)
|
||||
return '"{}"'.format(sp)
|
||||
|
||||
cf = dict(django_libsass.CUSTOM_FUNCTIONS)
|
||||
cf['static'] = static
|
||||
css = sass.compile(
|
||||
string="\n".join(sassrules),
|
||||
include_paths=[sassdir], output_style='compressed',
|
||||
custom_functions=django_libsass.CUSTOM_FUNCTIONS
|
||||
custom_functions=cf
|
||||
)
|
||||
checksum = hashlib.sha1(css.encode('utf-8')).hexdigest()
|
||||
fname = '{}/{}/presale.{}.css'.format(
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "lightbox/css/lightbox.scss" %}" />
|
||||
{% endcompress %}
|
||||
{% if css_file %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}" nonce="{{ request.csp_nonce }}" />
|
||||
{% else %}
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load safelink %}
|
||||
{% for line in cart.positions %}
|
||||
<div class="row cart-row {% if download and line.item.admission|default:event.settings.ticket_download_nonadm %}has-downloads{% endif %}">
|
||||
<div class="product">
|
||||
@@ -33,7 +34,10 @@
|
||||
<dd>
|
||||
{% if q.answer %}
|
||||
{% if q.answer.file %}
|
||||
<span class="fa fa-file"></span> {{ q.answer.file_link }}
|
||||
<span class="fa fa-file"></span>
|
||||
<a href="{{ q.answer.frontend_file_url }}?token={% answer_token request q.answer %}">
|
||||
{{ q.answer.file_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ q.answer|linebreaksbr }}
|
||||
{% endif %}
|
||||
|
||||
@@ -170,7 +170,8 @@
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name }}"
|
||||
data-title="{{ item.name|force_escape|force_escape }}"
|
||||
{# Yes, double-escape to prevent XSS in lightbox #}
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumbnail_url:'productlist' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
@@ -281,7 +282,8 @@
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name }}"
|
||||
data-title="{{ item.name|force_escape|force_escape }}"
|
||||
{# Yes, double-escape to prevent XSS in lightbox #}
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumbnail_url:'productlist' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name }}"
|
||||
data-title="{{ item.name|force_escape|force_escape }}"
|
||||
{# Yes, double-escape to prevent XSS in lightbox #}
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumbnail_url:'productlist' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
@@ -125,7 +126,8 @@
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
data-title="{{ item.name }}"
|
||||
data-title="{{ item.name|force_escape|force_escape }}"
|
||||
{# Yes, double-escape to prevent XSS in lightbox #}
|
||||
data-lightbox="{{ item.id }}">
|
||||
<img src="{{ item.picture|thumbnail_url:'productlist' }}"
|
||||
alt="{{ item.name }}"/>
|
||||
|
||||
@@ -22,6 +22,7 @@ from pretix.base.services.tickets import (
|
||||
get_cachedticket_for_order, get_cachedticket_for_position,
|
||||
)
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.helpers.safedownload import check_token
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.forms.checkout import InvoiceAddressForm
|
||||
from pretix.presale.views import CartMixin, EventViewMixin
|
||||
@@ -502,11 +503,15 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
|
||||
class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
answid = kwargs.get('answer')
|
||||
token = request.GET.get('token', '')
|
||||
|
||||
answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid)
|
||||
if not answer.file:
|
||||
return Http404()
|
||||
raise Http404()
|
||||
if not check_token(request, answer, token):
|
||||
raise Http404(_("This link is no longer valid. Please go back, refresh the page, and try again."))
|
||||
|
||||
ftype, _ = mimetypes.guess_type(answer.file.name)
|
||||
ftype, ignored = mimetypes.guess_type(answer.file.name)
|
||||
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* @license
|
||||
morris.js v0.5.0
|
||||
morris.js v0.5.1
|
||||
Copyright 2014 Olly Smith All rights reserved.
|
||||
Licensed under the BSD-2-Clause License.
|
||||
*/
|
||||
@@ -74,6 +74,7 @@ Licensed under the BSD-2-Clause License.
|
||||
__extends(Grid, _super);
|
||||
|
||||
function Grid(options) {
|
||||
this.hasToShow = __bind(this.hasToShow, this);
|
||||
this.resizeHandler = __bind(this.resizeHandler, this);
|
||||
var _this = this;
|
||||
if (typeof options.element === 'string') {
|
||||
@@ -197,7 +198,7 @@ Licensed under the BSD-2-Clause License.
|
||||
};
|
||||
|
||||
Grid.prototype.setData = function(data, redraw) {
|
||||
var e, idx, index, maxGoal, minGoal, ret, row, step, total, y, ykey, ymax, ymin, yval, _ref;
|
||||
var e, flatEvents, from, idx, index, maxGoal, minGoal, ret, row, step, to, total, y, ykey, ymax, ymin, yval, _i, _len, _ref, _ref1;
|
||||
if (redraw == null) {
|
||||
redraw = true;
|
||||
}
|
||||
@@ -254,7 +255,7 @@ Licensed under the BSD-2-Clause License.
|
||||
if ((yval != null) && typeof yval !== 'number') {
|
||||
yval = null;
|
||||
}
|
||||
if (yval != null) {
|
||||
if ((yval != null) && this.hasToShow(idx)) {
|
||||
if (this.cumulative) {
|
||||
total += yval;
|
||||
} else {
|
||||
@@ -288,21 +289,24 @@ Licensed under the BSD-2-Clause License.
|
||||
this.events = [];
|
||||
if (this.options.events.length > 0) {
|
||||
if (this.options.parseTime) {
|
||||
this.events = (function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = this.options.events;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
e = _ref[_i];
|
||||
_results.push(Morris.parseDate(e));
|
||||
_ref = this.options.events;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
e = _ref[_i];
|
||||
if (e instanceof Array) {
|
||||
from = e[0], to = e[1];
|
||||
this.events.push([Morris.parseDate(from), Morris.parseDate(to)]);
|
||||
} else {
|
||||
this.events.push(Morris.parseDate(e));
|
||||
}
|
||||
return _results;
|
||||
}).call(this);
|
||||
}
|
||||
} else {
|
||||
this.events = this.options.events;
|
||||
}
|
||||
this.xmax = Math.max(this.xmax, Math.max.apply(Math, this.events));
|
||||
this.xmin = Math.min(this.xmin, Math.min.apply(Math, this.events));
|
||||
flatEvents = $.map(this.events, function(e) {
|
||||
return e;
|
||||
});
|
||||
this.xmax = Math.max(this.xmax, Math.max.apply(Math, flatEvents));
|
||||
this.xmin = Math.min(this.xmin, Math.min.apply(Math, flatEvents));
|
||||
}
|
||||
if (this.xmin === this.xmax) {
|
||||
this.xmin -= 1;
|
||||
@@ -316,7 +320,7 @@ Licensed under the BSD-2-Clause License.
|
||||
}
|
||||
this.ymax += 1;
|
||||
}
|
||||
if (((_ref = this.options.axes) === true || _ref === 'both' || _ref === 'y') || this.options.grid === true) {
|
||||
if (((_ref1 = this.options.axes) === true || _ref1 === 'both' || _ref1 === 'y') || this.options.grid === true) {
|
||||
if (this.options.ymax === this.gridDefaults.ymax && this.options.ymin === this.gridDefaults.ymin) {
|
||||
this.grid = this.autoGridLines(this.ymin, this.ymax, this.options.numLines);
|
||||
this.ymin = Math.min(this.ymin, this.grid[0]);
|
||||
@@ -324,9 +328,9 @@ Licensed under the BSD-2-Clause License.
|
||||
} else {
|
||||
step = (this.ymax - this.ymin) / (this.options.numLines - 1);
|
||||
this.grid = (function() {
|
||||
var _i, _ref1, _ref2, _results;
|
||||
var _j, _ref2, _ref3, _results;
|
||||
_results = [];
|
||||
for (y = _i = _ref1 = this.ymin, _ref2 = this.ymax; step > 0 ? _i <= _ref2 : _i >= _ref2; y = _i += step) {
|
||||
for (y = _j = _ref2 = this.ymin, _ref3 = this.ymax; step > 0 ? _j <= _ref3 : _j >= _ref3; y = _j += step) {
|
||||
_results.push(y);
|
||||
}
|
||||
return _results;
|
||||
@@ -405,7 +409,7 @@ Licensed under the BSD-2-Clause License.
|
||||
};
|
||||
|
||||
Grid.prototype._calc = function() {
|
||||
var bottomOffsets, gridLine, h, i, w, yLabelWidths, _ref, _ref1;
|
||||
var angle, bottomOffsets, gridLine, h, i, w, yLabelWidths, _ref, _ref1;
|
||||
w = this.el.width();
|
||||
h = this.el.height();
|
||||
if (this.elementWidth !== w || this.elementHeight !== h || this.dirty) {
|
||||
@@ -427,23 +431,53 @@ Licensed under the BSD-2-Clause License.
|
||||
}
|
||||
return _results;
|
||||
}).call(this);
|
||||
this.left += Math.max.apply(Math, yLabelWidths);
|
||||
if (!this.options.horizontal) {
|
||||
this.left += Math.max.apply(Math, yLabelWidths);
|
||||
} else {
|
||||
this.bottom -= Math.max.apply(Math, yLabelWidths);
|
||||
}
|
||||
}
|
||||
if ((_ref1 = this.options.axes) === true || _ref1 === 'both' || _ref1 === 'x') {
|
||||
if (!this.options.horizontal) {
|
||||
angle = -this.options.xLabelAngle;
|
||||
} else {
|
||||
angle = -90;
|
||||
}
|
||||
bottomOffsets = (function() {
|
||||
var _i, _ref2, _results;
|
||||
_results = [];
|
||||
for (i = _i = 0, _ref2 = this.data.length; 0 <= _ref2 ? _i < _ref2 : _i > _ref2; i = 0 <= _ref2 ? ++_i : --_i) {
|
||||
_results.push(this.measureText(this.data[i].text, -this.options.xLabelAngle).height);
|
||||
_results.push(this.measureText(this.data[i].label, angle).height);
|
||||
}
|
||||
return _results;
|
||||
}).call(this);
|
||||
this.bottom -= Math.max.apply(Math, bottomOffsets);
|
||||
if (!this.options.horizontal) {
|
||||
this.bottom -= Math.max.apply(Math, bottomOffsets);
|
||||
} else {
|
||||
this.left += Math.max.apply(Math, bottomOffsets);
|
||||
}
|
||||
}
|
||||
this.width = Math.max(1, this.right - this.left);
|
||||
this.height = Math.max(1, this.bottom - this.top);
|
||||
this.dx = this.width / (this.xmax - this.xmin);
|
||||
this.dy = this.height / (this.ymax - this.ymin);
|
||||
if (!this.options.horizontal) {
|
||||
this.dx = this.width / (this.xmax - this.xmin);
|
||||
this.dy = this.height / (this.ymax - this.ymin);
|
||||
this.yStart = this.bottom;
|
||||
this.yEnd = this.top;
|
||||
this.xStart = this.left;
|
||||
this.xEnd = this.right;
|
||||
this.xSize = this.width;
|
||||
this.ySize = this.height;
|
||||
} else {
|
||||
this.dx = this.height / (this.xmax - this.xmin);
|
||||
this.dy = this.width / (this.ymax - this.ymin);
|
||||
this.yStart = this.left;
|
||||
this.yEnd = this.right;
|
||||
this.xStart = this.top;
|
||||
this.xEnd = this.bottom;
|
||||
this.xSize = this.height;
|
||||
this.ySize = this.width;
|
||||
}
|
||||
if (this.calc) {
|
||||
return this.calc();
|
||||
}
|
||||
@@ -451,14 +485,18 @@ Licensed under the BSD-2-Clause License.
|
||||
};
|
||||
|
||||
Grid.prototype.transY = function(y) {
|
||||
return this.bottom - (y - this.ymin) * this.dy;
|
||||
if (!this.options.horizontal) {
|
||||
return this.bottom - (y - this.ymin) * this.dy;
|
||||
} else {
|
||||
return this.left + (y - this.ymin) * this.dy;
|
||||
}
|
||||
};
|
||||
|
||||
Grid.prototype.transX = function(x) {
|
||||
if (this.data.length === 1) {
|
||||
return (this.left + this.right) / 2;
|
||||
return (this.xStart + this.xEnd) / 2;
|
||||
} else {
|
||||
return this.left + (x - this.xmin) * this.dx;
|
||||
return this.xStart + (x - this.xmin) * this.dx;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -485,32 +523,50 @@ Licensed under the BSD-2-Clause License.
|
||||
};
|
||||
|
||||
Grid.prototype.yAxisFormat = function(label) {
|
||||
return this.yLabelFormat(label);
|
||||
return this.yLabelFormat(label, 0);
|
||||
};
|
||||
|
||||
Grid.prototype.yLabelFormat = function(label) {
|
||||
Grid.prototype.yLabelFormat = function(label, i) {
|
||||
if (typeof this.options.yLabelFormat === 'function') {
|
||||
return this.options.yLabelFormat(label);
|
||||
return this.options.yLabelFormat(label, i);
|
||||
} else {
|
||||
return "" + this.options.preUnits + (Morris.commas(label)) + this.options.postUnits;
|
||||
}
|
||||
};
|
||||
|
||||
Grid.prototype.getYAxisLabelX = function() {
|
||||
return this.left - this.options.padding / 2;
|
||||
};
|
||||
|
||||
Grid.prototype.drawGrid = function() {
|
||||
var lineY, y, _i, _len, _ref, _ref1, _ref2, _results;
|
||||
var basePos, lineY, pos, _i, _len, _ref, _ref1, _ref2, _results;
|
||||
if (this.options.grid === false && ((_ref = this.options.axes) !== true && _ref !== 'both' && _ref !== 'y')) {
|
||||
return;
|
||||
}
|
||||
if (!this.options.horizontal) {
|
||||
basePos = this.getYAxisLabelX();
|
||||
} else {
|
||||
basePos = this.getXAxisLabelY();
|
||||
}
|
||||
_ref1 = this.grid;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
|
||||
lineY = _ref1[_i];
|
||||
y = this.transY(lineY);
|
||||
pos = this.transY(lineY);
|
||||
if ((_ref2 = this.options.axes) === true || _ref2 === 'both' || _ref2 === 'y') {
|
||||
this.drawYAxisLabel(this.left - this.options.padding / 2, y, this.yAxisFormat(lineY));
|
||||
if (!this.options.horizontal) {
|
||||
this.drawYAxisLabel(basePos, pos, this.yAxisFormat(lineY));
|
||||
} else {
|
||||
this.drawXAxisLabel(pos, basePos, this.yAxisFormat(lineY));
|
||||
}
|
||||
}
|
||||
if (this.options.grid) {
|
||||
_results.push(this.drawGridLine("M" + this.left + "," + y + "H" + (this.left + this.width)));
|
||||
pos = Math.floor(pos) + 0.5;
|
||||
if (!this.options.horizontal) {
|
||||
_results.push(this.drawGridLine("M" + this.xStart + "," + pos + "H" + this.xEnd));
|
||||
} else {
|
||||
_results.push(this.drawGridLine("M" + pos + "," + this.xStart + "V" + this.xEnd));
|
||||
}
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
@@ -543,11 +599,42 @@ Licensed under the BSD-2-Clause License.
|
||||
};
|
||||
|
||||
Grid.prototype.drawGoal = function(goal, color) {
|
||||
return this.raphael.path("M" + this.left + "," + (this.transY(goal)) + "H" + this.right).attr('stroke', color).attr('stroke-width', this.options.goalStrokeWidth);
|
||||
var path, y;
|
||||
y = Math.floor(this.transY(goal)) + 0.5;
|
||||
if (!this.options.horizontal) {
|
||||
path = "M" + this.xStart + "," + y + "H" + this.xEnd;
|
||||
} else {
|
||||
path = "M" + y + "," + this.xStart + "V" + this.xEnd;
|
||||
}
|
||||
return this.raphael.path(path).attr('stroke', color).attr('stroke-width', this.options.goalStrokeWidth);
|
||||
};
|
||||
|
||||
Grid.prototype.drawEvent = function(event, color) {
|
||||
return this.raphael.path("M" + (this.transX(event)) + "," + this.bottom + "V" + this.top).attr('stroke', color).attr('stroke-width', this.options.eventStrokeWidth);
|
||||
var from, path, to, x;
|
||||
if (event instanceof Array) {
|
||||
from = event[0], to = event[1];
|
||||
from = Math.floor(this.transX(from)) + 0.5;
|
||||
to = Math.floor(this.transX(to)) + 0.5;
|
||||
if (!this.options.horizontal) {
|
||||
return this.raphael.rect(from, this.yEnd, to - from, this.yStart - this.yEnd).attr({
|
||||
fill: color,
|
||||
stroke: false
|
||||
}).toBack();
|
||||
} else {
|
||||
return this.raphael.rect(this.yStart, from, this.yEnd - this.yStart, to - from).attr({
|
||||
fill: color,
|
||||
stroke: false
|
||||
}).toBack();
|
||||
}
|
||||
} else {
|
||||
x = Math.floor(this.transX(event)) + 0.5;
|
||||
if (!this.options.horizontal) {
|
||||
path = "M" + x + "," + this.yStart + "V" + this.yEnd;
|
||||
} else {
|
||||
path = "M" + this.yStart + "," + x + "H" + this.yEnd;
|
||||
}
|
||||
return this.raphael.path(path).attr('stroke', color).attr('stroke-width', this.options.eventStrokeWidth);
|
||||
}
|
||||
};
|
||||
|
||||
Grid.prototype.drawYAxisLabel = function(xPos, yPos, text) {
|
||||
@@ -586,6 +673,10 @@ Licensed under the BSD-2-Clause License.
|
||||
return this.redraw();
|
||||
};
|
||||
|
||||
Grid.prototype.hasToShow = function(i) {
|
||||
return this.options.shown === true || this.options.shown[i] === true;
|
||||
};
|
||||
|
||||
return Grid;
|
||||
|
||||
})(Morris.EventEmitter);
|
||||
@@ -662,13 +753,13 @@ Licensed under the BSD-2-Clause License.
|
||||
this.options.parent.append(this.el);
|
||||
}
|
||||
|
||||
Hover.prototype.update = function(html, x, y) {
|
||||
Hover.prototype.update = function(html, x, y, centre_y) {
|
||||
if (!html) {
|
||||
return this.hide();
|
||||
} else {
|
||||
this.html(html);
|
||||
this.show();
|
||||
return this.moveTo(x, y);
|
||||
return this.moveTo(x, y, centre_y);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -676,7 +767,7 @@ Licensed under the BSD-2-Clause License.
|
||||
return this.el.html(content);
|
||||
};
|
||||
|
||||
Hover.prototype.moveTo = function(x, y) {
|
||||
Hover.prototype.moveTo = function(x, y, centre_y) {
|
||||
var hoverHeight, hoverWidth, left, parentHeight, parentWidth, top;
|
||||
parentWidth = this.options.parent.innerWidth();
|
||||
parentHeight = this.options.parent.innerHeight();
|
||||
@@ -684,11 +775,18 @@ Licensed under the BSD-2-Clause License.
|
||||
hoverHeight = this.el.outerHeight();
|
||||
left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth);
|
||||
if (y != null) {
|
||||
top = y - hoverHeight - 10;
|
||||
if (top < 0) {
|
||||
top = y + 10;
|
||||
if (top + hoverHeight > parentHeight) {
|
||||
top = parentHeight / 2 - hoverHeight / 2;
|
||||
if (centre_y === true) {
|
||||
top = y - hoverHeight / 2;
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
} else {
|
||||
top = y - hoverHeight - 10;
|
||||
if (top < 0) {
|
||||
top = y + 10;
|
||||
if (top + hoverHeight > parentHeight) {
|
||||
top = parentHeight / 2 - hoverHeight / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -745,10 +843,14 @@ Licensed under the BSD-2-Clause License.
|
||||
pointStrokeColors: ['#ffffff'],
|
||||
pointFillColors: [],
|
||||
smooth: true,
|
||||
shown: true,
|
||||
xLabels: 'auto',
|
||||
xLabelFormat: null,
|
||||
xLabelMargin: 24,
|
||||
hideHover: false
|
||||
hideHover: false,
|
||||
trendLine: false,
|
||||
trendLineWidth: 2,
|
||||
trendLineColors: ['#689bc3', '#a2b3bf', '#64b764']
|
||||
};
|
||||
|
||||
Line.prototype.calc = function() {
|
||||
@@ -840,11 +942,15 @@ Licensed under the BSD-2-Clause License.
|
||||
Line.prototype.hoverContentForRow = function(index) {
|
||||
var content, j, row, y, _i, _len, _ref;
|
||||
row = this.data[index];
|
||||
content = "<div class='morris-hover-row-label'>" + row.label + "</div>";
|
||||
content = $("<div class='morris-hover-row-label'>").text(row.label);
|
||||
content = content.prop('outerHTML');
|
||||
_ref = row.y;
|
||||
for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) {
|
||||
y = _ref[j];
|
||||
content += "<div class='morris-hover-point' style='color: " + (this.colorFor(row, j, 'label')) + "'>\n " + this.options.labels[j] + ":\n " + (this.yLabelFormat(y)) + "\n</div>";
|
||||
if (this.options.labels[j] === false) {
|
||||
continue;
|
||||
}
|
||||
content += "<div class='morris-hover-point' style='color: " + (this.colorFor(row, j, 'label')) + "'>\n " + this.options.labels[j] + ":\n " + (this.yLabelFormat(y, j)) + "\n</div>";
|
||||
}
|
||||
if (typeof this.options.hoverCallback === 'function') {
|
||||
content = this.options.hoverCallback(index, this.options, content, row.src);
|
||||
@@ -954,11 +1060,20 @@ Licensed under the BSD-2-Clause License.
|
||||
var i, _i, _j, _ref, _ref1, _results;
|
||||
this.seriesPoints = [];
|
||||
for (i = _i = _ref = this.options.ykeys.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) {
|
||||
this._drawLineFor(i);
|
||||
if (this.hasToShow(i)) {
|
||||
if (this.options.trendLine !== false && this.options.trendLine === true || this.options.trendLine[i] === true) {
|
||||
this._drawTrendLine(i);
|
||||
}
|
||||
this._drawLineFor(i);
|
||||
}
|
||||
}
|
||||
_results = [];
|
||||
for (i = _j = _ref1 = this.options.ykeys.length - 1; _ref1 <= 0 ? _j <= 0 : _j >= 0; i = _ref1 <= 0 ? ++_j : --_j) {
|
||||
_results.push(this._drawPointFor(i));
|
||||
if (this.hasToShow(i)) {
|
||||
_results.push(this._drawPointFor(i));
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
@@ -987,6 +1102,38 @@ Licensed under the BSD-2-Clause License.
|
||||
}
|
||||
};
|
||||
|
||||
Line.prototype._drawTrendLine = function(index) {
|
||||
var a, b, data, datapoints, path, sum_x, sum_xx, sum_xy, sum_y, val, x, y, _i, _len, _ref;
|
||||
sum_x = 0;
|
||||
sum_y = 0;
|
||||
sum_xx = 0;
|
||||
sum_xy = 0;
|
||||
datapoints = 0;
|
||||
_ref = this.data;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
val = _ref[_i];
|
||||
x = val.x;
|
||||
y = val.y[index];
|
||||
if (y === void 0) {
|
||||
continue;
|
||||
}
|
||||
datapoints += 1;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_xx += x * x;
|
||||
sum_xy += x * y;
|
||||
}
|
||||
a = (datapoints * sum_xy - sum_x * sum_y) / (datapoints * sum_xx - sum_x * sum_x);
|
||||
b = (sum_y / datapoints) - ((a * sum_x) / datapoints);
|
||||
data = [{}, {}];
|
||||
data[0].x = this.transX(this.data[0].x);
|
||||
data[0].y = this.transY(this.data[0].x * a + b);
|
||||
data[1].x = this.transX(this.data[this.data.length - 1].x);
|
||||
data[1].y = this.transY(this.data[this.data.length - 1].x * a + b);
|
||||
path = Morris.Line.createPath(data, false, this.bottom);
|
||||
return path = this.raphael.path(path).attr('stroke', this.colorFor(null, index, 'trendLine')).attr('stroke-width', this.options.trendLineWidth);
|
||||
};
|
||||
|
||||
Line.createPath = function(coords, smooth, bottom) {
|
||||
var coord, g, grads, i, ix, lg, path, prevCoord, x1, x2, y1, y2, _i, _len;
|
||||
path = "";
|
||||
@@ -1078,8 +1225,10 @@ Licensed under the BSD-2-Clause License.
|
||||
return this.options.lineColors.call(this, row, sidx, type);
|
||||
} else if (type === 'point') {
|
||||
return this.options.pointFillColors[sidx % this.options.pointFillColors.length] || this.options.lineColors[sidx % this.options.lineColors.length];
|
||||
} else {
|
||||
} else if (type === 'line') {
|
||||
return this.options.lineColors[sidx % this.options.lineColors.length];
|
||||
} else {
|
||||
return this.options.trendLineColors[sidx % this.options.trendLineColors.length];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1120,6 +1269,9 @@ Licensed under the BSD-2-Clause License.
|
||||
};
|
||||
|
||||
Line.prototype.pointGrowSeries = function(index) {
|
||||
if (this.pointSizeForSeries(index) === 0) {
|
||||
return;
|
||||
}
|
||||
return Raphael.animation({
|
||||
r: this.pointSizeForSeries(index) + 3
|
||||
}, 25, 'linear');
|
||||
@@ -1409,7 +1561,9 @@ Licensed under the BSD-2-Clause License.
|
||||
barColors: ['#0b62a4', '#7a92a3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'],
|
||||
barOpacity: 1.0,
|
||||
barRadius: [0, 0, 0, 0],
|
||||
xLabelMargin: 50
|
||||
xLabelMargin: 50,
|
||||
horizontal: false,
|
||||
shown: true
|
||||
};
|
||||
|
||||
Bar.prototype.calc = function() {
|
||||
@@ -1426,7 +1580,7 @@ Licensed under the BSD-2-Clause License.
|
||||
_results = [];
|
||||
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
|
||||
row = _ref[idx];
|
||||
row._x = this.left + this.width * (idx + 0.5) / this.data.length;
|
||||
row._x = this.xStart + this.xSize * (idx + 0.5) / this.data.length;
|
||||
_results.push(row._y = (function() {
|
||||
var _j, _len1, _ref1, _results1;
|
||||
_ref1 = row.y;
|
||||
@@ -1454,28 +1608,54 @@ Licensed under the BSD-2-Clause License.
|
||||
};
|
||||
|
||||
Bar.prototype.drawXAxis = function() {
|
||||
var i, label, labelBox, margin, offset, prevAngleMargin, prevLabelMargin, row, textBox, ypos, _i, _ref, _results;
|
||||
ypos = this.bottom + (this.options.xAxisLabelTopPadding || this.options.padding / 2);
|
||||
var angle, basePos, i, label, labelBox, margin, maxSize, offset, prevAngleMargin, prevLabelMargin, row, size, startPos, textBox, _i, _ref, _results;
|
||||
if (!this.options.horizontal) {
|
||||
basePos = this.getXAxisLabelY();
|
||||
} else {
|
||||
basePos = this.getYAxisLabelX();
|
||||
}
|
||||
prevLabelMargin = null;
|
||||
prevAngleMargin = null;
|
||||
_results = [];
|
||||
for (i = _i = 0, _ref = this.data.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
|
||||
row = this.data[this.data.length - 1 - i];
|
||||
label = this.drawXAxisLabel(row._x, ypos, row.label);
|
||||
if (!this.options.horizontal) {
|
||||
label = this.drawXAxisLabel(row._x, basePos, row.label);
|
||||
} else {
|
||||
label = this.drawYAxisLabel(basePos, row._x - 0.5 * this.options.gridTextSize, row.label);
|
||||
}
|
||||
if (!this.options.horizontal) {
|
||||
angle = this.options.xLabelAngle;
|
||||
} else {
|
||||
angle = 0;
|
||||
}
|
||||
textBox = label.getBBox();
|
||||
label.transform("r" + (-this.options.xLabelAngle));
|
||||
label.transform("r" + (-angle));
|
||||
labelBox = label.getBBox();
|
||||
label.transform("t0," + (labelBox.height / 2) + "...");
|
||||
if (this.options.xLabelAngle !== 0) {
|
||||
offset = -0.5 * textBox.width * Math.cos(this.options.xLabelAngle * Math.PI / 180.0);
|
||||
if (angle !== 0) {
|
||||
offset = -0.5 * textBox.width * Math.cos(angle * Math.PI / 180.0);
|
||||
label.transform("t" + offset + ",0...");
|
||||
}
|
||||
if (((prevLabelMargin == null) || prevLabelMargin >= labelBox.x + labelBox.width || (prevAngleMargin != null) && prevAngleMargin >= labelBox.x) && labelBox.x >= 0 && (labelBox.x + labelBox.width) < this.el.width()) {
|
||||
if (this.options.xLabelAngle !== 0) {
|
||||
margin = 1.25 * this.options.gridTextSize / Math.sin(this.options.xLabelAngle * Math.PI / 180.0);
|
||||
prevAngleMargin = labelBox.x - margin;
|
||||
if (!this.options.horizontal) {
|
||||
startPos = labelBox.x;
|
||||
size = labelBox.width;
|
||||
maxSize = this.el.width();
|
||||
} else {
|
||||
startPos = labelBox.y;
|
||||
size = labelBox.height;
|
||||
maxSize = this.el.height();
|
||||
}
|
||||
if (((prevLabelMargin == null) || prevLabelMargin >= startPos + size || (prevAngleMargin != null) && prevAngleMargin >= startPos) && startPos >= 0 && (startPos + size) < maxSize) {
|
||||
if (angle !== 0) {
|
||||
margin = 1.25 * this.options.gridTextSize / Math.sin(angle * Math.PI / 180.0);
|
||||
prevAngleMargin = startPos - margin;
|
||||
}
|
||||
if (!this.options.horizontal) {
|
||||
_results.push(prevLabelMargin = startPos - this.options.xLabelMargin);
|
||||
} else {
|
||||
_results.push(prevLabelMargin = startPos);
|
||||
}
|
||||
_results.push(prevLabelMargin = labelBox.x - this.options.xLabelMargin);
|
||||
} else {
|
||||
_results.push(label.remove());
|
||||
}
|
||||
@@ -1483,10 +1663,23 @@ Licensed under the BSD-2-Clause License.
|
||||
return _results;
|
||||
};
|
||||
|
||||
Bar.prototype.getXAxisLabelY = function() {
|
||||
return this.bottom + (this.options.xAxisLabelTopPadding || this.options.padding / 2);
|
||||
};
|
||||
|
||||
Bar.prototype.drawSeries = function() {
|
||||
var barWidth, bottom, groupWidth, idx, lastTop, left, leftPadding, numBars, row, sidx, size, spaceLeft, top, ypos, zeroPos;
|
||||
groupWidth = this.width / this.options.data.length;
|
||||
numBars = this.options.stacked ? 1 : this.options.ykeys.length;
|
||||
var barWidth, bottom, groupWidth, i, idx, lastTop, left, leftPadding, numBars, row, sidx, size, spaceLeft, top, ypos, zeroPos, _i, _ref;
|
||||
groupWidth = this.xSize / this.options.data.length;
|
||||
if (this.options.stacked) {
|
||||
numBars = 1;
|
||||
} else {
|
||||
numBars = 0;
|
||||
for (i = _i = 0, _ref = this.options.ykeys.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
|
||||
if (this.hasToShow(i)) {
|
||||
numBars += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
barWidth = (groupWidth * this.options.barSizeRatio - this.options.barGap * (numBars - 1)) / numBars;
|
||||
if (this.options.barSize) {
|
||||
barWidth = Math.min(barWidth, this.options.barSize);
|
||||
@@ -1495,18 +1688,21 @@ Licensed under the BSD-2-Clause License.
|
||||
leftPadding = spaceLeft / 2;
|
||||
zeroPos = this.ymin <= 0 && this.ymax >= 0 ? this.transY(0) : null;
|
||||
return this.bars = (function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = this.data;
|
||||
var _j, _len, _ref1, _results;
|
||||
_ref1 = this.data;
|
||||
_results = [];
|
||||
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
|
||||
row = _ref[idx];
|
||||
for (idx = _j = 0, _len = _ref1.length; _j < _len; idx = ++_j) {
|
||||
row = _ref1[idx];
|
||||
lastTop = 0;
|
||||
_results.push((function() {
|
||||
var _j, _len1, _ref1, _results1;
|
||||
_ref1 = row._y;
|
||||
var _k, _len1, _ref2, _results1;
|
||||
_ref2 = row._y;
|
||||
_results1 = [];
|
||||
for (sidx = _j = 0, _len1 = _ref1.length; _j < _len1; sidx = ++_j) {
|
||||
ypos = _ref1[sidx];
|
||||
for (sidx = _k = 0, _len1 = _ref2.length; _k < _len1; sidx = ++_k) {
|
||||
ypos = _ref2[sidx];
|
||||
if (!this.hasToShow(sidx)) {
|
||||
continue;
|
||||
}
|
||||
if (ypos !== null) {
|
||||
if (zeroPos) {
|
||||
top = Math.min(ypos, zeroPos);
|
||||
@@ -1515,19 +1711,28 @@ Licensed under the BSD-2-Clause License.
|
||||
top = ypos;
|
||||
bottom = this.bottom;
|
||||
}
|
||||
left = this.left + idx * groupWidth + leftPadding;
|
||||
left = this.xStart + idx * groupWidth + leftPadding;
|
||||
if (!this.options.stacked) {
|
||||
left += sidx * (barWidth + this.options.barGap);
|
||||
}
|
||||
size = bottom - top;
|
||||
if (this.options.verticalGridCondition && this.options.verticalGridCondition(row.x)) {
|
||||
this.drawBar(this.left + idx * groupWidth, this.top, groupWidth, Math.abs(this.top - this.bottom), this.options.verticalGridColor, this.options.verticalGridOpacity, this.options.barRadius);
|
||||
if (!this.options.horizontal) {
|
||||
this.drawBar(this.xStart + idx * groupWidth, this.yEnd, groupWidth, this.ySize, this.options.verticalGridColor, this.options.verticalGridOpacity, this.options.barRadius);
|
||||
} else {
|
||||
this.drawBar(this.yStart, this.xStart + idx * groupWidth, this.ySize, groupWidth, this.options.verticalGridColor, this.options.verticalGridOpacity, this.options.barRadius);
|
||||
}
|
||||
}
|
||||
if (this.options.stacked) {
|
||||
top -= lastTop;
|
||||
}
|
||||
this.drawBar(left, top, barWidth, size, this.colorFor(row, sidx, 'bar'), this.options.barOpacity, this.options.barRadius);
|
||||
_results1.push(lastTop += size);
|
||||
if (!this.options.horizontal) {
|
||||
this.drawBar(left, top, barWidth, size, this.colorFor(row, sidx, 'bar'), this.options.barOpacity, this.options.barRadius);
|
||||
_results1.push(lastTop += size);
|
||||
} else {
|
||||
this.drawBar(top, left, size, barWidth, this.colorFor(row, sidx, 'bar'), this.options.barOpacity, this.options.barRadius);
|
||||
_results1.push(lastTop -= size);
|
||||
}
|
||||
} else {
|
||||
_results1.push(null);
|
||||
}
|
||||
@@ -1558,23 +1763,29 @@ Licensed under the BSD-2-Clause License.
|
||||
}
|
||||
};
|
||||
|
||||
Bar.prototype.hitTest = function(x) {
|
||||
Bar.prototype.hitTest = function(x, y) {
|
||||
var pos;
|
||||
if (this.data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
x = Math.max(Math.min(x, this.right), this.left);
|
||||
return Math.min(this.data.length - 1, Math.floor((x - this.left) / (this.width / this.data.length)));
|
||||
if (!this.options.horizontal) {
|
||||
pos = x;
|
||||
} else {
|
||||
pos = y;
|
||||
}
|
||||
pos = Math.max(Math.min(pos, this.xEnd), this.xStart);
|
||||
return Math.min(this.data.length - 1, Math.floor((pos - this.xStart) / (this.xSize / this.data.length)));
|
||||
};
|
||||
|
||||
Bar.prototype.onGridClick = function(x, y) {
|
||||
var index;
|
||||
index = this.hitTest(x);
|
||||
index = this.hitTest(x, y);
|
||||
return this.fire('click', index, this.data[index].src, x, y);
|
||||
};
|
||||
|
||||
Bar.prototype.onHoverMove = function(x, y) {
|
||||
var index, _ref;
|
||||
index = this.hitTest(x);
|
||||
index = this.hitTest(x, y);
|
||||
return (_ref = this.hover).update.apply(_ref, this.hoverContentForRow(index));
|
||||
};
|
||||
|
||||
@@ -1587,17 +1798,27 @@ Licensed under the BSD-2-Clause License.
|
||||
Bar.prototype.hoverContentForRow = function(index) {
|
||||
var content, j, row, x, y, _i, _len, _ref;
|
||||
row = this.data[index];
|
||||
content = "<div class='morris-hover-row-label'>" + row.label + "</div>";
|
||||
content = $("<div class='morris-hover-row-label'>").text(row.label);
|
||||
content = content.prop('outerHTML');
|
||||
_ref = row.y;
|
||||
for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) {
|
||||
y = _ref[j];
|
||||
content += "<div class='morris-hover-point' style='color: " + (this.colorFor(row, j, 'label')) + "'>\n " + this.options.labels[j] + ":\n " + (this.yLabelFormat(y)) + "\n</div>";
|
||||
if (this.options.labels[j] === false) {
|
||||
continue;
|
||||
}
|
||||
content += "<div class='morris-hover-point' style='color: " + (this.colorFor(row, j, 'label')) + "'>\n " + this.options.labels[j] + ":\n " + (this.yLabelFormat(y, j)) + "\n</div>";
|
||||
}
|
||||
if (typeof this.options.hoverCallback === 'function') {
|
||||
content = this.options.hoverCallback(index, this.options, content, row.src);
|
||||
}
|
||||
x = this.left + (index + 0.5) * this.width / this.data.length;
|
||||
return [content, x];
|
||||
if (!this.options.horizontal) {
|
||||
x = this.left + (index + 0.5) * this.width / this.data.length;
|
||||
return [content, x];
|
||||
} else {
|
||||
x = this.left + 0.5 * this.width;
|
||||
y = this.top + (index + 0.5) * this.height / this.data.length;
|
||||
return [content, x, y, true];
|
||||
}
|
||||
};
|
||||
|
||||
Bar.prototype.drawXAxisLabel = function(xPos, yPos, text) {
|
||||
|
||||
@@ -26,4 +26,11 @@ $(document).ready(function () {
|
||||
hideDeselected(true);
|
||||
}
|
||||
);
|
||||
|
||||
function toggleblock() {
|
||||
$("#new-quota-group").closest('fieldset').toggle(!$("#id_has_variations").prop('checked'));
|
||||
}
|
||||
|
||||
$("#id_has_variations").change(toggleblock);
|
||||
toggleblock();
|
||||
});
|
||||
|
||||
@@ -22,11 +22,11 @@ $(function () {
|
||||
$("<li>").append(
|
||||
$("<a>").attr("href", res.url).append(
|
||||
$("<div>").append(
|
||||
$("<span>").addClass("event-name-full").append(res.name)
|
||||
$("<span>").addClass("event-name-full").append($("<div>").text(res.name).html())
|
||||
).append(
|
||||
$("<span>").addClass("event-organizer").append(
|
||||
$("<span>").addClass("fa fa-users fa-fw")
|
||||
).append(" ").append(res.organizer)
|
||||
).append(" ").append($("<div>").text(res.organizer).html())
|
||||
).append(
|
||||
$("<span>").addClass("event-daterange").append(
|
||||
$("<span>").addClass("fa fa-calendar fa-fw")
|
||||
|
||||
@@ -43,3 +43,4 @@ vobject==0.9.*
|
||||
pycountry
|
||||
django-countries
|
||||
pyuca # for better sorting of country names in django-countries
|
||||
defusedcsv>=1.0.1
|
||||
|
||||
@@ -97,14 +97,15 @@ setup(
|
||||
'pycparser==2.13',
|
||||
'django-redis==4.7.*',
|
||||
'redis==2.10.5',
|
||||
'stripe==1.22.*',
|
||||
'stripe==1.62.*',
|
||||
'chardet<3.1.0,>=3.0.2',
|
||||
'mt-940==4.7',
|
||||
'django-i18nfield>=1.0.1',
|
||||
'vobject==0.9.*',
|
||||
'pycountry',
|
||||
'django-countries',
|
||||
'pyuca'
|
||||
'pyuca',
|
||||
'defusedcsv'
|
||||
],
|
||||
extras_require={
|
||||
'dev': [
|
||||
|
||||
@@ -202,6 +202,7 @@ event_permission_urls = [
|
||||
("can_change_orders", "orders/FOO/change", 200),
|
||||
("can_change_orders", "orders/FOO/comment", 405),
|
||||
("can_change_orders", "orders/FOO/locale", 200),
|
||||
("can_view_orders", "orders/FOO/answer/5/", 404),
|
||||
("can_change_vouchers", "vouchers/add", 200),
|
||||
("can_change_orders", "requiredactions/", 200),
|
||||
("can_change_vouchers", "vouchers/bulk_add", 200),
|
||||
|
||||
@@ -68,6 +68,18 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
|
||||
|
||||
|
||||
class ItemDisplayTest(EventTestMixin, SoupTest):
|
||||
def test_link_rewrite(self):
|
||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
|
||||
description="http://example.org [Sample](http://example.net)")
|
||||
q.items.add(item)
|
||||
html = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)).rendered_content
|
||||
|
||||
self.assertNotIn('href="http://example.org', html)
|
||||
self.assertNotIn('href="http://example.net', html)
|
||||
self.assertIn('href="/redirect/?url=http%3A//example.org%3A', html)
|
||||
self.assertIn('href="/redirect/?url=http%3A//example.net%3A', html)
|
||||
|
||||
def test_not_active(self):
|
||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=False)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import datetime
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
@@ -418,3 +420,39 @@ class OrdersTest(TestCase):
|
||||
assert self.order.payment_fee == Decimal('12.00')
|
||||
assert self.order.total == Decimal('23.00') + self.order.payment_fee
|
||||
assert self.order.invoices.count() == 3
|
||||
|
||||
def test_answer_download_token(self):
|
||||
q = self.event.questions.create(question="Foo", type="F")
|
||||
q.items.add(self.ticket)
|
||||
a = self.ticket_pos.answers.create(question=q, answer="file")
|
||||
val = SimpleUploadedFile("testfile.txt", b"file_content")
|
||||
a.file.save("testfile.txt", val)
|
||||
a.save()
|
||||
|
||||
self.event.settings.set('ticket_download', True)
|
||||
del self.event.settings['ticket_download_date']
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/answer/%s/' % (self.orga.slug, self.event.slug, self.order.code,
|
||||
self.order.secret, a.pk)
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
match = re.search(r"\?token=([^'\"&]+)", response.rendered_content)
|
||||
assert match
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/answer/%s/?token=%s' % (self.orga.slug, self.event.slug, self.order.code,
|
||||
self.order.secret, a.pk, match.group(1))
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
client2 = self.client_class()
|
||||
response = client2.get(
|
||||
'/%s/%s/order/%s/%s/answer/%s/?token=%s' % (self.orga.slug, self.event.slug, self.order.code,
|
||||
self.order.secret, a.pk, match.group(1))
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user