Compare commits

...

15 Commits

Author SHA1 Message Date
Raphael Michel
33382681f3 Prepare release 2017-08-21 14:31:18 +02:00
Raphael Michel
340f7aaba4 [SECURITY] Rewrite all links in rich texts 2017-08-21 14:30:58 +02:00
Raphael Michel
f96e525cb5 [SECURITY] Fix XSS vulnerability in typeahead.js 2017-08-21 14:30:58 +02:00
Raphael Michel
a87fe32a77 [SECURITY] Fix XSS vulnerability in Lightbox caption 2017-08-21 14:30:58 +02:00
Raphael Michel
55feddc239 [SECURITY] Support custom media URLs in CSP middleware 2017-08-21 14:30:58 +02:00
Raphael Michel
28e146f2ad [SECURITY] Add warning for download of unsafe files 2017-08-21 14:30:58 +02:00
Raphael Michel
e7d6265b22 [SECURITY] Tokens for downloading answer attachments 2017-08-21 14:30:33 +02:00
Raphael Michel
2603d321bb [SECURITY] Do not allow SVG files for logos 2017-08-21 14:27:55 +02:00
Raphael Michel
88f35cb361 [SECURITY] Fix XSS injection vulnerabilities in question answers, event, quota and product names 2017-08-21 14:27:55 +02:00
Raphael Michel
aed6515971 [SECURITY] Update to morris.js master to fix a XSS vulnerability 2017-08-21 14:27:55 +02:00
Raphael Michel
a0be1b8389 [SECURITY] Use defusedcsv for exports 2017-08-21 14:27:55 +02:00
Raphael Michel
17eb0a3817 Bump release 2017-08-01 21:07:10 +02:00
Nicole Klünder
a87cb75ed5 fix wrong stripe version in setup.py (#588) 2017-08-01 21:06:06 +02:00
Raphael Michel
f70eb95346 Hide quota options when creating a product with variations 2017-08-01 21:05:38 +02:00
Raphael Michel
9abdc6a3cc Fix reversal bug 2017-08-01 21:05:32 +02:00
37 changed files with 580 additions and 135 deletions

View File

@@ -1 +1 @@
__version__ = "1.6.0" __version__ = "1.6.2"

View File

@@ -1,9 +1,9 @@
import csv
import io import io
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
import pytz import pytz
from defusedcsv import csv
from django import forms from django import forms
from django.db.models import Sum from django.db.models import Sum
from django.dispatch import receiver from django.dispatch import receiver

View File

@@ -7,6 +7,7 @@ from django.core.urlresolvers import get_script_prefix
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils import timezone, translation from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers 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.deprecation import MiddlewareMixin
from django.utils.translation import LANGUAGE_SESSION_KEY from django.utils.translation import LANGUAGE_SESSION_KEY
from django.utils.translation.trans_real import ( from django.utils.translation.trans_real import (
@@ -165,6 +166,9 @@ class SecurityMiddleware(MiddlewareMixin):
'/api/v1/docs/', '/api/v1/docs/',
) )
def process_request(self, request):
request.csp_nonce = get_random_string(length=32)
def process_response(self, request, resp): def process_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400: if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error # 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 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'], 'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'child-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"], '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-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 # 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 # single-sign-on this can be nearly anything so we cannot really restrict
@@ -193,6 +197,9 @@ class SecurityMiddleware(MiddlewareMixin):
staticdomain = "'self'" staticdomain = "'self'"
dynamicdomain = "'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'): if settings.STATIC_URL.startswith('http'):
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)] staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
if settings.SITE_URL.startswith('http'): if settings.SITE_URL.startswith('http'):
@@ -212,5 +219,6 @@ class SecurityMiddleware(MiddlewareMixin):
dynamicdomain += " " + domain dynamicdomain += " " + domain
if request.path not in self.CSP_EXEMPT: 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 return resp

View File

@@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.models.event import SubEvent from pretix.base.models.event import SubEvent
@@ -68,7 +69,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug, 'organizer': self.event.organizer.slug,
'code': co.code 'code': co.code
}), }),
'val': co.code, 'val': escape(co.code),
} }
elif isinstance(co, Voucher): elif isinstance(co, Voucher):
a_text = _('Voucher {val}') a_text = _('Voucher {val}')
@@ -78,7 +79,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug, 'organizer': self.event.organizer.slug,
'voucher': co.id 'voucher': co.id
}), }),
'val': co.code[:6], 'val': escape(co.code[:6]),
} }
elif isinstance(co, Item): elif isinstance(co, Item):
a_text = _('Product {val}') a_text = _('Product {val}')
@@ -88,7 +89,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug, 'organizer': self.event.organizer.slug,
'item': co.id 'item': co.id
}), }),
'val': co.name, 'val': escape(co.name),
} }
elif isinstance(co, SubEvent): elif isinstance(co, SubEvent):
a_text = pgettext_lazy('subevent', 'Date {val}') a_text = pgettext_lazy('subevent', 'Date {val}')
@@ -98,7 +99,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug, 'organizer': self.event.organizer.slug,
'subevent': co.id 'subevent': co.id
}), }),
'val': str(co) 'val': escape(str(co))
} }
elif isinstance(co, Quota): elif isinstance(co, Quota):
a_text = _('Quota {val}') a_text = _('Quota {val}')
@@ -108,7 +109,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug, 'organizer': self.event.organizer.slug,
'quota': co.id 'quota': co.id
}), }),
'val': co.name, 'val': escape(co.name),
} }
elif isinstance(co, ItemCategory): elif isinstance(co, ItemCategory):
a_text = _('Category {val}') a_text = _('Category {val}')
@@ -118,7 +119,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug, 'organizer': self.event.organizer.slug,
'category': co.id 'category': co.id
}), }),
'val': co.name, 'val': escape(co.name),
} }
elif isinstance(co, Question): elif isinstance(co, Question):
a_text = _('Question {val}') a_text = _('Question {val}')
@@ -128,7 +129,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug, 'organizer': self.event.organizer.slug,
'question': co.id 'question': co.id
}), }),
'val': co.question, 'val': escape(co.question),
} }
if a_text and a_map: if a_text and a_map:

