Compare commits

...

8 Commits

Author SHA1 Message Date
Raphael Michel
3acc29f51a Bump release 2017-08-21 15:12:05 +02:00
Raphael Michel
4bae0d1b81 [SECURITY] Rewrite all links in rich texts 2017-08-21 15:11:54 +02:00
Raphael Michel
c38a850294 [SECURITY] Fix XSS vulnerability in Lightbox caption 2017-08-21 15:09:44 +02:00
Raphael Michel
a7ec475c40 [SECURITY] Do not allow SVG files for logos 2017-08-21 15:08:39 +02:00
Raphael Michel
ac1467bd4b [SECURITY] Fix XSS injection vulnerabilities in question answers, event, quota and product names 2017-08-21 15:08:26 +02:00
Raphael Michel
3f0af67345 [SECURITY] Update to morris.js master to fix a XSS vulnerability 2017-08-21 15:07:56 +02:00
Raphael Michel
4d14b6c096 [SECURITY] Use defusedcsv for exports 2017-08-21 15:06:58 +02:00
Raphael Michel
cb789bc06c Ignore style in tests, change import order 2017-06-01 13:28:28 +02:00
24 changed files with 407 additions and 119 deletions

View File

@@ -5,7 +5,6 @@ tests:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache bash .travis.sh style
- XDG_CACHE_HOME=/cache bash .travis.sh tests
tags:
- python3

View File

@@ -1 +1 @@
__version__ = "1.4.0"
__version__ = "1.4.1"

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

@@ -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()
@@ -42,6 +48,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):
"""
@@ -52,5 +67,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

@@ -612,7 +612,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

@@ -10,6 +10,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 (
@@ -138,7 +139,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={
@@ -269,7 +270,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

@@ -2,8 +2,9 @@ import time
from urllib.parse import urlparse
from django.conf import settings
from django.contrib.sessions.middleware import \
SessionMiddleware as BaseSessionMiddleware
from django.contrib.sessions.middleware import (
SessionMiddleware as BaseSessionMiddleware,
)
from django.core.cache import cache
from django.core.exceptions import DisallowedHost
from django.core.urlresolvers import set_urlconf

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

@@ -17,8 +17,9 @@ from django.views.generic import TemplateView, View
from pretix.base.models import Checkin, Event, Order, OrderPosition
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.urls import build_absolute_uri
from pretix.multidomain.urlreverse import \
build_absolute_uri as event_absolute_uri
from pretix.multidomain.urlreverse import (
build_absolute_uri as event_absolute_uri,
)
logger = logging.getLogger('pretix.plugins.pretixdroid')
API_VERSION = 3

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

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

@@ -39,3 +39,4 @@ chardet<3.1.0,>=3.0.2
mt-940==3.2
vobject==0.9.*
pycountry
defusedcsv>=1.0.1

View File

@@ -100,7 +100,8 @@ setup(
'mt-940==4.7',
'django-i18nfield>=1.0.1',
'vobject==0.9.*',
'pycountry'
'pycountry',
'defusedcsv'
],
extras_require={
'dev': [

View File

@@ -2,10 +2,9 @@ from django.db import DEFAULT_DB_ALIAS, connections
from django.test.utils import CaptureQueriesContext
# Inspired by /django/test/testcases.py
# but copied over to work without the unit test module
class _AssertNumQueriesContext(CaptureQueriesContext):
# Inspired by /django/test/testcases.py
# but copied over to work without the unit test module
def __init__(self, num, connection):
self.num = num
super(_AssertNumQueriesContext, self).__init__(connection)

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)