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 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 ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -66,7 +67,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}')
@@ -76,7 +77,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}')
@@ -86,7 +87,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, Quota): elif isinstance(co, Quota):
a_text = _('Quota {val}') a_text = _('Quota {val}')
@@ -96,7 +97,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}')
@@ -106,7 +107,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}')
@@ -116,7 +117,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

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

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

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

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

@@ -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 ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy

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

@@ -132,7 +132,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 }}"/>
@@ -243,7 +244,7 @@
<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 }}"
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

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

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

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

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

View File

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

View File

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

View File

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