View File

@@ -12,11 +12,10 @@ from django.db import models
from django.db.models import F, Sum from django.db.models import F, Sum
from django.db.models.signals import post_delete from django.db.models.signals import post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.encoding import escape_uri_path from django.utils.encoding import escape_uri_path
from django.utils.functional import cached_property 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.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import CountryField from django_countries.fields import CountryField
@@ -487,7 +486,19 @@ class QuestionAnswer(models.Model):
) )
@property @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 from pretix.multidomain.urlreverse import eventreverse
if self.file: if self.file:
@@ -502,12 +513,13 @@ class QuestionAnswer(models.Model):
'answer': self.pk, 'answer': self.pk,
}) })
return mark_safe("<a href='{}'>{}</a>".format( return url
url,
escape(self.file.name.split('.', 1)[-1])
))
return "" return ""
@property
def file_name(self):
return self.file.name.split('.', 1)[-1]
def __str__(self): def __str__(self):
if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True": if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True":
return str(_("Yes")) return str(_("Yes"))

View 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)

View File

@@ -1,6 +1,12 @@
import urllib.parse
import bleach import bleach
import markdown import markdown
from bleach import DEFAULT_CALLBACKS
from django import template 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 from django.utils.safestring import mark_safe
register = template.Library() 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 @register.filter
def rich_text(text: str, **kwargs): def rich_text(text: str, **kwargs):
""" """
@@ -58,5 +73,5 @@ def rich_text(text: str, **kwargs):
markdown.markdown(text), markdown.markdown(text),
tags=ALLOWED_TAGS, tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES, attributes=ALLOWED_ATTRIBUTES,
)) ), callbacks=DEFAULT_CALLBACKS + [safelink_callback])
return mark_safe(body_md) return mark_safe(body_md)

View File

@@ -1,5 +1,7 @@
from django import template from django import template
from pretix.helpers.safedownload import get_token
from ..views.redirect import safelink as sl from ..views.redirect import safelink as sl
register = template.Library() register = template.Library()
@@ -8,3 +10,8 @@ register = template.Library()
@register.simple_tag @register.simple_tag
def safelink(url): def safelink(url):
return sl(url) return sl(url)
@register.simple_tag
def answer_token(request, answer):
return get_token(request, answer)

View File

