Compare commits

...

13 Commits

Author SHA1 Message Date
Raphael Michel
a021ac6ecc Bump release 2017-08-21 14:53:46 +02:00
Raphael Michel
e6eb4945a7 [SECURITY] Support custom media URLs in CSP middleware 2017-08-21 14:53:37 +02:00
Raphael Michel
44b7b15b3e [SECURITY] Do not allow SVG files for logos 2017-08-21 14:53:21 +02:00
Raphael Michel
022ca8ad6c [SECURITY] Fix XSS injection vulnerabilities in question answers, event, quota and product names 2017-08-21 14:53:19 +02:00
Raphael Michel
5e37fbb4ef [SECURITY] Update to morris.js master to fix a XSS vulnerability 2017-08-21 14:52:41 +02:00
Raphael Michel
5049da95b5 [SECURITY] Use defusedcsv for exports 2017-08-21 14:52:31 +02:00
Raphael Michel
c78ca8bc00 [SECURITY] Rewrite all links in rich texts 2017-08-21 14:51:21 +02:00
Raphael Michel
c40e0bfdff [SECURITY] Fix XSS vulnerability in typeahead.js 2017-08-21 14:51:21 +02:00
Raphael Michel
ea79ebf105 [SECURITY] Fix XSS vulnerability in Lightbox caption 2017-08-21 14:51:00 +02:00
Raphael Michel
d4b9906638 Fix syntax error in setup.py 2017-07-02 18:57:10 +02:00
Raphael Michel
c1c4133ac6 Bump to 1.5.1 2017-07-02 18:45:00 +02:00
Raphael Michel
ff3e127648 Fix missing dependencies in setup.py 2017-07-02 18:44:39 +02:00
Raphael Michel
970b861947 Fix import in unit test 2017-07-02 17:36:38 +02:00
25 changed files with 436 additions and 123 deletions

View File

@@ -1 +1 @@
__version__ = "1.5.0"
__version__ = "1.5.2"

View File

@@ -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

View File

@@ -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

View File

@@ -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 ugettext_lazy as _
@@ -66,7 +67,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}')
@@ -76,7 +77,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}')
@@ -86,7 +87,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'item': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, Quota):
a_text = _('Quota {val}')
@@ -96,7 +97,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}')
@@ -106,7 +107,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}')
@@ -116,7 +117,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:

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

View File

@@ -617,7 +617,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.')

View File

@@ -121,7 +121,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.')

View File

@@ -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">

View File

@@ -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 %}
@@ -20,7 +21,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>

View File

@@ -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 (
@@ -135,7 +136,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={
@@ -256,7 +257,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',

View File

@@ -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

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

@@ -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 ugettext as _, ugettext_lazy

View File

@@ -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 %}

View File

@@ -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(

View File

@@ -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" %}"/>

View File

@@ -132,7 +132,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 }}"/>
@@ -243,7 +244,7 @@
<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 }}"
data-lightbox="{{ item.id }}">
<img src="{{ item.picture|thumbnail_url:'productlist' }}"
alt="{{ item.name }}"/>

View File

@@ -29,7 +29,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 }}"/>
@@ -116,7 +117,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 }}"/>

View File

@@ -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) {

View File

@@ -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")

View File

@@ -31,9 +31,6 @@ markdown
bleach==2.*
raven
django-i18nfield>=1.0.1
# API docs
coreapi==2.3.*
pygments
# Stripe
stripe==1.22.*
# PayPal
@@ -44,3 +41,4 @@ chardet<3.1.0,>=3.0.2
mt-940==3.2
vobject==0.9.*
pycountry
defusedcsv>=1.0.1

View File

@@ -63,12 +63,14 @@ setup(
keywords='tickets web shop ecommerce',
install_requires=[
'Django==1.11.*',
'djangorestframework==3.6.*',
'python-dateutil==2.4.*',
'pytz',
'django-bootstrap3==8.2.*',
'django-formset-js-improved==0.5.0.1',
'django-compressor==2.1',
'django-hierarkey==1.0.*',
'django-hierarkey==1.0.*,>=1.0.2',
'django-filter==1.0.*',
'reportlab==3.2.*',
'easy-thumbnails==2.4.*',
'PyPDF2==1.26.*',
@@ -100,7 +102,8 @@ setup(
'mt-940==4.7',
'django-i18nfield>=1.0.1',
'vobject==0.9.*',
'pycountry'
'pycountry',
'defusedcsv'
],
extras_require={
'dev': [

View File

@@ -1,8 +1,8 @@
import copy
import json
from datetime import datetime, timedelta
from unittest import mock
import mock
import pytest
from django.utils.timezone import now
from pytz import UTC

View File

@@ -66,6 +66,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)