@@ -690,7 +690,7 @@ class DisplaySettingsForm(SettingsForm):
) )
logo_image = ExtFileField( logo_image = ExtFileField(
label=_('Logo image'), label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"), ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False, required=False,
help_text=_('If you provide a logo image, we will by default not show your events name and date ' 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.') 'in the page header. We will show your logo with a maximal height of 120 pixels.')

View File

@@ -195,7 +195,7 @@ class ItemCreateForm(I18nModelForm):
instance = super().save(*args, **kwargs) 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: 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 = self.cleaned_data.get('quota_add_existing')
quota.items.add(self.instance) quota.items.add(self.instance)

View File

@@ -128,7 +128,7 @@ class OrganizerSettingsForm(SettingsForm):
organizer_logo_image = ExtFileField( organizer_logo_image = ExtFileField(
label=_('Logo image'), label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"), ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False, required=False,
help_text=_('If you provide a logo image, we will by default not show your organization name ' 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.') 'in the page header. We will show your logo with a maximal height of 120 pixels.')

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/items/base.html" %} {% extends "pretixcontrol/items/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load escapejson %}
{% load formset_tags %} {% load formset_tags %}
{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %} {% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
{% block inside %} {% block inside %}
@@ -58,7 +59,7 @@
<div class="chart" id="question_chart" data-type="{{ question.type }}"> <div class="chart" id="question_chart" data-type="{{ question.type }}">
</div> </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>
<div class="col-md-5 col-xs-12"> <div class="col-md-5 col-xs-12">
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/items/base.html" %} {% extends "pretixcontrol/items/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load escapejson %}
{% load eventsignal %} {% load eventsignal %}
{% block title %}{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}{% endblock %} {% block title %}{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}{% endblock %}
{% block inside %} {% block inside %}
@@ -25,7 +26,7 @@
<div class="chart" id="quota_chart"> <div class="chart" id="quota_chart">
</div> </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>
<div class="col-md-5 col-xs-12"> <div class="col-md-5 col-xs-12">
<legend>{% trans "Availability calculation" %}</legend> <legend>{% trans "Availability calculation" %}</legend>

View File

@@ -2,6 +2,7 @@
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load eventurl %} {% load eventurl %}
{% load safelink %}
{% block title %} {% block title %}
{% blocktrans trimmed with code=order.code %} {% blocktrans trimmed with code=order.code %}
Order details: {{ code }} Order details: {{ code }}
@@ -210,7 +211,14 @@
<dd> <dd>
{% if q.answer %} {% if q.answer %}
{% if q.answer.file %} {% 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 %} {% else %}
{{ q.answer|linebreaksbr }} {{ q.answer|linebreaksbr }}
{% endif %} {% endif %}

View File

@@ -128,6 +128,9 @@ urlpatterns = [
name='event.order.regeninvoice'), name='event.order.regeninvoice'),
url(r'^orders/(?P<code>[0-9A-Z]+)/invoices/(?P<id>\d+)/reissue$', orders.OrderInvoiceReissue.as_view(), url(r'^orders/(?P<code>[0-9A-Z]+)/invoices/(?P<id>\d+)/reissue$', orders.OrderInvoiceReissue.as_view(),
name='event.order.reissueinvoice'), 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(), url(r'^orders/(?P<code>[0-9A-Z]+)/extend$', orders.OrderExtend.as_view(),
name='event.order.extend'), name='event.order.extend'),
url(r'^orders/(?P<code>[0-9A-Z]+)/contact$', orders.OrderContactChange.as_view(), url(r'^orders/(?P<code>[0-9A-Z]+)/contact$', orders.OrderContactChange.as_view(),

View File

@@ -8,6 +8,7 @@ from django.shortcuts import render
from django.template.loader import get_template from django.template.loader import get_template
from django.utils import formats from django.utils import formats
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import ( from pretix.base.models import (
@@ -136,7 +137,7 @@ def quota_widgets(sender, **kwargs):
status, left = q.availability() status, left = q.availability()
widgets.append({ widgets.append({
'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e', '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', 'display_size': 'small',
'priority': 50, 'priority': 50,
'url': reverse('control:event.items.quotas.show', kwargs={ 'url': reverse('control:event.items.quotas.show', kwargs={
@@ -258,7 +259,8 @@ def user_event_widgets(**kwargs):
for event in events: for event in events:
widgets.append({ widgets.append({
'content': '<div class="event">{event}<span class="from">{df}</span><span class="to">{dt}</span></div>'.format( '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 '' dt=date_format(event.date_to, 'SHORT_DATE_FORMAT') if event.date_to else ''
), ),
'display_size': 'small', 'display_size': 'small',

View File

@@ -1,3 +1,5 @@
import mimetypes
import os
from datetime import timedelta from datetime import timedelta
import pytz import pytz
@@ -6,7 +8,7 @@ from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Count from django.db.models import Count
from django.http import FileResponse, Http404, HttpResponseNotAllowed 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.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
@@ -19,7 +21,8 @@ from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedFile, CachedTicket, Invoice, InvoiceAddress, Item, ItemVariation, 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.models.event import SubEvent
from pretix.base.services.export import export from pretix.base.services.export import export
@@ -41,6 +44,7 @@ from pretix.control.forms.orders import (
OrderMailForm, OrderPositionAddForm, OrderPositionChangeForm, OrderMailForm, OrderPositionAddForm, OrderPositionChangeForm,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
@@ -739,6 +743,29 @@ class OrderEmailHistory(EventPermissionRequiredMixin, OrderViewMixin, ListView):
return qs 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): class OverView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/orders/overview.html' template_name = 'pretixcontrol/orders/overview.html'
permission = 'can_view_orders' permission = 'can_view_orders'

View File

@@ -1,6 +1,6 @@
import csv
import io import io
from defusedcsv import csv
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import resolve, reverse from django.core.urlresolvers import resolve, reverse

View 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))

View 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

View File

@@ -4,7 +4,7 @@
{% block inner %} {% block inner %}
<h2>{% trans "Import result" %}</h2> <h2>{% trans "Import result" %}</h2>
{% if job.state == "running" or job.state == "pending" %} {% 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> <p>
<span class="fa big-grey-icon fa-cog fa big-rotating-icon"></span> <span class="fa big-grey-icon fa-cog fa big-rotating-icon"></span>
</p> </p>

View File

@@ -1,7 +1,7 @@
import csv
import io import io
from collections import OrderedDict from collections import OrderedDict
from defusedcsv import csv
from django import forms from django import forms
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils.translation import ( from django.utils.translation import (

View File

@@ -2,6 +2,7 @@
{% load i18n %} {% load i18n %}
{% load compress %} {% load compress %}
{% load staticfiles %} {% load staticfiles %}
{% load escapejson %}
{% block title %}{% trans "Statistics" %}{% endblock %} {% block title %}{% trans "Statistics" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Statistics" %}</h1> <h1>{% trans "Statistics" %}</h1>
@@ -30,9 +31,9 @@
<div id="obp_chart" class="chart"></div> <div id="obp_chart" class="chart"></div>
</div> </div>
</div> </div>
<script type="application/json" id="obd-data">{{ obd_data|safe }}</script> <script type="application/json" id="obd-data">{{ obd_data|escapejson }}</script>
<script type="application/json" id="rev-data">{{ rev_data|safe }}</script> <script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script>
<script type="application/json" id="obp-data">{{ obp_data|safe }}</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/text" id="currency">{{ request.event.currency }}</script>
<script type="application/javascript" src="{% static "pretixplugins/statistics/statistics.js" %}"></script> <script type="application/javascript" src="{% static "pretixplugins/statistics/statistics.js" %}"></script>
{% else %} {% else %}

View File

@@ -1,16 +1,19 @@
import hashlib import hashlib
import logging import logging
import os import os
from urllib.parse import urljoin, urlsplit
import django_libsass import django_libsass
import sass import sass
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import default_storage 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.models import Event
from pretix.base.services.async import ProfiledTask from pretix.base.services.async import ProfiledTask
from pretix.celery_app import app from pretix.celery_app import app
from pretix.multidomain.urlreverse import get_domain
logger = logging.getLogger('pretix.presale.style') 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('$brand-primary: {};'.format(event.settings.get('primary_color')))
sassrules.append('@import "main.scss";') 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( css = sass.compile(
string="\n".join(sassrules), string="\n".join(sassrules),
include_paths=[sassdir], output_style='compressed', include_paths=[sassdir], output_style='compressed',
custom_functions=django_libsass.CUSTOM_FUNCTIONS custom_functions=cf
) )
checksum = hashlib.sha1(css.encode('utf-8')).hexdigest() checksum = hashlib.sha1(css.encode('utf-8')).hexdigest()
fname = '{}/{}/presale.{}.css'.format( fname = '{}/{}/presale.{}.css'.format(

View File

@@ -11,7 +11,7 @@
<link rel="stylesheet" type="text/x-scss" href="{% static "lightbox/css/lightbox.scss" %}" /> <link rel="stylesheet" type="text/x-scss" href="{% static "lightbox/css/lightbox.scss" %}" />
{% endcompress %} {% endcompress %}
{% if css_file %} {% 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 %} {% else %}
{% compress css %} {% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/> <link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/>

View File

@@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load eventurl %} {% load eventurl %}
{% load safelink %}
{% for line in cart.positions %} {% 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="row cart-row {% if download and line.item.admission|default:event.settings.ticket_download_nonadm %}has-downloads{% endif %}">
<div class="product"> <div class="product">
@@ -33,7 +34,10 @@
<dd> <dd>
{% if q.answer %} {% if q.answer %}
{% if q.answer.file %} {% 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 %} {% else %}
{{ q.answer|linebreaksbr }} {{ q.answer|linebreaksbr }}
{% endif %} {% endif %}

View File

@@ -170,7 +170,8 @@
<div class="col-md-8 col-xs-12"> <div class="col-md-8 col-xs-12">
{% if item.picture %} {% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture" <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 }}"> data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumbnail_url:'productlist' }}" <img src="{{ item.picture|thumbnail_url:'productlist' }}"
alt="{{ item.name }}"/> alt="{{ item.name }}"/>
@@ -281,7 +282,8 @@
<div class="col-md-8 col-xs-12"> <div class="col-md-8 col-xs-12">
{% if item.picture %} {% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture" <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 }}"> data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumbnail_url:'productlist' }}" <img src="{{ item.picture|thumbnail_url:'productlist' }}"
alt="{{ item.name }}"/> alt="{{ item.name }}"/>

View File

@@ -38,7 +38,8 @@
<div class="col-md-8 col-xs-12"> <div class="col-md-8 col-xs-12">
{% if item.picture %} {% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture" <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 }}"> data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumbnail_url:'productlist' }}" <img src="{{ item.picture|thumbnail_url:'productlist' }}"
alt="{{ item.name }}"/> alt="{{ item.name }}"/>
@@ -125,7 +126,8 @@
<div class="col-md-8 col-xs-12"> <div class="col-md-8 col-xs-12">
{% if item.picture %} {% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture" <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 }}"> data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumbnail_url:'productlist' }}" <img src="{{ item.picture|thumbnail_url:'productlist' }}"
alt="{{ item.name }}"/> alt="{{ item.name }}"/>

View File

@@ -22,6 +22,7 @@ from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position, get_cachedticket_for_order, get_cachedticket_for_position,
) )
from pretix.base.signals import register_ticket_outputs from pretix.base.signals import register_ticket_outputs
from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.checkout import InvoiceAddressForm from pretix.presale.forms.checkout import InvoiceAddressForm
from pretix.presale.views import CartMixin, EventViewMixin from pretix.presale.views import CartMixin, EventViewMixin
@@ -502,11 +503,15 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
class AnswerDownload(EventViewMixin, OrderDetailMixin, View): class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
answid = kwargs.get('answer') answid = kwargs.get('answer')
token = request.GET.get('token', '')
answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid) answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid)
if not answer.file: 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 = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format( resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(), self.order.code, self.request.event.slug.upper(), self.order.code,

View File

@@ -1,5 +1,5 @@
/* @license /* @license
morris.js v0.5.0 morris.js v0.5.1
Copyright 2014 Olly Smith All rights reserved. Copyright 2014 Olly Smith All rights reserved.
Licensed under the BSD-2-Clause License. Licensed under the BSD-2-Clause License.
*/ */
@@ -74,6 +74,7 @@ Licensed under the BSD-2-Clause License.
__extends(Grid, _super); __extends(Grid, _super);
function Grid(options) { function Grid(options) {
this.hasToShow = __bind(this.hasToShow, this);
this.resizeHandler = __bind(this.resizeHandler, this); this.resizeHandler = __bind(this.resizeHandler, this);
var _this = this; var _this = this;
if (typeof options.element === 'string') { if (typeof options.element === 'string') {
@@ -197,7 +198,7 @@ Licensed under the BSD-2-Clause License.
}; };
Grid.prototype.setData = function(data, redraw) { 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) { if (redraw == null) {
redraw = true; redraw = true;
} }
@@ -254,7 +255,7 @@ Licensed under the BSD-2-Clause License.
if ((yval != null) && typeof yval !== 'number') { if ((yval != null) && typeof yval !== 'number') {
yval = null; yval = null;
} }
if (yval != null) { if ((yval != null) && this.hasToShow(idx)) {
if (this.cumulative) { if (this.cumulative) {
total += yval; total += yval;
} else { } else {
@@ -288,21 +289,24 @@ Licensed under the BSD-2-Clause License.
this.events = []; this.events = [];
if (this.options.events.length > 0) { if (this.options.events.length > 0) {
if (this.options.parseTime) { if (this.options.parseTime) {
this.events = (function() { _ref = this.options.events;
var _i, _len, _ref, _results; for (_i = 0, _len = _ref.length; _i < _len; _i++) {
_ref = this.options.events; e = _ref[_i];
_results = []; if (e instanceof Array) {
for (_i = 0, _len = _ref.length; _i < _len; _i++) { from = e[0], to = e[1];
e = _ref[_i]; this.events.push([Morris.parseDate(from), Morris.parseDate(to)]);
_results.push(Morris.parseDate(e)); } else {
this.events.push(Morris.parseDate(e));
} }
return _results; }
}).call(this);
} else { } else {
this.events = this.options.events; this.events = this.options.events;
} }
this.xmax = Math.max(this.xmax, Math.max.apply(Math, this.events)); flatEvents = $.map(this.events, function(e) {
this.xmin = Math.min(this.xmin, Math.min.apply(Math, this.events)); 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) { if (this.xmin === this.xmax) {
this.xmin -= 1; this.xmin -= 1;
@@ -316,7 +320,7 @@ Licensed under the BSD-2-Clause License.
} }
this.ymax += 1; 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) { 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.grid = this.autoGridLines(this.ymin, this.ymax, this.options.numLines);
this.ymin = Math.min(this.ymin, this.grid[0]); this.ymin = Math.min(this.ymin, this.grid[0]);
@@ -324,9 +328,9 @@ Licensed under the BSD-2-Clause License.
} else { } else {
step = (this.ymax - this.ymin) / (this.options.numLines - 1); step = (this.ymax - this.ymin) / (this.options.numLines - 1);
this.grid = (function() { this.grid = (function() {
var _i, _ref1, _ref2, _results; var _j, _ref2, _ref3, _results;
_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); _results.push(y);
} }
return _results; return _results;
@@ -405,7 +409,7 @@ Licensed under the BSD-2-Clause License.
}; };
Grid.prototype._calc = function() { 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(); w = this.el.width();
h = this.el.height(); h = this.el.height();
if (this.elementWidth !== w || this.elementHeight !== h || this.dirty) { if (this.elementWidth !== w || this.elementHeight !== h || this.dirty) {
@@ -427,23 +431,53 @@ Licensed under the BSD-2-Clause License.
} }
return _results; return _results;
}).call(this); }).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 ((_ref1 = this.options.axes) === true || _ref1 === 'both' || _ref1 === 'x') {
if (!this.options.horizontal) {
angle = -this.options.xLabelAngle;
} else {
angle = -90;
}
bottomOffsets = (function() { bottomOffsets = (function() {
var _i, _ref2, _results; var _i, _ref2, _results;
_results = []; _results = [];
for (i = _i = 0, _ref2 = this.data.length; 0 <= _ref2 ? _i < _ref2 : _i > _ref2; i = 0 <= _ref2 ? ++_i : --_i) { 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; return _results;
}).call(this); }).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.width = Math.max(1, this.right - this.left);
this.height = Math.max(1, this.bottom - this.top); this.height = Math.max(1, this.bottom - this.top);
this.dx = this.width / (this.xmax - this.xmin); if (!this.options.horizontal) {
this.dy = this.height / (this.ymax - this.ymin); 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) { if (this.calc) {
return this.calc(); return this.calc();
} }
@@ -451,14 +485,18 @@ Licensed under the BSD-2-Clause License.
}; };
Grid.prototype.transY = function(y) { 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) { Grid.prototype.transX = function(x) {
if (this.data.length === 1) { if (this.data.length === 1) {
return (this.left + this.right) / 2; return (this.xStart + this.xEnd) / 2;
} else { } 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) { 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') { if (typeof this.options.yLabelFormat === 'function') {
return this.options.yLabelFormat(label); return this.options.yLabelFormat(label, i);
} else { } else {
return "" + this.options.preUnits + (Morris.commas(label)) + this.options.postUnits; 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() { 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')) { if (this.options.grid === false && ((_ref = this.options.axes) !== true && _ref !== 'both' && _ref !== 'y')) {
return; return;
} }
if (!this.options.horizontal) {
basePos = this.getYAxisLabelX();
} else {
basePos = this.getXAxisLabelY();
}
_ref1 = this.grid; _ref1 = this.grid;
_results = []; _results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) { for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
lineY = _ref1[_i]; lineY = _ref1[_i];
y = this.transY(lineY); pos = this.transY(lineY);
if ((_ref2 = this.options.axes) === true || _ref2 === 'both' || _ref2 === 'y') { 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) { 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 { } else {
_results.push(void 0); _results.push(void 0);
} }
@@ -543,11 +599,42 @@ Licensed under the BSD-2-Clause License.
}; };
Grid.prototype.drawGoal = function(goal, color) { 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) { 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) { Grid.prototype.drawYAxisLabel = function(xPos, yPos, text) {
@@ -586,6 +673,10 @@ Licensed under the BSD-2-Clause License.
return this.redraw(); return this.redraw();
}; };
Grid.prototype.hasToShow = function(i) {
return this.options.shown === true || this.options.shown[i] === true;
};
return Grid; return Grid;
})(Morris.EventEmitter); })(Morris.EventEmitter);
@@ -662,13 +753,13 @@ Licensed under the BSD-2-Clause License.
this.options.parent.append(this.el); this.options.parent.append(this.el);
} }
Hover.prototype.update = function(html, x, y) { Hover.prototype.update = function(html, x, y, centre_y) {
if (!html) { if (!html) {
return this.hide(); return this.hide();
} else { } else {
this.html(html); this.html(html);
this.show(); 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); 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; var hoverHeight, hoverWidth, left, parentHeight, parentWidth, top;
parentWidth = this.options.parent.innerWidth(); parentWidth = this.options.parent.innerWidth();
parentHeight = this.options.parent.innerHeight(); parentHeight = this.options.parent.innerHeight();
@@ -684,11 +775,18 @@ Licensed under the BSD-2-Clause License.
hoverHeight = this.el.outerHeight(); hoverHeight = this.el.outerHeight();
left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth); left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth);
if (y != null) { if (y != null) {
top = y - hoverHeight - 10; if (centre_y === true) {
if (top < 0) { top = y - hoverHeight / 2;
top = y + 10; if (top < 0) {
if (top + hoverHeight > parentHeight) { top = 0;
top = parentHeight / 2 - hoverHeight / 2; }
} else {
top = y - hoverHeight - 10;
if (top < 0) {
top = y + 10;
if (top + hoverHeight > parentHeight) {
top = parentHeight / 2 - hoverHeight / 2;
}
} }
} }
} else { } else {
@@ -745,10 +843,14 @@ Licensed under the BSD-2-Clause License.
pointStrokeColors: ['#ffffff'], pointStrokeColors: ['#ffffff'],
pointFillColors: [], pointFillColors: [],
smooth: true, smooth: true,
shown: true,
xLabels: 'auto', xLabels: 'auto',
xLabelFormat: null, xLabelFormat: null,
xLabelMargin: 24, xLabelMargin: 24,
hideHover: false hideHover: false,
trendLine: false,
trendLineWidth: 2,
trendLineColors: ['#689bc3', '#a2b3bf', '#64b764']
}; };
Line.prototype.calc = function() { Line.prototype.calc = function() {
@@ -840,11 +942,15 @@ Licensed under the BSD-2-Clause License.
Line.prototype.hoverContentForRow = function(index) { Line.prototype.hoverContentForRow = function(index) {
var content, j, row, y, _i, _len, _ref; var content, j, row, y, _i, _len, _ref;
row = this.data[index]; 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; _ref = row.y;
for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) { for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) {
y = _ref[j]; 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') { if (typeof this.options.hoverCallback === 'function') {
content = this.options.hoverCallback(index, this.options, content, row.src); 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; var i, _i, _j, _ref, _ref1, _results;
this.seriesPoints = []; this.seriesPoints = [];
for (i = _i = _ref = this.options.ykeys.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) { 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 = []; _results = [];
for (i = _j = _ref1 = this.options.ykeys.length - 1; _ref1 <= 0 ? _j <= 0 : _j >= 0; i = _ref1 <= 0 ? ++_j : --_j) { 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; 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) { Line.createPath = function(coords, smooth, bottom) {
var coord, g, grads, i, ix, lg, path, prevCoord, x1, x2, y1, y2, _i, _len; var coord, g, grads, i, ix, lg, path, prevCoord, x1, x2, y1, y2, _i, _len;
path = ""; path = "";
@@ -1078,8 +1225,10 @@ Licensed under the BSD-2-Clause License.
return this.options.lineColors.call(this, row, sidx, type); return this.options.lineColors.call(this, row, sidx, type);
} else if (type === 'point') { } else if (type === 'point') {
return this.options.pointFillColors[sidx % this.options.pointFillColors.length] || this.options.lineColors[sidx % this.options.lineColors.length]; 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]; 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) { Line.prototype.pointGrowSeries = function(index) {
if (this.pointSizeForSeries(index) === 0) {
return;
}
return Raphael.animation({ return Raphael.animation({
r: this.pointSizeForSeries(index) + 3 r: this.pointSizeForSeries(index) + 3
}, 25, 'linear'); }, 25, 'linear');
@@ -1409,7 +1561,9 @@ Licensed under the BSD-2-Clause License.
barColors: ['#0b62a4', '#7a92a3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'], barColors: ['#0b62a4', '#7a92a3', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'],
barOpacity: 1.0, barOpacity: 1.0,
barRadius: [0, 0, 0, 0], barRadius: [0, 0, 0, 0],
xLabelMargin: 50 xLabelMargin: 50,
horizontal: false,
shown: true
}; };
Bar.prototype.calc = function() { Bar.prototype.calc = function() {
@@ -1426,7 +1580,7 @@ Licensed under the BSD-2-Clause License.
_results = []; _results = [];
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) { for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
row = _ref[idx]; 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() { _results.push(row._y = (function() {
var _j, _len1, _ref1, _results1; var _j, _len1, _ref1, _results1;
_ref1 = row.y; _ref1 = row.y;
@@ -1454,28 +1608,54 @@ Licensed under the BSD-2-Clause License.
}; };
Bar.prototype.drawXAxis = function() { Bar.prototype.drawXAxis = function() {
var i, label, labelBox, margin, offset, prevAngleMargin, prevLabelMargin, row, textBox, ypos, _i, _ref, _results; var angle, basePos, i, label, labelBox, margin, maxSize, offset, prevAngleMargin, prevLabelMargin, row, size, startPos, textBox, _i, _ref, _results;
ypos = this.bottom + (this.options.xAxisLabelTopPadding || this.options.padding / 2); if (!this.options.horizontal) {
basePos = this.getXAxisLabelY();
} else {
basePos = this.getYAxisLabelX();
}
prevLabelMargin = null; prevLabelMargin = null;
prevAngleMargin = null; prevAngleMargin = null;
_results = []; _results = [];
for (i = _i = 0, _ref = this.data.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 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]; 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(); textBox = label.getBBox();
label.transform("r" + (-this.options.xLabelAngle)); label.transform("r" + (-angle));
labelBox = label.getBBox(); labelBox = label.getBBox();
label.transform("t0," + (labelBox.height / 2) + "..."); label.transform("t0," + (labelBox.height / 2) + "...");
if (this.options.xLabelAngle !== 0) { if (angle !== 0) {
offset = -0.5 * textBox.width * Math.cos(this.options.xLabelAngle * Math.PI / 180.0); offset = -0.5 * textBox.width * Math.cos(angle * Math.PI / 180.0);
label.transform("t" + offset + ",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.horizontal) {
if (this.options.xLabelAngle !== 0) { startPos = labelBox.x;
margin = 1.25 * this.options.gridTextSize / Math.sin(this.options.xLabelAngle * Math.PI / 180.0); size = labelBox.width;
prevAngleMargin = labelBox.x - margin; 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 { } else {
_results.push(label.remove()); _results.push(label.remove());
} }
@@ -1483,10 +1663,23 @@ Licensed under the BSD-2-Clause License.
return _results; return _results;
}; };
Bar.prototype.getXAxisLabelY = function() {
return this.bottom + (this.options.xAxisLabelTopPadding || this.options.padding / 2);
};
Bar.prototype.drawSeries = function() { Bar.prototype.drawSeries = function() {
var barWidth, bottom, groupWidth, idx, lastTop, left, leftPadding, numBars, row, sidx, size, spaceLeft, top, ypos, zeroPos; var barWidth, bottom, groupWidth, i, idx, lastTop, left, leftPadding, numBars, row, sidx, size, spaceLeft, top, ypos, zeroPos, _i, _ref;
groupWidth = this.width / this.options.data.length; groupWidth = this.xSize / this.options.data.length;
numBars = this.options.stacked ? 1 : this.options.ykeys.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; barWidth = (groupWidth * this.options.barSizeRatio - this.options.barGap * (numBars - 1)) / numBars;
if (this.options.barSize) { if (this.options.barSize) {
barWidth = Math.min(barWidth, this.options.barSize); barWidth = Math.min(barWidth, this.options.barSize);
@@ -1495,18 +1688,21 @@ Licensed under the BSD-2-Clause License.
leftPadding = spaceLeft / 2; leftPadding = spaceLeft / 2;
zeroPos = this.ymin <= 0 && this.ymax >= 0 ? this.transY(0) : null; zeroPos = this.ymin <= 0 && this.ymax >= 0 ? this.transY(0) : null;
return this.bars = (function() { return this.bars = (function() {
var _i, _len, _ref, _results; var _j, _len, _ref1, _results;
_ref = this.data; _ref1 = this.data;
_results = []; _results = [];
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) { for (idx = _j = 0, _len = _ref1.length; _j < _len; idx = ++_j) {
row = _ref[idx]; row = _ref1[idx];
lastTop = 0; lastTop = 0;
_results.push((function() { _results.push((function() {
var _j, _len1, _ref1, _results1; var _k, _len1, _ref2, _results1;
_ref1 = row._y; _ref2 = row._y;
_results1 = []; _results1 = [];
for (sidx = _j = 0, _len1 = _ref1.length; _j < _len1; sidx = ++_j) { for (sidx = _k = 0, _len1 = _ref2.length; _k < _len1; sidx = ++_k) {
ypos = _ref1[sidx]; ypos = _ref2[sidx];
if (!this.hasToShow(sidx)) {
continue;
}
if (ypos !== null) { if (ypos !== null) {
if (zeroPos) { if (zeroPos) {
top = Math.min(ypos, zeroPos); top = Math.min(ypos, zeroPos);
@@ -1515,19 +1711,28 @@ Licensed under the BSD-2-Clause License.
top = ypos; top = ypos;
bottom = this.bottom; bottom = this.bottom;
} }
left = this.left + idx * groupWidth + leftPadding; left = this.xStart + idx * groupWidth + leftPadding;
if (!this.options.stacked) { if (!this.options.stacked) {
left += sidx * (barWidth + this.options.barGap); left += sidx * (barWidth + this.options.barGap);
} }
size = bottom - top; size = bottom - top;
if (this.options.verticalGridCondition && this.options.verticalGridCondition(row.x)) { 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) { if (this.options.stacked) {
top -= lastTop; top -= lastTop;
} }
this.drawBar(left, top, barWidth, size, this.colorFor(row, sidx, 'bar'), this.options.barOpacity, this.options.barRadius); if (!this.options.horizontal) {
_results1.push(lastTop += size); 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 { } else {
_results1.push(null); _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) { if (this.data.length === 0) {
return null; return null;
} }
x = Math.max(Math.min(x, this.right), this.left); if (!this.options.horizontal) {
return Math.min(this.data.length - 1, Math.floor((x - this.left) / (this.width / this.data.length))); 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) { Bar.prototype.onGridClick = function(x, y) {
var index; var index;
index = this.hitTest(x); index = this.hitTest(x, y);
return this.fire('click', index, this.data[index].src, x, y); return this.fire('click', index, this.data[index].src, x, y);
}; };
Bar.prototype.onHoverMove = function(x, y) { Bar.prototype.onHoverMove = function(x, y) {
var index, _ref; var index, _ref;
index = this.hitTest(x); index = this.hitTest(x, y);
return (_ref = this.hover).update.apply(_ref, this.hoverContentForRow(index)); 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) { Bar.prototype.hoverContentForRow = function(index) {
var content, j, row, x, y, _i, _len, _ref; var content, j, row, x, y, _i, _len, _ref;
row = this.data[index]; 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; _ref = row.y;
for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) { for (j = _i = 0, _len = _ref.length; _i < _len; j = ++_i) {
y = _ref[j]; 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') { if (typeof this.options.hoverCallback === 'function') {
content = this.options.hoverCallback(index, this.options, content, row.src); content = this.options.hoverCallback(index, this.options, content, row.src);
} }
x = this.left + (index + 0.5) * this.width / this.data.length; if (!this.options.horizontal) {
return [content, x]; 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) { Bar.prototype.drawXAxisLabel = function(xPos, yPos, text) {

View File

@@ -26,4 +26,11 @@ $(document).ready(function () {
hideDeselected(true); hideDeselected(true);
} }
); );
function toggleblock() {
$("#new-quota-group").closest('fieldset').toggle(!$("#id_has_variations").prop('checked'));
}
$("#id_has_variations").change(toggleblock);
toggleblock();
}); });

View File

@@ -22,11 +22,11 @@ $(function () {
$("<li>").append( $("<li>").append(
$("<a>").attr("href", res.url).append( $("<a>").attr("href", res.url).append(
$("<div>").append( $("<div>").append(
$("<span>").addClass("event-name-full").append(res.name) $("<span>").addClass("event-name-full").append($("<div>").text(res.name).html())
).append( ).append(
$("<span>").addClass("event-organizer").append( $("<span>").addClass("event-organizer").append(
$("<span>").addClass("fa fa-users fa-fw") $("<span>").addClass("fa fa-users fa-fw")
).append(" ").append(res.organizer) ).append(" ").append($("<div>").text(res.organizer).html())
).append( ).append(
$("<span>").addClass("event-daterange").append( $("<span>").addClass("event-daterange").append(
$("<span>").addClass("fa fa-calendar fa-fw") $("<span>").addClass("fa fa-calendar fa-fw")

View File

@@ -43,3 +43,4 @@ vobject==0.9.*
pycountry pycountry
django-countries django-countries
pyuca # for better sorting of country names in django-countries pyuca # for better sorting of country names in django-countries
defusedcsv>=1.0.1

View File

@@ -97,14 +97,15 @@ setup(
'pycparser==2.13', 'pycparser==2.13',
'django-redis==4.7.*', 'django-redis==4.7.*',
'redis==2.10.5', 'redis==2.10.5',
'stripe==1.22.*', 'stripe==1.62.*',
'chardet<3.1.0,>=3.0.2', 'chardet<3.1.0,>=3.0.2',
'mt-940==4.7', 'mt-940==4.7',
'django-i18nfield>=1.0.1', 'django-i18nfield>=1.0.1',
'vobject==0.9.*', 'vobject==0.9.*',
'pycountry', 'pycountry',
'django-countries', 'django-countries',
'pyuca' 'pyuca',
'defusedcsv'
], ],
extras_require={ extras_require={
'dev': [ 'dev': [

View File

@@ -202,6 +202,7 @@ event_permission_urls = [
("can_change_orders", "orders/FOO/change", 200), ("can_change_orders", "orders/FOO/change", 200),
("can_change_orders", "orders/FOO/comment", 405), ("can_change_orders", "orders/FOO/comment", 405),
("can_change_orders", "orders/FOO/locale", 200), ("can_change_orders", "orders/FOO/locale", 200),
("can_view_orders", "orders/FOO/answer/5/", 404),
("can_change_vouchers", "vouchers/add", 200), ("can_change_vouchers", "vouchers/add", 200),
("can_change_orders", "requiredactions/", 200), ("can_change_orders", "requiredactions/", 200),
("can_change_vouchers", "vouchers/bulk_add", 200), ("can_change_vouchers", "vouchers/bulk_add", 200),

View File

@@ -68,6 +68,18 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
class ItemDisplayTest(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): def test_not_active(self):
q = Quota.objects.create(event=self.event, name='Quota', size=2) 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) item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=False)

View File

@@ -1,7 +1,9 @@
import datetime import datetime
import re
from decimal import Decimal from decimal import Decimal
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
@@ -418,3 +420,39 @@ class OrdersTest(TestCase):
assert self.order.payment_fee == Decimal('12.00') assert self.order.payment_fee == Decimal('12.00')
assert self.order.total == Decimal('23.00') + self.order.payment_fee assert self.order.total == Decimal('23.00') + self.order.payment_fee
assert self.order.invoices.count() == 3 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