diff --git a/kobocat-template/static/css/kobo-branding.css b/kobocat-template/static/css/kobo-branding.css index 45cfcc0fc..47306af30 100644 --- a/kobocat-template/static/css/kobo-branding.css +++ b/kobocat-template/static/css/kobo-branding.css @@ -330,7 +330,7 @@ section { .single-project__header .container, .projects-list__header .container, .data-page__header .container { - max-width: 984px; + max-width: 960px; } .single-project__header .fa-caret-right, @@ -681,4 +681,4 @@ i.fa-trash-o { td.footable-first-column { padding-right: 0px !important; } -} \ No newline at end of file +} diff --git a/kobocat-template/static/css/kobo-single-project.css b/kobocat-template/static/css/kobo-single-project.css index 69fd8bb66..c96ce6fd7 100644 --- a/kobocat-template/static/css/kobo-single-project.css +++ b/kobocat-template/static/css/kobo-single-project.css @@ -189,7 +189,7 @@ button.single-project__button-delete { } .dashboard__left { - width: 72%; + width: 60%; float: left; margin-left: 1%; } @@ -197,6 +197,7 @@ button.single-project__button-delete { .dashboard__right { width: 25%; float: left; + margin-left: -13%; } h2.dashboard__group-label { @@ -205,9 +206,8 @@ h2.dashboard__group-label { } .dashboard__submissions { - width: 55%; + width: 33%; float: left; - margin-right: 5%; position: relative; } @@ -268,7 +268,7 @@ a.dashboard__button-download-data:hover span { } .dashboard__submissions a.dashboard__button { - width: 30%; + width: 60%; } a.dashboard__button-twoline span { @@ -351,8 +351,8 @@ a.dashboard__formbutton-refresh { .dashboard__download__drop { position: absolute; - right: 22px; - bottom: -150px; + left: 0; + bottom: -128px; width: 124px; border: 1px solid #E7E8E9; background: white; @@ -629,4 +629,4 @@ span.poshytip { .dashboard__download__drop { bottom: -190px; } -} \ No newline at end of file +} diff --git a/kobocat-template/templates/base.html b/kobocat-template/templates/base.html index e67996755..8399f7caa 100644 --- a/kobocat-template/templates/base.html +++ b/kobocat-template/templates/base.html @@ -1,97 +1,102 @@ {% load static %} - + {% block title %}{{ SITE_NAME }}{% endblock %} - + {% if GOOGLE_SITE_VERIFICATION %} - + {% endif %} - {% if csrf_token %}{% endif %} + {% if csrf_token %} + {% endif %} {% block styles %} - - - - - - - - - - - - + + + + + + + + + + - + - - - - - + + + + + - + {% endblock %} {% block additional-headers %}{% endblock %} - - - {% block body %} - {% if not user.is_authenticated %} - - -
-
- We no longer recommend using the legacy interface. Please access all our new features in the new interface (new map, table, reports, labeled data exports, etc.). If you have deployed your project directly in the legacy interface, there is no way to use these new features. This legacy interface will be removed shortly. -
-
- -
-
- - Support -
-
- - {% else %} - - - {% include "topbar.html"%} - - {% endif %} - - {% block before-content %}{% endblock %} - -
- {% block message %} + + +{% block body %} + {% if not user.is_authenticated %} + + +
+
+ + We no longer recommend using the legacy interface. Please access all our new features in the new interface (new map, table, reports, labeled data exports, etc.). +
+
+ +
+
+ + Support +
+
+ + {% else %} + + + {% include "topbar.html" %} + + {% endif %} + +
+ {% block message %} {% if message or messages or message_list %} - {% include "message.html" %} + {% include "message.html" %} {% endif %} - {% endblock %} - {% block content %} + {% endblock %} + {% block content %} {% if template %}{% include template %}{% endif %} {{ content|safe }} - {% endblock %} -
+ {% endblock %} +
- {% include "footer.html" %} +{% include "footer.html" %} - {% block below-content %}{% endblock %} +{% block below-content %}{% endblock %} - {% block javascript %} +{% block javascript %} @@ -108,25 +113,32 @@ - {% endblock %} +{% endblock %} - {% block additional-javascript %}{% endblock %} +{% block additional-javascript %}{% endblock %} - {% block google-analytics %} +{% block google-analytics %} {% if GOOGLE_ANALYTICS_PROPERTY_ID %} - + {% endif %} - {% endblock %} +{% endblock %} - - {% endblock %} + +{% endblock %} diff --git a/kobocat-template/templates/dashboard.html b/kobocat-template/templates/dashboard.html index cfc087dca..2444f6959 100644 --- a/kobocat-template/templates/dashboard.html +++ b/kobocat-template/templates/dashboard.html @@ -64,12 +64,11 @@

{% blocktrans %}Publish a Form Upload XLSForm{% endblocktrans

- {{ content_user.username }} + {{ content_user.username }} {% if profile.name %} | {{ profile.name }} {% endif %}

- {% trans "View Profile Page" %}
diff --git a/kobocat-template/templates/data_view.html b/kobocat-template/templates/data_view.html deleted file mode 100644 index e3453f318..000000000 --- a/kobocat-template/templates/data_view.html +++ /dev/null @@ -1,97 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% load static %} - -{% block additional-headers %} -{% load i18n %} - - - - -{% endblock %} - -{% block before-content %} - - -
-
-

{% trans "Data View" %}

-
-
- -{% endblock %} - -{% block content %} -
-
- -
-
-
-
-
-
-{% endblock %} - -{% block javascript %} - {{ block.super }} - - - - - - - - - - - -{% endblock %} diff --git a/kobocat-template/templates/instance.html b/kobocat-template/templates/instance.html deleted file mode 100644 index 7f2676861..000000000 --- a/kobocat-template/templates/instance.html +++ /dev/null @@ -1,190 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% load static %} - -{% block before-content %} -{% load i18n %} - - - -
-
-

{% trans "Browse Form Data" %}

-
-
- -{% endblock %} - -{% block content %} -{% load i18n %} -
-

{% trans "Loading..." %}

- Loading... -
-{% if messages %} -
{{messages}}
-{% endif %} -
- - -{% endblock %} - -{% block javascript %} -{{ block.super }} - - - - - - - - - - -{% endblock %} diff --git a/kobocat-template/templates/map.html b/kobocat-template/templates/map.html index 5711925ac..3d95696fe 100644 --- a/kobocat-template/templates/map.html +++ b/kobocat-template/templates/map.html @@ -3,12 +3,12 @@ {% load static %} {% block additional-headers %} - + - - - - @@ -16,274 +16,275 @@ {% block body %} - - {% include "topbar.html" %} - - - {% endif %} - - diff --git a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py index 81be884eb..1adcbd160 100644 --- a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py @@ -52,38 +52,42 @@ def setUp(self): self._add_permissions_to_user(AnonymousUser()) self.maxDiff = None - def publish_xls_form(self): - data = { - 'owner': self.user.username, - 'public': False, - 'public_data': False, - 'description': u'transportation_2011_07_25', - 'downloadable': True, - 'allows_sms': False, - 'encrypted': False, - 'sms_id_string': u'transportation_2011_07_25', - 'id_string': u'transportation_2011_07_25', - 'title': u'transportation_2011_07_25', - } - - path = os.path.join( - settings.ONADATA_DIR, "apps", "main", "tests", "fixtures", - "transportation", "transportation.xls") + def publish_xls_form(self, path=None, data=None, assert_=True): + if not data: + data = { + 'owner': self.user.username, + 'public': False, + 'public_data': False, + 'description': u'transportation_2011_07_25', + 'downloadable': True, + 'encrypted': False, + 'id_string': u'transportation_2011_07_25', + 'title': u'transportation_2011_07_25', + } + + if not path: + path = os.path.join( + settings.ONADATA_DIR, "apps", "main", "tests", "fixtures", + "transportation", "transportation.xls") xform_list_url = reverse('xform-list') with open(path) as xls_file: post_data = {'xls_file': xls_file} response = self.client.post(xform_list_url, data=post_data) - self.assertEqual(response.status_code, 201) - self.xform = XForm.objects.all().order_by('pk').reverse()[0] - data.update({ - 'url': - 'http://testserver/api/v1/forms/%s' % (self.xform.pk) - }) - - self.assertDictContainsSubset(data, response.data) - self.form_data = response.data + + if not assert_: + return response + + self.assertEqual(response.status_code, 201) + self.xform = XForm.objects.all().order_by('pk').reverse()[0] + data.update({ + 'url': + 'http://testserver/api/v1/forms/%s' % (self.xform.pk) + }) + + self.assertDictContainsSubset(data, response.data) + self.form_data = response.data def user_profile_data(self): return { diff --git a/onadata/apps/api/tests/viewsets/test_xform_viewset.py b/onadata/apps/api/tests/viewsets/test_xform_viewset.py index 5fe3aaba6..88300d3f8 100644 --- a/onadata/apps/api/tests/viewsets/test_xform_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_xform_viewset.py @@ -4,7 +4,6 @@ import os import re from datetime import datetime -from xml.dom import minidom, Node import pytz import requests @@ -13,6 +12,7 @@ from guardian.shortcuts import assign_perm from httmock import HTTMock, all_requests from rest_framework import status +from xml.dom import minidom, Node from onadata.apps.api.tests.viewsets.test_abstract_viewset import \ TestAbstractViewSet @@ -29,7 +29,7 @@ def enketo_mock(url, request): response = requests.Response() response.status_code = 201 response._content = \ - '{\n "url": "https:\\/\\/dmfrm.enketo.org\\/webform",\n'\ + '{\n "url": "https:\\/\\/dmfrm.enketo.org\\/webform",\n' \ ' "code": "200"\n}' return response @@ -39,7 +39,7 @@ def enketo_error_mock(url, request): response = requests.Response() response.status_code = 400 response._content = \ - '{\n "message": "no account exists for this OpenRosa server",\n'\ + '{\n "message": "no account exists for this OpenRosa server",\n' \ ' "code": "200"\n}' return response @@ -79,7 +79,7 @@ def test_form_list_other_user_access(self): alice_data = {'username': 'alice', 'email': 'alice@localhost.com'} self._login_user_and_profile(extra_post_data=alice_data) self.assertEqual(self.user.username, 'alice') - self.assertNotEqual(previous_user, self.user) + self.assertNotEqual(previous_user, self.user) request = self.factory.get('/', **self.extra) response = self.view(request) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -94,7 +94,7 @@ def test_form_list_filter_by_user(self): alice_data = {'username': 'alice', 'email': 'alice@localhost.com'} self._login_user_and_profile(extra_post_data=alice_data) self.assertEqual(self.user.username, 'alice') - self.assertNotEqual(previous_user, self.user) + self.assertNotEqual(previous_user, self.user) assign_perm(CAN_VIEW_XFORM, self.user, self.xform) view = XFormViewSet.as_view({ @@ -158,7 +158,7 @@ def test_form_format(self): } request = self.factory.get('/', **self.extra) # test for unsupported format - response = view(request, pk=formid, format='csvzip') + response = view(request, pk=formid, format='xlsx') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # test for supported formats @@ -181,13 +181,13 @@ def test_form_format(self): n for n in response_doc.getElementsByTagName("h:head")[0].childNodes if n.nodeType == Node.ELEMENT_NODE and - n.tagName == "model"][0] + n.tagName == "model"][0] # check for UUID and remove uuid_nodes = [ node for node in model_node.childNodes if node.nodeType == Node.ELEMENT_NODE - and node.getAttribute("nodeset") == "/transportation_2011_07_25/formhub/uuid"] + and node.getAttribute("nodeset") == "/transportation_2011_07_25/formhub/uuid"] self.assertEqual(len(uuid_nodes), 1) uuid_node = uuid_nodes[0] uuid_node.setAttribute("calculate", "''") @@ -238,34 +238,6 @@ def test_form_tags(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, []) - def test_enketo_url_no_account(self): - self.publish_xls_form() - view = XFormViewSet.as_view({ - 'get': 'enketo' - }) - formid = self.xform.pk - # no tags - request = self.factory.get('/', **self.extra) - with HTTMock(enketo_error_mock): - response = view(request, pk=formid) - data = {'message': "Enketo not properly configured."} - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, data) - - def test_enketo_url(self): - self.publish_xls_form() - view = XFormViewSet.as_view({ - 'get': 'enketo' - }) - formid = self.xform.pk - # no tags - request = self.factory.get('/', **self.extra) - with HTTMock(enketo_mock): - response = view(request, pk=formid) - data = {"enketo_url": "https://dmfrm.enketo.org/webform"} - self.assertEqual(response.data, data) - def test_publish_xlsform(self): view = XFormViewSet.as_view({ 'post': 'create' @@ -276,11 +248,9 @@ def test_publish_xlsform(self): 'public_data': False, 'description': 'transportation_2011_07_25', 'downloadable': True, - 'allows_sms': False, 'encrypted': False, - 'sms_id_string': 'transportation_2011_07_25', 'id_string': 'transportation_2011_07_25', - 'title': 'transportation_2011_07_25', + 'title': 'transportation_2011_07_25' } path = os.path.join( settings.ONADATA_DIR, "apps", "main", "tests", "fixtures", @@ -293,7 +263,7 @@ def test_publish_xlsform(self): xform = self.user.xforms.get(uuid=response.data.get('uuid')) data.update({ 'url': - 'http://testserver/api/v1/forms/%s' % xform.pk + 'http://testserver/api/v1/forms/%s' % xform.pk }) self.assertDictContainsSubset(data, response.data) self.assertTrue(xform.user.pk == self.user.pk) @@ -390,7 +360,7 @@ def test_set_form_bad_value(self): self.assertFalse(self.xform.__getattribute__(key)) self.assertEqual(response.data, {'shared': - ["'String' value must be either True or False."]}) + ["'String' value must be either True or False."]}) def test_set_form_bad_key(self): self.publish_xls_form() @@ -429,7 +399,6 @@ def test_xform_serializer_none(self): 'require_auth': False, 'description': '', 'downloadable': False, - 'allows_sms': False, 'uuid': '', 'instances_with_geopoints': False, 'num_of_submissions': 0, @@ -474,3 +443,24 @@ def test_csv_import_fail_invalid_field_post(self): response = view(request, pk=self.xform.id) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIsNotNone(response.data.get('error')) + + def test_cannot_publish_id_string_starting_with_number(self): + data = { + 'owner': self.user.username, + 'public': False, + 'public_data': False, + 'description': '2011_07_25_transportation', + 'downloadable': True, + 'encrypted': False, + 'id_string': '2011_07_25_transportation', + 'title': '2011_07_25_transportation', + } + + xls_path = os.path.join(settings.ONADATA_DIR, 'apps', 'main', 'tests', + 'fixtures', 'transportation', + 'transportation.id_starts_with_num.xls') + count = XForm.objects.count() + response = self.publish_xls_form(xls_path, data, assert_=False) + self.assertTrue('Names must begin with a letter' in response.content) + self.assertEqual(response.status_code, 400) + self.assertEqual(XForm.objects.count(), count) diff --git a/onadata/apps/api/tools.py b/onadata/apps/api/tools.py index 321d35490..34a6465ea 100644 --- a/onadata/apps/api/tools.py +++ b/onadata/apps/api/tools.py @@ -18,7 +18,7 @@ from rest_framework import exceptions from taggit.forms import TagField -from onadata.apps.main.forms import QuickConverter +from onadata.apps.main.forms import QuickConverterForm from onadata.apps.main.models import UserProfile from onadata.apps.viewer.models.parsed_instance import datetime_from_str from onadata.libs.utils.logger_tools import publish_form @@ -45,23 +45,6 @@ def _get_id_for_type(record, mongo_field): return {"$substr": [mongo_str, 0, 10]} if isinstance(date_field, datetime)\ else mongo_str -# TODO verify tests without this method, then delete -# def get_accessible_forms(owner=None, shared_form=False, shared_data=False): -# xforms = XForm.objects.filter() -# -# if shared_form and not shared_data: -# xforms = xforms.filter(shared=True) -# elif (shared_form and shared_data) or \ -# (owner == 'public' and not shared_form and not shared_data): -# xforms = xforms.filter(Q(shared=True) | Q(shared_data=True)) -# elif not shared_form and shared_data: -# xforms = xforms.filter(shared_data=True) -# -# if owner != 'public': -# xforms = xforms.filter(user__username=owner) -# -# return xforms.distinct() - def publish_xlsform(request, user, existing_xform=None): """ @@ -84,7 +67,7 @@ def publish_xlsform(request, user, existing_xform=None): ) def set_form(): - form = QuickConverter(request.POST, request.FILES) + form = QuickConverterForm(request.POST, request.FILES) if existing_xform: return form.publish(user, existing_xform.id_string) else: diff --git a/onadata/apps/api/viewsets/data_viewset.py b/onadata/apps/api/viewsets/data_viewset.py index 5dbdfd46d..bf1f32fee 100644 --- a/onadata/apps/api/viewsets/data_viewset.py +++ b/onadata/apps/api/viewsets/data_viewset.py @@ -379,8 +379,6 @@ class DataViewSet(AnonymousUserPublicFormsMixin, ModelViewSet): renderers.XLSRenderer, renderers.XLSXRenderer, renderers.CSVRenderer, - renderers.CSVZIPRenderer, - renderers.SAVZIPRenderer, renderers.RawXMLRenderer ] diff --git a/onadata/apps/api/viewsets/xform_viewset.py b/onadata/apps/api/viewsets/xform_viewset.py index a01c44a6b..a07373419 100644 --- a/onadata/apps/api/viewsets/xform_viewset.py +++ b/onadata/apps/api/viewsets/xform_viewset.py @@ -45,8 +45,6 @@ 'xls': Export.XLS_EXPORT, 'xlsx': Export.XLS_EXPORT, 'csv': Export.CSV_EXPORT, - 'csvzip': Export.CSV_ZIP_EXPORT, - 'savzip': Export.SAV_ZIP_EXPORT, } @@ -67,8 +65,6 @@ def _get_extension_from_export_type(export_type): if export_type == Export.XLS_EXPORT: extension = 'xlsx' - elif export_type in [Export.CSV_ZIP_EXPORT, Export.SAV_ZIP_EXPORT]: - extension = 'zip' return extension @@ -165,8 +161,7 @@ def response_for_format(form, format=None): def should_regenerate_export(xform, export_type, request): return should_create_new_export(xform, export_type) or\ 'start' in request.GET or 'end' in request.GET or\ - 'query' in request.GET or 'meta' in request.GET or\ - 'token' in request.GET + 'query' in request.GET def value_for_type(form, field, value): @@ -191,8 +186,7 @@ def log_export(request, xform, export_type): }, audit, request) -def custom_response_handler(request, xform, query, export_type, - token=None, meta=None): +def custom_response_handler(request, xform, query, export_type): export_type = _get_export_type(export_type) # check if we need to re-generate, @@ -237,7 +231,6 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet): account. - `xls_file`: the xlsform file. -- `xls_url`: the url to an xlsform - `owner`: username to the target account (Optional)
@@ -247,11 +240,6 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet):
 >       curl -X POST -F xls_file=@/path/to/form.xls \
 https://example.com/api/v1/forms
 >
-> OR post an xlsform url
->
->       curl -X POST -d \
-"xls_url=https://example.com/ukanga/forms/tutorial/form.xls" \
-https://example.com/api/v1/forms
 
 > Response
 >
@@ -260,9 +248,7 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet):
 >           "formid": 28058,
 >           "uuid": "853196d7d0a74bca9ecfadbf7e2f5c1f",
 >           "id_string": "Birds",
->           "sms_id_string": "Birds",
 >           "title": "Birds",
->           "allows_sms": false,
 >           "description": "",
 >           "downloadable": true,
 >           "encrypted": false,
@@ -317,9 +303,7 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet):
 >           "formid": 28058,
 >           "uuid": "853196d7d0a74bca9ecfadbf7e2f5c1f",
 >           "id_string": "Birds",
->           "sms_id_string": "Birds",
 >           "title": "Birds",
->           "allows_sms": false,
 >           "description": "",
 >           "downloadable": true,
 >           "encrypted": false,
@@ -353,9 +337,7 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet):
 >           "formid": 28058,
 >           "uuid": "853196d7d0a74bca9ecfadbf7e2f5c1f",
 >           "id_string": "Birds",
->           "sms_id_string": "Birds",
 >           "title": "Birds",
->           "allows_sms": false,
 >           "description": "Le description",
 >           "downloadable": true,
 >           "encrypted": false,
@@ -370,7 +352,7 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet):
 
 You may overwrite the form's contents while preserving its submitted data,
 `id_string` and all other attributes, by sending a `PATCH` that includes
-`xls_file` or `text_xls_form`. Use with caution, as this may compromise the
+`xls_file`. Use with caution, as this may compromise the
 methodology of your study!
 
 
@@ -539,22 +521,6 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet):
 >
 >        HTTP 200 OK
 
-## Get webform/enketo link
-
-
-GET /api/v1/forms/{pk}/enketo
- -> Request -> -> curl -X GET \ -https://example.com/api/v1/forms/28058/enketo -> -> Response -> -> {"enketo_url": "https://h6ic6.enketo.org/webform"} -> -> HTTP 200 OK - ## Get form data in xls, csv format. Get form data exported as xls, csv, csv zip, sav zip format. @@ -564,11 +530,6 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet): - `pk` - is the form unique identifier - `format` - is the data export format i.e csv, xls, csvzip, savzip -Params for the custom xls report - -- `meta` - the metadata id containing the template url -- `token` - the template url -
 GET /api/v1/forms/{pk}.{format}
 
@@ -583,46 +544,6 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet): > > HTTP 200 OK -> Example 2 Custom XLS reports (beta) -> -> curl -X GET https://example.com/api/v1/forms/28058.xls?meta=12121 -> or -> curl -X GET https://example.com/api/v1/forms/28058.xls?token={url} -> -> XLS file is downloaded -> -> Response -> -> HTTP 200 OK - -## Clone a form to a specific user account - -You can clone a form to a specific user account using `GET` with - -- `username` of the user you want to clone the form to - -
-GET /api/v1/forms/{pk}/clone
-
- -> Example -> -> curl -X GET https://example.com/api/v1/forms/123/clone \ --d username=alice - -> Response -> -> HTTP 201 CREATED -> { -> "url": "https://example.com/api/v1/forms/124", -> "formid": 124, -> "uuid": "853196d7d0a74bca9ecfadbf7e2f5c1e", -> "id_string": "Birds_cloned_1", -> "sms_id_string": "Birds_cloned_1", -> "title": "Birds_cloned_1", -> ... -> } - ## Import CSV data to existing form - `csv_file` a valid csv file with exported \ @@ -649,8 +570,6 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet): renderers.XLSRenderer, renderers.XLSXRenderer, renderers.CSVRenderer, - renderers.CSVZIPRenderer, - renderers.SAVZIPRenderer, renderers.RawXMLRenderer ] queryset = XForm.objects.all() @@ -673,9 +592,8 @@ def create(self, request, *args, **kwargs): if isinstance(survey, XForm): xform = XForm.objects.get(pk=survey.pk) # The XForm has been created, but `publish_xlsform` relies on - # `onadata.apps.main.forms.QuickConverter`, which uses standard - # Django forms and only recognizes the `xls_file`, `xls_url`, - # `dropbox_xls_url`, and `text_xls_form` fields. + # `onadata.apps.main.forms.QuickConverterForm`, which uses standard + # Django forms and only recognizes the `xls_file` fields. # Use the DRF serializer to update the XForm with values for other # fields. serializer = XFormSerializer( @@ -694,7 +612,7 @@ def create(self, request, *args, **kwargs): return Response(survey, status=status.HTTP_400_BAD_REQUEST) def update(self, request, pk, *args, **kwargs): - if 'xls_file' in request.FILES or 'text_xls_form' in request.data: + if 'xls_file' in request.FILES: # A new XLSForm has been uploaded and will replace the existing # form existing_xform = get_object_or_404(XForm, pk=pk) @@ -729,25 +647,6 @@ def form(self, request, format='json', **kwargs): return response - @detail_route(methods=['GET']) - def enketo(self, request, **kwargs): - self.object = self.get_object() - form_url = _get_form_url(self.object.user.username) - - data = {'message': _("Enketo not properly configured.")} - http_status = status.HTTP_400_BAD_REQUEST - - try: - url = enketo_url(form_url, self.object.id_string) - except EnketoError: - pass - else: - if url: - http_status = status.HTTP_200_OK - data = {"enketo_url": url} - - return Response(data, http_status) - def retrieve(self, request, *args, **kwargs): xform = self.get_object() export_type = kwargs.get('format') diff --git a/onadata/apps/export/__init__.py b/onadata/apps/export/__init__.py deleted file mode 100644 index 91f7c6c25..000000000 --- a/onadata/apps/export/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -################################################# -# THIS APP IS DEAD CODE AND SHOULD BE EXCISED # -# EVERY SINGLE ENDPOINT 500s EXCEPT export_menu # -################################################# diff --git a/onadata/apps/export/templates/export/export_html.html b/onadata/apps/export/templates/export/export_html.html deleted file mode 100644 index e29f975af..000000000 --- a/onadata/apps/export/templates/export/export_html.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -
-{% block content %} - - - -

{{ title }}

- -{{ table }} - - - -{% endblock %} -
diff --git a/onadata/apps/export/templates/export/export_menu.html b/onadata/apps/export/templates/export/export_menu.html deleted file mode 100644 index e22105856..000000000 --- a/onadata/apps/export/templates/export/export_menu.html +++ /dev/null @@ -1,163 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -
-{% block content %} - - - -

{% trans "Exports" %}:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% for lang in languages %} - - {% if lang == "_default" %} - - - - - - - - - - - - {% else %} - - - - - - - - - - - {% endif %} - -{% endfor %} - - -
{% trans "Groups included" %}{% trans "Groups excluded" %}
{% trans "HTML view" %}{% trans "CSV" %}{% trans "XLS" %}{% trans "HTML view" %}{% trans "CSV" %}{% trans "XLS" %}
- - {% trans "XML values and headers" %} - - - - {% trans "XML values and headers" %} - - - - {% trans "XML values and headers" %} - - - - {% trans "XML values and headers" %} - - - - {% trans "XML values and headers" %} - - - - {% trans "XML values and headers" %} - -
- - {% trans "Default labels" %} - - - - {% trans "Default labels" %} - - - - {% trans "Default labels" %} - - - - {% trans "Default labels" %} - - - - {% trans "Default labels" %} - - - - {% trans "Default labels" %} - -
- - {% trans lang %} - - - - {% trans lang %} - - - - {% trans lang %} - - - - {% trans lang %} - - - - {% trans lang %} - - - - {% trans lang %} - -
- -{% endblock %} -
diff --git a/onadata/apps/export/urls.py b/onadata/apps/export/urls.py deleted file mode 100644 index 5938b71bb..000000000 --- a/onadata/apps/export/urls.py +++ /dev/null @@ -1,25 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -################################################# -# THIS APP IS DEAD CODE AND SHOULD BE EXCISED # -# EVERY SINGLE ENDPOINT 500s EXCEPT export_menu # -################################################# - -from django.conf.urls import patterns, url - - -urlpatterns = patterns( - '', - url(r"(?P[^/]+)/$", - 'onadata.apps.export.views.export_menu', - name='formpack_export_menu'), - url(r"(?P[^/]+).csv$", - 'onadata.apps.export.views.csv_export', - name='formpack_csv_export'), - url(r"(?P[^/]+).xlsx$", - 'onadata.apps.export.views.xlsx_export', - name='formpack_xlsx_export'), - url(r"(?P[^/]+).html$", - 'onadata.apps.export.views.html_export', - name='formpack_html_export') -) diff --git a/onadata/apps/export/views.py b/onadata/apps/export/views.py deleted file mode 100644 index 148b278e5..000000000 --- a/onadata/apps/export/views.py +++ /dev/null @@ -1,164 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -import uuid -from datetime import datetime - -from django.conf import settings -from django.contrib.auth.models import User -from django.http import HttpResponse, HttpResponseForbidden -from django.shortcuts import render, get_object_or_404 -from django.utils.safestring import mark_safe -from django.utils.translation import ugettext as _ -from formpack import FormPack -from path import tempdir -from pure_pagination import Paginator, EmptyPage, PageNotAnInteger - -from onadata.libs.utils.user_auth import has_permission - - -################################################# -# THIS APP IS DEAD CODE AND SHOULD BE EXCISED # -# EVERY SINGLE ENDPOINT 500s EXCEPT export_menu # -################################################# - - -def readable_xform_required(func): - def _wrapper(request, username, id_string): - owner = get_object_or_404(User, username=username) - xform = get_object_or_404(owner.xforms, id_string=id_string) - if not has_permission(xform, owner, request): - return HttpResponseForbidden(_('Not shared.')) - return func(request, username, id_string) - return _wrapper - - -def get_instances_for_user_and_form(user, form_id): - userform_id = '{}_{}'.format(user, form_id) - query = {'_userform_id': userform_id} - return settings.MONGO_DB.instances.find(query) - - -def build_formpack(username, id_string): - user = User.objects.get(username=username) - xform = user.xforms.get(id_string=id_string) - schema = { - "id_string": id_string, - "version": 'v1', - "content": xform.to_kpi_content_schema(), - } - return FormPack([schema], id_string) - - -def build_export(request, username, id_string): - - hierarchy_in_labels = request.REQUEST.get( - 'hierarchy_in_labels', '' - ).lower() == 'true' - group_sep = request.REQUEST.get('groupsep', '/') - lang = request.REQUEST.get('lang', None) - - options = {'versions': 'v1', - 'header_lang': lang, - 'group_sep': group_sep, - 'translation': lang, - 'hierarchy_in_labels': hierarchy_in_labels, - 'copy_fields': ('_id', '_uuid', '_submission_time'), - 'force_index': True} - - formpack = build_formpack(username, id_string) - return formpack.export(**options) - - -def build_export_filename(export, extension): - form_type = 'labels' - if not export.translation: - form_type = "values" - elif export.translation != "_default": - form_type = export.translation - - return "{title} - {form_type} - {date:%Y-%m-%d-%H-%M}.{ext}".format( - form_type=form_type, - date=datetime.utcnow(), - title=export.title, - ext=extension - ) - - -@readable_xform_required -def export_menu(request, username, id_string): - - form_pack = build_formpack(username, id_string) - - context = { - 'languages': form_pack.available_translations, - 'username': username, - 'id_string': id_string - } - - return render(request, 'export/export_menu.html', context) - - -@readable_xform_required -def xlsx_export(request, username, id_string): - - export = build_export(request, username, id_string) - data = [("v1", get_instances_for_user_and_form(username, id_string))] - - with tempdir() as d: - tempfile = d / str(uuid.uuid4()) - export.to_xlsx(tempfile, data) - xlsx = tempfile.bytes() - - name = build_export_filename(export, 'xlsx') - ct = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - response = HttpResponse(xlsx, content_type=ct) - response['Content-Disposition'] = 'attachment; filename="%s"' % name - return response - - -@readable_xform_required -def csv_export(request, username, id_string): - - export = build_export(request, username, id_string) - data = [("v1", get_instances_for_user_and_form(username, id_string))] - - name = build_export_filename(export, 'csv') - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="%s"' % name - - for line in export.to_csv(data): - response.write(line + "\n") - - return response - - -@readable_xform_required -def html_export(request, username, id_string): - - limit = request.REQUEST.get('limit', 100) - - cursor = get_instances_for_user_and_form(username, id_string) - paginator = Paginator(cursor, limit, request=request) - - try: - page = paginator.page(request.REQUEST.get('page', 1)) - except (EmptyPage, PageNotAnInteger): - try: - page = paginator.page(1) - except (EmptyPage, PageNotAnInteger): - page = None - - context = { - 'page': page, - 'table': [] - } - - if page: - data = [("v1", page.object_list)] - export = build_export(request, username, id_string) - context['table'] = mark_safe("\n".join(export.to_html(data))) - context['title'] = id_string - - return render(request, 'export/export_html.html', context) - diff --git a/onadata/apps/logger/models/instance.py b/onadata/apps/logger/models/instance.py index 61a308969..3db023d52 100644 --- a/onadata/apps/logger/models/instance.py +++ b/onadata/apps/logger/models/instance.py @@ -100,7 +100,11 @@ def update_xform_submission_count_delete(sender, instance, **kwargs): xform.num_of_submissions -= 1 if xform.num_of_submissions < 0: xform.num_of_submissions = 0 - xform.save(update_fields=['num_of_submissions']) + # Update `date_modified` to detect outdated exports + # with deleted instances + xform.date_modified = timezone.now() + + xform.save(update_fields=['num_of_submissions', 'date_modified']) profile_qs = User.profile.get_queryset() try: profile = profile_qs.select_for_update()\ diff --git a/onadata/apps/logger/models/xform.py b/onadata/apps/logger/models/xform.py index 9c6a996c7..068452243 100644 --- a/onadata/apps/logger/models/xform.py +++ b/onadata/apps/logger/models/xform.py @@ -1,16 +1,13 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -import io import json import os import re from cStringIO import StringIO -from datetime import datetime from hashlib import md5 from xml.sax import saxutils -import pytz from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist @@ -18,7 +15,6 @@ from django.core.urlresolvers import reverse from django.db import models from django.db.models.signals import post_save, post_delete -from django.utils import timezone from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy, ugettext as _ from guardian.shortcuts import ( @@ -39,11 +35,6 @@ ) from onadata.libs.models.base_model import BaseModel -try: - from formpack.utils.xls_to_ss_structure import xls_to_dicts -except ImportError: - xls_to_dicts = False - XFORM_TITLE_LENGTH = 255 title_pattern = re.compile(r"([^<]+)") @@ -70,10 +61,10 @@ class XForm(BaseModel): shared = models.BooleanField(default=False) shared_data = models.BooleanField(default=False) downloadable = models.BooleanField(default=True) - allows_sms = models.BooleanField(default=False) encrypted = models.BooleanField(default=False) # the following fields are filled in automatically + allows_sms = models.BooleanField(default=False) sms_id_string = models.SlugField( editable=False, verbose_name=ugettext_lazy("SMS ID"), @@ -206,15 +197,12 @@ def save(self, *args, **kwargs): raise XLSFormError(_('In strict mode, the XForm ID must be a ' 'valid slug and contain no spaces.')) - if not self.sms_id_string: - try: - # try to guess the form's wanted sms_id_string - # from it's json rep (from XLSForm) - # otherwise, use id_string to ensure uniqueness - self.sms_id_string = json.loads(self.json).get('sms_keyword', - self.id_string) - except: - self.sms_id_string = self.id_string + # `sms_id_string` and `allows_sms` are deprecated fields. + # We keep them to avoid altering `logger_xform` which can be really big + # to avoid a long downtime during a migration. + if not self.pk or not self.sms_id_string: + self.sms_id_string = self.id_string + self.allows_sms = False super(XForm, self).save(*args, **kwargs) @@ -248,7 +236,8 @@ def time_of_last_submission(self): def time_of_last_submission_update(self): try: - # we also consider deleted instances in this case + # We don't need to filter on `deleted_at` field anymore. + # Instances are really deleted and not flagged as deleted. return self.instances.latest("date_modified").date_modified except ObjectDoesNotExist: pass @@ -271,7 +260,7 @@ def public_forms(cls): def _xls_file_io(self): """ - pulls the xls file from remote storage + Pulls the xls file from remote storage this should be used sparingly """ @@ -285,35 +274,6 @@ def _xls_file_io(self): else: return StringIO(ff.read()) - def to_kpi_content_schema(self): - """ - Parses xlsform structure into json representation - of spreadsheet structure. - """ - if not xls_to_dicts: - raise ImportError('formpack module needed') - content = xls_to_dicts(self._xls_file_io()) - # a temporary fix to the problem of list_name alias - return json.loads(re.sub('list name', 'list_name', - json.dumps(content, indent=4))) - - def to_xlsform(self): - """ - Generate an XLS format XLSForm copy of this form. - """ - file_path = self.xls.name - default_storage = get_storage_class()() - - if file_path != '' and default_storage.exists(file_path): - with default_storage.open(file_path) as xlsform_file: - if file_path.endswith('.csv'): - xlsform_io = convert_csv_to_xls(xlsform_file.read()) - else: - xlsform_io= io.BytesIO(xlsform_file.read()) - return xlsform_io - else: - return None - @property def settings(self): """ diff --git a/onadata/apps/logger/templates/list_xforms.html b/onadata/apps/logger/templates/list_xforms.html deleted file mode 100644 index 237c45350..000000000 --- a/onadata/apps/logger/templates/list_xforms.html +++ /dev/null @@ -1,42 +0,0 @@ -{% load i18n %} - - - - - - - - - - - - - {% for xform in xforms %} - - - - - - - - {% endfor %} - -
{% trans "Downloadable by Phone" %}{% trans "Form ID" %}{% trans "Number of Submissions" %}{% trans "Time of Last Submission" %}{% trans "CSV" %}
- - {% if xform.downloadable %} - [ {% trans 'yes' %} ] - {% else %} - [ {% trans 'no' %} ] - {% endif %} - - {{ xform.id_string }}{{ xform.submission_count }} - {% if xform.time_of_last_submission %} - {{ xform.time_of_last_submission }} - {% endif %} - {% trans "Download CSV" %}
- - diff --git a/onadata/apps/logger/templates/submission.html b/onadata/apps/logger/templates/submission.html index e95388b3c..478f3543e 100644 --- a/onadata/apps/logger/templates/submission.html +++ b/onadata/apps/logger/templates/submission.html @@ -3,7 +3,6 @@

{% trans "Your submission was successful!" %}

- - - - - - - - - - - -{% endblock %} diff --git a/onadata/apps/main/templates/dashboard.html b/onadata/apps/main/templates/dashboard.html deleted file mode 100644 index d4296c2df..000000000 --- a/onadata/apps/main/templates/dashboard.html +++ /dev/null @@ -1,110 +0,0 @@ -{% load humanize %} - -{% block content %} - -{% load i18n %} - -
-
-
-

{% blocktrans %}Publish a Form Upload XLSForm{% endblocktrans %}

- {% url "tutorial" as tutorial_url %} - {% blocktrans %}For a quick introduction on how to publish a form try do download tutorial.xls and publish it.{% endblocktrans %} -
-
- {% url "syntax" as syntax_url %} - {% blocktrans %}Learn about the XLSForm syntax here along with more form examples.{% endblocktrans %} -
-
-
- {% csrf_token %} -
- -
-
- - {{ form.as_table }} -
-
-
- - {{ form_url.as_table }} -
-
-
- -
-
-
-
-
- -
-
-
-
-
-   -
-
-
-
-
- {% if not profile.gravatar_exists %} - - {% endif %} - gravatar - {% if not profile.gravatar_exists %} - - {% endif %} -
-
-

- {{ content_user.username }} - {% if profile.name %} - | {{ profile.name }} - {% endif %} -

- {% trans "View Profile Page" %} -
-
-
-
-

{{ all_forms|intcomma }}

-
-
-

{{ num_forms|intcomma }}

-
-
-

{{ user_instances|intcomma }}

-
-
-
-
- {% trans "FORMS" %} -
-
- {% trans "SHARED FORMS" %} -
-
- {% trans "SUBMISSIONS" %} -
-
-
-
-
-
-{% include "published_surveys.html" %} -

-{% blocktrans with url=url %} -The url "of" this web application {{ url }} -must be given to ODK Collect before it will get forms from and -submit data to ona. In Collect's Main Menu, press the Menu -button. Select Server Preferences, then Server. Enter -{{ url }} as the server. {% endblocktrans %} -

-{% endblock %} diff --git a/onadata/apps/main/templates/eula.html b/onadata/apps/main/templates/eula.html deleted file mode 100644 index 45da07035..000000000 --- a/onadata/apps/main/templates/eula.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load i18n %} - -
-

Ona End User Agreement

- - -
diff --git a/onadata/apps/main/templates/faq.html b/onadata/apps/main/templates/faq.html deleted file mode 100644 index 9bb3ae267..000000000 --- a/onadata/apps/main/templates/faq.html +++ /dev/null @@ -1,9 +0,0 @@ -{% load i18n %} - -{% block content %} - -
- -
- -{% endblock %} diff --git a/onadata/apps/main/templates/getting_started.html b/onadata/apps/main/templates/getting_started.html deleted file mode 100644 index 8c43f8fa3..000000000 --- a/onadata/apps/main/templates/getting_started.html +++ /dev/null @@ -1,147 +0,0 @@ -{% load i18n %} - -
- - -

{% blocktrans %}ona aims to make collecting survey data with Android smart phones easy and free.{% endblocktrans %} -

-

- {% trans "One uses ona as follows:" %} -

    -
  1. {% trans "Translate survey into the xsl2xform syntax." %}
  2. -
  3. {% trans "Upload XLSForm file to ona." %}
  4. -
  5. {% trans "Use" %} ODK Collect {% trans "to download form and submit data." %}
  6. -
  7. {% trans "Use" %} ona {% trans "to download and map data submissions." %}
  8. -
- {% trans "This tutorial will walk you through each stop of the way." %} -

-
- -
- - -
-
-

- {% blocktrans %}The first step in collecting survey data is to actually write the survey. For this tutorial we will be using the following survey:{% endblocktrans %} -

    -
  1. {% trans "What is your name?" %}
  2. -
  3. {% trans "How old are you?" %}
  4. -
  5. {% trans "May I take your picture?" %}
  6. -
  7. {% trans "Do you have any children?" %}
  8. -
  9. {% trans "Record your GPS coordinates." %}
  10. -
  11. {% trans "What web browsers do you use?" %}
  12. -
- {% blocktrans %}The second step is writing this survey in the xls2xform syntax. The idea behind this approach is that each question in the survey is a row in the spreadsheet. By using a spreadsheet we can expose most of the XForm features supported by ODK Collect. Check out the images to the right that show the xls2xform syntax for this survey.{% endblocktrans %} -

-
-
- survey sheet
- choices sheet
-
-
- {% url "onadata.apps.main.views.syntax" as syntax_url %} - {% blocktrans %}For more in depth information about the XLSForm syntax go here.{% endblocktrans %} -
- -
- - -
-
-

- {% blocktrans %}After creating an account and signing into the site you can publish forms for use with ODK Collect. On the right, we submit tutorial.xls, described above, for publication.{% endblocktrans %} -

-
-
- publish survey -
-
-
-
-

- {% blocktrans %}After publishing our first form, a new table appears showing our new form. Next we need to use ODK Collect to submit data for this form...{% endblocktrans %} -

-
-
- published surveys -
-
-
- -
- - -
-
- -

- {% blocktrans %}The url "of" this web application must be given to ODK Collect before it will get forms from and submit data to ona. In ODK Collect's Main Menu, press the Menu button. Select Server Preferences, then Server. Enter {{ url }} as the server.{% endblocktrans %} -

-
- -
- -

- {% trans "Download your published form to your phone." %} -

-
- -
- -

- {% trans "Fill out the form on your phone." %} -

-
- -
- -

- {% blocktrans %}After filling out the form on your phone submit the data to the server.{% endblocktrans %} -

-
-
-
- -
- - -
-
-

- {% blocktrans %}After submitting data from ODK Collect to the server you can now download the data as a nicely formatted csv or xls file.{% endblocktrans %} -

-
-
- submitted data -
-
-
-
-

- {% trans "And look at submissions on a map:" %} -

-
-
- map
-
-
-
diff --git a/onadata/apps/main/templates/google_doc.html b/onadata/apps/main/templates/google_doc.html deleted file mode 100644 index 44b8b4adc..000000000 --- a/onadata/apps/main/templates/google_doc.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
{{ content|safe }}
-
-
- diff --git a/onadata/apps/main/templates/home.html b/onadata/apps/main/templates/home.html deleted file mode 100644 index b36579c17..000000000 --- a/onadata/apps/main/templates/home.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends 'ona_base.html' %} -{% load humanize %} -{% load i18n %} - -{% block styles %} - {{block.super}} - -{% endblock %} - -{% block content %} -{% if SITE_READ_ONLY%} -

{% blocktrans %}We're down for maintenance. We will be back shortly.{% endblocktrans %}

-{% endif %} -
-

Collect, Analyze and Share Data. -

-

Use mobile phones and the web to quickly collect, analyze and share data securely. Whether you are mapping, monitoring or enumerating - Ona is your trusted solution.

-
-
-
- -
-
- {% url "auth_login" as login_url %} -
- -
- -
- -
-

Or if you want to create an account, please

- {% url "registration_register" as registration_url %} - {% blocktrans %} - Sign up - {% endblocktrans %} - {% csrf_token %} - -
- {% url "auth_password_reset" as reset_url %} -

{% blocktrans %}Can't access your account? Reset your password.{% endblocktrans %}

-
-
- {% endblock %} diff --git a/onadata/apps/main/templates/people.html b/onadata/apps/main/templates/people.html deleted file mode 100644 index 3024a2c77..000000000 --- a/onadata/apps/main/templates/people.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends 'base.html' %} -{% load humanize %} -{% block content %} -{% load i18n %} - - - - - - - - - - - - - - - {% for user in users %} - - {% url "user_profile" user.username as profile_url %} - - - - - - - {% endfor %} - -
{% trans "User" %}{% trans "Name" %}{% trans "Organization" %}{% trans "Location" %}{% trans "Member Since" %}
gravatar  {{ user.username }}{{ user.profile.name }}{{ user.profile.organization }}{% if user.profile.city %}{{ user.profile.city }},{% endif %} {% if user.profile.country %}{{user.profile.country}}{% endif %}{{ user.date_joined|date:"F j, Y" }}
-
-{% endblock %} - -{% block javascript %} -{{ block.super }} - - - - -{% endblock %} diff --git a/onadata/apps/main/templates/privacy.html b/onadata/apps/main/templates/privacy.html deleted file mode 100644 index 4fb4943cb..000000000 --- a/onadata/apps/main/templates/privacy.html +++ /dev/null @@ -1,37 +0,0 @@ -{% load i18n %} - -
-

Ona Privacy Policy

-We take your privacy seriously. At Ona we collect and manage user data according to the following Privacy Policy. - -

Data Collected

- -We collect anonymous data from every visitor of the Website to monitor traffic and fix bugs. This includes information on web requests including IP addresses of site visitors, browser type and time of visit. When registering an account, we ask you to provide certain personal information such as your name and email to create your user profile. We will not share your personal information with third parties. Your password, which you will use to access the site, is stored encrypted and thus never accessible to Ona. Lost passwords can therefore only be reset and not retrieved. -

-You are able to view, change and remove your data associated with your profile. If you wish to delete your account please contact us at support@ona.io, and we will assist you in this request as soon as possible. -

-Minors and children under the age of 13 should not use any services offered by Ona. By using the Website, you represent that you are above the age of 13 and have the legal capacity to enter into a binding agreement. - -

User Data

- -We only use your personal information to provide you with Ona services or to communicate with you about our services or the Website.

- -We take the privacy, confidentiality and security of any data you submit or upload to Ona very seriously, because it is your data. You retain ownership of the data. We will never share or access your data, unless granted permission by you, in order to provide you with technical assistance. -

-In order to comply with the law, we may release account and other personal information to comply on issues related to national security or to protect the rights of Ona and its users. - -

Protecting Your Data

- -Ona is committed to protecting the data you entrust to us. We employ industry standard best practices (both technical and administrative) to protect against unauthorized access of your data. We cannot guarantee, however, its absolute security. To protect from loss of data, we do frequent system and incremental backups which are stored encrypted in data centers across different geographic zones. -

-To further protect your data, we encourage you never to share your login information and to change your passwords regularly. -

-If you have any questions regarding our security and backup procedures please contact us at support@ona.io. - -

Changes to the Privacy Policy

- -We reserve the right to modify this privacy statement at any time, so please review it frequently. If we make material changes to this policy, we will notify you here or by means of a notice on our homepage so that you are aware of any changes with relation to what information we collect, how we use it, and under what circumstances, if any, we disclose it. -

-Should you have any questions or concerns about our Privacy Policy please contact us as support@ona.io. - -
diff --git a/onadata/apps/main/templates/profile.html b/onadata/apps/main/templates/profile.html deleted file mode 100644 index 84ec8d963..000000000 --- a/onadata/apps/main/templates/profile.html +++ /dev/null @@ -1,203 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -{% block content %} - {% if show_dashboard %} - {% include "dashboard.html" %} - {% else %} - -
- - {{ profile.description }} -
-
- {% if profile.organization %} -

{% trans "Organization:" %} {{ profile.organization}}

- {% endif %} - {% if location %} -

{% trans "Location:" %} {{ location }}

- {% endif %} - {% if profile.home_page %} -

{% trans "Website:" %}{{ profile.home_page }}

- {% endif %} - {% with twitter=profile.twitter_clean %} - {% if twitter %} -

{% trans "Twitter:" %}@{{ twitter }}

- {% endif %} - {% endwith %} -

{% trans "Member since:" %} {{ content_user.date_joined|date:"F j, Y" }}

-
-
-

- - {% blocktrans count num_forms=num_forms %}{{ num_forms }} Shared form{% plural %}{{ num_forms }} Shared forms{% endblocktrans %} - -

-
-
-
- -
-
-

{% blocktrans %}Shared Forms & Public Data ({{ num_forms }}){% endblocktrans %}

- {% if forms %} - - - - - - - - - - - - {% for form in forms %} - - - - - - - - {% endfor %} - -
{% trans "Name" %}{% trans "Create Date" %}{% trans "XLSForm" %}{% trans "Public Data" %}{% trans "View" %}
- - {{ form.title }} - - {{ form.date_created|date:"N d, Y" }} - {% if form.shared %} - - - - {% else %} - {% trans "PRIVATE" %} - {% endif %} - - {% if form.shared_data %} - {% if form.submission_count %} -
- - - - - {% trans "Download" %} - {% if xform.shared_data %} - {% trans "PUBLIC" %} - {% endif %} - - -
- {% else %} - {% trans "NO DATA" %} - {% endif %} - {% else %} - {% trans "PRIVATE" %} - {% endif %} -
- {% if form.shared_data %} - {% if form.data_dictionary.has_instances_with_geopoints %} - - - - {% endif %} - {% else %} - {% trans "PRIVATE" %} - {% endif %} -
- {% else %} -

{% trans "None public" %}

- {% endif %} -
-
- {% endif %} - {% endblock %} - {% block javascript %} - {{ block.super }} - - - - - - -{% endblock %} diff --git a/onadata/apps/main/templates/published_surveys.html b/onadata/apps/main/templates/published_surveys.html deleted file mode 100644 index aa705045b..000000000 --- a/onadata/apps/main/templates/published_surveys.html +++ /dev/null @@ -1,180 +0,0 @@ -{% load humanize %} -{% load i18n %} -{% if all_forms or forms_shared_with %} - -{% if message and message.preview_url %} - -{% endif %} - -{% for xform_list in xforms_list %} -
- -{% if xform_list.xforms %} - - - - - - - - - - - - - - - {% for xform in xform_list.xforms %} - {% with submission_count=xform.submission_count time_of_last_submission=xform.time_of_last_submission has_instances_with_geopoints=xform.has_instances_with_geopoints %} - - - - - - - - - - - {% endwith %} - {% endfor %} - -
{% trans "Name" %}{% trans "Submissions" %}{% trans "Enter Data" %}{% trans "View" %}{% trans "Download" %}{% trans "Last Submission" %}{% trans "Active" %} 
- {{ xform.title }} {% if xform_list.id == 'shared'%}{% trans "Shared by" %} {{ xform.user.username }} - {% endif %} - {% if xform.shared %} - {% trans "PUBLIC" %} - {% endif %} - {% if xform.encrypted %} -   {% trans "ENCRYPTED" %} - {% endif %} -
CREATED: {{ xform.date_created|date:"N d, Y" }}
-
{{ submission_count|intcomma }} - {% trans "Web" %} - - {% if not xform.encrypted and has_instances_with_geopoints %} - - - - {% else %} - - {% endif %} - {% if not xform.encrypted and submission_count > 0 %} - - - - - - - - - - - - {% else %} - - - - {% endif %} - - {% if not xform.encrypted and submission_count %} - csv - xls - {% if has_instances_with_geopoints %} - kml - {% endif %} - {% if xform.shared_data %} - {% trans "PUBLIC" %} - {% endif %} - {% if xform.encrypted %} -   {% trans "ENCRYPTED" %} - {% endif %} - {% endif %} - - {% if time_of_last_submission %} - {{ time_of_last_submission|date:"N d, Y" }} - {% endif %} - {{ xform.downloadable|lower }} - {% if submission_count <= 0 %} - - {% endif %} - {% if submission_count <= 0 %} - - {% endif %} - {% if xform.can_be_replaced and xform.user == user %} -
- -
- {% endif %} -
-
-
- {% endif %} - {% endfor %} - - {% endif %} - diff --git a/onadata/apps/main/templates/resources.html b/onadata/apps/main/templates/resources.html deleted file mode 100644 index b09ec9f7f..000000000 --- a/onadata/apps/main/templates/resources.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block content %} - - -{% endblock %} diff --git a/onadata/apps/main/templates/settings.html b/onadata/apps/main/templates/settings.html deleted file mode 100644 index fdeba982d..000000000 --- a/onadata/apps/main/templates/settings.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} - -{% block content %} -
- - {% comment %} - Passwords should *only* be changed from KPI now! - - {% endcomment %} - -
-
-
- {% csrf_token %} - - - - - - - {% for field in form %} - - - - - {% endfor %} -
{{ content_user.username }}
- {{ field.label_tag }} - - {{ field.errors }} - {{ field }} - {% if field.field.required %}*{% endif %} -
- -
-
-
-
-{% endblock %} diff --git a/onadata/apps/main/templates/show.html b/onadata/apps/main/templates/show.html deleted file mode 100644 index 2ddabfccc..000000000 --- a/onadata/apps/main/templates/show.html +++ /dev/null @@ -1,635 +0,0 @@ -{% extends 'base.html' %} -{% load humanize %} -{% block content %} -{% load i18n %} - -
- -
-
- {% if can_edit %} -

{% trans "Form ID:" %} {{ xform.id_string }}

- {% endif %} -

{% trans "Description" %}

-
-
-
- {% csrf_token %} - - {% if can_edit %} - {% trans "edit" %} - - {% endif %} -
-
-
-
-

- {% trans "XLSForm" %} - {% trans "PUBLIC" %} - {% trans "PRIVATE" %} -   {% trans "ENCRYPTED" %} -

-
-
-
- {% trans "XLSForm" %} - - -
-
- {% trans "XForm" %} - - -
-
- {% trans "JSONForm" %} - - -
-
-
- -

- {% trans "License" %}: - {% if can_edit %} - {% for field in form_license_form %} - {{ field }} - {% endfor %} - {% trans "edit" %} - - {% else %} - {{ form_license|default:'No license' }} - {% endif %} -

-

-
-
    - {% if user.is_authenticated and content_user.username != user.username and xform.xls|length > 0 and not cloned %} -

    -
  • - {% trans "Clone" %} -
  • - {% endif %} -
-
-
-
-

- {% trans "Form Data" %} - {% trans "PUBLIC" %} - {% trans "PRIVATE" %} -   {% trans "ENCRYPTED" %} -

- {% if xform.shared_data or can_view %} - {% if not xform.encrypted and xform.submission_count > 0 %} -
-
-
- {% trans "csv" %} - - -
-
- {% trans "csv zip" %} - - -
-
- {% trans "sav zip" %} - - -
-
- {% trans "xls" %} - - -
-
- {% trans "kml" %} - - -
- -
- {% trans "zip" %} - - -
-
- {% trans "gdocs" %} - - -
-
-

{% trans "Submissions:" %} {{ xform.submission_count|intcomma }}

-
-

- {% trans "License:" %} - {% if can_edit %} - {% for field in data_license_form %} - {{ field }} - {% endfor %} - {% trans "edit" %} - - {% else %} - {{ data_license|default:'No license' }} - {% endif %} -

-

- - {% else %} - {% if can_edit %} - - {% else %} - {% trans "NO DATA" %} - {% endif %} - {% endif %} - {% endif %} - -
- - -
- {% if can_edit %} - - {% trans "Enter Web Form" %} - - {% trans "Mobile" %} - {% endif %} - {% if can_edit or xform.shared %} - - {% trans "Preview Form" %} - - {% endif %} - -
- {% if can_edit or xform.shared %} - - {% endif %} -
-
-
-
-
-
-
-

{% trans "Source" %}

-
-
- - {% if source.data_file %} -

{{ source.data_file.name }}

- {% endif %} - {% csrf_token %} - - {% if can_edit %} - {% trans "edit" %} - - {% endif %} - - - {% if can_edit %} - {% csrf_token %} - - {{ source_form.as_table }} -
- - {% endif %} -
-
-
-
-
-

{% trans "Methodology" %}

-
- {% for doc in supporting_docs %} -

{{ doc.data_value }} - {% if can_edit %} - || {% trans "remove" %} - {% endif %} -

- {% endfor %} - {% if can_edit %} -
- {% csrf_token %} - - {{ doc_form.as_table }} -
- -
- {% endif %} -
-
- {% if is_owner %} -
-
-

{% trans "Rest Services" %}

-
-
-
- {% endif %} -
-
-

{% blocktrans %}Media  jpeg/png/mp3/wav/3gp/csv/zip allowed{% endblocktrans %}

-
- {% for media in media_upload %} -

{{ media.data_value }} - {% if can_edit %} - – {% trans "remove" %} - {% endif %} -

- {% endfor %} - {% if can_edit %} -
- {% csrf_token %} - -
-
- - - - - - - -
- -
-
- - - - - - - -
- -
-
-
- {% endif %} -
-
-
-
-

{% trans "SMS Support" %} (beta)

-
-
- - {% if xform.allows_sms %} -

{% blocktrans %}This form has enabled SMS submissions.{% endblocktrans %} {% blocktrans with id_string=xform.sms_id_string %}Submit to this form using the “{{ id_string }}” identifier.{% endblocktrans %}

- {% endif %} - {% if can_edit %} - {% csrf_token %} - - - {{ sms_support_form.as_table }} -
- - {% endif %} -
- {% autoescape off %} -
- {% if xform.allows_sms %} -

{% trans "Expected Format:" %}

-

{{ sms_support_doc.line_names }}

-
    - {% for helper in sms_support_doc.helpers %} -
  • {{ forloop.counter0 }} {{ helper.0 }}: {{helper.1}}
  • - {% endfor %} -
-

{% trans "Example" %}

-

{{ sms_support_doc.line_values }}

- {% else %} -

{{ sms_compatible.text }}

- {% endif %} -

{% trans "Usage" %}

-

Note that for each case, you can select either the simplest form of the URL (containing only your username) or the one including the id_string of the form.

-

The latter works without specifyin the SMS Keyword in front of every message.

-

You would use it when your SMS Setup/Provider is used for only one form.

- - - {% for provider in sms_providers_doc %}{% endfor %} - {% endautoescape %} -
-
-
-
-
- {% if is_owner %} -

{% trans "Settings" %}

-
-
- - -
-
- -
- -
-
- -
- -
-
- -
- - - - {% trans "Delete" %} - -
-
-
-
-

{% trans "Sharing" %}

- {% if public_link %} - {% url "onadata.apps.main.views.show" xform.uuid as form_url %} - {% blocktrans %}Sharing by link is on with URL {{ base_url }}{{ form_url }}{% endblocktrans %} - {% else %} - {% blocktrans %}Sharing by link is off{% endblocktrans %} - {% endif %} - -
-
-
- {% csrf_token %} - - - -
- {% if users_with_perms|length %} -
    - {% for user, perms in users_with_perms %} -
  • {{ user }}: {{ perms }}
  • - {% endfor %} -
- {% endif %} -
- {% csrf_token %} - - {{ permission_form.as_table }} -
- -
-
- {% endif %} -
-
-
-
- - -{% endblock %} - -{% block styles %} -{{ block.super }} - -{% endblock %} - -{% block javascript %} -{{ block.super }} - - - {% if user.is_authenticated %} - - {% endif %} - - -{% endblock %} diff --git a/onadata/apps/main/templates/show_form_settings.html b/onadata/apps/main/templates/show_form_settings.html deleted file mode 100644 index 3d1196eb1..000000000 --- a/onadata/apps/main/templates/show_form_settings.html +++ /dev/null @@ -1,702 +0,0 @@ -{% extends 'base.html' %} -{% load humanize %} -{% block content %} -{% load i18n %} - -
- -
-
- {% if can_edit %} -

{% trans "Form ID:" %} {{ xform.id_string }}

- {% endif %} -

{% trans "Description" %}

-
-
-
- {% csrf_token %} - - {% if can_edit %} - {% trans "edit" %} - - {% endif %} -
-
-
-
-

- {% trans "XLSForm" %} - {% trans "PUBLIC" %} - {% trans "PRIVATE" %} -   {% trans "ENCRYPTED" %} -

-
-
-
- {% trans "XLSForm" %} - - -
-
- {% trans "XForm" %} - - -
-
- {% trans "JSONForm" %} - - -
-
-
- -

- {% trans "License" %}: - {% if can_edit %} - {% for field in form_license_form %} - {{ field }} - {% endfor %} - {% trans "edit" %} - - {% else %} - {{ form_license|default:'No license' }} - {% endif %} -

-

-
-
    - {% if user.is_authenticated and content_user.username != user.username and xform.xls|length > 0 and not cloned %} -

    -
  • - {% trans "Clone" %} -
  • - {% endif %} -
-
-
-
-

- {% trans "Form Data" %} - {% trans "PUBLIC" %} - {% trans "PRIVATE" %} -   {% trans "ENCRYPTED" %} -

- {% if xform.shared_data or can_view %} - {% if not xform.encrypted and xform.submission_count > 0 %} -
-
-
- {% trans "csv" %} - - -
-
- {% trans "csv zip" %} - - -
-
- {% trans "sav zip" %} - - -
-
- {% trans "xls" %} - - -
-
- {% trans "kml" %} - - -
- -
- {% trans "zip" %} - - -
-
- {% trans "gdocs" %} - - -
-
-
- {% trans "xls reports" %} - - -
-
-

{% trans "Submissions:" %} {{ xform.submission_count|intcomma }}

-
-

- {% trans "License:" %} - {% if can_edit %} - {% for field in data_license_form %} - {{ field }} - {% endfor %} - {% trans "edit" %} - - {% else %} - {{ data_license|default:'No license' }} - {% endif %} -

-

- - {% else %} - {% if can_edit %} - - {% else %} - {% trans "NO DATA" %} - {% endif %} - {% endif %} - {% endif %} - -
- - -
- {% if can_edit %} - - {% trans "Enter Web Form" %} - - {% trans "Mobile" %} - {% endif %} - {% if can_edit or xform.shared %} - - {% trans "Preview Form" %} - - {% endif %} - -
- {% if can_edit or xform.shared %} - - {% endif %} -
-
-
-
-
-
-
-

{% trans "Source" %}

-
-
- - {% if source.data_file %} -

{{ source.data_file.name }}

- {% endif %} - {% csrf_token %} - - {% if can_edit %} - {% trans "edit" %} - - {% endif %} - - - {% if can_edit %} - {% csrf_token %} - - {{ source_form.as_table }} -
- - {% endif %} -
-
-
-
-
-

{% trans "Methodology" %}

-
- {% for doc in supporting_docs %} -

{{ doc.data_value }} - {% if can_edit %} - || {% trans "remove" %} - {% endif %} -

- {% endfor %} - {% if can_edit %} -
- {% csrf_token %} - - {{ doc_form.as_table }} -
- -
- {% endif %} -
-
- {% if is_owner %} -
-
-

{% trans "Rest Services" %}

-
-
-
- {% endif %} -
-
-

{% blocktrans %}Media  jpeg/png/mp3/wav/3gp/csv/zip allowed{% endblocktrans %}

-
- {% for media in media_upload %} -

{{ media.data_value }} - {% if can_edit %} - – {% trans "remove" %} - {% endif %} -

- {% endfor %} - {% if can_edit %} -
- {% csrf_token %} - -
-
- - - - - - - -
- -
-
- - - - - - - -
- -
-
-
- {% endif %} -
-
-
-
-

{% trans "XLS Reports (beta)" %}

-
- {% if can_edit %} -
- {% csrf_token %} - -
-
- - {{ external_export_form.as_table }} -
- -
-
- - - - - - - - - - - - - - - -
- -
-
- - {% for meta in external_export %} - - - {% if can_edit %} - - {% endif %} - - {% endfor %} -
{{ meta.external_export_name }} {% trans "Delete" %}
-
-
-
- {% endif %} -
-
-
-
-

{% trans "SMS Support" %} (beta)

-
-
- - {% if xform.allows_sms %} -

{% blocktrans %}This form has enabled SMS submissions.{% endblocktrans %} {% blocktrans with id_string=xform.sms_id_string %}Submit to this form using the “{{ id_string }}” identifier.{% endblocktrans %}

- {% endif %} - {% if can_edit %} - {% csrf_token %} - - - {{ sms_support_form.as_table }} -
- - {% endif %} -
- {% autoescape off %} -
- {% if xform.allows_sms %} -

{% trans "Expected Format:" %}

-

{{ sms_support_doc.line_names }}

-
    - {% for helper in sms_support_doc.helpers %} -
  • {{ forloop.counter0 }} {{ helper.0 }}: {{helper.1}}
  • - {% endfor %} -
-

{% trans "Example" %}

-

{{ sms_support_doc.line_values }}

- {% else %} -

{{ sms_compatible.text }}

- {% endif %} -

{% trans "Usage" %}

-

Note that for each case, you can select either the simplest form of the URL (containing only your username) or the one including the id_string of the form.

-

The latter works without specifyin the SMS Keyword in front of every message.

-

You would use it when your SMS Setup/Provider is used for only one form.

- - - {% for provider in sms_providers_doc %}{% endfor %} - {% endautoescape %} -
-
-
-
-
- {% if is_owner %} -

{% trans "Settings" %}

-
-
- - -
-
- -
- -
-
- -
- -
-
- -
- - - - {% trans "Delete" %} - -
-
-
-
-

{% trans "Sharing" %}

- {% if public_link %} - {% url "onadata.apps.main.views.show" xform.uuid as form_url %} - {% blocktrans %}Sharing by link is on with URL {{ base_url }}{{ form_url }}{% endblocktrans %} - {% else %} - {% blocktrans %}Sharing by link is off{% endblocktrans %} - {% endif %} - -
-
-
- {% csrf_token %} - - - -
- {% if users_with_perms|length %} -
    - {% for user, perms in users_with_perms %} -
  • {{ user }}: {{ perms }}
  • - {% endfor %} -
- {% endif %} -
- {% csrf_token %} - - {{ permission_form.as_table }} -
- -
-
- {% endif %} -
-
-
-
- - -{% endblock %} - -{% block styles %} -{{ block.super }} - -{% endblock %} - -{% block javascript %} -{{ block.super }} - - - {% if user.is_authenticated %} - - {% endif %} - - -{% endblock %} diff --git a/onadata/apps/main/templates/support.html b/onadata/apps/main/templates/support.html deleted file mode 100644 index a5fa5a369..000000000 --- a/onadata/apps/main/templates/support.html +++ /dev/null @@ -1,22 +0,0 @@ -{% load i18n %} - -
-

Support

- -

Form Authoring with XLSForms

-Ona uses XLSForm standard to allow you to author forms in XLS. -

-XLSForm is a form standard created to help simplify the authoring of forms using Excel. Authoring is done in a human readable format using a familiar tool that almost everyone knows - Excel. XLSForms provide a practical standard for sharing and collaborating on authoring forms. They are simple to get started with but allow for the authoring of complex forms by someone familiar with the syntax. -

-For a full reference please use: XLSForm.org. Also, be sure to checkout the form gallery which provides examples you can download and modify. - -

Support

-If you encounter any problems using the Ona service please post a message to the Ona community mailing list. You can also contact our support team at: support@ona.io. - -

API Usage

-Ona provides a robust REST API that allow integration with other tools. We also use Ona's APIs extensively to power custom dashboards for our clients. We see a lot of potential as Ona as a data platform service. - -Please checkout the Ona API Docs. If you have any comments, suggestions or questions about the API use please email us at: platform@ona.io. - - -
diff --git a/onadata/apps/main/templates/syntax.html b/onadata/apps/main/templates/syntax.html deleted file mode 100644 index 92ad7c30c..000000000 --- a/onadata/apps/main/templates/syntax.html +++ /dev/null @@ -1,15 +0,0 @@ -{% load i18n %} - -
- -

Form Authoring with XLSForms

- -Ona uses XLSForm standard to allow you to author forms in XLS. -

-XLSForm is a form standard created to help simplify the authoring of forms using Excel. Authoring is done in a human readable format using a familiar tool that almost everyone knows - Excel. XLSForms provide a practical standard for sharing and collaborating on authoring forms. They are simple to get started with but allow for the authoring of complex forms by someone familiar with the syntax. -

-For a full reference please use: XLSForm.org. -

Also, be sure to checkout the form gallery which provides examples you can download and modify. - - -
diff --git a/onadata/apps/main/templates/tos.html b/onadata/apps/main/templates/tos.html deleted file mode 100644 index beab179eb..000000000 --- a/onadata/apps/main/templates/tos.html +++ /dev/null @@ -1,57 +0,0 @@ -{% load i18n %} - -
-

Ona Terms of Service

-

- Welcome to Ona.io, the website and service (“the Service”), owned and operated by Ona Systems, LLC (“Ona”). This page explains the terms in which you may use the service for data collection, management and analysis purposes. By using the Site or any services provided on the Site (collectively, “Services”), you acknowledge that you have read, understood, and agree to be bound by the following terms of service and any future modifications to this agreement (collectively, the “Terms”). The Terms apply to all visitors, authorized users, and others who access the Service ("Users"). Your use of the Service indicates that you accept these Terms and to the collection and use of your information as set forth in Ona’s Privacy Policy. If you do not accept these Terms, do not use the Service. -

- -

Using Ona

-

-You must register a user account to access the Ona service. Your account grants you permission to use the Site and Services. You must adhere to all policies posted within the Services. As a condition of your use of the Services, you warrant to Ona that you will not use the Services for any purpose that is unlawful or prohibited by these terms, conditions, or notices. You may not use the Services in any manner which could damage, disable, overburden, or impair the Services or interfere with any other party’s use and enjoyment of the Services. -

- -

Your Account

- -

-You acknowledge that you are responsible for all activity occurring under the use of your account. You are responsible for maintaining the security of your account and password. Ona will not be liable for any loss or damage that results from your failure to do so. You are solely responsible for all your account activity when using this service. -

-

-Ona makes no representation that usage of the Services or any related content are appropriate or available for use in your country. You acknowledge and agree that you are responsible for ensuring that your use of this service complies with all relevant local and international laws. -

- -

Your Data

- -

-You are responsible for any data or content uploaded to the service and retain full ownership of it. In using this service, you understand the service allows you to share your data with others and acknowledge that Ona will not be responsible for how your data is used by others if is shared or made public. If you leave this service, we will make all reasonable efforts to remove your data. - -

-

-We take the privacy and security of your data seriously and will make all reasonable efforts to ensure it as defined in our Privacy Policy. We will never share your data with third parties. Our team will not view your data directly, unless granted permission by you, in order to provide you technical assistance. - -

-

-To enhance your use of the service, we may copy, cache or modify your data to improve the systems performance. - -

- -

API Usage

- -

-When using the API you must include your account’s unique API key. Unauthorized use of your API key, including transfer to another user, is prohibited. You may not use the API in any manner which could damage overburden or impair the ability of Ona to over the service to other users. In such an event, Ona reserves the right to suspend or limit your accounts access to the API. -

- -

Disclaimers and Limitation of Liability

-

-We disclaim all responsibility and liability for the availability, timeliness, security or reliability of the Services or Site, or any software provided through the Site. THE SITE AND THE SERVICES ARE PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR OTHERWISE. WITHOUT LIMITATION, WE DISCLAIM ANY AND ALL WARRANTIES REGARDING THE ACCURACY, SECURITY, RELIABILITY, TIMELINESS, AND PERFORMANCE OF ALL SERVICES ASSOCIATED WITH USE OF THE SITE AND THE SERVICES. WE MAKE NO WARRANTY, EXPRESS OR IMPLIED, THAT YOUR USE OF THE SITE AND THE SERVICES WILL BE UNINTERRUPTED, TIMELY, OR ERROR-FREE. We will not be responsible for any harm to your computer, loss of data or other harm that results from your use of the Services. -

-

-TO THE EXTENT NOT PROHIBITED BY APPLICABLE LAW, WE WILL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING FROM THE USE OF OR INABILITY TO USE THE SITE OR THE SERVICES. YOU EXPRESSLY AGREE THAT YOUR USE OF THE SITE AND THE SERVICES IS SOLELY AT YOUR OWN RISK. UNDER NO CIRCUMSTANCES SHALL WE BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, OR CONSEQUENTIAL DAMAGES, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, INCOME, BUSINESS OPPORTUNITIES, AND ANY OTHER INJURY REGARDLESS OF THE NATURE OF THE CLAIM OR THE FORM OF ACTION, ARISING OUT OF OR CONNECTED WITH THE SITE (INCLUDING BUT NOT LIMITED TO ITS OPERATION, ITS CONTENTS, OR THE INFORMATION OR MATERIALS CONTAINED THEREIN, OR THE USE OR INABILITY TO USE THE SERVICES OR ANY OTHER SITE LINKED TO THE SITE, OR ANY CONTENT CONTAINED IN ANY SUCH SITE) EVEN IF WE, OR OUR SUPPLIERS OR LICENSORS HAVE BEEN NOTIFIED OF THE POSSIBILITY OF ANY DAMAGES. -

- -

U.S. DIGITAL MILLENNIUM COPYRIGHT ACT.

-

-If Ona receives a notice alleging that material from your account infringes another party’s copyright, Ona may disable your user account or remove alleged material in accordance with Title II of the Digital Millennium Copyright Act of 1998 (Section 512 of the U.S. Copyright Act). -

- -
diff --git a/onadata/apps/main/templates/tutorial.html b/onadata/apps/main/templates/tutorial.html deleted file mode 100644 index a5d4800bc..000000000 --- a/onadata/apps/main/templates/tutorial.html +++ /dev/null @@ -1,147 +0,0 @@ -{% load i18n %} - -
- - -

{% blocktrans %}ona aims to make collecting survey data with Android smart phones easy and free.{% endblocktrans %} -

-

- {% trans "One uses ona as follows:" %} -

    -
  1. {% trans "Translate survey into the xsl2xform syntax." %}
  2. -
  3. {% trans "Upload XLSForm file to ona." %}
  4. -
  5. {% trans "Use" %} ODK Collect {% trans "to download form and submit data." %}
  6. -
  7. {% trans "Use" %} ona {% trans "to download and map data submissions." %}
  8. -
- {% trans "This tutorial will walk you through each stop of the way." %} -

-
- -
- - -
-
-

- {% blocktrans %}The first step in collecting survey data is to actually write the survey. For this tutorial we will be using the following survey:{% endblocktrans %} -

    -
  1. {% trans "What is your name?" %}
  2. -
  3. {% trans "How old are you?" %}
  4. -
  5. {% trans "May I take your picture?" %}
  6. -
  7. {% trans "Do you have any children?" %}
  8. -
  9. {% trans "Record your GPS coordinates." %}
  10. -
  11. {% trans "What web browsers do you use?" %}
  12. -
- {% blocktrans %}The second step is writing this survey in the xls2xform syntax. The idea behind this approach is that each question in the survey is a row in the spreadsheet. By using a spreadsheet we can expose most of the XForm features supported by ODK Collect. Check out the images to the right that show the xls2xform syntax for this survey.{% endblocktrans %} -

-
-
- Survey Sheet
- Choices Sheet
-
-
- {% url "onadata.apps.main.views.syntax" as syntax_url %} - {% blocktrans %}For more in depth information about the XLSForm syntax go here.{% endblocktrans %} -
- -
- - -
-
-

- {% blocktrans %}After creating an account and signing into the site you can publish forms for use with ODK Collect. On the right, we submit tutorial.xls, described above, for publication.{% endblocktrans %} -

-
-
- Publish Survey -
-
-
-
-

- {% blocktrans %}After publishing our first form, a new table appears showing our new form. Next we need to use ODK Collect to submit data for this form...{% endblocktrans %} -

-
-
- Published Surveys -
-
-
- -
- - -
-
- -

- {% blocktrans %}The url "of" this web application must be given to ODK Collect before it will get forms from and submit data to ona. In ODK Collect's Main Menu, press the Menu button. Select Server Preferences, then Server. Enter {{ url }} as the server.{% endblocktrans %} -

-
- -
- -

- {% trans "Download your published form to your phone." %} -

-
- -
- -

- {% trans "Fill out the form on your phone." %} -

-
- -
- -

- {% blocktrans %}After filling out the form on your phone submit the data to the server.{% endblocktrans %} -

-
-
-
- -
- - -
-
-

- {% blocktrans %}After submitting data from ODK Collect to the server you can now download the data as a nicely formatted csv or xls file.{% endblocktrans %} -

-
-
- Submitted Data -
-
-
-
-

- {% trans "And look at submissions on a map:" %} -

-
-
- Map
-
-
-
diff --git a/onadata/apps/main/templates/xls2xform.html b/onadata/apps/main/templates/xls2xform.html deleted file mode 100644 index 63d2a0d2a..000000000 --- a/onadata/apps/main/templates/xls2xform.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load i18n %} - -
- -

- {% trans "If you're looking for the XLS2XForm converter you're in the right place. We've incorporated the XLS2XForm converter into ona." %} -

-

- {% trans "To use it simply:" %} -

    - {% url "registration_register" as registration_register %} -
  • {% blocktrans %}Signup for a free account with ona{% endblocktrans %}
  • -
  • {% trans "Upload your XLSForm to ona and it'll instantly be converted to an XForm which will be available on your phone." %}
  • -
  • {% trans "If you want to download the XForm, click on the form name and click on other to download the converted XForm" %}
  • -
- -

-
- diff --git a/onadata/apps/main/tests/test_base.py b/onadata/apps/main/tests/test_base.py index bb778174c..bc2c4c53d 100644 --- a/onadata/apps/main/tests/test_base.py +++ b/onadata/apps/main/tests/test_base.py @@ -17,6 +17,7 @@ from django.utils import timezone from django_digest.test import Client as DigestClient from django_digest.test import DigestAuth +from rest_framework.reverse import reverse from rest_framework.test import APIRequestFactory from onadata.apps.logger.models import XForm, Instance, Attachment @@ -89,19 +90,23 @@ def _create_user_and_login(self, username="bob", password="bob"): self.anon = Client() def _publish_xls_file(self, path): + + xform_list_url = reverse('xform-list') + if not path.startswith('/%s/' % self.user.username): path = os.path.join(self.this_directory, path) with open(path) as xls_file: post_data = {'xls_file': xls_file} - return self.client.post('/%s/' % self.user.username, post_data) + response = self.client.post(xform_list_url, data=post_data) + return response def _publish_xlsx_file(self): path = os.path.join(self.this_directory, 'fixtures', 'exp.xlsx') pre_count = XForm.objects.count() response = TestBase._publish_xls_file(self, path) # make sure publishing the survey worked - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 201) self.assertEqual(XForm.objects.count(), pre_count + 1) def _publish_xls_file_and_set_xform(self, path): diff --git a/onadata/apps/main/tests/test_form_api.py b/onadata/apps/main/tests/test_form_api.py deleted file mode 100644 index 39cf28a84..000000000 --- a/onadata/apps/main/tests/test_form_api.py +++ /dev/null @@ -1,175 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import base64 -import json - -from django.core.urlresolvers import reverse - -from onadata.apps.main.views import api -from onadata.apps.api.mongo_helper import MongoHelper -from onadata.apps.viewer.models.parsed_instance import ParsedInstance -from .test_base import TestBase - - -def dict_for_mongo_without_userform_id(parsed_instance): - d = parsed_instance.to_dict_for_mongo() - # remove _userform_id since its not returned by the API - d.pop(ParsedInstance.USERFORM_ID) - return d - - -class TestFormAPI(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form_and_submit_instance() - self.api_url = reverse(api, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - - def test_api(self): - # query string - response = self.client.get(self.api_url, {}) - self.assertEqual(response.status_code, 200) - d = dict_for_mongo_without_userform_id( - self.xform.instances.all()[0].parsed_instance) - find_d = json.loads(response.content)[0] - self.assertEqual(find_d, d) - - def test_api_with_query(self): - # query string - query = '{"transport/available_transportation_types_to_referral_facil'\ - 'ity":"none"}' - data = {'query': query} - response = self.client.get(self.api_url, data) - self.assertEqual(response.status_code, 200) - d = dict_for_mongo_without_userform_id( - self.xform.instances.all()[0].parsed_instance) - find_d = json.loads(response.content)[0] - self.assertEqual(find_d, d) - - def test_api_query_no_records(self): - # query string - query = '{"available_transporation_types_to_referral_facility": "bicy'\ - 'cle"}' - data = {'query': query} - response = self.client.get(self.api_url, data) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, '[]') - - def test_handle_bad_json(self): - response = self.client.get(self.api_url, {'query': 'bad'}) - self.assertEqual(response.status_code, 400) - self.assertEqual(True, 'JSON' in response.content) - - def test_api_jsonp(self): - # query string - callback = 'jsonpCallback' - response = self.client.get(self.api_url, {'callback': callback}) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content.startswith(callback + '('), True) - self.assertEqual(response.content.endswith(')'), True) - start = callback.__len__() + 1 - end = response.content.__len__() - 1 - content = response.content[start: end] - d = dict_for_mongo_without_userform_id( - self.xform.instances.all()[0].parsed_instance) - find_d = json.loads(content)[0] - self.assertEqual(find_d, d) - - def test_api_with_query_start_limit(self): - # query string - query = '{"transport/available_transportation_types_to_referral_facil'\ - 'ity":"none"}' - data = {'query': query, 'start': 0, 'limit': 10} - response = self.client.get(self.api_url, data) - self.assertEqual(response.status_code, 200) - d = dict_for_mongo_without_userform_id( - self.xform.instances.all()[0].parsed_instance) - find_d = json.loads(response.content)[0] - self.assertEqual(find_d, d) - - def test_api_with_query_invalid_start_limit(self): - # query string - query = '{"transport/available_transportation_types_to_referral_facil'\ - 'ity":"none"}' - data = {'query': query, 'start': -100, 'limit': -100} - response = self.client.get(self.api_url, data) - self.assertEqual(response.status_code, 400) - - def test_api_count(self): - # query string - query = '{"transport/available_transportation_types_to_referral_facil'\ - 'ity":"none"}' - data = {'query': query, 'count': 1} - response = self.client.get(self.api_url, data) - self.assertEqual(response.status_code, 200) - find_d = json.loads(response.content)[0] - self.assertTrue('count' in find_d) - self.assertEqual(find_d.get('count'), 1) - - def test_api_column_select(self): - # query string - query = '{"transport/available_transportation_types_to_referral_facil'\ - 'ity":"none"}' - columns = '["transport/available_transportation_types_to_referral_fac'\ - 'ility"]' - data = {'query': query, 'fields': columns} - response = self.client.get(self.api_url, data) - self.assertEqual(response.status_code, 200) - find_d = json.loads(response.content)[0] - self.assertTrue( - 'transport/available_transportation_types_to_referral_facility' in - find_d) - self.assertFalse('_attachments' in find_d) - - def test_api_decode_from_mongo(self): - field = "$section1.group01.question1" - encoded = MongoHelper.encode(field) - self.assertEqual(encoded, ( - "%(dollar)ssection1%(dot)sgroup01%(dot)squestion1" % { - "dollar": base64.b64encode("$"), - "dot": base64.b64encode(".")})) - decoded = MongoHelper.decode(encoded) - self.assertEqual(field, decoded) - - def test_api_with_or_query(self): - """Test that an or query is interpreted correctly since we use an - internal or query to filter out deleted records""" - for i in range(1, 3): - self._submit_transport_instance(i) - # record 0: does NOT have the 'transport/loop_over_transport_types_freq - # uency/ambulance/frequency_to_referral_facility' field - # record 1: 'transport/loop_over_transport_types_frequency/ambulance/fr - # equency_to_referral_facility': 'daily' - # record 2: 'transport/loop_over_transport_types_frequency/ambulance/fr - # equency_to_referral_facility': 'weekly' - params = { - 'query': - '{"$or": [{"transport/loop_over_transport_types_frequency/ambulanc' - 'e/frequency_to_referral_facility": "weekly"}, {"transport/loop_ov' - 'er_transport_types_frequency/ambulance/frequency_to_referral_faci' - 'lity": "daily"}]}'} - response = self.client.get(self.api_url, params) - self.assertEqual(response.status_code, 200) - data = json.loads(response.content) - self.assertEqual(len(data), 2) - - # check that blank params give us all our records i.e. 3 - params = {} - response = self.client.get(self.api_url, params) - self.assertEqual(response.status_code, 200) - data = json.loads(response.content) - self.assertEqual(len(data), 3) - - def test_api_cors_options(self): - response = self.anon.options(self.api_url) - allowed_headers = ['Accept', 'Origin', 'X-Requested-With', - 'Authorization'] - provided_headers = [h.strip() for h in response[ - 'Access-Control-Allow-Headers'].split(',')] - self.assertListEqual(allowed_headers, provided_headers) - self.assertEqual(response['Access-Control-Allow-Methods'], 'GET') - self.assertEqual(response['Access-Control-Allow-Origin'], '*') diff --git a/onadata/apps/main/tests/test_form_api_delete.py b/onadata/apps/main/tests/test_form_api_delete.py deleted file mode 100644 index a509d8e01..000000000 --- a/onadata/apps/main/tests/test_form_api_delete.py +++ /dev/null @@ -1,113 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -from datetime import datetime - -from django.conf import settings -from django.core.urlresolvers import reverse - -from onadata.apps.main.views import delete_data -from onadata.apps.viewer.models.parsed_instance import ParsedInstance -from onadata.apps.logger.models.instance import Instance -from onadata.libs.utils import common_tags -from .test_base import TestBase - - -class TestFormAPIDelete(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form_and_submit_instance() - self.delete_url = reverse(delete_data, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - self.mongo_args = { - 'username': self.user.username, 'id_string': self.xform.id_string, - 'query': "{}", 'limit': 1, - 'sort': '{"_id":-1}', 'fields': '["_id","_uuid"]'} - - def _get_data(self): - cursor = ParsedInstance.query_mongo(**self.mongo_args) - records = list(record for record in cursor) - return records - - def test_get_request_does_not_delete(self): - # not allowed 405 - count = Instance.objects.filter(deleted_at=None).count() - response = self.anon.get(self.delete_url) - self.assertEqual(response.status_code, 405) - self.assertEqual( - Instance.objects.filter(deleted_at=None).count(), count) - - def test_anon_user_cant_delete(self): - # Only authenticated user are allowed to access the url - count = Instance.objects.filter(deleted_at=None).count() - instance = Instance.objects.filter( - xform=self.xform).latest('date_created') - # delete - params = {'id': instance.id} - response = self.anon.post(self.delete_url, params) - self.assertEqual(response.status_code, 302) - self.assertIn("accounts/login/?next=", response["Location"]) - self.assertEqual( - Instance.objects.filter(deleted_at=None).count(), count) - - def test_delete_shared(self): - # Test if someone can delete data from a shared form - self.xform.shared = True - self.xform.save() - self._create_user_and_login("jo") - count = Instance.objects.filter(deleted_at=None).count() - instance = Instance.objects.filter( - xform=self.xform).latest('date_created') - # delete - params = {'id': instance.id} - response = self.client.post(self.delete_url, params) - self.assertEqual(response.status_code, 403) - self.assertEqual( - Instance.objects.filter(deleted_at=None).count(), count) - - def test_owner_can_delete(self): - # Test if Form owner can delete - # check record exist before delete and after delete - count = Instance.objects.filter(deleted_at=None).count() - instance = Instance.objects.filter( - xform=self.xform).latest('date_created') - self.assertEqual(instance.deleted_at, None) - # delete - params = {'id': instance.id} - response = self.client.post(self.delete_url, params) - self.assertEqual(response.status_code, 200) - self.assertEqual( - Instance.objects.filter(deleted_at=None).count(), count - 1) - instance = Instance.objects.get(id=instance.id) - self.assertTrue(isinstance(instance.deleted_at, datetime)) - self.assertNotEqual(instance.deleted_at, None) - query = '{"_id": %s}' % instance.id - self.mongo_args.update({"query": query}) - # check that query_mongo will not return the deleted record - after = list(ParsedInstance.query_mongo(**self.mongo_args)) - self.assertEqual(len(after), count - 1) - - def test_delete_updates_mongo(self): - count = Instance.objects.filter( - xform=self.xform, deleted_at=None).count() - instance = Instance.objects.filter( - xform=self.xform).latest('date_created') - # delete - params = {'id': instance.id} - response = self.client.post(self.delete_url, params) - self.assertEqual(response.status_code, 200) - self.assertEqual( - Instance.objects.filter( - xform=self.xform, deleted_at=None).count(), count - 1) - # check that instance's deleted_at is set - instance = Instance.objects.get(id=instance.id) - self.assertTrue(isinstance(instance.deleted_at, datetime)) - # check mongo record was marked as deleted - cursor = settings.MONGO_DB.instances.find( - {common_tags.ID: instance.id}) - self.assertEqual(cursor.count(), 1) - record = cursor.next() - self.assertIsNotNone(record[common_tags.DELETEDAT]) diff --git a/onadata/apps/main/tests/test_form_edit.py b/onadata/apps/main/tests/test_form_edit.py deleted file mode 100644 index 8876413dd..000000000 --- a/onadata/apps/main/tests/test_form_edit.py +++ /dev/null @@ -1,128 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -from django.core.urlresolvers import reverse - -from onadata.apps.main.models import MetaData -from onadata.apps.main.views import edit -from onadata.apps.logger.models import XForm -from onadata.apps.logger.views import delete_xform -from .test_base import TestBase - - -class TestFormEdit(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form_and_submit_instance() - self.edit_url = reverse(edit, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - - def test_anon_no_edit_post(self): - self.xform.shared = True - self.xform.save() - desc = 'Snooky' - response = self.anon.post(self.edit_url, {'description': desc}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertNotEqual( - XForm.objects.get(pk=self.xform.pk).description, desc) - self.assertEqual(response.status_code, 302) - - def test_not_owner_no_edit_post(self): - self.xform.shared = True - self.xform.save() - desc = 'Snooky' - self._create_user_and_login("jo") - response = self.client.post(self.edit_url, {'description': desc}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 403) - self.assertNotEqual( - XForm.objects.get(pk=self.xform.pk).description, desc) - - def test_user_description_edit_updates(self): - desc = 'Snooky' - response = self.client.post(self.edit_url, {'description': desc}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.get(pk=self.xform.pk).description, desc) - - def test_user_title_edit_updates(self): - desc = 'Snooky' - response = self.client.post(self.edit_url, {'title': desc}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.get(pk=self.xform.pk).title, desc) - - def test_user_form_license_edit_updates(self): - desc = 'Snooky' - response = self.client.post(self.edit_url, {'form-license': desc}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual(MetaData.form_license(self.xform).data_value, desc) - - def test_user_data_license_edit_updates(self): - desc = 'Snooky' - response = self.client.post(self.edit_url, {'data-license': desc}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual(MetaData.data_license(self.xform).data_value, desc) - - def test_user_toggle_data_privacy(self): - self.assertEqual(self.xform.shared, False) - response = self.client.post(self.edit_url, {'toggle_shared': 'data'}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.get(pk=self.xform.pk).shared_data, True) - - def test_user_toggle_data_privacy_off(self): - self.xform.shared_data = True - self.xform.save() - response = self.client.post(self.edit_url, {'toggle_shared': 'data'}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual( - XForm.objects.get(pk=self.xform.pk).shared_data, False) - - def test_user_toggle_form_privacy(self): - self.assertEqual(self.xform.shared, False) - response = self.client.post(self.edit_url, {'toggle_shared': 'form'}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.get(pk=self.xform.pk).shared, True) - - def test_user_toggle_form_privacy_off(self): - self.xform.shared = True - self.xform.save() - response = self.client.post(self.edit_url, {'toggle_shared': 'form'}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.get(pk=self.xform.pk).shared, False) - - def test_user_toggle_form_downloadable(self): - self.xform.downloadable = False - self.xform.save() - self.assertEqual(self.xform.downloadable, False) - response = self.client.post(self.edit_url, {'toggle_shared': 'active'}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual( - XForm.objects.get(pk=self.xform.pk).downloadable, True) - - def test_user_toggle_form_downloadable_off(self): - self.xform.downloadable = True - self.xform.save() - response = self.client.post(self.edit_url, {'toggle_shared': 'active'}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual( - XForm.objects.get(pk=self.xform.pk).downloadable, False) - - def test_delete_404(self): - bad_delete_url = reverse(delete_xform, kwargs={ - 'username': self.user.username, - 'id_string': 'non_existent_id_string' - }) - response = self.client.post(bad_delete_url) - self.assertEqual(response.status_code, 404) diff --git a/onadata/apps/main/tests/test_form_enter_data.py b/onadata/apps/main/tests/test_form_enter_data.py deleted file mode 100644 index 92d29c693..000000000 --- a/onadata/apps/main/tests/test_form_enter_data.py +++ /dev/null @@ -1,152 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import os -import re -import requests -import unittest - -from urlparse import urlparse -from time import time -from httmock import urlmatch, HTTMock - -from django.test import RequestFactory -from django.core.urlresolvers import reverse -from django.core.validators import URLValidator -from django.conf import settings -from django.contrib.auth.models import AnonymousUser - -from nose import SkipTest - -from onadata.apps.main.views import set_perm, show, qrcode -from onadata.apps.main.models import MetaData -from onadata.apps.logger.views import enter_data -from onadata.libs.utils.viewer_tools import enketo_url -from .test_base import TestBase - - -@urlmatch(netloc=r'(.*\.)?enketo\.formhub\.org$') -def enketo_mock(url, request): - response = requests.Response() - response.status_code = 201 - response._content = '{"url": "https://hmh2a.enketo.formhub.org"}' - return response - - -@urlmatch(netloc=r'(.*\.)?enketo\.formhub\.org$') -def enketo_error_mock(url, request): - response = requests.Response() - response.status_code = 400 - response._content = '{"message": ' \ - '"no account exists for this OpenRosa server"}' - return response - - -class TestFormEnterData(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form_and_submit_instance() - self.perm_url = reverse(set_perm, kwargs={ - 'username': self.user.username, 'id_string': self.xform.id_string}) - self.show_url = reverse(show, kwargs={'uuid': self.xform.uuid}) - self.url = reverse(enter_data, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - - def _running_enketo(self, check_url=False): - if hasattr(settings, 'ENKETO_URL') and \ - (not check_url or self._check_url(settings.ENKETO_URL)): - return True - return False - - def test_enketo_remote_server(self): - if not self._running_enketo(): - raise SkipTest - with HTTMock(enketo_mock): - server_url = 'https://testserver.com/bob' - form_id = "test_%s" % re.sub(re.compile("\."), "_", str(time())) - url = enketo_url(server_url, form_id) - self.assertIsInstance(url, basestring) - self.assertIsNone(URLValidator()(url)) - - def _get_grcode_view_response(self): - factory = RequestFactory() - request = factory.get('/') - request.user = self.user - response = qrcode( - request, self.user.username, self.xform.id_string) - - return response - - @unittest.skip('Fails under Django 1.6') - def test_qrcode_view(self): - with HTTMock(enketo_mock): - response = self._get_grcode_view_response() - qrfile = os.path.join( - self.this_directory, 'fixtures', 'qrcode.response') - with open(qrfile, 'r') as f: - data = f.read() - self.assertContains(response, data.strip(), status_code=200) - - @unittest.skip('Fails under Django 1.6') - def test_qrcode_view_with_enketo_error(self): - with HTTMock(enketo_error_mock): - response = self._get_grcode_view_response() - self.assertEqual(response.status_code, 400) - - @unittest.skip('Fails under Django 1.6') - def test_enter_data_redir(self): - if not self._running_enketo(): - raise SkipTest - with HTTMock(enketo_mock): - factory = RequestFactory() - request = factory.get('/') - request.user = self.user - response = enter_data( - request, self.user.username, self.xform.id_string) - # make sure response redirect to an enketo site - enketo_base_url = urlparse(settings.ENKETO_URL).netloc - redirected_base_url = urlparse(response['Location']).netloc - # TODO: checking if the form is valid on enketo side - self.assertIn(enketo_base_url, redirected_base_url) - self.assertEqual(response.status_code, 302) - - def test_enter_data_no_permission(self): - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 403) - - @unittest.skip('Fails under Django 1.6') - def test_public_with_link_to_share_toggle_on(self): - # sharing behavior as of 09/13/2012: - # it requires both data_share and form_share both turned on - # in order to grant anon access to form uploading - # TODO: findout 'for_user': 'all' and what it means - response = self.client.post(self.perm_url, {'for_user': 'all', - 'perm_type': 'link'}) - self.assertEqual(response.status_code, 302) - self.assertEqual(MetaData.public_link(self.xform), True) - # toggle shared on - self.xform.shared = True - self.xform.shared_data = True - self.xform.save() - response = self.anon.get(self.show_url) - self.assertEqual(response.status_code, 302) - if not self._running_enketo(): - raise SkipTest - with HTTMock(enketo_mock): - factory = RequestFactory() - request = factory.get('/') - request.user = AnonymousUser() - response = enter_data( - request, self.user.username, self.xform.id_string) - self.assertEqual(response.status_code, 302) - - def test_enter_data_non_existent_user(self): - url = reverse(enter_data, kwargs={ - 'username': 'nonexistentuser', - 'id_string': self.xform.id_string - }) - response = self.anon.get(url) - self.assertEqual(response.status_code, 404) diff --git a/onadata/apps/main/tests/test_form_errors.py b/onadata/apps/main/tests/test_form_errors.py index fb3363a52..d01a66602 100644 --- a/onadata/apps/main/tests/test_form_errors.py +++ b/onadata/apps/main/tests/test_form_errors.py @@ -18,7 +18,7 @@ def _create_xform(self): self.this_directory, "fixtures", "transportation", "transportation.xls") response = self._publish_xls_file(self.xls_path) - self.assertEquals(response.status_code, 200) + self.assertEquals(response.status_code, 201) self.xform = XForm.objects.all()[0] def test_bad_id_string(self): @@ -27,7 +27,7 @@ def test_bad_id_string(self): xls_path = os.path.join(self.this_directory, "fixtures", "transportation", "transportation.bad_id.xls") response = self._publish_xls_file(xls_path) - self.assertEquals(response.status_code, 200) + self.assertEquals(response.status_code, 400) self.assertEquals(XForm.objects.count(), count) @skip diff --git a/onadata/apps/main/tests/test_form_gallery.py b/onadata/apps/main/tests/test_form_gallery.py deleted file mode 100644 index 76f68409c..000000000 --- a/onadata/apps/main/tests/test_form_gallery.py +++ /dev/null @@ -1,42 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import os -import unittest - -from django.core.urlresolvers import reverse - -from onadata.apps.main.views import clone_xlsform -from onadata.apps.logger.models import XForm -from .test_base import TestBase - - -class TestFormGallery(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form() - self.url = reverse(clone_xlsform, - kwargs={'username': self.user.username}) - - def test_require_logged_in_user(self): - count = XForm.objects.count() - self.anon.post(self.url) - self.assertEqual(count, XForm.objects.count()) - - def test_clone_for_other_user(self): - self._create_user_and_login('alice', 'alice') - count = XForm.objects.count() - self.client.post( - self.url, {'id_string': self.xform.id_string, 'username': 'bob'}) - self.assertEqual(count + 1, XForm.objects.count()) - - def test_cannot_publish_id_string_starting_with_number(self): - xls_path = os.path.join(self.this_directory, "fixtures", - "transportation", - "transportation.id_starts_with_num.xls") - count = XForm.objects.count() - response = TestBase._publish_xls_file(self, xls_path) - - self.assertContains(response, 'Names must begin with a letter') - self.assertEqual(XForm.objects.count(), count) diff --git a/onadata/apps/main/tests/test_form_metadata.py b/onadata/apps/main/tests/test_form_metadata.py index 201a1d857..e611663be 100644 --- a/onadata/apps/main/tests/test_form_metadata.py +++ b/onadata/apps/main/tests/test_form_metadata.py @@ -1,16 +1,19 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import os import hashlib -import unittest from django.core.files.base import File from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.urlresolvers import reverse from onadata.apps.main.models import MetaData -from onadata.apps.main.views import show, edit, download_metadata,\ - download_media_data, delete_metadata +from onadata.apps.main.views import ( + show, + edit, + download_media_data, +) from .test_base import TestBase @@ -29,155 +32,61 @@ def setUp(self): 'id_string': self.xform.id_string }) - def _add_metadata(self, data_type='doc'): - if data_type == 'media': - name = 'screenshot.png' - else: - name = 'transportation.xls' + def _add_metadata(self): + name = 'screenshot.png' + data_type = 'media' path = os.path.join(self.this_directory, "fixtures", "transportation", name) with open(path) as doc_file: self.post_data = {} self.post_data[data_type] = doc_file self.client.post(self.edit_url, self.post_data) - if data_type == 'media': - self.doc = MetaData.objects.filter(data_type='media').reverse()[0] - self.doc_url = reverse(download_media_data, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string, - 'data_id': self.doc.id}) - else: - self.doc = MetaData.objects.all().reverse()[0] - self.doc_url = reverse(download_metadata, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string, - 'data_id': self.doc.id}) - return name - def test_adds_supporting_doc_on_submit(self): - count = len(MetaData.objects.filter(xform=self.xform, - data_type='supporting_doc')) - self._add_metadata() - self.assertEquals(count + 1, len(MetaData.objects.filter( - xform=self.xform, data_type='supporting_doc'))) + self.doc = MetaData.objects.filter(data_type=data_type).reverse()[0] + self.doc_url = reverse(download_media_data, kwargs={ + 'username': self.user.username, + 'id_string': self.xform.id_string, + 'data_id': self.doc.id}) + + return name def test_adds_supporting_media_on_submit(self): count = len(MetaData.objects.filter(xform=self.xform, data_type='media')) - self._add_metadata(data_type='media') + self._add_metadata() self.assertEquals(count + 1, len(MetaData.objects.filter( xform=self.xform, data_type='media'))) - def test_shows_supporting_doc_after_submit(self): - name = self._add_metadata() - response = self.client.get(self.url) - self.assertContains(response, name) - self.xform.shared = True - self.xform.save() - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, name) - - @unittest.skip('Is based on changed template') - def test_shows_supporting_media_after_submit(self): - name = self._add_metadata(data_type='media') - response = self.client.get(self.url) - self.assertContains(response, name) - self.xform.shared = True - self.xform.save() - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, name) - - def test_download_supporting_doc(self): - self._add_metadata() - response = self.client.get(self.doc_url) - self.assertEqual(response.status_code, 200) - fileName, ext = os.path.splitext(response['Content-Disposition']) - self.assertEqual(ext, '.xls') - - def test_no_download_supporting_doc_for_anon(self): - self._add_metadata() - response = self.anon.get(self.doc_url) - self.assertEqual(response.status_code, 403) - def test_download_supporting_media(self): - self._add_metadata(data_type='media') + self._add_metadata() response = self.client.get(self.doc_url) self.assertEqual(response.status_code, 200) fileName, ext = os.path.splitext(response['Content-Disposition']) self.assertEqual(ext, '.png') - def test_shared_download_supporting_doc_for_anon(self): - self._add_metadata() - self.xform.shared = True - self.xform.save() - response = self.anon.get(self.doc_url) - self.assertEqual(response.status_code, 200) - def test_shared_download_supporting_media_for_anon(self): - self._add_metadata(data_type='media') + self._add_metadata() self.xform.shared = True self.xform.save() response = self.anon.get(self.doc_url) self.assertEqual(response.status_code, 200) - def test_delete_supporting_doc(self): - count = MetaData.objects.filter(xform=self.xform, - data_type='supporting_doc').count() - self._add_metadata() - self.assertEqual(MetaData.objects.filter( - xform=self.xform, data_type='supporting_doc').count(), count + 1) - doc = MetaData.objects.filter(data_type='supporting_doc').reverse()[0] - self.delete_doc_url = reverse(delete_metadata, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string, - 'data_id': doc.id}) - response = self.client.get(self.delete_doc_url + '?del=true') - self.assertEqual(MetaData.objects.filter( - xform=self.xform, data_type='supporting_doc').count(), count) - self.assertEqual(response.status_code, 302) - def test_delete_supporting_media(self): count = MetaData.objects.filter( xform=self.xform, data_type='media').count() - self._add_metadata(data_type='media') + self._add_metadata() self.assertEqual(MetaData.objects.filter( xform=self.xform, data_type='media').count(), count + 1) response = self.client.get(self.doc_url + '?del=true') self.assertEqual(MetaData.objects.filter( xform=self.xform, data_type='media').count(), count) self.assertEqual(response.status_code, 302) - self._add_metadata(data_type='media') + self._add_metadata() response = self.anon.get(self.doc_url + '?del=true') self.assertEqual(MetaData.objects.filter( xform=self.xform, data_type='media').count(), count + 1) self.assertEqual(response.status_code, 403) - def test_user_source_edit_updates(self): - desc = 'Snooky' - response = self.client.post(self.edit_url, {'source': desc}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self.assertEqual(MetaData.source(self.xform).data_value, desc) - - def test_upload_source_file(self): - self._add_metadata('source') - self.assertNotEqual(MetaData.source(self.xform).data_file, None) - - def test_upload_source_file_set_value_to_name(self): - name = self._add_metadata('source') - self.assertEqual(MetaData.source(self.xform).data_value, name) - - def test_upload_source_file_keep_name(self): - desc = 'Snooky' - response = self.client.post(self.edit_url, {'source': desc}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - self._add_metadata('source') - self.assertNotEqual(MetaData.source(self.xform).data_file, None) - self.assertEqual(MetaData.source(self.xform).data_value, desc) - def test_media_file_hash(self): name = "screenshot.png" media_file = os.path.join( diff --git a/onadata/apps/main/tests/test_form_permissions.py b/onadata/apps/main/tests/test_form_permissions.py deleted file mode 100644 index 52ffaa81d..000000000 --- a/onadata/apps/main/tests/test_form_permissions.py +++ /dev/null @@ -1,256 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import os -import unittest - -from django.core.urlresolvers import reverse -from guardian.shortcuts import assign_perm, remove_perm - -from onadata.apps.logger.models import XForm -from onadata.apps.viewer.views import map_view -from onadata.apps.main.views import set_perm, show, edit, api, profile -from onadata.apps.main.models import MetaData -from .test_base import TestBase - - -class TestFormPermissions(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form() - s = 'transport_2011-07-25_19-05-49' - self._make_submission(os.path.join( - self.this_directory, 'fixtures', - 'transportation', 'instances', s, s + '.xml')) - self.submission = self.xform.instances.reverse()[0] - self.url = reverse(map_view, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string}) - self.perm_url = reverse(set_perm, kwargs={ - 'username': self.user.username, 'id_string': self.xform.id_string}) - self.edit_url = reverse(edit, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - self.show_url = reverse(show, kwargs={'uuid': self.xform.uuid}) - self.show_normal_url = reverse(show, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - self.api_url = reverse(api, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - - def test_set_permissions_for_user(self): - self._create_user_and_login('alice') - self.assertEqual(self.user.has_perm('change_xform', self.xform), False) - assign_perm('change_xform', self.user, self.xform) - self.assertEqual(self.user.has_perm('change_xform', self.xform), True) - xform = self.xform - self._publish_transportation_form() - self.assertNotEqual(xform, self.xform) - self.assertEqual(self.user.has_perm('change_xform', self.xform), True) - - def test_allow_map(self): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_restrict_map_for_anon(self): - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_restrict_map_for_not_owner(self): - self._create_user_and_login('alice') - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_allow_map_if_shared(self): - self.xform.shared_data = True - self.xform.save() - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_allow_map_if_user_given_permission(self): - self._create_user_and_login('alice') - assign_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_disallow_map_if_user_permission_revoked(self): - self._create_user_and_login('alice') - assign_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - remove_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_require_owner_to_add_perm(self): - response = self.anon.post(self.perm_url) - self.assertContains(response, 'Permission denied.', status_code=403) - self._create_user_and_login('alice') - response = self.client.post(self.perm_url) - self.assertContains(response, 'Permission denied.', status_code=403) - - def test_add_view_to_user(self): - user = self._create_user('alice', 'alice') - response = self.client.post(self.perm_url, { - 'for_user': user.username, 'perm_type': 'view'}) - self.assertEqual(response.status_code, 302) - alice = self._login('alice', 'alice') - response = alice.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_add_view_permisions_to_user(self): - user = self._create_user('alice', 'alice') - response = self.client.post(self.perm_url, { - 'for_user': user.username, 'perm_type': 'view'}) - self.assertEqual(response.status_code, 302) - alice = self._login('alice', 'alice') - response = alice.get(self.show_url) - self.assertEqual(response.status_code, 302) - response = alice.get(self.show_normal_url) - self.assertContains(response, 'View data in table') - - def test_add_edit_to_user(self): - user = self._create_user('alice', 'alice') - response = self.client.post(self.perm_url, { - 'for_user': user.username, 'perm_type': 'edit'}) - self.assertEqual(response.status_code, 302) - alice = self._login('alice', 'alice') - response = alice.post(self.edit_url) - self.assertEqual(response.status_code, 302) - - def test_remove_permissions_from_user(self): - user = self._create_user('alice', 'alice') - # Grant all permissions - for perm_type in ('view', 'edit', 'report'): - response = self.client.post(self.perm_url, { - 'for_user': user.username, 'perm_type': perm_type}) - self.assertEqual(response.status_code, 302) - self.assertEqual(user.has_perm('view_xform', self.xform), True) - self.assertEqual(user.has_perm('change_xform', self.xform), True) - self.assertEqual(user.has_perm('report_xform', self.xform), True) - # Revoke all permissions - response = self.client.post(self.perm_url, { - 'for_user': user.username, 'perm_type': 'remove'}) - self.assertEqual(response.status_code, 302) - self.assertEqual(user.has_perm('view_xform', self.xform), False) - self.assertEqual(user.has_perm('change_xform', self.xform), False) - self.assertEqual(user.has_perm('report_xform', self.xform), False) - - def test_public_with_link_to_share(self): - response = self.client.post(self.perm_url, { - 'for_user': 'all', 'perm_type': 'link'}) - self.assertEqual(response.status_code, 302) - self.assertEqual(MetaData.public_link(self.xform), True) - response = self.anon.get(self.show_url) - self.assertEqual(response.status_code, 302) - self.assertEqual(response['Location'], - '%s%s' % (self.base_url, self.show_normal_url)) - - def test_private_set_link_to_share_off(self): - response = self.client.post(self.perm_url, {'for_user': 'all', - 'perm_type': 'link'}) - self.assertEqual(MetaData.public_link(self.xform), True) - response = self.anon.get(self.show_url) - self.assertEqual(response.status_code, 302) - self.assertEqual(response['Location'], - '%s%s' % (self.base_url, self.show_normal_url)) - response = self.client.post(self.perm_url, {'for_user': 'none', - 'perm_type': 'link'}) - self.assertEqual(MetaData.public_link(self.xform), False) - response = self.anon.get(self.show_url) - self.assertEqual(response.status_code, 302) - # follow redirect - response = self.anon.get(response['Location']) - self.assertEqual(response.status_code, 302) - self.assertNotEqual(response['Location'], - '%s%s' % (self.base_url, self.show_normal_url)) - - def test_only_access_shared_link_form(self): - response = self.client.post(self.perm_url, {'for_user': 'all', - 'perm_type': 'link'}) - self.assertEqual(MetaData.public_link(self.xform), True) - # publish a second form to make sure the user cant access other forms - self._publish_xls_file(os.path.join( - self.this_directory, "fixtures", "csv_export", "tutorial.xls")) - xform_2 = XForm.objects.order_by('pk').reverse()[0] - url_2 = reverse(show, kwargs={ - 'username': self.user.username, - 'id_string': xform_2.id_string - }) - response = self.anon.get(url_2) - self.assertEqual(response.status_code, 302) - self.assertEqual(response["Location"], "%s/" % self.base_url) - - def test_public_with_link_to_share_toggle_on(self): - response = self.client.post(self.perm_url, {'for_user': 'toggle', - 'perm_type': 'link'}) - self.assertEqual(response.status_code, 302) - self.assertEqual(MetaData.public_link(self.xform), True) - response = self.anon.get(self.show_url) - self.assertEqual(response.status_code, 302) - self.assertEqual(response['Location'], - '%s%s' % (self.base_url, self.show_normal_url)) - response = self.anon.get(self.show_normal_url) - self.assertEqual(response.status_code, 200) - - def test_private_set_link_to_share_toggle_off(self): - response = self.client.post(self.perm_url, {'for_user': 'toggle', - 'perm_type': 'link'}) - self.assertEqual(MetaData.public_link(self.xform), True) - response = self.anon.get(self.show_url) - self.assertEqual(response.status_code, 302) - self.assertEqual(response['Location'], - '%s%s' % (self.base_url, self.show_normal_url)) - response = self.client.post(self.perm_url, {'for_user': 'none', - 'perm_type': 'link'}) - self.assertEqual(MetaData.public_link(self.xform), False) - response = self.anon.get(self.show_url) - # follow redirect - response = self.anon.get(response['Location']) - self.assertEqual(response.status_code, 302) - self.assertNotEqual(response['Location'], - '%s%s' % (self.base_url, self.show_normal_url)) - - def test_show_list_of_users_shared_with(self): - new_username = 'alice' - user = self._create_user(new_username, 'alice') - response = self.client.post(self.perm_url, {'for_user': user.username, - 'perm_type': 'view'}) - self.assertEqual(response.status_code, 302) - response = self.client.get(self.show_normal_url) - self.assertContains(response, new_username) - - def test_anon_reject_api(self): - response = self.anon.get(self.api_url) - self.assertEqual(response.status_code, 403) - - def test_client_allow_api(self): - response = self.client.get(self.api_url, {'query': '{}'}) - self.assertEqual(response.status_code, 200) - response = self.client.get(self.api_url) - self.assertEqual(response.status_code, 200) - - @unittest.skip('Is based on changed template') - def test_view_shared_form(self): - user = self._create_user('alice', 'alice') - response = self.client.post(self.perm_url, {'for_user': user.username, - 'perm_type': 'view'}) - self.assertEqual(response.status_code, 302) - alice = self._login('alice', 'alice') - response = alice.get(self.show_url) - self.assertEqual(response.status_code, 302) - response = alice.get(self.show_normal_url) - self.assertContains(response, 'Submissions:') - dashboard_url = reverse(profile, kwargs={ - 'username': 'alice' - }) - response = alice.get(dashboard_url) - self.assertContains( - response, "%s Shared by %s" % ( - self.xform.title, self.xform.user.username) - ) diff --git a/onadata/apps/main/tests/test_form_public_api.py b/onadata/apps/main/tests/test_form_public_api.py deleted file mode 100644 index 81bbda512..000000000 --- a/onadata/apps/main/tests/test_form_public_api.py +++ /dev/null @@ -1,35 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import json - -from django.core.urlresolvers import reverse - -from onadata.apps.main.views import public_api -from .test_base import TestBase - - -class TestFormPublicAPI(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form_and_submit_instance() - self.public_api_url = reverse(public_api, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - - def test_api(self): - - response = self.client.get(self.public_api_url, {}) - self.assertEqual(response.status_code, 200) - - data = json.loads(response.content) - - for field in ('username', 'id_string', 'shared', - 'shared_data', 'downloadable', - 'title', 'date_created', 'date_modified', 'uuid'): - self.assertIn(field, data) - - self.assertEqual(data.get('username'), self.user.username) - self.assertEqual(data.get('id_string'), self.xform.id_string) diff --git a/onadata/apps/main/tests/test_form_show.py b/onadata/apps/main/tests/test_form_show.py index dd21ee94d..9430444c1 100644 --- a/onadata/apps/main/tests/test_form_show.py +++ b/onadata/apps/main/tests/test_form_show.py @@ -6,13 +6,12 @@ from django.core.files.base import ContentFile from django.core.urlresolvers import reverse -from onadata.apps.main.views import show, form_photos, update_xform, profile,\ - enketo_preview +from onadata.apps.main.views import show, form_photos, \ + show_form_settings from onadata.apps.logger.models import XForm -from onadata.apps.logger.views import download_xlsform, download_jsonform,\ - download_xform, delete_xform -from onadata.apps.viewer.models.parsed_instance import ParsedInstance -from onadata.apps.viewer.views import export_list, map_view +from onadata.apps.logger.views import download_xlsform, download_jsonform, \ + download_xform +from onadata.apps.viewer.views import export_list from onadata.libs.utils.logger_tools import publish_xml_form from onadata.libs.utils.user_auth import http_auth_string from .test_base import TestBase @@ -178,29 +177,23 @@ def test_show_link_if_owner(self): 'id_string': self.xform.id_string, 'export_type': 'xls' })) - self.assertNotContains(response, reverse(map_view, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - })) + self.assertNotContains(response, 'GPS Points') # check that a form with geopoints has the map url response = self._publish_xls_file( os.path.join( os.path.dirname(__file__), "fixtures", "gps", "gps.xls")) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 201) self.xform = XForm.objects.latest('date_created') show_url = reverse(show, kwargs={ 'username': self.user.username, 'id_string': self.xform.id_string }) - map_url = reverse(map_view, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) + response = self.client.get(show_url) # check that map url doesnt show before we have submissions - self.assertNotContains(response, map_url) + self.assertNotContains(response, 'GPS Points') # make a submission self._make_submission( @@ -211,17 +204,7 @@ def test_show_link_if_owner(self): self.assertEqual(self.response.status_code, 201) # get new show view response = self.client.get(show_url) - self.assertContains(response, map_url) - - def test_user_sees_settings(self): - response = self.client.get(self.url) - self.assertContains(response, 'Settings') - - def test_anon_no_edit_btn(self): - self.xform.shared = True - self.xform.save() - response = self.anon.get(self.url) - self.assertNotContains(response, 'edit') + self.assertContains(response, 'GPS Points') def test_anon_no_toggle_data_share_btn(self): self.xform.shared = True @@ -230,20 +213,14 @@ def test_anon_no_toggle_data_share_btn(self): self.assertNotContains(response, 'PUBLIC') self.assertNotContains(response, 'PRIVATE') - def test_show_add_supporting_docs_if_owner(self): - response = self.client.get(self.url) - self.assertContains(response, 'Supporting document:') - def test_show_add_supporting_media_if_owner(self): - response = self.client.get(self.url) + url = reverse(show_form_settings, kwargs={ + 'username': self.user.username, + 'id_string': self.xform.id_string + }) + response = self.client.get(url) self.assertContains(response, 'Media Upload') - def test_hide_add_supporting_docs_if_not_owner(self): - self.xform.shared = True - self.xform.save() - response = self.anon.get(self.url) - self.assertNotContains(response, 'Upload') - def test_load_photo_page(self): response = self.client.get(reverse(form_photos, kwargs={ 'username': self.user.username, @@ -258,170 +235,6 @@ def test_load_from_uuid(self): self.assertEqual(response['Location'], '%s%s' % (self.base_url, self.url)) - @skip('Based on the old template') - def test_xls_replace_markup(self): - """ - Check that update form is only shown when there are no submissions - and the user is the owner - """ - # when we have 0 submissions, update markup exists - self.xform.shared = True - self.xform.save() - dashboard_url = reverse(profile, kwargs={ - 'username': 'bob' - }) - response = self.client.get(dashboard_url) - self.assertContains( - response, 'href="#replace-transportation_2011_07_25"') - # a non owner can't see the markup - response = self.anon.get(self.url) - self.assertNotContains( - response, 'href="#replace-transportation_2011_07_25"') - # when we have a submission, we cant update the xls form - self._submit_transport_instance() - response = self.client.get(dashboard_url) - self.assertNotContains( - response, 'href="#replace-transportation_2011_07_25"') - - def test_non_owner_cannot_replace_form(self): - """ - Test that a non owner cannot replace a shared xls form - """ - xform_update_url = reverse(update_xform, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - self.xform.shared = True - self.xform.save() - # create and login another user - self._create_user_and_login('peter', 'peter') - response = self.client.post(xform_update_url) - # since we are logged in, we'll be re-directed to our profile page - self.assertRedirects(response, self.base_url, - status_code=302, target_status_code=302) - - def test_replace_xform(self): - xform_update_url = reverse(update_xform, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - count = XForm.objects.count() - xls_path = os.path.join(self.this_directory, "fixtures", - "transportation", "transportation_updated.xls") - with open(xls_path, "r") as xls_file: - post_data = {'xls_file': xls_file} - self.client.post(xform_update_url, post_data) - self.assertEqual(XForm.objects.count(), count) - self.xform = XForm.objects.order_by('id').reverse()[0] - data_dictionary = self.xform.data_dictionary() - # look for the preferred_means question - # which is only in the updated xls - is_updated_form = len([e.name for e in data_dictionary.survey_elements - if e.name == 'preferred_means']) > 0 - self.assertTrue(is_updated_form) - - def test_update_form_doesnt_truncate_to_50_chars(self): - count = XForm.objects.count() - xls_path = os.path.join( - self.this_directory, - "fixtures", - "transportation", - "transportation_with_long_id_string.xls") - self._publish_xls_file_and_set_xform(xls_path) - - # Update the form - xform_update_url = reverse(update_xform, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - updated_xls_path = os.path.join( - self.this_directory, - "fixtures", - "transportation", - "transportation_with_long_id_string_updated.xls") - with open(updated_xls_path, "r") as xls_file: - post_data = {'xls_file': xls_file} - self.client.post(xform_update_url, post_data) - # Count should stay the same - self.assertEqual(XForm.objects.count(), count + 1) - self.xform = XForm.objects.order_by('id').reverse()[0] - data_dictionary = self.xform.data_dictionary() - # look for the preferred_means question - # which is only in the updated xls - is_updated_form = len([e.name for e in data_dictionary.survey_elements - if e.name == 'preferred_means']) > 0 - self.assertTrue(is_updated_form) - - def test_xform_delete(self): - id_string = self.xform.id_string - form_exists = XForm.objects.filter( - user=self.user, id_string=id_string).count() == 1 - self.assertTrue(form_exists) - xform_delete_url = reverse(delete_xform, kwargs={ - 'username': self.user.username, - 'id_string': id_string - }) - self.client.post(xform_delete_url) - form_deleted = XForm.objects.filter( - user=self.user, id_string=id_string).count() == 0 - self.assertTrue(form_deleted) - - def test_non_owner_cant_delete_xform(self): - id_string = self.xform.id_string - form_exists = XForm.objects.filter( - user=self.user, id_string=id_string).count() == 1 - self.assertTrue(form_exists) - xform_delete_url = reverse(delete_xform, kwargs={ - 'username': self.user.username, - 'id_string': id_string - }) - # save current user before we re-assign - bob = self.user - self._create_user_and_login('alice', 'alice') - self.client.post(xform_delete_url) - form_deleted = XForm.objects.filter( - user=bob, id_string=id_string).count() == 0 - self.assertFalse(form_deleted) - - def test_xform_delete_cascades_mongo_instances(self): - initial_mongo_count = ParsedInstance.query_mongo( - self.user.username, self.xform.id_string, '{}', '[]', '{}', - count=True)[0]["count"] - # submit instance - for i in range(len(self.surveys)): - self._submit_transport_instance(i) - # check mongo record exists - mongo_count = ParsedInstance.query_mongo( - self.user.username, self.xform.id_string, '{}', '[]', '{}', - count=True)[0]["count"] - self.assertEqual(mongo_count, initial_mongo_count + len(self.surveys)) - # delete form - xform_delete_url = reverse(delete_xform, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - self.client.post(xform_delete_url) - mongo_count = ParsedInstance.query_mongo( - self.user.username, self.xform.id_string, '{}', '[]', '{}', - count=True)[0]["count"] - self.assertEqual(mongo_count, initial_mongo_count) - - def test_enketo_preview(self): - url = reverse( - enketo_preview, kwargs={'username': self.user.username, - 'id_string': self.xform.id_string}) - response = self.client.get(url) - self.assertEqual(response.status_code, 302) - - def test_enketo_preview_works_on_shared_forms(self): - self.xform.shared = True - self.xform.save() - url = reverse( - enketo_preview, kwargs={'username': self.user.username, - 'id_string': self.xform.id_string}) - response = self.anon.get(url) - self.assertEqual(response.status_code, 302) - # TODO PLD disabling this test @skip('Insensitivity is not enforced upon creation of id_strings.') def test_form_urls_case_insensitive(self): diff --git a/onadata/apps/main/tests/test_google_doc.py b/onadata/apps/main/tests/test_google_doc.py deleted file mode 100644 index 6b757478e..000000000 --- a/onadata/apps/main/tests/test_google_doc.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import os - -from django.test import TestCase -from django.utils.encoding import smart_text - -from onadata.apps.main.google_doc import GoogleDoc - - -class TestGoogleDoc(TestCase): - - def test_view(self): - doc = GoogleDoc() - folder = os.path.join( - os.path.dirname(__file__), "fixtures", "google_doc" - ) - input_path = os.path.join(folder, "input.html") - with open(input_path) as f: - input_html = smart_text(f.read()) - doc.set_html(input_html) - self.assertEqual(doc._html, input_html) - self.assertEqual(len(doc._sections), 14) - output_path = os.path.join(folder, "navigation.html") - with open(output_path) as f: - self.assertEquals(doc._navigation_html(), f.read()) diff --git a/onadata/apps/main/tests/test_google_docs_export.py b/onadata/apps/main/tests/test_google_docs_export.py deleted file mode 100644 index a067fdc12..000000000 --- a/onadata/apps/main/tests/test_google_docs_export.py +++ /dev/null @@ -1,53 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -from django.core.urlresolvers import reverse -from mock import Mock, patch - -from onadata.apps.main.google_export import refresh_access_token -from onadata.apps.main.models import TokenStorageModel -from onadata.apps.viewer.views import google_xls_export -from onadata.libs.utils.google import oauth2_token as token -from .test_base import TestBase - - -class TestGoogleDocsExport(TestBase): - - def setUp(self): - self.token = token - self.refresh_token = '1/ISGBd-OBWr-RbXN2Fq879Xht1inmg_n4sJ_Wd4CoQNk' - self.token.refresh_token = self.refresh_token - self._create_user_and_login() - - @patch('gdata.docs.client.DocsClient') - @patch('urllib2.urlopen') - def test_google_docs_export(self, mock_urlopen, mock_docs_client): - mock_urlopen.return_value.read.return_value = '{"access_token": "ABC"}' - mock_xls_doc = Mock() - mock_xls_doc.find_html_link.return_value = 'link' - mock_docs_client = Mock() - mock_docs_client.CreateResource.return_value = mock_xls_doc - mock_docs_client.return_value = mock_docs_client - - self._publish_transportation_form() - self._make_submissions() - - initial_token_count = TokenStorageModel.objects.all().count() - self.token = refresh_access_token(self.token, self.user) - - self.assertIsNotNone(self.token.access_token) - self.assertEqual( - TokenStorageModel.objects.all().count(), initial_token_count + 1) - - response = self.client.get(reverse(google_xls_export, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - })) - self.assertEqual(response.status_code, 302) - # share the data, log out, and check the export - self._share_form_data() - self._logout() - response = self.client.get(reverse(google_xls_export, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - })) - self.assertEqual(response.status_code, 302) diff --git a/onadata/apps/main/tests/test_gps.py b/onadata/apps/main/tests/test_gps.py index 8f648f98d..054c2bf37 100644 --- a/onadata/apps/main/tests/test_gps.py +++ b/onadata/apps/main/tests/test_gps.py @@ -3,7 +3,6 @@ import os from onadata.apps.viewer.models.data_dictionary import DataDictionary -import unittest from .test_base import TestBase @@ -15,7 +14,6 @@ def test_gps(self): self._publish_survey() self._make_submissions() self._check_has_geopoints() - self._check_link_to_map_view() def _publish_survey(self): self.this_directory = os.path.dirname(__file__) @@ -38,9 +36,3 @@ def _check_has_geopoints(self): # should have been saved to dd.instances_with_geopoints during # submission self.assertTrue(dd.has_instances_with_geopoints()) - - @unittest.skip('Based on the old template') - def _check_link_to_map_view(self): - response = self.client.get("/%s/" % self.user.username) - map_url = 'href="/%s/forms/gps/map"' % self.user.username - self.assertContains(response, map_url) diff --git a/onadata/apps/main/tests/test_http_auth.py b/onadata/apps/main/tests/test_http_auth.py index 724e254ad..144a2abf4 100644 --- a/onadata/apps/main/tests/test_http_auth.py +++ b/onadata/apps/main/tests/test_http_auth.py @@ -3,7 +3,6 @@ from django.core.urlresolvers import reverse from onadata.apps.main.tests.test_base import TestBase -from onadata.apps.main import views class TestBasicHttpAuthentication(TestBase): @@ -12,19 +11,19 @@ def setUp(self): TestBase.setUp(self) self._create_user_and_login(username='bob', password='bob') self._publish_transportation_form() - self.api_url = reverse(views.api, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string + self.api_url = reverse('data-list', kwargs={ + 'pk': self.xform.pk, + 'format': 'json' }) self._logout() def test_http_auth(self): response = self.client.get(self.api_url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) # headers with invalid user/pass response = self.client.get(self.api_url, **self._set_auth_headers('x', 'y')) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 401) # headers with valid user/pass response = self.client.get(self.api_url, **self._set_auth_headers('bob', 'bob')) diff --git a/onadata/apps/main/tests/test_permissions.py b/onadata/apps/main/tests/test_permissions.py new file mode 100644 index 000000000..a230d38c1 --- /dev/null +++ b/onadata/apps/main/tests/test_permissions.py @@ -0,0 +1,62 @@ +import os +import unittest + +from django.core.urlresolvers import reverse +from guardian.shortcuts import assign_perm, remove_perm + +from onadata.apps.logger.models import XForm +#from onadata.apps.viewer.views import map_view +from onadata.apps.main.views import show, edit, profile +from onadata.apps.main.models import MetaData + +from .test_base import TestBase + + +class TestPermissions(TestBase): + + def setUp(self): + TestBase.setUp(self) + self._create_user_and_login() + self._publish_transportation_form() + s = 'transport_2011-07-25_19-05-49' + self._make_submission(os.path.join( + self.this_directory, 'fixtures', + 'transportation', 'instances', s, s + '.xml')) + self.submission = self.xform.instances.reverse()[0] + self.url = reverse(show, kwargs={ + 'username': self.user.username, + 'id_string': self.xform.id_string + }) + + def test_set_permissions_for_user(self): + # alice cannot view bob's forms + # `self.xform` belongs to bob + self._create_user_and_login('alice', 'alice') + # Alice is `self.user` now. + self.assertEqual(self.user.has_perm('view_xform', self.xform), False) + self.assertEqual(self.user.has_perm('change_xform', self.xform), False) + assign_perm('view_xform', self.user, self.xform) + self.assertEqual(self.user.has_perm('view_xform', self.xform), True) + assign_perm('change_xform', self.user, self.xform) + self.assertEqual(self.user.has_perm('change_xform', self.xform), True) + xform = self.xform + + remove_perm('view_xform', self.user, xform) + self.assertEqual(self.user.has_perm('view_xform', xform), False) + remove_perm('change_xform', self.user, xform) + self.assertEqual(self.user.has_perm('change_xform', xform), False) + + self._publish_transportation_form() + self.assertNotEqual(xform, self.xform) + # Alice is the owner of `self.xform` + self.assertEqual(self.user.has_perm('view_xform', self.xform), True) + self.assertEqual(self.user.has_perm('change_xform', self.xform), True) + + def test_add_view_to_user(self): + self._create_user_and_login('alice', 'alice') + alice = self.user + response = self.client.get(self.url) + self.assertEqual(response.status_code, 302) + assign_perm('view_xform', alice, self.xform) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) diff --git a/onadata/apps/main/tests/test_process.py b/onadata/apps/main/tests/test_process.py index 7cd003c3d..e5e5726b7 100644 --- a/onadata/apps/main/tests/test_process.py +++ b/onadata/apps/main/tests/test_process.py @@ -84,37 +84,6 @@ def test_uuid_submit(self): def test_publish_xlsx_file(self): self._publish_xlsx_file() - def test_google_url_upload(self): - if self._internet_on(url="http://google.com"): - xls_url = "https://docs.google.com/spreadsheet/pub?"\ - "key=0AvhZpT7ZLAWmdDhISGhqSjBOSl9XdXd5SHZHUUE2RFE&output=xls" - pre_count = XForm.objects.count() - response = self.client.post('/%s/' % self.user.username, - {'xls_url': xls_url}) - # make sure publishing the survey worked - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.count(), pre_count + 1) - - @unittest.skip('Fails under Django 1.6') - def test_url_upload(self): - if self._internet_on(url="http://google.com"): - xls_url = 'https://ona.io/examples/forms/tutorial/form.xls' - pre_count = XForm.objects.count() - response = self.client.post('/%s/' % self.user.username, - {'xls_url': xls_url}) - # make sure publishing the survey worked - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.count(), pre_count + 1) - - def test_bad_url_upload(self): - xls_url = 'formhuborg/pld/forms/transportation_2011_07_25/form.xls' - pre_count = XForm.objects.count() - response = self.client.post('/%s/' % self.user.username, - {'xls_url': xls_url}) - # make sure publishing the survey worked - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.count(), pre_count) - # This method tests a large number of xls files. # create a directory /main/test/fixtures/online_xls # containing the files you would like to test. @@ -136,25 +105,6 @@ def test_upload_all_xls(self): print('finished sub-folder %s' % root) self.assertEqual(success, True) - def test_url_upload_non_dot_xls_path(self): - if self._internet_on(): - xls_url = 'http://formhub.org/formhub_u/forms/tutorial/form.xls' - pre_count = XForm.objects.count() - response = self.client.post('/%s/' % self.user.username, - {'xls_url': xls_url}) - # make sure publishing the survey worked - self.assertEqual(response.status_code, 200) - self.assertEqual(XForm.objects.count(), pre_count + 1) - - def test_not_logged_in_cannot_upload(self): - path = os.path.join(self.this_directory, "fixtures", "transportation", - "transportation.xls") - if not path.startswith('/%s/' % self.user.username): - path = os.path.join(self.this_directory, path) - with open(path) as xls_file: - post_data = {'xls_file': xls_file} - return self.client.post('/%s/' % self.user.username, post_data) - def _publish_file(self, xls_path, strict=True): """ Returns False if not strict and publish fails @@ -162,7 +112,7 @@ def _publish_file(self, xls_path, strict=True): pre_count = XForm.objects.count() self.response = TestBase._publish_xls_file(self, xls_path) # make sure publishing the survey worked - self.assertEqual(self.response.status_code, 200) + self.assertEqual(self.response.status_code, 201) if XForm.objects.count() != pre_count + 1: # print file location print('\nPublish Failure for file: %s' % xls_path) @@ -464,7 +414,7 @@ def test_publish_bad_xls_with_unicode_in_error(self): 'form_with_unicode_in_relevant_column.xlsx') response = TestBase._publish_xls_file(self, path) # make sure we get a 200 response - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 201) def test_metadata_file_hash(self): self._publish_transportation_form() @@ -532,21 +482,6 @@ def test_uuid_injection_in_cascading_select(self): self.assertEqual( calculate_bind_node.getAttribute("calculate"), "'%s'" % xform.uuid) - def test_csv_publishing(self): - csv_text = '\n'.join([ - 'survey,,', ',type,name,label', - ',text,whatsyourname,"What is your name?"', - 'choices,,', - 'settings,,', ',id_string', ',identity']) - url = reverse('onadata.apps.main.views.profile', - kwargs={'username': self.user.username}) - num_xforms = XForm.objects.count() - params = { - 'text_xls_form': csv_text - } - self.response = self.client.post(url, params) - self.assertEqual(XForm.objects.count(), num_xforms + 1) - def test_truncate_xform_title_to_255(self): self._publish_transportation_form() title = "a" * (XFORM_TITLE_LENGTH + 1) diff --git a/onadata/apps/main/tests/test_style.py b/onadata/apps/main/tests/test_style.py deleted file mode 100644 index f79b8ff5f..000000000 --- a/onadata/apps/main/tests/test_style.py +++ /dev/null @@ -1,14 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -from subprocess import call -import unittest - -from django.test import TestCase - - -class TestStyle(TestCase): - - @unittest.skip('Fails under Django 1.6') - def test_flake8(self): - result = call(['flake8', '--exclude', 'migrations,src', '.']) - self.assertEqual(result, 0, "Code is not flake8.") diff --git a/onadata/apps/main/tests/test_user_settings.py b/onadata/apps/main/tests/test_user_settings.py deleted file mode 100644 index f7d4878ee..000000000 --- a/onadata/apps/main/tests/test_user_settings.py +++ /dev/null @@ -1,30 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import unittest -from django.core.urlresolvers import reverse - -from onadata.apps.main.models import UserProfile -from onadata.apps.main.views import profile_settings -from .test_base import TestBase - - -class TestUserSettings(TestBase): - - def setUp(self): - TestBase.setUp(self) - self.settings_url = reverse( - profile_settings, kwargs={'username': self.user.username}) - - def test_render_user_settings(self): - response = self.client.get(self.settings_url) - self.assertEqual(response.status_code, 200) - - def test_update_user_settings(self): - self.assertFalse(self.user.profile.require_auth) - post_data = { - 'require_auth': True, - } - response = self.client.post(self.settings_url, post_data) - self.assertEqual(response.status_code, 302) - self.user.profile.refresh_from_db() - self.assertTrue(self.user.profile.require_auth) diff --git a/onadata/apps/main/urls.py b/onadata/apps/main/urls.py index 9f318e955..58ce4d64a 100644 --- a/onadata/apps/main/urls.py +++ b/onadata/apps/main/urls.py @@ -28,39 +28,16 @@ url(r'^api/v1', RedirectView.as_view(url='/api/v1/')), # django default stuff - url(r'^accounts/', include('onadata.apps.main.registration_urls')), + url(r'^accounts/', include('registration.auth_urls')), url(r'^admin/', include(admin.site.urls)), url(r'^admin/doc/', include('django.contrib.admindocs.urls')), # oath2_provider url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), - # google urls - url(r'^gauthtest/$', - 'onadata.apps.main.google_export.google_oauth2_request', - name='google-auth'), - url(r'^gwelcome/$', - 'onadata.apps.main.google_export.google_auth_return', - name='google-auth-welcome'), - # main website views url(r'^$', 'onadata.apps.main.views.home'), - url(r'^tutorial/$', 'onadata.apps.main.views.tutorial', name='tutorial'), - url(r'^about-us/$', 'onadata.apps.main.views.about_us', name='about-us'), - url(r'^getting_started/$', 'onadata.apps.main.views.getting_started', - name='getting_started'), - url(r'^faq/$', 'onadata.apps.main.views.faq', name='faq'), - url(r'^syntax/$', 'onadata.apps.main.views.syntax', name='syntax'), - url(r'^privacy/$', 'onadata.apps.main.views.privacy', name='privacy'), - url(r'^tos/$', 'onadata.apps.main.views.tos', name='tos'), - url(r'^resources/$', 'onadata.apps.main.views.resources', - name='resources'), - url(r'^forms/$', 'onadata.apps.main.views.form_gallery', - name='forms_list'), url(r'^forms/(?P[^/]+)$', 'onadata.apps.main.views.show'), - url(r'^people/$', 'onadata.apps.main.views.members_list'), - url(r'^xls2xform/$', 'onadata.apps.main.views.xls2xform'), - url(r'^support/$', 'onadata.apps.main.views.support'), url(r'^login_redirect/$', 'onadata.apps.main.views.login_redirect'), # Bring back old url because it's still used by `kpi` # ToDo Remove when `kpi#gallery-2` is merged into master @@ -72,52 +49,20 @@ 'onadata.apps.viewer.views.attachment_url'), url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', {'packages': ('main', 'viewer',)}), - url(r'^typeahead_usernames', 'onadata.apps.main.views.username_list', - name='username_list'), url(r'^(?P[^/]+)/$', 'onadata.apps.main.views.profile', name='user_profile'), - url(r'^(?P[^/]+)/profile$', - 'onadata.apps.main.views.public_profile', - name='public_profile'), - url(r'^(?P[^/]+)/settings', - 'onadata.apps.main.views.profile_settings'), - url(r'^(?P[^/]+)/cloneform$', - 'onadata.apps.main.views.clone_xlsform'), - url(r'^(?P[^/]+)/activity$', - 'onadata.apps.main.views.activity'), - url(r'^(?P[^/]+)/activity/api$', - 'onadata.apps.main.views.activity_api'), - url(r'^activity/fields$', 'onadata.apps.main.views.activity_fields'), url(r'^(?P[^/]+)/api-token$', 'onadata.apps.main.views.api_token'), # form specific url(r'^(?P[^/]+)/forms/(?P[^/]+)$', 'onadata.apps.main.views.show'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/qrcode$', - 'onadata.apps.main.views.qrcode', name='get_qrcode'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/api$', - 'onadata.apps.main.views.api', name='mongo_view_api'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/public_api$', - 'onadata.apps.main.views.public_api', name='public_api'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/delete_data$', - 'onadata.apps.main.views.delete_data', name='delete_data'), url(r'^(?P[^/]+)/forms/(?P[^/]+)/edit$', 'onadata.apps.main.views.edit'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/perms$', - 'onadata.apps.main.views.set_perm'), url(r'^(?P[^/]+)/forms/(?P[^/]+)/photos', 'onadata.apps.main.views.form_photos'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/doc/(?P\d+)' - '', 'onadata.apps.main.views.download_metadata'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/delete-doc/(?P\d+)', 'onadata.apps.main.views.delete_metadata'), url(r'^(?P[^/]+)/forms/(?P[^/]+)/formid-media/(?P\d+)', 'onadata.apps.main.views.download_media_data'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/update$', - 'onadata.apps.main.views.update_xform'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/preview$', - 'onadata.apps.main.views.enketo_preview'), url(r'^(?P[^/]+)/forms/(?P[^/]+)/form_settings$', 'onadata.apps.main.views.show_form_settings', name='show_form_settings'), @@ -142,34 +87,8 @@ url(r"^(?P\w+)/forms/(?P[^/]+)/data\.xls", 'onadata.apps.viewer.views.data_export', name='xls_export', kwargs={'export_type': 'xls'}), - url(r"^(?P\w+)/forms/(?P[^/]+)/data\.csv.zip", - 'onadata.apps.viewer.views.data_export', name='csv_zip_export', - kwargs={'export_type': 'csv_zip'}), - url(r"^(?P\w+)/forms/(?P[^/]+)/data\.sav.zip", - 'onadata.apps.viewer.views.data_export', name='sav_zip_export', - kwargs={'export_type': 'sav_zip'}), url(r"^(?P\w+)/forms/(?P[^/]+)/data\.kml$", 'onadata.apps.viewer.views.kml_export'), - url(r"^(?P\w+)/forms/(?P[^/]+)/gdocs$", - 'onadata.apps.viewer.views.google_xls_export'), - url(r"^(?P\w+)/forms/(?P[^/]+)/map_embed", - 'onadata.apps.viewer.views.map_embed_view'), - url(r"^(?P\w+)/forms/(?P[^/]+)/map", - 'onadata.apps.viewer.views.map_view'), - url(r"^(?P\w+)/forms/(?P[^/]+)/instance", - 'onadata.apps.viewer.views.instance'), - url(r"^(?P\w+)/forms/(?P[^/]+)/enter-data", - 'onadata.apps.logger.views.enter_data', name='enter_data'), - url(r"^(?P\w+)/forms/(?P[^/]+)/add-submission-with", - 'onadata.apps.viewer.views.add_submission_with', - name='add_submission_with'), - url(r"^(?P\w+)/forms/(?P[^/]+)/thank_you_submission", - 'onadata.apps.viewer.views.thank_you_submission', - name='thank_you_submission'), - url(r"^(?P\w+)/forms/(?P[^/]+)/edit-data/(?P" - "\d+)$", 'onadata.apps.logger.views.edit_data', name='edit_data'), - url(r"^(?P\w+)/forms/(?P[^/]+)/view-data", - 'onadata.apps.viewer.views.data_view'), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" "/new$", 'onadata.apps.viewer.views.create_export'), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" @@ -181,10 +100,7 @@ url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" "/(?P[^/]+)$", 'onadata.apps.viewer.views.export_download'), - url(r'^(?P\w+)/exports/', include('onadata.apps.export.urls')), - - url(r'^(?P\w+)/reports/', include('onadata.apps.survey_report.urls')), - + # odk data urls url(r"^submission$", XFormSubmissionApi.as_view({'post': 'create', 'head': 'create'}), @@ -228,37 +144,6 @@ url(r"^(?P\w+)/forms/(?P[^/]+)/form\.json", 'onadata.apps.logger.views.download_jsonform', name="download_jsonform"), - url(r"^(?P\w+)/delete/(?P[^/]+)/$", - 'onadata.apps.logger.views.delete_xform'), - url(r"^(?P\w+)/(?P[^/]+)/toggle_downloadable/$", - 'onadata.apps.logger.views.toggle_downloadable'), - - # SMS support - url(r'^(?P[^/]+)/forms/(?P[^/]+)/sms_submission/(?P[a-z]+)/?$', - 'onadata.apps.sms_support.providers.import_submission_for_form', - name='sms_submission_form_api'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/sms_submission$', - 'onadata.apps.sms_support.views.import_submission_for_form', - name='sms_submission_form'), - url(r"^(?P[^/]+)/sms_submission/(?P[a-z]+)/?$", - 'onadata.apps.sms_support.providers.import_submission', - name='sms_submission_api'), - url(r'^(?P[^/]+)/forms/(?P[^/]+)/sms_multiple_submiss' - 'ions$', - 'onadata.apps.sms_support.views.import_multiple_submissions_for_form', - name='sms_submissions_form'), - url(r"^(?P[^/]+)/sms_multiple_submissions$", - 'onadata.apps.sms_support.views.import_multiple_submissions', - name='sms_submissions'), - url(r"^(?P[^/]+)/sms_submission$", - 'onadata.apps.sms_support.views.import_submission', - name='sms_submission'), - - # static media - # Media are now served by NginX. - # url(r'^media/(?P.*)$', 'django.views.static.serve', - # {'document_root': settings.MEDIA_ROOT}), url(r'^favicon\.ico', RedirectView.as_view(url='/static/images/favicon.ico')), diff --git a/onadata/apps/main/views.py b/onadata/apps/main/views.py index 48a630c9c..e388dc6f8 100644 --- a/onadata/apps/main/views.py +++ b/onadata/apps/main/views.py @@ -1,74 +1,40 @@ # coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import +from __future__ import unicode_literals, print_function, division, \ + absolute_import -import json import os -from datetime import datetime -from bson import json_util from django.conf import settings -from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType -from django.core.files.storage import default_storage from django.core.files.storage import get_storage_class from django.core.urlresolvers import reverse -from django.db import IntegrityError, transaction from django.http import HttpResponse -from django.http import HttpResponseBadRequest from django.http import HttpResponseForbidden from django.http import HttpResponseNotFound from django.http import HttpResponseRedirect from django.http import HttpResponseServerError from django.shortcuts import get_object_or_404 from django.shortcuts import render -from django.template import loader, RequestContext from django.utils.translation import ugettext as _ from django.views.decorators.http import require_GET -from django.views.decorators.http import require_POST -from django.views.decorators.http import require_http_methods -from guardian.shortcuts import assign_perm, remove_perm, get_users_with_perms from rest_framework.authtoken.models import Token from ssrf_protect.ssrf_protect import SSRFProtect, SSRFProtectException -from onadata.apps.logger.models import Instance, XForm -from onadata.apps.logger.views import enter_data -from onadata.apps.main.forms import UserProfileForm, FormLicenseForm, \ - DataLicenseForm, SupportDocForm, QuickConverterFile, QuickConverterURL, \ - QuickConverter, SourceForm, PermissionForm, MediaForm, ActivateSMSSupportFom -from onadata.apps.main.models import AuditLog, UserProfile, MetaData -from onadata.apps.sms_support.autodoc import get_autodoc_for -from onadata.apps.sms_support.providers import providers_doc -from onadata.apps.sms_support.tools import check_form_sms_compatibility, \ - is_sms_related -from onadata.apps.viewer.models.data_dictionary import DataDictionary, \ - upload_to -from onadata.apps.viewer.models.parsed_instance import \ - DATETIME_FORMAT, ParsedInstance -from onadata.libs.constants import ( - CAN_CHANGE_XFORM, - CAN_VALIDATE_XFORM, - CAN_DELETE_DATA_XFORM, - CAN_VIEW_XFORM, - CAN_ADD_SUBMISSIONS, +from onadata.apps.logger.models import XForm +from onadata.apps.main.forms import ( + MediaForm, ) -from onadata.libs.utils.decorators import is_owner +from onadata.apps.main.forms import QuickConverterForm +from onadata.apps.main.models import UserProfile, MetaData from onadata.libs.utils.log import audit_log, Actions -from onadata.libs.utils.logger_tools import response_with_mimetype_and_name, \ - publish_form -from onadata.libs.utils.qrcode import generate_qrcode +from onadata.libs.utils.logger_tools import response_with_mimetype_and_name from onadata.libs.utils.user_auth import ( - add_cors_headers, - check_and_set_user, check_and_set_user_and_form, get_xform_and_perms, - has_delete_data_permission, - has_permission, - helper_auth_helper, set_profile_data, ) -from onadata.libs.utils.viewer_tools import enketo_url def home(request): @@ -85,144 +51,16 @@ def login_redirect(request): kwargs={'username': request.user.username})) -@require_POST -@login_required -def clone_xlsform(request, username): - """ - Copy a public/Shared form to a users list of forms. - Eliminates the need to download Excel File and upload again. - """ - to_username = request.user.username - message = {'type': None, 'text': '....'} - message_list = [] - - def set_form(): - form_owner = request.POST.get('username') - id_string = request.POST.get('id_string') - xform = XForm.objects.get(user__username__iexact=form_owner, - id_string__exact=id_string) - if len(id_string) > 0 and id_string[0].isdigit(): - id_string = '_' + id_string - path = xform.xls.name - if default_storage.exists(path): - xls_file = upload_to(None, '%s%s.xls' % ( - id_string, XForm.CLONED_SUFFIX), to_username) - xls_data = default_storage.open(path) - xls_file = default_storage.save(xls_file, xls_data) - survey = DataDictionary.objects.create( - user=request.user, - xls=xls_file - ).survey - # log to cloner's account - audit = {} - audit_log( - Actions.FORM_CLONED, request.user, request.user, - _("Cloned form '%(id_string)s'.") % - { - 'id_string': survey.id_string, - }, audit, request) - clone_form_url = reverse( - show, kwargs={ - 'username': to_username, - 'id_string': xform.id_string + XForm.CLONED_SUFFIX}) - return { - 'type': 'alert-success', - 'text': _('Successfully cloned to %(form_url)s into your ' - '%(profile_url)s') % - {'form_url': '%(id_string)s ' % { - 'id_string': survey.id_string, - 'url': clone_form_url - }, - 'profile_url': 'profile.' % - reverse(profile, kwargs={'username': to_username})} - } - form_result = publish_form(set_form) - if form_result['type'] == 'alert-success': - # comment the following condition (and else) - # when we want to enable sms check for all. - # until then, it checks if form barely related to sms - if is_sms_related(form_result.get('form_o')): - form_result_sms = check_form_sms_compatibility(form_result) - message_list = [form_result, form_result_sms] - else: - message = form_result - else: - message = form_result - - context = RequestContext(request, { - 'message': message, 'message_list': message_list}) - - if request.is_ajax(): - res = loader.render_to_string( - 'message.html', - context_instance=context - ).replace("'", r"\'").replace('\n', '') - - return HttpResponse( - "$('#mfeedback').html('%s').show();" % res) - else: - return HttpResponse(message['text']) - - def profile(request, username): content_user = get_object_or_404(User, username__iexact=username) - form = QuickConverter() + form = QuickConverterForm() data = {'form': form} - # xlsform submission... - if request.method == 'POST' and request.user.is_authenticated(): - def set_form(): - form = QuickConverter(request.POST, request.FILES) - survey = form.publish(request.user).survey - audit = {} - audit_log( - Actions.FORM_PUBLISHED, request.user, content_user, - _("Published form '%(id_string)s'.") % - { - 'id_string': survey.id_string, - }, audit, request) - enketo_webform_url = reverse( - enter_data, - kwargs={'username': username, 'id_string': survey.id_string} - ) - return { - 'type': 'alert-success', - 'preview_url': reverse(enketo_preview, kwargs={ - 'username': username, - 'id_string': survey.id_string - }), - 'text': _('Successfully published %(form_id)s.' - ' Enter Web Form' - ' or ' - 'Preview Web Form') - % {'form_id': survey.id_string, - 'form_url': enketo_webform_url}, - 'form_o': survey - } - with transaction.atomic(): - # publish_form must be wrapped into `transaction.atomic` because of - # https://stackoverflow.com/a/23326971/1141214 - form_result = publish_form(set_form) - - if form_result['type'] == 'alert-success': - # comment the following condition (and else) - # when we want to enable sms check for all. - # until then, it checks if form barely related to sms - if is_sms_related(form_result.get('form_o')): - form_result_sms = check_form_sms_compatibility(form_result) - data['message_list'] = [form_result, form_result_sms] - else: - data['message'] = form_result - else: - data['message'] = form_result - # profile view... # for the same user -> dashboard if content_user == request.user: show_dashboard = True all_forms = content_user.xforms.count() - form = QuickConverterFile() - form_url = QuickConverterURL() request_url = request.build_absolute_uri( "/%s" % request.user.username) @@ -264,7 +102,6 @@ def set_form(): 'all_forms': all_forms, 'show_dashboard': show_dashboard, 'form': form, - 'form_url': form_url, 'url': url, 'user_xforms': user_xforms, 'xforms_list': xforms_list, @@ -276,75 +113,6 @@ def set_form(): return render(request, "profile.html", data) -def members_list(request): - if not request.user.is_staff and not request.user.is_superuser: - return HttpResponseForbidden(_('Forbidden.')) - users = User.objects.all() - template = 'people.html' - - return render(request, template, {'template': template, 'users': users}) - - -@login_required -def profile_settings(request, username): - content_user = check_and_set_user(request, username) - profile, created = UserProfile.objects.get_or_create(user=content_user) - if request.method == 'POST': - form = UserProfileForm(request.POST, instance=profile) - if form.is_valid(): - # get user - # user.email = cleaned_email - ''' - The email field has been removed from the profile form. - form.instance.user.email = form.cleaned_data['email'] - form.instance.user.save() - ''' - form.save() - # todo: add string rep. of settings to see what changed - audit = {} - audit_log( - Actions.PROFILE_SETTINGS_UPDATED, request.user, content_user, - _("Profile settings updated."), audit, request) - return HttpResponseRedirect(reverse( - public_profile, kwargs={'username': request.user.username} - )) - else: - form = UserProfileForm( - instance=profile, initial={"email": content_user.email}) - - return render(request, "settings.html", - {'content_user': content_user, 'form': form}) - - -@require_GET -def public_profile(request, username): - content_user = check_and_set_user(request, username) - if isinstance(content_user, HttpResponseRedirect): - return content_user - data = {} - set_profile_data(data, content_user) - data['is_owner'] = request.user == content_user - audit = {} - audit_log( - Actions.PUBLIC_PROFILE_ACCESSED, request.user, content_user, - _("Public profile accessed."), audit, request) - - return render(request, "profile.html", data) - - -@login_required -def dashboard(request): - content_user = request.user - data = { - 'form': QuickConverter(), - 'content_user': content_user, - 'url': request.build_absolute_uri("/%s" % request.user.username) - } - set_profile_data(data, content_user) - - return render(request, "dashboard.html", data) - - def redirect_to_public_link(request, uuid): xform = get_object_or_404(XForm, uuid=uuid) request.session['public_link'] = \ @@ -356,48 +124,6 @@ def redirect_to_public_link(request, uuid): })) -def set_xform_owner_data(data, xform, request, username, id_string): - data['sms_support_form'] = ActivateSMSSupportFom( - initial={'enable_sms_support': xform.allows_sms, - 'sms_id_string': xform.sms_id_string}) - if not xform.allows_sms: - data['sms_compatible'] = check_form_sms_compatibility( - None, json_survey=json.loads(xform.json)) - else: - url_root = request.build_absolute_uri('/')[:-1] - data['sms_providers_doc'] = providers_doc( - url_root=url_root, - username=username, - id_string=id_string) - data['url_root'] = url_root - - data['form_license_form'] = FormLicenseForm( - initial={'value': data['form_license']}) - data['data_license_form'] = DataLicenseForm( - initial={'value': data['data_license']}) - data['doc_form'] = SupportDocForm() - data['source_form'] = SourceForm() - data['media_form'] = MediaForm() - users_with_perms = [] - - for perm in get_users_with_perms(xform, attach_perms=True).items(): - has_perm = [] - if CAN_CHANGE_XFORM in perm[1]: - has_perm.append(_("Can Edit")) - if CAN_VIEW_XFORM in perm[1]: - has_perm.append(_("Can View")) - if CAN_ADD_SUBMISSIONS in perm[1]: - has_perm.append(_("Can submit to")) - if CAN_VALIDATE_XFORM in perm[1]: - has_perm.append(_(u"Can Validate")) - if CAN_DELETE_DATA_XFORM in perm[1]: - has_perm.append(_(u"Can Delete Data")) - users_with_perms.append((perm[0], " | ".join(has_perm))) - - data['users_with_perms'] = users_with_perms - data['permission_form'] = PermissionForm(username) - - @require_GET def show(request, username=None, id_string=None, uuid=None): if uuid: @@ -422,17 +148,10 @@ def show(request, username=None, id_string=None, uuid=None): data['xform'] = xform data['content_user'] = xform.user data['base_url'] = "https://%s" % request.get_host() - data['source'] = MetaData.source(xform) - data['form_license'] = MetaData.form_license(xform).data_value - data['data_license'] = MetaData.data_license(xform).data_value - data['supporting_docs'] = MetaData.supporting_docs(xform) data['media_upload'] = MetaData.media_upload(xform) if is_owner: - set_xform_owner_data(data, xform, request, username, id_string) - - if xform.allows_sms: - data['sms_support_doc'] = get_autodoc_for(xform) + data['media_form'] = MediaForm() return render(request, "show.html", data) @@ -463,16 +182,10 @@ def show_form_settings(request, username=None, id_string=None, uuid=None): data['content_user'] = xform.user data['base_url'] = "https://%s" % request.get_host() data['source'] = MetaData.source(xform) - data['form_license'] = MetaData.form_license(xform).data_value - data['data_license'] = MetaData.data_license(xform).data_value - data['supporting_docs'] = MetaData.supporting_docs(xform) data['media_upload'] = MetaData.media_upload(xform) if is_owner: - set_xform_owner_data(data, xform, request, username, id_string) - - if xform.allows_sms: - data['sms_support_doc'] = get_autodoc_for(xform) + data['media_form'] = MediaForm() return render(request, "show_form_settings.html", data) @@ -490,91 +203,6 @@ def api_token(request, username=None): return HttpResponseForbidden(_('Permission denied.')) -@require_http_methods(["GET", "OPTIONS"]) -def api(request, username=None, id_string=None): - """ - Returns all results as JSON. If a parameter string is passed, - it takes the 'query' parameter, converts this string to a dictionary, an - that is then used as a MongoDB query string. - - NOTE: only a specific set of operators are allow, currently $or and $and. - Please send a request if you'd like another operator to be enabled. - - NOTE: Your query must be valid JSON, double check it here, - http://json.parser.online.fr/ - - E.g. api?query='{"last_name": "Smith"}' - """ - if request.method == "OPTIONS": - response = HttpResponse() - add_cors_headers(response) - - return response - helper_auth_helper(request) - helper_auth_helper(request) - xform, owner = check_and_set_user_and_form(username, id_string, request) - - if not xform: - return HttpResponseForbidden(_('Not shared.')) - - try: - args = { - 'username': username, - 'id_string': id_string, - 'query': request.GET.get('query'), - 'fields': request.GET.get('fields'), - 'sort': request.GET.get('sort') - } - if 'start' in request.GET: - args["start"] = int(request.GET.get('start')) - if 'limit' in request.GET: - args["limit"] = int(request.GET.get('limit')) - if 'count' in request.GET: - args["count"] = True if int(request.GET.get('count')) > 0\ - else False - cursor = ParsedInstance.query_mongo(**args) - except ValueError as e: - return HttpResponseBadRequest(e.__str__()) - - records = list(record for record in cursor) - response_text = json_util.dumps(records) - - if 'callback' in request.GET and request.GET.get('callback') != '': - callback = request.GET.get('callback') - response_text = ("%s(%s)" % (callback, response_text)) - - response = HttpResponse(response_text, content_type='application/json') - add_cors_headers(response) - - return response - - -@require_GET -def public_api(request, username, id_string): - """ - Returns public information about the form as JSON - """ - - xform = get_object_or_404(XForm, - user__username__iexact=username, - id_string__exact=id_string) - - _DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' - exports = {'username': xform.user.username, - 'id_string': xform.id_string, - 'shared': xform.shared, - 'shared_data': xform.shared_data, - 'downloadable': xform.downloadable, - 'title': xform.title, - 'date_created': xform.date_created.strftime(_DATETIME_FORMAT), - 'date_modified': xform.date_modified.strftime(_DATETIME_FORMAT), - 'uuid': xform.uuid, - } - response_text = json.dumps(exports) - - return HttpResponse(response_text, content_type='application/json') - - @login_required def edit(request, username, id_string): xform = XForm.objects.get(user__username__iexact=username, @@ -583,160 +211,8 @@ def edit(request, username, id_string): if username == request.user.username or\ request.user.has_perm('logger.change_xform', xform): - if request.POST.get('description'): - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Description for '%(id_string)s' updated from " - "'%(old_description)s' to '%(new_description)s'.") % - { - 'id_string': xform.id_string, - 'old_description': xform.description, - 'new_description': request.POST['description'] - }, audit, request) - xform.description = request.POST['description'] - elif request.POST.get('title'): - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Title for '%(id_string)s' updated from " - "'%(old_title)s' to '%(new_title)s'.") % - { - 'id_string': xform.id_string, - 'old_title': xform.title, - 'new_title': request.POST.get('title') - }, audit, request) - xform.title = request.POST['title'] - elif request.POST.get('toggle_shared'): - if request.POST['toggle_shared'] == 'data': - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Data sharing updated for '%(id_string)s' from " - "'%(old_shared)s' to '%(new_shared)s'.") % - { - 'id_string': xform.id_string, - 'old_shared': _("shared") - if xform.shared_data else _("not shared"), - 'new_shared': _("shared") - if not xform.shared_data else _("not shared") - }, audit, request) - xform.shared_data = not xform.shared_data - elif request.POST['toggle_shared'] == 'form': - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Form sharing for '%(id_string)s' updated " - "from '%(old_shared)s' to '%(new_shared)s'.") % - { - 'id_string': xform.id_string, - 'old_shared': _("shared") - if xform.shared else _("not shared"), - 'new_shared': _("shared") - if not xform.shared else _("not shared") - }, audit, request) - xform.shared = not xform.shared - elif request.POST['toggle_shared'] == 'active': - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Active status for '%(id_string)s' updated from " - "'%(old_shared)s' to '%(new_shared)s'.") % - { - 'id_string': xform.id_string, - 'old_shared': _("shared") - if xform.downloadable else _("not shared"), - 'new_shared': _("shared") - if not xform.downloadable else _("not shared") - }, audit, request) - xform.downloadable = not xform.downloadable - elif request.POST.get('form-license'): - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Form License for '%(id_string)s' updated to " - "'%(form_license)s'.") % - { - 'id_string': xform.id_string, - 'form_license': request.POST['form-license'], - }, audit, request) - MetaData.form_license(xform, request.POST['form-license']) - elif request.POST.get('data-license'): - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Data license for '%(id_string)s' updated to " - "'%(data_license)s'.") % - { - 'id_string': xform.id_string, - 'data_license': request.POST['data-license'], - }, audit, request) - MetaData.data_license(xform, request.POST['data-license']) - elif request.POST.get('source') or request.FILES.get('source'): - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Source for '%(id_string)s' updated to '%(source)s'.") % - { - 'id_string': xform.id_string, - 'source': request.POST.get('source'), - }, audit, request) - MetaData.source(xform, request.POST.get('source'), - request.FILES.get('source')) - elif request.POST.get('enable_sms_support_trigger') is not None: - sms_support_form = ActivateSMSSupportFom(request.POST) - if sms_support_form.is_valid(): - audit = { - 'xform': xform.id_string - } - enabled = \ - sms_support_form.cleaned_data.get('enable_sms_support') - if enabled: - audit_action = Actions.SMS_SUPPORT_ACTIVATED - audit_message = _("SMS Support Activated on") - else: - audit_action = Actions.SMS_SUPPORT_DEACTIVATED - audit_message = _("SMS Support Deactivated on") - audit_log( - audit_action, request.user, owner, - audit_message - % {'id_string': xform.id_string}, audit, request) - # stored previous states to be able to rollback form status - # in case we can't save. - pe = xform.allows_sms - pid = xform.sms_id_string - xform.allows_sms = enabled - xform.sms_id_string = \ - sms_support_form.cleaned_data.get('sms_id_string') - compat = check_form_sms_compatibility(None, - json.loads(xform.json)) - if compat['type'] == 'alert-error': - xform.allows_sms = False - xform.sms_id_string = pid - try: - xform.save() - except IntegrityError: - # unfortunately, there's no feedback mechanism here - xform.allows_sms = pe - xform.sms_id_string = pid - elif request.POST.get('media_url'): + if request.POST.get('media_url'): uri = request.POST.get('media_url') try: SSRFProtect.validate(uri) @@ -756,17 +232,6 @@ def edit(request, username, id_string): }, audit, request) for aFile in request.FILES.getlist("media"): MetaData.media_upload(xform, aFile) - elif request.FILES.get('doc'): - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Supporting document added to '%(id_string)s'.") % - { - 'id_string': xform.id_string - }, audit, request) - MetaData.supporting_docs(xform, request.FILES.get('doc')) xform.update() @@ -784,181 +249,6 @@ def edit(request, username, id_string): return HttpResponseForbidden(_('Update failed.')) -def getting_started(request): - template = 'getting_started.html' - - return render(request, 'base.html', {'template': template}) - - -def support(request): - template = 'support.html' - - return render(request, 'base.html', {'template': template}) - - -def faq(request): - template = 'faq.html' - - return render(request, 'base.html', {'template': template}) - - -def xls2xform(request): - template = 'xls2xform.html' - - return render(request, 'base.html', {'template': template}) - - -def tutorial(request): - template = 'tutorial.html' - username = request.user.username if request.user.username else \ - 'your-user-name' - url = request.build_absolute_uri("/%s" % username) - - return render(request, 'base.html', {'template': template, 'url': url}) - - -def resources(request): - if 'fr' in request.LANGUAGE_CODE.lower(): - deck_id = 'a351f6b0a3730130c98b12e3c5740641' - else: - deck_id = '1a33a070416b01307b8022000a1de118' - - return render(request, 'resources.html', {'deck_id': deck_id}) - - -def about_us(request): - a_flatpage = '/about-us/' - username = request.user.username if request.user.username else \ - 'your-user-name' - url = request.build_absolute_uri("/%s" % username) - - return render(request, 'base.html', {'a_flatpage': a_flatpage, 'url': url}) - - -def privacy(request): - template = 'privacy.html' - - return render(request, 'base.html', {'template': template}) - - -def tos(request): - template = 'tos.html' - - return render(request, 'base.html', {'template': template}) - - -def syntax(request): - template = 'syntax.html' - - return render(request, 'base.html', {'template': template}) - - -def form_gallery(request): - """ - Return a list of urls for all the shared xls files. This could be - made a lot prettier. - """ - data = {} - if request.user.is_authenticated(): - data['loggedin_user'] = request.user - data['shared_forms'] = XForm.objects.filter(shared=True) - # build list of shared forms with cloned suffix - id_strings_with_cloned_suffix = [ - x.id_string + XForm.CLONED_SUFFIX for x in data['shared_forms'] - ] - # build list of id_strings for forms this user has cloned - data['cloned'] = [ - x.id_string.split(XForm.CLONED_SUFFIX)[0] - for x in XForm.objects.filter( - user__username__iexact=request.user.username, - id_string__in=id_strings_with_cloned_suffix - ) - ] - - return render(request, 'form_gallery.html', data) - - -def download_metadata(request, username, id_string, data_id): - xform = get_object_or_404(XForm, - user__username__iexact=username, - id_string__exact=id_string) - owner = xform.user - if username == request.user.username or xform.shared: - data = get_object_or_404(MetaData, pk=data_id) - file_path = data.data_file.name - filename, extension = os.path.splitext(file_path.split('/')[-1]) - extension = extension.strip('.') - dfs = get_storage_class()() - if dfs.exists(file_path): - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Document '%(filename)s' for '%(id_string)s' downloaded.") % - { - 'id_string': xform.id_string, - 'filename': "%s.%s" % (filename, extension) - }, audit, request) - response = response_with_mimetype_and_name( - data.data_file_type, - filename, extension=extension, show_date=False, - file_path=file_path) - return response - else: - return HttpResponseNotFound() - - return HttpResponseForbidden(_('Permission denied.')) - - -@login_required() -def delete_metadata(request, username, id_string, data_id): - xform = get_object_or_404(XForm, - user__username__iexact=username, - id_string__exact=id_string) - owner = xform.user - data = get_object_or_404(MetaData, pk=data_id) - dfs = get_storage_class()() - req_username = request.user.username - if request.GET.get('del', False) and username == req_username: - try: - dfs.delete(data.data_file.name) - data.delete() - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Document '%(filename)s' deleted from '%(id_string)s'.") % - { - 'id_string': xform.id_string, - 'filename': os.path.basename(data.data_file.name) - }, audit, request) - return HttpResponseRedirect(reverse(show, kwargs={ - 'username': username, - 'id_string': id_string - })) - except Exception: - return HttpResponseServerError() - elif request.GET.get('map_name_del', False) and username == req_username: - data.delete() - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_UPDATED, request.user, owner, - _("Map layer deleted from '%(id_string)s'.") % - { - 'id_string': xform.id_string, - }, audit, request) - return HttpResponseRedirect(reverse(show, kwargs={ - 'username': username, - 'id_string': id_string - })) - - return HttpResponseForbidden(_('Permission denied.')) - - def download_media_data(request, username, id_string, data_id): xform = get_object_or_404( XForm, user__username__iexact=username, @@ -1068,328 +358,3 @@ def form_photos(request, username, id_string): data['profilei'], created = UserProfile.objects.get_or_create(user=owner) return render(request, 'form_photos.html', data) - - -@require_POST -def set_perm(request, username, id_string): - xform = get_object_or_404(XForm, - user__username__iexact=username, - id_string__exact=id_string) - owner = xform.user - if username != request.user.username\ - and not has_permission(xform, username, request): - return HttpResponseForbidden(_('Permission denied.')) - - try: - perm_type = request.POST['perm_type'] - for_user = request.POST['for_user'] - except KeyError: - return HttpResponseBadRequest() - - perms_by_type = { - 'edit': CAN_CHANGE_XFORM, - 'view': CAN_VIEW_XFORM, - 'report': CAN_ADD_SUBMISSIONS, - 'validate': CAN_VALIDATE_XFORM, - 'delete_data': CAN_DELETE_DATA_XFORM - } - - if perm_type in perms_by_type.keys() or perm_type == 'remove': - try: - user = User.objects.get(username=for_user) - except User.DoesNotExist: - messages.add_message( - request, messages.INFO, - _("Wrong username %s." % for_user), - extra_tags='alert-error') - else: - if perm_type == 'remove': - audit = { - 'xform': xform.id_string - } - audit_message = _("All permissions on '{id_string}' " - "removed from '{for_user}'.") - audit_log( - Actions.FORM_PERMISSIONS_UPDATED, - request.user, - owner, - audit_message.format( - id_string=xform.id_string, - for_user=for_user - ), - audit, - request - ) - remove_perm(CAN_CHANGE_XFORM, user, xform) - remove_perm(CAN_VIEW_XFORM, user, xform) - remove_perm(CAN_ADD_SUBMISSIONS, user, xform) - remove_perm(CAN_VALIDATE_XFORM, user, xform) - remove_perm(CAN_DELETE_DATA_XFORM, user, xform) - - elif not user.has_perm(perms_by_type[perm_type], xform): - audit = { - 'xform': xform.id_string - } - audit_message = _("'{codename}' permission on '{id_string}' " - "assigned to '{for_user}'.") - audit_log( - Actions.FORM_PERMISSIONS_UPDATED, - request.user, - owner, - audit_message.format( - codename=perms_by_type[perm_type], - id_string=xform.id_string, - for_user=for_user - ), - audit, - request - ) - - assign_perm(perms_by_type[perm_type], user, xform) - - elif perm_type == 'link': - current = MetaData.public_link(xform) - if for_user == 'all': - MetaData.public_link(xform, True) - elif for_user == 'none': - MetaData.public_link(xform, False) - elif for_user == 'toggle': - MetaData.public_link(xform, not current) - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_PERMISSIONS_UPDATED, request.user, owner, - _("Public link on '%(id_string)s' %(action)s.") % - { - 'id_string': xform.id_string, - 'action': "created" - if for_user == "all" or - (for_user == "toggle" and not current) else "removed" - }, audit, request) - - if request.is_ajax(): - return HttpResponse( - json.dumps( - {'status': 'success'}), content_type='application/json') - if 'HTTP_REFERER' in request.META and request.META['HTTP_REFERER'].strip(): - return HttpResponseRedirect(request.META['HTTP_REFERER']) - - return HttpResponseRedirect(reverse(show, kwargs={ - 'username': username, - 'id_string': id_string - })) - - -@require_POST -@login_required -def delete_data(request, username=None, id_string=None): - xform, owner = check_and_set_user_and_form(username, id_string, request) - response_text = '' - if not xform or not has_delete_data_permission(xform, owner, request): - return HttpResponseForbidden(_('Not shared.')) - - data_id = request.POST.get('id') - if not data_id: - return HttpResponseBadRequest(_("id must be specified")) - - Instance.set_deleted_at(data_id) - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.SUBMISSION_DELETED, request.user, owner, - _("Deleted submission with id '%(record_id)s' " - "on '%(id_string)s'.") % - { - 'id_string': xform.id_string, - 'record_id': data_id - }, audit, request) - response_text = json.dumps({"success": "Deleted data %s" % data_id}) - if 'callback' in request.GET and request.GET.get('callback') != '': - callback = request.GET.get('callback') - response_text = ("%s(%s)" % (callback, response_text)) - - return HttpResponse(response_text, content_type='application/json') - - -@require_POST -@is_owner -def update_xform(request, username, id_string): - xform = get_object_or_404( - XForm, user__username__iexact=username, id_string__exact=id_string) - owner = xform.user - - def set_form(): - form = QuickConverter(request.POST, request.FILES) - survey = form.publish(request.user, id_string).survey - enketo_webform_url = reverse( - enter_data, - kwargs={'username': username, 'id_string': survey.id_string} - ) - audit = { - 'xform': xform.id_string - } - audit_log( - Actions.FORM_XLS_UPDATED, request.user, owner, - _("XLS for '%(id_string)s' updated.") % - { - 'id_string': xform.id_string, - }, audit, request) - return { - 'type': 'alert-success', - 'text': _('Successfully published %(form_id)s.' - ' Enter Web Form' - ' or ' - 'Preview Web Form') - % {'form_id': survey.id_string, - 'form_url': enketo_webform_url} - } - message = publish_form(set_form) - messages.add_message( - request, messages.INFO, message['text'], extra_tags=message['type']) - - return HttpResponseRedirect(reverse(show, kwargs={ - 'username': username, - 'id_string': id_string - })) - - -@is_owner -def activity(request, username): - owner = get_object_or_404(User, username=username) - - return render(request, 'activity.html', {'user': owner}) - - -def activity_fields(request): - fields = [ - { - 'id': 'created_on', - 'label': _('Performed On'), - 'type': 'datetime', - 'searchable': False - }, - { - 'id': 'action', - 'label': _('Action'), - 'type': 'string', - 'searchable': True, - 'options': sorted([Actions[e] for e in Actions.enums]) - }, - { - 'id': 'user', - 'label': 'Performed By', - 'type': 'string', - 'searchable': True - }, - { - 'id': 'msg', - 'label': 'Description', - 'type': 'string', - 'searchable': True - }, - ] - response_text = json.dumps(fields) - - return HttpResponse(response_text, content_type='application/json') - - -@is_owner -def activity_api(request, username): - from bson.objectid import ObjectId - - def stringify_unknowns(obj): - if isinstance(obj, ObjectId): - return str(obj) - if isinstance(obj, datetime): - return obj.strftime(DATETIME_FORMAT) - return None - try: - query_args = { - 'username': username, - 'query': json.loads(request.GET.get('query')) - if request.GET.get('query') else {}, - 'fields': json.loads(request.GET.get('fields')) - if request.GET.get('fields') else [], - 'sort': json.loads(request.GET.get('sort')) - if request.GET.get('sort') else {} - } - if 'start' in request.GET: - query_args["start"] = int(request.GET.get('start')) - if 'limit' in request.GET: - query_args["limit"] = int(request.GET.get('limit')) - if 'count' in request.GET: - query_args["count"] = True \ - if int(request.GET.get('count')) > 0 else False - cursor = AuditLog.query_mongo(**query_args) - except ValueError as e: - return HttpResponseBadRequest(e.__str__()) - - records = list(record for record in cursor) - response_text = json.dumps(records, default=stringify_unknowns) - if 'callback' in request.GET and request.GET.get('callback') != '': - callback = request.GET.get('callback') - response_text = ("%s(%s)" % (callback, response_text)) - - return HttpResponse(response_text, content_type='application/json') - - -def qrcode(request, username, id_string): - try: - formhub_url = "http://%s/" % request.get_host() - except: - formhub_url = "http://formhub.org/" - formhub_url = formhub_url + username - - if settings.TESTING_MODE: - formhub_url = "https://{}/{}".format(settings.TEST_HTTP_HOST, - settings.TEST_USERNAME) - - results = _("Unexpected Error occured: No QRCODE generated") - status = 200 - try: - url = enketo_url(formhub_url, id_string) - except Exception as e: - error_msg = _("Error Generating QRCODE: %s" % e) - results = """
%s
""" % error_msg - status = 400 - else: - if url: - image = generate_qrcode(url) - results = """%s -
%s""" \ - % (image, url, url, url) - else: - status = 400 - - return HttpResponse(results, content_type='text/html', status=status) - - -def enketo_preview(request, username, id_string): - xform = get_object_or_404( - XForm, user__username__iexact=username, id_string__exact=id_string) - owner = xform.user - if not has_permission(xform, owner, request, xform.shared): - return HttpResponseForbidden(_('Not shared.')) - enekto_preview_url = \ - "%(enketo_url)s?server=%(profile_url)s&id=%(id_string)s" % { - 'enketo_url': settings.ENKETO_PREVIEW_URL, - 'profile_url': request.build_absolute_uri( - reverse(profile, kwargs={'username': owner.username})), - 'id_string': xform.id_string - } - return HttpResponseRedirect(enekto_preview_url) - - -@require_GET -@login_required -def username_list(request): - data = [] - query = request.GET.get('query', None) - if query: - users = User.objects.values('username')\ - .filter(username__startswith=query, is_active=True, pk__gte=0) - data = [user['username'] for user in users] - - return HttpResponse(json.dumps(data), content_type='application/json') diff --git a/onadata/apps/restservice/services/kpi_hook.py b/onadata/apps/restservice/services/kpi_hook.py index a15824a95..fd1ea3cd6 100644 --- a/onadata/apps/restservice/services/kpi_hook.py +++ b/onadata/apps/restservice/services/kpi_hook.py @@ -16,18 +16,21 @@ class ServiceDefinition(RestServiceInterface): def send(self, endpoint, data): + # Will be used internally by KPI to fetch data with KoBoCatBackend post_data = { - "instance_id": data.get("instance_id") # Will be used internally by KPI to fetch data with KoboCatBackend + "instance_id": data.get("instance_id") } headers = {"Content-Type": "application/json"} - # Verify if endpoint starts with `/assets/` before sending the request to`kpi` + # Verify if endpoint starts with `/assets/` before sending + # the request to KPI pattern = r'{}'.format(settings.KPI_HOOK_ENDPOINT_PATTERN.replace( '{asset_uid}', '[^/]*')) # Match v2 and v1 endpoints. if re.match(pattern, endpoint) or re.match(pattern[7:], endpoint): - # Build the url in the service to avoid saving hardcoded domain name in the DB + # Build the url in the service to avoid saving hardcoded + # domain name in the DB url = "{}{}".format( settings.KPI_INTERNAL_URL, endpoint @@ -36,8 +39,11 @@ def send(self, endpoint, data): response.raise_for_status() # Save successful - Instance.objects.filter(pk=data.get("instance_id")).update(posted_to_kpi=True) + Instance.objects.filter(pk=data.get("instance_id")).update( + posted_to_kpi=True + ) else: - logging.warning('This endpoint: `{}` is not valid for `KPI Hook`'.format( - endpoint - )) + logging.warning( + f'This endpoint: `{endpoint}` is not valid for `KPI Hook`' + ) + diff --git a/onadata/apps/sms_support/__init__.py b/onadata/apps/sms_support/__init__.py deleted file mode 100644 index 134880430..000000000 --- a/onadata/apps/sms_support/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import diff --git a/onadata/apps/sms_support/autodoc.py b/onadata/apps/sms_support/autodoc.py deleted file mode 100644 index 3542a0fbd..000000000 --- a/onadata/apps/sms_support/autodoc.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python -# vim: ai ts=4 sts=4 et sw=4 nu - -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -""" SMS Support Automatic Documentation (Web View) - - Provides the get_autodoc_for(xform) function. - It is used inside the main.views.show() view to display - an HTML documentation about how to fill the SMS for that form. - - Output is HTML ; not raw. Output uses only span markup with classes - so it should be somewhat easy to restyle """ - -import json -import datetime - -from .tools import ( - DEFAULT_SEPARATOR, - DEFAULT_ALLOW_MEDIA, - MEDIA_TYPES, - DEFAULT_DATE_FORMAT, - DEFAULT_DATETIME_FORMAT -) - - -def get_sample_data_for(question, json_survey, as_names=False): - - """ return an example data for a particular question. - - If as_names is True, returns name (not sms_field) of the question """ - - xlsf_name = question.get('name') - xlsf_type = question.get('type') - xlsf_choices = question.get('children') - now = datetime.datetime.now() - xlsf_date_fmt = json_survey.get('sms_date_format', DEFAULT_DATE_FORMAT) \ - or DEFAULT_DATE_FORMAT - xlsf_datetime_fmt = json_survey.get('sms_date_format', - DEFAULT_DATETIME_FORMAT) \ - or DEFAULT_DATETIME_FORMAT - - def safe_wrap(value): - return unicode(value) - - if as_names: - return xlsf_name - - if xlsf_type == 'text': - return safe_wrap('lorem ipsum') - elif xlsf_type == 'integer': - return safe_wrap(4) - elif xlsf_type == 'decimal': - return safe_wrap(1.2) - elif xlsf_type == 'select one': - return safe_wrap(' '.join([c.get('sms_option') - for c in xlsf_choices][:1])) - elif xlsf_type == 'select all that apply': - return safe_wrap(' '.join([c.get('sms_option') - for c in xlsf_choices][:2])) - elif xlsf_type == 'geopoint': - return safe_wrap('12.65 -8') - elif xlsf_type in MEDIA_TYPES: - exts = {'audio': 'mp3', 'video': 'avi', 'photo': 'jpg'} - return safe_wrap('x.%s;dGhpc' % exts.get(xlsf_type, 'ext')) - elif xlsf_type == 'barcode': - return safe_wrap('abc') - elif xlsf_type == 'date': - return safe_wrap(now.strftime(xlsf_date_fmt)) - elif xlsf_type == 'datetime': - return safe_wrap(now.strftime(xlsf_datetime_fmt)) - elif xlsf_type == 'note': - return None - else: - return safe_wrap('?') - - -def get_helper_text(question, json_survey): - - """ The full sentence (html) of the helper for a question - - Includes the type, a description - and potentialy accepted values or format """ - - xlsf_type = question.get('type') - xlsf_choices = question.get('children') - xlsf_date_fmt = json_survey.get('sms_date_format', DEFAULT_DATE_FORMAT) \ - or DEFAULT_DATE_FORMAT - xlsf_datetime_fmt = json_survey.get('sms_date_format', - DEFAULT_DATETIME_FORMAT) \ - or DEFAULT_DATETIME_FORMAT - separator = json_survey.get('sms_separator', DEFAULT_SEPARATOR) \ - or DEFAULT_SEPARATOR - - def safe_wrap(value, xlsf_type=xlsf_type): - value = ('%(type)s ' - '%(text)s' - % {'type': xlsf_type, 'text': value}) - return unicode(value) - - if xlsf_type == 'text': - return safe_wrap('Any string (excluding "%s")' % separator) - elif xlsf_type == 'integer': - return safe_wrap('Any integer digit.') - elif xlsf_type == 'decimal': - return safe_wrap('A decimal or integer value.') - elif xlsf_type == 'select one': - helper = 'Select one of the following:' - helper += '
    ' - helper += ''.join(['
  • ' - '%(sms_option)s %(label)s
  • ' - % {'sms_option': c.get('sms_option'), - 'label': c.get('label')} - for c in xlsf_choices]) - helper += '
' - return safe_wrap(helper) - elif xlsf_type == 'select all that apply': - helper = 'Select none, one or more in:' - helper += '
    ' - helper += ''.join(['
  • ' - '%(sms_option)s %(label)s
  • ' - % {'sms_option': c.get('sms_option'), - 'label': c.get('label')} - for c in xlsf_choices]) - helper += '
' - return safe_wrap(helper) - elif xlsf_type == 'geopoint': - helper = ('GPS coordinates as ' - 'latitude longitude.' - '
Optionnaly add ' - 'altitude precision after. All of them are decimal.') - return safe_wrap(helper) - elif xlsf_type in MEDIA_TYPES: - exts = {'audio': 'mp3', 'video': 'avi', 'photo': 'jpg'} - helper = ('File name and base64 data of the file as in ' - 'x.%s;dGhpc.' - '
It is not intented to be filled by ' - 'humans.' % exts.get(xlsf_type, 'ext')) - return safe_wrap(helper) - elif xlsf_type == 'barcode': - return safe_wrap('A string representing the value behind the barcode.') - elif xlsf_type == 'date': - return safe_wrap('A date in the format: ' - '%s' - % xlsf_date_fmt) - elif xlsf_type == 'datetime': - return safe_wrap('A datetime in the format: ' - '%s' - % xlsf_datetime_fmt) - else: - return safe_wrap('?') - - -def get_autodoc_for(xform): - - """ The generated documentation in a dict (HTML output) - - line_names: example line filled with question line_names - line_values: example line filled with fake (yet valid) data - helpers: list of tuples (name, text) of helper texts. - - Helper texts are based on type of question and accepted values """ - - json_survey = json.loads(xform.json) - - # setup formatting values - separator = json_survey.get('sms_separator', DEFAULT_SEPARATOR) \ - or DEFAULT_SEPARATOR - sms_allow_media = bool(json_survey.get('sms_allow_media', - DEFAULT_ALLOW_MEDIA) or DEFAULT_ALLOW_MEDIA) - - helpers = [] - line_names = ('%(keyword)s' - '%(qid)d ' - % {'keyword': xform.sms_id_string, 'qid': len(helpers)}) - line_values = ('%(keyword)s ' - % {'keyword': xform.sms_id_string}) - helpers.append(('keyword', 'The keyword used to identify the form.' - '
Omit if using a form-aware URL.')) - - for group in json_survey.get('children', {}): - sms_field = group.get('sms_field', '') - if not sms_field or sms_field.lower() == 'meta': - continue - - line_values += ('' - '%(sep)s%(sms_field)s ' - % {'sep': separator, - 'sms_field': group.get('sms_field')}) - line_names += ('' - '%(sep)s%(sms_field)s ' - % {'sep': separator, - 'sms_field': group.get('sms_field')}) - - for question in group.get('children', {}): - type_id = question.get('type') - - if type_id in MEDIA_TYPES and not sms_allow_media: - continue - - qid = len(helpers) - sample = get_sample_data_for(question, json_survey) - if sample is None: - continue - sample_name = get_sample_data_for(question, json_survey, - as_names=True) - - line_values += ('%(value)s ' - % {'type': type_id, 'value': sample}) - line_names += ('%(value)s' - '%(h)s ' - % {'type': type_id, - 'value': sample_name, 'h': qid}) - helpers.append((sample_name, - get_helper_text(question, json_survey))) - - line_values += '' - - return {'line_values': line_values, - 'line_names': line_names, - 'helpers': helpers} diff --git a/onadata/apps/sms_support/models.py b/onadata/apps/sms_support/models.py deleted file mode 100644 index 134880430..000000000 --- a/onadata/apps/sms_support/models.py +++ /dev/null @@ -1,2 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import diff --git a/onadata/apps/sms_support/parser.py b/onadata/apps/sms_support/parser.py deleted file mode 100644 index b7013a7ef..000000000 --- a/onadata/apps/sms_support/parser.py +++ /dev/null @@ -1,381 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import base64 -from datetime import datetime, date -import json -import re -import StringIO - -from django.utils.translation import ugettext as _ - -from onadata.apps.logger.models import XForm -from onadata.apps.sms_support.tools import SMS_API_ERROR, SMS_PARSING_ERROR,\ - SMS_SUBMISSION_REFUSED, sms_media_to_file, generate_instance,\ - DEFAULT_SEPARATOR, NA_VALUE, META_FIELDS, MEDIA_TYPES,\ - DEFAULT_DATE_FORMAT, DEFAULT_DATETIME_FORMAT, SMS_SUBMISSION_ACCEPTED,\ - is_last -from onadata.libs.utils.logger_tools import dict2xform - - -class SMSSyntaxError(ValueError): - pass - - -class SMSCastingError(ValueError): - - def __init__(self, message, question=None): - if question: - message = _("%(question)s: %(message)s") % {'question': question, - 'message': message} - super(SMSCastingError, self).__init__(message) - - -def parse_sms_text(xform, identity, text): - - json_survey = json.loads(xform.json) - - separator = json_survey.get('sms_separator', DEFAULT_SEPARATOR) \ - or DEFAULT_SEPARATOR - - try: - allow_media = bool(json_survey.get('sms_allow_media', False)) - except: - raise - allow_media = False - - xlsf_date_fmt = json_survey.get('sms_date_format', DEFAULT_DATE_FORMAT) \ - or DEFAULT_DATE_FORMAT - xlsf_datetime_fmt = json_survey.get('sms_date_format', - DEFAULT_DATETIME_FORMAT) \ - or DEFAULT_DATETIME_FORMAT - - # extract SMS data into indexed groups of values - groups = {} - for group in text.split(separator)[1:]: - group_id, group_text = [s.strip() for s in group.split(None, 1)] - groups.update({group_id: [s.strip() for s in group_text.split(None)]}) - - def cast_sms_value(value, question, medias=[]): - ''' Check data type of value and return cleaned version ''' - - xlsf_type = question.get('type') - xlsf_name = question.get('name') - xlsf_choices = question.get('children') - xlsf_required = bool(question.get('bind', {}) - .get('required', '').lower() in ('yes', 'true')) - - # we don't handle constraint for now as it's a little complex and - # unsafe. - # xlsf_constraint=question.get('constraint') - - if xlsf_required and not len(value): - raise SMSCastingError(_("Required field missing"), xlsf_name) - - def safe_wrap(func): - try: - return func() - except Exception as e: - raise SMSCastingError(_("%(error)s") % {'error': e}, - xlsf_name) - - def media_value(value, medias): - ''' handle media values - - extract name and base64 data. - fills the media holder with (name, data) tuple ''' - try: - filename, b64content = value.split(';', 1) - medias.append((filename, - base64.b64decode(b64content))) - return filename - except Exception as e: - raise SMSCastingError(_("Media file format incorrect. %(except)s") - % {'except': repr(e)}, xlsf_name) - - if xlsf_type == 'text': - return safe_wrap(lambda: unicode(value)) - elif xlsf_type == 'integer': - return safe_wrap(lambda: int(value)) - elif xlsf_type == 'decimal': - return safe_wrap(lambda: float(value)) - elif xlsf_type == 'select one': - for choice in xlsf_choices: - if choice.get('sms_option') == value: - return choice.get('name') - raise SMSCastingError(_("No matching choice " - "for '%(input)s'") - % {'input': value}, - xlsf_name) - elif xlsf_type == 'select all that apply': - values = [s.strip() for s in value.split()] - ret_values = [] - for indiv_value in values: - for choice in xlsf_choices: - if choice.get('sms_option') == indiv_value: - ret_values.append(choice.get('name')) - return " ".join(ret_values) - elif xlsf_type == 'geopoint': - err_msg = _("Incorrect geopoint coordinates.") - geodata = [s.strip() for s in value.split()] - if len(geodata) < 2 and len(geodata) > 4: - raise SMSCastingError(err_msg, xlsf_name) - try: - # check that latitude and longitude are floats - lat, lon = [float(v) for v in geodata[:2]] - # and within sphere boundaries - if lat < -90 or lat > 90 or lon < -180 and lon > 180: - raise SMSCastingError(err_msg, xlsf_name) - if len(geodata) == 4: - # check that altitude and accuracy are integers - [int(v) for v in geodata[2:4]] - elif len(geodata) == 3: - # check that altitude is integer - int(geodata[2]) - except Exception as e: - raise SMSCastingError(e.message, xlsf_name) - return " ".join(geodata) - - elif xlsf_type in MEDIA_TYPES: - # media content (image, video, audio) must be formatted as: - # file_name;base64 encodeed content. - # Example: hello.jpg;dGhpcyBpcyBteSBwaWN0dXJlIQ== - return media_value(value, medias) - elif xlsf_type == 'barcode': - return safe_wrap(lambda: unicode(value)) - elif xlsf_type == 'date': - return safe_wrap(lambda: datetime.strptime(value, - xlsf_date_fmt).date()) - elif xlsf_type == 'datetime': - return safe_wrap(lambda: datetime.strptime(value, - xlsf_datetime_fmt)) - elif xlsf_type == 'note': - return safe_wrap(lambda: '') - raise SMSCastingError(_("Unsuported column '%(type)s'") - % {'type': xlsf_type}, xlsf_name) - - def get_meta_value(xlsf_type, identity): - ''' XLSForm Meta field value ''' - if xlsf_type in ('deviceid', 'subscriberid', 'imei'): - return NA_VALUE - elif xlsf_type in ('start', 'end'): - return datetime.now().isoformat() - elif xlsf_type == 'today': - return date.today().isoformat() - elif xlsf_type == 'phonenumber': - return identity - return NA_VALUE - - # holder for all properly formated answers - survey_answers = {} - # list of (name, data) tuples for media contents - medias = [] - # keep track of required questions - notes = [] - - # loop on all XLSForm questions - for expected_group in json_survey.get('children', [{}]): - if not expected_group.get('type') == 'group': - # non-grouped questions are not valid for SMS - continue - - # retrieve part of SMS text for this group - group_id = expected_group.get('sms_field') - answers = groups.get(group_id) - if not group_id or (not answers and not group_id.startswith('meta')): - # group is not meant to be filled by SMS - # or hasn't been filled - continue - - # Add a holder for this group's answers data - survey_answers.update({expected_group.get('name'): {}}) - - # retrieve question definition for each answer - egroups = expected_group.get('children', [{}]) - - # number of intermediate, omited questions (medias) - step_back = 0 - for idx, question in enumerate(egroups): - - real_value = None - - question_type = question.get('type') - if question_type in ('calculate'): - # 'calculate' question are not implemented. - # 'note' ones are just meant to be displayed on device - continue - - if question_type == 'note': - if not question.get('constraint', ''): - notes.append(question.get('label')) - continue - - if not allow_media and question_type in MEDIA_TYPES: - # if medias for SMS has not been explicitly allowed - # they are considered excluded. - step_back += 1 - continue - - # pop the number of skipped questions - # so that out index is valid even if the form - # contain medias questions (and medias are disabled) - sidx = idx - step_back - - if question_type in META_FIELDS: - # some question are not to be fed by users - real_value = get_meta_value(xlsf_type=question_type, - identity=identity) - else: - # actual SMS-sent answer. - # Only last answer/question of each group is allowed - # to have multiple spaces - if is_last(idx, egroups): - answer = " ".join(answers[idx:]) - else: - answer = answers[sidx] - - if real_value is None: - # retrieve actual value and fail if it doesn't meet reqs. - real_value = cast_sms_value(answer, - question=question, medias=medias) - - # set value to its question name - survey_answers[expected_group.get('name')] \ - .update({question.get('name'): real_value}) - - return survey_answers, medias, notes - - -def process_incoming_smses(username, incomings, - id_string=None): - ''' Process Incoming (identity, text[, id_string]) SMS ''' - - xforms = [] - medias = [] - xforms_notes = [] - responses = [] - json_submissions = [] - resp_str = {'success': _("[SUCCESS] Your submission has been accepted. " - "It's ID is {{ id }}.")} - - def process_incoming(incoming, id_string): - # assign variables - if len(incoming) >= 2: - identity = incoming[0].strip().lower() - text = incoming[1].strip().lower() - # if the tuple contain an id_string, use it, otherwise default - if len(incoming) and id_string is None >= 3: - id_string = incoming[2] - else: - responses.append({'code': SMS_API_ERROR, - 'text': _("Missing 'identity' " - "or 'text' field.")}) - return - - if not len(identity.strip()) or not len(text.strip()): - responses.append({'code': SMS_API_ERROR, - 'text': _("'identity' and 'text' fields can " - "not be empty.")}) - return - - # if no id_string has been supplied - # we expect the SMS to be prefixed with the form's sms_id_string - if id_string is None: - keyword, text = [s.strip() for s in text.split(None, 1)] - xform = XForm.objects.get(user__username=username, - sms_id_string=keyword) - else: - xform = XForm.objects.get(user__username=username, - id_string=id_string) - - if not xform.allows_sms: - responses.append({'code': SMS_SUBMISSION_REFUSED, - 'text': _("The form '%(id_string)s' does not " - "accept SMS submissions.") - % {'id_string': xform.id_string}}) - return - - # parse text into a dict object of groups with values - json_submission, medias_submission, notes = parse_sms_text(xform, - identity, - text) - - # retrieve sms_response if exist in the form. - json_survey = json.loads(xform.json) - if json_survey.get('sms_response'): - resp_str.update({'success': json_survey.get('sms_response')}) - - # check that the form contains at least one filled group - meta_groups = sum([1 for k in json_submission.keys() - if k.startswith('meta')]) - if len(json_submission.keys()) <= meta_groups: - responses.append({'code': SMS_PARSING_ERROR, - 'text': _("There must be at least one group of " - "questions filled.")}) - return - - # check that required fields have been filled - required_fields = [f.get('name') - for g in json_survey.get('children', {}) - for f in g.get('children', {}) - if f.get('bind', {}).get('required', 'no') == 'yes'] - submitted_fields = {} - for group in json_submission.values(): - submitted_fields.update(group) - - for field in required_fields: - if not submitted_fields.get(field): - responses.append({'code': SMS_SUBMISSION_REFUSED, - 'text': _("Required field `%(field)s` is " - "missing.") % {'field': field}}) - return - - # convert dict object into an XForm string - xml_submission = dict2xform(jsform=json_submission, - form_id=xform.id_string) - - # compute notes - data = {} - for g in json_submission.values(): - data.update(g) - for idx, note in enumerate(notes): - try: - notes[idx] = note.replace('${', '{').format(**data) - except: - pass - - # process_incoming expectes submission to be a file-like object - xforms.append(StringIO.StringIO(xml_submission)) - medias.append(medias_submission) - json_submissions.append(json_submission) - xforms_notes.append(notes) - - for incoming in incomings: - try: - process_incoming(incoming, id_string) - except Exception as e: - responses.append({'code': SMS_PARSING_ERROR, 'text': str(e)}) - - for idx, xform in enumerate(xforms): - # generate_instance expects media as a request.FILES.values() list - xform_medias = [sms_media_to_file(f, n) for n, f in medias[idx]] - # create the instance in the data base - response = generate_instance(username=username, - xml_file=xform, - media_files=xform_medias) - if response.get('code') == SMS_SUBMISSION_ACCEPTED: - success_response = re.sub(r'{{\s*[i,d,I,D]{2}\s*}}', - response.get('id'), - resp_str.get('success'), re.I) - - # extend success_response with data from the answers - data = {} - for g in json_submissions[idx].values(): - data.update(g) - success_response = success_response.replace('${', - '{').format(**data) - response.update({'text': success_response}) - # add sendouts (notes) - response.update({'sendouts': xforms_notes[idx]}) - responses.append(response) - - return responses diff --git a/onadata/apps/sms_support/providers/__init__.py b/onadata/apps/sms_support/providers/__init__.py deleted file mode 100644 index 3dedbbb44..000000000 --- a/onadata/apps/sms_support/providers/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python -# vim: ai ts=4 sts=4 et sw=4 nu -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - - -from django.http import HttpResponse -from django.views.decorators.csrf import csrf_exempt - -from .smssync import (import_submission as imp_sub_smssync, - import_submission_for_form as imp_sub_form_smssync, - autodoc as autodoc_smssync) -from .telerivet import (import_submission as imp_sub_telerivet, - import_submission_for_form as imp_sub_form_telerivet, - autodoc as autodoc_telerivet) -from .twilio import (import_submission as imp_sub_twilio, - import_submission_for_form as imp_sub_form_twilio, - autodoc as autodoc_twilio) -from .textit import (import_submission as imp_sub_textit, - import_submission_for_form as imp_sub_form_textit, - autodoc as autodoc_textit) - -SMSSYNC = 'smssync' -TELERIVET = 'telerivet' -TWILIO = 'twilio' -TEXTIT = 'textit' - -PROVIDERS = { - SMSSYNC: {'name': "SMS Sync", - 'imp': imp_sub_smssync, - 'imp_form': imp_sub_form_smssync, - 'doc': autodoc_smssync}, - TELERIVET: {'name': "Telerivet", - 'imp': imp_sub_telerivet, - 'imp_form': imp_sub_form_telerivet, - 'doc': autodoc_telerivet}, - TWILIO: {'name': "Twilio", - 'imp': imp_sub_twilio, - 'imp_form': imp_sub_form_twilio, - 'doc': autodoc_twilio}, - TEXTIT: {'name': "Text It", - 'imp': imp_sub_textit, - 'imp_form': imp_sub_form_textit, - 'doc': autodoc_textit} -} - - -def unknown_service(request, username=None, id_string=None): - """ 400 view for request with unknown service name """ - r = HttpResponse("Unknown SMS Gateway Service", content_type='text/plain') - r.status_code = 400 - return r - - -@csrf_exempt -def import_submission(request, username, service): - """ Proxy to the service's import_submission view """ - return PROVIDERS.get(service.lower(), {}).get( - 'imp', unknown_service)(request, username) - - -@csrf_exempt -def import_submission_for_form(request, username, id_string, service): - """ Proxy to the service's import_submission_for_form view """ - return PROVIDERS.get(service.lower(), {}).get( - 'imp_form', unknown_service)(request, username, id_string) - - -def providers_doc(url_root, username, id_string): - return [{'id': pid, - 'name': p.get('name'), - 'doc': p.get('doc')(url_root, username, id_string)} - for pid, p in PROVIDERS.items()] diff --git a/onadata/apps/sms_support/providers/smssync.py b/onadata/apps/sms_support/providers/smssync.py deleted file mode 100644 index 5506e7d89..000000000 --- a/onadata/apps/sms_support/providers/smssync.py +++ /dev/null @@ -1,122 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -""" Ushaidi's SMSSync gateway - - Supports Receiving and replying SMS from/to the SMSSync App. - - See: http://smssync.ushahidi.com/doc """ - -import json -import datetime - -from django.http import HttpResponse -from django.core.urlresolvers import reverse -from django.views.decorators.http import require_POST -from django.views.decorators.csrf import csrf_exempt -from django.utils.translation import ugettext as _ - -from onadata.apps.sms_support.tools import SMS_API_ERROR,\ - SMS_SUBMISSION_ACCEPTED -from onadata.apps.sms_support.parser import process_incoming_smses - - -def autodoc(url_root, username, id_string): - urla = url_root + reverse('sms_submission_api', - kwargs={'username': username, - 'service': 'smssync'}) - urlb = url_root + reverse('sms_submission_form_api', - kwargs={'username': username, - 'id_string': id_string, - 'service': 'smssync'}) - doc = ('

' + - _("%(service)s Instructions:") - % {'service': '' - 'Ushaidi\'s SMS Sync'} - + '

  1. ' + - _("Download the SMS Sync App on your phone serving as a gateway.") - + '
  2. ' + - _("Configure the app to point to one of the following URLs") - + '
    %(urla)s' - + '
    %(urlb)s

    ' + - _("Optionnaly set a keyword to prevent non-formhub " - "messages to be sent.") - + '
  3. ' + - _("In the preferences, tick the box to allow " - "replies from the server.") - + '

' + - _("That's it. Now Send an SMS Formhub submission to the number " - "of that phone. It will create a submission on Formhub.") - + '

') % {'urla': urla, 'urlb': urlb} - return doc - - -def get_response(data): - message = data.get('text') - if data.get('code') == SMS_API_ERROR: - success = False - message = None - elif data.get('code') != SMS_SUBMISSION_ACCEPTED: - success = True - message = _("[ERROR] %s") % message - else: - success = True - - response = { - "payload": { - "success": success, - "task": "send"}} - - if message: - messages = [{"to": data.get('identity'), "message": message}] - sendouts = data.get('sendouts', []) - if len(sendouts): - messages += [{"to": data.get('identity'), "message": text} - for text in sendouts] - response['payload'].update({"messages": messages}) - return HttpResponse(json.dumps(response), content_type='application/json') - - -@require_POST -@csrf_exempt -def import_submission(request, username): - """ Proxy to import_submission_for_form with None as id_string """ - return import_submission_for_form(request, username, None) - - -@require_POST -@csrf_exempt -def import_submission_for_form(request, username, id_string): - """ Retrieve and process submission from SMSSync Request """ - - sms_identity = request.POST.get('from', '').strip() - sms_text = request.POST.get('message', '').strip() - now_timestamp = datetime.datetime.now().strftime('%s') - sent_timestamp = request.POST.get('sent_timestamp', now_timestamp).strip() - try: - sms_time = datetime.datetime.fromtimestamp(float(sent_timestamp)) - except ValueError: - sms_time = datetime.datetime.now() - - return process_message_for_smssync(username=username, - sms_identity=sms_identity, - sms_text=sms_text, - sms_time=sms_time, - id_string=id_string) - - -def process_message_for_smssync(username, - sms_identity, sms_text, sms_time, id_string): - """ Process a text instance and return in SMSSync expected format """ - - if not sms_identity or not sms_text: - return get_response({'code': SMS_API_ERROR, - 'text': _("`identity` and `message` are " - "both required and must not be " - "empty.")}) - - incomings = [(sms_identity, sms_text)] - response = process_incoming_smses(username, incomings, id_string)[-1] - response.update({'identity': sms_identity}) - - return get_response(response) diff --git a/onadata/apps/sms_support/providers/telerivet.py b/onadata/apps/sms_support/providers/telerivet.py deleted file mode 100644 index 94c983602..000000000 --- a/onadata/apps/sms_support/providers/telerivet.py +++ /dev/null @@ -1,110 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -""" Telerivet WebHook gateway - - Supports Receiving and replying SMS from/to Telerivet Service - - See: http://telerivet.com/help/api/webhook/receiving """ - -import json -import datetime - -from django.http import HttpResponse -from django.core.urlresolvers import reverse -from django.views.decorators.http import require_POST -from django.views.decorators.csrf import csrf_exempt -from django.utils.translation import ugettext as _ - -from onadata.apps.sms_support.tools import SMS_API_ERROR,\ - SMS_SUBMISSION_ACCEPTED -from onadata.apps.sms_support.parser import process_incoming_smses - - -def autodoc(url_root, username, id_string): - urla = url_root + reverse('sms_submission_api', - kwargs={'username': username, - 'service': 'telerivet'}) - urlb = url_root + reverse('sms_submission_form_api', - kwargs={'username': username, - 'id_string': id_string, - 'service': 'telerivet'}) - doc = ('

' + - _("%(service)s Instructions:") - % {'service': '' - 'Telerivet\'s Webhook API'} - + '

  1. ' + - _("Sign in to Telerivet.com and go to Service Page.") - + '
  2. ' + - _("Follow instructions to add an application with either URL:") - + '
    %(urla)s' - + '
    %(urlb)s

    ' - + '

' + - _("That's it. Now Send an SMS Formhub submission to your Telerivet" - " phone number. It will create a submission on Formhub.") - + '

') % {'urla': urla, 'urlb': urlb} - return doc - - -def get_response(data): - - message = data.get('text') - - if data.get('code') == SMS_API_ERROR: - message = None - elif data.get('code') != SMS_SUBMISSION_ACCEPTED: - message = _("[ERROR] %s") % message - - response = {} - - if message: - messages = [{"content": message}] - sendouts = data.get('sendouts', []) - if len(sendouts): - messages += [{"content": text} for text in sendouts] - response.update({"messages": messages}) - return HttpResponse(json.dumps(response), content_type='application/json') - - -@require_POST -@csrf_exempt -def import_submission(request, username): - """ Proxy to import_submission_for_form with None as id_string """ - - return import_submission_for_form(request, username, None) - - -@require_POST -@csrf_exempt -def import_submission_for_form(request, username, id_string): - """ Retrieve and process submission from SMSSync Request """ - - sms_identity = request.POST.get('from_number', '').strip() - sms_text = request.POST.get('content', '').strip() - now_timestamp = datetime.datetime.now().strftime('%s') - sent_timestamp = request.POST.get('time_created', now_timestamp).strip() - try: - sms_time = datetime.datetime.fromtimestamp(float(sent_timestamp)) - except ValueError: - sms_time = datetime.datetime.now() - - return process_message_for_telerivet(username=username, - sms_identity=sms_identity, - sms_text=sms_text, - sms_time=sms_time, - id_string=id_string) - - -def process_message_for_telerivet(username, - sms_identity, sms_text, sms_time, id_string): - """ Process a text instance and return in SMSSync expected format """ - - if not sms_identity or not sms_text: - return get_response({'code': SMS_API_ERROR, - 'text': _("`identity` and `message` are " - "both required and must not be " - "empty.")}) - - incomings = [(sms_identity, sms_text)] - response = process_incoming_smses(username, incomings, id_string)[-1] - - return get_response(response) diff --git a/onadata/apps/sms_support/providers/textit.py b/onadata/apps/sms_support/providers/textit.py deleted file mode 100644 index ecbcd5402..000000000 --- a/onadata/apps/sms_support/providers/textit.py +++ /dev/null @@ -1,133 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -""" Nyaruka's TextIt gateway - - Supports Receiving and sending reply SMS from/to the TextIt App. - - See: https://textit.in/api/v1/webhook/ """ - -import datetime -import dateutil -import json - -from django.http import HttpResponse -from django.core.urlresolvers import reverse -from django.views.decorators.http import require_POST -from django.views.decorators.csrf import csrf_exempt -from django.utils.translation import ugettext as _ - -from onadata.apps.sms_support.tools import SMS_API_ERROR,\ - SMS_SUBMISSION_ACCEPTED -from onadata.apps.sms_support.parser import process_incoming_smses - -TEXTIT_URL = 'https://api.textit.in/api/v1/sms.json' - - -def autodoc(url_root, username, id_string): - urla = url_root + reverse('sms_submission_api', - kwargs={'username': username, - 'service': 'textit'}) - urlb = url_root + reverse('sms_submission_form_api', - kwargs={'username': username, - 'id_string': id_string, - 'service': 'textit'}) - doc = ('

' + - _("%(service)s Instructions:") - % {'service': '' - 'TextIt\'s Webhook API'} - + '

  1. ' + - _("Sign in to TextIt.in and go to Account Page.") - + '
  2. ' + - _("Tick “Incoming SMS Messages” and set Webhook URL to either:") - + '
    %(urla)s' - + '
    %(urlb)s

    ' - + '

' + - _("That's it. Now Send an SMS Formhub submission to your TextIt" - " phone number. It will create a submission on Formhub.") - + '

') % {'urla': urla, 'urlb': urlb} - return doc - - -def get_response(data): - - message = data.get('text') - if data.get('code') == SMS_API_ERROR: - message = None - elif data.get('code') != SMS_SUBMISSION_ACCEPTED: - message = _("[ERROR] %s") % message - - # send a response - if message: - messages = [message, ] - sendouts = data.get('sendouts', []) - if len(sendouts): - messages += sendouts - for text in messages: - payload = data.get('payload', {}) - payload.update({'text': text}) - if payload.get('phone'): - send_sms_via_textit(payload) - - return HttpResponse() - - -def send_sms_via_textit(payload): - response = {"phone": [payload.get('phone')], - "text": payload.get('text')} - - return HttpResponse(json.dumps(response), content_type='application/json') - - -@require_POST -@csrf_exempt -def import_submission(request, username): - """ Proxy to import_submission_for_form with None as id_string """ - return import_submission_for_form(request, username, None) - - -@require_POST -@csrf_exempt -def import_submission_for_form(request, username, id_string): - """ Retrieve and process submission from SMSSync Request """ - - sms_event = request.POST.get('event', '').strip() - - if not sms_event == 'mo_sms': - return HttpResponse() - - sms_identity = request.POST.get('phone', '').strip() - sms_relayer = request.POST.get('relayer', '').strip() - sms_text = request.POST.get('text', '').strip() - now_time = datetime.datetime.now().isoformat() - sent_time = request.POST.get('time', now_time).strip() - - try: - sms_time = dateutil.parser.parse(sent_time) - except ValueError: - sms_time = datetime.datetime.now() - - return process_message_for_textit(username=username, - sms_identity=sms_identity, - sms_text=sms_text, - sms_time=sms_time, - id_string=id_string, - payload={'phone': sms_identity, - 'relayer': sms_relayer}) - - -def process_message_for_textit(username, sms_identity, sms_text, sms_time, - id_string, payload={}): - """ Process a text instance and return in SMSSync expected format """ - - if not sms_identity or not sms_text: - return get_response({'code': SMS_API_ERROR, - 'text': _("`identity` and `message` are " - "both required and must not be " - "empty.")}) - - incomings = [(sms_identity, sms_text)] - response = process_incoming_smses(username, incomings, id_string)[-1] - response.update({'payload': payload}) - - return get_response(response) diff --git a/onadata/apps/sms_support/providers/twilio.py b/onadata/apps/sms_support/providers/twilio.py deleted file mode 100644 index 16bb92ec6..000000000 --- a/onadata/apps/sms_support/providers/twilio.py +++ /dev/null @@ -1,116 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -""" Twilio SMS gateway - - Supports Receiving and replying SMS from/to Twilio. - URL must be set to POST method in Twilio. - - See: http://www.twilio.com/docs/api/twiml/sms/twilio_request - http://www.twilio.com/docs/api/twiml/sms/your_response """ - -import datetime - -from dict2xml import dict2xml -from django.http import HttpResponse -from django.core.urlresolvers import reverse -from django.views.decorators.http import require_POST -from django.views.decorators.csrf import csrf_exempt -from django.utils.translation import ugettext as _ - -from onadata.apps.sms_support.tools import SMS_API_ERROR,\ - SMS_SUBMISSION_ACCEPTED -from onadata.apps.sms_support.parser import process_incoming_smses - - -def autodoc(url_root, username, id_string): - urla = url_root + reverse('sms_submission_api', - kwargs={'username': username, - 'service': 'twilio'}) - urlb = url_root + reverse('sms_submission_form_api', - kwargs={'username': username, - 'id_string': id_string, - 'service': 'twilio'}) - doc = ('

' + - _("%(service)s Instructions:") - % {'service': '' - 'Twilio\'s SMS Request'} - + '

  1. ' + - _("Sign in to Twilio.com and go your Application.") - + '
  2. ' + - _("Follow instructions to add one of the following URLs, " - "selecting the HTTP POST method:") - + '
    %(urla)s' - + '
    %(urlb)s

    ' - + '

' + - _("That's it. Now Send an SMS Formhub submission to your Twilio" - " phone number. It will create a submission on Formhub.") - + '

') % {'urla': urla, 'urlb': urlb} - return doc - - -def get_response(data): - - xml_head = '' - response_dict = {'Response': {}} - message = data.get('text') - - if data.get('code') == SMS_API_ERROR: - message = None - elif data.get('code') != SMS_SUBMISSION_ACCEPTED: - message = _("[ERROR] %s") % message - - if message: - messages = [message, ] - sendouts = data.get('sendouts', []) - if len(sendouts): - messages += sendouts - response_dict.update({"Response": {'Sms': messages}}) - - response = xml_head + dict2xml(response_dict) - return HttpResponse(response, content_type='text/xml') - - -@require_POST -@csrf_exempt -def import_submission(request, username): - """ Proxy to import_submission_for_form with None as id_string """ - - return import_submission_for_form(request, username, None) - - -@require_POST -@csrf_exempt -def import_submission_for_form(request, username, id_string): - """ Retrieve and process submission from SMSSync Request """ - - sms_identity = request.POST.get('From', '').strip() - sms_text = request.POST.get('Body', '').strip() - now_timestamp = datetime.datetime.now().strftime('%s') - sent_timestamp = request.POST.get('time_created', now_timestamp).strip() - try: - sms_time = datetime.datetime.fromtimestamp(float(sent_timestamp)) - except ValueError: - sms_time = datetime.datetime.now() - - return process_message_for_twilio(username=username, - sms_identity=sms_identity, - sms_text=sms_text, - sms_time=sms_time, - id_string=id_string) - - -def process_message_for_twilio(username, - sms_identity, sms_text, sms_time, id_string): - """ Process a text instance and return in SMSSync expected format """ - - if not sms_identity or not sms_text: - return get_response({'code': SMS_API_ERROR, - 'text': _("`identity` and `message` are " - "both required and must not be " - "empty.")}) - - incomings = [(sms_identity, sms_text)] - response = process_incoming_smses(username, incomings, id_string)[-1] - - return get_response(response) diff --git a/onadata/apps/sms_support/tests/__init__.py b/onadata/apps/sms_support/tests/__init__.py deleted file mode 100644 index 82c5b036c..000000000 --- a/onadata/apps/sms_support/tests/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -# from test_parser import TestParser -# from test_notallowed import TestNotAllowed diff --git a/onadata/apps/sms_support/tests/fixtures/sms_tutorial.xls b/onadata/apps/sms_support/tests/fixtures/sms_tutorial.xls deleted file mode 100644 index 5ad97a2a8..000000000 Binary files a/onadata/apps/sms_support/tests/fixtures/sms_tutorial.xls and /dev/null differ diff --git a/onadata/apps/sms_support/tests/test_base_sms.py b/onadata/apps/sms_support/tests/test_base_sms.py deleted file mode 100644 index e23b675c8..000000000 --- a/onadata/apps/sms_support/tests/test_base_sms.py +++ /dev/null @@ -1,50 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import os -import string -import random - -from onadata.apps.main.tests.test_base import TestBase -from onadata.apps.logger.models import XForm -from onadata.apps.sms_support.parser import process_incoming_smses - - -class TestBaseSMS(TestBase): - - def setUp(self): - TestBase.setUp(self) - - def setup_form(self, allow_sms=True): - self.id_string = 'sms_test_form' - self.sms_keyword = 'test' - self.username = 'auser' - self.password = 'auser' - self.this_directory = os.path.dirname(__file__) - - # init FH - self._create_user_and_login(username=self.username, - password=self.password) - - # create a test form and activate SMS Support. - self._publish_xls_file_and_set_xform(os.path.join(self.this_directory, - 'fixtures', - 'sms_tutorial.xls')) - - if allow_sms: - xform = XForm.objects.get(id_string=self.id_string) - xform.allows_sms = True - xform.save() - self.xform = xform - - def random_identity(self): - return ''.join([random.choice(string.digits + string.letters) - for x in xrange(8)]) - - def response_for_text(self, username, text, - id_string=None, identity=None): - if identity is None: - identity = self.random_identity() - - return process_incoming_smses(username=username, - id_string=None, - incomings=[(identity, text)])[0] diff --git a/onadata/apps/sms_support/tests/test_notallowed.py b/onadata/apps/sms_support/tests/test_notallowed.py deleted file mode 100644 index dd8355ccf..000000000 --- a/onadata/apps/sms_support/tests/test_notallowed.py +++ /dev/null @@ -1,24 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -from onadata.apps.sms_support.tools import SMS_SUBMISSION_REFUSED - -from .test_base_sms import TestBaseSMS - - -class TestNotAllowed(TestBaseSMS): - - def setUp(self): - TestBaseSMS.setUp(self) - self.setup_form(allow_sms=False) - - def test_refused_not_enabled(self): - # SMS submissions not allowed - result = self.response_for_text(self.username, 'test allo') - self.assertEqual(result['code'], SMS_SUBMISSION_REFUSED) - - def test_allow_sms(self): - result = self.response_for_text(self.username, - 'test +a 1 y 1950-02-22 john doe') - self.assertEqual(result['code'], SMS_SUBMISSION_REFUSED) - self.assertEqual(result.get('id'), None) diff --git a/onadata/apps/sms_support/tests/test_parser.py b/onadata/apps/sms_support/tests/test_parser.py deleted file mode 100644 index 199673b73..000000000 --- a/onadata/apps/sms_support/tests/test_parser.py +++ /dev/null @@ -1,57 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -from onadata.apps.sms_support.tools import ( - SMS_API_ERROR, SMS_PARSING_ERROR, - SMS_SUBMISSION_ACCEPTED, - SMS_SUBMISSION_REFUSED -) -from .test_base_sms import TestBaseSMS - - -class TestParser(TestBaseSMS): - - def setUp(self): - TestBaseSMS.setUp(self) - self.setup_form(allow_sms=True) - - def test_api_error(self): - # missing identity or text - result = self.response_for_text(self.username, 'hello', identity='') - self.assertEqual(result['code'], SMS_API_ERROR) - - result = self.response_for_text(self.username, text='') - self.assertEqual(result['code'], SMS_API_ERROR) - - def test_invalid_syntax(self): - # invalid text message - result = self.response_for_text(self.username, 'hello') - self.assertEqual(result['code'], SMS_PARSING_ERROR) - - def test_invalid_group(self): - # invalid text message - result = self.response_for_text(self.username, '++a 20', - id_string=self.id_string) - self.assertEqual(result['code'], SMS_PARSING_ERROR) - - def test_refused_with_keyword(self): - # submission has proper keywrd with invalid text - result = self.response_for_text(self.username, 'test allo') - self.assertEqual(result['code'], SMS_PARSING_ERROR) - - def test_sucessful_submission(self): - result = self.response_for_text(self.username, - 'test +a 1 y 1950-02-22 john doe') - self.assertEqual(result['code'], SMS_SUBMISSION_ACCEPTED) - self.assertTrue(result['id']) - - def test_invalid_type(self): - result = self.response_for_text(self.username, - 'test +a yes y 1950-02-22 john doe') - self.assertEqual(result['code'], SMS_PARSING_ERROR) - - def test_missing_required_field(self): - # required field name missing - result = self.response_for_text(self.username, - 'test +b ff') - self.assertEqual(result['code'], SMS_SUBMISSION_REFUSED) diff --git a/onadata/apps/sms_support/tools.py b/onadata/apps/sms_support/tools.py deleted file mode 100644 index 982380546..000000000 --- a/onadata/apps/sms_support/tools.py +++ /dev/null @@ -1,327 +0,0 @@ -# vim: ai ts=4 sts=4 et sw=4 nu -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - - -import mimetypes -import io -import copy -from xml.parsers.expat import ExpatError - -from django.core.files.uploadedfile import InMemoryUploadedFile -from django.utils.translation import ugettext as _ -from django.contrib.auth.models import User -from django.http import HttpRequest - -from onadata.apps.logger.xform_instance_parser import InstanceEmptyError,\ - InstanceInvalidUserError, DuplicateInstance -from onadata.apps.logger.exceptions import FormInactiveError -from onadata.apps.logger.models import XForm -from onadata.libs.utils.log import audit_log, Actions -from onadata.libs.utils.logger_tools import create_instance - - -SMS_API_ERROR = 'SMS_API_ERROR' -SMS_PARSING_ERROR = 'SMS_PARSING_ERROR' -SMS_SUBMISSION_ACCEPTED = 'SMS_SUBMISSION_ACCEPTED' -SMS_SUBMISSION_REFUSED = 'SMS_SUBMISSION_REFUSED' -SMS_INTERNAL_ERROR = 'SMS_INTERNAL_ERROR' - -BASE64_ALPHABET = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' - 'abcdefghijklmnopqrstuvwxyz0123456789+/=') -DEFAULT_SEPARATOR = '+' -DEFAULT_ALLOW_MEDIA = False -NA_VALUE = 'n/a' -BASE64_ALPHABET = None -META_FIELDS = ('start', 'end', 'today', 'deviceid', 'subscriberid', - 'imei', 'phonenumber') -MEDIA_TYPES = ('audio', 'video', 'photo') -DEFAULT_DATE_FORMAT = '%Y-%m-%d' -DEFAULT_DATETIME_FORMAT = '%Y-%m-%d-%H:%M' -SENSITIVE_FIELDS = ('text', 'select all that apply', 'geopoint', 'barcode') - - -def is_last(index, items): - return index == len(items) - 1 or (items[-1].get('type') == 'note' - and index == len(items) - 2) - - -def get_sms_instance_id(instance): - """ Human-friendly unique ID of a submission for latter ref/update - - For now, we strip down to the first 8 chars of the UUID. - Until we figure out what we really want (might as well be used - by formhub XML) """ - return instance.uuid[:8] - - -def sms_media_to_file(file_object, name): - if isinstance(file_object, basestring): - file_object = io.BytesIO(file_object) - - def getsize(f): - f.seek(0) - f.read() - s = f.tell() - f.seek(0) - return s - - name = name.strip() - content_type, charset = mimetypes.guess_type(name) - size = getsize(file_object) - return InMemoryUploadedFile(file=file_object, name=name, - field_name=None, content_type=content_type, - charset=charset, size=size) - - -def generate_instance(username, xml_file, media_files, uuid=None): - ''' Process an XForm submission as if done via HTTP - - :param IO xml_file: file-like object containing XML XForm - :param string username: username of the Form's owner - :param list media_files: a list of UploadedFile objects - :param string uuid: an optionnal uuid for the instance. - - :returns a (status, message) tuple. ''' - - try: - instance = create_instance( - username, - xml_file, - media_files, - uuid=uuid - ) - except InstanceInvalidUserError: - return {'code': SMS_SUBMISSION_REFUSED, - 'text': _("Username or ID required.")} - except InstanceEmptyError: - return {'code': SMS_INTERNAL_ERROR, - 'text': _("Received empty submission. " - "No instance was created")} - except FormInactiveError: - return {'code': SMS_SUBMISSION_REFUSED, - 'text': _("Form is not active")} - except XForm.DoesNotExist: - return {'code': SMS_SUBMISSION_REFUSED, - 'text': _("Form does not exist on this account")} - except ExpatError: - return {'code': SMS_INTERNAL_ERROR, - 'text': _("Improperly formatted XML.")} - except DuplicateInstance: - return {'code': SMS_SUBMISSION_REFUSED, - 'text': _("Duplicate submission")} - - if instance is None: - return {'code': SMS_INTERNAL_ERROR, - 'text': _("Unable to create submission.")} - - user = User.objects.get(username=username) - - audit = { - "xform": instance.xform.id_string - } - audit_log(Actions.SUBMISSION_CREATED, - user, instance.xform.user, - _("Created submission on form %(id_string)s.") % - {"id_string": instance.xform.id_string}, audit, HttpRequest()) - - xml_file.close() - if len(media_files): - [_file.close() for _file in media_files] - - return {'code': SMS_SUBMISSION_ACCEPTED, - 'text': _("[SUCCESS] Your submission has been accepted."), - 'id': get_sms_instance_id(instance)} - - -def is_sms_related(json_survey): - ''' Whether a form is considered to want sms Support - - return True if one sms-related field is defined. ''' - - def treat(value, key=None): - if key is None: - return False - if key in ('sms_field', 'sms_option') and value: - if not value.lower() in ('no', 'false'): - return True - - def walk(dl): - if not isinstance(dl, (dict, list)): - return False - iterator = [(None, e) for e in dl] \ - if isinstance(dl, list) else dl.items() - for k, v in iterator: - if k == 'parent': - continue - if treat(v, k): - return True - if walk(v): - return True - return False - - return walk(json_survey) - - -def check_form_sms_compatibility(form, json_survey=None): - ''' Tests all SMS related rules on the XForm representation - - Returns a view-compatible dict(type, text) with warnings or - a success message ''' - - if json_survey is None: - json_survey = form.get('form_o', {}) - form_text = "%s
" % form['text'] - else: - form_text = "" - - def prep_return(msg, comp=None): - - from django.core.urlresolvers import reverse - - error = 'alert-info' - warning = 'alert-info' - success = 'alert-success' - outro = ("
Please check the " - "SMS Syntax Page." % {'syntax_url': reverse('syntax')}) - - # no compatibility at all - if not comp: - alert = error - msg = ("%(prefix)s %(msg)s" - % {'prefix': "Your Form is not SMS-compatible" - ". If you want to later enable " - "SMS Support, please fix:
", - 'msg': msg}) - # no blocker but could be improved - elif comp == 1: - alert = warning - msg = ("%(prefix)s
    %(msg)s
" - % {'prefix': "Your form can be used with SMS, " - "knowing that:", 'msg': msg}) - # SMS compatible - else: - outro = "" - alert = success - - return {'type': alert, - 'text': "%(msg)s%(outro)s" - % {'intro': form_text, 'msg': msg, 'outro': outro}} - - # first level children. should be groups - groups = json_survey.get('children', [{}]) - - # BLOCKERS - # overload SENSITIVE_FIELDS if date or datetime format contain spaces. - sensitive_fields = copy.copy(SENSITIVE_FIELDS) - date_format = json_survey.get('sms_date_format', DEFAULT_DATE_FORMAT) \ - or DEFAULT_DATE_FORMAT - datetime_format = json_survey.get('sms_datetime_format', - DEFAULT_DATETIME_FORMAT) \ - or DEFAULT_DATETIME_FORMAT - if len(date_format.split()) > 1: - sensitive_fields += ('date', ) - if len(datetime_format.split()) > 1: - sensitive_fields += ('datetime', ) - - # must not contain out-of-group questions - if sum([1 for e in groups if e.get('type') != 'group']): - return prep_return(_("All your questions must be in groups.")) - # all groups must have an sms_field - bad_groups = [e.get('name') for e in groups if not e.get('sms_field', '') - and not e.get('name', '') == 'meta'] - if len(bad_groups): - return prep_return(_("All your groups must have an 'sms_field' " - "(use 'meta' prefixed ones for non-fillable " - "groups). %s" % bad_groups[-1])) - # all select_one or select_multiple fields muts have sms_option for each. - for group in groups: - fields = group.get('children', [{}]) - for field in fields: - xlsf_type = field.get('type') - xlsf_name = field.get('name') - xlsf_choices = field.get('children') - if xlsf_type in ('select one', 'select all that apply'): - nb_choices = len(xlsf_choices) - options = list(set([c.get('sms_option', '') or None - for c in xlsf_choices])) - try: - options.remove(None) - except ValueError: - pass - nb_options = len(options) - if nb_choices != nb_options: - return prep_return( - _("Not all options in the choices list for " - "%s have an " - "sms_option value.") % xlsf_name - ) - - # has sensitive (space containing) fields in non-last position - for group in groups: - fields = group.get('children', [{}]) - last_pos = len(fields) - 1 - # consider last field to be last before note if there's a trailing note - if fields[last_pos].get('type') == 'note': - if len(fields) - 1: - last_pos -= 1 - for idx, field in enumerate(fields): - if idx != last_pos and field.get('type', '') in sensitive_fields: - return prep_return(_("Questions for which values can contain " - "spaces are only allowed on last " - "position of group (%s)" - % field.get('name'))) - # separator is not set or is within BASE64 alphabet and sms_allow_media - separator = json_survey.get('sms_separator', DEFAULT_SEPARATOR) \ - or DEFAULT_SEPARATOR - sms_allow_media = bool(json_survey.get('sms_allow_media', - DEFAULT_ALLOW_MEDIA) or DEFAULT_ALLOW_MEDIA) - if sms_allow_media and separator in BASE64_ALPHABET: - return prep_return(_("When allowing medias ('sms_allow_media'), your " - "separator (%s) must be outside Base64 alphabet " - "(letters, digits and +/=). " - "You case use '#' instead." % separator)) - - # WARNINGS - warnings = [] - # sms_separator not set - if not json_survey.get('sms_separator', ''): - warnings.append("
  • You have not set a separator. Default '+' " - "separator will be used.
  • ") - # has date field with no sms_date_format - if not json_survey.get('sms_date_format', ''): - for group in groups: - if sum([1 for e in group.get('children', [{}]) - if e.get('type') == 'date']): - warnings.append("
  • You have 'date' fields without " - "explicitly setting a date format. " - "Default (%s) will be used.
  • " - % DEFAULT_DATE_FORMAT) - break - # has datetime field with no datetime format - if not json_survey.get('sms_date_format', ''): - for group in groups: - if sum([1 for e in group.get('children', [{}]) - if e.get('type') == 'datetime']): - warnings.append("
  • You have 'datetime' fields without " - "explicitly setting a datetime format. " - "Default (%s) will be used.
  • " - % DEFAULT_DATETIME_FORMAT) - break - - # date or datetime format contain space - if 'date' in sensitive_fields: - warnings.append("
  • 'sms_date_format' contains space which will " - "require 'date' questions to be positioned at " - "the end of groups (%s).
  • " % date_format) - if 'datetime' in sensitive_fields: - warnings.append("
  • 'sms_datetime_format' contains space which will " - "require 'datetime' questions to be positioned at " - "the end of groups (%s).
  • " % datetime_format) - - if len(warnings): - return prep_return("".join(warnings), comp=1) - - # Good to go - return prep_return(_("Note that your form is also SMS comptatible."), 2) diff --git a/onadata/apps/sms_support/views.py b/onadata/apps/sms_support/views.py deleted file mode 100644 index 4100f435b..000000000 --- a/onadata/apps/sms_support/views.py +++ /dev/null @@ -1,82 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -import json - -from django.http import HttpResponse -from django.views.decorators.http import require_GET, require_POST -from django.views.decorators.csrf import csrf_exempt -from django.utils.translation import ugettext as _ - -from .tools import SMS_API_ERROR -from .parser import process_incoming_smses - - -def get_response(data): - response = {'status': data.get('code'), - 'message': data.get('text'), - 'instanceID': data.get('id'), - 'sendouts': data.get('sendouts')} - return HttpResponse(json.dumps(response), content_type='application/json') - - -@require_GET -def import_submission(request, username): - """ Process an SMS text as a form submission - - :param string identity: phone number of the sender - :param string text: SMS content - - :returns: a JSON dict with: - 'status': one of 'ACCEPTED', 'REJECTED', 'PARSING_FAILED' - 'message': Error message if not ACCEPTED. - 'id: Unique submission ID if ACCEPTED. """ - - return import_submission_for_form(request, username, None) - - -@require_POST -@csrf_exempt -def import_multiple_submissions(request, username): - ''' Process several POSTED SMS texts as XForm submissions - - :param json messages: JSON list of {"identity": "x", "text": "x"} - :returns json list of - {"status": "x", "message": "x", "id": "x"} ''' - - return import_multiple_submissions_for_form(request, username, None) - - -@require_GET -def import_submission_for_form(request, username, id_string): - """ idem import_submission with a defined id_string """ - - sms_identity = request.GET.get('identity', '').strip() - sms_text = request.GET.get('text', '').strip() - - if not sms_identity or not sms_text: - return get_response({'code': SMS_API_ERROR, - 'text': _("`identity` and `message` are " - "both required and must not be " - "empty.")}) - incomings = [(sms_identity, sms_text)] - response = process_incoming_smses(username, incomings, id_string)[-1] - - return get_response(response) - - -@require_POST -@csrf_exempt -def import_multiple_submissions_for_form(request, username, id_string): - """ idem import_multiple_submissions with a defined id_string """ - - messages = json.loads(request.POST.get('messages', '[]')) - incomings = [(m.get('identity', ''), m.get('text', '')) for m in messages] - - responses = [{'status': d.get('code'), - 'message': d.get('text'), - 'instanceID': d.get('id'), - 'sendouts': d.get('sendouts')} for d - in process_incoming_smses(username, incomings, id_string)] - - return HttpResponse(json.dumps(responses), content_type='application/json') diff --git a/onadata/apps/survey_report/__init__.py b/onadata/apps/survey_report/__init__.py deleted file mode 100644 index 134880430..000000000 --- a/onadata/apps/survey_report/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import diff --git a/onadata/apps/survey_report/static/js/Chart.js b/onadata/apps/survey_report/static/js/Chart.js deleted file mode 100644 index e1215f9f0..000000000 --- a/onadata/apps/survey_report/static/js/Chart.js +++ /dev/null @@ -1,9214 +0,0 @@ -(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92); - g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92); - b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92); - - var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); - var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); - var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); - - return [x * 100, y *100, z * 100]; -} - -function rgb2lab(rgb) { - var xyz = rgb2xyz(rgb), - x = xyz[0], - y = xyz[1], - z = xyz[2], - l, a, b; - - x /= 95.047; - y /= 100; - z /= 108.883; - - x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); - y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); - z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); - - l = (116 * y) - 16; - a = 500 * (x - y); - b = 200 * (y - z); - - return [l, a, b]; -} - -function rgb2lch(args) { - return lab2lch(rgb2lab(args)); -} - -function hsl2rgb(hsl) { - var h = hsl[0] / 360, - s = hsl[1] / 100, - l = hsl[2] / 100, - t1, t2, t3, rgb, val; - - if (s == 0) { - val = l * 255; - return [val, val, val]; - } - - if (l < 0.5) - t2 = l * (1 + s); - else - t2 = l + s - l * s; - t1 = 2 * l - t2; - - rgb = [0, 0, 0]; - for (var i = 0; i < 3; i++) { - t3 = h + 1 / 3 * - (i - 1); - t3 < 0 && t3++; - t3 > 1 && t3--; - - if (6 * t3 < 1) - val = t1 + (t2 - t1) * 6 * t3; - else if (2 * t3 < 1) - val = t2; - else if (3 * t3 < 2) - val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; - else - val = t1; - - rgb[i] = val * 255; - } - - return rgb; -} - -function hsl2hsv(hsl) { - var h = hsl[0], - s = hsl[1] / 100, - l = hsl[2] / 100, - sv, v; - - if(l === 0) { - // no need to do calc on black - // also avoids divide by 0 error - return [0, 0, 0]; - } - - l *= 2; - s *= (l <= 1) ? l : 2 - l; - v = (l + s) / 2; - sv = (2 * s) / (l + s); - return [h, sv * 100, v * 100]; -} - -function hsl2hwb(args) { - return rgb2hwb(hsl2rgb(args)); -} - -function hsl2cmyk(args) { - return rgb2cmyk(hsl2rgb(args)); -} - -function hsl2keyword(args) { - return rgb2keyword(hsl2rgb(args)); -} - - -function hsv2rgb(hsv) { - var h = hsv[0] / 60, - s = hsv[1] / 100, - v = hsv[2] / 100, - hi = Math.floor(h) % 6; - - var f = h - Math.floor(h), - p = 255 * v * (1 - s), - q = 255 * v * (1 - (s * f)), - t = 255 * v * (1 - (s * (1 - f))), - v = 255 * v; - - switch(hi) { - case 0: - return [v, t, p]; - case 1: - return [q, v, p]; - case 2: - return [p, v, t]; - case 3: - return [p, q, v]; - case 4: - return [t, p, v]; - case 5: - return [v, p, q]; - } -} - -function hsv2hsl(hsv) { - var h = hsv[0], - s = hsv[1] / 100, - v = hsv[2] / 100, - sl, l; - - l = (2 - s) * v; - sl = s * v; - sl /= (l <= 1) ? l : 2 - l; - sl = sl || 0; - l /= 2; - return [h, sl * 100, l * 100]; -} - -function hsv2hwb(args) { - return rgb2hwb(hsv2rgb(args)) -} - -function hsv2cmyk(args) { - return rgb2cmyk(hsv2rgb(args)); -} - -function hsv2keyword(args) { - return rgb2keyword(hsv2rgb(args)); -} - -// http://dev.w3.org/csswg/css-color/#hwb-to-rgb -function hwb2rgb(hwb) { - var h = hwb[0] / 360, - wh = hwb[1] / 100, - bl = hwb[2] / 100, - ratio = wh + bl, - i, v, f, n; - - // wh + bl cant be > 1 - if (ratio > 1) { - wh /= ratio; - bl /= ratio; - } - - i = Math.floor(6 * h); - v = 1 - bl; - f = 6 * h - i; - if ((i & 0x01) != 0) { - f = 1 - f; - } - n = wh + f * (v - wh); // linear interpolation - - switch (i) { - default: - case 6: - case 0: r = v; g = n; b = wh; break; - case 1: r = n; g = v; b = wh; break; - case 2: r = wh; g = v; b = n; break; - case 3: r = wh; g = n; b = v; break; - case 4: r = n; g = wh; b = v; break; - case 5: r = v; g = wh; b = n; break; - } - - return [r * 255, g * 255, b * 255]; -} - -function hwb2hsl(args) { - return rgb2hsl(hwb2rgb(args)); -} - -function hwb2hsv(args) { - return rgb2hsv(hwb2rgb(args)); -} - -function hwb2cmyk(args) { - return rgb2cmyk(hwb2rgb(args)); -} - -function hwb2keyword(args) { - return rgb2keyword(hwb2rgb(args)); -} - -function cmyk2rgb(cmyk) { - var c = cmyk[0] / 100, - m = cmyk[1] / 100, - y = cmyk[2] / 100, - k = cmyk[3] / 100, - r, g, b; - - r = 1 - Math.min(1, c * (1 - k) + k); - g = 1 - Math.min(1, m * (1 - k) + k); - b = 1 - Math.min(1, y * (1 - k) + k); - return [r * 255, g * 255, b * 255]; -} - -function cmyk2hsl(args) { - return rgb2hsl(cmyk2rgb(args)); -} - -function cmyk2hsv(args) { - return rgb2hsv(cmyk2rgb(args)); -} - -function cmyk2hwb(args) { - return rgb2hwb(cmyk2rgb(args)); -} - -function cmyk2keyword(args) { - return rgb2keyword(cmyk2rgb(args)); -} - - -function xyz2rgb(xyz) { - var x = xyz[0] / 100, - y = xyz[1] / 100, - z = xyz[2] / 100, - r, g, b; - - r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); - g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); - b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); - - // assume sRGB - r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) - : r = (r * 12.92); - - g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) - : g = (g * 12.92); - - b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) - : b = (b * 12.92); - - r = Math.min(Math.max(0, r), 1); - g = Math.min(Math.max(0, g), 1); - b = Math.min(Math.max(0, b), 1); - - return [r * 255, g * 255, b * 255]; -} - -function xyz2lab(xyz) { - var x = xyz[0], - y = xyz[1], - z = xyz[2], - l, a, b; - - x /= 95.047; - y /= 100; - z /= 108.883; - - x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); - y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); - z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); - - l = (116 * y) - 16; - a = 500 * (x - y); - b = 200 * (y - z); - - return [l, a, b]; -} - -function xyz2lch(args) { - return lab2lch(xyz2lab(args)); -} - -function lab2xyz(lab) { - var l = lab[0], - a = lab[1], - b = lab[2], - x, y, z, y2; - - if (l <= 8) { - y = (l * 100) / 903.3; - y2 = (7.787 * (y / 100)) + (16 / 116); - } else { - y = 100 * Math.pow((l + 16) / 116, 3); - y2 = Math.pow(y / 100, 1/3); - } - - x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3); - - z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3); - - return [x, y, z]; -} - -function lab2lch(lab) { - var l = lab[0], - a = lab[1], - b = lab[2], - hr, h, c; - - hr = Math.atan2(b, a); - h = hr * 360 / 2 / Math.PI; - if (h < 0) { - h += 360; - } - c = Math.sqrt(a * a + b * b); - return [l, c, h]; -} - -function lab2rgb(args) { - return xyz2rgb(lab2xyz(args)); -} - -function lch2lab(lch) { - var l = lch[0], - c = lch[1], - h = lch[2], - a, b, hr; - - hr = h / 360 * 2 * Math.PI; - a = c * Math.cos(hr); - b = c * Math.sin(hr); - return [l, a, b]; -} - -function lch2xyz(args) { - return lab2xyz(lch2lab(args)); -} - -function lch2rgb(args) { - return lab2rgb(lch2lab(args)); -} - -function keyword2rgb(keyword) { - return cssKeywords[keyword]; -} - -function keyword2hsl(args) { - return rgb2hsl(keyword2rgb(args)); -} - -function keyword2hsv(args) { - return rgb2hsv(keyword2rgb(args)); -} - -function keyword2hwb(args) { - return rgb2hwb(keyword2rgb(args)); -} - -function keyword2cmyk(args) { - return rgb2cmyk(keyword2rgb(args)); -} - -function keyword2lab(args) { - return rgb2lab(keyword2rgb(args)); -} - -function keyword2xyz(args) { - return rgb2xyz(keyword2rgb(args)); -} - -var cssKeywords = { - aliceblue: [240,248,255], - antiquewhite: [250,235,215], - aqua: [0,255,255], - aquamarine: [127,255,212], - azure: [240,255,255], - beige: [245,245,220], - bisque: [255,228,196], - black: [0,0,0], - blanchedalmond: [255,235,205], - blue: [0,0,255], - blueviolet: [138,43,226], - brown: [165,42,42], - burlywood: [222,184,135], - cadetblue: [95,158,160], - chartreuse: [127,255,0], - chocolate: [210,105,30], - coral: [255,127,80], - cornflowerblue: [100,149,237], - cornsilk: [255,248,220], - crimson: [220,20,60], - cyan: [0,255,255], - darkblue: [0,0,139], - darkcyan: [0,139,139], - darkgoldenrod: [184,134,11], - darkgray: [169,169,169], - darkgreen: [0,100,0], - darkgrey: [169,169,169], - darkkhaki: [189,183,107], - darkmagenta: [139,0,139], - darkolivegreen: [85,107,47], - darkorange: [255,140,0], - darkorchid: [153,50,204], - darkred: [139,0,0], - darksalmon: [233,150,122], - darkseagreen: [143,188,143], - darkslateblue: [72,61,139], - darkslategray: [47,79,79], - darkslategrey: [47,79,79], - darkturquoise: [0,206,209], - darkviolet: [148,0,211], - deeppink: [255,20,147], - deepskyblue: [0,191,255], - dimgray: [105,105,105], - dimgrey: [105,105,105], - dodgerblue: [30,144,255], - firebrick: [178,34,34], - floralwhite: [255,250,240], - forestgreen: [34,139,34], - fuchsia: [255,0,255], - gainsboro: [220,220,220], - ghostwhite: [248,248,255], - gold: [255,215,0], - goldenrod: [218,165,32], - gray: [128,128,128], - green: [0,128,0], - greenyellow: [173,255,47], - grey: [128,128,128], - honeydew: [240,255,240], - hotpink: [255,105,180], - indianred: [205,92,92], - indigo: [75,0,130], - ivory: [255,255,240], - khaki: [240,230,140], - lavender: [230,230,250], - lavenderblush: [255,240,245], - lawngreen: [124,252,0], - lemonchiffon: [255,250,205], - lightblue: [173,216,230], - lightcoral: [240,128,128], - lightcyan: [224,255,255], - lightgoldenrodyellow: [250,250,210], - lightgray: [211,211,211], - lightgreen: [144,238,144], - lightgrey: [211,211,211], - lightpink: [255,182,193], - lightsalmon: [255,160,122], - lightseagreen: [32,178,170], - lightskyblue: [135,206,250], - lightslategray: [119,136,153], - lightslategrey: [119,136,153], - lightsteelblue: [176,196,222], - lightyellow: [255,255,224], - lime: [0,255,0], - limegreen: [50,205,50], - linen: [250,240,230], - magenta: [255,0,255], - maroon: [128,0,0], - mediumaquamarine: [102,205,170], - mediumblue: [0,0,205], - mediumorchid: [186,85,211], - mediumpurple: [147,112,219], - mediumseagreen: [60,179,113], - mediumslateblue: [123,104,238], - mediumspringgreen: [0,250,154], - mediumturquoise: [72,209,204], - mediumvioletred: [199,21,133], - midnightblue: [25,25,112], - mintcream: [245,255,250], - mistyrose: [255,228,225], - moccasin: [255,228,181], - navajowhite: [255,222,173], - navy: [0,0,128], - oldlace: [253,245,230], - olive: [128,128,0], - olivedrab: [107,142,35], - orange: [255,165,0], - orangered: [255,69,0], - orchid: [218,112,214], - palegoldenrod: [238,232,170], - palegreen: [152,251,152], - paleturquoise: [175,238,238], - palevioletred: [219,112,147], - papayawhip: [255,239,213], - peachpuff: [255,218,185], - peru: [205,133,63], - pink: [255,192,203], - plum: [221,160,221], - powderblue: [176,224,230], - purple: [128,0,128], - rebeccapurple: [102, 51, 153], - red: [255,0,0], - rosybrown: [188,143,143], - royalblue: [65,105,225], - saddlebrown: [139,69,19], - salmon: [250,128,114], - sandybrown: [244,164,96], - seagreen: [46,139,87], - seashell: [255,245,238], - sienna: [160,82,45], - silver: [192,192,192], - skyblue: [135,206,235], - slateblue: [106,90,205], - slategray: [112,128,144], - slategrey: [112,128,144], - snow: [255,250,250], - springgreen: [0,255,127], - steelblue: [70,130,180], - tan: [210,180,140], - teal: [0,128,128], - thistle: [216,191,216], - tomato: [255,99,71], - turquoise: [64,224,208], - violet: [238,130,238], - wheat: [245,222,179], - white: [255,255,255], - whitesmoke: [245,245,245], - yellow: [255,255,0], - yellowgreen: [154,205,50] -}; - -var reverseKeywords = {}; -for (var key in cssKeywords) { - reverseKeywords[JSON.stringify(cssKeywords[key])] = key; -} - -},{}],3:[function(require,module,exports){ -var conversions = require("./conversions"); - -var convert = function() { - return new Converter(); -} - -for (var func in conversions) { - // export Raw versions - convert[func + "Raw"] = (function(func) { - // accept array or plain args - return function(arg) { - if (typeof arg == "number") - arg = Array.prototype.slice.call(arguments); - return conversions[func](arg); - } - })(func); - - var pair = /(\w+)2(\w+)/.exec(func), - from = pair[1], - to = pair[2]; - - // export rgb2hsl and ["rgb"]["hsl"] - convert[from] = convert[from] || {}; - - convert[from][to] = convert[func] = (function(func) { - return function(arg) { - if (typeof arg == "number") - arg = Array.prototype.slice.call(arguments); - - var val = conversions[func](arg); - if (typeof val == "string" || val === undefined) - return val; // keyword - - for (var i = 0; i < val.length; i++) - val[i] = Math.round(val[i]); - return val; - } - })(func); -} - - -/* Converter does lazy conversion and caching */ -var Converter = function() { - this.convs = {}; -}; - -/* Either get the values for a space or - set the values for a space, depending on args */ -Converter.prototype.routeSpace = function(space, args) { - var values = args[0]; - if (values === undefined) { - // color.rgb() - return this.getValues(space); - } - // color.rgb(10, 10, 10) - if (typeof values == "number") { - values = Array.prototype.slice.call(args); - } - - return this.setValues(space, values); -}; - -/* Set the values for a space, invalidating cache */ -Converter.prototype.setValues = function(space, values) { - this.space = space; - this.convs = {}; - this.convs[space] = values; - return this; -}; - -/* Get the values for a space. If there's already - a conversion for the space, fetch it, otherwise - compute it */ -Converter.prototype.getValues = function(space) { - var vals = this.convs[space]; - if (!vals) { - var fspace = this.space, - from = this.convs[fspace]; - vals = convert[fspace][space](from); - - this.convs[space] = vals; - } - return vals; -}; - -["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function(space) { - Converter.prototype[space] = function(vals) { - return this.routeSpace(space, arguments); - } -}); - -module.exports = convert; -},{"./conversions":2}],4:[function(require,module,exports){ -/* MIT license */ -var colorNames = require('color-name'); - -module.exports = { - getRgba: getRgba, - getHsla: getHsla, - getRgb: getRgb, - getHsl: getHsl, - getHwb: getHwb, - getAlpha: getAlpha, - - hexString: hexString, - rgbString: rgbString, - rgbaString: rgbaString, - percentString: percentString, - percentaString: percentaString, - hslString: hslString, - hslaString: hslaString, - hwbString: hwbString, - keyword: keyword -} - -function getRgba(string) { - if (!string) { - return; - } - var abbr = /^#([a-fA-F0-9]{3})$/, - hex = /^#([a-fA-F0-9]{6})$/, - rgba = /^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/, - per = /^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/, - keyword = /(\w+)/; - - var rgb = [0, 0, 0], - a = 1, - match = string.match(abbr); - if (match) { - match = match[1]; - for (var i = 0; i < rgb.length; i++) { - rgb[i] = parseInt(match[i] + match[i], 16); - } - } - else if (match = string.match(hex)) { - match = match[1]; - for (var i = 0; i < rgb.length; i++) { - rgb[i] = parseInt(match.slice(i * 2, i * 2 + 2), 16); - } - } - else if (match = string.match(rgba)) { - for (var i = 0; i < rgb.length; i++) { - rgb[i] = parseInt(match[i + 1]); - } - a = parseFloat(match[4]); - } - else if (match = string.match(per)) { - for (var i = 0; i < rgb.length; i++) { - rgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55); - } - a = parseFloat(match[4]); - } - else if (match = string.match(keyword)) { - if (match[1] == "transparent") { - return [0, 0, 0, 0]; - } - rgb = colorNames[match[1]]; - if (!rgb) { - return; - } - } - - for (var i = 0; i < rgb.length; i++) { - rgb[i] = scale(rgb[i], 0, 255); - } - if (!a && a != 0) { - a = 1; - } - else { - a = scale(a, 0, 1); - } - rgb[3] = a; - return rgb; -} - -function getHsla(string) { - if (!string) { - return; - } - var hsl = /^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/; - var match = string.match(hsl); - if (match) { - var alpha = parseFloat(match[4]); - var h = scale(parseInt(match[1]), 0, 360), - s = scale(parseFloat(match[2]), 0, 100), - l = scale(parseFloat(match[3]), 0, 100), - a = scale(isNaN(alpha) ? 1 : alpha, 0, 1); - return [h, s, l, a]; - } -} - -function getHwb(string) { - if (!string) { - return; - } - var hwb = /^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/; - var match = string.match(hwb); - if (match) { - var alpha = parseFloat(match[4]); - var h = scale(parseInt(match[1]), 0, 360), - w = scale(parseFloat(match[2]), 0, 100), - b = scale(parseFloat(match[3]), 0, 100), - a = scale(isNaN(alpha) ? 1 : alpha, 0, 1); - return [h, w, b, a]; - } -} - -function getRgb(string) { - var rgba = getRgba(string); - return rgba && rgba.slice(0, 3); -} - -function getHsl(string) { - var hsla = getHsla(string); - return hsla && hsla.slice(0, 3); -} - -function getAlpha(string) { - var vals = getRgba(string); - if (vals) { - return vals[3]; - } - else if (vals = getHsla(string)) { - return vals[3]; - } - else if (vals = getHwb(string)) { - return vals[3]; - } -} - -// generators -function hexString(rgb) { - return "#" + hexDouble(rgb[0]) + hexDouble(rgb[1]) - + hexDouble(rgb[2]); -} - -function rgbString(rgba, alpha) { - if (alpha < 1 || (rgba[3] && rgba[3] < 1)) { - return rgbaString(rgba, alpha); - } - return "rgb(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ")"; -} - -function rgbaString(rgba, alpha) { - if (alpha === undefined) { - alpha = (rgba[3] !== undefined ? rgba[3] : 1); - } - return "rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] - + ", " + alpha + ")"; -} - -function percentString(rgba, alpha) { - if (alpha < 1 || (rgba[3] && rgba[3] < 1)) { - return percentaString(rgba, alpha); - } - var r = Math.round(rgba[0]/255 * 100), - g = Math.round(rgba[1]/255 * 100), - b = Math.round(rgba[2]/255 * 100); - - return "rgb(" + r + "%, " + g + "%, " + b + "%)"; -} - -function percentaString(rgba, alpha) { - var r = Math.round(rgba[0]/255 * 100), - g = Math.round(rgba[1]/255 * 100), - b = Math.round(rgba[2]/255 * 100); - return "rgba(" + r + "%, " + g + "%, " + b + "%, " + (alpha || rgba[3] || 1) + ")"; -} - -function hslString(hsla, alpha) { - if (alpha < 1 || (hsla[3] && hsla[3] < 1)) { - return hslaString(hsla, alpha); - } - return "hsl(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%)"; -} - -function hslaString(hsla, alpha) { - if (alpha === undefined) { - alpha = (hsla[3] !== undefined ? hsla[3] : 1); - } - return "hsla(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%, " - + alpha + ")"; -} - -// hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax -// (hwb have alpha optional & 1 is default value) -function hwbString(hwb, alpha) { - if (alpha === undefined) { - alpha = (hwb[3] !== undefined ? hwb[3] : 1); - } - return "hwb(" + hwb[0] + ", " + hwb[1] + "%, " + hwb[2] + "%" - + (alpha !== undefined && alpha !== 1 ? ", " + alpha : "") + ")"; -} - -function keyword(rgb) { - return reverseNames[rgb.slice(0, 3)]; -} - -// helpers -function scale(num, min, max) { - return Math.min(Math.max(min, num), max); -} - -function hexDouble(num) { - var str = num.toString(16).toUpperCase(); - return (str.length < 2) ? "0" + str : str; -} - - -//create a list of reverse color names -var reverseNames = {}; -for (var name in colorNames) { - reverseNames[colorNames[name]] = name; -} - -},{"color-name":5}],5:[function(require,module,exports){ -module.exports = { - "aliceblue": [240, 248, 255], - "antiquewhite": [250, 235, 215], - "aqua": [0, 255, 255], - "aquamarine": [127, 255, 212], - "azure": [240, 255, 255], - "beige": [245, 245, 220], - "bisque": [255, 228, 196], - "black": [0, 0, 0], - "blanchedalmond": [255, 235, 205], - "blue": [0, 0, 255], - "blueviolet": [138, 43, 226], - "brown": [165, 42, 42], - "burlywood": [222, 184, 135], - "cadetblue": [95, 158, 160], - "chartreuse": [127, 255, 0], - "chocolate": [210, 105, 30], - "coral": [255, 127, 80], - "cornflowerblue": [100, 149, 237], - "cornsilk": [255, 248, 220], - "crimson": [220, 20, 60], - "cyan": [0, 255, 255], - "darkblue": [0, 0, 139], - "darkcyan": [0, 139, 139], - "darkgoldenrod": [184, 134, 11], - "darkgray": [169, 169, 169], - "darkgreen": [0, 100, 0], - "darkgrey": [169, 169, 169], - "darkkhaki": [189, 183, 107], - "darkmagenta": [139, 0, 139], - "darkolivegreen": [85, 107, 47], - "darkorange": [255, 140, 0], - "darkorchid": [153, 50, 204], - "darkred": [139, 0, 0], - "darksalmon": [233, 150, 122], - "darkseagreen": [143, 188, 143], - "darkslateblue": [72, 61, 139], - "darkslategray": [47, 79, 79], - "darkslategrey": [47, 79, 79], - "darkturquoise": [0, 206, 209], - "darkviolet": [148, 0, 211], - "deeppink": [255, 20, 147], - "deepskyblue": [0, 191, 255], - "dimgray": [105, 105, 105], - "dimgrey": [105, 105, 105], - "dodgerblue": [30, 144, 255], - "firebrick": [178, 34, 34], - "floralwhite": [255, 250, 240], - "forestgreen": [34, 139, 34], - "fuchsia": [255, 0, 255], - "gainsboro": [220, 220, 220], - "ghostwhite": [248, 248, 255], - "gold": [255, 215, 0], - "goldenrod": [218, 165, 32], - "gray": [128, 128, 128], - "green": [0, 128, 0], - "greenyellow": [173, 255, 47], - "grey": [128, 128, 128], - "honeydew": [240, 255, 240], - "hotpink": [255, 105, 180], - "indianred": [205, 92, 92], - "indigo": [75, 0, 130], - "ivory": [255, 255, 240], - "khaki": [240, 230, 140], - "lavender": [230, 230, 250], - "lavenderblush": [255, 240, 245], - "lawngreen": [124, 252, 0], - "lemonchiffon": [255, 250, 205], - "lightblue": [173, 216, 230], - "lightcoral": [240, 128, 128], - "lightcyan": [224, 255, 255], - "lightgoldenrodyellow": [250, 250, 210], - "lightgray": [211, 211, 211], - "lightgreen": [144, 238, 144], - "lightgrey": [211, 211, 211], - "lightpink": [255, 182, 193], - "lightsalmon": [255, 160, 122], - "lightseagreen": [32, 178, 170], - "lightskyblue": [135, 206, 250], - "lightslategray": [119, 136, 153], - "lightslategrey": [119, 136, 153], - "lightsteelblue": [176, 196, 222], - "lightyellow": [255, 255, 224], - "lime": [0, 255, 0], - "limegreen": [50, 205, 50], - "linen": [250, 240, 230], - "magenta": [255, 0, 255], - "maroon": [128, 0, 0], - "mediumaquamarine": [102, 205, 170], - "mediumblue": [0, 0, 205], - "mediumorchid": [186, 85, 211], - "mediumpurple": [147, 112, 219], - "mediumseagreen": [60, 179, 113], - "mediumslateblue": [123, 104, 238], - "mediumspringgreen": [0, 250, 154], - "mediumturquoise": [72, 209, 204], - "mediumvioletred": [199, 21, 133], - "midnightblue": [25, 25, 112], - "mintcream": [245, 255, 250], - "mistyrose": [255, 228, 225], - "moccasin": [255, 228, 181], - "navajowhite": [255, 222, 173], - "navy": [0, 0, 128], - "oldlace": [253, 245, 230], - "olive": [128, 128, 0], - "olivedrab": [107, 142, 35], - "orange": [255, 165, 0], - "orangered": [255, 69, 0], - "orchid": [218, 112, 214], - "palegoldenrod": [238, 232, 170], - "palegreen": [152, 251, 152], - "paleturquoise": [175, 238, 238], - "palevioletred": [219, 112, 147], - "papayawhip": [255, 239, 213], - "peachpuff": [255, 218, 185], - "peru": [205, 133, 63], - "pink": [255, 192, 203], - "plum": [221, 160, 221], - "powderblue": [176, 224, 230], - "purple": [128, 0, 128], - "rebeccapurple": [102, 51, 153], - "red": [255, 0, 0], - "rosybrown": [188, 143, 143], - "royalblue": [65, 105, 225], - "saddlebrown": [139, 69, 19], - "salmon": [250, 128, 114], - "sandybrown": [244, 164, 96], - "seagreen": [46, 139, 87], - "seashell": [255, 245, 238], - "sienna": [160, 82, 45], - "silver": [192, 192, 192], - "skyblue": [135, 206, 235], - "slateblue": [106, 90, 205], - "slategray": [112, 128, 144], - "slategrey": [112, 128, 144], - "snow": [255, 250, 250], - "springgreen": [0, 255, 127], - "steelblue": [70, 130, 180], - "tan": [210, 180, 140], - "teal": [0, 128, 128], - "thistle": [216, 191, 216], - "tomato": [255, 99, 71], - "turquoise": [64, 224, 208], - "violet": [238, 130, 238], - "wheat": [245, 222, 179], - "white": [255, 255, 255], - "whitesmoke": [245, 245, 245], - "yellow": [255, 255, 0], - "yellowgreen": [154, 205, 50] -}; -},{}],6:[function(require,module,exports){ -/* MIT license */ - -var convert = require("color-convert"), - string = require("color-string"); - -var Color = function(obj) { - if (obj instanceof Color) return obj; - if (!(this instanceof Color)) return new Color(obj); - - this.values = { - rgb: [0, 0, 0], - hsl: [0, 0, 0], - hsv: [0, 0, 0], - hwb: [0, 0, 0], - cmyk: [0, 0, 0, 0], - alpha: 1 - } - - // parse Color() argument - if (typeof obj == "string") { - var vals = string.getRgba(obj); - if (vals) { - this.setValues("rgb", vals); - } else if (vals = string.getHsla(obj)) { - this.setValues("hsl", vals); - } else if (vals = string.getHwb(obj)) { - this.setValues("hwb", vals); - } else { - throw new Error("Unable to parse color from string \"" + obj + "\""); - } - } else if (typeof obj == "object") { - var vals = obj; - if (vals["r"] !== undefined || vals["red"] !== undefined) { - this.setValues("rgb", vals) - } else if (vals["l"] !== undefined || vals["lightness"] !== undefined) { - this.setValues("hsl", vals) - } else if (vals["v"] !== undefined || vals["value"] !== undefined) { - this.setValues("hsv", vals) - } else if (vals["w"] !== undefined || vals["whiteness"] !== undefined) { - this.setValues("hwb", vals) - } else if (vals["c"] !== undefined || vals["cyan"] !== undefined) { - this.setValues("cmyk", vals) - } else { - throw new Error("Unable to parse color from object " + JSON.stringify(obj)); - } - } -} - -Color.prototype = { - rgb: function(vals) { - return this.setSpace("rgb", arguments); - }, - hsl: function(vals) { - return this.setSpace("hsl", arguments); - }, - hsv: function(vals) { - return this.setSpace("hsv", arguments); - }, - hwb: function(vals) { - return this.setSpace("hwb", arguments); - }, - cmyk: function(vals) { - return this.setSpace("cmyk", arguments); - }, - - rgbArray: function() { - return this.values.rgb; - }, - hslArray: function() { - return this.values.hsl; - }, - hsvArray: function() { - return this.values.hsv; - }, - hwbArray: function() { - if (this.values.alpha !== 1) { - return this.values.hwb.concat([this.values.alpha]) - } - return this.values.hwb; - }, - cmykArray: function() { - return this.values.cmyk; - }, - rgbaArray: function() { - var rgb = this.values.rgb; - return rgb.concat([this.values.alpha]); - }, - hslaArray: function() { - var hsl = this.values.hsl; - return hsl.concat([this.values.alpha]); - }, - alpha: function(val) { - if (val === undefined) { - return this.values.alpha; - } - this.setValues("alpha", val); - return this; - }, - - red: function(val) { - return this.setChannel("rgb", 0, val); - }, - green: function(val) { - return this.setChannel("rgb", 1, val); - }, - blue: function(val) { - return this.setChannel("rgb", 2, val); - }, - hue: function(val) { - return this.setChannel("hsl", 0, val); - }, - saturation: function(val) { - return this.setChannel("hsl", 1, val); - }, - lightness: function(val) { - return this.setChannel("hsl", 2, val); - }, - saturationv: function(val) { - return this.setChannel("hsv", 1, val); - }, - whiteness: function(val) { - return this.setChannel("hwb", 1, val); - }, - blackness: function(val) { - return this.setChannel("hwb", 2, val); - }, - value: function(val) { - return this.setChannel("hsv", 2, val); - }, - cyan: function(val) { - return this.setChannel("cmyk", 0, val); - }, - magenta: function(val) { - return this.setChannel("cmyk", 1, val); - }, - yellow: function(val) { - return this.setChannel("cmyk", 2, val); - }, - black: function(val) { - return this.setChannel("cmyk", 3, val); - }, - - hexString: function() { - return string.hexString(this.values.rgb); - }, - rgbString: function() { - return string.rgbString(this.values.rgb, this.values.alpha); - }, - rgbaString: function() { - return string.rgbaString(this.values.rgb, this.values.alpha); - }, - percentString: function() { - return string.percentString(this.values.rgb, this.values.alpha); - }, - hslString: function() { - return string.hslString(this.values.hsl, this.values.alpha); - }, - hslaString: function() { - return string.hslaString(this.values.hsl, this.values.alpha); - }, - hwbString: function() { - return string.hwbString(this.values.hwb, this.values.alpha); - }, - keyword: function() { - return string.keyword(this.values.rgb, this.values.alpha); - }, - - rgbNumber: function() { - return (this.values.rgb[0] << 16) | (this.values.rgb[1] << 8) | this.values.rgb[2]; - }, - - luminosity: function() { - // http://www.w3.org/TR/WCAG20/#relativeluminancedef - var rgb = this.values.rgb; - var lum = []; - for (var i = 0; i < rgb.length; i++) { - var chan = rgb[i] / 255; - lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4) - } - return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; - }, - - contrast: function(color2) { - // http://www.w3.org/TR/WCAG20/#contrast-ratiodef - var lum1 = this.luminosity(); - var lum2 = color2.luminosity(); - if (lum1 > lum2) { - return (lum1 + 0.05) / (lum2 + 0.05) - }; - return (lum2 + 0.05) / (lum1 + 0.05); - }, - - level: function(color2) { - var contrastRatio = this.contrast(color2); - return (contrastRatio >= 7.1) ? 'AAA' : (contrastRatio >= 4.5) ? 'AA' : ''; - }, - - dark: function() { - // YIQ equation from http://24ways.org/2010/calculating-color-contrast - var rgb = this.values.rgb, - yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; - return yiq < 128; - }, - - light: function() { - return !this.dark(); - }, - - negate: function() { - var rgb = [] - for (var i = 0; i < 3; i++) { - rgb[i] = 255 - this.values.rgb[i]; - } - this.setValues("rgb", rgb); - return this; - }, - - lighten: function(ratio) { - this.values.hsl[2] += this.values.hsl[2] * ratio; - this.setValues("hsl", this.values.hsl); - return this; - }, - - darken: function(ratio) { - this.values.hsl[2] -= this.values.hsl[2] * ratio; - this.setValues("hsl", this.values.hsl); - return this; - }, - - saturate: function(ratio) { - this.values.hsl[1] += this.values.hsl[1] * ratio; - this.setValues("hsl", this.values.hsl); - return this; - }, - - desaturate: function(ratio) { - this.values.hsl[1] -= this.values.hsl[1] * ratio; - this.setValues("hsl", this.values.hsl); - return this; - }, - - whiten: function(ratio) { - this.values.hwb[1] += this.values.hwb[1] * ratio; - this.setValues("hwb", this.values.hwb); - return this; - }, - - blacken: function(ratio) { - this.values.hwb[2] += this.values.hwb[2] * ratio; - this.setValues("hwb", this.values.hwb); - return this; - }, - - greyscale: function() { - var rgb = this.values.rgb; - // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale - var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11; - this.setValues("rgb", [val, val, val]); - return this; - }, - - clearer: function(ratio) { - this.setValues("alpha", this.values.alpha - (this.values.alpha * ratio)); - return this; - }, - - opaquer: function(ratio) { - this.setValues("alpha", this.values.alpha + (this.values.alpha * ratio)); - return this; - }, - - rotate: function(degrees) { - var hue = this.values.hsl[0]; - hue = (hue + degrees) % 360; - hue = hue < 0 ? 360 + hue : hue; - this.values.hsl[0] = hue; - this.setValues("hsl", this.values.hsl); - return this; - }, - - mix: function(color2, weight) { - weight = 1 - (weight == null ? 0.5 : weight); - - // algorithm from Sass's mix(). Ratio of first color in mix is - // determined by the alphas of both colors and the weight - var t1 = weight * 2 - 1, - d = this.alpha() - color2.alpha(); - - var weight1 = (((t1 * d == -1) ? t1 : (t1 + d) / (1 + t1 * d)) + 1) / 2; - var weight2 = 1 - weight1; - - var rgb = this.rgbArray(); - var rgb2 = color2.rgbArray(); - - for (var i = 0; i < rgb.length; i++) { - rgb[i] = rgb[i] * weight1 + rgb2[i] * weight2; - } - this.setValues("rgb", rgb); - - var alpha = this.alpha() * weight + color2.alpha() * (1 - weight); - this.setValues("alpha", alpha); - - return this; - }, - - toJSON: function() { - return this.rgb(); - }, - - clone: function() { - return new Color(this.rgb()); - } -} - - -Color.prototype.getValues = function(space) { - var vals = {}; - for (var i = 0; i < space.length; i++) { - vals[space.charAt(i)] = this.values[space][i]; - } - if (this.values.alpha != 1) { - vals["a"] = this.values.alpha; - } - // {r: 255, g: 255, b: 255, a: 0.4} - return vals; -} - -Color.prototype.setValues = function(space, vals) { - var spaces = { - "rgb": ["red", "green", "blue"], - "hsl": ["hue", "saturation", "lightness"], - "hsv": ["hue", "saturation", "value"], - "hwb": ["hue", "whiteness", "blackness"], - "cmyk": ["cyan", "magenta", "yellow", "black"] - }; - - var maxes = { - "rgb": [255, 255, 255], - "hsl": [360, 100, 100], - "hsv": [360, 100, 100], - "hwb": [360, 100, 100], - "cmyk": [100, 100, 100, 100] - }; - - var alpha = 1; - if (space == "alpha") { - alpha = vals; - } else if (vals.length) { - // [10, 10, 10] - this.values[space] = vals.slice(0, space.length); - alpha = vals[space.length]; - } else if (vals[space.charAt(0)] !== undefined) { - // {r: 10, g: 10, b: 10} - for (var i = 0; i < space.length; i++) { - this.values[space][i] = vals[space.charAt(i)]; - } - alpha = vals.a; - } else if (vals[spaces[space][0]] !== undefined) { - // {red: 10, green: 10, blue: 10} - var chans = spaces[space]; - for (var i = 0; i < space.length; i++) { - this.values[space][i] = vals[chans[i]]; - } - alpha = vals.alpha; - } - this.values.alpha = Math.max(0, Math.min(1, (alpha !== undefined ? alpha : this.values.alpha))); - if (space == "alpha") { - return; - } - - // cap values of the space prior converting all values - for (var i = 0; i < space.length; i++) { - var capped = Math.max(0, Math.min(maxes[space][i], this.values[space][i])); - this.values[space][i] = Math.round(capped); - } - - // convert to all the other color spaces - for (var sname in spaces) { - if (sname != space) { - this.values[sname] = convert[space][sname](this.values[space]) - } - - // cap values - for (var i = 0; i < sname.length; i++) { - var capped = Math.max(0, Math.min(maxes[sname][i], this.values[sname][i])); - this.values[sname][i] = Math.round(capped); - } - } - return true; -} - -Color.prototype.setSpace = function(space, args) { - var vals = args[0]; - if (vals === undefined) { - // color.rgb() - return this.getValues(space); - } - // color.rgb(10, 10, 10) - if (typeof vals == "number") { - vals = Array.prototype.slice.call(args); - } - this.setValues(space, vals); - return this; -} - -Color.prototype.setChannel = function(space, index, val) { - if (val === undefined) { - // color.red() - return this.values[space][index]; - } - // color.red(100) - this.values[space][index] = val; - this.setValues(space, this.values[space]); - return this; -} - -window.Color = module.exports = Color - -},{"color-convert":3,"color-string":4}],7:[function(require,module,exports){ -/*! - * Chart.js - * http://chartjs.org/ - * Version: 2.0.2 - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - - -var Chart = require('./core/core.js')(); - -require('./core/core.helpers')(Chart); -require('./core/core.element')(Chart); -require('./core/core.animation')(Chart); -require('./core/core.controller')(Chart); -require('./core/core.datasetController')(Chart); -require('./core/core.layoutService')(Chart); -require('./core/core.legend')(Chart); -require('./core/core.scale')(Chart); -require('./core/core.scaleService')(Chart); -require('./core/core.title')(Chart); -require('./core/core.tooltip')(Chart); - -require('./controllers/controller.bar')(Chart); -require('./controllers/controller.bubble')(Chart); -require('./controllers/controller.doughnut')(Chart); -require('./controllers/controller.line')(Chart); -require('./controllers/controller.polarArea')(Chart); -require('./controllers/controller.radar')(Chart); - -require('./scales/scale.category')(Chart); -require('./scales/scale.linear')(Chart); -require('./scales/scale.logarithmic')(Chart); -require('./scales/scale.radialLinear')(Chart); -require('./scales/scale.time')(Chart); - -require('./elements/element.arc')(Chart); -require('./elements/element.line')(Chart); -require('./elements/element.point')(Chart); -require('./elements/element.rectangle')(Chart); - -require('./charts/Chart.Bar')(Chart); -require('./charts/Chart.Bubble')(Chart); -require('./charts/Chart.Doughnut')(Chart); -require('./charts/Chart.Line')(Chart); -require('./charts/Chart.PolarArea')(Chart); -require('./charts/Chart.Radar')(Chart); -require('./charts/Chart.Scatter')(Chart); - -window.Chart = module.exports = Chart; - -},{"./charts/Chart.Bar":8,"./charts/Chart.Bubble":9,"./charts/Chart.Doughnut":10,"./charts/Chart.Line":11,"./charts/Chart.PolarArea":12,"./charts/Chart.Radar":13,"./charts/Chart.Scatter":14,"./controllers/controller.bar":15,"./controllers/controller.bubble":16,"./controllers/controller.doughnut":17,"./controllers/controller.line":18,"./controllers/controller.polarArea":19,"./controllers/controller.radar":20,"./core/core.animation":21,"./core/core.controller":22,"./core/core.datasetController":23,"./core/core.element":24,"./core/core.helpers":25,"./core/core.js":26,"./core/core.layoutService":27,"./core/core.legend":28,"./core/core.scale":29,"./core/core.scaleService":30,"./core/core.title":31,"./core/core.tooltip":32,"./elements/element.arc":33,"./elements/element.line":34,"./elements/element.point":35,"./elements/element.rectangle":36,"./scales/scale.category":37,"./scales/scale.linear":38,"./scales/scale.logarithmic":39,"./scales/scale.radialLinear":40,"./scales/scale.time":41}],8:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - Chart.Bar = function(context, config) { - config.type = 'bar'; - - return new Chart(context, config); - }; - -}; -},{}],9:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - Chart.Bubble = function(context, config) { - config.type = 'bubble'; - return new Chart(context, config); - }; - -}; -},{}],10:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - Chart.Doughnut = function(context, config) { - config.type = 'doughnut'; - - return new Chart(context, config); - }; - -}; -},{}],11:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - Chart.Line = function(context, config) { - config.type = 'line'; - - return new Chart(context, config); - }; - -}; -},{}],12:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - Chart.PolarArea = function(context, config) { - config.type = 'polarArea'; - - return new Chart(context, config); - }; - -}; -},{}],13:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - var defaultConfig = { - aspectRatio: 1 - }; - - Chart.Radar = function(context, config) { - config.options = helpers.configMerge(defaultConfig, config.options); - config.type = 'radar'; - - return new Chart(context, config); - }; - -}; - -},{}],14:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var defaultConfig = { - hover: { - mode: 'single' - }, - - scales: { - xAxes: [{ - type: "linear", // scatter should not use a category axis - position: "bottom", - id: "x-axis-1" // need an ID so datasets can reference the scale - }], - yAxes: [{ - type: "linear", - position: "left", - id: "y-axis-1" - }] - }, - - tooltips: { - callbacks: { - title: function(tooltipItems, data) { - // Title doesn't make sense for scatter since we format the data as a point - return ''; - }, - label: function(tooltipItem, data) { - return '(' + tooltipItem.xLabel + ', ' + tooltipItem.yLabel + ')'; - } - } - } - }; - - // Register the default config for this type - Chart.defaults.scatter = defaultConfig; - - // Scatter charts use line controllers - Chart.controllers.scatter = Chart.controllers.line; - - Chart.Scatter = function(context, config) { - config.type = 'scatter'; - return new Chart(context, config); - }; - -}; -},{}],15:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.bar = { - hover: { - mode: "label" - }, - - scales: { - xAxes: [{ - type: "category", - - // Specific to Bar Controller - categoryPercentage: 0.8, - barPercentage: 0.9, - - // grid line settings - gridLines: { - offsetGridLines: true - } - }], - yAxes: [{ - type: "linear" - }] - } - }; - - Chart.controllers.bar = Chart.DatasetController.extend({ - initialize: function(chart, datasetIndex) { - Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex); - - // Use this to indicate that this is a bar dataset. - this.getDataset().bar = true; - }, - // Get the number of datasets that display bars. We use this to correctly calculate the bar width - getBarCount: function getBarCount() { - var barCount = 0; - helpers.each(this.chart.data.datasets, function(dataset) { - if (helpers.isDatasetVisible(dataset) && dataset.bar) { - ++barCount; - } - }); - return barCount; - }, - - addElements: function() { - this.getDataset().metaData = this.getDataset().metaData || []; - helpers.each(this.getDataset().data, function(value, index) { - this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Rectangle({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - }, this); - }, - addElementAndReset: function(index) { - this.getDataset().metaData = this.getDataset().metaData || []; - var rectangle = new Chart.elements.Rectangle({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - - var numBars = this.getBarCount(); - - this.updateElement(rectangle, index, true, numBars); - this.getDataset().metaData.splice(index, 0, rectangle); - }, - - update: function update(reset) { - var numBars = this.getBarCount(); - - helpers.each(this.getDataset().metaData, function(rectangle, index) { - this.updateElement(rectangle, index, reset, numBars); - }, this); - }, - - updateElement: function updateElement(rectangle, index, reset, numBars) { - - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var yScale = this.getScaleForId(this.getDataset().yAxisID); - - var yScalePoint; - - if (yScale.min < 0 && yScale.max < 0) { - // all less than 0. use the top - yScalePoint = yScale.getPixelForValue(yScale.max); - } else if (yScale.min > 0 && yScale.max > 0) { - yScalePoint = yScale.getPixelForValue(yScale.min); - } else { - yScalePoint = yScale.getPixelForValue(0); - } - - helpers.extend(rectangle, { - // Utility - _chart: this.chart.chart, - _xScale: xScale, - _yScale: yScale, - _datasetIndex: this.index, - _index: index, - - - // Desired view properties - _model: { - x: this.calculateBarX(index, this.index), - y: reset ? yScalePoint : this.calculateBarY(index, this.index), - - // Tooltip - label: this.chart.data.labels[index], - datasetLabel: this.getDataset().label, - - // Appearance - base: reset ? yScalePoint : this.calculateBarBase(this.index, index), - width: this.calculateBarWidth(numBars), - backgroundColor: rectangle.custom && rectangle.custom.backgroundColor ? rectangle.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.rectangle.backgroundColor), - borderSkipped: rectangle.custom && rectangle.custom.borderSkipped ? rectangle.custom.borderSkipped : this.chart.options.elements.rectangle.borderSkipped, - borderColor: rectangle.custom && rectangle.custom.borderColor ? rectangle.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.rectangle.borderColor), - borderWidth: rectangle.custom && rectangle.custom.borderWidth ? rectangle.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.rectangle.borderWidth) - } - }); - rectangle.pivot(); - }, - - calculateBarBase: function(datasetIndex, index) { - - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var yScale = this.getScaleForId(this.getDataset().yAxisID); - - var base = 0; - - if (yScale.options.stacked) { - - var value = this.chart.data.datasets[datasetIndex].data[index]; - - if (value < 0) { - for (var i = 0; i < datasetIndex; i++) { - var negDS = this.chart.data.datasets[i]; - if (helpers.isDatasetVisible(negDS) && negDS.yAxisID === yScale.id && negDS.bar) { - base += negDS.data[index] < 0 ? negDS.data[index] : 0; - } - } - } else { - for (var j = 0; j < datasetIndex; j++) { - var posDS = this.chart.data.datasets[j]; - if (helpers.isDatasetVisible(posDS) && posDS.yAxisID === yScale.id && posDS.bar) { - base += posDS.data[index] > 0 ? posDS.data[index] : 0; - } - } - } - - return yScale.getPixelForValue(base); - } - - base = yScale.getPixelForValue(yScale.min); - - if (yScale.beginAtZero || ((yScale.min <= 0 && yScale.max >= 0) || (yScale.min >= 0 && yScale.max <= 0))) { - base = yScale.getPixelForValue(0, 0); - //base += yScale.options.gridLines.lineWidth; - } else if (yScale.min < 0 && yScale.max < 0) { - // All values are negative. Use the top as the base - base = yScale.getPixelForValue(yScale.max); - } - - return base; - - }, - - getRuler: function() { - - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var yScale = this.getScaleForId(this.getDataset().yAxisID); - var datasetCount = this.getBarCount(); - - var tickWidth = (function() { - var min = xScale.getPixelForTick(1) - xScale.getPixelForTick(0); - for (var i = 2; i < this.getDataset().data.length; i++) { - min = Math.min(xScale.getPixelForTick(i) - xScale.getPixelForTick(i - 1), min); - } - return min; - }).call(this); - var categoryWidth = tickWidth * xScale.options.categoryPercentage; - var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2; - var fullBarWidth = categoryWidth / datasetCount; - var barWidth = fullBarWidth * xScale.options.barPercentage; - var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage); - - return { - datasetCount: datasetCount, - tickWidth: tickWidth, - categoryWidth: categoryWidth, - categorySpacing: categorySpacing, - fullBarWidth: fullBarWidth, - barWidth: barWidth, - barSpacing: barSpacing - }; - }, - - calculateBarWidth: function() { - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var ruler = this.getRuler(); - return xScale.options.stacked ? ruler.categoryWidth : ruler.barWidth; - }, - - // Get bar index from the given dataset index accounting for the fact that not all bars are visible - getBarIndex: function(datasetIndex) { - var barIndex = 0; - - for (var j = 0; j < datasetIndex; ++j) { - if (helpers.isDatasetVisible(this.chart.data.datasets[j]) && this.chart.data.datasets[j].bar) { - ++barIndex; - } - } - - return barIndex; - }, - - calculateBarX: function(index, datasetIndex) { - - var yScale = this.getScaleForId(this.getDataset().yAxisID); - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var barIndex = this.getBarIndex(datasetIndex); - - var ruler = this.getRuler(); - var leftTick = xScale.getPixelForValue(null, index, datasetIndex, this.chart.isCombo); - leftTick -= this.chart.isCombo ? (ruler.tickWidth / 2) : 0; - - if (xScale.options.stacked) { - return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing; - } - - return leftTick + - (ruler.barWidth / 2) + - ruler.categorySpacing + - (ruler.barWidth * barIndex) + - (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); - }, - - calculateBarY: function(index, datasetIndex) { - - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var yScale = this.getScaleForId(this.getDataset().yAxisID); - - var value = this.getDataset().data[index]; - - if (yScale.options.stacked) { - - var sumPos = 0, - sumNeg = 0; - - for (var i = 0; i < datasetIndex; i++) { - var ds = this.chart.data.datasets[i]; - if (helpers.isDatasetVisible(ds) && ds.bar && ds.yAxisID === yScale.id) { - if (ds.data[index] < 0) { - sumNeg += ds.data[index] || 0; - } else { - sumPos += ds.data[index] || 0; - } - } - } - - if (value < 0) { - return yScale.getPixelForValue(sumNeg + value); - } else { - return yScale.getPixelForValue(sumPos + value); - } - - return yScale.getPixelForValue(value); - } - - return yScale.getPixelForValue(value); - }, - - draw: function(ease) { - var easingDecimal = ease || 1; - helpers.each(this.getDataset().metaData, function(rectangle, index) { - var d = this.getDataset().data[index]; - if (d !== null && d !== undefined && !isNaN(d)) { - rectangle.transition(easingDecimal).draw(); - } - }, this); - }, - - setHoverStyle: function(rectangle) { - var dataset = this.chart.data.datasets[rectangle._datasetIndex]; - var index = rectangle._index; - - rectangle._model.backgroundColor = rectangle.custom && rectangle.custom.hoverBackgroundColor ? rectangle.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(rectangle._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - rectangle._model.borderColor = rectangle.custom && rectangle.custom.hoverBorderColor ? rectangle.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(rectangle._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - rectangle._model.borderWidth = rectangle.custom && rectangle.custom.hoverBorderWidth ? rectangle.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, rectangle._model.borderWidth); - }, - - removeHoverStyle: function(rectangle) { - var dataset = this.chart.data.datasets[rectangle._datasetIndex]; - var index = rectangle._index; - - rectangle._model.backgroundColor = rectangle.custom && rectangle.custom.backgroundColor ? rectangle.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.rectangle.backgroundColor); - rectangle._model.borderColor = rectangle.custom && rectangle.custom.borderColor ? rectangle.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.rectangle.borderColor); - rectangle._model.borderWidth = rectangle.custom && rectangle.custom.borderWidth ? rectangle.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.rectangle.borderWidth); - } - - }); -}; - -},{}],16:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.bubble = { - hover: { - mode: "single" - }, - - scales: { - xAxes: [{ - type: "linear", // bubble should probably use a linear scale by default - position: "bottom", - id: "x-axis-0" // need an ID so datasets can reference the scale - }], - yAxes: [{ - type: "linear", - position: "left", - id: "y-axis-0" - }] - }, - - tooltips: { - callbacks: { - title: function(tooltipItems, data) { - // Title doesn't make sense for scatter since we format the data as a point - return ''; - }, - label: function(tooltipItem, data) { - var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; - var dataPoint = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; - return datasetLabel + ': (' + dataPoint.x + ', ' + dataPoint.y + ', ' + dataPoint.r + ')'; - } - } - } - }; - - - Chart.controllers.bubble = Chart.DatasetController.extend({ - addElements: function() { - - this.getDataset().metaData = this.getDataset().metaData || []; - - helpers.each(this.getDataset().data, function(value, index) { - this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Point({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - }, this); - }, - addElementAndReset: function(index) { - this.getDataset().metaData = this.getDataset().metaData || []; - var point = new Chart.elements.Point({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - - // Reset the point - this.updateElement(point, index, true); - - // Add to the points array - this.getDataset().metaData.splice(index, 0, point); - }, - - update: function update(reset) { - var points = this.getDataset().metaData; - - var yScale = this.getScaleForId(this.getDataset().yAxisID); - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var scaleBase; - - if (yScale.min < 0 && yScale.max < 0) { - scaleBase = yScale.getPixelForValue(yScale.max); - } else if (yScale.min > 0 && yScale.max > 0) { - scaleBase = yScale.getPixelForValue(yScale.min); - } else { - scaleBase = yScale.getPixelForValue(0); - } - - // Update Points - helpers.each(points, function(point, index) { - this.updateElement(point, index, reset); - }, this); - - }, - - updateElement: function(point, index, reset) { - var yScale = this.getScaleForId(this.getDataset().yAxisID); - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var scaleBase; - - if (yScale.min < 0 && yScale.max < 0) { - scaleBase = yScale.getPixelForValue(yScale.max); - } else if (yScale.min > 0 && yScale.max > 0) { - scaleBase = yScale.getPixelForValue(yScale.min); - } else { - scaleBase = yScale.getPixelForValue(0); - } - - helpers.extend(point, { - // Utility - _chart: this.chart.chart, - _xScale: xScale, - _yScale: yScale, - _datasetIndex: this.index, - _index: index, - - // Desired view properties - _model: { - x: reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(this.getDataset().data[index], index, this.index, this.chart.isCombo), - y: reset ? scaleBase : yScale.getPixelForValue(this.getDataset().data[index], index, this.index), - // Appearance - radius: reset ? 0 : point.custom && point.custom.radius ? point.custom.radius : this.getRadius(this.getDataset().data[index]), - backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.point.backgroundColor), - borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.point.borderColor), - borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.point.borderWidth), - - // Tooltip - hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.getDataset().hitRadius, index, this.chart.options.elements.point.hitRadius) - } - }); - - point._model.skip = point.custom && point.custom.skip ? point.custom.skip : (isNaN(point._model.x) || isNaN(point._model.y)); - - point.pivot(); - }, - - getRadius: function(value) { - return value.r || this.chart.options.elements.point.radius; - }, - - draw: function(ease) { - var easingDecimal = ease || 1; - - // Transition and Draw the Points - helpers.each(this.getDataset().metaData, function(point, index) { - point.transition(easingDecimal); - point.draw(); - }); - - }, - - setHoverStyle: function(point) { - // Point - var dataset = this.chart.data.datasets[point._datasetIndex]; - var index = point._index; - - point._model.radius = point.custom && point.custom.hoverRadius ? point.custom.hoverRadius : (helpers.getValueAtIndexOrDefault(dataset.hoverRadius, index, this.chart.options.elements.point.hoverRadius)) + this.getRadius(this.getDataset().data[point._index]); - point._model.backgroundColor = point.custom && point.custom.hoverBackgroundColor ? point.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(point._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - point._model.borderColor = point.custom && point.custom.hoverBorderColor ? point.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(point._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - point._model.borderWidth = point.custom && point.custom.hoverBorderWidth ? point.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, point._model.borderWidth); - }, - - removeHoverStyle: function(point) { - var dataset = this.chart.data.datasets[point._datasetIndex]; - var index = point._index; - - point._model.radius = point.custom && point.custom.radius ? point.custom.radius : this.getRadius(this.getDataset().data[point._index]); - point._model.backgroundColor = point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.point.backgroundColor); - point._model.borderColor = point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.point.borderColor); - point._model.borderWidth = point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.point.borderWidth); - } - }); -}; -},{}],17:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.doughnut = { - animation: { - //Boolean - Whether we animate the rotation of the Doughnut - animateRotate: true, - //Boolean - Whether we animate scaling the Doughnut from the centre - animateScale: false - }, - aspectRatio: 1, - hover: { - mode: 'single' - }, - legendCallback: function(chart) { - var text = []; - text.push('
      '); - - if (chart.data.datasets.length) { - for (var i = 0; i < chart.data.datasets[0].data.length; ++i) { - text.push('
    • '); - if (chart.data.labels[i]) { - text.push(chart.data.labels[i]); - } - text.push('
    • '); - } - } - - text.push('
    '); - return text.join(""); - }, - legend: { - labels: { - generateLabels: function(data) { - if (data.labels.length && data.datasets.length) { - return data.labels.map(function(label, i) { - return { - text: label, - fillStyle: data.datasets[0].backgroundColor[i], - hidden: isNaN(data.datasets[0].data[i]), - - // Extra data used for toggling the correct item - index: i - }; - }); - } else { - return []; - } - } - }, - onClick: function(e, legendItem) { - helpers.each(this.chart.data.datasets, function(dataset) { - dataset.metaHiddenData = dataset.metaHiddenData || []; - var idx = legendItem.index; - - if (!isNaN(dataset.data[idx])) { - dataset.metaHiddenData[idx] = dataset.data[idx]; - dataset.data[idx] = NaN; - } else if (!isNaN(dataset.metaHiddenData[idx])) { - dataset.data[idx] = dataset.metaHiddenData[idx]; - } - }); - - this.chart.update(); - } - }, - - //The percentage of the chart that we cut out of the middle. - cutoutPercentage: 50, - - //The rotation of the chart, where the first data arc begins. - rotation: Math.PI * -0.5, - - //The total circumference of the chart. - circumference: Math.PI * 2.0, - - // Need to override these to give a nice default - tooltips: { - callbacks: { - title: function() { - return ''; - }, - label: function(tooltipItem, data) { - return data.labels[tooltipItem.index] + ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; - } - } - } - }; - - Chart.defaults.pie = helpers.clone(Chart.defaults.doughnut); - helpers.extend(Chart.defaults.pie, { - cutoutPercentage: 0 - }); - - - Chart.controllers.doughnut = Chart.controllers.pie = Chart.DatasetController.extend({ - linkScales: function() { - // no scales for doughnut - }, - - addElements: function() { - this.getDataset().metaData = this.getDataset().metaData || []; - helpers.each(this.getDataset().data, function(value, index) { - this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Arc({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - }, this); - }, - addElementAndReset: function(index, colorForNewElement) { - this.getDataset().metaData = this.getDataset().metaData || []; - var arc = new Chart.elements.Arc({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - - if (colorForNewElement && helpers.isArray(this.getDataset().backgroundColor)) { - this.getDataset().backgroundColor.splice(index, 0, colorForNewElement); - } - - // Reset the point - this.updateElement(arc, index, true); - - // Add to the points array - this.getDataset().metaData.splice(index, 0, arc); - }, - - getVisibleDatasetCount: function getVisibleDatasetCount() { - return helpers.where(this.chart.data.datasets, function(ds) { - return helpers.isDatasetVisible(ds); - }).length; - }, - - // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly - getRingIndex: function getRingIndex(datasetIndex) { - var ringIndex = 0; - - for (var j = 0; j < datasetIndex; ++j) { - if (helpers.isDatasetVisible(this.chart.data.datasets[j])) { - ++ringIndex; - } - } - - return ringIndex; - }, - - update: function update(reset) { - var availableWidth = this.chart.chartArea.right - this.chart.chartArea.left - this.chart.options.elements.arc.borderWidth; - var availableHeight = this.chart.chartArea.bottom - this.chart.chartArea.top - this.chart.options.elements.arc.borderWidth; - var minSize = Math.min(availableWidth, availableHeight); - var offset = {x: 0, y: 0}; - - // If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc - if (this.chart.options.circumference && this.chart.options.circumference < Math.PI * 2.0) { - var startAngle = this.chart.options.rotation % (Math.PI * 2.0); - startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0); - var endAngle = startAngle + this.chart.options.circumference; - var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)}; - var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)}; - var contains0 = (startAngle <= 0 && 0 <= endAngle) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle); - var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle); - var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle); - var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle); - var cutout = this.chart.options.cutoutPercentage / 100.0; - var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))}; - var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))}; - var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5}; - minSize = Math.min(availableWidth / size.width, availableHeight / size.height); - offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5}; - } - - this.chart.outerRadius = Math.max(minSize / 2, 0); - this.chart.innerRadius = Math.max(this.chart.options.cutoutPercentage ? (this.chart.outerRadius / 100) * (this.chart.options.cutoutPercentage) : 1, 0); - this.chart.radiusLength = (this.chart.outerRadius - this.chart.innerRadius) / this.getVisibleDatasetCount(); - this.chart.offsetX = offset.x * this.chart.outerRadius; - this.chart.offsetY = offset.y * this.chart.outerRadius; - - this.getDataset().total = 0; - helpers.each(this.getDataset().data, function(value) { - if (!isNaN(value)) { - this.getDataset().total += Math.abs(value); - } - }, this); - - this.outerRadius = this.chart.outerRadius - (this.chart.radiusLength * this.getRingIndex(this.index)); - this.innerRadius = this.outerRadius - this.chart.radiusLength; - - helpers.each(this.getDataset().metaData, function(arc, index) { - this.updateElement(arc, index, reset); - }, this); - }, - updateElement: function(arc, index, reset) { - var centerX = (this.chart.chartArea.left + this.chart.chartArea.right) / 2; - var centerY = (this.chart.chartArea.top + this.chart.chartArea.bottom) / 2; - var startAngle = this.chart.options.rotation || (Math.PI * -0.5); // non reset case handled later - var endAngle = this.chart.options.rotation || (Math.PI * -0.5); // non reset case handled later - var circumference = reset && this.chart.options.animation.animateRotate ? 0 : this.calculateCircumference(this.getDataset().data[index]) * ((this.chart.options.circumference || (2.0 * Math.PI)) / (2.0 * Math.PI)); - var innerRadius = reset && this.chart.options.animation.animateScale ? 0 : this.innerRadius; - var outerRadius = reset && this.chart.options.animation.animateScale ? 0 : this.outerRadius; - - helpers.extend(arc, { - // Utility - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index, - - // Desired view properties - _model: { - x: centerX + this.chart.offsetX, - y: centerY + this.chart.offsetY, - startAngle: startAngle, - endAngle: endAngle, - circumference: circumference, - outerRadius: outerRadius, - innerRadius: innerRadius, - - backgroundColor: arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor), - hoverBackgroundColor: arc.custom && arc.custom.hoverBackgroundColor ? arc.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().hoverBackgroundColor, index, this.chart.options.elements.arc.hoverBackgroundColor), - borderWidth: arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth), - borderColor: arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(this.getDataset().label, index, this.chart.data.labels[index]) - } - }); - - // Set correct angles if not resetting - if (!reset) { - - if (index === 0) { - arc._model.startAngle = this.chart.options.rotation || (Math.PI * -0.5); - } else { - arc._model.startAngle = this.getDataset().metaData[index - 1]._model.endAngle; - } - - arc._model.endAngle = arc._model.startAngle + arc._model.circumference; - } - - arc.pivot(); - }, - - draw: function(ease) { - var easingDecimal = ease || 1; - helpers.each(this.getDataset().metaData, function(arc, index) { - arc.transition(easingDecimal).draw(); - }); - }, - - setHoverStyle: function(arc) { - var dataset = this.chart.data.datasets[arc._datasetIndex]; - var index = arc._index; - - arc._model.backgroundColor = arc.custom && arc.custom.hoverBackgroundColor ? arc.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(arc._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - arc._model.borderColor = arc.custom && arc.custom.hoverBorderColor ? arc.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(arc._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - arc._model.borderWidth = arc.custom && arc.custom.hoverBorderWidth ? arc.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, arc._model.borderWidth); - }, - - removeHoverStyle: function(arc) { - var dataset = this.chart.data.datasets[arc._datasetIndex]; - var index = arc._index; - - arc._model.backgroundColor = arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor); - arc._model.borderColor = arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor); - arc._model.borderWidth = arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth); - }, - - calculateCircumference: function(value) { - if (this.getDataset().total > 0 && !isNaN(value)) { - return (Math.PI * 1.999999) * (value / this.getDataset().total); - } else { - return 0; - } - } - }); -}; -},{}],18:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.line = { - showLines: true, - - hover: { - mode: "label" - }, - - scales: { - xAxes: [{ - type: "category", - id: 'x-axis-0' - }], - yAxes: [{ - type: "linear", - id: 'y-axis-0' - }] - } - }; - - - Chart.controllers.line = Chart.DatasetController.extend({ - addElements: function() { - - this.getDataset().metaData = this.getDataset().metaData || []; - - this.getDataset().metaDataset = this.getDataset().metaDataset || new Chart.elements.Line({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _points: this.getDataset().metaData - }); - - helpers.each(this.getDataset().data, function(value, index) { - this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Point({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - }, this); - }, - addElementAndReset: function(index) { - this.getDataset().metaData = this.getDataset().metaData || []; - var point = new Chart.elements.Point({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - - // Reset the point - this.updateElement(point, index, true); - - // Add to the points array - this.getDataset().metaData.splice(index, 0, point); - - // Make sure bezier control points are updated - if (this.chart.options.showLines && this.chart.options.elements.line.tension !== 0) - this.updateBezierControlPoints(); - }, - - update: function update(reset) { - var line = this.getDataset().metaDataset; - var points = this.getDataset().metaData; - - var yScale = this.getScaleForId(this.getDataset().yAxisID); - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var scaleBase; - - if (yScale.min < 0 && yScale.max < 0) { - scaleBase = yScale.getPixelForValue(yScale.max); - } else if (yScale.min > 0 && yScale.max > 0) { - scaleBase = yScale.getPixelForValue(yScale.min); - } else { - scaleBase = yScale.getPixelForValue(0); - } - - // Update Line - if (this.chart.options.showLines) { - // Utility - line._scale = yScale; - line._datasetIndex = this.index; - // Data - line._children = points; - // Model - line._model = { - // Appearance - tension: line.custom && line.custom.tension ? line.custom.tension : helpers.getValueOrDefault(this.getDataset().tension, this.chart.options.elements.line.tension), - backgroundColor: line.custom && line.custom.backgroundColor ? line.custom.backgroundColor : (this.getDataset().backgroundColor || this.chart.options.elements.line.backgroundColor), - borderWidth: line.custom && line.custom.borderWidth ? line.custom.borderWidth : (this.getDataset().borderWidth || this.chart.options.elements.line.borderWidth), - borderColor: line.custom && line.custom.borderColor ? line.custom.borderColor : (this.getDataset().borderColor || this.chart.options.elements.line.borderColor), - borderCapStyle: line.custom && line.custom.borderCapStyle ? line.custom.borderCapStyle : (this.getDataset().borderCapStyle || this.chart.options.elements.line.borderCapStyle), - borderDash: line.custom && line.custom.borderDash ? line.custom.borderDash : (this.getDataset().borderDash || this.chart.options.elements.line.borderDash), - borderDashOffset: line.custom && line.custom.borderDashOffset ? line.custom.borderDashOffset : (this.getDataset().borderDashOffset || this.chart.options.elements.line.borderDashOffset), - borderJoinStyle: line.custom && line.custom.borderJoinStyle ? line.custom.borderJoinStyle : (this.getDataset().borderJoinStyle || this.chart.options.elements.line.borderJoinStyle), - fill: line.custom && line.custom.fill ? line.custom.fill : (this.getDataset().fill !== undefined ? this.getDataset().fill : this.chart.options.elements.line.fill), - // Scale - scaleTop: yScale.top, - scaleBottom: yScale.bottom, - scaleZero: scaleBase - }; - line.pivot(); - } - - // Update Points - helpers.each(points, function(point, index) { - this.updateElement(point, index, reset); - }, this); - - if (this.chart.options.showLines && this.chart.options.elements.line.tension !== 0) - this.updateBezierControlPoints(); - }, - - getPointBackgroundColor: function(point, index) { - var backgroundColor = this.chart.options.elements.point.backgroundColor; - var dataset = this.getDataset(); - - if (point.custom && point.custom.backgroundColor) { - backgroundColor = point.custom.backgroundColor; - } else if (dataset.pointBackgroundColor) { - backgroundColor = helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, backgroundColor); - } else if (dataset.backgroundColor) { - backgroundColor = dataset.backgroundColor; - } - - return backgroundColor; - }, - getPointBorderColor: function(point, index) { - var borderColor = this.chart.options.elements.point.borderColor; - var dataset = this.getDataset(); - - if (point.custom && point.custom.borderColor) { - borderColor = point.custom.borderColor; - } else if (dataset.pointBorderColor) { - borderColor = helpers.getValueAtIndexOrDefault(this.getDataset().pointBorderColor, index, borderColor); - } else if (dataset.borderColor) { - borderColor = dataset.borderColor; - } - - return borderColor; - }, - getPointBorderWidth: function(point, index) { - var borderWidth = this.chart.options.elements.point.borderWidth; - var dataset = this.getDataset(); - - if (point.custom && point.custom.borderWidth !== undefined) { - borderWidth = point.custom.borderWidth; - } else if (dataset.pointBorderWidth !== undefined) { - borderWidth = helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, borderWidth); - } else if (dataset.borderWidth !== undefined) { - borderWidth = dataset.borderWidth; - } - - return borderWidth; - }, - - updateElement: function(point, index, reset) { - var yScale = this.getScaleForId(this.getDataset().yAxisID); - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var scaleBase; - - if (yScale.min < 0 && yScale.max < 0) { - scaleBase = yScale.getPixelForValue(yScale.max); - } else if (yScale.min > 0 && yScale.max > 0) { - scaleBase = yScale.getPixelForValue(yScale.min); - } else { - scaleBase = yScale.getPixelForValue(0); - } - - // Utility - point._chart = this.chart.chart; - point._xScale = xScale; - point._yScale = yScale; - point._datasetIndex = this.index; - point._index = index; - - // Desired view properties - point._model = { - x: xScale.getPixelForValue(this.getDataset().data[index], index, this.index, this.chart.isCombo), - y: reset ? scaleBase : this.calculatePointY(this.getDataset().data[index], index, this.index, this.chart.isCombo), - // Appearance - tension: point.custom && point.custom.tension ? point.custom.tension : helpers.getValueOrDefault(this.getDataset().tension, this.chart.options.elements.line.tension), - radius: point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(this.getDataset().radius, index, this.chart.options.elements.point.radius), - pointStyle: point.custom && point.custom.pointStyle ? point.custom.pointStyle : helpers.getValueAtIndexOrDefault(this.getDataset().pointStyle, index, this.chart.options.elements.point.pointStyle), - backgroundColor: this.getPointBackgroundColor(point, index), - borderColor: this.getPointBorderColor(point, index), - borderWidth: this.getPointBorderWidth(point, index), - // Tooltip - hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.getDataset().hitRadius, index, this.chart.options.elements.point.hitRadius) - }; - - point._model.skip = point.custom && point.custom.skip ? point.custom.skip : (isNaN(point._model.x) || isNaN(point._model.y)); - }, - - calculatePointY: function(value, index, datasetIndex, isCombo) { - - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var yScale = this.getScaleForId(this.getDataset().yAxisID); - - if (yScale.options.stacked) { - - var sumPos = 0, - sumNeg = 0; - - for (var i = 0; i < datasetIndex; i++) { - var ds = this.chart.data.datasets[i]; - if (ds.type === 'line' && helpers.isDatasetVisible(ds)) { - if (ds.data[index] < 0) { - sumNeg += ds.data[index] || 0; - } else { - sumPos += ds.data[index] || 0; - } - } - } - - if (value < 0) { - return yScale.getPixelForValue(sumNeg + value); - } else { - return yScale.getPixelForValue(sumPos + value); - } - } - - return yScale.getPixelForValue(value); - }, - - updateBezierControlPoints: function() { - // Update bezier control points - helpers.each(this.getDataset().metaData, function(point, index) { - var controlPoints = helpers.splineCurve( - helpers.previousItem(this.getDataset().metaData, index)._model, - point._model, - helpers.nextItem(this.getDataset().metaData, index)._model, - point._model.tension - ); - - // Prevent the bezier going outside of the bounds of the graph - point._model.controlPointPreviousX = Math.max(Math.min(controlPoints.previous.x, this.chart.chartArea.right), this.chart.chartArea.left); - point._model.controlPointPreviousY = Math.max(Math.min(controlPoints.previous.y, this.chart.chartArea.bottom), this.chart.chartArea.top); - - point._model.controlPointNextX = Math.max(Math.min(controlPoints.next.x, this.chart.chartArea.right), this.chart.chartArea.left); - point._model.controlPointNextY = Math.max(Math.min(controlPoints.next.y, this.chart.chartArea.bottom), this.chart.chartArea.top); - - // Now pivot the point for animation - point.pivot(); - }, this); - }, - - draw: function(ease) { - var easingDecimal = ease || 1; - - // Transition Point Locations - helpers.each(this.getDataset().metaData, function(point) { - point.transition(easingDecimal); - }); - - // Transition and Draw the line - if (this.chart.options.showLines) - this.getDataset().metaDataset.transition(easingDecimal).draw(); - - // Draw the points - helpers.each(this.getDataset().metaData, function(point) { - point.draw(); - }); - }, - - setHoverStyle: function(point) { - // Point - var dataset = this.chart.data.datasets[point._datasetIndex]; - var index = point._index; - - point._model.radius = point.custom && point.custom.hoverRadius ? point.custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); - point._model.backgroundColor = point.custom && point.custom.hoverBackgroundColor ? point.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(point._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - point._model.borderColor = point.custom && point.custom.hoverBorderColor ? point.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(point._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - point._model.borderWidth = point.custom && point.custom.hoverBorderWidth ? point.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, point._model.borderWidth); - }, - - removeHoverStyle: function(point) { - var dataset = this.chart.data.datasets[point._datasetIndex]; - var index = point._index; - - point._model.radius = point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(this.getDataset().radius, index, this.chart.options.elements.point.radius); - point._model.backgroundColor = this.getPointBackgroundColor(point, index); - point._model.borderColor = this.getPointBorderColor(point, index); - point._model.borderWidth = this.getPointBorderWidth(point, index); - } - }); -}; - -},{}],19:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.polarArea = { - - scale: { - type: "radialLinear", - lineArc: true // so that lines are circular - }, - - //Boolean - Whether to animate the rotation of the chart - animateRotate: true, - animateScale: true, - - aspectRatio: 1, - legendCallback: function(chart) { - var text = []; - text.push('
      '); - - if (chart.data.datasets.length) { - for (var i = 0; i < chart.data.datasets[0].data.length; ++i) { - text.push('
    • '); - if (chart.data.labels[i]) { - text.push(chart.data.labels[i]); - } - text.push('
    • '); - } - } - - text.push('
    '); - return text.join(""); - }, - legend: { - labels: { - generateLabels: function(data) { - if (data.labels.length && data.datasets.length) { - return data.labels.map(function(label, i) { - return { - text: label, - fillStyle: data.datasets[0].backgroundColor[i], - hidden: isNaN(data.datasets[0].data[i]), - - // Extra data used for toggling the correct item - index: i - }; - }); - } else { - return []; - } - } - }, - onClick: function(e, legendItem) { - helpers.each(this.chart.data.datasets, function(dataset) { - dataset.metaHiddenData = dataset.metaHiddenData || []; - var idx = legendItem.index; - - if (!isNaN(dataset.data[idx])) { - dataset.metaHiddenData[idx] = dataset.data[idx]; - dataset.data[idx] = NaN; - } else if (!isNaN(dataset.metaHiddenData[idx])) { - dataset.data[idx] = dataset.metaHiddenData[idx]; - } - }); - - this.chart.update(); - } - }, - - // Need to override these to give a nice default - tooltips: { - callbacks: { - title: function() { - return ''; - }, - label: function(tooltipItem, data) { - return data.labels[tooltipItem.index] + ': ' + tooltipItem.yLabel; - } - } - } - }; - - Chart.controllers.polarArea = Chart.DatasetController.extend({ - linkScales: function() { - // no scales for doughnut - }, - addElements: function() { - this.getDataset().metaData = this.getDataset().metaData || []; - helpers.each(this.getDataset().data, function(value, index) { - this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Arc({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - }, this); - }, - addElementAndReset: function(index) { - this.getDataset().metaData = this.getDataset().metaData || []; - var arc = new Chart.elements.Arc({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - - // Reset the point - this.updateElement(arc, index, true); - - // Add to the points array - this.getDataset().metaData.splice(index, 0, arc); - }, - getVisibleDatasetCount: function getVisibleDatasetCount() { - return helpers.where(this.chart.data.datasets, function(ds) { - return helpers.isDatasetVisible(ds); - }).length; - }, - - update: function update(reset) { - var minSize = Math.min(this.chart.chartArea.right - this.chart.chartArea.left, this.chart.chartArea.bottom - this.chart.chartArea.top); - this.chart.outerRadius = Math.max((minSize - this.chart.options.elements.arc.borderWidth / 2) / 2, 0); - this.chart.innerRadius = Math.max(this.chart.options.cutoutPercentage ? (this.chart.outerRadius / 100) * (this.chart.options.cutoutPercentage) : 1, 0); - this.chart.radiusLength = (this.chart.outerRadius - this.chart.innerRadius) / this.getVisibleDatasetCount(); - - this.getDataset().total = 0; - helpers.each(this.getDataset().data, function(value) { - this.getDataset().total += Math.abs(value); - }, this); - - this.outerRadius = this.chart.outerRadius - (this.chart.radiusLength * this.index); - this.innerRadius = this.outerRadius - this.chart.radiusLength; - - helpers.each(this.getDataset().metaData, function(arc, index) { - this.updateElement(arc, index, reset); - }, this); - }, - - updateElement: function(arc, index, reset) { - var circumference = this.calculateCircumference(this.getDataset().data[index]); - var centerX = (this.chart.chartArea.left + this.chart.chartArea.right) / 2; - var centerY = (this.chart.chartArea.top + this.chart.chartArea.bottom) / 2; - - // If there is NaN data before us, we need to calculate the starting angle correctly. - // We could be way more efficient here, but its unlikely that the polar area chart will have a lot of data - var notNullIndex = 0; - for (var i = 0; i < index; ++i) { - if (!isNaN(this.getDataset().data[i])) { - ++notNullIndex; - } - } - - var startAngle = (-0.5 * Math.PI) + (circumference * notNullIndex); - var endAngle = startAngle + circumference; - - var resetModel = { - x: centerX, - y: centerY, - innerRadius: 0, - outerRadius: this.chart.options.animateScale ? 0 : this.chart.scale.getDistanceFromCenterForValue(this.getDataset().data[index]), - startAngle: this.chart.options.animateRotate ? Math.PI * -0.5 : startAngle, - endAngle: this.chart.options.animateRotate ? Math.PI * -0.5 : endAngle, - - backgroundColor: arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor), - borderWidth: arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth), - borderColor: arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(this.chart.data.labels, index, this.chart.data.labels[index]) - }; - - helpers.extend(arc, { - // Utility - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index, - _scale: this.chart.scale, - - // Desired view properties - _model: reset ? resetModel : { - x: centerX, - y: centerY, - innerRadius: 0, - outerRadius: this.chart.scale.getDistanceFromCenterForValue(this.getDataset().data[index]), - startAngle: startAngle, - endAngle: endAngle, - - backgroundColor: arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor), - borderWidth: arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth), - borderColor: arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(this.chart.data.labels, index, this.chart.data.labels[index]) - } - }); - - arc.pivot(); - }, - - draw: function(ease) { - var easingDecimal = ease || 1; - helpers.each(this.getDataset().metaData, function(arc, index) { - arc.transition(easingDecimal).draw(); - }); - }, - - setHoverStyle: function(arc) { - var dataset = this.chart.data.datasets[arc._datasetIndex]; - var index = arc._index; - - arc._model.backgroundColor = arc.custom && arc.custom.hoverBackgroundColor ? arc.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(arc._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - arc._model.borderColor = arc.custom && arc.custom.hoverBorderColor ? arc.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(arc._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - arc._model.borderWidth = arc.custom && arc.custom.hoverBorderWidth ? arc.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, arc._model.borderWidth); - }, - - removeHoverStyle: function(arc) { - var dataset = this.chart.data.datasets[arc._datasetIndex]; - var index = arc._index; - - arc._model.backgroundColor = arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor); - arc._model.borderColor = arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor); - arc._model.borderWidth = arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth); - }, - - calculateCircumference: function(value) { - if (isNaN(value)) { - return 0; - } else { - // Count the number of NaN values - var numNaN = helpers.where(this.getDataset().data, function(data) { - return isNaN(data); - }).length; - - return (2 * Math.PI) / (this.getDataset().data.length - numNaN); - } - } - }); - -}; -},{}],20:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - - Chart.defaults.radar = { - scale: { - type: "radialLinear" - }, - elements: { - line: { - tension: 0 // no bezier in radar - } - } - }; - - Chart.controllers.radar = Chart.DatasetController.extend({ - linkScales: function() { - // No need. Single scale only - }, - - addElements: function() { - - this.getDataset().metaData = this.getDataset().metaData || []; - - this.getDataset().metaDataset = this.getDataset().metaDataset || new Chart.elements.Line({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _points: this.getDataset().metaData, - _loop: true - }); - - helpers.each(this.getDataset().data, function(value, index) { - this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Point({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index, - _model: { - x: 0, //xScale.getPixelForValue(null, index, true), - y: 0 //this.chartArea.bottom, - } - }); - }, this); - }, - addElementAndReset: function(index) { - this.getDataset().metaData = this.getDataset().metaData || []; - var point = new Chart.elements.Point({ - _chart: this.chart.chart, - _datasetIndex: this.index, - _index: index - }); - - // Reset the point - this.updateElement(point, index, true); - - // Add to the points array - this.getDataset().metaData.splice(index, 0, point); - - // Make sure bezier control points are updated - this.updateBezierControlPoints(); - }, - - update: function update(reset) { - - var line = this.getDataset().metaDataset; - var points = this.getDataset().metaData; - - var scale = this.chart.scale; - var scaleBase; - - if (scale.min < 0 && scale.max < 0) { - scaleBase = scale.getPointPositionForValue(0, scale.max); - } else if (scale.min > 0 && scale.max > 0) { - scaleBase = scale.getPointPositionForValue(0, scale.min); - } else { - scaleBase = scale.getPointPositionForValue(0, 0); - } - - helpers.extend(this.getDataset().metaDataset, { - // Utility - _datasetIndex: this.index, - // Data - _children: this.getDataset().metaData, - // Model - _model: { - // Appearance - tension: line.custom && line.custom.tension ? line.custom.tension : helpers.getValueOrDefault(this.getDataset().tension, this.chart.options.elements.line.tension), - backgroundColor: line.custom && line.custom.backgroundColor ? line.custom.backgroundColor : (this.getDataset().backgroundColor || this.chart.options.elements.line.backgroundColor), - borderWidth: line.custom && line.custom.borderWidth ? line.custom.borderWidth : (this.getDataset().borderWidth || this.chart.options.elements.line.borderWidth), - borderColor: line.custom && line.custom.borderColor ? line.custom.borderColor : (this.getDataset().borderColor || this.chart.options.elements.line.borderColor), - fill: line.custom && line.custom.fill ? line.custom.fill : (this.getDataset().fill !== undefined ? this.getDataset().fill : this.chart.options.elements.line.fill), - borderCapStyle: line.custom && line.custom.borderCapStyle ? line.custom.borderCapStyle : (this.getDataset().borderCapStyle || this.chart.options.elements.line.borderCapStyle), - borderDash: line.custom && line.custom.borderDash ? line.custom.borderDash : (this.getDataset().borderDash || this.chart.options.elements.line.borderDash), - borderDashOffset: line.custom && line.custom.borderDashOffset ? line.custom.borderDashOffset : (this.getDataset().borderDashOffset || this.chart.options.elements.line.borderDashOffset), - borderJoinStyle: line.custom && line.custom.borderJoinStyle ? line.custom.borderJoinStyle : (this.getDataset().borderJoinStyle || this.chart.options.elements.line.borderJoinStyle), - - // Scale - scaleTop: scale.top, - scaleBottom: scale.bottom, - scaleZero: scaleBase - } - }); - - this.getDataset().metaDataset.pivot(); - - // Update Points - helpers.each(points, function(point, index) { - this.updateElement(point, index, reset); - }, this); - - - // Update bezier control points - this.updateBezierControlPoints(); - }, - updateElement: function(point, index, reset) { - var pointPosition = this.chart.scale.getPointPositionForValue(index, this.getDataset().data[index]); - - helpers.extend(point, { - // Utility - _datasetIndex: this.index, - _index: index, - _scale: this.chart.scale, - - // Desired view properties - _model: { - x: reset ? this.chart.scale.xCenter : pointPosition.x, // value not used in dataset scale, but we want a consistent API between scales - y: reset ? this.chart.scale.yCenter : pointPosition.y, - - // Appearance - tension: point.custom && point.custom.tension ? point.custom.tension : helpers.getValueOrDefault(this.getDataset().tension, this.chart.options.elements.line.tension), - radius: point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(this.getDataset().pointRadius, index, this.chart.options.elements.point.radius), - backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().pointBackgroundColor, index, this.chart.options.elements.point.backgroundColor), - borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().pointBorderColor, index, this.chart.options.elements.point.borderColor), - borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().pointBorderWidth, index, this.chart.options.elements.point.borderWidth), - pointStyle: point.custom && point.custom.pointStyle ? point.custom.pointStyle : helpers.getValueAtIndexOrDefault(this.getDataset().pointStyle, index, this.chart.options.elements.point.pointStyle), - - // Tooltip - hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.getDataset().hitRadius, index, this.chart.options.elements.point.hitRadius) - } - }); - - point._model.skip = point.custom && point.custom.skip ? point.custom.skip : (isNaN(point._model.x) || isNaN(point._model.y)); - }, - updateBezierControlPoints: function() { - helpers.each(this.getDataset().metaData, function(point, index) { - var controlPoints = helpers.splineCurve( - helpers.previousItem(this.getDataset().metaData, index, true)._model, - point._model, - helpers.nextItem(this.getDataset().metaData, index, true)._model, - point._model.tension - ); - - // Prevent the bezier going outside of the bounds of the graph - point._model.controlPointPreviousX = Math.max(Math.min(controlPoints.previous.x, this.chart.chartArea.right), this.chart.chartArea.left); - point._model.controlPointPreviousY = Math.max(Math.min(controlPoints.previous.y, this.chart.chartArea.bottom), this.chart.chartArea.top); - - point._model.controlPointNextX = Math.max(Math.min(controlPoints.next.x, this.chart.chartArea.right), this.chart.chartArea.left); - point._model.controlPointNextY = Math.max(Math.min(controlPoints.next.y, this.chart.chartArea.bottom), this.chart.chartArea.top); - - // Now pivot the point for animation - point.pivot(); - }, this); - }, - - draw: function(ease) { - var easingDecimal = ease || 1; - - // Transition Point Locations - helpers.each(this.getDataset().metaData, function(point, index) { - point.transition(easingDecimal); - }); - - // Transition and Draw the line - this.getDataset().metaDataset.transition(easingDecimal).draw(); - - // Draw the points - helpers.each(this.getDataset().metaData, function(point) { - point.draw(); - }); - }, - - setHoverStyle: function(point) { - // Point - var dataset = this.chart.data.datasets[point._datasetIndex]; - var index = point._index; - - point._model.radius = point.custom && point.custom.hoverRadius ? point.custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); - point._model.backgroundColor = point.custom && point.custom.hoverBackgroundColor ? point.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(point._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - point._model.borderColor = point.custom && point.custom.hoverBorderColor ? point.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(point._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - point._model.borderWidth = point.custom && point.custom.hoverBorderWidth ? point.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, point._model.borderWidth); - }, - - removeHoverStyle: function(point) { - var dataset = this.chart.data.datasets[point._datasetIndex]; - var index = point._index; - - point._model.radius = point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(this.getDataset().radius, index, this.chart.options.elements.point.radius); - point._model.backgroundColor = point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().pointBackgroundColor, index, this.chart.options.elements.point.backgroundColor); - point._model.borderColor = point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().pointBorderColor, index, this.chart.options.elements.point.borderColor); - point._model.borderWidth = point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().pointBorderWidth, index, this.chart.options.elements.point.borderWidth); - } - }); -}; -},{}],21:[function(require,module,exports){ -/*global window: false */ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.global.animation = { - duration: 1000, - easing: "easeOutQuart", - onProgress: helpers.noop, - onComplete: helpers.noop - }; - - Chart.Animation = Chart.Element.extend({ - currentStep: null, // the current animation step - numSteps: 60, // default number of steps - easing: "", // the easing to use for this animation - render: null, // render function used by the animation service - - onAnimationProgress: null, // user specified callback to fire on each step of the animation - onAnimationComplete: null // user specified callback to fire when the animation finishes - }); - - Chart.animationService = { - frameDuration: 17, - animations: [], - dropFrames: 0, - request: null, - addAnimation: function(chartInstance, animationObject, duration, lazy) { - - if (!lazy) { - chartInstance.animating = true; - } - - for (var index = 0; index < this.animations.length; ++index) { - if (this.animations[index].chartInstance === chartInstance) { - // replacing an in progress animation - this.animations[index].animationObject = animationObject; - return; - } - } - - this.animations.push({ - chartInstance: chartInstance, - animationObject: animationObject - }); - - // If there are no animations queued, manually kickstart a digest, for lack of a better word - if (this.animations.length === 1) { - this.requestAnimationFrame(); - } - }, - // Cancel the animation for a given chart instance - cancelAnimation: function(chartInstance) { - var index = helpers.findIndex(this.animations, function(animationWrapper) { - return animationWrapper.chartInstance === chartInstance; - }); - - if (index !== -1) { - this.animations.splice(index, 1); - chartInstance.animating = false; - } - }, - requestAnimationFrame: function() { - var me = this; - if (me.request === null) { - // Skip animation frame requests until the active one is executed. - // This can happen when processing mouse events, e.g. 'mousemove' - // and 'mouseout' events will trigger multiple renders. - me.request = helpers.requestAnimFrame.call(window, function() { - me.request = null; - me.startDigest(); - }); - } - }, - startDigest: function() { - - var startTime = Date.now(); - var framesToDrop = 0; - - if (this.dropFrames > 1) { - framesToDrop = Math.floor(this.dropFrames); - this.dropFrames = this.dropFrames % 1; - } - - var i = 0; - while (i < this.animations.length) { - if (this.animations[i].animationObject.currentStep === null) { - this.animations[i].animationObject.currentStep = 0; - } - - this.animations[i].animationObject.currentStep += 1 + framesToDrop; - - if (this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps) { - this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps; - } - - this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject); - if (this.animations[i].animationObject.onAnimationProgress && this.animations[i].animationObject.onAnimationProgress.call) { - this.animations[i].animationObject.onAnimationProgress.call(this.animations[i].chartInstance, this.animations[i]); - } - - if (this.animations[i].animationObject.currentStep === this.animations[i].animationObject.numSteps) { - if (this.animations[i].animationObject.onAnimationComplete && this.animations[i].animationObject.onAnimationComplete.call) { - this.animations[i].animationObject.onAnimationComplete.call(this.animations[i].chartInstance, this.animations[i]); - } - - // executed the last frame. Remove the animation. - this.animations[i].chartInstance.animating = false; - - this.animations.splice(i, 1); - } else { - ++i; - } - } - - var endTime = Date.now(); - var dropFrames = (endTime - startTime) / this.frameDuration; - - this.dropFrames += dropFrames; - - // Do we have more stuff to animate? - if (this.animations.length > 0) { - this.requestAnimationFrame(); - } - } - }; -}; -},{}],22:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - //Create a dictionary of chart types, to allow for extension of existing types - Chart.types = {}; - - //Store a reference to each instance - allowing us to globally resize chart instances on window resize. - //Destroy method on the chart will remove the instance of the chart from this reference. - Chart.instances = {}; - - // Controllers available for dataset visualization eg. bar, line, slice, etc. - Chart.controllers = {}; - - // The main controller of a chart - Chart.Controller = function(instance) { - - this.chart = instance; - this.config = instance.config; - this.options = this.config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[this.config.type], this.config.options || {}); - this.id = helpers.uid(); - - Object.defineProperty(this, 'data', { - get: function() { - return this.config.data; - } - }); - - //Add the chart instance to the global namespace - Chart.instances[this.id] = this; - - if (this.options.responsive) { - // Silent resize before chart draws - this.resize(true); - } - - this.initialize(); - - return this; - }; - - helpers.extend(Chart.Controller.prototype, { - - initialize: function initialize() { - - // TODO - // If BeforeInit(this) doesn't return false, proceed - - this.bindEvents(); - - // Make sure controllers are built first so that each dataset is bound to an axis before the scales - // are built - this.ensureScalesHaveIDs(); - this.buildOrUpdateControllers(); - this.buildScales(); - this.buildSurroundingItems(); - this.updateLayout(); - this.resetElements(); - this.initToolTip(); - this.update(); - - // TODO - // If AfterInit(this) doesn't return false, proceed - - return this; - }, - - clear: function clear() { - helpers.clear(this.chart); - return this; - }, - - stop: function stop() { - // Stops any current animation loop occuring - Chart.animationService.cancelAnimation(this); - return this; - }, - - resize: function resize(silent) { - var canvas = this.chart.canvas; - var newWidth = helpers.getMaximumWidth(this.chart.canvas); - var newHeight = (this.options.maintainAspectRatio && isNaN(this.chart.aspectRatio) === false && isFinite(this.chart.aspectRatio) && this.chart.aspectRatio !== 0) ? newWidth / this.chart.aspectRatio : helpers.getMaximumHeight(this.chart.canvas); - - var sizeChanged = this.chart.width !== newWidth || this.chart.height !== newHeight; - - if (!sizeChanged) - return this; - - canvas.width = this.chart.width = newWidth; - canvas.height = this.chart.height = newHeight; - - helpers.retinaScale(this.chart); - - if (!silent) { - this.stop(); - this.update(this.options.responsiveAnimationDuration); - } - - return this; - }, - ensureScalesHaveIDs: function ensureScalesHaveIDs() { - var defaultXAxisID = 'x-axis-'; - var defaultYAxisID = 'y-axis-'; - - if (this.options.scales) { - if (this.options.scales.xAxes && this.options.scales.xAxes.length) { - helpers.each(this.options.scales.xAxes, function(xAxisOptions, index) { - xAxisOptions.id = xAxisOptions.id || (defaultXAxisID + index); - }); - } - - if (this.options.scales.yAxes && this.options.scales.yAxes.length) { - // Build the y axes - helpers.each(this.options.scales.yAxes, function(yAxisOptions, index) { - yAxisOptions.id = yAxisOptions.id || (defaultYAxisID + index); - }); - } - } - }, - buildScales: function buildScales() { - // Map of scale ID to scale object so we can lookup later - this.scales = {}; - - // Build the x axes - if (this.options.scales) { - if (this.options.scales.xAxes && this.options.scales.xAxes.length) { - helpers.each(this.options.scales.xAxes, function(xAxisOptions, index) { - var xType = helpers.getValueOrDefault(xAxisOptions.type, 'category'); - var ScaleClass = Chart.scaleService.getScaleConstructor(xType); - if (ScaleClass) { - var scale = new ScaleClass({ - ctx: this.chart.ctx, - options: xAxisOptions, - chart: this, - id: xAxisOptions.id - }); - - this.scales[scale.id] = scale; - } - }, this); - } - - if (this.options.scales.yAxes && this.options.scales.yAxes.length) { - // Build the y axes - helpers.each(this.options.scales.yAxes, function(yAxisOptions, index) { - var yType = helpers.getValueOrDefault(yAxisOptions.type, 'linear'); - var ScaleClass = Chart.scaleService.getScaleConstructor(yType); - if (ScaleClass) { - var scale = new ScaleClass({ - ctx: this.chart.ctx, - options: yAxisOptions, - chart: this, - id: yAxisOptions.id - }); - - this.scales[scale.id] = scale; - } - }, this); - } - } - if (this.options.scale) { - // Build radial axes - var ScaleClass = Chart.scaleService.getScaleConstructor(this.options.scale.type); - if (ScaleClass) { - var scale = new ScaleClass({ - ctx: this.chart.ctx, - options: this.options.scale, - chart: this - }); - - this.scale = scale; - - this.scales.radialScale = scale; - } - } - - Chart.scaleService.addScalesToLayout(this); - }, - - buildSurroundingItems: function() { - if (this.options.title) { - this.titleBlock = new Chart.Title({ - ctx: this.chart.ctx, - options: this.options.title, - chart: this - }); - - Chart.layoutService.addBox(this, this.titleBlock); - } - - if (this.options.legend) { - this.legend = new Chart.Legend({ - ctx: this.chart.ctx, - options: this.options.legend, - chart: this - }); - - Chart.layoutService.addBox(this, this.legend); - } - }, - - updateLayout: function() { - Chart.layoutService.update(this, this.chart.width, this.chart.height); - }, - - buildOrUpdateControllers: function buildOrUpdateControllers() { - var types = []; - var newControllers = []; - - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - if (!dataset.type) { - dataset.type = this.config.type; - } - - var type = dataset.type; - types.push(type); - - if (dataset.controller) { - dataset.controller.updateIndex(datasetIndex); - } else { - dataset.controller = new Chart.controllers[type](this, datasetIndex); - newControllers.push(dataset.controller); - } - }, this); - - if (types.length > 1) { - for (var i = 1; i < types.length; i++) { - if (types[i] !== types[i - 1]) { - this.isCombo = true; - break; - } - } - } - - return newControllers; - }, - - resetElements: function resetElements() { - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - dataset.controller.reset(); - }); - }, - - update: function update(animationDuration, lazy) { - // In case the entire data object changed - this.tooltip._data = this.data; - - // Make sure dataset controllers are updated and new controllers are reset - var newControllers = this.buildOrUpdateControllers(); - - Chart.layoutService.update(this, this.chart.width, this.chart.height); - - // Can only reset the new controllers after the scales have been updated - helpers.each(newControllers, function(controller) { - controller.reset(); - }); - - // Make sure all dataset controllers have correct meta data counts - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - dataset.controller.buildOrUpdateElements(); - }); - - // This will loop through any data and do the appropriate element update for the type - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - dataset.controller.update(); - }); - this.render(animationDuration, lazy); - }, - - render: function render(duration, lazy) { - - if (this.options.animation && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && this.options.animation.duration !== 0))) { - var animation = new Chart.Animation(); - animation.numSteps = (duration || this.options.animation.duration) / 16.66; //60 fps - animation.easing = this.options.animation.easing; - - // render function - animation.render = function(chartInstance, animationObject) { - var easingFunction = helpers.easingEffects[animationObject.easing]; - var stepDecimal = animationObject.currentStep / animationObject.numSteps; - var easeDecimal = easingFunction(stepDecimal); - - chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep); - }; - - // user events - animation.onAnimationProgress = this.options.animation.onProgress; - animation.onAnimationComplete = this.options.animation.onComplete; - - Chart.animationService.addAnimation(this, animation, duration, lazy); - } else { - this.draw(); - if (this.options.animation && this.options.animation.onComplete && this.options.animation.onComplete.call) { - this.options.animation.onComplete.call(this); - } - } - return this; - }, - - draw: function(ease) { - var easingDecimal = ease || 1; - this.clear(); - - // Draw all the scales - helpers.each(this.boxes, function(box) { - box.draw(this.chartArea); - }, this); - if (this.scale) { - this.scale.draw(); - } - - // Clip out the chart area so that anything outside does not draw. This is necessary for zoom and pan to function - this.chart.ctx.save(); - this.chart.ctx.beginPath(); - this.chart.ctx.rect(this.chartArea.left, this.chartArea.top, this.chartArea.right - this.chartArea.left, this.chartArea.bottom - this.chartArea.top); - this.chart.ctx.clip(); - - // Draw each dataset via its respective controller (reversed to support proper line stacking) - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - if (helpers.isDatasetVisible(dataset)) { - dataset.controller.draw(ease); - } - }, null, true); - - // Restore from the clipping operation - this.chart.ctx.restore(); - - // Finally draw the tooltip - this.tooltip.transition(easingDecimal).draw(); - }, - - // Get the single element that was clicked on - // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw - getElementAtEvent: function(e) { - - var eventPosition = helpers.getRelativePosition(e, this.chart); - var elementsArray = []; - - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - if (helpers.isDatasetVisible(dataset)) { - helpers.each(dataset.metaData, function(element, index) { - if (element.inRange(eventPosition.x, eventPosition.y)) { - elementsArray.push(element); - return elementsArray; - } - }); - } - }); - - return elementsArray; - }, - - getElementsAtEvent: function(e) { - var eventPosition = helpers.getRelativePosition(e, this.chart); - var elementsArray = []; - - var found = (function() { - if (this.data.datasets) { - for (var i = 0; i < this.data.datasets.length; i++) { - if (helpers.isDatasetVisible(this.data.datasets[i])) { - for (var j = 0; j < this.data.datasets[i].metaData.length; j++) { - if (this.data.datasets[i].metaData[j].inRange(eventPosition.x, eventPosition.y)) { - return this.data.datasets[i].metaData[j]; - } - } - } - } - } - }).call(this); - - if (!found) { - return elementsArray; - } - - helpers.each(this.data.datasets, function(dataset, dsIndex) { - if (helpers.isDatasetVisible(dataset)) { - elementsArray.push(dataset.metaData[found._index]); - } - }); - - return elementsArray; - }, - - getDatasetAtEvent: function(e) { - var elementsArray = this.getElementAtEvent(e); - - if (elementsArray.length > 0) { - elementsArray = this.data.datasets[elementsArray[0]._datasetIndex].metaData; - } - - return elementsArray; - }, - - generateLegend: function generateLegend() { - return this.options.legendCallback(this); - }, - - destroy: function destroy() { - this.clear(); - helpers.unbindEvents(this, this.events); - helpers.removeResizeListener(this.chart.canvas.parentNode); - - // Reset canvas height/width attributes - var canvas = this.chart.canvas; - canvas.width = this.chart.width; - canvas.height = this.chart.height; - - // if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here - if (this.chart.originalDevicePixelRatio !== undefined) { - this.chart.ctx.scale(1 / this.chart.originalDevicePixelRatio, 1 / this.chart.originalDevicePixelRatio); - } - - // Reset to the old style since it may have been changed by the device pixel ratio changes - canvas.style.width = this.chart.originalCanvasStyleWidth; - canvas.style.height = this.chart.originalCanvasStyleHeight; - - delete Chart.instances[this.id]; - }, - - toBase64Image: function toBase64Image() { - return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); - }, - - initToolTip: function initToolTip() { - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _chartInstance: this, - _data: this.data, - _options: this.options - }, this); - }, - - bindEvents: function bindEvents() { - helpers.bindEvents(this, this.options.events, function(evt) { - this.eventHandler(evt); - }); - }, - eventHandler: function eventHandler(e) { - this.lastActive = this.lastActive || []; - this.lastTooltipActive = this.lastTooltipActive || []; - - // Find Active Elements for hover and tooltips - if (e.type === 'mouseout') { - this.active = []; - this.tooltipActive = []; - } else { - - var _this = this; - var getItemsForMode = function(mode) { - switch (mode) { - case 'single': - return _this.getElementAtEvent(e); - case 'label': - return _this.getElementsAtEvent(e); - case 'dataset': - return _this.getDatasetAtEvent(e); - default: - return e; - } - }; - - this.active = getItemsForMode(this.options.hover.mode); - this.tooltipActive = getItemsForMode(this.options.tooltips.mode); - } - - // On Hover hook - if (this.options.hover.onHover) { - this.options.hover.onHover.call(this, this.active); - } - - if (e.type === 'mouseup' || e.type === 'click') { - if (this.options.onClick) { - this.options.onClick.call(this, e, this.active); - } - - if (this.legend && this.legend.handleEvent) { - this.legend.handleEvent(e); - } - } - - var dataset; - var index; - - // Remove styling for last active (even if it may still be active) - if (this.lastActive.length) { - switch (this.options.hover.mode) { - case 'single': - this.data.datasets[this.lastActive[0]._datasetIndex].controller.removeHoverStyle(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index); - break; - case 'label': - case 'dataset': - for (var i = 0; i < this.lastActive.length; i++) { - if (this.lastActive[i]) - this.data.datasets[this.lastActive[i]._datasetIndex].controller.removeHoverStyle(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index); - } - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if (this.active.length && this.options.hover.mode) { - switch (this.options.hover.mode) { - case 'single': - this.data.datasets[this.active[0]._datasetIndex].controller.setHoverStyle(this.active[0]); - break; - case 'label': - case 'dataset': - for (var j = 0; j < this.active.length; j++) { - if (this.active[j]) - this.data.datasets[this.active[j]._datasetIndex].controller.setHoverStyle(this.active[j]); - } - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if (this.options.tooltips.enabled || this.options.tooltips.custom) { - - // The usual updates - this.tooltip.initialize(); - this.tooltip._active = this.tooltipActive; - this.tooltip.update(); - } - - // Hover animations - this.tooltip.pivot(); - - if (!this.animating) { - var changed; - - helpers.each(this.active, function(element, index) { - if (element !== this.lastActive[index]) { - changed = true; - } - }, this); - - helpers.each(this.tooltipActive, function(element, index) { - if (element !== this.lastTooltipActive[index]) { - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((this.lastActive.length !== this.active.length) || - (this.lastTooltipActive.length !== this.tooltipActive.length) || - changed) { - - this.stop(); - - if (this.options.tooltips.enabled || this.options.tooltips.custom) { - this.tooltip.update(true); - } - - // We only need to render at this point. Updating will cause scales to be recomputed generating flicker & using more - // memory than necessary. - this.render(this.options.hover.animationDuration, true); - } - } - - // Remember Last Actives - this.lastActive = this.active; - this.lastTooltipActive = this.tooltipActive; - return this; - } - }); -}; - -},{}],23:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - // Base class for all dataset controllers (line, bar, etc) - Chart.DatasetController = function(chart, datasetIndex) { - this.initialize.call(this, chart, datasetIndex); - }; - - helpers.extend(Chart.DatasetController.prototype, { - initialize: function(chart, datasetIndex) { - this.chart = chart; - this.index = datasetIndex; - this.linkScales(); - this.addElements(); - }, - updateIndex: function(datasetIndex) { - this.index = datasetIndex; - }, - - linkScales: function() { - if (!this.getDataset().xAxisID) { - this.getDataset().xAxisID = this.chart.options.scales.xAxes[0].id; - } - - if (!this.getDataset().yAxisID) { - this.getDataset().yAxisID = this.chart.options.scales.yAxes[0].id; - } - }, - - getDataset: function() { - return this.chart.data.datasets[this.index]; - }, - - getScaleForId: function(scaleID) { - return this.chart.scales[scaleID]; - }, - - reset: function() { - this.update(true); - }, - - buildOrUpdateElements: function buildOrUpdateElements() { - // Handle the number of data points changing - var numData = this.getDataset().data.length; - var numMetaData = this.getDataset().metaData.length; - - // Make sure that we handle number of datapoints changing - if (numData < numMetaData) { - // Remove excess bars for data points that have been removed - this.getDataset().metaData.splice(numData, numMetaData - numData); - } else if (numData > numMetaData) { - // Add new elements - for (var index = numMetaData; index < numData; ++index) { - this.addElementAndReset(index); - } - } - }, - - // Controllers should implement the following - addElements: helpers.noop, - addElementAndReset: helpers.noop, - draw: helpers.noop, - removeHoverStyle: helpers.noop, - setHoverStyle: helpers.noop, - update: helpers.noop - }); - - Chart.DatasetController.extend = helpers.inherits; - -}; -},{}],24:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.elements = {}; - - Chart.Element = function(configuration) { - helpers.extend(this, configuration); - this.initialize.apply(this, arguments); - }; - helpers.extend(Chart.Element.prototype, { - initialize: function() {}, - pivot: function() { - if (!this._view) { - this._view = helpers.clone(this._model); - } - this._start = helpers.clone(this._view); - return this; - }, - transition: function(ease) { - if (!this._view) { - this._view = helpers.clone(this._model); - } - - // No animation -> No Transition - if (ease === 1) { - this._view = this._model; - this._start = null; - return this; - } - - if (!this._start) { - this.pivot(); - } - - helpers.each(this._model, function(value, key) { - - if (key[0] === '_' || !this._model.hasOwnProperty(key)) { - // Only non-underscored properties - } - - // Init if doesn't exist - else if (!this._view.hasOwnProperty(key)) { - if (typeof value === 'number' && !isNaN(this._view[key])) { - this._view[key] = value * ease; - } else { - this._view[key] = value; - } - } - - // No unnecessary computations - else if (value === this._view[key]) { - // It's the same! Woohoo! - } - - // Color transitions if possible - else if (typeof value === 'string') { - try { - var color = helpers.color(this._start[key]).mix(helpers.color(this._model[key]), ease); - this._view[key] = color.rgbString(); - } catch (err) { - this._view[key] = value; - } - } - // Number transitions - else if (typeof value === 'number') { - var startVal = this._start[key] !== undefined && isNaN(this._start[key]) === false ? this._start[key] : 0; - this._view[key] = ((this._model[key] - startVal) * ease) + startVal; - } - // Everything else - else { - this._view[key] = value; - } - }, this); - - return this; - }, - tooltipPosition: function() { - return { - x: this._model.x, - y: this._model.y - }; - }, - hasValue: function() { - return helpers.isNumber(this._model.x) && helpers.isNumber(this._model.y); - } - }); - - Chart.Element.extend = helpers.inherits; - -}; - -},{}],25:[function(require,module,exports){ -/*global window: false */ -/*global document: false */ -"use strict"; - -var color = require('chartjs-color'); - -module.exports = function(Chart) { - - //Global Chart helpers object for utility methods and classes - var helpers = Chart.helpers = {}; - - //-- Basic js utility methods - helpers.each = function(loopable, callback, self, reverse) { - // Check to see if null or undefined firstly. - var i, len; - if (helpers.isArray(loopable)) { - len = loopable.length; - if (reverse) { - for (i = len - 1; i >= 0; i--) { - callback.call(self, loopable[i], i); - } - } else { - for (i = 0; i < len; i++) { - callback.call(self, loopable[i], i); - } - } - } else if (typeof loopable === 'object') { - var keys = Object.keys(loopable); - len = keys.length; - for (i = 0; i < len; i++) { - callback.call(self, loopable[keys[i]], keys[i]); - } - } - }; - helpers.clone = function(obj) { - var objClone = {}; - helpers.each(obj, function(value, key) { - if (obj.hasOwnProperty(key)) { - if (helpers.isArray(value)) { - objClone[key] = value.slice(0); - } else if (typeof value === 'object' && value !== null) { - objClone[key] = helpers.clone(value); - } else { - objClone[key] = value; - } - } - }); - return objClone; - }; - helpers.extend = function(base) { - var len = arguments.length; - var additionalArgs = []; - for (var i = 1; i < len; i++) { - additionalArgs.push(arguments[i]); - } - helpers.each(additionalArgs, function(extensionObject) { - helpers.each(extensionObject, function(value, key) { - if (extensionObject.hasOwnProperty(key)) { - base[key] = value; - } - }); - }); - return base; - }; - // Need a special merge function to chart configs since they are now grouped - helpers.configMerge = function(_base) { - var base = helpers.clone(_base); - helpers.each(Array.prototype.slice.call(arguments, 1), function(extension) { - helpers.each(extension, function(value, key) { - if (extension.hasOwnProperty(key)) { - if (key === 'scales') { - // Scale config merging is complex. Add out own function here for that - base[key] = helpers.scaleMerge(base.hasOwnProperty(key) ? base[key] : {}, value); - - } else if (key === 'scale') { - // Used in polar area & radar charts since there is only one scale - base[key] = helpers.configMerge(base.hasOwnProperty(key) ? base[key] : {}, Chart.scaleService.getScaleDefaults(value.type), value); - } else if (base.hasOwnProperty(key) && helpers.isArray(base[key]) && helpers.isArray(value)) { - // In this case we have an array of objects replacing another array. Rather than doing a strict replace, - // merge. This allows easy scale option merging - var baseArray = base[key]; - - helpers.each(value, function(valueObj, index) { - - if (index < baseArray.length) { - if (typeof baseArray[index] === 'object' && baseArray[index] !== null && typeof valueObj === 'object' && valueObj !== null) { - // Two objects are coming together. Do a merge of them. - baseArray[index] = helpers.configMerge(baseArray[index], valueObj); - } else { - // Just overwrite in this case since there is nothing to merge - baseArray[index] = valueObj; - } - } else { - baseArray.push(valueObj); // nothing to merge - } - }); - - } else if (base.hasOwnProperty(key) && typeof base[key] === "object" && base[key] !== null && typeof value === "object") { - // If we are overwriting an object with an object, do a merge of the properties. - base[key] = helpers.configMerge(base[key], value); - - } else { - // can just overwrite the value in this case - base[key] = value; - } - } - }); - }); - - return base; - }; - helpers.extendDeep = function(_base) { - return _extendDeep.apply(this, arguments); - - function _extendDeep(dst) { - helpers.each(arguments, function(obj) { - if (obj !== dst) { - helpers.each(obj, function(value, key) { - if (dst[key] && dst[key].constructor && dst[key].constructor === Object) { - _extendDeep(dst[key], value); - } else { - dst[key] = value; - } - }); - } - }); - return dst; - } - }; - helpers.scaleMerge = function(_base, extension) { - var base = helpers.clone(_base); - - helpers.each(extension, function(value, key) { - if (extension.hasOwnProperty(key)) { - if (key === 'xAxes' || key === 'yAxes') { - // These properties are arrays of items - if (base.hasOwnProperty(key)) { - helpers.each(value, function(valueObj, index) { - var axisType = helpers.getValueOrDefault(valueObj.type, key === 'xAxes' ? 'category' : 'linear'); - var axisDefaults = Chart.scaleService.getScaleDefaults(axisType); - if (index >= base[key].length || !base[key][index].type) { - base[key].push(helpers.configMerge(axisDefaults, valueObj)); - } else if (valueObj.type && valueObj.type !== base[key][index].type) { - // Type changed. Bring in the new defaults before we bring in valueObj so that valueObj can override the correct scale defaults - base[key][index] = helpers.configMerge(base[key][index], axisDefaults, valueObj); - } else { - // Type is the same - base[key][index] = helpers.configMerge(base[key][index], valueObj); - } - }); - } else { - base[key] = []; - helpers.each(value, function(valueObj) { - var axisType = helpers.getValueOrDefault(valueObj.type, key === 'xAxes' ? 'category' : 'linear'); - base[key].push(helpers.configMerge(Chart.scaleService.getScaleDefaults(axisType), valueObj)); - }); - } - } else if (base.hasOwnProperty(key) && typeof base[key] === "object" && base[key] !== null && typeof value === "object") { - // If we are overwriting an object with an object, do a merge of the properties. - base[key] = helpers.configMerge(base[key], value); - - } else { - // can just overwrite the value in this case - base[key] = value; - } - } - }); - - return base; - }; - helpers.getValueAtIndexOrDefault = function(value, index, defaultValue) { - if (value === undefined || value === null) { - return defaultValue; - } - - if (helpers.isArray(value)) { - return index < value.length ? value[index] : defaultValue; - } - - return value; - }; - helpers.getValueOrDefault = function(value, defaultValue) { - return value === undefined ? defaultValue : value; - }; - helpers.indexOf = function(arrayToSearch, item) { - if (Array.prototype.indexOf) { - return arrayToSearch.indexOf(item); - } else { - for (var i = 0; i < arrayToSearch.length; i++) { - if (arrayToSearch[i] === item) - return i; - } - return -1; - } - }; - helpers.where = function(collection, filterCallback) { - var filtered = []; - - helpers.each(collection, function(item) { - if (filterCallback(item)) { - filtered.push(item); - } - }); - - return filtered; - }; - helpers.findIndex = function(arrayToSearch, callback, thisArg) { - var index = -1; - if (Array.prototype.findIndex) { - index = arrayToSearch.findIndex(callback, thisArg); - } else { - for (var i = 0; i < arrayToSearch.length; ++i) { - thisArg = thisArg !== undefined ? thisArg : arrayToSearch; - - if (callback.call(thisArg, arrayToSearch[i], i, arrayToSearch)) { - index = i; - break; - } - } - } - - return index; - }; - helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex) { - // Default to start of the array - if (startIndex === undefined || startIndex === null) { - startIndex = -1; - } - for (var i = startIndex + 1; i < arrayToSearch.length; i++) { - var currentItem = arrayToSearch[i]; - if (filterCallback(currentItem)) { - return currentItem; - } - } - }; - helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) { - // Default to end of the array - if (startIndex === undefined || startIndex === null) { - startIndex = arrayToSearch.length; - } - for (var i = startIndex - 1; i >= 0; i--) { - var currentItem = arrayToSearch[i]; - if (filterCallback(currentItem)) { - return currentItem; - } - } - }; - helpers.inherits = function(extensions) { - //Basic javascript inheritance based on the model created in Backbone.js - var parent = this; - var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function() { - return parent.apply(this, arguments); - }; - - var Surrogate = function() { - this.constructor = ChartElement; - }; - Surrogate.prototype = parent.prototype; - ChartElement.prototype = new Surrogate(); - - ChartElement.extend = helpers.inherits; - - if (extensions) { - helpers.extend(ChartElement.prototype, extensions); - } - - ChartElement.__super__ = parent.prototype; - - return ChartElement; - }; - helpers.noop = function() {}; - helpers.uid = (function() { - var id = 0; - return function() { - return "chart-" + id++; - }; - })(); - helpers.warn = function(str) { - //Method for warning of errors - if (console && typeof console.warn === "function") { - console.warn(str); - } - }; - //-- Math methods - helpers.isNumber = function(n) { - return !isNaN(parseFloat(n)) && isFinite(n); - }; - helpers.almostEquals = function(x, y, epsilon) { - return Math.abs(x - y) < epsilon; - }; - helpers.max = function(array) { - return array.reduce(function(max, value) { - if (!isNaN(value)) { - return Math.max(max, value); - } else { - return max; - } - }, Number.NEGATIVE_INFINITY); - }; - helpers.min = function(array) { - return array.reduce(function(min, value) { - if (!isNaN(value)) { - return Math.min(min, value); - } else { - return min; - } - }, Number.POSITIVE_INFINITY); - }; - helpers.sign = function(x) { - if (Math.sign) { - return Math.sign(x); - } else { - x = +x; // convert to a number - if (x === 0 || isNaN(x)) { - return x; - } - return x > 0 ? 1 : -1; - } - }; - helpers.log10 = function(x) { - if (Math.log10) { - return Math.log10(x); - } else { - return Math.log(x) / Math.LN10; - } - }; - helpers.toRadians = function(degrees) { - return degrees * (Math.PI / 180); - }; - helpers.toDegrees = function(radians) { - return radians * (180 / Math.PI); - }; - // Gets the angle from vertical upright to the point about a centre. - helpers.getAngleFromPoint = function(centrePoint, anglePoint) { - var distanceFromXCenter = anglePoint.x - centrePoint.x, - distanceFromYCenter = anglePoint.y - centrePoint.y, - radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); - - var angle = Math.atan2(distanceFromYCenter, distanceFromXCenter); - - if (angle < (-0.5 * Math.PI)) { - angle += 2.0 * Math.PI; // make sure the returned angle is in the range of (-PI/2, 3PI/2] - } - - return { - angle: angle, - distance: radialDistanceFromCenter - }; - }; - helpers.aliasPixel = function(pixelWidth) { - return (pixelWidth % 2 === 0) ? 0 : 0.5; - }; - helpers.splineCurve = function(firstPoint, middlePoint, afterPoint, t) { - //Props to Rob Spencer at scaled innovation for his post on splining between points - //http://scaledinnovation.com/analytics/splines/aboutSplines.html - - // This function must also respect "skipped" points - - var previous = firstPoint.skip ? middlePoint : firstPoint, - current = middlePoint, - next = afterPoint.skip ? middlePoint : afterPoint; - - var d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2)); - var d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2)); - - var s01 = d01 / (d01 + d12); - var s12 = d12 / (d01 + d12); - - // If all points are the same, s01 & s02 will be inf - s01 = isNaN(s01) ? 0 : s01; - s12 = isNaN(s12) ? 0 : s12; - - var fa = t * s01; // scaling factor for triangle Ta - var fb = t * s12; - - return { - previous: { - x: current.x - fa * (next.x - previous.x), - y: current.y - fa * (next.y - previous.y) - }, - next: { - x: current.x + fb * (next.x - previous.x), - y: current.y + fb * (next.y - previous.y) - } - }; - }; - helpers.nextItem = function(collection, index, loop) { - if (loop) { - return index >= collection.length - 1 ? collection[0] : collection[index + 1]; - } - - return index >= collection.length - 1 ? collection[collection.length - 1] : collection[index + 1]; - }; - helpers.previousItem = function(collection, index, loop) { - if (loop) { - return index <= 0 ? collection[collection.length - 1] : collection[index - 1]; - } - return index <= 0 ? collection[0] : collection[index - 1]; - }; - // Implementation of the nice number algorithm used in determining where axis labels will go - helpers.niceNum = function(range, round) { - var exponent = Math.floor(helpers.log10(range)); - var fraction = range / Math.pow(10, exponent); - var niceFraction; - - if (round) { - if (fraction < 1.5) { - niceFraction = 1; - } else if (fraction < 3) { - niceFraction = 2; - } else if (fraction < 7) { - niceFraction = 5; - } else { - niceFraction = 10; - } - } else { - if (fraction <= 1.0) { - niceFraction = 1; - } else if (fraction <= 2) { - niceFraction = 2; - } else if (fraction <= 5) { - niceFraction = 5; - } else { - niceFraction = 10; - } - } - - return niceFraction * Math.pow(10, exponent); - }; - //Easing functions adapted from Robert Penner's easing equations - //http://www.robertpenner.com/easing/ - var easingEffects = helpers.easingEffects = { - linear: function(t) { - return t; - }, - easeInQuad: function(t) { - return t * t; - }, - easeOutQuad: function(t) { - return -1 * t * (t - 2); - }, - easeInOutQuad: function(t) { - if ((t /= 1 / 2) < 1) { - return 1 / 2 * t * t; - } - return -1 / 2 * ((--t) * (t - 2) - 1); - }, - easeInCubic: function(t) { - return t * t * t; - }, - easeOutCubic: function(t) { - return 1 * ((t = t / 1 - 1) * t * t + 1); - }, - easeInOutCubic: function(t) { - if ((t /= 1 / 2) < 1) { - return 1 / 2 * t * t * t; - } - return 1 / 2 * ((t -= 2) * t * t + 2); - }, - easeInQuart: function(t) { - return t * t * t * t; - }, - easeOutQuart: function(t) { - return -1 * ((t = t / 1 - 1) * t * t * t - 1); - }, - easeInOutQuart: function(t) { - if ((t /= 1 / 2) < 1) { - return 1 / 2 * t * t * t * t; - } - return -1 / 2 * ((t -= 2) * t * t * t - 2); - }, - easeInQuint: function(t) { - return 1 * (t /= 1) * t * t * t * t; - }, - easeOutQuint: function(t) { - return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); - }, - easeInOutQuint: function(t) { - if ((t /= 1 / 2) < 1) { - return 1 / 2 * t * t * t * t * t; - } - return 1 / 2 * ((t -= 2) * t * t * t * t + 2); - }, - easeInSine: function(t) { - return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; - }, - easeOutSine: function(t) { - return 1 * Math.sin(t / 1 * (Math.PI / 2)); - }, - easeInOutSine: function(t) { - return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); - }, - easeInExpo: function(t) { - return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); - }, - easeOutExpo: function(t) { - return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); - }, - easeInOutExpo: function(t) { - if (t === 0) { - return 0; - } - if (t === 1) { - return 1; - } - if ((t /= 1 / 2) < 1) { - return 1 / 2 * Math.pow(2, 10 * (t - 1)); - } - return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); - }, - easeInCirc: function(t) { - if (t >= 1) { - return t; - } - return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); - }, - easeOutCirc: function(t) { - return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); - }, - easeInOutCirc: function(t) { - if ((t /= 1 / 2) < 1) { - return -1 / 2 * (Math.sqrt(1 - t * t) - 1); - } - return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); - }, - easeInElastic: function(t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) { - return 0; - } - if ((t /= 1) === 1) { - return 1; - } - if (!p) { - p = 1 * 0.3; - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else { - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); - }, - easeOutElastic: function(t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) { - return 0; - } - if ((t /= 1) === 1) { - return 1; - } - if (!p) { - p = 1 * 0.3; - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else { - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; - }, - easeInOutElastic: function(t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) { - return 0; - } - if ((t /= 1 / 2) === 2) { - return 1; - } - if (!p) { - p = 1 * (0.3 * 1.5); - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else { - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - if (t < 1) { - return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); - } - return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; - }, - easeInBack: function(t) { - var s = 1.70158; - return 1 * (t /= 1) * t * ((s + 1) * t - s); - }, - easeOutBack: function(t) { - var s = 1.70158; - return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); - }, - easeInOutBack: function(t) { - var s = 1.70158; - if ((t /= 1 / 2) < 1) { - return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); - } - return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); - }, - easeInBounce: function(t) { - return 1 - easingEffects.easeOutBounce(1 - t); - }, - easeOutBounce: function(t) { - if ((t /= 1) < (1 / 2.75)) { - return 1 * (7.5625 * t * t); - } else if (t < (2 / 2.75)) { - return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); - } else if (t < (2.5 / 2.75)) { - return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); - } else { - return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); - } - }, - easeInOutBounce: function(t) { - if (t < 1 / 2) { - return easingEffects.easeInBounce(t * 2) * 0.5; - } - return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; - } - }; - //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ - helpers.requestAnimFrame = (function() { - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - return window.setTimeout(callback, 1000 / 60); - }; - })(); - helpers.cancelAnimFrame = (function() { - return window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.mozCancelAnimationFrame || - window.oCancelAnimationFrame || - window.msCancelAnimationFrame || - function(callback) { - return window.clearTimeout(callback, 1000 / 60); - }; - })(); - //-- DOM methods - helpers.getRelativePosition = function(evt, chart) { - var mouseX, mouseY; - var e = evt.originalEvent || evt, - canvas = evt.currentTarget || evt.srcElement, - boundingRect = canvas.getBoundingClientRect(); - - if (e.touches && e.touches.length > 0) { - mouseX = e.touches[0].clientX; - mouseY = e.touches[0].clientY; - - } else { - mouseX = e.clientX; - mouseY = e.clientY; - } - - // Scale mouse coordinates into canvas coordinates - // by following the pattern laid out by 'jerryj' in the comments of - // http://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/ - var paddingLeft = parseFloat(helpers.getStyle(canvas, 'padding-left')); - var paddingTop = parseFloat(helpers.getStyle(canvas, 'padding-top')); - var paddingRight = parseFloat(helpers.getStyle(canvas, 'padding-right')); - var paddingBottom = parseFloat(helpers.getStyle(canvas, 'padding-bottom')); - var width = boundingRect.right - boundingRect.left - paddingLeft - paddingRight; - var height = boundingRect.bottom - boundingRect.top - paddingTop - paddingBottom; - - // We divide by the current device pixel ratio, because the canvas is scaled up by that amount in each direction. However - // the backend model is in unscaled coordinates. Since we are going to deal with our model coordinates, we go back here - mouseX = Math.round((mouseX - boundingRect.left - paddingLeft) / (width) * canvas.width / chart.currentDevicePixelRatio); - mouseY = Math.round((mouseY - boundingRect.top - paddingTop) / (height) * canvas.height / chart.currentDevicePixelRatio); - - return { - x: mouseX, - y: mouseY - }; - - }; - helpers.addEvent = function(node, eventType, method) { - if (node.addEventListener) { - node.addEventListener(eventType, method); - } else if (node.attachEvent) { - node.attachEvent("on" + eventType, method); - } else { - node["on" + eventType] = method; - } - }; - helpers.removeEvent = function(node, eventType, handler) { - if (node.removeEventListener) { - node.removeEventListener(eventType, handler, false); - } else if (node.detachEvent) { - node.detachEvent("on" + eventType, handler); - } else { - node["on" + eventType] = helpers.noop; - } - }; - helpers.bindEvents = function(chartInstance, arrayOfEvents, handler) { - // Create the events object if it's not already present - if (!chartInstance.events) - chartInstance.events = {}; - - helpers.each(arrayOfEvents, function(eventName) { - chartInstance.events[eventName] = function() { - handler.apply(chartInstance, arguments); - }; - helpers.addEvent(chartInstance.chart.canvas, eventName, chartInstance.events[eventName]); - }); - }; - helpers.unbindEvents = function(chartInstance, arrayOfEvents) { - helpers.each(arrayOfEvents, function(handler, eventName) { - helpers.removeEvent(chartInstance.chart.canvas, eventName, handler); - }); - }; - - // Private helper function to convert max-width/max-height values that may be percentages into a number - function parseMaxStyle(styleValue, node, parentProperty) { - var valueInPixels; - if (typeof(styleValue) === 'string') { - valueInPixels = parseInt(styleValue, 10); - - if (styleValue.indexOf('%') != -1) { - // percentage * size in dimension - valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty]; - } - } else { - valueInPixels = styleValue; - } - - return valueInPixels; - } - - // Private helper to get a constraint dimension - // @param domNode : the node to check the constraint on - // @param maxStyle : the style that defines the maximum for the direction we are using (max-width / max-height) - // @param percentageProperty : property of parent to use when calculating width as a percentage - function getConstraintDimension(domNode, maxStyle, percentageProperty) { - var constrainedDimension; - var constrainedNode = document.defaultView.getComputedStyle(domNode)[maxStyle]; - var constrainedContainer = document.defaultView.getComputedStyle(domNode.parentNode)[maxStyle]; - var hasCNode = constrainedNode !== null && constrainedNode !== "none"; - var hasCContainer = constrainedContainer !== null && constrainedContainer !== "none"; - - if (hasCNode || hasCContainer) { - constrainedDimension = Math.min((hasCNode ? parseMaxStyle(constrainedNode, domNode, percentageProperty) : Number.POSITIVE_INFINITY), (hasCContainer ? parseMaxStyle(constrainedContainer, domNode.parentNode, percentageProperty) : Number.POSITIVE_INFINITY)); - } - return constrainedDimension; - } - // returns Number or undefined if no constraint - helpers.getConstraintWidth = function(domNode) { - return getConstraintDimension(domNode, 'max-width', 'clientWidth'); - }; - // returns Number or undefined if no constraint - helpers.getConstraintHeight = function(domNode) { - return getConstraintDimension(domNode, 'max-height', 'clientHeight'); - }; - helpers.getMaximumWidth = function(domNode) { - var container = domNode.parentNode; - var padding = parseInt(helpers.getStyle(container, 'padding-left')) + parseInt(helpers.getStyle(container, 'padding-right')); - - var w = container.clientWidth - padding; - var cw = helpers.getConstraintWidth(domNode); - if (cw !== undefined) { - w = Math.min(w, cw); - } - - return w; - }; - helpers.getMaximumHeight = function(domNode) { - var container = domNode.parentNode; - var padding = parseInt(helpers.getStyle(container, 'padding-top')) + parseInt(helpers.getStyle(container, 'padding-bottom')); - - var h = container.clientHeight - padding; - var ch = helpers.getConstraintHeight(domNode); - if (ch !== undefined) { - h = Math.min(h, ch); - } - - return h; - }; - helpers.getStyle = function(el, property) { - return el.currentStyle ? - el.currentStyle[property] : - document.defaultView.getComputedStyle(el, null).getPropertyValue(property); - }; - helpers.retinaScale = function(chart) { - var ctx = chart.ctx; - var width = chart.canvas.width; - var height = chart.canvas.height; - var pixelRatio = chart.currentDevicePixelRatio = window.devicePixelRatio || 1; - - if (pixelRatio !== 1) { - ctx.canvas.height = height * pixelRatio; - ctx.canvas.width = width * pixelRatio; - ctx.scale(pixelRatio, pixelRatio); - - // Store the device pixel ratio so that we can go backwards in `destroy`. - // The devicePixelRatio changes with zoom, so there are no guarantees that it is the same - // when destroy is called - chart.originalDevicePixelRatio = chart.originalDevicePixelRatio || pixelRatio; - } - - ctx.canvas.style.width = width + 'px'; - ctx.canvas.style.height = height + 'px'; - }; - //-- Canvas methods - helpers.clear = function(chart) { - chart.ctx.clearRect(0, 0, chart.width, chart.height); - }; - helpers.fontString = function(pixelSize, fontStyle, fontFamily) { - return fontStyle + " " + pixelSize + "px " + fontFamily; - }; - helpers.longestText = function(ctx, font, arrayOfStrings, cache) { - cache = cache || {}; - cache.data = cache.data || {}; - cache.garbageCollect = cache.garbageCollect || []; - - if (cache.font !== font) { - cache.data = {}; - cache.garbageCollect = []; - cache.font = font; - } - - ctx.font = font; - var longest = 0; - helpers.each(arrayOfStrings, function(string) { - // Undefined strings should not be measured - if (string !== undefined && string !== null) { - var textWidth = cache.data[string]; - if (!textWidth) { - textWidth = cache.data[string] = ctx.measureText(string).width; - cache.garbageCollect.push(string); - } - - if (textWidth > longest) { - longest = textWidth; - } - } - }); - - var gcLen = cache.garbageCollect.length / 2; - if (gcLen > arrayOfStrings.length) { - for (var i = 0; i < gcLen; i++) { - delete cache.data[cache.garbageCollect[i]]; - } - cache.garbageCollect.splice(0, gcLen); - } - - return longest; - }; - helpers.drawRoundedRectangle = function(ctx, x, y, width, height, radius) { - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); - }; - helpers.color = function(c) { - if (!color) { - console.log('Color.js not found!'); - return c; - } - - /* global CanvasGradient */ - if (c instanceof CanvasGradient) { - return color(Chart.defaults.global.defaultColor); - } - - return color(c); - }; - helpers.addResizeListener = function(node, callback) { - // Hide an iframe before the node - var hiddenIframe = document.createElement('iframe'); - var hiddenIframeClass = 'chartjs-hidden-iframe'; - - if (hiddenIframe.classlist) { - // can use classlist - hiddenIframe.classlist.add(hiddenIframeClass); - } else { - hiddenIframe.setAttribute('class', hiddenIframeClass); - } - - // Set the style - hiddenIframe.style.width = '100%'; - hiddenIframe.style.display = 'block'; - hiddenIframe.style.border = 0; - hiddenIframe.style.height = 0; - hiddenIframe.style.margin = 0; - hiddenIframe.style.position = 'absolute'; - hiddenIframe.style.left = 0; - hiddenIframe.style.right = 0; - hiddenIframe.style.top = 0; - hiddenIframe.style.bottom = 0; - - // Insert the iframe so that contentWindow is available - node.insertBefore(hiddenIframe, node.firstChild); - - (hiddenIframe.contentWindow || hiddenIframe).onresize = function() { - if (callback) { - callback(); - } - }; - }; - helpers.removeResizeListener = function(node) { - var hiddenIframe = node.querySelector('.chartjs-hidden-iframe'); - - // Remove the resize detect iframe - if (hiddenIframe) { - hiddenIframe.parentNode.removeChild(hiddenIframe); - } - }; - helpers.isArray = function(obj) { - if (!Array.isArray) { - return Object.prototype.toString.call(obj) === '[object Array]'; - } - return Array.isArray(obj); - }; - helpers.pushAllIfDefined = function(element, array) { - if (typeof element === "undefined") { - return; - } - - if (helpers.isArray(element)) { - array.push.apply(array, element); - } else { - array.push(element); - } - }; - helpers.isDatasetVisible = function(dataset) { - return !dataset.hidden; - }; - helpers.callCallback = function(fn, args, _tArg) { - if (fn && typeof fn.call === 'function') { - fn.apply(_tArg, args); - } - }; - -}; - -},{"chartjs-color":6}],26:[function(require,module,exports){ -"use strict"; - -module.exports = function() { - - //Occupy the global variable of Chart, and create a simple base class - var Chart = function(context, config) { - this.config = config; - - // Support a jQuery'd canvas element - if (context.length && context[0].getContext) { - context = context[0]; - } - - // Support a canvas domnode - if (context.getContext) { - context = context.getContext("2d"); - } - - this.ctx = context; - this.canvas = context.canvas; - - // Figure out what the size of the chart will be. - // If the canvas has a specified width and height, we use those else - // we look to see if the canvas node has a CSS width and height. - // If there is still no height, fill the parent container - this.width = context.canvas.width || parseInt(Chart.helpers.getStyle(context.canvas, 'width')) || Chart.helpers.getMaximumWidth(context.canvas); - this.height = context.canvas.height || parseInt(Chart.helpers.getStyle(context.canvas, 'height')) || Chart.helpers.getMaximumHeight(context.canvas); - - this.aspectRatio = this.width / this.height; - - if (isNaN(this.aspectRatio) || isFinite(this.aspectRatio) === false) { - // If the canvas has no size, try and figure out what the aspect ratio will be. - // Some charts prefer square canvases (pie, radar, etc). If that is specified, use that - // else use the canvas default ratio of 2 - this.aspectRatio = config.aspectRatio !== undefined ? config.aspectRatio : 2; - } - - // Store the original style of the element so we can set it back - this.originalCanvasStyleWidth = context.canvas.style.width; - this.originalCanvasStyleHeight = context.canvas.style.height; - - // High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - Chart.helpers.retinaScale(this); - - if (config) { - this.controller = new Chart.Controller(this); - } - - // Always bind this so that if the responsive state changes we still work - var _this = this; - Chart.helpers.addResizeListener(context.canvas.parentNode, function() { - if (_this.controller && _this.controller.config.options.responsive) { - _this.controller.resize(); - } - }); - - return this.controller ? this.controller : this; - - }; - - //Globally expose the defaults to allow for user updating/changing - Chart.defaults = { - global: { - responsive: true, - responsiveAnimationDuration: 0, - maintainAspectRatio: true, - events: ["mousemove", "mouseout", "click", "touchstart", "touchmove"], - hover: { - onHover: null, - mode: 'single', - animationDuration: 400 - }, - onClick: null, - defaultColor: 'rgba(0,0,0,0.1)', - defaultFontColor: '#666', - defaultFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - defaultFontSize: 12, - defaultFontStyle: 'normal', - showLines: true, - - // Element defaults defined in element extensions - elements: {}, - - // Legend callback string - legendCallback: function(chart) { - var text = []; - text.push('
      '); - for (var i = 0; i < chart.data.datasets.length; i++) { - text.push('
    • '); - if (chart.data.datasets[i].label) { - text.push(chart.data.datasets[i].label); - } - text.push('
    • '); - } - text.push('
    '); - - return text.join(""); - } - } - }; - - return Chart; - -}; - -},{}],27:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - // The layout service is very self explanatory. It's responsible for the layout within a chart. - // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need - // It is this service's responsibility of carrying out that layout. - Chart.layoutService = { - defaults: {}, - - // Register a box to a chartInstance. A box is simply a reference to an object that requires layout. eg. Scales, Legend, Plugins. - addBox: function(chartInstance, box) { - if (!chartInstance.boxes) { - chartInstance.boxes = []; - } - chartInstance.boxes.push(box); - }, - - removeBox: function(chartInstance, box) { - if (!chartInstance.boxes) { - return; - } - chartInstance.boxes.splice(chartInstance.boxes.indexOf(box), 1); - }, - - // The most important function - update: function(chartInstance, width, height) { - - if (!chartInstance) { - return; - } - - var xPadding = 0; - var yPadding = 0; - - var leftBoxes = helpers.where(chartInstance.boxes, function(box) { - return box.options.position === "left"; - }); - var rightBoxes = helpers.where(chartInstance.boxes, function(box) { - return box.options.position === "right"; - }); - var topBoxes = helpers.where(chartInstance.boxes, function(box) { - return box.options.position === "top"; - }); - var bottomBoxes = helpers.where(chartInstance.boxes, function(box) { - return box.options.position === "bottom"; - }); - - // Boxes that overlay the chartarea such as the radialLinear scale - var chartAreaBoxes = helpers.where(chartInstance.boxes, function(box) { - return box.options.position === "chartArea"; - }); - - function fullWidthSorter(a, b) { - - } - - // Ensure that full width boxes are at the very top / bottom - topBoxes.sort(function(a, b) { - return (b.options.fullWidth ? 1 : 0) - (a.options.fullWidth ? 1 : 0); - }); - bottomBoxes.sort(function(a, b) { - return (a.options.fullWidth ? 1 : 0) - (b.options.fullWidth ? 1 : 0); - }); - - // Essentially we now have any number of boxes on each of the 4 sides. - // Our canvas looks like the following. - // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and - // B1 is the bottom axis - // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays - // These locations are single-box locations only, when trying to register a chartArea location that is already taken, - // an error will be thrown. - // - // |----------------------------------------------------| - // | T1 (Full Width) | - // |----------------------------------------------------| - // | | | T2 | | - // | |----|-------------------------------------|----| - // | | | C1 | | C2 | | - // | | |----| |----| | - // | | | | | - // | L1 | L2 | ChartArea (C0) | R1 | - // | | | | | - // | | |----| |----| | - // | | | C3 | | C4 | | - // | |----|-------------------------------------|----| - // | | | B1 | | - // |----------------------------------------------------| - // | B2 (Full Width) | - // |----------------------------------------------------| - // - // What we do to find the best sizing, we do the following - // 1. Determine the minimum size of the chart area. - // 2. Split the remaining width equally between each vertical axis - // 3. Split the remaining height equally between each horizontal axis - // 4. Give each layout the maximum size it can be. The layout will return it's minimum size - // 5. Adjust the sizes of each axis based on it's minimum reported size. - // 6. Refit each axis - // 7. Position each axis in the final location - // 8. Tell the chart the final location of the chart area - // 9. Tell any axes that overlay the chart area the positions of the chart area - - // Step 1 - var chartWidth = width - (2 * xPadding); - var chartHeight = height - (2 * yPadding); - var chartAreaWidth = chartWidth / 2; // min 50% - var chartAreaHeight = chartHeight / 2; // min 50% - - // Step 2 - var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); - - // Step 3 - var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); - - // Step 4 - var maxChartAreaWidth = chartWidth; - var maxChartAreaHeight = chartHeight; - var minBoxSizes = []; - - helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); - - function getMinimumBoxSize(box) { - var minSize; - var isHorizontal = box.isHorizontal(); - - if (isHorizontal) { - minSize = box.update(box.options.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); - maxChartAreaHeight -= minSize.height; - } else { - minSize = box.update(verticalBoxWidth, chartAreaHeight); - maxChartAreaWidth -= minSize.width; - } - - minBoxSizes.push({ - horizontal: isHorizontal, - minSize: minSize, - box: box - }); - } - - // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could - // be if the axes are drawn at their minimum sizes. - - // Steps 5 & 6 - var totalLeftBoxesWidth = xPadding; - var totalRightBoxesWidth = xPadding; - var totalTopBoxesHeight = yPadding; - var totalBottomBoxesHeight = yPadding; - - // Update, and calculate the left and right margins for the horizontal boxes - helpers.each(leftBoxes.concat(rightBoxes), fitBox); - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - // Set the Left and Right margins for the horizontal boxes - helpers.each(topBoxes.concat(bottomBoxes), fitBox); - - // Function to fit a box - function fitBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBoxSize) { - return minBoxSize.box === box; - }); - - if (minBoxSize) { - if (box.isHorizontal()) { - var scaleMargin = { - left: totalLeftBoxesWidth, - right: totalRightBoxesWidth, - top: 0, - bottom: 0 - }; - - // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends - // on the margin. Sometimes they need to increase in size slightly - box.update(box.options.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); - } else { - box.update(minBoxSize.minSize.width, maxChartAreaHeight); - } - } - } - - // Figure out how much margin is on the top and bottom of the vertical boxes - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - // Let the left layout know the final margin - helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox); - - function finalFitVerticalBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBoxSize) { - return minBoxSize.box === box; - }); - - var scaleMargin = { - left: 0, - right: 0, - top: totalTopBoxesHeight, - bottom: totalBottomBoxesHeight - }; - - if (minBoxSize) { - box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); - } - } - - // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) - totalLeftBoxesWidth = xPadding; - totalRightBoxesWidth = xPadding; - totalTopBoxesHeight = yPadding; - totalBottomBoxesHeight = yPadding; - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - // Figure out if our chart area changed. This would occur if the dataset layout label rotation - // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do - // without calling `fit` again - var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; - var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; - - if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { - helpers.each(leftBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(rightBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(topBoxes, function(box) { - box.width = newMaxChartAreaWidth; - }); - - helpers.each(bottomBoxes, function(box) { - box.width = newMaxChartAreaWidth; - }); - - maxChartAreaHeight = newMaxChartAreaHeight; - maxChartAreaWidth = newMaxChartAreaWidth; - } - - // Step 7 - Position the boxes - var left = xPadding; - var top = yPadding; - var right = 0; - var bottom = 0; - - helpers.each(leftBoxes.concat(topBoxes), placeBox); - - // Account for chart width and height - left += maxChartAreaWidth; - top += maxChartAreaHeight; - - helpers.each(rightBoxes, placeBox); - helpers.each(bottomBoxes, placeBox); - - function placeBox(box) { - if (box.isHorizontal()) { - box.left = box.options.fullWidth ? xPadding : totalLeftBoxesWidth; - box.right = box.options.fullWidth ? width - xPadding : totalLeftBoxesWidth + maxChartAreaWidth; - box.top = top; - box.bottom = top + box.height; - - // Move to next point - top = box.bottom; - - } else { - - box.left = left; - box.right = left + box.width; - box.top = totalTopBoxesHeight; - box.bottom = totalTopBoxesHeight + maxChartAreaHeight; - - // Move to next point - left = box.right; - } - } - - // Step 8 - chartInstance.chartArea = { - left: totalLeftBoxesWidth, - top: totalTopBoxesHeight, - right: totalLeftBoxesWidth + maxChartAreaWidth, - bottom: totalTopBoxesHeight + maxChartAreaHeight - }; - - // Step 9 - helpers.each(chartAreaBoxes, function(box) { - box.left = chartInstance.chartArea.left; - box.top = chartInstance.chartArea.top; - box.right = chartInstance.chartArea.right; - box.bottom = chartInstance.chartArea.bottom; - - box.update(maxChartAreaWidth, maxChartAreaHeight); - }); - } - }; -}; - -},{}],28:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.global.legend = { - - display: true, - position: 'top', - fullWidth: true, // marks that this box should take the full width of the canvas (pushing down other boxes) - reverse: false, - - // a callback that will handle - onClick: function(e, legendItem) { - var dataset = this.chart.data.datasets[legendItem.datasetIndex]; - dataset.hidden = !dataset.hidden; - - // We hid a dataset ... rerender the chart - this.chart.update(); - }, - - labels: { - boxWidth: 40, - padding: 10, - // Generates labels shown in the legend - // Valid properties to return: - // text : text to display - // fillStyle : fill of coloured box - // strokeStyle: stroke of coloured box - // hidden : if this legend item refers to a hidden item - // lineCap : cap style for line - // lineDash - // lineDashOffset : - // lineJoin : - // lineWidth : - generateLabels: function(data) { - return helpers.isArray(data.datasets) ? data.datasets.map(function(dataset, i) { - return { - text: dataset.label, - fillStyle: dataset.backgroundColor, - hidden: dataset.hidden, - lineCap: dataset.borderCapStyle, - lineDash: dataset.borderDash, - lineDashOffset: dataset.borderDashOffset, - lineJoin: dataset.borderJoinStyle, - lineWidth: dataset.borderWidth, - strokeStyle: dataset.borderColor, - - // Below is extra data used for toggling the datasets - datasetIndex: i - }; - }, this) : []; - } - } - }; - - Chart.Legend = Chart.Element.extend({ - - initialize: function(config) { - helpers.extend(this, config); - - // Contains hit boxes for each dataset (in dataset order) - this.legendHitBoxes = []; - - // Are we in doughnut mode which has a different data type - this.doughnutMode = false; - }, - - // These methods are ordered by lifecyle. Utilities then follow. - // Any function defined here is inherited by all legend types. - // Any function can be extended by the legend type - - beforeUpdate: helpers.noop, - update: function(maxWidth, maxHeight, margins) { - - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - this.beforeUpdate(); - - // Absorb the master measurements - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - this.margins = margins; - - // Dimensions - this.beforeSetDimensions(); - this.setDimensions(); - this.afterSetDimensions(); - // Labels - this.beforeBuildLabels(); - this.buildLabels(); - this.afterBuildLabels(); - - // Fit - this.beforeFit(); - this.fit(); - this.afterFit(); - // - this.afterUpdate(); - - return this.minSize; - - }, - afterUpdate: helpers.noop, - - // - - beforeSetDimensions: helpers.noop, - setDimensions: function() { - // Set the unconstrained dimension before label rotation - if (this.isHorizontal()) { - // Reset position before calculating rotation - this.width = this.maxWidth; - this.left = 0; - this.right = this.width; - } else { - this.height = this.maxHeight; - - // Reset position before calculating rotation - this.top = 0; - this.bottom = this.height; - } - - // Reset padding - this.paddingLeft = 0; - this.paddingTop = 0; - this.paddingRight = 0; - this.paddingBottom = 0; - - // Reset minSize - this.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: helpers.noop, - - // - - beforeBuildLabels: helpers.noop, - buildLabels: function() { - this.legendItems = this.options.labels.generateLabels.call(this, this.chart.data); - if(this.options.reverse){ - this.legendItems.reverse(); - } - }, - afterBuildLabels: helpers.noop, - - // - - beforeFit: helpers.noop, - fit: function() { - - var ctx = this.ctx; - var fontSize = helpers.getValueOrDefault(this.options.labels.fontSize, Chart.defaults.global.defaultFontSize); - var fontStyle = helpers.getValueOrDefault(this.options.labels.fontStyle, Chart.defaults.global.defaultFontStyle); - var fontFamily = helpers.getValueOrDefault(this.options.labels.fontFamily, Chart.defaults.global.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); - - // Reset hit boxes - this.legendHitBoxes = []; - - // Width - if (this.isHorizontal()) { - this.minSize.width = this.maxWidth; // fill all the width - } else { - this.minSize.width = this.options.display ? 10 : 0; - } - - // height - if (this.isHorizontal()) { - this.minSize.height = this.options.display ? 10 : 0; - } else { - this.minSize.height = this.maxHeight; // fill all the height - } - - // Increase sizes here - if (this.options.display) { - if (this.isHorizontal()) { - // Labels - - // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one - this.lineWidths = [0]; - var totalHeight = this.legendItems.length ? fontSize + (this.options.labels.padding) : 0; - - ctx.textAlign = "left"; - ctx.textBaseline = 'top'; - ctx.font = labelFont; - - helpers.each(this.legendItems, function(legendItem, i) { - var width = this.options.labels.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - if (this.lineWidths[this.lineWidths.length - 1] + width + this.options.labels.padding >= this.width) { - totalHeight += fontSize + (this.options.labels.padding); - this.lineWidths[this.lineWidths.length] = this.left; - } - - // Store the hitbox width and height here. Final position will be updated in `draw` - this.legendHitBoxes[i] = { - left: 0, - top: 0, - width: width, - height: fontSize - }; - - this.lineWidths[this.lineWidths.length - 1] += width + this.options.labels.padding; - }, this); - - this.minSize.height += totalHeight; - - } else { - // TODO vertical - } - } - - this.width = this.minSize.width; - this.height = this.minSize.height; - - }, - afterFit: helpers.noop, - - // Shared Methods - isHorizontal: function() { - return this.options.position === "top" || this.options.position === "bottom"; - }, - - // Actualy draw the legend on the canvas - draw: function() { - if (this.options.display) { - var ctx = this.ctx; - var cursor = { - x: this.left + ((this.width - this.lineWidths[0]) / 2), - y: this.top + this.options.labels.padding, - line: 0 - }; - - var fontColor = helpers.getValueOrDefault(this.options.labels.fontColor, Chart.defaults.global.defaultFontColor); - var fontSize = helpers.getValueOrDefault(this.options.labels.fontSize, Chart.defaults.global.defaultFontSize); - var fontStyle = helpers.getValueOrDefault(this.options.labels.fontStyle, Chart.defaults.global.defaultFontStyle); - var fontFamily = helpers.getValueOrDefault(this.options.labels.fontFamily, Chart.defaults.global.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); - - // Horizontal - if (this.isHorizontal()) { - // Labels - ctx.textAlign = "left"; - ctx.textBaseline = 'top'; - ctx.lineWidth = 0.5; - ctx.strokeStyle = fontColor; // for strikethrough effect - ctx.fillStyle = fontColor; // render in correct colour - ctx.font = labelFont; - - helpers.each(this.legendItems, function(legendItem, i) { - var textWidth = ctx.measureText(legendItem.text).width; - var width = this.options.labels.boxWidth + (fontSize / 2) + textWidth; - - if (cursor.x + width >= this.width) { - cursor.y += fontSize + (this.options.labels.padding); - cursor.line++; - cursor.x = this.left + ((this.width - this.lineWidths[cursor.line]) / 2); - } - - // Set the ctx for the box - ctx.save(); - - var itemOrDefault = function(item, defaulVal) { - return item !== undefined ? item : defaulVal; - }; - - ctx.fillStyle = itemOrDefault(legendItem.fillStyle, Chart.defaults.global.defaultColor); - ctx.lineCap = itemOrDefault(legendItem.lineCap, Chart.defaults.global.elements.line.borderCapStyle); - ctx.lineDashOffset = itemOrDefault(legendItem.lineDashOffset, Chart.defaults.global.elements.line.borderDashOffset); - ctx.lineJoin = itemOrDefault(legendItem.lineJoin, Chart.defaults.global.elements.line.borderJoinStyle); - ctx.lineWidth = itemOrDefault(legendItem.lineWidth, Chart.defaults.global.elements.line.borderWidth); - ctx.strokeStyle = itemOrDefault(legendItem.strokeStyle, Chart.defaults.global.defaultColor); - - if (ctx.setLineDash) { - // IE 9 and 10 do not support line dash - ctx.setLineDash(itemOrDefault(legendItem.lineDash, Chart.defaults.global.elements.line.borderDash)); - } - - // Draw the box - ctx.strokeRect(cursor.x, cursor.y, this.options.labels.boxWidth, fontSize); - ctx.fillRect(cursor.x, cursor.y, this.options.labels.boxWidth, fontSize); - - ctx.restore(); - - this.legendHitBoxes[i].left = cursor.x; - this.legendHitBoxes[i].top = cursor.y; - - // Fill the actual label - ctx.fillText(legendItem.text, this.options.labels.boxWidth + (fontSize / 2) + cursor.x, cursor.y); - - if (legendItem.hidden) { - // Strikethrough the text if hidden - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.moveTo(this.options.labels.boxWidth + (fontSize / 2) + cursor.x, cursor.y + (fontSize / 2)); - ctx.lineTo(this.options.labels.boxWidth + (fontSize / 2) + cursor.x + textWidth, cursor.y + (fontSize / 2)); - ctx.stroke(); - } - - cursor.x += width + (this.options.labels.padding); - }, this); - } else { - - } - } - }, - - // Handle an event - handleEvent: function(e) { - var position = helpers.getRelativePosition(e, this.chart.chart); - - if (position.x >= this.left && position.x <= this.right && position.y >= this.top && position.y <= this.bottom) { - // See if we are touching one of the dataset boxes - for (var i = 0; i < this.legendHitBoxes.length; ++i) { - var hitBox = this.legendHitBoxes[i]; - - if (position.x >= hitBox.left && position.x <= hitBox.left + hitBox.width && position.y >= hitBox.top && position.y <= hitBox.top + hitBox.height) { - // Touching an element - if (this.options.onClick) { - this.options.onClick.call(this, e, this.legendItems[i]); - } - break; - } - } - } - } - }); - -}; - -},{}],29:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.scale = { - display: true, - - // grid line settings - gridLines: { - display: true, - color: "rgba(0, 0, 0, 0.1)", - lineWidth: 1, - drawOnChartArea: true, - drawTicks: true, - zeroLineWidth: 1, - zeroLineColor: "rgba(0,0,0,0.25)", - offsetGridLines: false - }, - - // scale label - scaleLabel: { - // actual label - labelString: '', - - // display property - display: false - }, - - // label settings - ticks: { - beginAtZero: false, - maxRotation: 50, - mirror: false, - padding: 10, - reverse: false, - display: true, - autoSkip: true, - autoSkipPadding: 0, - callback: function(value) { - return '' + value; - } - } - }; - - Chart.Scale = Chart.Element.extend({ - - // These methods are ordered by lifecyle. Utilities then follow. - // Any function defined here is inherited by all scale types. - // Any function can be extended by the scale type - - beforeUpdate: function() { - helpers.callCallback(this.options.beforeUpdate, [this]); - }, - update: function(maxWidth, maxHeight, margins) { - - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - this.beforeUpdate(); - - // Absorb the master measurements - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - this.margins = helpers.extend({ - left: 0, - right: 0, - top: 0, - bottom: 0 - }, margins); - - // Dimensions - this.beforeSetDimensions(); - this.setDimensions(); - this.afterSetDimensions(); - - // Data min/max - this.beforeDataLimits(); - this.determineDataLimits(); - this.afterDataLimits(); - - // Ticks - this.beforeBuildTicks(); - this.buildTicks(); - this.afterBuildTicks(); - - this.beforeTickToLabelConversion(); - this.convertTicksToLabels(); - this.afterTickToLabelConversion(); - - // Tick Rotation - this.beforeCalculateTickRotation(); - this.calculateTickRotation(); - this.afterCalculateTickRotation(); - // Fit - this.beforeFit(); - this.fit(); - this.afterFit(); - // - this.afterUpdate(); - - return this.minSize; - - }, - afterUpdate: function() { - helpers.callCallback(this.options.afterUpdate, [this]); - }, - - // - - beforeSetDimensions: function() { - helpers.callCallback(this.options.beforeSetDimensions, [this]); - }, - setDimensions: function() { - // Set the unconstrained dimension before label rotation - if (this.isHorizontal()) { - // Reset position before calculating rotation - this.width = this.maxWidth; - this.left = 0; - this.right = this.width; - } else { - this.height = this.maxHeight; - - // Reset position before calculating rotation - this.top = 0; - this.bottom = this.height; - } - - // Reset padding - this.paddingLeft = 0; - this.paddingTop = 0; - this.paddingRight = 0; - this.paddingBottom = 0; - }, - afterSetDimensions: function() { - helpers.callCallback(this.options.afterSetDimensions, [this]); - }, - - // Data limits - beforeDataLimits: function() { - helpers.callCallback(this.options.beforeDataLimits, [this]); - }, - determineDataLimits: helpers.noop, - afterDataLimits: function() { - helpers.callCallback(this.options.afterDataLimits, [this]); - }, - - // - beforeBuildTicks: function() { - helpers.callCallback(this.options.beforeBuildTicks, [this]); - }, - buildTicks: helpers.noop, - afterBuildTicks: function() { - helpers.callCallback(this.options.afterBuildTicks, [this]); - }, - - beforeTickToLabelConversion: function() { - helpers.callCallback(this.options.beforeTickToLabelConversion, [this]); - }, - convertTicksToLabels: function() { - // Convert ticks to strings - this.ticks = this.ticks.map(function(numericalTick, index, ticks) { - if (this.options.ticks.userCallback) { - return this.options.ticks.userCallback(numericalTick, index, ticks); - } - return this.options.ticks.callback(numericalTick, index, ticks); - }, - this); - }, - afterTickToLabelConversion: function() { - helpers.callCallback(this.options.afterTickToLabelConversion, [this]); - }, - - // - - beforeCalculateTickRotation: function() { - helpers.callCallback(this.options.beforeCalculateTickRotation, [this]); - }, - calculateTickRotation: function() { - //Get the width of each grid by calculating the difference - //between x offsets between 0 and 1. - var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(this.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(this.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - this.ctx.font = tickLabelFont; - - var firstWidth = this.ctx.measureText(this.ticks[0]).width; - var lastWidth = this.ctx.measureText(this.ticks[this.ticks.length - 1]).width; - var firstRotated; - - this.labelRotation = 0; - this.paddingRight = 0; - this.paddingLeft = 0; - - if (this.options.display) { - if (this.isHorizontal()) { - this.paddingRight = lastWidth / 2 + 3; - this.paddingLeft = firstWidth / 2 + 3; - - if (!this.longestTextCache) { - this.longestTextCache = {}; - } - var originalLabelWidth = helpers.longestText(this.ctx, tickLabelFont, this.ticks, this.longestTextCache); - var labelWidth = originalLabelWidth; - var cosRotation; - var sinRotation; - - // Allow 3 pixels x2 padding either side for label readability - // only the index matters for a dataset scale, but we want a consistent interface between scales - var tickWidth = this.getPixelForTick(1) - this.getPixelForTick(0) - 6; - - //Max label rotation can be set or default to 90 - also act as a loop counter - while (labelWidth > tickWidth && this.labelRotation < this.options.ticks.maxRotation) { - cosRotation = Math.cos(helpers.toRadians(this.labelRotation)); - sinRotation = Math.sin(helpers.toRadians(this.labelRotation)); - - firstRotated = cosRotation * firstWidth; - - // We're right aligning the text now. - if (firstRotated + tickFontSize / 2 > this.yLabelWidth) { - this.paddingLeft = firstRotated + tickFontSize / 2; - } - - this.paddingRight = tickFontSize / 2; - - if (sinRotation * originalLabelWidth > this.maxHeight) { - // go back one step - this.labelRotation--; - break; - } - - this.labelRotation++; - labelWidth = cosRotation * originalLabelWidth; - } - } - } - - if (this.margins) { - this.paddingLeft = Math.max(this.paddingLeft - this.margins.left, 0); - this.paddingRight = Math.max(this.paddingRight - this.margins.right, 0); - } - }, - afterCalculateTickRotation: function() { - helpers.callCallback(this.options.afterCalculateTickRotation, [this]); - }, - - // - - beforeFit: function() { - helpers.callCallback(this.options.beforeFit, [this]); - }, - fit: function() { - - this.minSize = { - width: 0, - height: 0 - }; - - var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(this.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(this.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - - var scaleLabelFontSize = helpers.getValueOrDefault(this.options.scaleLabel.fontSize, Chart.defaults.global.defaultFontSize); - var scaleLabelFontStyle = helpers.getValueOrDefault(this.options.scaleLabel.fontStyle, Chart.defaults.global.defaultFontStyle); - var scaleLabelFontFamily = helpers.getValueOrDefault(this.options.scaleLabel.fontFamily, Chart.defaults.global.defaultFontFamily); - var scaleLabelFont = helpers.fontString(scaleLabelFontSize, scaleLabelFontStyle, scaleLabelFontFamily); - - // Width - if (this.isHorizontal()) { - // subtract the margins to line up with the chartArea if we are a full width scale - this.minSize.width = this.isFullWidth() ? this.maxWidth - this.margins.left - this.margins.right : this.maxWidth; - } else { - this.minSize.width = this.options.gridLines.display && this.options.display ? 10 : 0; - } - - // height - if (this.isHorizontal()) { - this.minSize.height = this.options.gridLines.display && this.options.display ? 10 : 0; - } else { - this.minSize.height = this.maxHeight; // fill all the height - } - - // Are we showing a title for the scale? - if (this.options.scaleLabel.display) { - if (this.isHorizontal()) { - this.minSize.height += (scaleLabelFontSize * 1.5); - } else { - this.minSize.width += (scaleLabelFontSize * 1.5); - } - } - - if (this.options.ticks.display && this.options.display) { - // Don't bother fitting the ticks if we are not showing them - if (!this.longestTextCache) { - this.longestTextCache = {}; - } - - var largestTextWidth = helpers.longestText(this.ctx, tickLabelFont, this.ticks, this.longestTextCache); - - if (this.isHorizontal()) { - // A horizontal axis is more constrained by the height. - this.longestLabelWidth = largestTextWidth; - - // TODO - improve this calculation - var labelHeight = (Math.sin(helpers.toRadians(this.labelRotation)) * this.longestLabelWidth) + 1.5 * tickFontSize; - - this.minSize.height = Math.min(this.maxHeight, this.minSize.height + labelHeight); - this.ctx.font = tickLabelFont; - - var firstLabelWidth = this.ctx.measureText(this.ticks[0]).width; - var lastLabelWidth = this.ctx.measureText(this.ticks[this.ticks.length - 1]).width; - - // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned which means that the right padding is dominated - // by the font height - var cosRotation = Math.cos(helpers.toRadians(this.labelRotation)); - var sinRotation = Math.sin(helpers.toRadians(this.labelRotation)); - this.paddingLeft = this.labelRotation !== 0 ? (cosRotation * firstLabelWidth) + 3 : firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges - this.paddingRight = this.labelRotation !== 0 ? (sinRotation * (tickFontSize / 2)) + 3 : lastLabelWidth / 2 + 3; // when rotated - } else { - // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first - var maxLabelWidth = this.maxWidth - this.minSize.width; - - // Account for padding - if (!this.options.ticks.mirror) { - largestTextWidth += this.options.ticks.padding; - } - - if (largestTextWidth < maxLabelWidth) { - // We don't need all the room - this.minSize.width += largestTextWidth; - } else { - // Expand to max size - this.minSize.width = this.maxWidth; - } - - this.paddingTop = tickFontSize / 2; - this.paddingBottom = tickFontSize / 2; - } - } - - if (this.margins) { - this.paddingLeft = Math.max(this.paddingLeft - this.margins.left, 0); - this.paddingTop = Math.max(this.paddingTop - this.margins.top, 0); - this.paddingRight = Math.max(this.paddingRight - this.margins.right, 0); - this.paddingBottom = Math.max(this.paddingBottom - this.margins.bottom, 0); - } - - this.width = this.minSize.width; - this.height = this.minSize.height; - - }, - afterFit: function() { - helpers.callCallback(this.options.afterFit, [this]); - }, - - // Shared Methods - isHorizontal: function() { - return this.options.position === "top" || this.options.position === "bottom"; - }, - isFullWidth: function() { - return (this.options.fullWidth); - }, - - // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not - getRightValue: function getRightValue(rawValue) { - // Null and undefined values first - if (rawValue === null || typeof(rawValue) === 'undefined') { - return NaN; - } - // isNaN(object) returns true, so make sure NaN is checking for a number - if (typeof(rawValue) === 'number' && isNaN(rawValue)) { - return NaN; - } - // If it is in fact an object, dive in one more level - if (typeof(rawValue) === "object") { - if (rawValue instanceof Date) { - return rawValue; - } else { - return getRightValue(this.isHorizontal() ? rawValue.x : rawValue.y); - } - } - - // Value is good, return it - return rawValue; - }, - - // Used to get the value to display in the tooltip for the data at the given index - // function getLabelForIndex(index, datasetIndex) - getLabelForIndex: helpers.noop, - - // Used to get data value locations. Value can either be an index or a numerical value - getPixelForValue: helpers.noop, - - // Used for tick location, should - getPixelForTick: function(index, includeOffset) { - if (this.isHorizontal()) { - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - var tickWidth = innerWidth / Math.max((this.ticks.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); - var pixel = (tickWidth * index) + this.paddingLeft; - - if (includeOffset) { - pixel += tickWidth / 2; - } - - var finalVal = this.left + Math.round(pixel); - finalVal += this.isFullWidth() ? this.margins.left : 0; - return finalVal; - } else { - var innerHeight = this.height - (this.paddingTop + this.paddingBottom); - return this.top + (index * (innerHeight / (this.ticks.length - 1))); - } - }, - - // Utility for getting the pixel location of a percentage of scale - getPixelForDecimal: function(decimal /*, includeOffset*/ ) { - if (this.isHorizontal()) { - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - var valueOffset = (innerWidth * decimal) + this.paddingLeft; - - var finalVal = this.left + Math.round(valueOffset); - finalVal += this.isFullWidth() ? this.margins.left : 0; - return finalVal; - } else { - return this.top + (decimal * this.height); - } - }, - - // Actualy draw the scale on the canvas - // @param {rectangle} chartArea : the area of the chart to draw full grid lines on - draw: function(chartArea) { - if (this.options.display) { - - var setContextLineSettings; - var isRotated = this.labelRotation !== 0; - var skipRatio; - var scaleLabelX; - var scaleLabelY; - var useAutoskipper = this.options.ticks.autoSkip; - - - // figure out the maximum number of gridlines to show - var maxTicks; - - if (this.options.ticks.maxTicksLimit) { - maxTicks = this.options.ticks.maxTicksLimit; - } - - var tickFontColor = helpers.getValueOrDefault(this.options.ticks.fontColor, Chart.defaults.global.defaultFontColor); - var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(this.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(this.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - - var scaleLabelFontColor = helpers.getValueOrDefault(this.options.scaleLabel.fontColor, Chart.defaults.global.defaultFontColor); - var scaleLabelFontSize = helpers.getValueOrDefault(this.options.scaleLabel.fontSize, Chart.defaults.global.defaultFontSize); - var scaleLabelFontStyle = helpers.getValueOrDefault(this.options.scaleLabel.fontStyle, Chart.defaults.global.defaultFontStyle); - var scaleLabelFontFamily = helpers.getValueOrDefault(this.options.scaleLabel.fontFamily, Chart.defaults.global.defaultFontFamily); - var scaleLabelFont = helpers.fontString(scaleLabelFontSize, scaleLabelFontStyle, scaleLabelFontFamily); - - var cosRotation = Math.cos(helpers.toRadians(this.labelRotation)); - var sinRotation = Math.sin(helpers.toRadians(this.labelRotation)); - var longestRotatedLabel = this.longestLabelWidth * cosRotation; - var rotatedLabelHeight = tickFontSize * sinRotation; - - // Make sure we draw text in the correct color and font - this.ctx.fillStyle = tickFontColor; - - if (this.isHorizontal()) { - setContextLineSettings = true; - var yTickStart = this.options.position === "bottom" ? this.top : this.bottom - 10; - var yTickEnd = this.options.position === "bottom" ? this.top + 10 : this.bottom; - skipRatio = false; - - if (((longestRotatedLabel / 2) + this.options.ticks.autoSkipPadding) * this.ticks.length > (this.width - (this.paddingLeft + this.paddingRight))) { - skipRatio = 1 + Math.floor((((longestRotatedLabel / 2) + this.options.ticks.autoSkipPadding) * this.ticks.length) / (this.width - (this.paddingLeft + this.paddingRight))); - } - - // if they defined a max number of ticks, - // increase skipRatio until that number is met - if (maxTicks && this.ticks.length > maxTicks) { - while (!skipRatio || this.ticks.length / (skipRatio || 1) > maxTicks) { - if (!skipRatio) { - skipRatio = 1; - } - skipRatio += 1; - } - } - - if (!useAutoskipper) { - skipRatio = false; - } - - helpers.each(this.ticks, function(label, index) { - // Blank ticks - var isLastTick = this.ticks.length === index + 1; - - // Since we always show the last tick,we need may need to hide the last shown one before - var shouldSkip = (skipRatio > 1 && index % skipRatio > 0) || (index % skipRatio === 0 && index + skipRatio > this.ticks.length); - if (shouldSkip && !isLastTick || (label === undefined || label === null)) { - return; - } - var xLineValue = this.getPixelForTick(index); // xvalues for grid lines - var xLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines); // x values for ticks (need to consider offsetLabel option) - - if (this.options.gridLines.display) { - if (index === (typeof this.zeroLineIndex !== 'undefined' ? this.zeroLineIndex : 0)) { - // Draw the first index specially - this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; - this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; - setContextLineSettings = true; // reset next time - } else if (setContextLineSettings) { - this.ctx.lineWidth = this.options.gridLines.lineWidth; - this.ctx.strokeStyle = this.options.gridLines.color; - setContextLineSettings = false; - } - - xLineValue += helpers.aliasPixel(this.ctx.lineWidth); - - // Draw the label area - this.ctx.beginPath(); - - if (this.options.gridLines.drawTicks) { - this.ctx.moveTo(xLineValue, yTickStart); - this.ctx.lineTo(xLineValue, yTickEnd); - } - - // Draw the chart area - if (this.options.gridLines.drawOnChartArea) { - this.ctx.moveTo(xLineValue, chartArea.top); - this.ctx.lineTo(xLineValue, chartArea.bottom); - } - - // Need to stroke in the loop because we are potentially changing line widths & colours - this.ctx.stroke(); - } - - if (this.options.ticks.display) { - this.ctx.save(); - this.ctx.translate(xLabelValue, (isRotated) ? this.top + 12 : this.options.position === "top" ? this.bottom - 10 : this.top + 10); - this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); - this.ctx.font = tickLabelFont; - this.ctx.textAlign = (isRotated) ? "right" : "center"; - this.ctx.textBaseline = (isRotated) ? "middle" : this.options.position === "top" ? "bottom" : "top"; - this.ctx.fillText(label, 0, 0); - this.ctx.restore(); - } - }, this); - - if (this.options.scaleLabel.display) { - // Draw the scale label - this.ctx.textAlign = "center"; - this.ctx.textBaseline = 'middle'; - this.ctx.fillStyle = scaleLabelFontColor; // render in correct colour - this.ctx.font = scaleLabelFont; - - scaleLabelX = this.left + ((this.right - this.left) / 2); // midpoint of the width - scaleLabelY = this.options.position === 'bottom' ? this.bottom - (scaleLabelFontSize / 2) : this.top + (scaleLabelFontSize / 2); - - this.ctx.fillText(this.options.scaleLabel.labelString, scaleLabelX, scaleLabelY); - } - - } else { - setContextLineSettings = true; - var xTickStart = this.options.position === "right" ? this.left : this.right - 5; - var xTickEnd = this.options.position === "right" ? this.left + 5 : this.right; - - helpers.each(this.ticks, function(label, index) { - // If the callback returned a null or undefined value, do not draw this line - if (label === undefined || label === null) { - return; - } - - var yLineValue = this.getPixelForTick(index); // xvalues for grid lines - - if (this.options.gridLines.display) { - if (index === (typeof this.zeroLineIndex !== 'undefined' ? this.zeroLineIndex : 0)) { - // Draw the first index specially - this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; - this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; - setContextLineSettings = true; // reset next time - } else if (setContextLineSettings) { - this.ctx.lineWidth = this.options.gridLines.lineWidth; - this.ctx.strokeStyle = this.options.gridLines.color; - setContextLineSettings = false; - } - - yLineValue += helpers.aliasPixel(this.ctx.lineWidth); - - // Draw the label area - this.ctx.beginPath(); - - if (this.options.gridLines.drawTicks) { - this.ctx.moveTo(xTickStart, yLineValue); - this.ctx.lineTo(xTickEnd, yLineValue); - } - - // Draw the chart area - if (this.options.gridLines.drawOnChartArea) { - this.ctx.moveTo(chartArea.left, yLineValue); - this.ctx.lineTo(chartArea.right, yLineValue); - } - - // Need to stroke in the loop because we are potentially changing line widths & colours - this.ctx.stroke(); - } - - if (this.options.ticks.display) { - var xLabelValue; - var yLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines); // x values for ticks (need to consider offsetLabel option) - - this.ctx.save(); - - if (this.options.position === "left") { - if (this.options.ticks.mirror) { - xLabelValue = this.right + this.options.ticks.padding; - this.ctx.textAlign = "left"; - } else { - xLabelValue = this.right - this.options.ticks.padding; - this.ctx.textAlign = "right"; - } - } else { - // right side - if (this.options.ticks.mirror) { - xLabelValue = this.left - this.options.ticks.padding; - this.ctx.textAlign = "right"; - } else { - xLabelValue = this.left + this.options.ticks.padding; - this.ctx.textAlign = "left"; - } - } - - this.ctx.translate(xLabelValue, yLabelValue); - this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); - this.ctx.font = tickLabelFont; - this.ctx.textBaseline = "middle"; - this.ctx.fillText(label, 0, 0); - this.ctx.restore(); - } - }, this); - - if (this.options.scaleLabel.display) { - // Draw the scale label - scaleLabelX = this.options.position === 'left' ? this.left + (scaleLabelFontSize / 2) : this.right - (scaleLabelFontSize / 2); - scaleLabelY = this.top + ((this.bottom - this.top) / 2); - var rotation = this.options.position === 'left' ? -0.5 * Math.PI : 0.5 * Math.PI; - - this.ctx.save(); - this.ctx.translate(scaleLabelX, scaleLabelY); - this.ctx.rotate(rotation); - this.ctx.textAlign = "center"; - this.ctx.fillStyle =scaleLabelFontColor; // render in correct colour - this.ctx.font = scaleLabelFont; - this.ctx.textBaseline = 'middle'; - this.ctx.fillText(this.options.scaleLabel.labelString, 0, 0); - this.ctx.restore(); - } - } - - // Draw the line at the edge of the axis - this.ctx.lineWidth = this.options.gridLines.lineWidth; - this.ctx.strokeStyle = this.options.gridLines.color; - var x1 = this.left, - x2 = this.right, - y1 = this.top, - y2 = this.bottom; - - if (this.isHorizontal()) { - y1 = y2 = this.options.position === 'top' ? this.bottom : this.top; - y1 += helpers.aliasPixel(this.ctx.lineWidth); - y2 += helpers.aliasPixel(this.ctx.lineWidth); - } else { - x1 = x2 = this.options.position === 'left' ? this.right : this.left; - x1 += helpers.aliasPixel(this.ctx.lineWidth); - x2 += helpers.aliasPixel(this.ctx.lineWidth); - } - - this.ctx.beginPath(); - this.ctx.moveTo(x1, y1); - this.ctx.lineTo(x2, y2); - this.ctx.stroke(); - } - } - }); -}; - -},{}],30:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.scaleService = { - // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then - // use the new chart options to grab the correct scale - constructors: {}, - // Use a registration function so that we can move to an ES6 map when we no longer need to support - // old browsers - - // Scale config defaults - defaults: {}, - registerScaleType: function(type, scaleConstructor, defaults) { - this.constructors[type] = scaleConstructor; - this.defaults[type] = helpers.clone(defaults); - }, - getScaleConstructor: function(type) { - return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; - }, - getScaleDefaults: function(type) { - // Return the scale defaults merged with the global settings so that we always use the latest ones - return this.defaults.hasOwnProperty(type) ? helpers.scaleMerge(Chart.defaults.scale, this.defaults[type]) : {}; - }, - addScalesToLayout: function(chartInstance) { - // Adds each scale to the chart.boxes array to be sized accordingly - helpers.each(chartInstance.scales, function(scale) { - Chart.layoutService.addBox(chartInstance, scale); - }); - } - }; -}; -},{}],31:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.global.title = { - display: false, - position: 'top', - fullWidth: true, // marks that this box should take the full width of the canvas (pushing down other boxes) - - fontStyle: 'bold', - padding: 10, - - // actual title - text: '' - }; - - Chart.Title = Chart.Element.extend({ - - initialize: function(config) { - helpers.extend(this, config); - this.options = helpers.configMerge(Chart.defaults.global.title, config.options); - - // Contains hit boxes for each dataset (in dataset order) - this.legendHitBoxes = []; - }, - - // These methods are ordered by lifecyle. Utilities then follow. - - beforeUpdate: helpers.noop, - update: function(maxWidth, maxHeight, margins) { - - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - this.beforeUpdate(); - - // Absorb the master measurements - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - this.margins = margins; - - // Dimensions - this.beforeSetDimensions(); - this.setDimensions(); - this.afterSetDimensions(); - // Labels - this.beforeBuildLabels(); - this.buildLabels(); - this.afterBuildLabels(); - - // Fit - this.beforeFit(); - this.fit(); - this.afterFit(); - // - this.afterUpdate(); - - return this.minSize; - - }, - afterUpdate: helpers.noop, - - // - - beforeSetDimensions: helpers.noop, - setDimensions: function() { - // Set the unconstrained dimension before label rotation - if (this.isHorizontal()) { - // Reset position before calculating rotation - this.width = this.maxWidth; - this.left = 0; - this.right = this.width; - } else { - this.height = this.maxHeight; - - // Reset position before calculating rotation - this.top = 0; - this.bottom = this.height; - } - - // Reset padding - this.paddingLeft = 0; - this.paddingTop = 0; - this.paddingRight = 0; - this.paddingBottom = 0; - - // Reset minSize - this.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: helpers.noop, - - // - - beforeBuildLabels: helpers.noop, - buildLabels: helpers.noop, - afterBuildLabels: helpers.noop, - - // - - beforeFit: helpers.noop, - fit: function() { - - var ctx = this.ctx; - var fontSize = helpers.getValueOrDefault(this.options.fontSize, Chart.defaults.global.defaultFontSize); - var fontStyle = helpers.getValueOrDefault(this.options.fontStyle, Chart.defaults.global.defaultFontStyle); - var fontFamily = helpers.getValueOrDefault(this.options.fontFamily, Chart.defaults.global.defaultFontFamily); - var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); - - // Width - if (this.isHorizontal()) { - this.minSize.width = this.maxWidth; // fill all the width - } else { - this.minSize.width = 0; - } - - // height - if (this.isHorizontal()) { - this.minSize.height = 0; - } else { - this.minSize.height = this.maxHeight; // fill all the height - } - - // Increase sizes here - if (this.isHorizontal()) { - - // Title - if (this.options.display) { - this.minSize.height += fontSize + (this.options.padding * 2); - } - } else { - if (this.options.display) { - this.minSize.width += fontSize + (this.options.padding * 2); - } - } - - this.width = this.minSize.width; - this.height = this.minSize.height; - - }, - afterFit: helpers.noop, - - // Shared Methods - isHorizontal: function() { - return this.options.position === "top" || this.options.position === "bottom"; - }, - - // Actualy draw the title block on the canvas - draw: function() { - if (this.options.display) { - var ctx = this.ctx; - var titleX, titleY; - - var fontColor = helpers.getValueOrDefault(this.options.fontColor, Chart.defaults.global.defaultFontColor); - var fontSize = helpers.getValueOrDefault(this.options.fontSize, Chart.defaults.global.defaultFontSize); - var fontStyle = helpers.getValueOrDefault(this.options.fontStyle, Chart.defaults.global.defaultFontStyle); - var fontFamily = helpers.getValueOrDefault(this.options.fontFamily, Chart.defaults.global.defaultFontFamily); - var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); - - ctx.fillStyle = fontColor; // render in correct colour - ctx.font = titleFont; - - // Horizontal - if (this.isHorizontal()) { - // Title - ctx.textAlign = "center"; - ctx.textBaseline = 'middle'; - - titleX = this.left + ((this.right - this.left) / 2); // midpoint of the width - titleY = this.top + ((this.bottom - this.top) / 2); // midpoint of the height - - ctx.fillText(this.options.text, titleX, titleY); - } else { - - // Title - titleX = this.options.position === 'left' ? this.left + (fontSize / 2) : this.right - (fontSize / 2); - titleY = this.top + ((this.bottom - this.top) / 2); - var rotation = this.options.position === 'left' ? -0.5 * Math.PI : 0.5 * Math.PI; - - ctx.save(); - ctx.translate(titleX, titleY); - ctx.rotate(rotation); - ctx.textAlign = "center"; - ctx.textBaseline = 'middle'; - ctx.fillText(this.options.text, 0, 0); - ctx.restore(); - } - } - } - }); -}; -},{}],32:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.global.tooltips = { - enabled: true, - custom: null, - mode: 'single', - backgroundColor: "rgba(0,0,0,0.8)", - titleFontStyle: "bold", - titleSpacing: 2, - titleMarginBottom: 6, - titleColor: "#fff", - titleAlign: "left", - bodySpacing: 2, - bodyColor: "#fff", - bodyAlign: "left", - footerFontStyle: "bold", - footerSpacing: 2, - footerMarginTop: 6, - footerColor: "#fff", - footerAlign: "left", - yPadding: 6, - xPadding: 6, - yAlign : 'center', - xAlign : 'center', - caretSize: 5, - cornerRadius: 6, - multiKeyBackground: '#fff', - callbacks: { - // Args are: (tooltipItems, data) - beforeTitle: helpers.noop, - title: function(tooltipItems, data) { - // Pick first xLabel for now - var title = ''; - - if (tooltipItems.length > 0) { - if (tooltipItems[0].xLabel) { - title = tooltipItems[0].xLabel; - } else if (data.labels.length > 0 && tooltipItems[0].index < data.labels.length) { - title = data.labels[tooltipItems[0].index]; - } - } - - return title; - }, - afterTitle: helpers.noop, - - // Args are: (tooltipItems, data) - beforeBody: helpers.noop, - - // Args are: (tooltipItem, data) - beforeLabel: helpers.noop, - label: function(tooltipItem, data) { - var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; - return datasetLabel + ': ' + tooltipItem.yLabel; - }, - afterLabel: helpers.noop, - - // Args are: (tooltipItems, data) - afterBody: helpers.noop, - - // Args are: (tooltipItems, data) - beforeFooter: helpers.noop, - footer: helpers.noop, - afterFooter: helpers.noop - } - }; - - // Helper to push or concat based on if the 2nd parameter is an array or not - function pushOrConcat(base, toPush) { - if (toPush) { - if (helpers.isArray(toPush)) { - base = base.concat(toPush); - } else { - base.push(toPush); - } - } - - return base; - } - - Chart.Tooltip = Chart.Element.extend({ - initialize: function() { - var options = this._options; - helpers.extend(this, { - _model: { - // Positioning - xPadding: options.tooltips.xPadding, - yPadding: options.tooltips.yPadding, - xAlign : options.tooltips.yAlign, - yAlign : options.tooltips.xAlign, - - // Body - bodyColor: options.tooltips.bodyColor, - _bodyFontFamily: helpers.getValueOrDefault(options.tooltips.bodyFontFamily, Chart.defaults.global.defaultFontFamily), - _bodyFontStyle: helpers.getValueOrDefault(options.tooltips.bodyFontStyle, Chart.defaults.global.defaultFontStyle), - _bodyAlign: options.tooltips.bodyAlign, - bodyFontSize: helpers.getValueOrDefault(options.tooltips.bodyFontSize, Chart.defaults.global.defaultFontSize), - bodySpacing: options.tooltips.bodySpacing, - - // Title - titleColor: options.tooltips.titleColor, - _titleFontFamily: helpers.getValueOrDefault(options.tooltips.titleFontFamily, Chart.defaults.global.defaultFontFamily), - _titleFontStyle: helpers.getValueOrDefault(options.tooltips.titleFontStyle, Chart.defaults.global.defaultFontStyle), - titleFontSize: helpers.getValueOrDefault(options.tooltips.titleFontSize, Chart.defaults.global.defaultFontSize), - _titleAlign: options.tooltips.titleAlign, - titleSpacing: options.tooltips.titleSpacing, - titleMarginBottom: options.tooltips.titleMarginBottom, - - // Footer - footerColor: options.tooltips.footerColor, - _footerFontFamily: helpers.getValueOrDefault(options.tooltips.footerFontFamily, Chart.defaults.global.defaultFontFamily), - _footerFontStyle: helpers.getValueOrDefault(options.tooltips.footerFontStyle, Chart.defaults.global.defaultFontStyle), - footerFontSize: helpers.getValueOrDefault(options.tooltips.footerFontSize, Chart.defaults.global.defaultFontSize), - _footerAlign: options.tooltips.footerAlign, - footerSpacing: options.tooltips.footerSpacing, - footerMarginTop: options.tooltips.footerMarginTop, - - // Appearance - caretSize: options.tooltips.caretSize, - cornerRadius: options.tooltips.cornerRadius, - backgroundColor: options.tooltips.backgroundColor, - opacity: 0, - legendColorBackground: options.tooltips.multiKeyBackground - } - }); - }, - - // Get the title - // Args are: (tooltipItem, data) - getTitle: function() { - var beforeTitle = this._options.tooltips.callbacks.beforeTitle.apply(this, arguments), - title = this._options.tooltips.callbacks.title.apply(this, arguments), - afterTitle = this._options.tooltips.callbacks.afterTitle.apply(this, arguments); - - var lines = []; - lines = pushOrConcat(lines, beforeTitle); - lines = pushOrConcat(lines, title); - lines = pushOrConcat(lines, afterTitle); - - return lines; - }, - - // Args are: (tooltipItem, data) - getBeforeBody: function() { - var lines = this._options.tooltips.callbacks.beforeBody.apply(this, arguments); - return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; - }, - - // Args are: (tooltipItem, data) - getBody: function(tooltipItems, data) { - var lines = []; - - helpers.each(tooltipItems, function(bodyItem) { - helpers.pushAllIfDefined(this._options.tooltips.callbacks.beforeLabel.call(this, bodyItem, data), lines); - helpers.pushAllIfDefined(this._options.tooltips.callbacks.label.call(this, bodyItem, data), lines); - helpers.pushAllIfDefined(this._options.tooltips.callbacks.afterLabel.call(this, bodyItem, data), lines); - }, this); - - return lines; - }, - - // Args are: (tooltipItem, data) - getAfterBody: function() { - var lines = this._options.tooltips.callbacks.afterBody.apply(this, arguments); - return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; - }, - - // Get the footer and beforeFooter and afterFooter lines - // Args are: (tooltipItem, data) - getFooter: function() { - var beforeFooter = this._options.tooltips.callbacks.beforeFooter.apply(this, arguments); - var footer = this._options.tooltips.callbacks.footer.apply(this, arguments); - var afterFooter = this._options.tooltips.callbacks.afterFooter.apply(this, arguments); - - var lines = []; - lines = pushOrConcat(lines, beforeFooter); - lines = pushOrConcat(lines, footer); - lines = pushOrConcat(lines, afterFooter); - - return lines; - }, - - getAveragePosition: function(elements) { - - if (!elements.length) { - return false; - } - - var xPositions = []; - var yPositions = []; - - helpers.each(elements, function(el) { - if (el) { - var pos = el.tooltipPosition(); - xPositions.push(pos.x); - yPositions.push(pos.y); - } - }); - - var x = 0, - y = 0; - for (var i = 0; i < xPositions.length; i++) { - x += xPositions[i]; - y += yPositions[i]; - } - - return { - x: Math.round(x / xPositions.length), - y: Math.round(y / xPositions.length) - }; - - }, - - update: function(changed) { - if (this._active.length) { - this._model.opacity = 1; - - var element = this._active[0], - labelColors = [], - tooltipPosition; - - var tooltipItems = []; - - if (this._options.tooltips.mode === 'single') { - var yScale = element._yScale || element._scale; // handle radar || polarArea charts - tooltipItems.push({ - xLabel: element._xScale ? element._xScale.getLabelForIndex(element._index, element._datasetIndex) : '', - yLabel: yScale ? yScale.getLabelForIndex(element._index, element._datasetIndex) : '', - index: element._index, - datasetIndex: element._datasetIndex - }); - tooltipPosition = this.getAveragePosition(this._active); - } else { - helpers.each(this._data.datasets, function(dataset, datasetIndex) { - if (!helpers.isDatasetVisible(dataset)) { - return; - } - var currentElement = dataset.metaData[element._index]; - if (currentElement) { - var yScale = element._yScale || element._scale; // handle radar || polarArea charts - - tooltipItems.push({ - xLabel: currentElement._xScale ? currentElement._xScale.getLabelForIndex(currentElement._index, currentElement._datasetIndex) : '', - yLabel: yScale ? yScale.getLabelForIndex(currentElement._index, currentElement._datasetIndex) : '', - index: element._index, - datasetIndex: datasetIndex - }); - } - }, null, element._yScale.options.stacked); - - helpers.each(this._active, function(active) { - if (active) { - labelColors.push({ - borderColor: active._view.borderColor, - backgroundColor: active._view.backgroundColor - }); - } - }, null, element._yScale.options.stacked); - - tooltipPosition = this.getAveragePosition(this._active); - tooltipPosition.y = this._active[0]._yScale.getPixelForDecimal(0.5); - } - - // Build the Text Lines - helpers.extend(this._model, { - title: this.getTitle(tooltipItems, this._data), - beforeBody: this.getBeforeBody(tooltipItems, this._data), - body: this.getBody(tooltipItems, this._data), - afterBody: this.getAfterBody(tooltipItems, this._data), - footer: this.getFooter(tooltipItems, this._data) - }); - - helpers.extend(this._model, { - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - caretPadding: helpers.getValueOrDefault(tooltipPosition.padding, 2), - labelColors: labelColors - }); - - // We need to determine alignment of - var tooltipSize = this.getTooltipSize(this._model); - this.determineAlignment(tooltipSize); // Smart Tooltip placement to stay on the canvas - - helpers.extend(this._model, this.getBackgroundPoint(this._model, tooltipSize)); - } else { - this._model.opacity = 0; - } - - if (changed && this._options.tooltips.custom) { - this._options.tooltips.custom.call(this, this._model); - } - - return this; - }, - getTooltipSize: function getTooltipSize(vm) { - var ctx = this._chart.ctx; - - var size = { - height: vm.yPadding * 2, // Tooltip Padding - width: 0 - }; - var combinedBodyLength = vm.body.length + vm.beforeBody.length + vm.afterBody.length; - - size.height += vm.title.length * vm.titleFontSize; // Title Lines - size.height += (vm.title.length - 1) * vm.titleSpacing; // Title Line Spacing - size.height += vm.title.length ? vm.titleMarginBottom : 0; // Title's bottom Margin - size.height += combinedBodyLength * vm.bodyFontSize; // Body Lines - size.height += combinedBodyLength ? (combinedBodyLength - 1) * vm.bodySpacing : 0; // Body Line Spacing - size.height += vm.footer.length ? vm.footerMarginTop : 0; // Footer Margin - size.height += vm.footer.length * (vm.footerFontSize); // Footer Lines - size.height += vm.footer.length ? (vm.footer.length - 1) * vm.footerSpacing : 0; // Footer Line Spacing - - // Width - ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily); - helpers.each(vm.title, function(line) { - size.width = Math.max(size.width, ctx.measureText(line).width); - }); - - ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); - helpers.each(vm.beforeBody.concat(vm.afterBody), function(line) { - size.width = Math.max(size.width, ctx.measureText(line).width); - }); - helpers.each(vm.body, function(line) { - size.width = Math.max(size.width, ctx.measureText(line).width + (this._options.tooltips.mode !== 'single' ? (vm.bodyFontSize + 2) : 0)); - }, this); - - ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); - helpers.each(vm.footer, function(line) { - size.width = Math.max(size.width, ctx.measureText(line).width); - }); - size.width += 2 * vm.xPadding; - - return size; - }, - determineAlignment: function determineAlignment(size) { - if (this._model.y < size.height) { - this._model.yAlign = 'top'; - } else if (this._model.y > (this._chart.height - size.height)) { - this._model.yAlign = 'bottom'; - } - - var lf, rf; // functions to determine left, right alignment - var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart - var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges - var _this = this; - var midX = (this._chartInstance.chartArea.left + this._chartInstance.chartArea.right) / 2; - var midY = (this._chartInstance.chartArea.top + this._chartInstance.chartArea.bottom) / 2; - - if (this._model.yAlign === 'center') { - lf = function(x) { - return x <= midX; - }; - rf = function(x) { - return x > midX; - }; - } else { - lf = function(x) { - return x <= (size.width / 2); - }; - rf = function(x) { - return x >= (_this._chart.width - (size.width / 2)); - }; - } - - olf = function(x) { - return x + size.width > _this._chart.width; - }; - orf = function(x) { - return x - size.width < 0; - }; - yf = function(y) { - return y <= midY ? 'top' : 'bottom'; - }; - - if (lf(this._model.x)) { - this._model.xAlign = 'left'; - - // Is tooltip too wide and goes over the right side of the chart.? - if (olf(this._model.x)) { - this._model.xAlign = 'center'; - this._model.yAlign = yf(this._model.y); - } - } else if (rf(this._model.x)) { - this._model.xAlign = 'right'; - - // Is tooltip too wide and goes outside left edge of canvas? - if (orf(this._model.x)) { - this._model.xAlign = 'center'; - this._model.yAlign = yf(this._model.y); - } - } - }, - getBackgroundPoint: function getBackgroundPoint(vm, size) { - // Background Position - var pt = { - x: vm.x, - y: vm.y - }; - - if (vm.xAlign === 'right') { - pt.x -= size.width; - } else if (vm.xAlign === 'center') { - pt.x -= (size.width / 2); - } - - if (vm.yAlign === 'top') { - pt.y += vm.caretPadding + vm.caretSize; - } else if (vm.yAlign === 'bottom') { - pt.y -= size.height + vm.caretPadding + vm.caretSize; - } else { - pt.y -= (size.height / 2); - } - - if (vm.yAlign === 'center') { - if (vm.xAlign === 'left') { - pt.x += vm.caretPadding + vm.caretSize; - } else if (vm.xAlign === 'right') { - pt.x -= vm.caretPadding + vm.caretSize; - } - } else { - if (vm.xAlign === 'left') { - pt.x -= vm.cornerRadius + vm.caretPadding; - } else if (vm.xAlign === 'right') { - pt.x += vm.cornerRadius + vm.caretPadding; - } - } - - return pt; - }, - drawCaret: function drawCaret(tooltipPoint, size, opacity, caretPadding) { - var vm = this._view; - var ctx = this._chart.ctx; - var x1, x2, x3; - var y1, y2, y3; - - if (vm.yAlign === 'center') { - // Left or right side - if (vm.xAlign === 'left') { - x1 = tooltipPoint.x; - x2 = x1 - vm.caretSize; - x3 = x1; - } else { - x1 = tooltipPoint.x + size.width; - x2 = x1 + vm.caretSize; - x3 = x1; - } - - y2 = tooltipPoint.y + (size.height / 2); - y1 = y2 - vm.caretSize; - y3 = y2 + vm.caretSize; - } else { - if (vm.xAlign === 'left') { - x1 = tooltipPoint.x + vm.cornerRadius; - x2 = x1 + vm.caretSize; - x3 = x2 + vm.caretSize; - } else if (vm.xAlign === 'right') { - x1 = tooltipPoint.x + size.width - vm.cornerRadius; - x2 = x1 - vm.caretSize; - x3 = x2 - vm.caretSize; - } else { - x2 = tooltipPoint.x + (size.width / 2); - x1 = x2 - vm.caretSize; - x3 = x2 + vm.caretSize; - } - - if (vm.yAlign === 'top') { - y1 = tooltipPoint.y; - y2 = y1 - vm.caretSize; - y3 = y1; - } else { - y1 = tooltipPoint.y + size.height; - y2 = y1 + vm.caretSize; - y3 = y1; - } - } - - var bgColor = helpers.color(vm.backgroundColor); - ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString(); - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.lineTo(x3, y3); - ctx.closePath(); - ctx.fill(); - }, - drawTitle: function drawTitle(pt, vm, ctx, opacity) { - if (vm.title.length) { - ctx.textAlign = vm._titleAlign; - ctx.textBaseline = "top"; - - var titleColor = helpers.color(vm.titleColor); - ctx.fillStyle = titleColor.alpha(opacity * titleColor.alpha()).rgbString(); - ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily); - - helpers.each(vm.title, function(title, i) { - ctx.fillText(title, pt.x, pt.y); - pt.y += vm.titleFontSize + vm.titleSpacing; // Line Height and spacing - - if (i + 1 === vm.title.length) { - pt.y += vm.titleMarginBottom - vm.titleSpacing; // If Last, add margin, remove spacing - } - }); - } - }, - drawBody: function drawBody(pt, vm, ctx, opacity) { - ctx.textAlign = vm._bodyAlign; - ctx.textBaseline = "top"; - - var bodyColor = helpers.color(vm.bodyColor); - ctx.fillStyle = bodyColor.alpha(opacity * bodyColor.alpha()).rgbString(); - ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); - - // Before Body - helpers.each(vm.beforeBody, function(beforeBody) { - ctx.fillText(beforeBody, pt.x, pt.y); - pt.y += vm.bodyFontSize + vm.bodySpacing; - }); - - helpers.each(vm.body, function(body, i) { - // Draw Legend-like boxes if needed - if (this._options.tooltips.mode !== 'single') { - // Fill a white rect so that colours merge nicely if the opacity is < 1 - ctx.fillStyle = helpers.color(vm.legendColorBackground).alpha(opacity).rgbaString(); - ctx.fillRect(pt.x, pt.y, vm.bodyFontSize, vm.bodyFontSize); - - // Border - ctx.strokeStyle = helpers.color(vm.labelColors[i].borderColor).alpha(opacity).rgbaString(); - ctx.strokeRect(pt.x, pt.y, vm.bodyFontSize, vm.bodyFontSize); - - // Inner square - ctx.fillStyle = helpers.color(vm.labelColors[i].backgroundColor).alpha(opacity).rgbaString(); - ctx.fillRect(pt.x + 1, pt.y + 1, vm.bodyFontSize - 2, vm.bodyFontSize - 2); - - ctx.fillStyle = helpers.color(vm.bodyColor).alpha(opacity).rgbaString(); // Return fill style for text - } - - // Body Line - ctx.fillText(body, pt.x + (this._options.tooltips.mode !== 'single' ? (vm.bodyFontSize + 2) : 0), pt.y); - - pt.y += vm.bodyFontSize + vm.bodySpacing; - }, this); - - // After Body - helpers.each(vm.afterBody, function(afterBody) { - ctx.fillText(afterBody, pt.x, pt.y); - pt.y += vm.bodyFontSize; - }); - - pt.y -= vm.bodySpacing; // Remove last body spacing - }, - drawFooter: function drawFooter(pt, vm, ctx, opacity) { - if (vm.footer.length) { - pt.y += vm.footerMarginTop; - - ctx.textAlign = vm._footerAlign; - ctx.textBaseline = "top"; - - var footerColor = helpers.color(vm.footerColor); - ctx.fillStyle = footerColor.alpha(opacity * footerColor.alpha()).rgbString(); - ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); - - helpers.each(vm.footer, function(footer) { - ctx.fillText(footer, pt.x, pt.y); - pt.y += vm.footerFontSize + vm.footerSpacing; - }); - } - }, - draw: function draw() { - var ctx = this._chart.ctx; - var vm = this._view; - - if (vm.opacity === 0) { - return; - } - - var caretPadding = vm.caretPadding; - var tooltipSize = this.getTooltipSize(vm); - var pt = { - x: vm.x, - y: vm.y - }; - - // IE11/Edge does not like very small opacities, so snap to 0 - var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity; - - if (this._options.tooltips.enabled) { - // Draw Background - var bgColor = helpers.color(vm.backgroundColor); - ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString(); - helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius); - ctx.fill(); - - // Draw Caret - this.drawCaret(pt, tooltipSize, opacity, caretPadding); - - // Draw Title, Body, and Footer - pt.x += vm.xPadding; - pt.y += vm.yPadding; - - // Titles - this.drawTitle(pt, vm, ctx, opacity); - - // Body - this.drawBody(pt, vm, ctx, opacity); - - // Footer - this.drawFooter(pt, vm, ctx, opacity); - } - } - }); -}; - -},{}],33:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart, moment) { - - var helpers = Chart.helpers; - - Chart.defaults.global.elements.arc = { - backgroundColor: Chart.defaults.global.defaultColor, - borderColor: "#fff", - borderWidth: 2 - }; - - Chart.elements.Arc = Chart.Element.extend({ - inLabelRange: function(mouseX) { - var vm = this._view; - - if (vm) { - return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hoverRadius, 2)); - } else { - return false; - } - }, - inRange: function(chartX, chartY) { - - var vm = this._view; - - if (vm) { - var pointRelativePosition = helpers.getAngleFromPoint(vm, { - x: chartX, - y: chartY - }); - - //Sanitise angle range - var startAngle = vm.startAngle; - var endAngle = vm.endAngle; - while (endAngle < startAngle) { - endAngle += 2.0 * Math.PI; - } - while (pointRelativePosition.angle > endAngle) { - pointRelativePosition.angle -= 2.0 * Math.PI; - } - while (pointRelativePosition.angle < startAngle) { - pointRelativePosition.angle += 2.0 * Math.PI; - } - - //Check if within the range of the open/close angle - var betweenAngles = (pointRelativePosition.angle >= startAngle && pointRelativePosition.angle <= endAngle), - withinRadius = (pointRelativePosition.distance >= vm.innerRadius && pointRelativePosition.distance <= vm.outerRadius); - - return (betweenAngles && withinRadius); - } else { - return false; - } - }, - tooltipPosition: function() { - var vm = this._view; - - var centreAngle = vm.startAngle + ((vm.endAngle - vm.startAngle) / 2), - rangeFromCentre = (vm.outerRadius - vm.innerRadius) / 2 + vm.innerRadius; - return { - x: vm.x + (Math.cos(centreAngle) * rangeFromCentre), - y: vm.y + (Math.sin(centreAngle) * rangeFromCentre) - }; - }, - draw: function() { - - var ctx = this._chart.ctx; - var vm = this._view; - - ctx.beginPath(); - - ctx.arc(vm.x, vm.y, vm.outerRadius, vm.startAngle, vm.endAngle); - - ctx.arc(vm.x, vm.y, vm.innerRadius, vm.endAngle, vm.startAngle, true); - - ctx.closePath(); - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - ctx.fillStyle = vm.backgroundColor; - - ctx.fill(); - ctx.lineJoin = 'bevel'; - - if (vm.borderWidth) { - ctx.stroke(); - } - } - }); -}; - -},{}],34:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.global.elements.line = { - tension: 0.4, - backgroundColor: Chart.defaults.global.defaultColor, - borderWidth: 3, - borderColor: Chart.defaults.global.defaultColor, - borderCapStyle: 'butt', - borderDash: [], - borderDashOffset: 0.0, - borderJoinStyle: 'miter', - fill: true // do we fill in the area between the line and its base axis - }; - - Chart.elements.Line = Chart.Element.extend({ - lineToNextPoint: function(previousPoint, point, nextPoint, skipHandler, previousSkipHandler) { - var ctx = this._chart.ctx; - - if (point._view.skip) { - skipHandler.call(this, previousPoint, point, nextPoint); - } else if (previousPoint._view.skip) { - previousSkipHandler.call(this, previousPoint, point, nextPoint); - } else if (point._view.tension === 0) { - ctx.lineTo(point._view.x, point._view.y); - } else { - // Line between points - ctx.bezierCurveTo( - previousPoint._view.controlPointNextX, - previousPoint._view.controlPointNextY, - point._view.controlPointPreviousX, - point._view.controlPointPreviousY, - point._view.x, - point._view.y - ); - } - }, - - draw: function() { - var _this = this; - - var vm = this._view; - var ctx = this._chart.ctx; - var first = this._children[0]; - var last = this._children[this._children.length - 1]; - - function loopBackToStart(drawLineToCenter) { - if (!first._view.skip && !last._view.skip) { - // Draw a bezier line from last to first - ctx.bezierCurveTo( - last._view.controlPointNextX, - last._view.controlPointNextY, - first._view.controlPointPreviousX, - first._view.controlPointPreviousY, - first._view.x, - first._view.y - ); - } else if (drawLineToCenter) { - // Go to center - ctx.lineTo(_this._view.scaleZero.x, _this._view.scaleZero.y); - } - } - - ctx.save(); - - // If we had points and want to fill this line, do so. - if (this._children.length > 0 && vm.fill) { - // Draw the background first (so the border is always on top) - ctx.beginPath(); - - helpers.each(this._children, function(point, index) { - var previous = helpers.previousItem(this._children, index); - var next = helpers.nextItem(this._children, index); - - // First point moves to it's starting position no matter what - if (index === 0) { - if (this._loop) { - ctx.moveTo(vm.scaleZero.x, vm.scaleZero.y); - } else { - ctx.moveTo(point._view.x, vm.scaleZero); - } - - if (point._view.skip) { - if (!this._loop) { - ctx.moveTo(next._view.x, this._view.scaleZero); - } - } else { - ctx.lineTo(point._view.x, point._view.y); - } - } else { - this.lineToNextPoint(previous, point, next, function(previousPoint, point, nextPoint) { - if (this._loop) { - // Go to center - ctx.lineTo(this._view.scaleZero.x, this._view.scaleZero.y); - } else { - ctx.lineTo(previousPoint._view.x, this._view.scaleZero); - ctx.moveTo(nextPoint._view.x, this._view.scaleZero); - } - }, function(previousPoint, point) { - // If we skipped the last point, draw a line to ourselves so that the fill is nice - ctx.lineTo(point._view.x, point._view.y); - }); - } - }, this); - - // For radial scales, loop back around to the first point - if (this._loop) { - loopBackToStart(true); - } else { - //Round off the line by going to the base of the chart, back to the start, then fill. - ctx.lineTo(this._children[this._children.length - 1]._view.x, vm.scaleZero); - ctx.lineTo(this._children[0]._view.x, vm.scaleZero); - } - - ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.defaultColor; - ctx.closePath(); - ctx.fill(); - } - - // Now draw the line between all the points with any borders - ctx.lineCap = vm.borderCapStyle || Chart.defaults.global.elements.line.borderCapStyle; - - // IE 9 and 10 do not support line dash - if (ctx.setLineDash) { - ctx.setLineDash(vm.borderDash || Chart.defaults.global.elements.line.borderDash); - } - - ctx.lineDashOffset = vm.borderDashOffset || Chart.defaults.global.elements.line.borderDashOffset; - ctx.lineJoin = vm.borderJoinStyle || Chart.defaults.global.elements.line.borderJoinStyle; - ctx.lineWidth = vm.borderWidth || Chart.defaults.global.elements.line.borderWidth; - ctx.strokeStyle = vm.borderColor || Chart.defaults.global.defaultColor; - ctx.beginPath(); - - helpers.each(this._children, function(point, index) { - var previous = helpers.previousItem(this._children, index); - var next = helpers.nextItem(this._children, index); - - if (index === 0) { - ctx.moveTo(point._view.x, point._view.y); - } else { - this.lineToNextPoint(previous, point, next, function(previousPoint, point, nextPoint) { - ctx.moveTo(nextPoint._view.x, nextPoint._view.y); - }, function(previousPoint, point) { - // If we skipped the last point, move up to our point preventing a line from being drawn - ctx.moveTo(point._view.x, point._view.y); - }); - } - }, this); - - if (this._loop && this._children.length > 0) { - loopBackToStart(); - } - - ctx.stroke(); - ctx.restore(); - } - }); -}; -},{}],35:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.global.elements.point = { - radius: 3, - pointStyle: 'circle', - backgroundColor: Chart.defaults.global.defaultColor, - borderWidth: 1, - borderColor: Chart.defaults.global.defaultColor, - // Hover - hitRadius: 1, - hoverRadius: 4, - hoverBorderWidth: 1 - }; - - - Chart.elements.Point = Chart.Element.extend({ - inRange: function(mouseX, mouseY) { - var vm = this._view; - - if (vm) { - var hoverRange = vm.hitRadius + vm.radius; - return ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(hoverRange, 2)); - } else { - return false; - } - }, - inLabelRange: function(mouseX) { - var vm = this._view; - - if (vm) { - return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)); - } else { - return false; - } - }, - tooltipPosition: function() { - var vm = this._view; - return { - x: vm.x, - y: vm.y, - padding: vm.radius + vm.borderWidth - }; - }, - draw: function() { - - var vm = this._view; - var ctx = this._chart.ctx; - - - if (vm.skip) { - return; - } - - if (typeof vm.pointStyle === 'object' && ((vm.pointStyle.toString() === '[object HTMLImageElement]') || (vm.pointStyle.toString() === '[object HTMLCanvasElement]'))) { - ctx.drawImage(vm.pointStyle, vm.x - vm.pointStyle.width / 2, vm.y - vm.pointStyle.height / 2); - return; - } - - if (!isNaN(vm.radius) && vm.radius > 0) { - - ctx.strokeStyle = vm.borderColor || Chart.defaults.global.defaultColor; - ctx.lineWidth = helpers.getValueOrDefault(vm.borderWidth, Chart.defaults.global.elements.point.borderWidth); - - ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.defaultColor; - - var radius = vm.radius; - - var xOffset; - var yOffset; - - switch (vm.pointStyle) { - // Default includes circle - default: ctx.beginPath(); - ctx.arc(vm.x, vm.y, radius, 0, Math.PI * 2); - ctx.closePath(); - ctx.fill(); - break; - case 'triangle': - ctx.beginPath(); - var edgeLength = 3 * radius / Math.sqrt(3); - var height = edgeLength * Math.sqrt(3) / 2; - ctx.moveTo(vm.x - edgeLength / 2, vm.y + height / 3); - ctx.lineTo(vm.x + edgeLength / 2, vm.y + height / 3); - ctx.lineTo(vm.x, vm.y - 2 * height / 3); - ctx.closePath(); - ctx.fill(); - break; - case 'rect': - ctx.fillRect(vm.x - 1 / Math.SQRT2 * radius, vm.y - 1 / Math.SQRT2 * radius, 2 / Math.SQRT2 * radius, 2 / Math.SQRT2 * radius); - ctx.strokeRect(vm.x - 1 / Math.SQRT2 * radius, vm.y - 1 / Math.SQRT2 * radius, 2 / Math.SQRT2 * radius, 2 / Math.SQRT2 * radius); - break; - case 'rectRot': - ctx.translate(vm.x, vm.y); - ctx.rotate(Math.PI / 4); - ctx.fillRect(-1 / Math.SQRT2 * radius, -1 / Math.SQRT2 * radius, 2 / Math.SQRT2 * radius, 2 / Math.SQRT2 * radius); - ctx.strokeRect(-1 / Math.SQRT2 * radius, -1 / Math.SQRT2 * radius, 2 / Math.SQRT2 * radius, 2 / Math.SQRT2 * radius); - ctx.setTransform(1, 0, 0, 1, 0, 0); - break; - case 'cross': - ctx.beginPath(); - ctx.moveTo(vm.x, vm.y + radius); - ctx.lineTo(vm.x, vm.y - radius); - ctx.moveTo(vm.x - radius, vm.y); - ctx.lineTo(vm.x + radius, vm.y); - ctx.closePath(); - break; - case 'crossRot': - ctx.beginPath(); - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(vm.x - xOffset, vm.y - yOffset); - ctx.lineTo(vm.x + xOffset, vm.y + yOffset); - ctx.moveTo(vm.x - xOffset, vm.y + yOffset); - ctx.lineTo(vm.x + xOffset, vm.y - yOffset); - ctx.closePath(); - break; - case 'star': - ctx.beginPath(); - ctx.moveTo(vm.x, vm.y + radius); - ctx.lineTo(vm.x, vm.y - radius); - ctx.moveTo(vm.x - radius, vm.y); - ctx.lineTo(vm.x + radius, vm.y); - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(vm.x - xOffset, vm.y - yOffset); - ctx.lineTo(vm.x + xOffset, vm.y + yOffset); - ctx.moveTo(vm.x - xOffset, vm.y + yOffset); - ctx.lineTo(vm.x + xOffset, vm.y - yOffset); - ctx.closePath(); - break; - case 'line': - ctx.beginPath(); - ctx.moveTo(vm.x - radius, vm.y); - ctx.lineTo(vm.x + radius, vm.y); - ctx.closePath(); - break; - case 'dash': - ctx.beginPath(); - ctx.moveTo(vm.x, vm.y); - ctx.lineTo(vm.x + radius, vm.y); - ctx.closePath(); - break; - } - - ctx.stroke(); - } - } - }); -}; -},{}],36:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - Chart.defaults.global.elements.rectangle = { - backgroundColor: Chart.defaults.global.defaultColor, - borderWidth: 0, - borderColor: Chart.defaults.global.defaultColor, - borderSkipped: 'bottom' - }; - - Chart.elements.Rectangle = Chart.Element.extend({ - draw: function() { - - var ctx = this._chart.ctx; - var vm = this._view; - - var halfWidth = vm.width / 2, - leftX = vm.x - halfWidth, - rightX = vm.x + halfWidth, - top = vm.base - (vm.base - vm.y), - halfStroke = vm.borderWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - // Corner points, from bottom-left to bottom-right clockwise - // | 1 2 | - // | 0 3 | - var corners = [ - [leftX, vm.base], - [leftX, top], - [rightX, top], - [rightX, vm.base] - ]; - - // Find first (starting) corner with fallback to 'bottom' - var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); - if (startCorner === -1) - startCorner = 0; - - function cornerAt(index) { - return corners[(startCorner + index) % 4]; - } - - // Draw rectangle from 'startCorner' - ctx.moveTo.apply(ctx, cornerAt(0)); - for (var i = 1; i < 4; i++) - ctx.lineTo.apply(ctx, cornerAt(i)); - - ctx.fill(); - if (vm.borderWidth) { - ctx.stroke(); - } - }, - height: function() { - var vm = this._view; - return vm.base - vm.y; - }, - inRange: function(mouseX, mouseY) { - var vm = this._view; - var inRange = false; - - if (vm) { - if (vm.y < vm.base) { - inRange = (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base); - } else { - inRange = (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y); - } - } - - return inRange; - }, - inLabelRange: function(mouseX) { - var vm = this._view; - - if (vm) { - return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2); - } else { - return false; - } - }, - tooltipPosition: function() { - var vm = this._view; - return { - x: vm.x, - y: vm.y - }; - } - }); - -}; -},{}],37:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - // Default config for a category scale - var defaultConfig = { - position: "bottom" - }; - - var DatasetScale = Chart.Scale.extend({ - buildTicks: function(index) { - this.startIndex = 0; - this.endIndex = this.chart.data.labels.length; - var findIndex; - - if (this.options.ticks.min !== undefined) { - // user specified min value - findIndex = helpers.indexOf(this.chart.data.labels, this.options.ticks.min); - this.startIndex = findIndex !== -1 ? findIndex : this.startIndex; - } - - if (this.options.ticks.max !== undefined) { - // user specified max value - findIndex = helpers.indexOf(this.chart.data.labels, this.options.ticks.max); - this.endIndex = findIndex !== -1 ? findIndex : this.endIndex; - } - - // If we are viewing some subset of labels, slice the original array - this.ticks = (this.startIndex === 0 && this.endIndex === this.chart.data.labels.length) ? this.chart.data.labels : this.chart.data.labels.slice(this.startIndex, this.endIndex + 1); - }, - - getLabelForIndex: function(index, datasetIndex) { - return this.ticks[index]; - }, - - // Used to get data value locations. Value can either be an index or a numerical value - getPixelForValue: function(value, index, datasetIndex, includeOffset) { - // 1 is added because we need the length but we have the indexes - var offsetAmt = Math.max((this.ticks.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); - - if (this.isHorizontal()) { - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - var valueWidth = innerWidth / offsetAmt; - var widthOffset = (valueWidth * (index - this.startIndex)) + this.paddingLeft; - - if (this.options.gridLines.offsetGridLines && includeOffset) { - widthOffset += (valueWidth / 2); - } - - return this.left + Math.round(widthOffset); - } else { - var innerHeight = this.height - (this.paddingTop + this.paddingBottom); - var valueHeight = innerHeight / offsetAmt; - var heightOffset = (valueHeight * (index - this.startIndex)) + this.paddingTop; - - if (this.options.gridLines.offsetGridLines && includeOffset) { - heightOffset += (valueHeight / 2); - } - - return this.top + Math.round(heightOffset); - } - }, - getPixelForTick: function(index, includeOffset) { - return this.getPixelForValue(this.ticks[index], index + this.startIndex, null, includeOffset); - } - }); - - Chart.scaleService.registerScaleType("category", DatasetScale, defaultConfig); - -}; -},{}],38:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - var defaultConfig = { - position: "left", - ticks: { - callback: function(tickValue, index, ticks) { - var delta = ticks[1] - ticks[0]; - - // If we have a number like 2.5 as the delta, figure out how many decimal places we need - if (Math.abs(delta) > 1) { - if (tickValue !== Math.floor(tickValue)) { - // not an integer - delta = tickValue - Math.floor(tickValue); - } - } - - var logDelta = helpers.log10(Math.abs(delta)); - var tickString = ''; - - if (tickValue !== 0) { - var numDecimal = -1 * Math.floor(logDelta); - numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places - tickString = tickValue.toFixed(numDecimal); - } else { - tickString = '0'; // never show decimal places for 0 - } - - return tickString; - } - } - }; - - var LinearScale = Chart.Scale.extend({ - determineDataLimits: function() { - // First Calculate the range - this.min = null; - this.max = null; - - if (this.options.stacked) { - var valuesPerType = {}; - var hasPositiveValues = false; - var hasNegativeValues = false; - - helpers.each(this.chart.data.datasets, function(dataset) { - if (valuesPerType[dataset.type] === undefined) { - valuesPerType[dataset.type] = { - positiveValues: [], - negativeValues: [] - }; - } - - // Store these per type - var positiveValues = valuesPerType[dataset.type].positiveValues; - var negativeValues = valuesPerType[dataset.type].negativeValues; - - if (helpers.isDatasetVisible(dataset) && (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id)) { - helpers.each(dataset.data, function(rawValue, index) { - - var value = +this.getRightValue(rawValue); - if (isNaN(value)) { - return; - } - - positiveValues[index] = positiveValues[index] || 0; - negativeValues[index] = negativeValues[index] || 0; - - if (this.options.relativePoints) { - positiveValues[index] = 100; - } else { - if (value < 0) { - hasNegativeValues = true; - negativeValues[index] += value; - } else { - hasPositiveValues = true; - positiveValues[index] += value; - } - } - }, this); - } - }, this); - - helpers.each(valuesPerType, function(valuesForType) { - var values = valuesForType.positiveValues.concat(valuesForType.negativeValues); - var minVal = helpers.min(values); - var maxVal = helpers.max(values); - this.min = this.min === null ? minVal : Math.min(this.min, minVal); - this.max = this.max === null ? maxVal : Math.max(this.max, maxVal); - }, this); - - } else { - helpers.each(this.chart.data.datasets, function(dataset) { - if (helpers.isDatasetVisible(dataset) && (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id)) { - helpers.each(dataset.data, function(rawValue, index) { - var value = +this.getRightValue(rawValue); - if (isNaN(value)) { - return; - } - - if (this.min === null) { - this.min = value; - } else if (value < this.min) { - this.min = value; - } - - if (this.max === null) { - this.max = value; - } else if (value > this.max) { - this.max = value; - } - }, this); - } - }, this); - } - - // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, - // do nothing since that would make the chart weird. If the user really wants a weird chart - // axis, they can manually override it - if (this.options.ticks.beginAtZero) { - var minSign = helpers.sign(this.min); - var maxSign = helpers.sign(this.max); - - if (minSign < 0 && maxSign < 0) { - // move the top up to 0 - this.max = 0; - } else if (minSign > 0 && maxSign > 0) { - // move the botttom down to 0 - this.min = 0; - } - } - - if (this.options.ticks.min !== undefined) { - this.min = this.options.ticks.min; - } else if (this.options.ticks.suggestedMin !== undefined) { - this.min = Math.min(this.min, this.options.ticks.suggestedMin); - } - - if (this.options.ticks.max !== undefined) { - this.max = this.options.ticks.max; - } else if (this.options.ticks.suggestedMax !== undefined) { - this.max = Math.max(this.max, this.options.ticks.suggestedMax); - } - - if (this.min === this.max) { - this.min--; - this.max++; - } - }, - buildTicks: function() { - - // Then calulate the ticks - this.ticks = []; - - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - - var maxTicks; - - if (this.isHorizontal()) { - maxTicks = Math.min(this.options.ticks.maxTicksLimit ? this.options.ticks.maxTicksLimit : 11, Math.ceil(this.width / 50)); - } else { - // The factor of 2 used to scale the font size has been experimentally determined. - var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - maxTicks = Math.min(this.options.ticks.maxTicksLimit ? this.options.ticks.maxTicksLimit : 11, Math.ceil(this.height / (2 * tickFontSize))); - } - - // Make sure we always have at least 2 ticks - maxTicks = Math.max(2, maxTicks); - - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. - - var spacing; - var fixedStepSizeSet = (this.options.ticks.fixedStepSize && this.options.ticks.fixedStepSize > 0) || (this.options.ticks.stepSize && this.options.ticks.stepSize > 0); - if (fixedStepSizeSet) { - spacing = helpers.getValueOrDefault(this.options.ticks.fixedStepSize, this.options.ticks.stepSize); - } else { - var niceRange = helpers.niceNum(this.max - this.min, false); - spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); - } - var niceMin = Math.floor(this.min / spacing) * spacing; - var niceMax = Math.ceil(this.max / spacing) * spacing; - var numSpaces = (niceMax - niceMin) / spacing; - - // If very close to our rounded value, use it. - if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { - numSpaces = Math.round(numSpaces); - } else { - numSpaces = Math.ceil(numSpaces); - } - - // Put the values into the ticks array - this.ticks.push(this.options.ticks.min !== undefined ? this.options.ticks.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - this.ticks.push(niceMin + (j * spacing)); - } - this.ticks.push(this.options.ticks.max !== undefined ? this.options.ticks.max : niceMax); - - if (this.options.position === "left" || this.options.position === "right") { - // We are in a vertical orientation. The top value is the highest. So reverse the array - this.ticks.reverse(); - } - - // At this point, we need to update our max and min given the tick values since we have expanded the - // range of the scale - this.max = helpers.max(this.ticks); - this.min = helpers.min(this.ticks); - - if (this.options.ticks.reverse) { - this.ticks.reverse(); - - this.start = this.max; - this.end = this.min; - } else { - this.start = this.min; - this.end = this.max; - } - }, - getLabelForIndex: function(index, datasetIndex) { - return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); - }, - convertTicksToLabels: function() { - this.ticksAsNumbers = this.ticks.slice(); - this.zeroLineIndex = this.ticks.indexOf(0); - - Chart.Scale.prototype.convertTicksToLabels.call(this); - }, - // Utils - getPixelForValue: function(value, index, datasetIndex, includeOffset) { - // This must be called after fit has been run so that - // this.left, this.top, this.right, and this.bottom have been defined - var rightValue = +this.getRightValue(value); - var pixel; - var range = this.end - this.start; - - if (this.isHorizontal()) { - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - pixel = this.left + (innerWidth / range * (rightValue - this.start)); - return Math.round(pixel + this.paddingLeft); - } else { - var innerHeight = this.height - (this.paddingTop + this.paddingBottom); - pixel = (this.bottom - this.paddingBottom) - (innerHeight / range * (rightValue - this.start)); - return Math.round(pixel); - } - }, - getPixelForTick: function(index, includeOffset) { - return this.getPixelForValue(this.ticksAsNumbers[index], null, null, includeOffset); - } - }); - Chart.scaleService.registerScaleType("linear", LinearScale, defaultConfig); - -}; -},{}],39:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - var defaultConfig = { - position: "left", - - // label settings - ticks: { - callback: function(value, index, arr) { - var remain = value / (Math.pow(10, Math.floor(Chart.helpers.log10(value)))); - - if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === arr.length - 1) { - return value.toExponential(); - } else { - return ''; - } - } - } - }; - - var LogarithmicScale = Chart.Scale.extend({ - determineDataLimits: function() { - // Calculate Range - this.min = null; - this.max = null; - - if (this.options.stacked) { - var valuesPerType = {}; - - helpers.each(this.chart.data.datasets, function(dataset) { - if (helpers.isDatasetVisible(dataset) && (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id)) { - if (valuesPerType[dataset.type] === undefined) { - valuesPerType[dataset.type] = []; - } - - helpers.each(dataset.data, function(rawValue, index) { - var values = valuesPerType[dataset.type]; - var value = +this.getRightValue(rawValue); - if (isNaN(value)) { - return; - } - - values[index] = values[index] || 0; - - if (this.options.relativePoints) { - values[index] = 100; - } else { - // Don't need to split positive and negative since the log scale can't handle a 0 crossing - values[index] += value; - } - }, this); - } - }, this); - - helpers.each(valuesPerType, function(valuesForType) { - var minVal = helpers.min(valuesForType); - var maxVal = helpers.max(valuesForType); - this.min = this.min === null ? minVal : Math.min(this.min, minVal); - this.max = this.max === null ? maxVal : Math.max(this.max, maxVal); - }, this); - - } else { - helpers.each(this.chart.data.datasets, function(dataset) { - if (helpers.isDatasetVisible(dataset) && (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id)) { - helpers.each(dataset.data, function(rawValue, index) { - var value = +this.getRightValue(rawValue); - if (isNaN(value)) { - return; - } - - if (this.min === null) { - this.min = value; - } else if (value < this.min) { - this.min = value; - } - - if (this.max === null) { - this.max = value; - } else if (value > this.max) { - this.max = value; - } - }, this); - } - }, this); - } - - this.min = this.options.ticks.min !== undefined ? this.options.ticks.min : this.min; - this.max = this.options.ticks.max !== undefined ? this.options.ticks.max : this.max; - - if (this.min === this.max) { - if (this.min !== 0 && this.min !== null) { - this.min = Math.pow(10, Math.floor(helpers.log10(this.min)) - 1); - this.max = Math.pow(10, Math.floor(helpers.log10(this.max)) + 1); - } else { - this.min = 1; - this.max = 10; - } - } - }, - buildTicks: function() { - // Reset the ticks array. Later on, we will draw a grid line at these positions - // The array simply contains the numerical value of the spots where ticks will be - this.ticks = []; - - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - - var tickVal = this.options.ticks.min !== undefined ? this.options.ticks.min : Math.pow(10, Math.floor(helpers.log10(this.min))); - - while (tickVal < this.max) { - this.ticks.push(tickVal); - - var exp = Math.floor(helpers.log10(tickVal)); - var significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; - - if (significand === 10) { - significand = 1; - ++exp; - } - - tickVal = significand * Math.pow(10, exp); - } - - var lastTick = this.options.ticks.max !== undefined ? this.options.ticks.max : tickVal; - this.ticks.push(lastTick); - - if (this.options.position === "left" || this.options.position === "right") { - // We are in a vertical orientation. The top value is the highest. So reverse the array - this.ticks.reverse(); - } - - // At this point, we need to update our max and min given the tick values since we have expanded the - // range of the scale - this.max = helpers.max(this.ticks); - this.min = helpers.min(this.ticks); - - if (this.options.ticks.reverse) { - this.ticks.reverse(); - - this.start = this.max; - this.end = this.min; - } else { - this.start = this.min; - this.end = this.max; - } - }, - convertTicksToLabels: function() { - this.tickValues = this.ticks.slice(); - - Chart.Scale.prototype.convertTicksToLabels.call(this); - }, - // Get the correct tooltip label - getLabelForIndex: function(index, datasetIndex) { - return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); - }, - getPixelForTick: function(index, includeOffset) { - return this.getPixelForValue(this.tickValues[index], null, null, includeOffset); - }, - getPixelForValue: function(value, index, datasetIndex, includeOffset) { - var pixel; - - var newVal = +this.getRightValue(value); - var range = helpers.log10(this.end) - helpers.log10(this.start); - - if (this.isHorizontal()) { - - if (newVal === 0) { - pixel = this.left + this.paddingLeft; - } else { - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - pixel = this.left + (innerWidth / range * (helpers.log10(newVal) - helpers.log10(this.start))); - pixel += this.paddingLeft; - } - } else { - // Bottom - top since pixels increase downard on a screen - if (newVal === 0) { - pixel = this.top + this.paddingTop; - } else { - var innerHeight = this.height - (this.paddingTop + this.paddingBottom); - pixel = (this.bottom - this.paddingBottom) - (innerHeight / range * (helpers.log10(newVal) - helpers.log10(this.start))); - } - } - - return pixel; - } - - }); - Chart.scaleService.registerScaleType("logarithmic", LogarithmicScale, defaultConfig); - -}; -},{}],40:[function(require,module,exports){ -"use strict"; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - - var defaultConfig = { - display: true, - - //Boolean - Whether to animate scaling the chart from the centre - animate: true, - lineArc: false, - position: "chartArea", - - angleLines: { - display: true, - color: "rgba(0, 0, 0, 0.1)", - lineWidth: 1 - }, - - // label settings - ticks: { - //Boolean - Show a backdrop to the scale label - showLabelBackdrop: true, - - //String - The colour of the label backdrop - backdropColor: "rgba(255,255,255,0.75)", - - //Number - The backdrop padding above & below the label in pixels - backdropPaddingY: 2, - - //Number - The backdrop padding to the side of the label in pixels - backdropPaddingX: 2 - }, - - pointLabels: { - //Number - Point label font size in pixels - fontSize: 10, - - //Function - Used to convert point labels - callback: function(label) { - return label; - } - } - }; - - var LinearRadialScale = Chart.Scale.extend({ - getValueCount: function() { - return this.chart.data.labels.length; - }, - setDimensions: function() { - // Set the unconstrained dimension before label rotation - this.width = this.maxWidth; - this.height = this.maxHeight; - this.xCenter = Math.round(this.width / 2); - this.yCenter = Math.round(this.height / 2); - - var minSize = helpers.min([this.height, this.width]); - var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - this.drawingArea = (this.options.display) ? (minSize / 2) - (tickFontSize / 2 + this.options.ticks.backdropPaddingY) : (minSize / 2); - }, - determineDataLimits: function() { - this.min = null; - this.max = null; - - helpers.each(this.chart.data.datasets, function(dataset) { - if (helpers.isDatasetVisible(dataset)) { - helpers.each(dataset.data, function(rawValue, index) { - var value = +this.getRightValue(rawValue); - if (isNaN(value)) { - return; - } - - if (this.min === null) { - this.min = value; - } else if (value < this.min) { - this.min = value; - } - - if (this.max === null) { - this.max = value; - } else if (value > this.max) { - this.max = value; - } - }, this); - } - }, this); - - // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, - // do nothing since that would make the chart weird. If the user really wants a weird chart - // axis, they can manually override it - if (this.options.ticks.beginAtZero) { - var minSign = helpers.sign(this.min); - var maxSign = helpers.sign(this.max); - - if (minSign < 0 && maxSign < 0) { - // move the top up to 0 - this.max = 0; - } else if (minSign > 0 && maxSign > 0) { - // move the botttom down to 0 - this.min = 0; - } - } - - if (this.options.ticks.min !== undefined) { - this.min = this.options.ticks.min; - } else if (this.options.ticks.suggestedMin !== undefined) { - this.min = Math.min(this.min, this.options.ticks.suggestedMin); - } - - if (this.options.ticks.max !== undefined) { - this.max = this.options.ticks.max; - } else if (this.options.ticks.suggestedMax !== undefined) { - this.max = Math.max(this.max, this.options.ticks.suggestedMax); - } - - if (this.min === this.max) { - this.min--; - this.max++; - } - }, - buildTicks: function() { - - - this.ticks = []; - - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - var maxTicks = Math.min(this.options.ticks.maxTicksLimit ? this.options.ticks.maxTicksLimit : 11, Math.ceil(this.drawingArea / (1.5 * tickFontSize))); - maxTicks = Math.max(2, maxTicks); // Make sure we always have at least 2 ticks - - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. - - var niceRange = helpers.niceNum(this.max - this.min, false); - var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); - var niceMin = Math.floor(this.min / spacing) * spacing; - var niceMax = Math.ceil(this.max / spacing) * spacing; - - var numSpaces = Math.ceil((niceMax - niceMin) / spacing); - - // Put the values into the ticks array - this.ticks.push(this.options.ticks.min !== undefined ? this.options.ticks.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - this.ticks.push(niceMin + (j * spacing)); - } - this.ticks.push(this.options.ticks.max !== undefined ? this.options.ticks.max : niceMax); - - // At this point, we need to update our max and min given the tick values since we have expanded the - // range of the scale - this.max = helpers.max(this.ticks); - this.min = helpers.min(this.ticks); - - if (this.options.ticks.reverse) { - this.ticks.reverse(); - - this.start = this.max; - this.end = this.min; - } else { - this.start = this.min; - this.end = this.max; - } - - this.zeroLineIndex = this.ticks.indexOf(0); - }, - convertTicksToLabels: function() { - Chart.Scale.prototype.convertTicksToLabels.call(this); - - // Point labels - this.pointLabels = this.chart.data.labels.map(this.options.pointLabels.callback, this); - }, - getLabelForIndex: function(index, datasetIndex) { - return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); - }, - fit: function() { - /* - * Right, this is really confusing and there is a lot of maths going on here - * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 - * - * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif - * - * Solution: - * - * We assume the radius of the polygon is half the size of the canvas at first - * at each index we check if the text overlaps. - * - * Where it does, we store that angle and that index. - * - * After finding the largest index and angle we calculate how much we need to remove - * from the shape radius to move the point inwards by that x. - * - * We average the left and right distances to get the maximum shape radius that can fit in the box - * along with labels. - * - * Once we have that, we can find the centre point for the chart, by taking the x text protrusion - * on each side, removing that from the size, halving it and adding the left x protrusion width. - * - * This will mean we have a shape fitted to the canvas, as large as it can be with the labels - * and position it in the most space efficient manner - * - * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif - */ - - var pointLabelFontSize = helpers.getValueOrDefault(this.options.pointLabels.fontSize, Chart.defaults.global.defaultFontSize); - var pointLabeFontStyle = helpers.getValueOrDefault(this.options.pointLabels.fontStyle, Chart.defaults.global.defaultFontStyle); - var pointLabeFontFamily = helpers.getValueOrDefault(this.options.pointLabels.fontFamily, Chart.defaults.global.defaultFontFamily); - var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily); - - // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. - // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = helpers.min([(this.height / 2 - pointLabelFontSize - 5), this.width / 2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft, - maxWidthRadius; - this.ctx.font = pointLabeFont; - - for (i = 0; i < this.getValueCount(); i++) { - // 5px to space the text slightly out - similar to what we do in the draw function. - pointPosition = this.getPointPosition(i, largestPossibleRadius); - textWidth = this.ctx.measureText(this.pointLabels[i] ? this.pointLabels[i] : '').width + 5; - if (i === 0 || i === this.getValueCount() / 2) { - // If we're at index zero, or exactly the middle, we're at exactly the top/bottom - // of the radar chart, so text will be aligned centrally, so we'll half it and compare - // w/left and right text sizes - halfTextWidth = textWidth / 2; - if (pointPosition.x + halfTextWidth > furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } else if (i < this.getValueCount() / 2) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - } else if (i > this.getValueCount() / 2) { - // More than half the values means we'll right align the text - if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } - } - } - - xProtrusionLeft = furthestLeft; - xProtrusionRight = Math.ceil(furthestRight - this.width); - - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); - - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI / 2); - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI / 2); - - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (helpers.isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (helpers.isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2); - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); - }, - setCenterPoint: function(leftMovement, rightMovement) { - - var maxRight = this.width - rightMovement - this.drawingArea, - maxLeft = leftMovement + this.drawingArea; - - this.xCenter = Math.round(((maxLeft + maxRight) / 2) + this.left); - // Always vertically in the centre as the text height doesn't change - this.yCenter = Math.round((this.height / 2) + this.top); - }, - - getIndexAngle: function(index) { - var angleMultiplier = (Math.PI * 2) / this.getValueCount(); - // Start from the top instead of right, so remove a quarter of the circle - - return index * angleMultiplier - (Math.PI / 2); - }, - getDistanceFromCenterForValue: function(value) { - if (value === null) { - return 0; // null always in center - } - - // Take into account half font size + the yPadding of the top value - var scalingFactor = this.drawingArea / (this.max - this.min); - if (this.options.reverse) { - return (this.max - value) * scalingFactor; - } else { - return (value - this.min) * scalingFactor; - } - }, - getPointPosition: function(index, distanceFromCenter) { - var thisAngle = this.getIndexAngle(index); - return { - x: Math.round(Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, - y: Math.round(Math.sin(thisAngle) * distanceFromCenter) + this.yCenter - }; - }, - getPointPositionForValue: function(index, value) { - return this.getPointPosition(index, this.getDistanceFromCenterForValue(value)); - }, - draw: function() { - if (this.options.display) { - var ctx = this.ctx; - helpers.each(this.ticks, function(label, index) { - // Don't draw a centre value (if it is minimum) - if (index > 0 || this.options.reverse) { - var yCenterOffset = this.getDistanceFromCenterForValue(this.ticks[index]); - var yHeight = this.yCenter - yCenterOffset; - - // Draw circular lines around the scale - if (this.options.gridLines.display) { - ctx.strokeStyle = this.options.gridLines.color; - ctx.lineWidth = this.options.gridLines.lineWidth; - - if (this.options.lineArc) { - // Draw circular arcs between the points - ctx.beginPath(); - ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - } else { - // Draw straight lines connecting each index - ctx.beginPath(); - for (var i = 0; i < this.getValueCount(); i++) { - var pointPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.ticks[index])); - if (i === 0) { - ctx.moveTo(pointPosition.x, pointPosition.y); - } else { - ctx.lineTo(pointPosition.x, pointPosition.y); - } - } - ctx.closePath(); - ctx.stroke(); - } - } - - if (this.options.ticks.display) { - var tickFontColor = helpers.getValueOrDefault(this.options.ticks.fontColor, Chart.defaults.global.defaultFontColor); - var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(this.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(this.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - ctx.font = tickLabelFont; - - if (this.options.ticks.showLabelBackdrop) { - var labelWidth = ctx.measureText(label).width; - ctx.fillStyle = this.options.ticks.backdropColor; - ctx.fillRect( - this.xCenter - labelWidth / 2 - this.options.ticks.backdropPaddingX, - yHeight - tickFontSize / 2 - this.options.ticks.backdropPaddingY, - labelWidth + this.options.ticks.backdropPaddingX * 2, - tickFontSize + this.options.ticks.backdropPaddingY * 2 - ); - } - - ctx.textAlign = 'center'; - ctx.textBaseline = "middle"; - ctx.fillStyle = tickFontColor; - ctx.fillText(label, this.xCenter, yHeight); - } - } - }, this); - - if (!this.options.lineArc) { - ctx.lineWidth = this.options.angleLines.lineWidth; - ctx.strokeStyle = this.options.angleLines.color; - - for (var i = this.getValueCount() - 1; i >= 0; i--) { - if (this.options.angleLines.display) { - var outerPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.options.reverse ? this.min : this.max)); - ctx.beginPath(); - ctx.moveTo(this.xCenter, this.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.options.reverse ? this.min : this.max) + 5); - - var pointLabelFontColor = helpers.getValueOrDefault(this.options.pointLabels.fontColor, Chart.defaults.global.defaultFontColor); - var pointLabelFontSize = helpers.getValueOrDefault(this.options.pointLabels.fontSize, Chart.defaults.global.defaultFontSize); - var pointLabeFontStyle = helpers.getValueOrDefault(this.options.pointLabels.fontStyle, Chart.defaults.global.defaultFontStyle); - var pointLabeFontFamily = helpers.getValueOrDefault(this.options.pointLabels.fontFamily, Chart.defaults.global.defaultFontFamily); - var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily); - - ctx.font = pointLabeFont; - ctx.fillStyle = pointLabelFontColor; - - var labelsCount = this.pointLabels.length, - halfLabelsCount = this.pointLabels.length / 2, - quarterLabelsCount = halfLabelsCount / 2, - upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), - exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); - if (i === 0) { - ctx.textAlign = 'center'; - } else if (i === halfLabelsCount) { - ctx.textAlign = 'center'; - } else if (i < halfLabelsCount) { - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (exactQuarter) { - ctx.textBaseline = 'middle'; - } else if (upperHalf) { - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(this.pointLabels[i] ? this.pointLabels[i] : '', pointLabelPosition.x, pointLabelPosition.y); - } - } - } - } - }); - Chart.scaleService.registerScaleType("radialLinear", LinearRadialScale, defaultConfig); - -}; -},{}],41:[function(require,module,exports){ -/*global window: false */ -"use strict"; - -var moment = require('moment'); -moment = typeof(moment) === 'function' ? moment : window.moment; - -module.exports = function(Chart) { - - var helpers = Chart.helpers; - var time = { - units: [{ - name: 'millisecond', - steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] - }, { - name: 'second', - steps: [1, 2, 5, 10, 30] - }, { - name: 'minute', - steps: [1, 2, 5, 10, 30] - }, { - name: 'hour', - steps: [1, 2, 3, 6, 12] - }, { - name: 'day', - steps: [1, 2, 5] - }, { - name: 'week', - maxStep: 4 - }, { - name: 'month', - maxStep: 3 - }, { - name: 'quarter', - maxStep: 4 - }, { - name: 'year', - maxStep: false - }] - }; - - var defaultConfig = { - position: "bottom", - - time: { - parser: false, // false == a pattern string from http://momentjs.com/docs/#/parsing/string-format/ or a custom callback that converts its argument to a moment - format: false, // DEPRECATED false == date objects, moment object, callback or a pattern string from http://momentjs.com/docs/#/parsing/string-format/ - unit: false, // false == automatic or override with week, month, year, etc. - round: false, // none, or override with week, month, year, etc. - displayFormat: false, // DEPRECATED - - // defaults to unit's corresponding unitFormat below or override using pattern string from http://momentjs.com/docs/#/displaying/format/ - displayFormats: { - 'millisecond': 'h:mm:ss.SSS a', // 11:20:01.123 AM, - 'second': 'h:mm:ss a', // 11:20:01 AM - 'minute': 'h:mm:ss a', // 11:20:01 AM - 'hour': 'MMM D, hA', // Sept 4, 5PM - 'day': 'll', // Sep 4 2015 - 'week': 'll', // Week 46, or maybe "[W]WW - YYYY" ? - 'month': 'MMM YYYY', // Sept 2015 - 'quarter': '[Q]Q - YYYY', // Q3 - 'year': 'YYYY' // 2015 - } - }, - ticks: { - autoSkip: false - } - }; - - var TimeScale = Chart.Scale.extend({ - initialize: function() { - if (!moment) { - throw new Error('Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com'); - } - - Chart.Scale.prototype.initialize.call(this); - }, - getLabelMoment: function(datasetIndex, index) { - return this.labelMoments[datasetIndex][index]; - }, - determineDataLimits: function() { - this.labelMoments = []; - - // Only parse these once. If the dataset does not have data as x,y pairs, we will use - // these - var scaleLabelMoments = []; - if (this.chart.data.labels && this.chart.data.labels.length > 0) { - helpers.each(this.chart.data.labels, function(label, index) { - var labelMoment = this.parseTime(label); - if (this.options.time.round) { - labelMoment.startOf(this.options.time.round); - } - scaleLabelMoments.push(labelMoment); - }, this); - - this.firstTick = moment.min.call(this, scaleLabelMoments); - this.lastTick = moment.max.call(this, scaleLabelMoments); - } else { - this.firstTick = null; - this.lastTick = null; - } - - helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { - var momentsForDataset = []; - - if (typeof dataset.data[0] === 'object') { - helpers.each(dataset.data, function(value, index) { - var labelMoment = this.parseTime(this.getRightValue(value)); - if (this.options.time.round) { - labelMoment.startOf(this.options.time.round); - } - momentsForDataset.push(labelMoment); - - // May have gone outside the scale ranges, make sure we keep the first and last ticks updated - this.firstTick = this.firstTick !== null ? moment.min(this.firstTick, labelMoment) : labelMoment; - this.lastTick = this.lastTick !== null ? moment.max(this.lastTick, labelMoment) : labelMoment; - }, this); - } else { - // We have no labels. Use the ones from the scale - momentsForDataset = scaleLabelMoments; - } - - this.labelMoments.push(momentsForDataset); - }, this); - - // Set these after we've done all the data - if (this.options.time.min) { - this.firstTick = this.parseTime(this.options.time.min); - } - - if (this.options.time.max) { - this.lastTick = this.parseTime(this.options.time.max); - } - - // We will modify these, so clone for later - this.firstTick = (this.firstTick || moment()).clone(); - this.lastTick = (this.lastTick || moment()).clone(); - }, - buildTicks: function(index) { - - this.ctx.save(); - var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(this.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(this.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - this.ctx.font = tickLabelFont; - - this.ticks = []; - this.unitScale = 1; // How much we scale the unit by, ie 2 means 2x unit per step - this.scaleSizeInUnits = 0; // How large the scale is in the base unit (seconds, minutes, etc) - - // Set unit override if applicable - if (this.options.time.unit) { - this.tickUnit = this.options.time.unit || 'day'; - this.displayFormat = this.options.time.displayFormats[this.tickUnit]; - this.scaleSizeInUnits = this.lastTick.diff(this.firstTick, this.tickUnit, true); - this.unitScale = helpers.getValueOrDefault(this.options.time.unitStepSize, 1); - } else { - // Determine the smallest needed unit of the time - var innerWidth = this.isHorizontal() ? this.width - (this.paddingLeft + this.paddingRight) : this.height - (this.paddingTop + this.paddingBottom); - - // Crude approximation of what the label length might be - var tempFirstLabel = this.tickFormatFunction(this.firstTick, 0, []); - var tickLabelWidth = this.ctx.measureText(tempFirstLabel).width; - var cosRotation = Math.cos(helpers.toRadians(this.options.ticks.maxRotation)); - var sinRotation = Math.sin(helpers.toRadians(this.options.ticks.maxRotation)); - tickLabelWidth = (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation); - var labelCapacity = innerWidth / (tickLabelWidth); - - // Start as small as possible - this.tickUnit = 'millisecond'; - this.scaleSizeInUnits = this.lastTick.diff(this.firstTick, this.tickUnit, true); - this.displayFormat = this.options.time.displayFormats[this.tickUnit]; - - var unitDefinitionIndex = 0; - var unitDefinition = time.units[unitDefinitionIndex]; - - // While we aren't ideal and we don't have units left - while (unitDefinitionIndex < time.units.length) { - // Can we scale this unit. If `false` we can scale infinitely - this.unitScale = 1; - - if (helpers.isArray(unitDefinition.steps) && Math.ceil(this.scaleSizeInUnits / labelCapacity) < helpers.max(unitDefinition.steps)) { - // Use one of the prefedined steps - for (var idx = 0; idx < unitDefinition.steps.length; ++idx) { - if (unitDefinition.steps[idx] >= Math.ceil(this.scaleSizeInUnits / labelCapacity)) { - this.unitScale = helpers.getValueOrDefault(this.options.time.unitStepSize, unitDefinition.steps[idx]); - break; - } - } - - break; - } else if ((unitDefinition.maxStep === false) || (Math.ceil(this.scaleSizeInUnits / labelCapacity) < unitDefinition.maxStep)) { - // We have a max step. Scale this unit - this.unitScale = helpers.getValueOrDefault(this.options.time.unitStepSize, Math.ceil(this.scaleSizeInUnits / labelCapacity)); - break; - } else { - // Move to the next unit up - ++unitDefinitionIndex; - unitDefinition = time.units[unitDefinitionIndex]; - - this.tickUnit = unitDefinition.name; - this.scaleSizeInUnits = this.lastTick.diff(this.firstTick, this.tickUnit, true); - this.displayFormat = this.options.time.displayFormats[unitDefinition.name]; - } - } - } - - var roundedStart; - - // Only round the first tick if we have no hard minimum - if (!this.options.time.min) { - this.firstTick.startOf(this.tickUnit); - roundedStart = this.firstTick; - } else { - roundedStart = this.firstTick.clone().startOf(this.tickUnit); - } - - // Only round the last tick if we have no hard maximum - if (!this.options.time.max) { - this.lastTick.endOf(this.tickUnit); - } - - this.smallestLabelSeparation = this.width; - - helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { - for (var i = 1; i < this.labelMoments[datasetIndex].length; i++) { - this.smallestLabelSeparation = Math.min(this.smallestLabelSeparation, this.labelMoments[datasetIndex][i].diff(this.labelMoments[datasetIndex][i - 1], this.tickUnit, true)); - } - }, this); - - // Tick displayFormat override - if (this.options.time.displayFormat) { - this.displayFormat = this.options.time.displayFormat; - } - - // first tick. will have been rounded correctly if options.time.min is not specified - this.ticks.push(this.firstTick.clone()); - - // For every unit in between the first and last moment, create a moment and add it to the ticks tick - for (var i = 1; i < this.scaleSizeInUnits; ++i) { - var newTick = roundedStart.clone().add(i, this.tickUnit); - - // Are we greater than the max time - if (this.options.time.max && newTick.diff(this.lastTick, this.tickUnit, true) >= 0) { - break; - } - - if (i % this.unitScale === 0) { - this.ticks.push(newTick); - } - } - - // Always show the right tick - if (this.ticks[this.ticks.length - 1].diff(this.lastTick, this.tickUnit) !== 0 || this.scaleSizeInUnits === 0) { - // this is a weird case. If the option is the same as the end option, we can't just diff the times because the tick was created from the roundedStart - // but the last tick was not rounded. - if (this.options.time.max) { - this.ticks.push(this.lastTick.clone()); - this.scaleSizeInUnits = this.lastTick.diff(this.ticks[0], this.tickUnit, true); - } else { - this.scaleSizeInUnits = Math.ceil(this.scaleSizeInUnits / this.unitScale) * this.unitScale; - this.ticks.push(this.firstTick.clone().add(this.scaleSizeInUnits, this.tickUnit)); - this.lastTick = this.ticks[this.ticks.length - 1].clone(); - } - } - this.ctx.restore(); - }, - // Get tooltip label - getLabelForIndex: function(index, datasetIndex) { - var label = this.chart.data.labels && index < this.chart.data.labels.length ? this.chart.data.labels[index] : ''; - - if (typeof this.chart.data.datasets[datasetIndex].data[0] === 'object') { - label = this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); - } - - // Format nicely - if (this.options.time.tooltipFormat) { - label = this.parseTime(label).format(this.options.time.tooltipFormat); - } - - return label; - }, - // Function to format an individual tick mark - tickFormatFunction: function tickFormatFunction(tick, index, ticks) { - var formattedTick = tick.format(this.displayFormat); - - if (this.options.ticks.userCallback) { - return this.options.ticks.userCallback(formattedTick, index, ticks); - } else { - return formattedTick; - } - }, - convertTicksToLabels: function() { - this.ticks = this.ticks.map(this.tickFormatFunction, this); - }, - getPixelForValue: function(value, index, datasetIndex, includeOffset) { - var labelMoment = this.getLabelMoment(datasetIndex, index); - - if (labelMoment) { - var offset = labelMoment.diff(this.firstTick, this.tickUnit, true); - - var decimal = offset / this.scaleSizeInUnits; - - if (this.isHorizontal()) { - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - var valueWidth = innerWidth / Math.max(this.ticks.length - 1, 1); - var valueOffset = (innerWidth * decimal) + this.paddingLeft; - - return this.left + Math.round(valueOffset); - } else { - var innerHeight = this.height - (this.paddingTop + this.paddingBottom); - var valueHeight = innerHeight / Math.max(this.ticks.length - 1, 1); - var heightOffset = (innerHeight * decimal) + this.paddingTop; - - return this.top + Math.round(heightOffset); - } - } - }, - parseTime: function(label) { - if (typeof this.options.time.parser === 'string') { - return moment(label, this.options.time.parser); - } - if (typeof this.options.time.parser === 'function') { - return this.options.time.parser(label); - } - // Date objects - if (typeof label.getMonth === 'function' || typeof label === 'number') { - return moment(label); - } - // Moment support - if (label.isValid && label.isValid()) { - return label; - } - // Custom parsing (return an instance of moment) - if (typeof this.options.time.format !== 'string' && this.options.time.format.call) { - console.warn("options.time.format is deprecated and replaced by options.time.parser. See http://nnnick.github.io/Chart.js/docs-v2/#scales-time-scale"); - return this.options.time.format(label); - } - // Moment format parsing - return moment(label, this.options.time.format); - } - }); - Chart.scaleService.registerScaleType("time", TimeScale, defaultConfig); - -}; - -},{"moment":1}]},{},[7]); diff --git a/onadata/apps/survey_report/static/js/Chart.min.js b/onadata/apps/survey_report/static/js/Chart.min.js deleted file mode 100644 index b8e77fe8b..000000000 --- a/onadata/apps/survey_report/static/js/Chart.min.js +++ /dev/null @@ -1,14 +0,0 @@ -!function t(e,i,a){function s(n,r){if(!i[n]){if(!e[n]){var l="function"==typeof require&&require;if(!r&&l)return l(n,!0);if(o)return o(n,!0);var h=new Error("Cannot find module '"+n+"'");throw h.code="MODULE_NOT_FOUND",h}var c=i[n]={exports:{}};e[n][0].call(c.exports,function(t){var i=e[n][1][t];return s(i?i:t)},c,c.exports,t,e,i,a)}return i[n].exports}for(var o="function"==typeof require&&require,n=0;ne&&(e+=360),a=(r+l)/2,i=l==r?0:.5>=a?h/(l+r):h/(2-l-r),[e,100*i,100*a]}function s(t){var e,i,a,s=t[0],o=t[1],n=t[2],r=Math.min(s,o,n),l=Math.max(s,o,n),h=l-r;return i=0==l?0:h/l*1e3/10,l==r?e=0:s==l?e=(o-n)/h:o==l?e=2+(n-s)/h:n==l&&(e=4+(s-o)/h),e=Math.min(60*e,360),0>e&&(e+=360),a=l/255*1e3/10,[e,i,a]}function o(t){var e=t[0],i=t[1],s=t[2],o=a(t)[0],n=1/255*Math.min(e,Math.min(i,s)),s=1-1/255*Math.max(e,Math.max(i,s));return[o,100*n,100*s]}function n(t){var e,i,a,s,o=t[0]/255,n=t[1]/255,r=t[2]/255;return s=Math.min(1-o,1-n,1-r),e=(1-o-s)/(1-s)||0,i=(1-n-s)/(1-s)||0,a=(1-r-s)/(1-s)||0,[100*e,100*i,100*a,100*s]}function l(t){return G[JSON.stringify(t)]}function h(t){var e=t[0]/255,i=t[1]/255,a=t[2]/255;e=e>.04045?Math.pow((e+.055)/1.055,2.4):e/12.92,i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92,a=a>.04045?Math.pow((a+.055)/1.055,2.4):a/12.92;var s=.4124*e+.3576*i+.1805*a,o=.2126*e+.7152*i+.0722*a,n=.0193*e+.1192*i+.9505*a;return[100*s,100*o,100*n]}function c(t){var e,i,a,s=h(t),o=s[0],n=s[1],r=s[2];return o/=95.047,n/=100,r/=108.883,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,n=n>.008856?Math.pow(n,1/3):7.787*n+16/116,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,e=116*n-16,i=500*(o-n),a=200*(n-r),[e,i,a]}function d(t){return W(c(t))}function u(t){var e,i,a,s,o,n=t[0]/360,r=t[1]/100,l=t[2]/100;if(0==r)return o=255*l,[o,o,o];i=.5>l?l*(1+r):l+r-l*r,e=2*l-i,s=[0,0,0];for(var h=0;3>h;h++)a=n+1/3*-(h-1),0>a&&a++,a>1&&a--,o=1>6*a?e+6*(i-e)*a:1>2*a?i:2>3*a?e+(i-e)*(2/3-a)*6:e,s[h]=255*o;return s}function f(t){var e,i,a=t[0],s=t[1]/100,o=t[2]/100;return 0===o?[0,0,0]:(o*=2,s*=1>=o?o:2-o,i=(o+s)/2,e=2*s/(o+s),[a,100*e,100*i])}function m(t){return o(u(t))}function p(t){return n(u(t))}function x(t){return l(u(t))}function v(t){var e=t[0]/60,i=t[1]/100,a=t[2]/100,s=Math.floor(e)%6,o=e-Math.floor(e),n=255*a*(1-i),r=255*a*(1-i*o),l=255*a*(1-i*(1-o)),a=255*a;switch(s){case 0:return[a,l,n];case 1:return[r,a,n];case 2:return[n,a,l];case 3:return[n,r,a];case 4:return[l,n,a];case 5:return[a,n,r]}}function y(t){var e,i,a=t[0],s=t[1]/100,o=t[2]/100;return i=(2-s)*o,e=s*o,e/=1>=i?i:2-i,e=e||0,i/=2,[a,100*e,100*i]}function k(t){return o(v(t))}function D(t){return n(v(t))}function S(t){return l(v(t))}function C(t){var e,i,a,s,o=t[0]/360,n=t[1]/100,l=t[2]/100,h=n+l;switch(h>1&&(n/=h,l/=h),e=Math.floor(6*o),i=1-l,a=6*o-e,0!=(1&e)&&(a=1-a),s=n+a*(i-n),e){default:case 6:case 0:r=i,g=s,b=n;break;case 1:r=s,g=i,b=n;break;case 2:r=n,g=i,b=s;break;case 3:r=n,g=s,b=i;break;case 4:r=s,g=n,b=i;break;case 5:r=i,g=n,b=s}return[255*r,255*g,255*b]}function w(t){return a(C(t))}function _(t){return s(C(t))}function A(t){return n(C(t))}function I(t){return l(C(t))}function M(t){var e,i,a,s=t[0]/100,o=t[1]/100,n=t[2]/100,r=t[3]/100;return e=1-Math.min(1,s*(1-r)+r),i=1-Math.min(1,o*(1-r)+r),a=1-Math.min(1,n*(1-r)+r),[255*e,255*i,255*a]}function F(t){return a(M(t))}function P(t){return s(M(t))}function T(t){return o(M(t))}function V(t){return l(M(t))}function R(t){var e,i,a,s=t[0]/100,o=t[1]/100,n=t[2]/100;return e=3.2406*s+-1.5372*o+n*-.4986,i=s*-.9689+1.8758*o+.0415*n,a=.0557*s+o*-.204+1.057*n,e=e>.0031308?1.055*Math.pow(e,1/2.4)-.055:e=12.92*e,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i=12.92*i,a=a>.0031308?1.055*Math.pow(a,1/2.4)-.055:a=12.92*a,e=Math.min(Math.max(0,e),1),i=Math.min(Math.max(0,i),1),a=Math.min(Math.max(0,a),1),[255*e,255*i,255*a]}function O(t){var e,i,a,s=t[0],o=t[1],n=t[2];return s/=95.047,o/=100,n/=108.883,s=s>.008856?Math.pow(s,1/3):7.787*s+16/116,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,n=n>.008856?Math.pow(n,1/3):7.787*n+16/116,e=116*o-16,i=500*(s-o),a=200*(o-n),[e,i,a]}function L(t){return W(O(t))}function z(t){var e,i,a,s,o=t[0],n=t[1],r=t[2];return 8>=o?(i=100*o/903.3,s=7.787*(i/100)+16/116):(i=100*Math.pow((o+16)/116,3),s=Math.pow(i/100,1/3)),e=.008856>=e/95.047?e=95.047*(n/500+s-16/116)/7.787:95.047*Math.pow(n/500+s,3),a=.008859>=a/108.883?a=108.883*(s-r/200-16/116)/7.787:108.883*Math.pow(s-r/200,3),[e,i,a]}function W(t){var e,i,a,s=t[0],o=t[1],n=t[2];return e=Math.atan2(n,o),i=360*e/2/Math.PI,0>i&&(i+=360),a=Math.sqrt(o*o+n*n),[s,a,i]}function B(t){return R(z(t))}function N(t){var e,i,a,s=t[0],o=t[1],n=t[2];return a=n/360*2*Math.PI,e=o*Math.cos(a),i=o*Math.sin(a),[s,e,i]}function H(t){return z(N(t))}function E(t){return B(N(t))}function U(t){return X[t]}function j(t){return a(U(t))}function q(t){return s(U(t))}function Y(t){return o(U(t))}function Q(t){return n(U(t))}function J(t){return c(U(t))}function Z(t){return h(U(t))}e.exports={rgb2hsl:a,rgb2hsv:s,rgb2hwb:o,rgb2cmyk:n,rgb2keyword:l,rgb2xyz:h,rgb2lab:c,rgb2lch:d,hsl2rgb:u,hsl2hsv:f,hsl2hwb:m,hsl2cmyk:p,hsl2keyword:x,hsv2rgb:v,hsv2hsl:y,hsv2hwb:k,hsv2cmyk:D,hsv2keyword:S,hwb2rgb:C,hwb2hsl:w,hwb2hsv:_,hwb2cmyk:A,hwb2keyword:I,cmyk2rgb:M,cmyk2hsl:F,cmyk2hsv:P,cmyk2hwb:T,cmyk2keyword:V,keyword2rgb:U,keyword2hsl:j,keyword2hsv:q,keyword2hwb:Y,keyword2cmyk:Q,keyword2lab:J,keyword2xyz:Z,xyz2rgb:R,xyz2lab:O,xyz2lch:L,lab2xyz:z,lab2rgb:B,lab2lch:W,lch2lab:N,lch2xyz:H,lch2rgb:E};var X={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},G={};for(var $ in X)G[JSON.stringify(X[$])]=$},{}],3:[function(t,e,i){var a=t("./conversions"),s=function(){return new h};for(var o in a){s[o+"Raw"]=function(t){return function(e){return"number"==typeof e&&(e=Array.prototype.slice.call(arguments)),a[t](e)}}(o);var n=/(\w+)2(\w+)/.exec(o),r=n[1],l=n[2];s[r]=s[r]||{},s[r][l]=s[o]=function(t){return function(e){"number"==typeof e&&(e=Array.prototype.slice.call(arguments));var i=a[t](e);if("string"==typeof i||void 0===i)return i;for(var s=0;se||t[3]&&t[3]<1?d(t,e):"rgb("+t[0]+", "+t[1]+", "+t[2]+")"}function d(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"rgba("+t[0]+", "+t[1]+", "+t[2]+", "+e+")"}function u(t,e){if(1>e||t[3]&&t[3]<1)return g(t,e);var i=Math.round(t[0]/255*100),a=Math.round(t[1]/255*100),s=Math.round(t[2]/255*100);return"rgb("+i+"%, "+a+"%, "+s+"%)"}function g(t,e){var i=Math.round(t[0]/255*100),a=Math.round(t[1]/255*100),s=Math.round(t[2]/255*100);return"rgba("+i+"%, "+a+"%, "+s+"%, "+(e||t[3]||1)+")"}function f(t,e){return 1>e||t[3]&&t[3]<1?m(t,e):"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"}function m(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hsla("+t[0]+", "+t[1]+"%, "+t[2]+"%, "+e+")"}function p(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"}function b(t){return k[t.slice(0,3)]}function x(t,e,i){return Math.min(Math.max(e,t),i)}function v(t){var e=t.toString(16).toUpperCase();return e.length<2?"0"+e:e}var y=t("color-name");e.exports={getRgba:a,getHsla:s,getRgb:n,getHsl:r,getHwb:o,getAlpha:l,hexString:h,rgbString:c,rgbaString:d,percentString:u,percentaString:g,hslString:f,hslaString:m,hwbString:p,keyword:b};var k={};for(var D in y)k[y[D]]=D},{"color-name":6}],5:[function(t,e,i){var a=t("color-convert"),s=t("color-string"),o=function(t){if(t instanceof o)return t;if(!(this instanceof o))return new o(t);if(this.values={rgb:[0,0,0],hsl:[0,0,0],hsv:[0,0,0],hwb:[0,0,0],cmyk:[0,0,0,0],alpha:1},"string"==typeof t){var e=s.getRgba(t);if(e)this.setValues("rgb",e);else if(e=s.getHsla(t))this.setValues("hsl",e);else{if(!(e=s.getHwb(t)))throw new Error('Unable to parse color from string "'+t+'"');this.setValues("hwb",e)}}else if("object"==typeof t){var e=t;if(void 0!==e.r||void 0!==e.red)this.setValues("rgb",e);else if(void 0!==e.l||void 0!==e.lightness)this.setValues("hsl",e);else if(void 0!==e.v||void 0!==e.value)this.setValues("hsv",e);else if(void 0!==e.w||void 0!==e.whiteness)this.setValues("hwb",e);else{if(void 0===e.c&&void 0===e.cyan)throw new Error("Unable to parse color from object "+JSON.stringify(t));this.setValues("cmyk",e)}}};o.prototype={rgb:function(t){return this.setSpace("rgb",arguments)},hsl:function(t){return this.setSpace("hsl",arguments)},hsv:function(t){return this.setSpace("hsv",arguments)},hwb:function(t){return this.setSpace("hwb",arguments)},cmyk:function(t){return this.setSpace("cmyk",arguments)},rgbArray:function(){return this.values.rgb},hslArray:function(){return this.values.hsl},hsvArray:function(){return this.values.hsv},hwbArray:function(){return 1!==this.values.alpha?this.values.hwb.concat([this.values.alpha]):this.values.hwb},cmykArray:function(){return this.values.cmyk},rgbaArray:function(){var t=this.values.rgb;return t.concat([this.values.alpha])},hslaArray:function(){var t=this.values.hsl;return t.concat([this.values.alpha])},alpha:function(t){return void 0===t?this.values.alpha:(this.setValues("alpha",t),this)},red:function(t){return this.setChannel("rgb",0,t)},green:function(t){return this.setChannel("rgb",1,t)},blue:function(t){return this.setChannel("rgb",2,t)},hue:function(t){return this.setChannel("hsl",0,t)},saturation:function(t){return this.setChannel("hsl",1,t)},lightness:function(t){return this.setChannel("hsl",2,t)},saturationv:function(t){return this.setChannel("hsv",1,t)},whiteness:function(t){return this.setChannel("hwb",1,t)},blackness:function(t){return this.setChannel("hwb",2,t)},value:function(t){return this.setChannel("hsv",2,t)},cyan:function(t){return this.setChannel("cmyk",0,t)},magenta:function(t){return this.setChannel("cmyk",1,t)},yellow:function(t){return this.setChannel("cmyk",2,t)},black:function(t){return this.setChannel("cmyk",3,t)},hexString:function(){return s.hexString(this.values.rgb)},rgbString:function(){return s.rgbString(this.values.rgb,this.values.alpha)},rgbaString:function(){return s.rgbaString(this.values.rgb,this.values.alpha)},percentString:function(){return s.percentString(this.values.rgb,this.values.alpha)},hslString:function(){return s.hslString(this.values.hsl,this.values.alpha)},hslaString:function(){return s.hslaString(this.values.hsl,this.values.alpha)},hwbString:function(){return s.hwbString(this.values.hwb,this.values.alpha)},keyword:function(){return s.keyword(this.values.rgb,this.values.alpha)},rgbNumber:function(){return this.values.rgb[0]<<16|this.values.rgb[1]<<8|this.values.rgb[2]},luminosity:function(){for(var t=this.values.rgb,e=[],i=0;i=a?a/12.92:Math.pow((a+.055)/1.055,2.4)}return.2126*e[0]+.7152*e[1]+.0722*e[2]},contrast:function(t){var e=this.luminosity(),i=t.luminosity();return e>i?(e+.05)/(i+.05):(i+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb,e=(299*t[0]+587*t[1]+114*t[2])/1e3;return 128>e},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;3>e;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){return this.values.hsl[2]+=this.values.hsl[2]*t,this.setValues("hsl",this.values.hsl),this},darken:function(t){return this.values.hsl[2]-=this.values.hsl[2]*t,this.setValues("hsl",this.values.hsl),this},saturate:function(t){return this.values.hsl[1]+=this.values.hsl[1]*t,this.setValues("hsl",this.values.hsl),this},desaturate:function(t){return this.values.hsl[1]-=this.values.hsl[1]*t,this.setValues("hsl",this.values.hsl),this},whiten:function(t){return this.values.hwb[1]+=this.values.hwb[1]*t,this.setValues("hwb",this.values.hwb),this},blacken:function(t){return this.values.hwb[2]+=this.values.hwb[2]*t,this.setValues("hwb",this.values.hwb),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){return this.setValues("alpha",this.values.alpha-this.values.alpha*t),this},opaquer:function(t){return this.setValues("alpha",this.values.alpha+this.values.alpha*t),this},rotate:function(t){var e=this.values.hsl[0];return e=(e+t)%360,e=0>e?360+e:e,this.values.hsl[0]=e,this.setValues("hsl",this.values.hsl),this},mix:function(t,e){e=1-(null==e?.5:e);for(var i=2*e-1,a=this.alpha()-t.alpha(),s=((i*a==-1?i:(i+a)/(1+i*a))+1)/2,o=1-s,n=this.rgbArray(),r=t.rgbArray(),l=0;l0&&r.max>0?r.getPixelForValue(r.min):r.getPixelForValue(0),e.extend(t,{_chart:this.chart.chart,_xScale:n,_yScale:r,_datasetIndex:this.index,_index:i,_model:{x:this.calculateBarX(i,this.index),y:a?o:this.calculateBarY(i,this.index),label:this.chart.data.labels[i],datasetLabel:this.getDataset().label,base:a?o:this.calculateBarBase(this.index,i),width:this.calculateBarWidth(s),backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.rectangle.backgroundColor),borderSkipped:t.custom&&t.custom.borderSkipped?t.custom.borderSkipped:this.chart.options.elements.rectangle.borderSkipped,borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.rectangle.borderColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.rectangle.borderWidth)}}),t.pivot()},calculateBarBase:function(t,i){var a=(this.getScaleForId(this.getDataset().xAxisID),this.getScaleForId(this.getDataset().yAxisID)),s=0;if(a.options.stacked){var o=this.chart.data.datasets[t].data[i];if(0>o)for(var n=0;t>n;n++){var r=this.chart.data.datasets[n];e.isDatasetVisible(r)&&r.yAxisID===a.id&&r.bar&&(s+=r.data[i]<0?r.data[i]:0)}else for(var l=0;t>l;l++){var h=this.chart.data.datasets[l];e.isDatasetVisible(h)&&h.yAxisID===a.id&&h.bar&&(s+=h.data[i]>0?h.data[i]:0)}return a.getPixelForValue(s)}return s=a.getPixelForValue(a.min),a.beginAtZero||a.min<=0&&a.max>=0||a.min>=0&&a.max<=0?s=a.getPixelForValue(0,0):a.min<0&&a.max<0&&(s=a.getPixelForValue(a.max)),s},getRuler:function(){var t=this.getScaleForId(this.getDataset().xAxisID),e=(this.getScaleForId(this.getDataset().yAxisID),this.getBarCount()),i=function(){for(var e=t.getPixelForTick(1)-t.getPixelForTick(0),i=2;ia;++a)e.isDatasetVisible(this.chart.data.datasets[a])&&this.chart.data.datasets[a].bar&&++i;return i},calculateBarX:function(t,e){var i=(this.getScaleForId(this.getDataset().yAxisID),this.getScaleForId(this.getDataset().xAxisID)),a=this.getBarIndex(e),s=this.getRuler(),o=i.getPixelForValue(null,t,e,this.chart.isCombo);return o-=this.chart.isCombo?s.tickWidth/2:0,i.options.stacked?o+s.categoryWidth/2+s.categorySpacing:o+s.barWidth/2+s.categorySpacing+s.barWidth*a+s.barSpacing/2+s.barSpacing*a},calculateBarY:function(t,i){var a=(this.getScaleForId(this.getDataset().xAxisID),this.getScaleForId(this.getDataset().yAxisID)),s=this.getDataset().data[t];if(a.options.stacked){for(var o=0,n=0,r=0;i>r;r++){var l=this.chart.data.datasets[r];e.isDatasetVisible(l)&&l.bar&&l.yAxisID===a.id&&(l.data[t]<0?n+=l.data[t]||0:o+=l.data[t]||0)}return 0>s?a.getPixelForValue(n+s):a.getPixelForValue(o+s)}return a.getPixelForValue(s)},draw:function(t){var i=t||1;e.each(this.getDataset().metaData,function(t,e){var a=this.getDataset().data[e];null===a||void 0===a||isNaN(a)||t.transition(i).draw()},this)},setHoverStyle:function(t){var i=this.chart.data.datasets[t._datasetIndex],a=t._index;t._model.backgroundColor=t.custom&&t.custom.hoverBackgroundColor?t.custom.hoverBackgroundColor:e.getValueAtIndexOrDefault(i.hoverBackgroundColor,a,e.color(t._model.backgroundColor).saturate(.5).darken(.1).rgbString()),t._model.borderColor=t.custom&&t.custom.hoverBorderColor?t.custom.hoverBorderColor:e.getValueAtIndexOrDefault(i.hoverBorderColor,a,e.color(t._model.borderColor).saturate(.5).darken(.1).rgbString()),t._model.borderWidth=t.custom&&t.custom.hoverBorderWidth?t.custom.hoverBorderWidth:e.getValueAtIndexOrDefault(i.hoverBorderWidth,a,t._model.borderWidth)},removeHoverStyle:function(t){var i=(this.chart.data.datasets[t._datasetIndex],t._index);t._model.backgroundColor=t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.rectangle.backgroundColor),t._model.borderColor=t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.rectangle.borderColor),t._model.borderWidth=t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.rectangle.borderWidth)}})}},{}],16:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.bubble={hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-0"}],yAxes:[{type:"linear",position:"left",id:"y-axis-0"}]},tooltips:{callbacks:{title:function(t,e){return""},label:function(t,e){var i=e.datasets[t.datasetIndex].label||"",a=e.datasets[t.datasetIndex].data[t.index];return i+": ("+a.x+", "+a.y+", "+a.r+")"}}}},t.controllers.bubble=t.DatasetController.extend({addElements:function(){this.getDataset().metaData=this.getDataset().metaData||[],e.each(this.getDataset().data,function(e,i){this.getDataset().metaData[i]=this.getDataset().metaData[i]||new t.elements.Point({_chart:this.chart.chart,_datasetIndex:this.index,_index:i})},this)},addElementAndReset:function(e){this.getDataset().metaData=this.getDataset().metaData||[];var i=new t.elements.Point({_chart:this.chart.chart,_datasetIndex:this.index,_index:e});this.updateElement(i,e,!0),this.getDataset().metaData.splice(e,0,i)},update:function(t){var i,a=this.getDataset().metaData,s=this.getScaleForId(this.getDataset().yAxisID);this.getScaleForId(this.getDataset().xAxisID);i=s.min<0&&s.max<0?s.getPixelForValue(s.max):s.min>0&&s.max>0?s.getPixelForValue(s.min):s.getPixelForValue(0),e.each(a,function(e,i){this.updateElement(e,i,t)},this)},updateElement:function(t,i,a){var s,o=this.getScaleForId(this.getDataset().yAxisID),n=this.getScaleForId(this.getDataset().xAxisID);s=o.min<0&&o.max<0?o.getPixelForValue(o.max):o.min>0&&o.max>0?o.getPixelForValue(o.min):o.getPixelForValue(0),e.extend(t,{_chart:this.chart.chart,_xScale:n,_yScale:o,_datasetIndex:this.index,_index:i,_model:{x:a?n.getPixelForDecimal(.5):n.getPixelForValue(this.getDataset().data[i],i,this.index,this.chart.isCombo),y:a?s:o.getPixelForValue(this.getDataset().data[i],i,this.index),radius:a?0:t.custom&&t.custom.radius?t.custom.radius:this.getRadius(this.getDataset().data[i]),backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.point.backgroundColor),borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.point.borderColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.point.borderWidth),hitRadius:t.custom&&t.custom.hitRadius?t.custom.hitRadius:e.getValueAtIndexOrDefault(this.getDataset().hitRadius,i,this.chart.options.elements.point.hitRadius)}}),t._model.skip=t.custom&&t.custom.skip?t.custom.skip:isNaN(t._model.x)||isNaN(t._model.y),t.pivot()},getRadius:function(t){return t.r||this.chart.options.elements.point.radius},draw:function(t){var i=t||1;e.each(this.getDataset().metaData,function(t,e){t.transition(i),t.draw()})},setHoverStyle:function(t){var i=this.chart.data.datasets[t._datasetIndex],a=t._index;t._model.radius=t.custom&&t.custom.hoverRadius?t.custom.hoverRadius:e.getValueAtIndexOrDefault(i.hoverRadius,a,this.chart.options.elements.point.hoverRadius)+this.getRadius(this.getDataset().data[t._index]),t._model.backgroundColor=t.custom&&t.custom.hoverBackgroundColor?t.custom.hoverBackgroundColor:e.getValueAtIndexOrDefault(i.hoverBackgroundColor,a,e.color(t._model.backgroundColor).saturate(.5).darken(.1).rgbString()),t._model.borderColor=t.custom&&t.custom.hoverBorderColor?t.custom.hoverBorderColor:e.getValueAtIndexOrDefault(i.hoverBorderColor,a,e.color(t._model.borderColor).saturate(.5).darken(.1).rgbString()),t._model.borderWidth=t.custom&&t.custom.hoverBorderWidth?t.custom.hoverBorderWidth:e.getValueAtIndexOrDefault(i.hoverBorderWidth,a,t._model.borderWidth)},removeHoverStyle:function(t){var i=(this.chart.data.datasets[t._datasetIndex],t._index);t._model.radius=t.custom&&t.custom.radius?t.custom.radius:this.getRadius(this.getDataset().data[t._index]),t._model.backgroundColor=t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.point.backgroundColor),t._model.borderColor=t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.point.borderColor),t._model.borderWidth=t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.point.borderWidth)}})}},{}],17:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.doughnut={animation:{animateRotate:!0,animateScale:!1},aspectRatio:1,hover:{mode:"single"},legendCallback:function(t){var e=[];if(e.push('
      '),t.data.datasets.length)for(var i=0;i'),t.data.labels[i]&&e.push(t.data.labels[i]),e.push("");return e.push("
    "),e.join("")},legend:{labels:{generateLabels:function(t){return t.labels.length&&t.datasets.length?t.labels.map(function(e,i){return{text:e,fillStyle:t.datasets[0].backgroundColor[i],hidden:isNaN(t.datasets[0].data[i]),index:i}}):[]}},onClick:function(t,i){e.each(this.chart.data.datasets,function(t){t.metaHiddenData=t.metaHiddenData||[];var e=i.index;isNaN(t.data[e])?isNaN(t.metaHiddenData[e])||(t.data[e]=t.metaHiddenData[e]):(t.metaHiddenData[e]=t.data[e],t.data[e]=NaN)}),this.chart.update()}},cutoutPercentage:50,tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+e.datasets[t.datasetIndex].data[t.index]}}}},t.defaults.pie=e.clone(t.defaults.doughnut),e.extend(t.defaults.pie,{cutoutPercentage:0}),t.controllers.doughnut=t.controllers.pie=t.DatasetController.extend({linkScales:function(){},addElements:function(){this.getDataset().metaData=this.getDataset().metaData||[],e.each(this.getDataset().data,function(e,i){this.getDataset().metaData[i]=this.getDataset().metaData[i]||new t.elements.Arc({_chart:this.chart.chart,_datasetIndex:this.index,_index:i})},this)},addElementAndReset:function(i,a){this.getDataset().metaData=this.getDataset().metaData||[];var s=new t.elements.Arc({_chart:this.chart.chart,_datasetIndex:this.index,_index:i});a&&e.isArray(this.getDataset().backgroundColor)&&this.getDataset().backgroundColor.splice(i,0,a),this.updateElement(s,i,!0),this.getDataset().metaData.splice(i,0,s)},getVisibleDatasetCount:function(){return e.where(this.chart.data.datasets,function(t){return e.isDatasetVisible(t)}).length},getRingIndex:function(t){for(var i=0,a=0;t>a;++a)e.isDatasetVisible(this.chart.data.datasets[a])&&++i;return i},update:function(t){var i=Math.min(this.chart.chartArea.right-this.chart.chartArea.left,this.chart.chartArea.bottom-this.chart.chartArea.top);this.chart.outerRadius=Math.max(i/2-this.chart.options.elements.arc.borderWidth/2,0),this.chart.innerRadius=Math.max(this.chart.options.cutoutPercentage?this.chart.outerRadius/100*this.chart.options.cutoutPercentage:1,0),this.chart.radiusLength=(this.chart.outerRadius-this.chart.innerRadius)/this.getVisibleDatasetCount(),this.getDataset().total=0,e.each(this.getDataset().data,function(t){isNaN(t)||(this.getDataset().total+=Math.abs(t))},this),this.outerRadius=this.chart.outerRadius-this.chart.radiusLength*this.getRingIndex(this.index),this.innerRadius=this.outerRadius-this.chart.radiusLength,e.each(this.getDataset().metaData,function(e,i){this.updateElement(e,i,t)},this)},updateElement:function(t,i,a){var s=(this.chart.chartArea.left+this.chart.chartArea.right)/2,o=(this.chart.chartArea.top+this.chart.chartArea.bottom)/2,n=Math.PI*-.5,r=Math.PI*-.5,l=a&&this.chart.options.animation.animateRotate?0:this.calculateCircumference(this.getDataset().data[i]),h=a&&this.chart.options.animation.animateScale?0:this.innerRadius,c=a&&this.chart.options.animation.animateScale?0:this.outerRadius;e.extend(t,{_chart:this.chart.chart,_datasetIndex:this.index,_index:i,_model:{x:s,y:o,startAngle:n,endAngle:r,circumference:l,outerRadius:c,innerRadius:h,backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.arc.backgroundColor),hoverBackgroundColor:t.custom&&t.custom.hoverBackgroundColor?t.custom.hoverBackgroundColor:e.getValueAtIndexOrDefault(this.getDataset().hoverBackgroundColor,i,this.chart.options.elements.arc.hoverBackgroundColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.arc.borderWidth),borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.arc.borderColor),label:e.getValueAtIndexOrDefault(this.getDataset().label,i,this.chart.data.labels[i])}}),a||(0===i?t._model.startAngle=Math.PI*-.5:t._model.startAngle=this.getDataset().metaData[i-1]._model.endAngle,t._model.endAngle=t._model.startAngle+t._model.circumference),t.pivot()},draw:function(t){var i=t||1;e.each(this.getDataset().metaData,function(t,e){t.transition(i).draw()})},setHoverStyle:function(t){var i=this.chart.data.datasets[t._datasetIndex],a=t._index;t._model.backgroundColor=t.custom&&t.custom.hoverBackgroundColor?t.custom.hoverBackgroundColor:e.getValueAtIndexOrDefault(i.hoverBackgroundColor,a,e.color(t._model.backgroundColor).saturate(.5).darken(.1).rgbString()),t._model.borderColor=t.custom&&t.custom.hoverBorderColor?t.custom.hoverBorderColor:e.getValueAtIndexOrDefault(i.hoverBorderColor,a,e.color(t._model.borderColor).saturate(.5).darken(.1).rgbString()),t._model.borderWidth=t.custom&&t.custom.hoverBorderWidth?t.custom.hoverBorderWidth:e.getValueAtIndexOrDefault(i.hoverBorderWidth,a,t._model.borderWidth)},removeHoverStyle:function(t){var i=(this.chart.data.datasets[t._datasetIndex],t._index);t._model.backgroundColor=t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.arc.backgroundColor),t._model.borderColor=t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.arc.borderColor),t._model.borderWidth=t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.arc.borderWidth)},calculateCircumference:function(t){return this.getDataset().total>0&&!isNaN(t)?1.999999*Math.PI*(t/this.getDataset().total):0}})}},{}],18:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.line={showLines:!0,hover:{mode:"label"},scales:{xAxes:[{type:"category",id:"x-axis-0"}],yAxes:[{type:"linear",id:"y-axis-0"}]}},t.controllers.line=t.DatasetController.extend({addElements:function(){this.getDataset().metaData=this.getDataset().metaData||[],this.getDataset().metaDataset=this.getDataset().metaDataset||new t.elements.Line({_chart:this.chart.chart,_datasetIndex:this.index,_points:this.getDataset().metaData}),e.each(this.getDataset().data,function(e,i){this.getDataset().metaData[i]=this.getDataset().metaData[i]||new t.elements.Point({_chart:this.chart.chart,_datasetIndex:this.index,_index:i})},this)},addElementAndReset:function(e){this.getDataset().metaData=this.getDataset().metaData||[];var i=new t.elements.Point({_chart:this.chart.chart,_datasetIndex:this.index,_index:e});this.updateElement(i,e,!0),this.getDataset().metaData.splice(e,0,i),this.chart.options.showLines&&0!==this.chart.options.elements.line.tension&&this.updateBezierControlPoints()},update:function(t){var i,a=this.getDataset().metaDataset,s=this.getDataset().metaData,o=this.getScaleForId(this.getDataset().yAxisID);this.getScaleForId(this.getDataset().xAxisID);i=o.min<0&&o.max<0?o.getPixelForValue(o.max):o.min>0&&o.max>0?o.getPixelForValue(o.min):o.getPixelForValue(0),this.chart.options.showLines&&(a._scale=o,a._datasetIndex=this.index,a._children=s,a._model={tension:a.custom&&a.custom.tension?a.custom.tension:e.getValueOrDefault(this.getDataset().tension,this.chart.options.elements.line.tension),backgroundColor:a.custom&&a.custom.backgroundColor?a.custom.backgroundColor:this.getDataset().backgroundColor||this.chart.options.elements.line.backgroundColor,borderWidth:a.custom&&a.custom.borderWidth?a.custom.borderWidth:this.getDataset().borderWidth||this.chart.options.elements.line.borderWidth,borderColor:a.custom&&a.custom.borderColor?a.custom.borderColor:this.getDataset().borderColor||this.chart.options.elements.line.borderColor,borderCapStyle:a.custom&&a.custom.borderCapStyle?a.custom.borderCapStyle:this.getDataset().borderCapStyle||this.chart.options.elements.line.borderCapStyle,borderDash:a.custom&&a.custom.borderDash?a.custom.borderDash:this.getDataset().borderDash||this.chart.options.elements.line.borderDash,borderDashOffset:a.custom&&a.custom.borderDashOffset?a.custom.borderDashOffset:this.getDataset().borderDashOffset||this.chart.options.elements.line.borderDashOffset,borderJoinStyle:a.custom&&a.custom.borderJoinStyle?a.custom.borderJoinStyle:this.getDataset().borderJoinStyle||this.chart.options.elements.line.borderJoinStyle,fill:a.custom&&a.custom.fill?a.custom.fill:void 0!==this.getDataset().fill?this.getDataset().fill:this.chart.options.elements.line.fill,scaleTop:o.top,scaleBottom:o.bottom,scaleZero:i},a.pivot()),e.each(s,function(e,i){this.updateElement(e,i,t)},this),this.chart.options.showLines&&0!==this.chart.options.elements.line.tension&&this.updateBezierControlPoints()},getPointBackgroundColor:function(t,i){var a=this.chart.options.elements.point.backgroundColor,s=this.getDataset();return t.custom&&t.custom.backgroundColor?a=t.custom.backgroundColor:s.pointBackgroundColor?a=e.getValueAtIndexOrDefault(s.pointBackgroundColor,i,a):s.backgroundColor&&(a=s.backgroundColor),a},getPointBorderColor:function(t,i){var a=this.chart.options.elements.point.borderColor,s=this.getDataset();return t.custom&&t.custom.borderColor?a=t.custom.borderColor:s.pointBorderColor?a=e.getValueAtIndexOrDefault(this.getDataset().pointBorderColor,i,a):s.borderColor&&(a=s.borderColor),a},getPointBorderWidth:function(t,i){var a=this.chart.options.elements.point.borderWidth,s=this.getDataset();return t.custom&&void 0!==t.custom.borderWidth?a=t.custom.borderWidth:void 0!==s.pointBorderWidth?a=e.getValueAtIndexOrDefault(s.pointBorderWidth,i,a):void 0!==s.borderWidth&&(a=s.borderWidth),a},updateElement:function(t,i,a){var s,o=this.getScaleForId(this.getDataset().yAxisID),n=this.getScaleForId(this.getDataset().xAxisID);s=o.min<0&&o.max<0?o.getPixelForValue(o.max):o.min>0&&o.max>0?o.getPixelForValue(o.min):o.getPixelForValue(0),t._chart=this.chart.chart,t._xScale=n,t._yScale=o,t._datasetIndex=this.index,t._index=i,t._model={x:n.getPixelForValue(this.getDataset().data[i],i,this.index,this.chart.isCombo),y:a?s:this.calculatePointY(this.getDataset().data[i],i,this.index,this.chart.isCombo),tension:t.custom&&t.custom.tension?t.custom.tension:e.getValueOrDefault(this.getDataset().tension,this.chart.options.elements.line.tension),radius:t.custom&&t.custom.radius?t.custom.radius:e.getValueAtIndexOrDefault(this.getDataset().radius,i,this.chart.options.elements.point.radius),pointStyle:t.custom&&t.custom.pointStyle?t.custom.pointStyle:e.getValueAtIndexOrDefault(this.getDataset().pointStyle,i,this.chart.options.elements.point.pointStyle),backgroundColor:this.getPointBackgroundColor(t,i),borderColor:this.getPointBorderColor(t,i),borderWidth:this.getPointBorderWidth(t,i),hitRadius:t.custom&&t.custom.hitRadius?t.custom.hitRadius:e.getValueAtIndexOrDefault(this.getDataset().hitRadius,i,this.chart.options.elements.point.hitRadius)},t._model.skip=t.custom&&t.custom.skip?t.custom.skip:isNaN(t._model.x)||isNaN(t._model.y)},calculatePointY:function(t,i,a,s){var o=(this.getScaleForId(this.getDataset().xAxisID),this.getScaleForId(this.getDataset().yAxisID));if(o.options.stacked){for(var n=0,r=0,l=0;a>l;l++){var h=this.chart.data.datasets[l];"line"===h.type&&e.isDatasetVisible(h)&&(h.data[i]<0?r+=h.data[i]||0:n+=h.data[i]||0)}return 0>t?o.getPixelForValue(r+t):o.getPixelForValue(n+t)}return o.getPixelForValue(t)},updateBezierControlPoints:function(){e.each(this.getDataset().metaData,function(t,i){var a=e.splineCurve(e.previousItem(this.getDataset().metaData,i)._model,t._model,e.nextItem(this.getDataset().metaData,i)._model,t._model.tension);t._model.controlPointPreviousX=Math.max(Math.min(a.previous.x,this.chart.chartArea.right),this.chart.chartArea.left),t._model.controlPointPreviousY=Math.max(Math.min(a.previous.y,this.chart.chartArea.bottom),this.chart.chartArea.top),t._model.controlPointNextX=Math.max(Math.min(a.next.x,this.chart.chartArea.right),this.chart.chartArea.left),t._model.controlPointNextY=Math.max(Math.min(a.next.y,this.chart.chartArea.bottom),this.chart.chartArea.top),t.pivot()},this)},draw:function(t){var i=t||1;e.each(this.getDataset().metaData,function(t){t.transition(i)}),this.chart.options.showLines&&this.getDataset().metaDataset.transition(i).draw(),e.each(this.getDataset().metaData,function(t){t.draw()})},setHoverStyle:function(t){var i=this.chart.data.datasets[t._datasetIndex],a=t._index;t._model.radius=t.custom&&t.custom.hoverRadius?t.custom.hoverRadius:e.getValueAtIndexOrDefault(i.pointHoverRadius,a,this.chart.options.elements.point.hoverRadius),t._model.backgroundColor=t.custom&&t.custom.hoverBackgroundColor?t.custom.hoverBackgroundColor:e.getValueAtIndexOrDefault(i.pointHoverBackgroundColor,a,e.color(t._model.backgroundColor).saturate(.5).darken(.1).rgbString()),t._model.borderColor=t.custom&&t.custom.hoverBorderColor?t.custom.hoverBorderColor:e.getValueAtIndexOrDefault(i.pointHoverBorderColor,a,e.color(t._model.borderColor).saturate(.5).darken(.1).rgbString()),t._model.borderWidth=t.custom&&t.custom.hoverBorderWidth?t.custom.hoverBorderWidth:e.getValueAtIndexOrDefault(i.pointHoverBorderWidth,a,t._model.borderWidth)},removeHoverStyle:function(t){var i=(this.chart.data.datasets[t._datasetIndex],t._index);t._model.radius=t.custom&&t.custom.radius?t.custom.radius:e.getValueAtIndexOrDefault(this.getDataset().radius,i,this.chart.options.elements.point.radius),t._model.backgroundColor=this.getPointBackgroundColor(t,i),t._model.borderColor=this.getPointBorderColor(t,i),t._model.borderWidth=this.getPointBorderWidth(t,i)}})}},{}],19:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.polarArea={scale:{type:"radialLinear",lineArc:!0},animateRotate:!0,animateScale:!0,aspectRatio:1,legendCallback:function(t){var e=[];if(e.push('
      '),t.data.datasets.length)for(var i=0;i'),t.data.labels[i]&&e.push(t.data.labels[i]),e.push("");return e.push("
    "),e.join("")},legend:{labels:{generateLabels:function(t){return t.labels.length&&t.datasets.length?t.labels.map(function(e,i){return{text:e,fillStyle:t.datasets[0].backgroundColor[i],hidden:isNaN(t.datasets[0].data[i]),index:i}}):[]}},onClick:function(t,i){e.each(this.chart.data.datasets,function(t){t.metaHiddenData=t.metaHiddenData||[];var e=i.index;isNaN(t.data[e])?isNaN(t.metaHiddenData[e])||(t.data[e]=t.metaHiddenData[e]):(t.metaHiddenData[e]=t.data[e],t.data[e]=NaN)}),this.chart.update()}},tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+t.yLabel}}}},t.controllers.polarArea=t.DatasetController.extend({linkScales:function(){},addElements:function(){this.getDataset().metaData=this.getDataset().metaData||[],e.each(this.getDataset().data,function(e,i){this.getDataset().metaData[i]=this.getDataset().metaData[i]||new t.elements.Arc({_chart:this.chart.chart,_datasetIndex:this.index,_index:i})},this)},addElementAndReset:function(e){this.getDataset().metaData=this.getDataset().metaData||[];var i=new t.elements.Arc({_chart:this.chart.chart,_datasetIndex:this.index,_index:e});this.updateElement(i,e,!0),this.getDataset().metaData.splice(e,0,i)},getVisibleDatasetCount:function(){return e.where(this.chart.data.datasets,function(t){return e.isDatasetVisible(t)}).length},update:function(t){var i=Math.min(this.chart.chartArea.right-this.chart.chartArea.left,this.chart.chartArea.bottom-this.chart.chartArea.top);this.chart.outerRadius=Math.max((i-this.chart.options.elements.arc.borderWidth/2)/2,0),this.chart.innerRadius=Math.max(this.chart.options.cutoutPercentage?this.chart.outerRadius/100*this.chart.options.cutoutPercentage:1,0),this.chart.radiusLength=(this.chart.outerRadius-this.chart.innerRadius)/this.getVisibleDatasetCount(),this.getDataset().total=0,e.each(this.getDataset().data,function(t){this.getDataset().total+=Math.abs(t)},this),this.outerRadius=this.chart.outerRadius-this.chart.radiusLength*this.index,this.innerRadius=this.outerRadius-this.chart.radiusLength,e.each(this.getDataset().metaData,function(e,i){this.updateElement(e,i,t)},this)},updateElement:function(t,i,a){for(var s=this.calculateCircumference(this.getDataset().data[i]),o=(this.chart.chartArea.left+this.chart.chartArea.right)/2,n=(this.chart.chartArea.top+this.chart.chartArea.bottom)/2,r=0,l=0;i>l;++l)isNaN(this.getDataset().data[l])||++r;var h=-.5*Math.PI+s*r,c=h+s,d={x:o,y:n,innerRadius:0,outerRadius:this.chart.options.animateScale?0:this.chart.scale.getDistanceFromCenterForValue(this.getDataset().data[i]),startAngle:this.chart.options.animateRotate?Math.PI*-.5:h,endAngle:this.chart.options.animateRotate?Math.PI*-.5:c,backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.arc.backgroundColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.arc.borderWidth),borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.arc.borderColor),label:e.getValueAtIndexOrDefault(this.chart.data.labels,i,this.chart.data.labels[i])};e.extend(t,{_chart:this.chart.chart,_datasetIndex:this.index,_index:i,_scale:this.chart.scale,_model:a?d:{x:o,y:n,innerRadius:0, -outerRadius:this.chart.scale.getDistanceFromCenterForValue(this.getDataset().data[i]),startAngle:h,endAngle:c,backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.arc.backgroundColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.arc.borderWidth),borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.arc.borderColor),label:e.getValueAtIndexOrDefault(this.chart.data.labels,i,this.chart.data.labels[i])}}),t.pivot()},draw:function(t){var i=t||1;e.each(this.getDataset().metaData,function(t,e){t.transition(i).draw()})},setHoverStyle:function(t){var i=this.chart.data.datasets[t._datasetIndex],a=t._index;t._model.backgroundColor=t.custom&&t.custom.hoverBackgroundColor?t.custom.hoverBackgroundColor:e.getValueAtIndexOrDefault(i.hoverBackgroundColor,a,e.color(t._model.backgroundColor).saturate(.5).darken(.1).rgbString()),t._model.borderColor=t.custom&&t.custom.hoverBorderColor?t.custom.hoverBorderColor:e.getValueAtIndexOrDefault(i.hoverBorderColor,a,e.color(t._model.borderColor).saturate(.5).darken(.1).rgbString()),t._model.borderWidth=t.custom&&t.custom.hoverBorderWidth?t.custom.hoverBorderWidth:e.getValueAtIndexOrDefault(i.hoverBorderWidth,a,t._model.borderWidth)},removeHoverStyle:function(t){var i=(this.chart.data.datasets[t._datasetIndex],t._index);t._model.backgroundColor=t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().backgroundColor,i,this.chart.options.elements.arc.backgroundColor),t._model.borderColor=t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().borderColor,i,this.chart.options.elements.arc.borderColor),t._model.borderWidth=t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().borderWidth,i,this.chart.options.elements.arc.borderWidth)},calculateCircumference:function(t){if(isNaN(t))return 0;var i=e.where(this.getDataset().data,function(t){return isNaN(t)}).length;return 2*Math.PI/(this.getDataset().data.length-i)}})}},{}],20:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.radar={scale:{type:"radialLinear"},elements:{line:{tension:0}}},t.controllers.radar=t.DatasetController.extend({linkScales:function(){},addElements:function(){this.getDataset().metaData=this.getDataset().metaData||[],this.getDataset().metaDataset=this.getDataset().metaDataset||new t.elements.Line({_chart:this.chart.chart,_datasetIndex:this.index,_points:this.getDataset().metaData,_loop:!0}),e.each(this.getDataset().data,function(e,i){this.getDataset().metaData[i]=this.getDataset().metaData[i]||new t.elements.Point({_chart:this.chart.chart,_datasetIndex:this.index,_index:i,_model:{x:0,y:0}})},this)},addElementAndReset:function(e){this.getDataset().metaData=this.getDataset().metaData||[];var i=new t.elements.Point({_chart:this.chart.chart,_datasetIndex:this.index,_index:e});this.updateElement(i,e,!0),this.getDataset().metaData.splice(e,0,i),this.updateBezierControlPoints()},update:function(t){var i,a=this.getDataset().metaDataset,s=this.getDataset().metaData,o=this.chart.scale;i=o.min<0&&o.max<0?o.getPointPositionForValue(0,o.max):o.min>0&&o.max>0?o.getPointPositionForValue(0,o.min):o.getPointPositionForValue(0,0),e.extend(this.getDataset().metaDataset,{_datasetIndex:this.index,_children:this.getDataset().metaData,_model:{tension:a.custom&&a.custom.tension?a.custom.tension:e.getValueOrDefault(this.getDataset().tension,this.chart.options.elements.line.tension),backgroundColor:a.custom&&a.custom.backgroundColor?a.custom.backgroundColor:this.getDataset().backgroundColor||this.chart.options.elements.line.backgroundColor,borderWidth:a.custom&&a.custom.borderWidth?a.custom.borderWidth:this.getDataset().borderWidth||this.chart.options.elements.line.borderWidth,borderColor:a.custom&&a.custom.borderColor?a.custom.borderColor:this.getDataset().borderColor||this.chart.options.elements.line.borderColor,fill:a.custom&&a.custom.fill?a.custom.fill:void 0!==this.getDataset().fill?this.getDataset().fill:this.chart.options.elements.line.fill,borderCapStyle:a.custom&&a.custom.borderCapStyle?a.custom.borderCapStyle:this.getDataset().borderCapStyle||this.chart.options.elements.line.borderCapStyle,borderDash:a.custom&&a.custom.borderDash?a.custom.borderDash:this.getDataset().borderDash||this.chart.options.elements.line.borderDash,borderDashOffset:a.custom&&a.custom.borderDashOffset?a.custom.borderDashOffset:this.getDataset().borderDashOffset||this.chart.options.elements.line.borderDashOffset,borderJoinStyle:a.custom&&a.custom.borderJoinStyle?a.custom.borderJoinStyle:this.getDataset().borderJoinStyle||this.chart.options.elements.line.borderJoinStyle,scaleTop:o.top,scaleBottom:o.bottom,scaleZero:i}}),this.getDataset().metaDataset.pivot(),e.each(s,function(e,i){this.updateElement(e,i,t)},this),this.updateBezierControlPoints()},updateElement:function(t,i,a){var s=this.chart.scale.getPointPositionForValue(i,this.getDataset().data[i]);e.extend(t,{_datasetIndex:this.index,_index:i,_scale:this.chart.scale,_model:{x:a?this.chart.scale.xCenter:s.x,y:a?this.chart.scale.yCenter:s.y,tension:t.custom&&t.custom.tension?t.custom.tension:e.getValueOrDefault(this.getDataset().tension,this.chart.options.elements.line.tension),radius:t.custom&&t.custom.radius?t.custom.radius:e.getValueAtIndexOrDefault(this.getDataset().pointRadius,i,this.chart.options.elements.point.radius),backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().pointBackgroundColor,i,this.chart.options.elements.point.backgroundColor),borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().pointBorderColor,i,this.chart.options.elements.point.borderColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().pointBorderWidth,i,this.chart.options.elements.point.borderWidth),pointStyle:t.custom&&t.custom.pointStyle?t.custom.pointStyle:e.getValueAtIndexOrDefault(this.getDataset().pointStyle,i,this.chart.options.elements.point.pointStyle),hitRadius:t.custom&&t.custom.hitRadius?t.custom.hitRadius:e.getValueAtIndexOrDefault(this.getDataset().hitRadius,i,this.chart.options.elements.point.hitRadius)}}),t._model.skip=t.custom&&t.custom.skip?t.custom.skip:isNaN(t._model.x)||isNaN(t._model.y)},updateBezierControlPoints:function(){e.each(this.getDataset().metaData,function(t,i){var a=e.splineCurve(e.previousItem(this.getDataset().metaData,i,!0)._model,t._model,e.nextItem(this.getDataset().metaData,i,!0)._model,t._model.tension);t._model.controlPointPreviousX=Math.max(Math.min(a.previous.x,this.chart.chartArea.right),this.chart.chartArea.left),t._model.controlPointPreviousY=Math.max(Math.min(a.previous.y,this.chart.chartArea.bottom),this.chart.chartArea.top),t._model.controlPointNextX=Math.max(Math.min(a.next.x,this.chart.chartArea.right),this.chart.chartArea.left),t._model.controlPointNextY=Math.max(Math.min(a.next.y,this.chart.chartArea.bottom),this.chart.chartArea.top),t.pivot()},this)},draw:function(t){var i=t||1;e.each(this.getDataset().metaData,function(t,e){t.transition(i)}),this.getDataset().metaDataset.transition(i).draw(),e.each(this.getDataset().metaData,function(t){t.draw()})},setHoverStyle:function(t){var i=this.chart.data.datasets[t._datasetIndex],a=t._index;t._model.radius=t.custom&&t.custom.hoverRadius?t.custom.hoverRadius:e.getValueAtIndexOrDefault(i.pointHoverRadius,a,this.chart.options.elements.point.hoverRadius),t._model.backgroundColor=t.custom&&t.custom.hoverBackgroundColor?t.custom.hoverBackgroundColor:e.getValueAtIndexOrDefault(i.pointHoverBackgroundColor,a,e.color(t._model.backgroundColor).saturate(.5).darken(.1).rgbString()),t._model.borderColor=t.custom&&t.custom.hoverBorderColor?t.custom.hoverBorderColor:e.getValueAtIndexOrDefault(i.pointHoverBorderColor,a,e.color(t._model.borderColor).saturate(.5).darken(.1).rgbString()),t._model.borderWidth=t.custom&&t.custom.hoverBorderWidth?t.custom.hoverBorderWidth:e.getValueAtIndexOrDefault(i.pointHoverBorderWidth,a,t._model.borderWidth)},removeHoverStyle:function(t){var i=(this.chart.data.datasets[t._datasetIndex],t._index);t._model.radius=t.custom&&t.custom.radius?t.custom.radius:e.getValueAtIndexOrDefault(this.getDataset().radius,i,this.chart.options.elements.point.radius),t._model.backgroundColor=t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:e.getValueAtIndexOrDefault(this.getDataset().pointBackgroundColor,i,this.chart.options.elements.point.backgroundColor),t._model.borderColor=t.custom&&t.custom.borderColor?t.custom.borderColor:e.getValueAtIndexOrDefault(this.getDataset().pointBorderColor,i,this.chart.options.elements.point.borderColor),t._model.borderWidth=t.custom&&t.custom.borderWidth?t.custom.borderWidth:e.getValueAtIndexOrDefault(this.getDataset().pointBorderWidth,i,this.chart.options.elements.point.borderWidth)}})}},{}],21:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.global.animation={duration:1e3,easing:"easeOutQuart",onProgress:e.noop,onComplete:e.noop},t.Animation=t.Element.extend({currentStep:null,numSteps:60,easing:"",render:null,onAnimationProgress:null,onAnimationComplete:null}),t.animationService={frameDuration:17,animations:[],dropFrames:0,request:null,addAnimation:function(t,e,i,a){a||(t.animating=!0);for(var s=0;s1&&(e=Math.floor(this.dropFrames),this.dropFrames=this.dropFrames%1);for(var i=0;ithis.animations[i].animationObject.numSteps&&(this.animations[i].animationObject.currentStep=this.animations[i].animationObject.numSteps),this.animations[i].animationObject.render(this.animations[i].chartInstance,this.animations[i].animationObject),this.animations[i].animationObject.onAnimationProgress&&this.animations[i].animationObject.onAnimationProgress.call&&this.animations[i].animationObject.onAnimationProgress.call(this.animations[i].chartInstance,this.animations[i]),this.animations[i].animationObject.currentStep===this.animations[i].animationObject.numSteps?(this.animations[i].animationObject.onAnimationComplete&&this.animations[i].animationObject.onAnimationComplete.call&&this.animations[i].animationObject.onAnimationComplete.call(this.animations[i].chartInstance,this.animations[i]),this.animations[i].chartInstance.animating=!1,this.animations.splice(i,1)):++i;var a=Date.now(),s=(a-t)/this.frameDuration;this.dropFrames+=s,this.animations.length>0&&this.requestAnimationFrame()}}}},{}],22:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.types={},t.instances={},t.controllers={},t.Controller=function(i){return this.chart=i,this.config=i.config,this.options=this.config.options=e.configMerge(t.defaults.global,t.defaults[this.config.type],this.config.options||{}),this.id=e.uid(),Object.defineProperty(this,"data",{get:function(){return this.config.data}}),t.instances[this.id]=this,this.options.responsive&&this.resize(!0),this.initialize(),this},e.extend(t.Controller.prototype,{initialize:function(){return this.bindEvents(),this.ensureScalesHaveIDs(),this.buildOrUpdateControllers(),this.buildScales(),this.buildSurroundingItems(),this.updateLayout(),this.resetElements(),this.initToolTip(),this.update(),this},clear:function(){return e.clear(this.chart),this},stop:function(){return t.animationService.cancelAnimation(this),this},resize:function(t){var i=this.chart.canvas,a=e.getMaximumWidth(this.chart.canvas),s=this.options.maintainAspectRatio&&isNaN(this.chart.aspectRatio)===!1&&isFinite(this.chart.aspectRatio)&&0!==this.chart.aspectRatio?a/this.chart.aspectRatio:e.getMaximumHeight(this.chart.canvas),o=this.chart.width!==a||this.chart.height!==s;return o?(i.width=this.chart.width=a,i.height=this.chart.height=s,e.retinaScale(this.chart),t||(this.stop(),this.update(this.options.responsiveAnimationDuration)),this):this},ensureScalesHaveIDs:function(){var t="x-axis-",i="y-axis-";this.options.scales&&(this.options.scales.xAxes&&this.options.scales.xAxes.length&&e.each(this.options.scales.xAxes,function(e,i){e.id=e.id||t+i}),this.options.scales.yAxes&&this.options.scales.yAxes.length&&e.each(this.options.scales.yAxes,function(t,e){t.id=t.id||i+e}))},buildScales:function(){if(this.scales={},this.options.scales&&(this.options.scales.xAxes&&this.options.scales.xAxes.length&&e.each(this.options.scales.xAxes,function(i,a){var s=e.getValueOrDefault(i.type,"category"),o=t.scaleService.getScaleConstructor(s);if(o){var n=new o({ctx:this.chart.ctx,options:i,chart:this,id:i.id});this.scales[n.id]=n}},this),this.options.scales.yAxes&&this.options.scales.yAxes.length&&e.each(this.options.scales.yAxes,function(i,a){var s=e.getValueOrDefault(i.type,"linear"),o=t.scaleService.getScaleConstructor(s);if(o){var n=new o({ctx:this.chart.ctx,options:i,chart:this,id:i.id});this.scales[n.id]=n}},this)),this.options.scale){var i=t.scaleService.getScaleConstructor(this.options.scale.type);if(i){var a=new i({ctx:this.chart.ctx,options:this.options.scale,chart:this});this.scale=a,this.scales.radialScale=a}}t.scaleService.addScalesToLayout(this)},buildSurroundingItems:function(){this.options.title&&(this.titleBlock=new t.Title({ctx:this.chart.ctx,options:this.options.title,chart:this}),t.layoutService.addBox(this,this.titleBlock)),this.options.legend&&(this.legend=new t.Legend({ctx:this.chart.ctx,options:this.options.legend,chart:this}),t.layoutService.addBox(this,this.legend))},updateLayout:function(){t.layoutService.update(this,this.chart.width,this.chart.height)},buildOrUpdateControllers:function(){var i=[],a=[];if(e.each(this.data.datasets,function(e,s){e.type||(e.type=this.config.type);var o=e.type;i.push(o),e.controller?e.controller.updateIndex(s):(e.controller=new t.controllers[o](this,s),a.push(e.controller))},this),i.length>1)for(var s=1;s0&&(e=this.data.datasets[e[0]._datasetIndex].metaData),e},generateLegend:function(){return this.options.legendCallback(this)},destroy:function(){this.clear(),e.unbindEvents(this,this.events),e.removeResizeListener(this.chart.canvas.parentNode);var i=this.chart.canvas;i.width=this.chart.width,i.height=this.chart.height,void 0!==this.chart.originalDevicePixelRatio&&this.chart.ctx.scale(1/this.chart.originalDevicePixelRatio,1/this.chart.originalDevicePixelRatio),i.style.width=this.chart.originalCanvasStyleWidth,i.style.height=this.chart.originalCanvasStyleHeight,delete t.instances[this.id]},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)},initToolTip:function(){this.tooltip=new t.Tooltip({_chart:this.chart,_chartInstance:this,_data:this.data,_options:this.options},this)},bindEvents:function(){e.bindEvents(this,this.options.events,function(t){this.eventHandler(t)})},eventHandler:function(t){if(this.lastActive=this.lastActive||[],this.lastTooltipActive=this.lastTooltipActive||[],"mouseout"===t.type)this.active=[],this.tooltipActive=[];else{var i=this,a=function(e){switch(e){case"single":return i.getElementAtEvent(t);case"label":return i.getElementsAtEvent(t);case"dataset":return i.getDatasetAtEvent(t);default:return t}};this.active=a(this.options.hover.mode),this.tooltipActive=a(this.options.tooltips.mode)}this.options.hover.onHover&&this.options.hover.onHover.call(this,this.active),("mouseup"===t.type||"click"===t.type)&&(this.options.onClick&&this.options.onClick.call(this,t,this.active),this.legend&&this.legend.handleEvent&&this.legend.handleEvent(t));if(this.lastActive.length)switch(this.options.hover.mode){case"single":this.data.datasets[this.lastActive[0]._datasetIndex].controller.removeHoverStyle(this.lastActive[0],this.lastActive[0]._datasetIndex,this.lastActive[0]._index);break;case"label":case"dataset":for(var s=0;st)this.getDataset().metaData.splice(t,e-t);else if(t>e)for(var i=e;t>i;++i)this.addElementAndReset(i)},addElements:e.noop,addElementAndReset:e.noop,draw:e.noop,removeHoverStyle:e.noop,setHoverStyle:e.noop,update:e.noop}),t.DatasetController.extend=e.inherits}},{}],24:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.elements={},t.Element=function(t){e.extend(this,t),this.initialize.apply(this,arguments)},e.extend(t.Element.prototype,{initialize:function(){},pivot:function(){return this._view||(this._view=e.clone(this._model)),this._start=e.clone(this._view),this},transition:function(t){return this._view||(this._view=e.clone(this._model)),1===t?(this._view=this._model,this._start=null,this):(this._start||this.pivot(),e.each(this._model,function(i,a){if("_"!==a[0]&&this._model.hasOwnProperty(a))if(this._view.hasOwnProperty(a))if(i===this._view[a]);else if("string"==typeof i)try{var s=e.color(this._start[a]).mix(e.color(this._model[a]),t);this._view[a]=s.rgbString()}catch(o){this._view[a]=i}else if("number"==typeof i){var n=void 0!==this._start[a]&&isNaN(this._start[a])===!1?this._start[a]:0;this._view[a]=(this._model[a]-n)*t+n}else this._view[a]=i;else"number"!=typeof i||isNaN(this._view[a])?this._view[a]=i:this._view[a]=i*t;else;},this),this)},tooltipPosition:function(){return{x:this._model.x,y:this._model.y}},hasValue:function(){return e.isNumber(this._model.x)&&e.isNumber(this._model.y)}}),t.Element.extend=e.inherits}},{}],25:[function(t,e,i){"use strict";var a=t("chartjs-color");e.exports=function(t){function e(t,e,i){var a;return"string"==typeof t?(a=parseInt(t,10),-1!=t.indexOf("%")&&(a=a/100*e.parentNode[i])):a=t,a}function i(t,i,a){var s,o=document.defaultView.getComputedStyle(t)[i],n=document.defaultView.getComputedStyle(t.parentNode)[i],r=null!==o&&"none"!==o,l=null!==n&&"none"!==n;return(r||l)&&(s=Math.min(r?e(o,t,a):Number.POSITIVE_INFINITY,l?e(n,t.parentNode,a):Number.POSITIVE_INFINITY)),s}var s=t.helpers={};s.each=function(t,e,i,a){var o,n;if(s.isArray(t))if(n=t.length,a)for(o=n-1;o>=0;o--)e.call(i,t[o],o);else for(o=0;n>o;o++)e.call(i,t[o],o);else if("object"==typeof t){var r=Object.keys(t);for(n=r.length,o=0;n>o;o++)e.call(i,t[r[o]],r[o])}},s.clone=function(t){var e={};return s.each(t,function(i,a){t.hasOwnProperty(a)&&(s.isArray(i)?e[a]=i.slice(0):"object"==typeof i&&null!==i?e[a]=s.clone(i):e[a]=i)}),e},s.extend=function(t){for(var e=arguments.length,i=[],a=1;e>a;a++)i.push(arguments[a]);return s.each(i,function(e){s.each(e,function(i,a){e.hasOwnProperty(a)&&(t[a]=i)})}),t},s.configMerge=function(e){var i=s.clone(e);return s.each(Array.prototype.slice.call(arguments,1),function(e){s.each(e,function(a,o){if(e.hasOwnProperty(o))if("scales"===o)i[o]=s.scaleMerge(i.hasOwnProperty(o)?i[o]:{},a);else if("scale"===o)i[o]=s.configMerge(i.hasOwnProperty(o)?i[o]:{},t.scaleService.getScaleDefaults(a.type),a);else if(i.hasOwnProperty(o)&&s.isArray(i[o])&&s.isArray(a)){var n=i[o];s.each(a,function(t,e){e=a[o].length||!a[o][i].type?a[o].push(s.configMerge(r,e)):e.type&&e.type!==a[o][i].type?a[o][i]=s.configMerge(a[o][i],r,e):a[o][i]=s.configMerge(a[o][i],e)}):(a[o]=[],s.each(e,function(e){var i=s.getValueOrDefault(e.type,"xAxes"===o?"category":"linear");a[o].push(s.configMerge(t.scaleService.getScaleDefaults(i),e))})):a.hasOwnProperty(o)&&"object"==typeof a[o]&&null!==a[o]&&"object"==typeof e?a[o]=s.configMerge(a[o],e):a[o]=e)}),a},s.getValueAtIndexOrDefault=function(t,e,i){return void 0===t||null===t?i:s.isArray(t)?e=0;a--){var s=t[a];if(e(s))return s}},s.inherits=function(t){var e=this,i=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return e.apply(this,arguments)},a=function(){this.constructor=i};return a.prototype=e.prototype,i.prototype=new a,i.extend=s.inherits,t&&s.extend(i.prototype,t),i.__super__=e.prototype,i},s.noop=function(){},s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),s.warn=function(t){console&&"function"==typeof console.warn&&console.warn(t)},s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},s.almostEquals=function(t,e,i){return Math.abs(t-e)0?1:-1)},s.log10=function(t){return Math.log10?Math.log10(t):Math.log(t)/Math.LN10},s.toRadians=function(t){return t*(Math.PI/180)},s.toDegrees=function(t){return t*(180/Math.PI)},s.getAngleFromPoint=function(t,e){var i=e.x-t.x,a=e.y-t.y,s=Math.sqrt(i*i+a*a),o=Math.atan2(a,i);return o<-.5*Math.PI&&(o+=2*Math.PI),{angle:o,distance:s}},s.aliasPixel=function(t){return t%2===0?0:.5},s.splineCurve=function(t,e,i,a){var s=t.skip?e:t,o=e,n=i.skip?e:i,r=Math.sqrt(Math.pow(o.x-s.x,2)+Math.pow(o.y-s.y,2)),l=Math.sqrt(Math.pow(n.x-o.x,2)+Math.pow(n.y-o.y,2)),h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;var d=a*h,u=a*c;return{previous:{x:o.x-d*(n.x-s.x),y:o.y-d*(n.y-s.y)},next:{x:o.x+u*(n.x-s.x),y:o.y+u*(n.y-s.y)}}},s.nextItem=function(t,e,i){return i?e>=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},s.previousItem=function(t,e,i){return i?0>=e?t[t.length-1]:t[e-1]:0>=e?t[0]:t[e-1]},s.niceNum=function(t,e){var i,a=Math.floor(s.log10(t)),o=t/Math.pow(10,a);return i=e?1.5>o?1:3>o?2:7>o?5:10:1>=o?1:2>=o?2:5>=o?5:10,i*Math.pow(10,a)};var o=s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,i=0,a=1;return 0===t?0:1===(t/=1)?1:(i||(i=.3),at?-.5*(a*Math.pow(2,10*(t-=1))*Math.sin((1*t-e)*(2*Math.PI)/i)):a*Math.pow(2,-10*(t-=1))*Math.sin((1*t-e)*(2*Math.PI)/i)*.5+1)},easeInBack:function(t){var e=1.70158;return 1*(t/=1)*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return 1*((t=t/1-1)*t*((e+1)*t+e)+1)},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?.5*(t*t*(((e*=1.525)+1)*t-e)):.5*((t-=2)*t*(((e*=1.525)+1)*t+e)+2)},easeInBounce:function(t){return 1-o.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?1*(7.5625*t*t):2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*o.easeInBounce(2*t):.5*o.easeOutBounce(2*t-1)+.5}};s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),s.getRelativePosition=function(t,e){var i,a,o=t.originalEvent||t,n=t.currentTarget||t.srcElement,r=n.getBoundingClientRect();o.touches&&o.touches.length>0?(i=o.touches[0].clientX,a=o.touches[0].clientY):(i=o.clientX,a=o.clientY);var l=parseFloat(s.getStyle(n,"padding-left")),h=parseFloat(s.getStyle(n,"padding-top")),c=parseFloat(s.getStyle(n,"padding-right")),d=parseFloat(s.getStyle(n,"padding-bottom")),u=r.right-r.left-l-c,g=r.bottom-r.top-h-d;return i=Math.round((i-r.left-l)/u*n.width/e.currentDevicePixelRatio),a=Math.round((a-r.top-h)/g*n.height/e.currentDevicePixelRatio),{x:i,y:a}},s.addEvent=function(t,e,i){t.addEventListener?t.addEventListener(e,i):t.attachEvent?t.attachEvent("on"+e,i):t["on"+e]=i; -},s.removeEvent=function(t,e,i){t.removeEventListener?t.removeEventListener(e,i,!1):t.detachEvent?t.detachEvent("on"+e,i):t["on"+e]=s.noop},s.bindEvents=function(t,e,i){t.events||(t.events={}),s.each(e,function(e){t.events[e]=function(){i.apply(t,arguments)},s.addEvent(t.chart.canvas,e,t.events[e])})},s.unbindEvents=function(t,e){s.each(e,function(e,i){s.removeEvent(t.chart.canvas,i,e)})},s.getConstraintWidth=function(t){return i(t,"max-width","clientWidth")},s.getConstraintHeight=function(t){return i(t,"max-height","clientHeight")},s.getMaximumWidth=function(t){var e=t.parentNode,i=parseInt(s.getStyle(e,"padding-left"))+parseInt(s.getStyle(e,"padding-right")),a=e.clientWidth-i,o=s.getConstraintWidth(t);return void 0!==o&&(a=Math.min(a,o)),a},s.getMaximumHeight=function(t){var e=t.parentNode,i=parseInt(s.getStyle(e,"padding-top"))+parseInt(s.getStyle(e,"padding-bottom")),a=e.clientHeight-i,o=s.getConstraintHeight(t);return void 0!==o&&(a=Math.min(a,o)),a},s.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},s.retinaScale=function(t){var e=t.ctx,i=t.canvas.width,a=t.canvas.height,s=t.currentDevicePixelRatio=window.devicePixelRatio||1;1!==s&&(e.canvas.height=a*s,e.canvas.width=i*s,e.scale(s,s),t.originalDevicePixelRatio=t.originalDevicePixelRatio||s),e.canvas.style.width=i+"px",e.canvas.style.height=a+"px"},s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},s.fontString=function(t,e,i){return e+" "+t+"px "+i},s.longestText=function(t,e,i,a){a=a||{},a.data=a.data||{},a.garbageCollect=a.garbageCollect||[],a.font!==e&&(a.data={},a.garbageCollect=[],a.font=e),t.font=e;var o=0;s.each(i,function(e){if(void 0!==e&&null!==e){var i=a.data[e];i||(i=a.data[e]=t.measureText(e).width,a.garbageCollect.push(e)),i>o&&(o=i)}});var n=a.garbageCollect.length/2;if(n>i.length){for(var r=0;n>r;r++)delete a.data[a.garbageCollect[r]];a.garbageCollect.splice(0,n)}return o},s.drawRoundedRectangle=function(t,e,i,a,s,o){t.beginPath(),t.moveTo(e+o,i),t.lineTo(e+a-o,i),t.quadraticCurveTo(e+a,i,e+a,i+o),t.lineTo(e+a,i+s-o),t.quadraticCurveTo(e+a,i+s,e+a-o,i+s),t.lineTo(e+o,i+s),t.quadraticCurveTo(e,i+s,e,i+s-o),t.lineTo(e,i+o),t.quadraticCurveTo(e,i,e+o,i),t.closePath()},s.color=function(e){return a?a(e instanceof CanvasGradient?t.defaults.global.defaultColor:e):(console.log("Color.js not found!"),e)},s.addResizeListener=function(t,e){var i=document.createElement("iframe"),a="chartjs-hidden-iframe";i.classlist?i.classlist.add(a):i.setAttribute("class",a),i.style.width="100%",i.style.display="block",i.style.border=0,i.style.height=0,i.style.margin=0,i.style.position="absolute",i.style.left=0,i.style.right=0,i.style.top=0,i.style.bottom=0,t.insertBefore(i,t.firstChild),(i.contentWindow||i).onresize=function(){e&&e()}},s.removeResizeListener=function(t){var e=t.querySelector(".chartjs-hidden-iframe");e&&e.parentNode.removeChild(e)},s.isArray=function(t){return Array.isArray?Array.isArray(t):"[object Array]"===Object.prototype.toString.call(t)},s.pushAllIfDefined=function(t,e){"undefined"!=typeof t&&(s.isArray(t)?e.push.apply(e,t):e.push(t))},s.isDatasetVisible=function(t){return!t.hidden},s.callCallback=function(t,e,i){t&&"function"==typeof t.call&&t.apply(i,e)}}},{"chartjs-color":5}],26:[function(t,e,i){"use strict";e.exports=function(){var t=function(e,i){this.config=i,e.length&&e[0].getContext&&(e=e[0]),e.getContext&&(e=e.getContext("2d")),this.ctx=e,this.canvas=e.canvas,this.width=e.canvas.width||parseInt(t.helpers.getStyle(e.canvas,"width"))||t.helpers.getMaximumWidth(e.canvas),this.height=e.canvas.height||parseInt(t.helpers.getStyle(e.canvas,"height"))||t.helpers.getMaximumHeight(e.canvas),this.aspectRatio=this.width/this.height,(isNaN(this.aspectRatio)||isFinite(this.aspectRatio)===!1)&&(this.aspectRatio=void 0!==i.aspectRatio?i.aspectRatio:2),this.originalCanvasStyleWidth=e.canvas.style.width,this.originalCanvasStyleHeight=e.canvas.style.height,t.helpers.retinaScale(this),i&&(this.controller=new t.Controller(this));var a=this;return t.helpers.addResizeListener(e.canvas.parentNode,function(){a.controller&&a.controller.config.options.responsive&&a.controller.resize()}),this.controller?this.controller:this};return t.defaults={global:{responsive:!0,responsiveAnimationDuration:0,maintainAspectRatio:!0,events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"single",animationDuration:400},onClick:null,defaultColor:"rgba(0,0,0,0.1)",defaultFontColor:"#666",defaultFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",defaultFontSize:12,defaultFontStyle:"normal",showLines:!0,elements:{},legendCallback:function(t){var e=[];e.push('
      ');for(var i=0;i'),t.data.datasets[i].label&&e.push(t.data.datasets[i].label),e.push("");return e.push("
    "),e.join("")}}},t}},{}],27:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.layoutService={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),t.boxes.push(e)},removeBox:function(t,e){t.boxes&&t.boxes.splice(t.boxes.indexOf(e),1)},update:function(t,i,a){function s(t){var e,i=t.isHorizontal();i?(e=t.update(t.options.fullWidth?m:k,y),D-=e.height):(e=t.update(v,x),k-=e.width),S.push({horizontal:i,minSize:e,box:t})}function o(t){var i=e.findNextWhere(S,function(e){return e.box===t});if(i)if(t.isHorizontal()){var a={left:C,right:w,top:0,bottom:0};t.update(t.options.fullWidth?m:k,p/2,a)}else t.update(i.minSize.width,D)}function n(t){var i=e.findNextWhere(S,function(e){return e.box===t}),a={left:0,right:0,top:_,bottom:A};i&&t.update(i.minSize.width,D,a)}function r(t){t.isHorizontal()?(t.left=t.options.fullWidth?l:C,t.right=t.options.fullWidth?i-l:C+k,t.top=P,t.bottom=P+t.height,P=t.bottom):(t.left=F,t.right=F+t.width,t.top=_,t.bottom=_+D,F=t.right)}if(t){var l=0,h=0,c=e.where(t.boxes,function(t){return"left"===t.options.position}),d=e.where(t.boxes,function(t){return"right"===t.options.position}),u=e.where(t.boxes,function(t){return"top"===t.options.position}),g=e.where(t.boxes,function(t){return"bottom"===t.options.position}),f=e.where(t.boxes,function(t){return"chartArea"===t.options.position});u.sort(function(t,e){return(e.options.fullWidth?1:0)-(t.options.fullWidth?1:0)}),g.sort(function(t,e){return(t.options.fullWidth?1:0)-(e.options.fullWidth?1:0)});var m=i-2*l,p=a-2*h,b=m/2,x=p/2,v=(i-b)/(c.length+d.length),y=(a-x)/(u.length+g.length),k=m,D=p,S=[];e.each(c.concat(d,u,g),s);var C=l,w=l,_=h,A=h;e.each(c.concat(d),o),e.each(c,function(t){C+=t.width}),e.each(d,function(t){w+=t.width}),e.each(u.concat(g),o),e.each(u,function(t){_+=t.height}),e.each(g,function(t){A+=t.height}),e.each(c.concat(d),n),C=l,w=l,_=h,A=h,e.each(c,function(t){C+=t.width}),e.each(d,function(t){w+=t.width}),e.each(u,function(t){_+=t.height}),e.each(g,function(t){A+=t.height});var I=a-_-A,M=i-C-w;(M!==k||I!==D)&&(e.each(c,function(t){t.height=I}),e.each(d,function(t){t.height=I}),e.each(u,function(t){t.width=M}),e.each(g,function(t){t.width=M}),D=I,k=M);var F=l,P=h;e.each(c.concat(u),r),F+=k,P+=D,e.each(d,r),e.each(g,r),t.chartArea={left:C,top:_,right:C+k,bottom:_+D},e.each(f,function(e){e.left=t.chartArea.left,e.top=t.chartArea.top,e.right=t.chartArea.right,e.bottom=t.chartArea.bottom,e.update(k,D)})}}}}},{}],28:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.global.legend={display:!0,position:"top",fullWidth:!0,reverse:!1,onClick:function(t,e){var i=this.chart.data.datasets[e.datasetIndex];i.hidden=!i.hidden,this.chart.update()},labels:{boxWidth:40,padding:10,generateLabels:function(t){return e.isArray(t.datasets)?t.datasets.map(function(t,e){return{text:t.label,fillStyle:t.backgroundColor,hidden:t.hidden,lineCap:t.borderCapStyle,lineDash:t.borderDash,lineDashOffset:t.borderDashOffset,lineJoin:t.borderJoinStyle,lineWidth:t.borderWidth,strokeStyle:t.borderColor,datasetIndex:e}},this):[]}}},t.Legend=t.Element.extend({initialize:function(t){e.extend(this,t),this.legendHitBoxes=[],this.doughnutMode=!1},beforeUpdate:e.noop,update:function(t,e,i){return this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this.margins=i,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this.beforeBuildLabels(),this.buildLabels(),this.afterBuildLabels(),this.beforeFit(),this.fit(),this.afterFit(),this.afterUpdate(),this.minSize},afterUpdate:e.noop,beforeSetDimensions:e.noop,setDimensions:function(){this.isHorizontal()?(this.width=this.maxWidth,this.left=0,this.right=this.width):(this.height=this.maxHeight,this.top=0,this.bottom=this.height),this.paddingLeft=0,this.paddingTop=0,this.paddingRight=0,this.paddingBottom=0,this.minSize={width:0,height:0}},afterSetDimensions:e.noop,beforeBuildLabels:e.noop,buildLabels:function(){this.legendItems=this.options.labels.generateLabels.call(this,this.chart.data),this.options.reverse&&this.legendItems.reverse()},afterBuildLabels:e.noop,beforeFit:e.noop,fit:function(){var i=this.ctx,a=e.getValueOrDefault(this.options.labels.fontSize,t.defaults.global.defaultFontSize),s=e.getValueOrDefault(this.options.labels.fontStyle,t.defaults.global.defaultFontStyle),o=e.getValueOrDefault(this.options.labels.fontFamily,t.defaults.global.defaultFontFamily),n=e.fontString(a,s,o);if(this.legendHitBoxes=[],this.isHorizontal()?this.minSize.width=this.maxWidth:this.minSize.width=this.options.display?10:0,this.isHorizontal()?this.minSize.height=this.options.display?10:0:this.minSize.height=this.maxHeight,this.options.display&&this.isHorizontal()){this.lineWidths=[0];var r=this.legendItems.length?a+this.options.labels.padding:0;i.textAlign="left",i.textBaseline="top",i.font=n,e.each(this.legendItems,function(t,e){var s=this.options.labels.boxWidth+a/2+i.measureText(t.text).width;this.lineWidths[this.lineWidths.length-1]+s+this.options.labels.padding>=this.width&&(r+=a+this.options.labels.padding,this.lineWidths[this.lineWidths.length]=this.left),this.legendHitBoxes[e]={left:0,top:0,width:s,height:a},this.lineWidths[this.lineWidths.length-1]+=s+this.options.labels.padding},this),this.minSize.height+=r}this.width=this.minSize.width,this.height=this.minSize.height},afterFit:e.noop,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){if(this.options.display){var i=this.ctx,a={x:this.left+(this.width-this.lineWidths[0])/2,y:this.top+this.options.labels.padding,line:0},s=e.getValueOrDefault(this.options.labels.fontColor,t.defaults.global.defaultFontColor),o=e.getValueOrDefault(this.options.labels.fontSize,t.defaults.global.defaultFontSize),n=e.getValueOrDefault(this.options.labels.fontStyle,t.defaults.global.defaultFontStyle),r=e.getValueOrDefault(this.options.labels.fontFamily,t.defaults.global.defaultFontFamily),l=e.fontString(o,n,r);this.isHorizontal()&&(i.textAlign="left",i.textBaseline="top",i.lineWidth=.5,i.strokeStyle=s,i.fillStyle=s,i.font=l,e.each(this.legendItems,function(e,s){var n=i.measureText(e.text).width,r=this.options.labels.boxWidth+o/2+n;a.x+r>=this.width&&(a.y+=o+this.options.labels.padding,a.line++,a.x=this.left+(this.width-this.lineWidths[a.line])/2),i.save();var l=function(t,e){return void 0!==t?t:e};i.fillStyle=l(e.fillStyle,t.defaults.global.defaultColor),i.lineCap=l(e.lineCap,t.defaults.global.elements.line.borderCapStyle),i.lineDashOffset=l(e.lineDashOffset,t.defaults.global.elements.line.borderDashOffset),i.lineJoin=l(e.lineJoin,t.defaults.global.elements.line.borderJoinStyle),i.lineWidth=l(e.lineWidth,t.defaults.global.elements.line.borderWidth),i.strokeStyle=l(e.strokeStyle,t.defaults.global.defaultColor),i.setLineDash&&i.setLineDash(l(e.lineDash,t.defaults.global.elements.line.borderDash)),i.strokeRect(a.x,a.y,this.options.labels.boxWidth,o),i.fillRect(a.x,a.y,this.options.labels.boxWidth,o),i.restore(),this.legendHitBoxes[s].left=a.x,this.legendHitBoxes[s].top=a.y,i.fillText(e.text,this.options.labels.boxWidth+o/2+a.x,a.y),e.hidden&&(i.beginPath(),i.lineWidth=2,i.moveTo(this.options.labels.boxWidth+o/2+a.x,a.y+o/2),i.lineTo(this.options.labels.boxWidth+o/2+a.x+n,a.y+o/2),i.stroke()),a.x+=r+this.options.labels.padding},this))}},handleEvent:function(t){var i=e.getRelativePosition(t,this.chart.chart);if(i.x>=this.left&&i.x<=this.right&&i.y>=this.top&&i.y<=this.bottom)for(var a=0;a=s.left&&i.x<=s.left+s.width&&i.y>=s.top&&i.y<=s.top+s.height){this.options.onClick&&this.options.onClick.call(this,t,this.legendItems[a]);break}}}})}},{}],29:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.scale={display:!0,gridLines:{display:!0,color:"rgba(0, 0, 0, 0.1)",lineWidth:1,drawOnChartArea:!0,drawTicks:!0,zeroLineWidth:1,zeroLineColor:"rgba(0,0,0,0.25)",offsetGridLines:!1},scaleLabel:{labelString:"",display:!1},ticks:{beginAtZero:!1,maxRotation:90,mirror:!1,padding:10,reverse:!1,display:!0,autoSkip:!0,autoSkipPadding:20,callback:function(t){return""+t}}},t.Scale=t.Element.extend({beforeUpdate:function(){e.callCallback(this.options.beforeUpdate,[this])},update:function(t,i,a){return this.beforeUpdate(),this.maxWidth=t,this.maxHeight=i,this.margins=e.extend({left:0,right:0,top:0,bottom:0},a),this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this.beforeBuildTicks(),this.buildTicks(),this.afterBuildTicks(),this.beforeTickToLabelConversion(),this.convertTicksToLabels(),this.afterTickToLabelConversion(),this.beforeCalculateTickRotation(),this.calculateTickRotation(),this.afterCalculateTickRotation(),this.beforeFit(),this.fit(),this.afterFit(),this.afterUpdate(),this.minSize},afterUpdate:function(){e.callCallback(this.options.afterUpdate,[this])},beforeSetDimensions:function(){e.callCallback(this.options.beforeSetDimensions,[this])},setDimensions:function(){this.isHorizontal()?(this.width=this.maxWidth,this.left=0,this.right=this.width):(this.height=this.maxHeight,this.top=0,this.bottom=this.height),this.paddingLeft=0,this.paddingTop=0,this.paddingRight=0,this.paddingBottom=0},afterSetDimensions:function(){e.callCallback(this.options.afterSetDimensions,[this])},beforeDataLimits:function(){e.callCallback(this.options.beforeDataLimits,[this])},determineDataLimits:e.noop,afterDataLimits:function(){e.callCallback(this.options.afterDataLimits,[this])},beforeBuildTicks:function(){e.callCallback(this.options.beforeBuildTicks,[this])},buildTicks:e.noop,afterBuildTicks:function(){e.callCallback(this.options.afterBuildTicks,[this])},beforeTickToLabelConversion:function(){e.callCallback(this.options.beforeTickToLabelConversion,[this])},convertTicksToLabels:function(){this.ticks=this.ticks.map(function(t,e,i){return this.options.ticks.userCallback?this.options.ticks.userCallback(t,e,i):this.options.ticks.callback(t,e,i)},this)},afterTickToLabelConversion:function(){e.callCallback(this.options.afterTickToLabelConversion,[this])},beforeCalculateTickRotation:function(){e.callCallback(this.options.beforeCalculateTickRotation,[this])},calculateTickRotation:function(){var i=e.getValueOrDefault(this.options.ticks.fontSize,t.defaults.global.defaultFontSize),a=e.getValueOrDefault(this.options.ticks.fontStyle,t.defaults.global.defaultFontStyle),s=e.getValueOrDefault(this.options.ticks.fontFamily,t.defaults.global.defaultFontFamily),o=e.fontString(i,a,s);this.ctx.font=o;var n,r=this.ctx.measureText(this.ticks[0]).width,l=this.ctx.measureText(this.ticks[this.ticks.length-1]).width;if(this.labelRotation=0,this.paddingRight=0,this.paddingLeft=0,this.options.display&&this.isHorizontal()){this.paddingRight=l/2+3,this.paddingLeft=r/2+3,this.longestTextCache||(this.longestTextCache={});for(var h,c,d=e.longestText(this.ctx,o,this.ticks,this.longestTextCache),u=d,g=this.getPixelForTick(1)-this.getPixelForTick(0)-6;u>g&&this.labelRotationthis.yLabelWidth&&(this.paddingLeft=n+i/2),this.paddingRight=i/2,c*d>this.maxHeight){this.labelRotation--;break}this.labelRotation++,u=h*d}}this.margins&&(this.paddingLeft=Math.max(this.paddingLeft-this.margins.left,0),this.paddingRight=Math.max(this.paddingRight-this.margins.right,0))},afterCalculateTickRotation:function(){e.callCallback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){e.callCallback(this.options.beforeFit,[this])},fit:function(){this.minSize={width:0,height:0};var i=e.getValueOrDefault(this.options.ticks.fontSize,t.defaults.global.defaultFontSize),a=e.getValueOrDefault(this.options.ticks.fontStyle,t.defaults.global.defaultFontStyle),s=e.getValueOrDefault(this.options.ticks.fontFamily,t.defaults.global.defaultFontFamily),o=e.fontString(i,a,s),n=e.getValueOrDefault(this.options.scaleLabel.fontSize,t.defaults.global.defaultFontSize),r=e.getValueOrDefault(this.options.scaleLabel.fontStyle,t.defaults.global.defaultFontStyle),l=e.getValueOrDefault(this.options.scaleLabel.fontFamily,t.defaults.global.defaultFontFamily);e.fontString(n,r,l);if(this.isHorizontal()?this.minSize.width=this.isFullWidth()?this.maxWidth-this.margins.left-this.margins.right:this.maxWidth:this.minSize.width=this.options.gridLines.display&&this.options.display?10:0,this.isHorizontal()?this.minSize.height=this.options.gridLines.display&&this.options.display?10:0:this.minSize.height=this.maxHeight,this.options.scaleLabel.display&&(this.isHorizontal()?this.minSize.height+=1.5*n:this.minSize.width+=1.5*n),this.options.ticks.display&&this.options.display){this.longestTextCache||(this.longestTextCache={});var h=e.longestText(this.ctx,o,this.ticks,this.longestTextCache);if(this.isHorizontal()){this.longestLabelWidth=h;var c=Math.sin(e.toRadians(this.labelRotation))*this.longestLabelWidth+1.5*i;this.minSize.height=Math.min(this.maxHeight,this.minSize.height+c),this.ctx.font=o;var d=this.ctx.measureText(this.ticks[0]).width,u=this.ctx.measureText(this.ticks[this.ticks.length-1]).width,g=Math.cos(e.toRadians(this.labelRotation)),f=Math.sin(e.toRadians(this.labelRotation));this.paddingLeft=0!==this.labelRotation?g*d+3:d/2+3,this.paddingRight=0!==this.labelRotation?f*(i/2)+3:u/2+3}else{var m=this.maxWidth-this.minSize.width;this.options.ticks.mirror||(h+=this.options.ticks.padding),m>h?this.minSize.width+=h:this.minSize.width=this.maxWidth,this.paddingTop=i/2,this.paddingBottom=i/2}}this.margins&&(this.paddingLeft=Math.max(this.paddingLeft-this.margins.left,0),this.paddingTop=Math.max(this.paddingTop-this.margins.top,0),this.paddingRight=Math.max(this.paddingRight-this.margins.right,0),this.paddingBottom=Math.max(this.paddingBottom-this.margins.bottom,0)),this.width=this.minSize.width,this.height=this.minSize.height},afterFit:function(){e.callCallback(this.options.afterFit,[this])},isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},isFullWidth:function(){return this.options.fullWidth},getRightValue:function i(t){return null===t||"undefined"==typeof t?NaN:"number"==typeof t&&isNaN(t)?NaN:"object"==typeof t?t instanceof Date?t:i(this.isHorizontal()?t.x:t.y):t},getLabelForIndex:e.noop,getPixelForValue:e.noop,getPixelForTick:function(t,e){if(this.isHorizontal()){var i=this.width-(this.paddingLeft+this.paddingRight),a=i/Math.max(this.ticks.length-(this.options.gridLines.offsetGridLines?0:1),1),s=a*t+this.paddingLeft;e&&(s+=a/2);var o=this.left+Math.round(s);return o+=this.isFullWidth()?this.margins.left:0}var n=this.height-(this.paddingTop+this.paddingBottom);return this.top+t*(n/(this.ticks.length-1))},getPixelForDecimal:function(t){if(this.isHorizontal()){var e=this.width-(this.paddingLeft+this.paddingRight),i=e*t+this.paddingLeft,a=this.left+Math.round(i);return a+=this.isFullWidth()?this.margins.left:0}return this.top+t*this.height},draw:function(i){if(this.options.display){var a,s,o,n,r,l=0!==this.labelRotation,h=this.options.ticks.autoSkip;this.options.ticks.maxTicksLimit&&(r=this.options.ticks.maxTicksLimit);var c=e.getValueOrDefault(this.options.ticks.fontColor,t.defaults.global.defaultFontColor),d=e.getValueOrDefault(this.options.ticks.fontSize,t.defaults.global.defaultFontSize),u=e.getValueOrDefault(this.options.ticks.fontStyle,t.defaults.global.defaultFontStyle),g=e.getValueOrDefault(this.options.ticks.fontFamily,t.defaults.global.defaultFontFamily),f=e.fontString(d,u,g),m=e.getValueOrDefault(this.options.scaleLabel.fontColor,t.defaults.global.defaultFontColor),p=e.getValueOrDefault(this.options.scaleLabel.fontSize,t.defaults.global.defaultFontSize),b=e.getValueOrDefault(this.options.scaleLabel.fontStyle,t.defaults.global.defaultFontStyle),x=e.getValueOrDefault(this.options.scaleLabel.fontFamily,t.defaults.global.defaultFontFamily),v=e.fontString(p,b,x),y=Math.cos(e.toRadians(this.labelRotation)),k=(Math.sin(e.toRadians(this.labelRotation)),this.longestLabelWidth*y);if(this.ctx.fillStyle=c,this.isHorizontal()){a=!0;var D="bottom"===this.options.position?this.top:this.bottom-10,S="bottom"===this.options.position?this.top+10:this.bottom;if(s=!1,(k/2+this.options.ticks.autoSkipPadding)*this.ticks.length>this.width-(this.paddingLeft+this.paddingRight)&&(s=1+Math.floor((k/2+this.options.ticks.autoSkipPadding)*this.ticks.length/(this.width-(this.paddingLeft+this.paddingRight)))),r&&this.ticks.length>r)for(;!s||this.ticks.length/(s||1)>r;)s||(s=1),s+=1;h||(s=!1),e.each(this.ticks,function(t,o){var n=this.ticks.length===o+1,r=s>1&&o%s>0||o%s===0&&o+s>this.ticks.length;if((!r||n)&&void 0!==t&&null!==t){var h=this.getPixelForTick(o),c=this.getPixelForTick(o,this.options.gridLines.offsetGridLines);this.options.gridLines.display&&(o===("undefined"!=typeof this.zeroLineIndex?this.zeroLineIndex:0)?(this.ctx.lineWidth=this.options.gridLines.zeroLineWidth,this.ctx.strokeStyle=this.options.gridLines.zeroLineColor,a=!0):a&&(this.ctx.lineWidth=this.options.gridLines.lineWidth,this.ctx.strokeStyle=this.options.gridLines.color,a=!1),h+=e.aliasPixel(this.ctx.lineWidth),this.ctx.beginPath(),this.options.gridLines.drawTicks&&(this.ctx.moveTo(h,D),this.ctx.lineTo(h,S)),this.options.gridLines.drawOnChartArea&&(this.ctx.moveTo(h,i.top),this.ctx.lineTo(h,i.bottom)),this.ctx.stroke()),this.options.ticks.display&&(this.ctx.save(),this.ctx.translate(c,l?this.top+12:"top"===this.options.position?this.bottom-10:this.top+10),this.ctx.rotate(-1*e.toRadians(this.labelRotation)),this.ctx.font=f,this.ctx.textAlign=l?"right":"center",this.ctx.textBaseline=l?"middle":"top"===this.options.position?"bottom":"top",this.ctx.fillText(t,0,0),this.ctx.restore())}},this),this.options.scaleLabel.display&&(this.ctx.textAlign="center",this.ctx.textBaseline="middle",this.ctx.fillStyle=m,this.ctx.font=v,o=this.left+(this.right-this.left)/2,n="bottom"===this.options.position?this.bottom-p/2:this.top+p/2,this.ctx.fillText(this.options.scaleLabel.labelString,o,n))}else{a=!0;var C="right"===this.options.position?this.left:this.right-5,w="right"===this.options.position?this.left+5:this.right;if(e.each(this.ticks,function(t,s){if(void 0!==t&&null!==t){var o=this.getPixelForTick(s);if(this.options.gridLines.display&&(s===("undefined"!=typeof this.zeroLineIndex?this.zeroLineIndex:0)?(this.ctx.lineWidth=this.options.gridLines.zeroLineWidth,this.ctx.strokeStyle=this.options.gridLines.zeroLineColor,a=!0):a&&(this.ctx.lineWidth=this.options.gridLines.lineWidth,this.ctx.strokeStyle=this.options.gridLines.color,a=!1),o+=e.aliasPixel(this.ctx.lineWidth),this.ctx.beginPath(),this.options.gridLines.drawTicks&&(this.ctx.moveTo(C,o),this.ctx.lineTo(w,o)),this.options.gridLines.drawOnChartArea&&(this.ctx.moveTo(i.left,o),this.ctx.lineTo(i.right,o)),this.ctx.stroke()),this.options.ticks.display){var n,r=this.getPixelForTick(s,this.options.gridLines.offsetGridLines);this.ctx.save(),"left"===this.options.position?this.options.ticks.mirror?(n=this.right+this.options.ticks.padding,this.ctx.textAlign="left"):(n=this.right-this.options.ticks.padding,this.ctx.textAlign="right"):this.options.ticks.mirror?(n=this.left-this.options.ticks.padding,this.ctx.textAlign="right"):(n=this.left+this.options.ticks.padding,this.ctx.textAlign="left"),this.ctx.translate(n,r),this.ctx.rotate(-1*e.toRadians(this.labelRotation)),this.ctx.font=f,this.ctx.textBaseline="middle",this.ctx.fillText(t,0,0),this.ctx.restore()}}},this),this.options.scaleLabel.display){o="left"===this.options.position?this.left+p/2:this.right-p/2,n=this.top+(this.bottom-this.top)/2;var _="left"===this.options.position?-.5*Math.PI:.5*Math.PI;this.ctx.save(),this.ctx.translate(o,n),this.ctx.rotate(_),this.ctx.textAlign="center",this.ctx.fillStyle=m,this.ctx.font=v,this.ctx.textBaseline="middle",this.ctx.fillText(this.options.scaleLabel.labelString,0,0),this.ctx.restore()}}this.ctx.lineWidth=this.options.gridLines.lineWidth,this.ctx.strokeStyle=this.options.gridLines.color;var A=this.left,I=this.right,M=this.top,F=this.bottom;this.isHorizontal()?(M=F="top"===this.options.position?this.bottom:this.top,M+=e.aliasPixel(this.ctx.lineWidth),F+=e.aliasPixel(this.ctx.lineWidth)):(A=I="left"===this.options.position?this.right:this.left,A+=e.aliasPixel(this.ctx.lineWidth),I+=e.aliasPixel(this.ctx.lineWidth)),this.ctx.beginPath(),this.ctx.moveTo(A,M),this.ctx.lineTo(I,F),this.ctx.stroke()}}})}},{}],30:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.scaleService={constructors:{},defaults:{},registerScaleType:function(t,i,a){this.constructors[t]=i,this.defaults[t]=e.clone(a)},getScaleConstructor:function(t){return this.constructors.hasOwnProperty(t)?this.constructors[t]:void 0},getScaleDefaults:function(i){return this.defaults.hasOwnProperty(i)?e.scaleMerge(t.defaults.scale,this.defaults[i]):{}},addScalesToLayout:function(i){e.each(i.scales,function(e){t.layoutService.addBox(i,e)})}}}},{}],31:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.global.title={display:!1,position:"top",fullWidth:!0,fontStyle:"bold",padding:10,text:""},t.Title=t.Element.extend({initialize:function(i){e.extend(this,i),this.options=e.configMerge(t.defaults.global.title,i.options),this.legendHitBoxes=[]},beforeUpdate:e.noop,update:function(t,e,i){return this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this.margins=i,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this.beforeBuildLabels(),this.buildLabels(),this.afterBuildLabels(),this.beforeFit(),this.fit(),this.afterFit(),this.afterUpdate(),this.minSize},afterUpdate:e.noop,beforeSetDimensions:e.noop,setDimensions:function(){this.isHorizontal()?(this.width=this.maxWidth,this.left=0,this.right=this.width):(this.height=this.maxHeight,this.top=0,this.bottom=this.height),this.paddingLeft=0,this.paddingTop=0,this.paddingRight=0,this.paddingBottom=0,this.minSize={width:0,height:0}},afterSetDimensions:e.noop,beforeBuildLabels:e.noop,buildLabels:e.noop,afterBuildLabels:e.noop,beforeFit:e.noop,fit:function(){var i=(this.ctx,e.getValueOrDefault(this.options.fontSize,t.defaults.global.defaultFontSize)),a=e.getValueOrDefault(this.options.fontStyle,t.defaults.global.defaultFontStyle),s=e.getValueOrDefault(this.options.fontFamily,t.defaults.global.defaultFontFamily);e.fontString(i,a,s);this.isHorizontal()?this.minSize.width=this.maxWidth:this.minSize.width=0,this.isHorizontal()?this.minSize.height=0:this.minSize.height=this.maxHeight,this.isHorizontal()?this.options.display&&(this.minSize.height+=i+2*this.options.padding):this.options.display&&(this.minSize.width+=i+2*this.options.padding),this.width=this.minSize.width,this.height=this.minSize.height},afterFit:e.noop,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){if(this.options.display){var i,a,s=this.ctx,o=e.getValueOrDefault(this.options.fontColor,t.defaults.global.defaultFontColor),n=e.getValueOrDefault(this.options.fontSize,t.defaults.global.defaultFontSize),r=e.getValueOrDefault(this.options.fontStyle,t.defaults.global.defaultFontStyle),l=e.getValueOrDefault(this.options.fontFamily,t.defaults.global.defaultFontFamily),h=e.fontString(n,r,l);if(s.fillStyle=o,s.font=h,this.isHorizontal())s.textAlign="center",s.textBaseline="middle",i=this.left+(this.right-this.left)/2,a=this.top+(this.bottom-this.top)/2,s.fillText(this.options.text,i,a);else{i="left"===this.options.position?this.left+n/2:this.right-n/2,a=this.top+(this.bottom-this.top)/2;var c="left"===this.options.position?-.5*Math.PI:.5*Math.PI;s.save(),s.translate(i,a),s.rotate(c),s.textAlign="center",s.textBaseline="middle",s.fillText(this.options.text,0,0),s.restore()}}}})}},{}],32:[function(t,e,i){"use strict";e.exports=function(t){function e(t,e){return e&&(i.isArray(e)?t=t.concat(e):t.push(e)),t}var i=t.helpers;t.defaults.global.tooltips={enabled:!0,custom:null,mode:"single",backgroundColor:"rgba(0,0,0,0.8)",titleFontStyle:"bold",titleSpacing:2,titleMarginBottom:6,titleColor:"#fff",titleAlign:"left",bodySpacing:2,bodyColor:"#fff",bodyAlign:"left",footerFontStyle:"bold",footerSpacing:2,footerMarginTop:6,footerColor:"#fff",footerAlign:"left",yPadding:6,xPadding:6,yAlign:"center",xAlign:"center",caretSize:5,cornerRadius:6,multiKeyBackground:"#fff",callbacks:{beforeTitle:i.noop,title:function(t,e){var i="";return t.length>0&&(t[0].xLabel?i=t[0].xLabel:e.labels.length>0&&t[0].indexthis._chart.height-t.height&&(this._model.yAlign="bottom");var e,i,a,s,o,n=this,r=(this._chartInstance.chartArea.left+this._chartInstance.chartArea.right)/2,l=(this._chartInstance.chartArea.top+this._chartInstance.chartArea.bottom)/2;"center"===this._model.yAlign?(e=function(t){return r>=t},i=function(t){return t>r}):(e=function(e){return e<=t.width/2},i=function(e){return e>=n._chart.width-t.width/2}),a=function(e){return e+t.width>n._chart.width},s=function(e){return e-t.width<0},o=function(t){return l>=t?"top":"bottom"},e(this._model.x)?(this._model.xAlign="left",a(this._model.x)&&(this._model.xAlign="center",this._model.yAlign=o(this._model.y))):i(this._model.x)&&(this._model.xAlign="right",s(this._model.x)&&(this._model.xAlign="center",this._model.yAlign=o(this._model.y)))},getBackgroundPoint:function(t,e){var i={x:t.x,y:t.y};return"right"===t.xAlign?i.x-=e.width:"center"===t.xAlign&&(i.x-=e.width/2),"top"===t.yAlign?i.y+=t.caretPadding+t.caretSize:"bottom"===t.yAlign?i.y-=e.height+t.caretPadding+t.caretSize:i.y-=e.height/2,"center"===t.yAlign?"left"===t.xAlign?i.x+=t.caretPadding+t.caretSize:"right"===t.xAlign&&(i.x-=t.caretPadding+t.caretSize):"left"===t.xAlign?i.x-=t.cornerRadius+t.caretPadding:"right"===t.xAlign&&(i.x+=t.cornerRadius+t.caretPadding),i},drawCaret:function(t,e,a,s){var o,n,r,l,h,c,d=this._view,u=this._chart.ctx;"center"===d.yAlign?("left"===d.xAlign?(o=t.x,n=o-d.caretSize,r=o):(o=t.x+e.width,n=o+d.caretSize,r=o),h=t.y+e.height/2,l=h-d.caretSize,c=h+d.caretSize):("left"===d.xAlign?(o=t.x+d.cornerRadius,n=o+d.caretSize,r=n+d.caretSize):"right"===d.xAlign?(o=t.x+e.width-d.cornerRadius,n=o-d.caretSize,r=n-d.caretSize):(n=t.x+e.width/2,o=n-d.caretSize,r=n+d.caretSize),"top"===d.yAlign?(l=t.y,h=l-d.caretSize,c=l):(l=t.y+e.height,h=l+d.caretSize,c=l));var g=i.color(d.backgroundColor);u.fillStyle=g.alpha(a*g.alpha()).rgbString(),u.beginPath(),u.moveTo(o,l),u.lineTo(n,h),u.lineTo(r,c),u.closePath(),u.fill()},drawTitle:function(t,e,a,s){if(e.title.length){a.textAlign=e._titleAlign,a.textBaseline="top";var o=i.color(e.titleColor);a.fillStyle=o.alpha(s*o.alpha()).rgbString(),a.font=i.fontString(e.titleFontSize,e._titleFontStyle,e._titleFontFamily),i.each(e.title,function(i,s){a.fillText(i,t.x,t.y),t.y+=e.titleFontSize+e.titleSpacing,s+1===e.title.length&&(t.y+=e.titleMarginBottom-e.titleSpacing)})}},drawBody:function(t,e,a,s){a.textAlign=e._bodyAlign,a.textBaseline="top";var o=i.color(e.bodyColor);a.fillStyle=o.alpha(s*o.alpha()).rgbString(),a.font=i.fontString(e.bodyFontSize,e._bodyFontStyle,e._bodyFontFamily),i.each(e.beforeBody,function(i){a.fillText(i,t.x,t.y),t.y+=e.bodyFontSize+e.bodySpacing}),i.each(e.body,function(o,n){"single"!==this._options.tooltips.mode&&(a.fillStyle=i.color(e.legendColorBackground).alpha(s).rgbaString(),a.fillRect(t.x,t.y,e.bodyFontSize,e.bodyFontSize),a.strokeStyle=i.color(e.labelColors[n].borderColor).alpha(s).rgbaString(),a.strokeRect(t.x,t.y,e.bodyFontSize,e.bodyFontSize),a.fillStyle=i.color(e.labelColors[n].backgroundColor).alpha(s).rgbaString(),a.fillRect(t.x+1,t.y+1,e.bodyFontSize-2,e.bodyFontSize-2),a.fillStyle=i.color(e.bodyColor).alpha(s).rgbaString()),a.fillText(o,t.x+("single"!==this._options.tooltips.mode?e.bodyFontSize+2:0),t.y),t.y+=e.bodyFontSize+e.bodySpacing},this),i.each(e.afterBody,function(i){a.fillText(i,t.x,t.y),t.y+=e.bodyFontSize}),t.y-=e.bodySpacing},drawFooter:function(t,e,a,s){if(e.footer.length){t.y+=e.footerMarginTop,a.textAlign=e._footerAlign,a.textBaseline="top";var o=i.color(e.footerColor);a.fillStyle=o.alpha(s*o.alpha()).rgbString(),a.font=i.fontString(e.footerFontSize,e._footerFontStyle,e._footerFontFamily),i.each(e.footer,function(i){a.fillText(i,t.x,t.y),t.y+=e.footerFontSize+e.footerSpacing})}},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var a=e.caretPadding,s=this.getTooltipSize(e),o={x:e.x,y:e.y},n=Math.abs(e.opacity<.001)?0:e.opacity;if(this._options.tooltips.enabled){var r=i.color(e.backgroundColor);t.fillStyle=r.alpha(n*r.alpha()).rgbString(),i.drawRoundedRectangle(t,o.x,o.y,s.width,s.height,e.cornerRadius),t.fill(),this.drawCaret(o,s,n,a),o.x+=e.xPadding,o.y+=e.yPadding,this.drawTitle(o,e,t,n),this.drawBody(o,e,t,n),this.drawFooter(o,e,t,n)}}}})}},{}],33:[function(t,e,i){"use strict";e.exports=function(t,e){var i=t.helpers;t.defaults.global.elements.arc={backgroundColor:t.defaults.global.defaultColor,borderColor:"#fff",borderWidth:2},t.elements.Arc=t.Element.extend({inLabelRange:function(t){var e=this._view;return e?Math.pow(t-e.x,2)1.5*Math.PI?a.startAngle-2*Math.PI:a.startAngle,n=a.endAngle<-.5*Math.PI?a.endAngle+2*Math.PI:a.endAngle>1.5*Math.PI?a.endAngle-2*Math.PI:a.endAngle,r=s.angle>=o&&s.angle<=n,l=s.distance>=a.innerRadius&&s.distance<=a.outerRadius;return r&&l}return!1},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,i=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*i,y:t.y+Math.sin(e)*i}},draw:function(){var t=this._chart.ctx,e=this._view;t.beginPath(),t.arc(e.x,e.y,e.outerRadius,e.startAngle,e.endAngle),t.arc(e.x,e.y,e.innerRadius,e.endAngle,e.startAngle,!0),t.closePath(),t.strokeStyle=e.borderColor,t.lineWidth=e.borderWidth,t.fillStyle=e.backgroundColor,t.fill(),t.lineJoin="bevel",e.borderWidth&&t.stroke()}})}},{}],34:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.global.elements.line={tension:.4,backgroundColor:t.defaults.global.defaultColor,borderWidth:3,borderColor:t.defaults.global.defaultColor,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",fill:!0},t.elements.Line=t.Element.extend({lineToNextPoint:function(t,e,i,a,s){var o=this._chart.ctx;e._view.skip?a.call(this,t,e,i):t._view.skip?s.call(this,t,e,i):0===e._view.tension?o.lineTo(e._view.x,e._view.y):o.bezierCurveTo(t._view.controlPointNextX,t._view.controlPointNextY,e._view.controlPointPreviousX,e._view.controlPointPreviousY,e._view.x,e._view.y)},draw:function(){function i(t){n._view.skip||r._view.skip?t&&o.lineTo(a._view.scaleZero.x,a._view.scaleZero.y):o.bezierCurveTo(r._view.controlPointNextX,r._view.controlPointNextY,n._view.controlPointPreviousX,n._view.controlPointPreviousY,n._view.x,n._view.y)}var a=this,s=this._view,o=this._chart.ctx,n=this._children[0],r=this._children[this._children.length-1];o.save(),this._children.length>0&&s.fill&&(o.beginPath(),e.each(this._children,function(t,i){var a=e.previousItem(this._children,i),n=e.nextItem(this._children,i);0===i?(this._loop?o.moveTo(s.scaleZero.x,s.scaleZero.y):o.moveTo(t._view.x,s.scaleZero),t._view.skip?this._loop||o.moveTo(n._view.x,this._view.scaleZero):o.lineTo(t._view.x,t._view.y)):this.lineToNextPoint(a,t,n,function(t,e,i){this._loop?o.lineTo(this._view.scaleZero.x,this._view.scaleZero.y):(o.lineTo(t._view.x,this._view.scaleZero),o.moveTo(i._view.x,this._view.scaleZero))},function(t,e){o.lineTo(e._view.x,e._view.y)})},this),this._loop?i(!0):(o.lineTo(this._children[this._children.length-1]._view.x,s.scaleZero),o.lineTo(this._children[0]._view.x,s.scaleZero)),o.fillStyle=s.backgroundColor||t.defaults.global.defaultColor,o.closePath(),o.fill()),o.lineCap=s.borderCapStyle||t.defaults.global.elements.line.borderCapStyle,o.setLineDash&&o.setLineDash(s.borderDash||t.defaults.global.elements.line.borderDash),o.lineDashOffset=s.borderDashOffset||t.defaults.global.elements.line.borderDashOffset,o.lineJoin=s.borderJoinStyle||t.defaults.global.elements.line.borderJoinStyle,o.lineWidth=s.borderWidth||t.defaults.global.elements.line.borderWidth,o.strokeStyle=s.borderColor||t.defaults.global.defaultColor,o.beginPath(),e.each(this._children,function(t,i){var a=e.previousItem(this._children,i),s=e.nextItem(this._children,i);0===i?o.moveTo(t._view.x,t._view.y):this.lineToNextPoint(a,t,s,function(t,e,i){o.moveTo(i._view.x,i._view.y)},function(t,e){o.moveTo(e._view.x,e._view.y)})},this),this._loop&&this._children.length>0&&i(),o.stroke(),o.restore()}})}},{}],35:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.global.elements.point={radius:3,pointStyle:"circle",backgroundColor:t.defaults.global.defaultColor,borderWidth:1,borderColor:t.defaults.global.defaultColor,hitRadius:1,hoverRadius:4,hoverBorderWidth:1},t.elements.Point=t.Element.extend({inRange:function(t,e){var i=this._view;if(i){var a=i.hitRadius+i.radius;return Math.pow(t-i.x,2)+Math.pow(e-i.y,2)0){a.strokeStyle=i.borderColor||t.defaults.global.defaultColor,a.lineWidth=e.getValueOrDefault(i.borderWidth,t.defaults.global.elements.point.borderWidth),a.fillStyle=i.backgroundColor||t.defaults.global.defaultColor;var s,o,n=i.radius;switch(i.pointStyle){default:a.beginPath(),a.arc(i.x,i.y,n,0,2*Math.PI),a.closePath(),a.fill();break;case"triangle":a.beginPath();var r=3*n/Math.sqrt(3),l=r*Math.sqrt(3)/2;a.moveTo(i.x-r/2,i.y+l/3),a.lineTo(i.x+r/2,i.y+l/3),a.lineTo(i.x,i.y-2*l/3),a.closePath(),a.fill();break;case"rect":a.fillRect(i.x-1/Math.SQRT2*n,i.y-1/Math.SQRT2*n,2/Math.SQRT2*n,2/Math.SQRT2*n),a.strokeRect(i.x-1/Math.SQRT2*n,i.y-1/Math.SQRT2*n,2/Math.SQRT2*n,2/Math.SQRT2*n);break;case"rectRot":a.translate(i.x,i.y),a.rotate(Math.PI/4),a.fillRect(-1/Math.SQRT2*n,-1/Math.SQRT2*n,2/Math.SQRT2*n,2/Math.SQRT2*n),a.strokeRect(-1/Math.SQRT2*n,-1/Math.SQRT2*n,2/Math.SQRT2*n,2/Math.SQRT2*n),a.setTransform(1,0,0,1,0,0);break;case"cross":a.beginPath(),a.moveTo(i.x,i.y+n),a.lineTo(i.x,i.y-n),a.moveTo(i.x-n,i.y),a.lineTo(i.x+n,i.y),a.closePath();break;case"crossRot":a.beginPath(),s=Math.cos(Math.PI/4)*n,o=Math.sin(Math.PI/4)*n,a.moveTo(i.x-s,i.y-o),a.lineTo(i.x+s,i.y+o),a.moveTo(i.x-s,i.y+o),a.lineTo(i.x+s,i.y-o),a.closePath();break;case"star":a.beginPath(),a.moveTo(i.x,i.y+n),a.lineTo(i.x,i.y-n),a.moveTo(i.x-n,i.y),a.lineTo(i.x+n,i.y),s=Math.cos(Math.PI/4)*n,o=Math.sin(Math.PI/4)*n,a.moveTo(i.x-s,i.y-o),a.lineTo(i.x+s,i.y+o),a.moveTo(i.x-s,i.y+o),a.lineTo(i.x+s,i.y-o),a.closePath();break;case"line":a.beginPath(),a.moveTo(i.x-n,i.y),a.lineTo(i.x+n,i.y),a.closePath();break;case"dash":a.beginPath(),a.moveTo(i.x,i.y),a.lineTo(i.x+n,i.y),a.closePath()}a.stroke()}}}})}},{}],36:[function(t,e,i){"use strict";e.exports=function(t){t.helpers;t.defaults.global.elements.rectangle={backgroundColor:t.defaults.global.defaultColor,borderWidth:0,borderColor:t.defaults.global.defaultColor,borderSkipped:"bottom"},t.elements.Rectangle=t.Element.extend({draw:function(){function t(t){return l[(c+t)%4]}var e=this._chart.ctx,i=this._view,a=i.width/2,s=i.x-a,o=i.x+a,n=i.base-(i.base-i.y),r=i.borderWidth/2;i.borderWidth&&(s+=r,o-=r,n+=r),e.beginPath(),e.fillStyle=i.backgroundColor,e.strokeStyle=i.borderColor,e.lineWidth=i.borderWidth;var l=[[s,i.base],[s,n],[o,n],[o,i.base]],h=["bottom","left","top","right"],c=h.indexOf(i.borderSkipped,0);-1===c&&(c=0),e.moveTo.apply(e,t(0));for(var d=1;4>d;d++)e.lineTo.apply(e,t(d));e.fill(),i.borderWidth&&e.stroke()},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){var i=this._view,a=!1;return i&&(a=i.y=i.x-i.width/2&&t<=i.x+i.width/2&&e>=i.y&&e<=i.base:t>=i.x-i.width/2&&t<=i.x+i.width/2&&e>=i.base&&e<=i.y),a},inLabelRange:function(t){var e=this._view;return e?t>=e.x-e.width/2&&t<=e.x+e.width/2:!1},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}})}},{}],37:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers,i={position:"bottom"},a=t.Scale.extend({buildTicks:function(t){this.startIndex=0,this.endIndex=this.chart.data.labels.length;var i;void 0!==this.options.ticks.min&&(i=e.indexOf(this.chart.data.labels,this.options.ticks.min),this.startIndex=-1!==i?i:this.startIndex),void 0!==this.options.ticks.max&&(i=e.indexOf(this.chart.data.labels,this.options.ticks.max),this.endIndex=-1!==i?i:this.endIndex),this.ticks=0===this.startIndex&&this.endIndex===this.chart.data.labels.length?this.chart.data.labels:this.chart.data.labels.slice(this.startIndex,this.endIndex+1)},getLabelForIndex:function(t,e){return this.ticks[t]},getPixelForValue:function(t,e,i,a){var s=Math.max(this.ticks.length-(this.options.gridLines.offsetGridLines?0:1),1);if(this.isHorizontal()){var o=this.width-(this.paddingLeft+this.paddingRight),n=o/s,r=n*(e-this.startIndex)+this.paddingLeft;return this.options.gridLines.offsetGridLines&&a&&(r+=n/2),this.left+Math.round(r)}var l=this.height-(this.paddingTop+this.paddingBottom),h=l/s,c=h*(e-this.startIndex)+this.paddingTop;return this.options.gridLines.offsetGridLines&&a&&(c+=h/2),this.top+Math.round(c)},getPixelForTick:function(t,e){return this.getPixelForValue(this.ticks[t],t+this.startIndex,null,e)}});t.scaleService.registerScaleType("category",a,i)}},{}],38:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers,i={position:"left",ticks:{callback:function(t,i,a){var s=a[1]-a[0];Math.abs(s)>1&&t!==Math.floor(t)&&(s=t-Math.floor(t));var o=e.log10(Math.abs(s)),n="";if(0!==t){var r=-1*Math.floor(o);r=Math.max(Math.min(r,20),0),n=t.toFixed(r)}else n="0";return n}}},a=t.Scale.extend({determineDataLimits:function(){if(this.min=null,this.max=null,this.options.stacked){var t={},i=!1,a=!1;e.each(this.chart.data.datasets,function(s){void 0===t[s.type]&&(t[s.type]={positiveValues:[],negativeValues:[]});var o=t[s.type].positiveValues,n=t[s.type].negativeValues;e.isDatasetVisible(s)&&(this.isHorizontal()?s.xAxisID===this.id:s.yAxisID===this.id)&&e.each(s.data,function(t,e){var s=+this.getRightValue(t);isNaN(s)||(o[e]=o[e]||0,n[e]=n[e]||0,this.options.relativePoints?o[e]=100:0>s?(a=!0,n[e]+=s):(i=!0,o[e]+=s))},this)},this),e.each(t,function(t){var i=t.positiveValues.concat(t.negativeValues),a=e.min(i),s=e.max(i);this.min=null===this.min?a:Math.min(this.min,a),this.max=null===this.max?s:Math.max(this.max,s)},this)}else e.each(this.chart.data.datasets,function(t){e.isDatasetVisible(t)&&(this.isHorizontal()?t.xAxisID===this.id:t.yAxisID===this.id)&&e.each(t.data,function(t,e){var i=+this.getRightValue(t);isNaN(i)||(null===this.min?this.min=i:ithis.max&&(this.max=i))},this)},this);if(this.options.ticks.beginAtZero){var s=e.sign(this.min),o=e.sign(this.max);0>s&&0>o?this.max=0:s>0&&o>0&&(this.min=0)}void 0!==this.options.ticks.min?this.min=this.options.ticks.min:void 0!==this.options.ticks.suggestedMin&&(this.min=Math.min(this.min,this.options.ticks.suggestedMin)),void 0!==this.options.ticks.max?this.max=this.options.ticks.max:void 0!==this.options.ticks.suggestedMax&&(this.max=Math.max(this.max,this.options.ticks.suggestedMax)),this.min===this.max&&(this.min--,this.max++)},buildTicks:function(){this.ticks=[];var i;if(this.isHorizontal())i=Math.min(this.options.ticks.maxTicksLimit?this.options.ticks.maxTicksLimit:11,Math.ceil(this.width/50));else{var a=e.getValueOrDefault(this.options.ticks.fontSize,t.defaults.global.defaultFontSize);i=Math.min(this.options.ticks.maxTicksLimit?this.options.ticks.maxTicksLimit:11,Math.ceil(this.height/(2*a)))}i=Math.max(2,i);var s,o=this.options.ticks.fixedStepSize&&this.options.ticks.fixedStepSize>0||this.options.ticks.stepSize&&this.options.ticks.stepSize>0;if(o)s=e.getValueOrDefault(this.options.ticks.fixedStepSize,this.options.ticks.stepSize);else{var n=e.niceNum(this.max-this.min,!1);s=e.niceNum(n/(i-1),!0)}var r=Math.floor(this.min/s)*s,l=Math.ceil(this.max/s)*s,h=(l-r)/s;h=e.almostEquals(h,Math.round(h),s/1e3)?Math.round(h):Math.ceil(h),this.ticks.push(void 0!==this.options.ticks.min?this.options.ticks.min:r);for(var c=1;h>c;++c)this.ticks.push(r+c*s);this.ticks.push(void 0!==this.options.ticks.max?this.options.ticks.max:l),("left"===this.options.position||"right"===this.options.position)&&this.ticks.reverse(),this.max=e.max(this.ticks),this.min=e.min(this.ticks),this.options.ticks.reverse?(this.ticks.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},convertTicksToLabels:function(){this.ticksAsNumbers=this.ticks.slice(),this.zeroLineIndex=this.ticks.indexOf(0),t.Scale.prototype.convertTicksToLabels.call(this)},getPixelForValue:function(t,e,i,a){var s,o=+this.getRightValue(t),n=this.end-this.start;if(this.isHorizontal()){var r=this.width-(this.paddingLeft+this.paddingRight);return s=this.left+r/n*(o-this.start),Math.round(s+this.paddingLeft)}var l=this.height-(this.paddingTop+this.paddingBottom);return s=this.bottom-this.paddingBottom-l/n*(o-this.start),Math.round(s)},getPixelForTick:function(t,e){return this.getPixelForValue(this.ticksAsNumbers[t],null,null,e)}});t.scaleService.registerScaleType("linear",a,i)}},{}],39:[function(t,e,i){"use strict";e.exports=function(t){var e=t.helpers,i={position:"left",ticks:{callback:function(e,i,a){var s=e/Math.pow(10,Math.floor(t.helpers.log10(e)));return 1===s||2===s||5===s||0===i||i===a.length-1?e.toExponential():""}}},a=t.Scale.extend({determineDataLimits:function(){if(this.min=null,this.max=null,this.options.stacked){var t={};e.each(this.chart.data.datasets,function(i){e.isDatasetVisible(i)&&(this.isHorizontal()?i.xAxisID===this.id:i.yAxisID===this.id)&&(void 0===t[i.type]&&(t[i.type]=[]),e.each(i.data,function(e,a){var s=t[i.type],o=+this.getRightValue(e);isNaN(o)||(s[a]=s[a]||0,this.options.relativePoints?s[a]=100:s[a]+=o)},this))},this),e.each(t,function(t){var i=e.min(t),a=e.max(t);this.min=null===this.min?i:Math.min(this.min,i),this.max=null===this.max?a:Math.max(this.max,a)},this)}else e.each(this.chart.data.datasets,function(t){e.isDatasetVisible(t)&&(this.isHorizontal()?t.xAxisID===this.id:t.yAxisID===this.id)&&e.each(t.data,function(t,e){var i=+this.getRightValue(t);isNaN(i)||(null===this.min?this.min=i:ithis.max&&(this.max=i))},this)},this);this.min=void 0!==this.options.ticks.min?this.options.ticks.min:this.min,this.max=void 0!==this.options.ticks.max?this.options.ticks.max:this.max,this.min===this.max&&(0!==this.min&&null!==this.min?(this.min=Math.pow(10,Math.floor(e.log10(this.min))-1),this.max=Math.pow(10,Math.floor(e.log10(this.max))+1)):(this.min=1,this.max=10))},buildTicks:function(){this.ticks=[];for(var t=void 0!==this.options.ticks.min?this.options.ticks.min:Math.pow(10,Math.floor(e.log10(this.min)));tthis.max&&(this.max=i))},this)},this),this.options.ticks.beginAtZero){var t=e.sign(this.min),i=e.sign(this.max);0>t&&0>i?this.max=0:t>0&&i>0&&(this.min=0)}void 0!==this.options.ticks.min?this.min=this.options.ticks.min:void 0!==this.options.ticks.suggestedMin&&(this.min=Math.min(this.min,this.options.ticks.suggestedMin)),void 0!==this.options.ticks.max?this.max=this.options.ticks.max:void 0!==this.options.ticks.suggestedMax&&(this.max=Math.max(this.max,this.options.ticks.suggestedMax)),this.min===this.max&&(this.min--,this.max++)},buildTicks:function(){this.ticks=[];var i=e.getValueOrDefault(this.options.ticks.fontSize,t.defaults.global.defaultFontSize),a=Math.min(this.options.ticks.maxTicksLimit?this.options.ticks.maxTicksLimit:11,Math.ceil(this.drawingArea/(1.5*i)));a=Math.max(2,a);var s=e.niceNum(this.max-this.min,!1),o=e.niceNum(s/(a-1),!0),n=Math.floor(this.min/o)*o,r=Math.ceil(this.max/o)*o,l=Math.ceil((r-n)/o);this.ticks.push(void 0!==this.options.ticks.min?this.options.ticks.min:n);for(var h=1;l>h;++h)this.ticks.push(n+h*o);this.ticks.push(void 0!==this.options.ticks.max?this.options.ticks.max:r),this.max=e.max(this.ticks),this.min=e.min(this.ticks),this.options.ticks.reverse?(this.ticks.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),this.zeroLineIndex=this.ticks.indexOf(0)},convertTicksToLabels:function(){t.Scale.prototype.convertTicksToLabels.call(this),this.pointLabels=this.chart.data.labels.map(this.options.pointLabels.callback,this)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var i,a,s,o,n,r,l,h,c,d,u,g,f=e.getValueOrDefault(this.options.pointLabels.fontSize,t.defaults.global.defaultFontSize),m=e.getValueOrDefault(this.options.pointLabels.fontStyle,t.defaults.global.defaultFontStyle),p=e.getValueOrDefault(this.options.pointLabels.fontFamily,t.defaults.global.defaultFontFamily),b=e.fontString(f,m,p),x=e.min([this.height/2-f-5,this.width/2]),v=this.width,y=0;for(this.ctx.font=b,a=0;av&&(v=i.x+o,n=a),i.x-ov&&(v=i.x+s,n=a):a>this.getValueCount()/2&&i.x-s0||this.options.reverse){var o=this.getDistanceFromCenterForValue(this.ticks[s]),n=this.yCenter-o;if(this.options.gridLines.display)if(i.strokeStyle=this.options.gridLines.color,i.lineWidth=this.options.gridLines.lineWidth,this.options.lineArc)i.beginPath(),i.arc(this.xCenter,this.yCenter,o,0,2*Math.PI),i.closePath(),i.stroke();else{i.beginPath();for(var r=0;r=0;a--){if(this.options.angleLines.display){var s=this.getPointPosition(a,this.getDistanceFromCenterForValue(this.options.reverse?this.min:this.max));i.beginPath(),i.moveTo(this.xCenter,this.yCenter),i.lineTo(s.x,s.y),i.stroke(),i.closePath()}var o=this.getPointPosition(a,this.getDistanceFromCenterForValue(this.options.reverse?this.min:this.max)+5),n=e.getValueOrDefault(this.options.pointLabels.fontColor,t.defaults.global.defaultFontColor),r=e.getValueOrDefault(this.options.pointLabels.fontSize,t.defaults.global.defaultFontSize),l=e.getValueOrDefault(this.options.pointLabels.fontStyle,t.defaults.global.defaultFontStyle),h=e.getValueOrDefault(this.options.pointLabels.fontFamily,t.defaults.global.defaultFontFamily),c=e.fontString(r,l,h);i.font=c,i.fillStyle=n;var d=this.pointLabels.length,u=this.pointLabels.length/2,g=u/2,f=g>a||a>d-g,m=a===g||a===d-g;0===a?i.textAlign="center":a===u?i.textAlign="center":u>a?i.textAlign="left":i.textAlign="right",m?i.textBaseline="middle":f?i.textBaseline="bottom":i.textBaseline="top",i.fillText(this.pointLabels[a]?this.pointLabels[a]:"",o.x,o.y)}}}}});t.scaleService.registerScaleType("radialLinear",a,i)}},{}],41:[function(t,e,i){"use strict";var a=t("moment");a="function"==typeof a?a:window.moment,e.exports=function(t){var e=t.helpers,i={units:[{name:"millisecond",steps:[1,2,5,10,20,50,100,250,500]},{name:"second",steps:[1,2,5,10,30]},{name:"minute",steps:[1,2,5,10,30]},{name:"hour",steps:[1,2,3,6,12]},{name:"day",steps:[1,2,5]},{name:"week",maxStep:4},{name:"month",maxStep:3},{name:"quarter",maxStep:4},{name:"year",maxStep:!1}]},s={position:"bottom",time:{parser:!1,format:!1,unit:!1,round:!1,displayFormat:!1,displayFormats:{millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm:ss a",hour:"MMM D, hA",day:"ll",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"}},ticks:{autoSkip:!1}},o=t.Scale.extend({initialize:function(){if(!a)throw new Error("Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com");t.Scale.prototype.initialize.call(this)},getLabelMoment:function(t,e){return this.labelMoments[t][e]},determineDataLimits:function(){this.labelMoments=[];var t=[];this.chart.data.labels&&this.chart.data.labels.length>0?(e.each(this.chart.data.labels,function(e,i){var a=this.parseTime(e);this.options.time.round&&a.startOf(this.options.time.round), -t.push(a)},this),this.firstTick=a.min.call(this,t),this.lastTick=a.max.call(this,t)):(this.firstTick=null,this.lastTick=null),e.each(this.chart.data.datasets,function(i,s){var o=[];"object"==typeof i.data[0]?e.each(i.data,function(t,e){var i=this.parseTime(this.getRightValue(t));this.options.time.round&&i.startOf(this.options.time.round),o.push(i),this.firstTick=null!==this.firstTick?a.min(this.firstTick,i):i,this.lastTick=null!==this.lastTick?a.max(this.lastTick,i):i},this):o=t,this.labelMoments.push(o)},this),this.options.time.min&&(this.firstTick=this.parseTime(this.options.time.min)),this.options.time.max&&(this.lastTick=this.parseTime(this.options.time.max)),this.firstTick=(this.firstTick||a()).clone(),this.lastTick=(this.lastTick||a()).clone()},buildTicks:function(a){if(this.ticks=[],this.unitScale=1,this.scaleSizeInUnits=0,this.options.time.unit)this.tickUnit=this.options.time.unit||"day",this.displayFormat=this.options.time.displayFormats[this.tickUnit],this.scaleSizeInUnits=this.lastTick.diff(this.firstTick,this.tickUnit,!0),this.unitScale=e.getValueOrDefault(this.options.time.unitStepSize,1);else{var s=e.getValueOrDefault(this.options.ticks.fontSize,t.defaults.global.defaultFontSize),o=this.isHorizontal()?this.width-(this.paddingLeft+this.paddingRight):this.height-(this.paddingTop+this.paddingBottom),n=this.tickFormatFunction(this.firstTick,0,[]),r=n.length*s,l=Math.cos(e.toRadians(this.options.ticks.maxRotation)),h=Math.sin(e.toRadians(this.options.ticks.maxRotation));r=r*l+s*h;var c=o/(r+10);this.tickUnit="millisecond",this.scaleSizeInUnits=this.lastTick.diff(this.firstTick,this.tickUnit,!0),this.displayFormat=this.options.time.displayFormats[this.tickUnit];for(var d=0,u=i.units[d];dMath.ceil(this.scaleSizeInUnits/c)){this.unitScale=e.getValueOrDefault(this.options.time.unitStepSize,u.steps[g]);break}break}if(u.maxStep===!1||Math.ceil(this.scaleSizeInUnits/c)=0)break;m%this.unitScale===0&&this.ticks.push(p)}(0!==this.ticks[this.ticks.length-1].diff(this.lastTick,this.tickUnit)||0===this.scaleSizeInUnits)&&(this.options.time.max?(this.ticks.push(this.lastTick.clone()),this.scaleSizeInUnits=this.lastTick.diff(this.ticks[0],this.tickUnit,!0)):(this.scaleSizeInUnits=Math.ceil(this.scaleSizeInUnits/this.unitScale)*this.unitScale,this.ticks.push(this.firstTick.clone().add(this.scaleSizeInUnits,this.tickUnit)),this.lastTick=this.ticks[this.ticks.length-1].clone()))},getLabelForIndex:function(t,e){var i=this.chart.data.labels&&t - -
    - -
    -
    -

    {% trans "Automatic report" %}

    -
    -
    - -{% endblock %} - -{% block content %} - - - -
    - -

    - - - - - - - - - - - - -

    - -
    - - - -{% for field, name, stat in stats %} - -
    - -

    {{ name }}

    - -
    -

    - {% blocktrans with total=stat.total_count provided=stats.submissions_count|subtract:stat.not_provided not_provided=stat.not_provided question_type=field.data_type %} - - {{ provided }} out of {{ total }} respondents answered - this question ({{ not_provided}} were without data). - Question type: '{{question_type}}' - - {% endblocktrans %} -

    -
    - - {% if stat.show_graph %} -
    Graph will come here later on
    - {% endif %} - - {# if you got frequency, you have a table with metrics #} - {% if 'frequency' in stat %} - - - - - - - - - - - - {% for answer, _ in stat.frequency %} - 10 %}class="hiddenrows"{% endif %}> - - - - - - - - {% endfor %} - - -
    {% trans "Value" %}{% trans "Frequency" %}{% trans "Percentage" %}
    - {{ answer }} - - {{ stat.frequency|index:forloop.counter0|index:1 }} - {{ stat.percentage|index:forloop.counter0|index:1 }}%
    - -

    Show more results

    - - {% endif %} - - {% if 'mean' in stat %} - - - - - - - - - - - - - - - - - - -
    {% trans "Mean" %}{% trans "Median" %}{% trans "Mode" %}{% trans "Standard deviation" %}
    {{ stat.mean }} {{ stat.median }} {{ stat.mode }} {{ stat.stdev }}
    - - {% endif %} - -
    - -{% endfor %} - - - -{% endblock %} - -{% block javascript %} -{{ block.super }} - - - - - -{% endblock %} diff --git a/onadata/apps/survey_report/templates/survey_report/auto_report_split_by.html b/onadata/apps/survey_report/templates/survey_report/auto_report_split_by.html deleted file mode 100644 index 497739083..000000000 --- a/onadata/apps/survey_report/templates/survey_report/auto_report_split_by.html +++ /dev/null @@ -1,458 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% load data_manipulation %} - -{% block additional-headers %} - -
    - -
    - -
    -
    -

    {% trans "Automatic report" %}

    -
    -
    - -{% endblock %} - -{% block content %} - - - -

    - {% blocktrans %} - This is an automated report based on raw data submitted to this project. - Please conduct proper data cleaning prior to using the - graphs and figures used on this page. - {% endblocktrans %} -{% trans "" %} -

    - -
    - -

    - - - - - - - - - - - - - -

    - -
    - - -{% for field, name, stat in stats %} - -
    - -

    {{ name }}

    - -
    -

    - {% blocktrans with total=stats.submissions_count provided=stats.submissions_count|subtract:stat.not_provided not_provided=stat.not_provided question_type=field.data_type %} - - {{ provided }} out of {{ total }} respondents answered - this question ({{ not_provided}} were without data). - Question type: '{{question_type}}' - - {% endblocktrans %} -

    -
    - - - {% if stat.show_graph %} -
    Graph will come here later on
    - {% endif %} - - - {# if you got percentage, you have a table with metrics #} - {% if stat.values.0.1.percentage %} - - - - - - {% for splitter, value in stat.values.0.1.frequency %} - {% if splitter != "..." %} - - {% else %} - - {% endif %} - {% endfor %} - - - - - {% for value, split_by in stat.values %} - 10 %}class="hiddenrows"{% endif %}> - - - {% for splitter, value in split_by.frequency %} - - {% endfor %} - - - {% endfor %} - - -
    {{ splitter }}{% trans "[other responses]" %}
    - {{ value }} - {{ value }}{% if value != "*"%}{% endif %}
    - -

    Show more results

    - - - {% endif %} - - {% if stat.values.0.1.mean %} - - - - - - - - - - - - - - {% for split_by, statistics in stat.values %} - 10 %}class="hiddenrows"{% endif %}> - - - {% if statistics.mean != '*' %} - - {% else %} - - {% endif %} - - {% if statistics.median != '*' %} - - {% else %} - - {% endif %} - - - - {% if statistics.stdev != '*' %} - - {% else %} - - {% endif %} - - {% endfor %} - -
    {% trans "Split by" %}{% trans "Mean" %}{% trans "Median" %}{% trans "Mode" %}{% trans "Standard deviation" %}
    {{ split_by }}{{ statistics.mean|floatformat:2 }}*{{ statistics.median|floatformat:2 }}*{{ statistics.mode }}{{ statistics.stdev|floatformat:2 }}*
    - -

    Show more results

    - - {% endif %} - -
    - -{% endfor %} - - - -{% endblock %} - -{% block javascript %} -{{ block.super }} - - - - - -{% endblock %} diff --git a/onadata/apps/survey_report/templates/survey_report/autoreport_menu.html b/onadata/apps/survey_report/templates/survey_report/autoreport_menu.html deleted file mode 100644 index f8af26a19..000000000 --- a/onadata/apps/survey_report/templates/survey_report/autoreport_menu.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -
    -{% block content %} - - - -

    {% trans "Automatic report" %}:

    - -
    - -

    - - - - - - - - - - - - -

    - -
    - - {% endblock %} -
    diff --git a/onadata/apps/survey_report/templates/survey_report/export_html.html b/onadata/apps/survey_report/templates/survey_report/export_html.html deleted file mode 100644 index 69633005b..000000000 --- a/onadata/apps/survey_report/templates/survey_report/export_html.html +++ /dev/null @@ -1,225 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -
    -{% block content %} - - - - - -
    - -
    - - - - - - - - - - - - - - -
    - -
    - - - - - - - {% for label in labels %} - - {% endfor %} - - - - - - - {% for submission_id, row in data %} - - {% url 'onadata.apps.viewer.views.instance' username id_string as submission_url %} - - - {% for value in row %} - - {% endfor %} - - {% endfor %} - - - -
    {% trans "View or Edit" %}{{ label }}
    {% trans "Open" %}{{ value }}
    - - - -{% endblock %} -
    diff --git a/onadata/apps/survey_report/templates/survey_report/export_menu.html b/onadata/apps/survey_report/templates/survey_report/export_menu.html deleted file mode 100644 index 4dfa5de23..000000000 --- a/onadata/apps/survey_report/templates/survey_report/export_menu.html +++ /dev/null @@ -1,105 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -{% block additional-headers %} - -
    - -
    - -
    -
    -

    {% trans "Advanced Exports" %}

    -
    -
    - -{% endblock %} - - -{% block content %} - - - - - -
    - -
    - - -

    - - -

    - -

    - - -

    - -

    - - -

    -

    - - -

    - -

    -
    - -
    - - {% endblock %} diff --git a/onadata/apps/survey_report/templates/survey_report/view_submission.html b/onadata/apps/survey_report/templates/survey_report/view_submission.html deleted file mode 100644 index 57e3bc4c9..000000000 --- a/onadata/apps/survey_report/templates/survey_report/view_submission.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -
    -{% block content %} - - - -

    {{ title }}

    - - - - {% for name, value in submission %} - - - - {% endfor %} - -
    {{ name }}{{ value }} -
    - -{% endblock %} - diff --git a/onadata/apps/survey_report/templatetags/__init__.py b/onadata/apps/survey_report/templatetags/__init__.py deleted file mode 100644 index 134880430..000000000 --- a/onadata/apps/survey_report/templatetags/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import diff --git a/onadata/apps/survey_report/templatetags/data_manipulation.py b/onadata/apps/survey_report/templatetags/data_manipulation.py deleted file mode 100644 index ebf0fe30c..000000000 --- a/onadata/apps/survey_report/templatetags/data_manipulation.py +++ /dev/null @@ -1,16 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -from django import template - -register = template.Library() - -@register.filter -def index(l, i): - try: - return l[i] - except: - return None - -@register.filter -def subtract(value, arg): - return value - arg diff --git a/onadata/apps/survey_report/urls.py b/onadata/apps/survey_report/urls.py deleted file mode 100644 index 600711f5c..000000000 --- a/onadata/apps/survey_report/urls.py +++ /dev/null @@ -1,32 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -from django.conf.urls import patterns, url - - -urlpatterns = patterns( - '', - url(r"(?P[^/]+)/export/$", - 'onadata.apps.survey_report.views.export_menu', - name='formpack_export_menu'), - - url(r"(?P[^/]+)/export.csv$", - 'onadata.apps.survey_report.views.csv_export', - name='formpack_csv_export'), - url(r"(?P[^/]+)/export.xlsx$", - 'onadata.apps.survey_report.views.xlsx_export', - name='formpack_xlsx_export'), - url(r"(?P[^/]+)/export.html$", - 'onadata.apps.survey_report.views.html_export', - name='formpack_html_export'), - - url(r"(?P[^/]+)/submission/(?P\d+).html$", - 'onadata.apps.survey_report.views.view_one_submission', - name='formpack_one_submission'), - - url(r"(?P[^/]+)/digest/$", - 'onadata.apps.survey_report.views.autoreport_menu', - name='formpack_autoreport_menu'), - url(r"(?P[^/]+)/digest.html$", - 'onadata.apps.survey_report.views.auto_report', - name='formpack_auto_report') -) diff --git a/onadata/apps/survey_report/views.py b/onadata/apps/survey_report/views.py deleted file mode 100644 index 01d7f87b3..000000000 --- a/onadata/apps/survey_report/views.py +++ /dev/null @@ -1,288 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import - -import uuid - -from datetime import datetime - -from django.conf import settings -from django.contrib.auth.models import User -from django.shortcuts import render, get_object_or_404, redirect -from django.http import HttpResponse, HttpResponseForbidden, Http404, QueryDict -from django.utils.safestring import mark_safe -from django.utils.translation import ugettext as _ -from django.core.urlresolvers import reverse - -from pure_pagination import Paginator, EmptyPage, PageNotAnInteger - -from path import tempdir - -from onadata.libs.utils.user_auth import has_permission - -from formpack import FormPack - - -def readable_xform_required(func): - def _wrapper(request, username, id_string, *args, **kwargs): - owner = get_object_or_404(User, username=username) - xform = get_object_or_404(owner.xforms, id_string=id_string) - if not has_permission(xform, owner, request): - return HttpResponseForbidden(_('Not shared.')) - return func(request, username, id_string, *args, **kwargs) - return _wrapper - - -def get_instances_for_user_and_form(user, form_id, submission=None): - userform_id = '{}_{}'.format(user, form_id) - query = {'_userform_id': userform_id, '_deleted_at': {'$exists': False}} - if submission: - query['_id'] = submission - return settings.MONGO_DB.instances.find(query) - - -def build_formpack(username, id_string): - user = User.objects.get(username=username) - xform = user.xforms.get(id_string=id_string) - schema = { - "id_string": id_string, - "version": 'v1', - "content": xform.to_kpi_content_schema(), - } - return user, xform, FormPack([schema], xform.title) - - -def build_export_context(request, username, id_string): - - hierarchy_in_labels = request.REQUEST.get( - 'hierarchy_in_labels', '' - ).lower() in ('true', 'on') - group_sep = request.REQUEST.get('group_sep', '/') - - user, xform, formpack = build_formpack(username, id_string) - - translations = formpack.available_translations - lang = request.REQUEST.get('lang', None) or next(iter(translations), None) - - options = {'versions': 'v1', - 'group_sep': group_sep, - 'lang': lang, - 'hierarchy_in_labels': hierarchy_in_labels, - 'copy_fields': ('_id', '_uuid', '_submission_time'), - 'force_index': True - } - - return { - 'username': username, - 'id_string': id_string, - 'languages': translations, - 'headers_lang': lang, - 'formpack': formpack, - 'xform': xform, - 'group_sep': group_sep, - 'lang': lang, - 'hierarchy_in_labels': hierarchy_in_labels, - 'export': formpack.export(**options) - } - - -def build_export_filename(export, extension): - form_type = 'labels' - if not export.lang: - form_type = "values" - elif export.lang != "_default": - form_type = export.lang - - return "{title} - {form_type} - {date:%Y-%m-%d-%H-%M}.{ext}".format( - form_type=form_type, - date=datetime.utcnow(), - title=export.title, - ext=extension - ) - - -@readable_xform_required -def export_menu(request, username, id_string): - - req = request.REQUEST - export_type = req.get('type', None) - if export_type: - q = QueryDict('', mutable=True) - q['lang'] = req.get('lang') - q['hierarchy_in_labels'] = req.get('hierarchy_in_labels') - q['group_sep'] = req.get('group_sep', '/') - - if export_type == "xlsx": - url = reverse('formpack_xlsx_export', args=(username, id_string)) - return redirect(url + '?' + q.urlencode()) - if export_type == "csv": - url = reverse('formpack_csv_export', args=(username, id_string)) - return redirect(url + '?' + q.urlencode()) - - context = build_export_context(request, username, id_string) - return render(request, 'survey_report/export_menu.html', context) - - -@readable_xform_required -def autoreport_menu(request, username, id_string): - - user, xform, form_pack = build_formpack(username, id_string) - - # exclude fields in repeat group - split_by_fields = form_pack.get_fields_for_versions(data_types="select_one") - - context = { - 'languages': form_pack.available_translations, - 'username': username, - 'id_string': id_string, - 'split_by_fields': split_by_fields - } - - return render(request, 'survey_report/autoreport_menu.html', context) - - -@readable_xform_required -def xlsx_export(request, username, id_string): - - export = build_export_context(request, username, id_string)['export'] - data = [("v1", get_instances_for_user_and_form(username, id_string))] - - with tempdir() as d: - tempfile = d / str(uuid.uuid4()) - export.to_xlsx(tempfile, data) - xlsx = tempfile.bytes() - - name = build_export_filename(export, 'xlsx') - ct = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - response = HttpResponse(xlsx, content_type=ct) - response['Content-Disposition'] = 'attachment; filename="%s"' % name - return response - - -@readable_xform_required -def csv_export(request, username, id_string): - - export = build_export_context(request, username, id_string)['export'] - data = [("v1", get_instances_for_user_and_form(username, id_string))] - - name = build_export_filename(export, 'csv') - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="%s"' % name - - for line in export.to_csv(data): - response.write(line + "\n") - - return response - - -@readable_xform_required -def html_export(request, username, id_string): - - limit = int(request.REQUEST.get('limit', 100)) - - cursor = get_instances_for_user_and_form(username, id_string) - paginator = Paginator(cursor, limit, request=request) - - try: - page = paginator.page(request.REQUEST.get('page', 1)) - except (EmptyPage, PageNotAnInteger): - try: - page = paginator.page(1) - except (EmptyPage, PageNotAnInteger): - raise Http404('This report has no submissions') - - data = [("v1", page.object_list)] - context = build_export_context(request, username, id_string) - - context.update({ - 'page': page, - 'table': [], - 'title': id_string, - }) - - export = context['export'] - sections = list(export.labels.items()) - section, labels = sections[0] - id_index = labels.index('_id') - - # generator dublicating the "_id" to allow to make a link to each - # submission - def make_table(submissions): - for chunk in export.parse_submissions(submissions): - for section_name, rows in chunk.items(): - if section == section_name: - for row in rows: - yield row[id_index], row - - context['labels'] = labels - context['data'] = make_table(data) - - return render(request, 'survey_report/export_html.html', context) - - -@readable_xform_required -def auto_report(request, username, id_string): - - user, xform, formpack = build_formpack(username, id_string) - report = formpack.autoreport() - - limit = int(request.REQUEST.get('limit', 20)) - split_by = request.REQUEST.get('split_by') or None - - fields = [field.name for field in formpack.get_fields_for_versions()] - paginator = Paginator(fields, limit, request=request) - - try: - page = paginator.page(request.REQUEST.get('page', 1)) - except (EmptyPage, PageNotAnInteger): - try: - page = paginator.page(1) - except (EmptyPage, PageNotAnInteger): - raise Http404('This report has no submissions') - - # remove fields in a group - split_by_fields = formpack.get_fields_for_versions(data_types="select_one") - translations = formpack.available_translations - lang = request.REQUEST.get('lang', None) or next(iter(translations), None) - - ctx = { - 'page': page, - 'stats': [], - 'title': xform.title, - 'split_by': split_by, - 'split_by_fields': split_by_fields, - 'username': username, - 'id_string': id_string, - 'languages': translations, - 'headers_lang': lang, - 'xform': xform - } - - data = [("v1", get_instances_for_user_and_form(username, id_string))] - ctx['stats'] = report.get_stats(data, page.object_list, lang, split_by) - - if split_by: - - return render(request, 'survey_report/auto_report_split_by.html', ctx) - - return render(request, 'survey_report/auto_report.html', ctx) - - -@readable_xform_required -def view_one_submission(request, username, id_string, submission): - - submission = int(submission) - instances = get_instances_for_user_and_form(username, id_string, submission) - instances = list(instances) - if not instances: - raise Http404('Unable to find this submission') - - context = { - 'title': id_string - } - - data = [("v1", instances)] - export = build_export_context(request, username, id_string)['export'] - submission = next(iter(export.to_dict(data).values())) - context['submission'] = zip(submission['fields'], submission['data'][0]) - - return render(request, 'survey_report/view_submission.html', context) diff --git a/onadata/apps/viewer/migrations/0004_update_meta_data_export_types.py b/onadata/apps/viewer/migrations/0004_update_meta_data_export_types.py new file mode 100644 index 000000000..6615b59af --- /dev/null +++ b/onadata/apps/viewer/migrations/0004_update_meta_data_export_types.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0003_auto_20171123_1521'), + ] + + operations = [ + migrations.AlterField( + model_name='export', + name='export_type', + field=models.CharField(default=b'xls', max_length=10, choices=[(b'xls', b'Excel'), (b'csv', b'CSV'), (b'zip', b'ZIP'), (b'kml', b'kml')]), + ), + ] diff --git a/onadata/apps/viewer/models/export.py b/onadata/apps/viewer/models/export.py index 4f1628777..29b5e8b59 100644 --- a/onadata/apps/viewer/models/export.py +++ b/onadata/apps/viewer/models/export.py @@ -30,33 +30,20 @@ def __str__(self): CSV_EXPORT = 'csv' KML_EXPORT = 'kml' ZIP_EXPORT = 'zip' - GDOC_EXPORT = 'gdoc' - CSV_ZIP_EXPORT = 'csv_zip' - SAV_ZIP_EXPORT = 'sav_zip' - SAV_EXPORT = 'sav' - ANALYSER_EXPORT = 'analyser' EXPORT_MIMES = { 'xls': 'vnd.ms-excel', 'xlsx': 'vnd.openxmlformats', 'csv': 'csv', 'zip': 'zip', - 'csv_zip': 'zip', - 'sav_zip': 'zip', - 'sav': 'sav', 'kml': 'vnd.google-earth.kml+xml' } EXPORT_TYPES = [ (XLS_EXPORT, 'Excel'), (CSV_EXPORT, 'CSV'), - (GDOC_EXPORT, 'GDOC'), (ZIP_EXPORT, 'ZIP'), - (KML_EXPORT, 'kml'), - (CSV_ZIP_EXPORT, 'CSV ZIP'), - (SAV_ZIP_EXPORT, 'SAV ZIP'), - (SAV_EXPORT, 'SAV'), - (ANALYSER_EXPORT, 'Analyser') + (KML_EXPORT, 'kml') ] EXPORT_TYPE_DICT = dict(export_type for export_type in EXPORT_TYPES) @@ -177,10 +164,16 @@ def exports_outdated(cls, xform, export_type): except cls.DoesNotExist: return True else: - if latest_export.time_of_last_submission is not None \ - and xform.time_of_last_submission_update() is not None: - return latest_export.time_of_last_submission <\ - xform.time_of_last_submission_update() + xform.refresh_from_db(fields=['date_modified']) + xform_last_submission_time = xform.time_of_last_submission_update() + export_last_submission_time = latest_export.time_of_last_submission + if export_last_submission_time is None: + return True + + if export_last_submission_time < xform.date_modified: + return True + elif xform_last_submission_time is not None: + return export_last_submission_time < xform_last_submission_time else: # return true if we can't determine the status, to force # auto-generation diff --git a/onadata/apps/viewer/tasks.py b/onadata/apps/viewer/tasks.py index 7da6ae9e3..5d303e0f2 100644 --- a/onadata/apps/viewer/tasks.py +++ b/onadata/apps/viewer/tasks.py @@ -36,9 +36,7 @@ def _create_export(xform, export_type): 'export_id': export.id, 'query': query, } - if export_type in [Export.XLS_EXPORT, Export.GDOC_EXPORT, - Export.CSV_EXPORT, Export.CSV_ZIP_EXPORT, - Export.SAV_ZIP_EXPORT]: + if export_type in [Export.XLS_EXPORT, Export.CSV_EXPORT]: if options and "group_delimiter" in options: arguments["group_delimiter"] = options["group_delimiter"] if options and "split_select_multiples" in options: @@ -49,17 +47,11 @@ def _create_export(xform, export_type): options["binary_select_multiples"] # start async export - if export_type in [Export.XLS_EXPORT, Export.GDOC_EXPORT]: + if export_type == Export.XLS_EXPORT: result = create_xls_export.apply_async((), arguments, countdown=10) elif export_type == Export.CSV_EXPORT: result = create_csv_export.apply_async( (), arguments, countdown=10) - elif export_type == Export.CSV_ZIP_EXPORT: - result = create_csv_zip_export.apply_async( - (), arguments, countdown=10) - elif export_type == Export.SAV_ZIP_EXPORT: - result = create_sav_zip_export.apply_async( - (), arguments, countdown=10) else: raise Export.ExportTypeError elif export_type == Export.ZIP_EXPORT: @@ -70,8 +62,6 @@ def _create_export(xform, export_type): # start async export result = create_kml_export.apply_async( (), arguments, countdown=10) - elif export_type == Export.ANALYSER_EXPORT: - result = create_analyser_export.apply_async((), arguments, countdown=10) else: raise Export.ExportTypeError if result: @@ -126,45 +116,6 @@ def create_xls_export(username, id_string, export_id, query=None, return gen_export.id -@task() -def create_analyser_export(username, id_string, export_id, query=None): - # Mostly a serving of copy pasta based on the above `create_xls_export()`. Enjoy. - - # we re-query the db instead of passing model objects according to - # http://docs.celeryproject.org/en/latest/userguide/tasks.html#state - ext = 'xlsx' - - try: - export = Export.objects.get(id=export_id) - except Export.DoesNotExist: - # no export for this ID return None. - return None - - # though export is not available when for has 0 submissions, we - # catch this since it potentially stops celery - try: - gen_export = generate_export(Export.ANALYSER_EXPORT, ext, username, id_string, export_id, - query, group_delimiter='/', split_select_multiples=True, - binary_select_multiples=False) - except (Exception, NoRecordsFoundError) as e: - export.internal_status = Export.FAILED - export.save() - # mail admins - details = { - 'export_id': export_id, - 'username': username, - 'id_string': id_string - } - report_exception("Analyser Export Exception: Export ID - " - "%(export_id)s, /%(username)s/%(id_string)s" - % details, e, sys.exc_info()) - # Raise for now to let celery know we failed - # - doesnt seem to break celery` - raise - else: - return gen_export.id - - @task() def create_csv_export(username, id_string, export_id, query=None, group_delimiter='/', split_select_multiples=True, @@ -255,65 +206,6 @@ def create_zip_export(username, id_string, export_id, query=None): return gen_export.id -@task() -def create_csv_zip_export(username, id_string, export_id, query=None, - group_delimiter='/', split_select_multiples=True, - binary_select_multiples=False): - export = Export.objects.get(id=export_id) - try: - # though export is not available when for has 0 submissions, we - # catch this since it potentially stops celery - gen_export = generate_export( - Export.CSV_ZIP_EXPORT, 'zip', username, id_string, export_id, - query, group_delimiter, split_select_multiples, - binary_select_multiples) - except (Exception, NoRecordsFoundError) as e: - export.internal_status = Export.FAILED - export.save() - # mail admins - details = { - 'export_id': export_id, - 'username': username, - 'id_string': id_string - } - report_exception("CSV ZIP Export Exception: Export ID - " - "%(export_id)s, /%(username)s/%(id_string)s" - % details, e, sys.exc_info()) - raise - else: - return gen_export.id - - -@task() -def create_sav_zip_export(username, id_string, export_id, query=None, - group_delimiter='/', split_select_multiples=True, - binary_select_multiples=False): - export = Export.objects.get(id=export_id) - try: - # though export is not available when for has 0 submissions, we - # catch this since it potentially stops celery - gen_export = generate_export( - Export.SAV_ZIP_EXPORT, 'zip', username, id_string, export_id, - query, group_delimiter, split_select_multiples, - binary_select_multiples - ) - except (Exception, NoRecordsFoundError) as e: - export.internal_status = Export.FAILED - export.save() - # mail admins - details = { - 'export_id': export_id, - 'username': username, - 'id_string': id_string - } - report_exception("SAV ZIP Export Exception: Export ID - " - "%(export_id)s, /%(username)s/%(id_string)s" - % details, e, sys.exc_info()) - raise - else: - return gen_export.id - - @task() def delete_export(export_id): try: diff --git a/onadata/apps/viewer/templates/charts.html b/onadata/apps/viewer/templates/charts.html deleted file mode 100644 index 1df785383..000000000 --- a/onadata/apps/viewer/templates/charts.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'base.html' %} -{% load charts_snippet %} - -{% block additional-headers %} - - - - - -{% endblock %} - -{% block content %} -
    - - -{% charts_snippet summaries %} - -
    -
    - -
    -{% endblock %} - -{% block javascript %} -{{ block.super }} - -{% endblock %} diff --git a/onadata/apps/viewer/templates/charts_snippet.html b/onadata/apps/viewer/templates/charts_snippet.html deleted file mode 100644 index 783c2cda7..000000000 --- a/onadata/apps/viewer/templates/charts_snippet.html +++ /dev/null @@ -1,58 +0,0 @@ -{% load lookup_filter %} - {% for summary in summaries %} -
    - {% with field_name=summary.field_name field_xpath=summary.field_xpath field_label=summary.field_label data_type=summary.data_type field_type=summary.field_type data=summary.data %} -

    {{ field_label }}

    -
    -
    -
    -
    -
    -
    - {% if data_type == "numeric" %} -
    - {% endif %} -
    -
    - - {% endwith %} -
    - {% endfor %} \ No newline at end of file diff --git a/onadata/apps/viewer/templates/data_view.html b/onadata/apps/viewer/templates/data_view.html deleted file mode 100644 index c55fa6298..000000000 --- a/onadata/apps/viewer/templates/data_view.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -{% block additional-headers %} - - - - -{% endblock %} - -{% block content %} -
    - -
    - -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block javascript %} - {{ block.super }} - - - - - - - - - - - -{% endblock %} diff --git a/onadata/apps/viewer/templates/instance.html b/onadata/apps/viewer/templates/instance.html deleted file mode 100644 index f4d2b0982..000000000 --- a/onadata/apps/viewer/templates/instance.html +++ /dev/null @@ -1,168 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -{% block content %} - - -
    -

    Loading...

    - Loading... -
    -{% if messages %} -
    {{messages}}
    -{% endif %} -
    - - -{% endblock %} - -{% block javascript %} -{{ block.super }} - - - - - - - - - - -{% endblock %} diff --git a/onadata/apps/viewer/templates/map.html b/onadata/apps/viewer/templates/map.html deleted file mode 100644 index cb9ed6794..000000000 --- a/onadata/apps/viewer/templates/map.html +++ /dev/null @@ -1,285 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -{% block additional-headers %} - - - - - - -{% endblock %} - -{% block body %} - - - {% include "topbar.html" %} - -
    - - - -{% block javascript %} -{{ block.super }} - - - - - - - - - - - - - - - - - - -{% endblock %} - -{% endblock %} diff --git a/onadata/apps/viewer/templates/stats_tables.html b/onadata/apps/viewer/templates/stats_tables.html deleted file mode 100644 index 2057e43f1..000000000 --- a/onadata/apps/viewer/templates/stats_tables.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends 'base.html' %} -{% load lookup_filter %} - -{% block additional-headers %} - - -{% endblock %} - -{% block content %} -
    - - -
    -
    -
    - - -
    - -
    -
    -
    - - -
    - - - - - -
    -
    -
    - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -{% endblock %} - -{% block javascript %} -{{ block.super }} - - - - - - -{% endblock %} diff --git a/onadata/apps/viewer/templates/survey.html b/onadata/apps/viewer/templates/survey.html deleted file mode 100644 index ec6c8c476..000000000 --- a/onadata/apps/viewer/templates/survey.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load i18n %}{% if image_urls %} -
      -{% for image_url in image_urls %} -
    • - -
    • -{% endfor %} -
    -{% endif %} -{% if languages|length > 1 %} - -{% endif %} - - - - - - - - {% for pair in label_value_pairs %} - - - - {% endfor %} - -
    {% trans "Question" %}{%trans "Response" %}
    {% for language, label in pair.0 %} - - {% endfor %}{{ pair.1 }}
    - diff --git a/onadata/apps/viewer/tests/test_data_view.py b/onadata/apps/viewer/tests/test_data_view.py deleted file mode 100644 index 9f0ca9a54..000000000 --- a/onadata/apps/viewer/tests/test_data_view.py +++ /dev/null @@ -1,63 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import unittest - -from django.core.urlresolvers import reverse -from guardian.shortcuts import assign_perm, remove_perm - -from onadata.apps.main.tests.test_base import TestBase -from onadata.apps.viewer.views import data_view - - -class TestDataView(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form_and_submit_instance() - self.url = reverse(data_view, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - - def test_data_view(self): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - - @unittest.skip('Fails under Django 1.6') - def test_data_view_with_username_and_id_string_in_uppercase(self): - url = reverse(data_view, kwargs={ - 'username': self.user.username.upper(), - 'id_string': self.xform.id_string.upper() - }) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_restrict_for_anon(self): - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_restrict_for_not_owner(self): - self._create_user_and_login('alice') - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_allow_if_shared(self): - self.xform.shared_data = True - self.xform.save() - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_allow_if_user_given_permission(self): - self._create_user_and_login('alice') - assign_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_disallow_if_user_permission_revoked(self): - self._create_user_and_login('alice') - assign_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - remove_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) diff --git a/onadata/apps/viewer/tests/test_export_builder.py b/onadata/apps/viewer/tests/test_export_builder.py index 5f803336e..60ee60330 100644 --- a/onadata/apps/viewer/tests/test_export_builder.py +++ b/onadata/apps/viewer/tests/test_export_builder.py @@ -249,92 +249,6 @@ def test_build_sections_from_survey(self): self.assertEqual( sorted(expected_element_names), sorted(element_names)) - def test_zipped_csv_export_works(self): - survey = self._create_childrens_survey() - export_builder = ExportBuilder() - export_builder.set_survey(survey) - temp_zip_file = NamedTemporaryFile(suffix='.zip') - export_builder.to_zipped_csv(temp_zip_file.name, self.data) - temp_zip_file.seek(0) - temp_dir = tempfile.mkdtemp() - zip_file = zipfile.ZipFile(temp_zip_file.name, "r") - zip_file.extractall(temp_dir) - zip_file.close() - temp_zip_file.close() - - # generate data to compare with - index = 1 - indices = {} - survey_name = survey.name - outputs = [] - for d in self.data: - outputs.append( - dict_to_joined_export(d, index, indices, survey_name)) - index += 1 - - # check that each file exists - self.assertTrue( - os.path.exists( - os.path.join(temp_dir, "{0}.csv".format(survey.name)))) - with open( - os.path.join( - temp_dir, "{0}.csv".format(survey.name))) as csv_file: - reader = csv.reader(csv_file) - rows = [r for r in reader] - - # open comparison file - with open(_logger_fixture_path( - 'csvs', 'childrens_survey.csv')) as fixture_csv: - fixture_reader = csv.reader(fixture_csv) - expected_rows = [r for r in fixture_reader] - self.assertEqual(rows, expected_rows) - - self.assertTrue( - os.path.exists( - os.path.join(temp_dir, "children.csv"))) - with open(os.path.join(temp_dir, "children.csv")) as csv_file: - reader = csv.reader(csv_file) - rows = [r for r in reader] - - # open comparison file - with open(_logger_fixture_path( - 'csvs', 'children.csv')) as fixture_csv: - fixture_reader = csv.reader(fixture_csv) - expected_rows = [r for r in fixture_reader] - self.assertEqual(rows, expected_rows) - - self.assertTrue( - os.path.exists( - os.path.join(temp_dir, "children_cartoons.csv"))) - with open(os.path.join(temp_dir, "children_cartoons.csv")) as csv_file: - reader = csv.reader(csv_file) - rows = [r for r in reader] - - # open comparison file - with open(_logger_fixture_path( - 'csvs', 'children_cartoons.csv')) as fixture_csv: - fixture_reader = csv.reader(fixture_csv) - expected_rows = [r for r in fixture_reader] - self.assertEqual(rows, expected_rows) - - self.assertTrue( - os.path.exists( - os.path.join(temp_dir, "children_cartoons_characters.csv"))) - with open(os.path.join( - temp_dir, "children_cartoons_characters.csv")) as csv_file: - reader = csv.reader(csv_file) - rows = [r for r in reader] - - # open comparison file - with open(_logger_fixture_path( - 'csvs', - 'children_cartoons_characters.csv')) as fixture_csv: - fixture_reader = csv.reader(fixture_csv) - expected_rows = [r for r in fixture_reader] - self.assertEqual(rows, expected_rows) - - shutil.rmtree(temp_dir) - def test_decode_mongo_encoded_section_names(self): data = { 'main_section': [1, 2, 3, 4], @@ -349,77 +263,6 @@ def test_decode_mongo_encoded_section_names(self): } self.assertEqual(result, expected_result) - @unittest.skip('Fails with Python2') - def test_zipped_csv_export_works_with_unicode(self): - """ - cvs writer doesnt handle unicode we we have to encode to ascii - """ - survey = create_survey_from_xls(_logger_fixture_path( - 'childrens_survey_unicode.xls')) - export_builder = ExportBuilder() - export_builder.set_survey(survey) - temp_zip_file = NamedTemporaryFile(suffix='.zip') - export_builder.to_zipped_csv(temp_zip_file.name, self.data_utf8) - temp_zip_file.seek(0) - temp_dir = tempfile.mkdtemp() - zip_file = zipfile.ZipFile(temp_zip_file.name, "r") - zip_file.extractall(temp_dir) - zip_file.close() - temp_zip_file.close() - # check that the children's file (which has the unicode header) exists - self.assertTrue( - os.path.exists( - os.path.join(temp_dir, "children.info.csv"))) - # check file's contents - with open(os.path.join(temp_dir, "children.info.csv")) as csv_file: - reader = csv.reader(csv_file) - expected_headers = ['children.info/name.first', - 'children.info/age', - 'children.info/fav_colors', - 'children.info/fav_colors/red\u2019s', - 'children.info/fav_colors/blue\u2019s', - 'children.info/fav_colors/pink\u2019s', - 'children.info/ice_creams', - 'children.info/ice_creams/vanilla', - 'children.info/ice_creams/strawberry', - 'children.info/ice_creams/chocolate', '_id', - '_uuid', '_submission_time', '_index', - '_parent_table_name', '_parent_index', - '_tags', '_notes'] - rows = [row for row in reader] - actual_headers = [h.decode('utf-8') for h in rows[0]] - self.assertEqual(sorted(actual_headers), sorted(expected_headers)) - data = dict(zip(rows[0], rows[1])) - self.assertEqual( - data['children.info/fav_colors/red\u2019s'.encode('utf-8')], - 'True') - self.assertEqual( - data['children.info/fav_colors/blue\u2019s'.encode('utf-8')], - 'True') - self.assertEqual( - data['children.info/fav_colors/pink\u2019s'.encode('utf-8')], - 'False') - # check that red and blue are set to true - shutil.rmtree(temp_dir) - - @unittest.skip('Fails with Python2') - def test_xls_export_works_with_unicode(self): - survey = create_survey_from_xls(_logger_fixture_path( - 'childrens_survey_unicode.xls')) - export_builder = ExportBuilder() - export_builder.set_survey(survey) - temp_xls_file = NamedTemporaryFile(suffix='.xlsx') - export_builder.to_xls_export(temp_xls_file.name, self.data_utf8) - temp_xls_file.seek(0) - # check that values for red\u2019s and blue\u2019s are set to true - wb = load_workbook(temp_xls_file.name) - children_sheet = wb.get_sheet_by_name("children.info") - data = dict([(r[0].value, r[1].value) for r in children_sheet.columns]) - self.assertTrue(data['children.info/fav_colors/red\u2019s']) - self.assertTrue(data['children.info/fav_colors/blue\u2019s']) - self.assertFalse(data['children.info/fav_colors/pink\u2019s']) - temp_xls_file.close() - def test_generation_of_multi_selects_works(self): survey = self._create_childrens_survey() export_builder = ExportBuilder() @@ -952,53 +795,3 @@ def test_convert_types(self): converted_val = ExportBuilder.convert_type(val, 'date') self.assertIsInstance(converted_val, datetime.date) self.assertEqual(converted_val, expected_val) - - def test_to_sav_export(self): - survey = self._create_childrens_survey() - export_builder = ExportBuilder() - export_builder.set_survey(survey) - temp_zip_file = NamedTemporaryFile(suffix='.zip') - filename = temp_zip_file.name - export_builder.to_zipped_sav(filename, self.data) - temp_zip_file.seek(0) - temp_dir = tempfile.mkdtemp() - zip_file = zipfile.ZipFile(temp_zip_file.name, "r") - zip_file.extractall(temp_dir) - zip_file.close() - temp_zip_file.close() - - # generate data to compare with - index = 1 - indices = {} - survey_name = survey.name - outputs = [] - for d in self.data: - outputs.append( - dict_to_joined_export(d, index, indices, survey_name)) - index += 1 - - # check that each file exists - self.assertTrue( - os.path.exists( - os.path.join(temp_dir, "{0}.sav".format(survey.name)))) - - def _test_sav_file(section): - with SavReader( - os.path.join( - temp_dir, "{0}.sav".format(section)), - returnHeader=True) as reader: - header = next(reader) - rows = [r for r in reader] - - # open comparison file - with SavReader(_logger_fixture_path( - 'spss', "{0}.sav".format(section)), - returnHeader=True) as fixture_reader: - fixture_header = next(fixture_reader) - self.assertEqual(header, fixture_header) - expected_rows = [r for r in fixture_reader] - self.assertEqual(rows, expected_rows) - - for section in export_builder.sections: - section_name = section['name'].replace('/', '_') - _test_sav_file(section_name) diff --git a/onadata/apps/viewer/tests/test_export_list.py b/onadata/apps/viewer/tests/test_export_list.py index 18bf6fcce..853b0d8df 100644 --- a/onadata/apps/viewer/tests/test_export_list.py +++ b/onadata/apps/viewer/tests/test_export_list.py @@ -72,30 +72,6 @@ def test_zip_export_list(self): response = self.client.get(url) self.assertEqual(response.status_code, 200) - def test_gdoc_export_list(self): - kwargs = {'username': self.user.username, - 'id_string': self.xform.id_string, - 'export_type': Export.GDOC_EXPORT} - url = reverse(export_list, kwargs=kwargs) - response = self.client.get(url) - self.assertEqual(response.status_code, 302) - - def test_csv_zip_export_list(self): - kwargs = {'username': self.user.username, - 'id_string': self.xform.id_string, - 'export_type': Export.CSV_ZIP_EXPORT} - url = reverse(export_list, kwargs=kwargs) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_sav_zip_export_list(self): - kwargs = {'username': self.user.username, - 'id_string': self.xform.id_string, - 'export_type': Export.SAV_ZIP_EXPORT} - url = reverse(export_list, kwargs=kwargs) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - class TestDataExportURL(TestBase): @@ -147,32 +123,3 @@ def test_xls_export_url(self): filename = self._filename_from_disposition(content_disposition) basename, ext = os.path.splitext(filename) self.assertEqual(ext, '.xlsx') - - @unittest.skip('Fails under Django 1.6') - def test_csv_zip_export_url(self): - self._submit_transport_instance() - url = reverse('csv_zip_export', kwargs={ - 'username': self.user.username.upper(), - 'id_string': self.xform.id_string.upper(), - }) - response = self.client.get(url) - headers = dict(response.items()) - self.assertEqual(headers['Content-Type'], 'application/zip') - content_disposition = headers['Content-Disposition'] - filename = self._filename_from_disposition(content_disposition) - basename, ext = os.path.splitext(filename) - self.assertEqual(ext, '.zip') - - def test_sav_zip_export_url(self): - self._submit_transport_instance() - url = reverse('sav_zip_export', kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string, - }) - response = self.client.get(url) - headers = dict(response.items()) - self.assertEqual(headers['Content-Type'], 'application/zip') - content_disposition = headers['Content-Disposition'] - filename = self._filename_from_disposition(content_disposition) - basename, ext = os.path.splitext(filename) - self.assertEqual(ext, '.zip') diff --git a/onadata/apps/viewer/tests/test_exports.py b/onadata/apps/viewer/tests/test_exports.py index a13e489d4..261b3ac83 100644 --- a/onadata/apps/viewer/tests/test_exports.py +++ b/onadata/apps/viewer/tests/test_exports.py @@ -14,14 +14,12 @@ from django.utils.dateparse import parse_datetime from xlrd import open_workbook -from onadata.apps.main.views import delete_data from onadata.apps.main.tests.test_base import TestBase from onadata.apps.viewer.tests.export_helpers import viewer_fixture_path from onadata.apps.viewer.views import delete_export, export_list,\ create_export, export_progress, export_download from onadata.apps.viewer.xls_writer import XlsWriter from onadata.apps.viewer.models.export import Export -from onadata.apps.main.models.meta_data import MetaData from onadata.apps.viewer.models.parsed_instance import ParsedInstance from onadata.apps.logger.models import Instance from onadata.apps.viewer.tasks import create_xls_export @@ -460,11 +458,11 @@ def test_deleted_submission_not_in_export(self): # get id of second submission instance_id = Instance.objects.filter( xform=self.xform).order_by('id').reverse()[0].id - delete_url = reverse( - delete_data, kwargs={"username": self.user.username, - "id_string": self.xform.id_string}) - params = {'id': instance_id} - self.client.post(delete_url, params) + delete_url = reverse('data-detail', kwargs={ + 'pk': self.xform.pk, + 'dataid': instance_id + }) + self.client.delete(delete_url) count = ParsedInstance.query_mongo( self.user.username, self.xform.id_string, '{}', '[]', '{}', count=True)[0]['count'] @@ -652,19 +650,20 @@ def test_direct_export_returns_newset_export_if_not_updated_since(self): sleep(1) # and when we delete - delete_url = reverse(delete_data, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) instance = Instance.objects.filter().order_by('-pk')[0] - response = self.client.post(delete_url, {'id': instance.id}) - self.assertEqual(response.status_code, 200) + delete_url = reverse('data-detail', kwargs={ + 'pk': self.xform.pk, + 'dataid': instance.pk + }) + response = self.client.delete(delete_url) + self.assertEqual(response.status_code, 204) response = self.client.get(csv_export_url) self.assertEqual(response.status_code, 200) # we should have an extra export now that the data # has been updated by the delete num_csv_exports = Export.objects.filter( xform=self.xform, export_type=Export.CSV_EXPORT).count() + self.xform.refresh_from_db() self.assertEqual(num_csv_exports, initial_num_csv_exports + 3) def test_exports_outdated_doesnt_consider_failed_exports(self): @@ -1044,19 +1043,6 @@ def test_dict_to_joined_export_works(self): output['children/cartoons/characters'])[0], expected_output['children/cartoons/characters'][index]) - def test_generate_csv_zip_export(self): - # publish xls form - self._publish_transportation_form_and_submit_instance() - # create export db object - export = generate_export( - Export.CSV_ZIP_EXPORT, "zip", self.user.username, - self.xform.id_string, group_delimiter='/', - split_select_multiples=True) - storage = get_storage_class()() - self.assertTrue(storage.exists(export.filepath)) - path, ext = os.path.splitext(export.filename) - self.assertEqual(ext, '.zip') - def test_dict_to_joined_export_notes(self): submission = { "_id": 579828, diff --git a/onadata/apps/viewer/tests/test_instance_view.py b/onadata/apps/viewer/tests/test_instance_view.py deleted file mode 100644 index 262acc082..000000000 --- a/onadata/apps/viewer/tests/test_instance_view.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -from django.core.urlresolvers import reverse -from guardian.shortcuts import assign_perm, remove_perm - -from onadata.apps.main.tests.test_base import TestBase -from onadata.apps.viewer.views import instance - - -class TestInstanceView(TestBase): - - def setUp(self): - TestBase.setUp(self) - self._create_user_and_login() - self._publish_transportation_form_and_submit_instance() - self.url = reverse(instance, kwargs={ - 'username': self.user.username, - 'id_string': self.xform.id_string - }) - - def test_instance_view(self): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_restrict_for_anon(self): - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_restrict_for_not_owner(self): - self._create_user_and_login('alice') - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_allow_if_shared(self): - self.xform.shared_data = True - self.xform.save() - response = self.anon.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_allow_if_user_given_permission(self): - self._create_user_and_login('alice') - assign_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_disallow_if_user_permission_revoked(self): - self._create_user_and_login('alice') - assign_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - remove_perm('change_xform', self.user, self.xform) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) diff --git a/onadata/apps/viewer/views.py b/onadata/apps/viewer/views.py index d3c6f69f5..a901cca9d 100644 --- a/onadata/apps/viewer/views.py +++ b/onadata/apps/viewer/views.py @@ -6,8 +6,6 @@ import os import re from datetime import datetime -from tempfile import NamedTemporaryFile -from time import strftime, strptime import rest_framework.request from django.conf import settings @@ -28,9 +26,6 @@ from rest_framework.settings import api_settings from onadata.apps.logger.models import XForm, Attachment -from onadata.apps.logger.views import download_jsonform -from onadata.apps.main.models import UserProfile, MetaData, TokenStorageModel -from onadata.apps.viewer.models.data_dictionary import DataDictionary from onadata.apps.viewer.models.export import Export from onadata.apps.viewer.tasks import create_async_export from onadata.libs.exceptions import NoRecordsFoundError @@ -40,15 +35,13 @@ should_create_new_export, kml_export_data, newset_export_for) -from onadata.libs.utils.google import google_export_xls, redirect_uri from onadata.libs.utils.image_tools import image_url from onadata.libs.utils.log import audit_log, Actions from onadata.libs.utils.logger_tools import response_with_mimetype_and_name, \ disposition_ext_and_date -from onadata.libs.utils.user_auth import has_permission, get_xform_and_perms, \ - helper_auth_helper, has_edit_permission +from onadata.libs.utils.user_auth import has_permission, \ + helper_auth_helper from onadata.libs.utils.viewer_tools import export_def_from_filename -from .xls_writer import XlsWriter media_file_logger = logging.getLogger('media_files') @@ -69,162 +62,11 @@ def _set_submission_time_to_query(query, request): return query -def encode(time_str): - time = strptime(time_str, "%Y_%m_%d_%H_%M_%S") - return strftime("%Y-%m-%d %H:%M:%S", time) - - def format_date_for_mongo(x): return datetime.strptime(x, '%y_%m_%d_%H_%M_%S')\ .strftime('%Y-%m-%dT%H:%M:%S') -def instances_for_export(dd, start=None, end=None): - if start and not end: - return dd.instances.filter(date_created__gte=start) - elif end and not start: - return dd.instances.filter(date_created__lte=end) - elif start and end: - return dd.instances.filter(date_created__gte=start, - date_created__lte=end) - - -def dd_for_params(id_string, owner, request): - start = end = None - dd = DataDictionary.objects.get(id_string__exact=id_string, - user=owner) - if request.GET.get('start'): - try: - start = encode(request.GET['start']) - except ValueError: - # bad format - return [False, - HttpResponseBadRequest( - _('Start time format must be YY_MM_DD_hh_mm_ss')) - ] - if request.GET.get('end'): - try: - end = encode(request.GET['end']) - except ValueError: - # bad format - return [False, - HttpResponseBadRequest( - _('End time format must be YY_MM_DD_hh_mm_ss')) - ] - if start or end: - dd.instances_for_export = instances_for_export(dd, start, end) - - return [True, dd] - - -def parse_label_for_display(pi, xpath): - label = pi.data_dictionary.get_label(xpath) - if not type(label) == dict: - label = {'Unknown': label} - return label.items() - - -def average(values): - if len(values): - return sum(values, 0.0) / len(values) - return None - - -def map_view(request, username, id_string, template='map.html'): - owner = get_object_or_404(User, username__iexact=username) - xform = get_object_or_404(XForm, id_string__exact=id_string, user=owner) - if not has_permission(xform, owner, request): - return HttpResponseForbidden(_('Not shared.')) - data = {'content_user': owner, 'xform': xform} - data['profile'], created = UserProfile.objects.get_or_create(user=owner) - # Follow the example of onadata.apps.main.views.show - data['can_edit'] = has_edit_permission(xform, owner, request) - - data['form_view'] = True - data['jsonform_url'] = reverse(download_jsonform, - kwargs={"username": username, - "id_string": id_string}) - data['enketo_edit_url'] = reverse('edit_data', - kwargs={"username": username, - "id_string": id_string, - "data_id": 0}) - data['enketo_add_url'] = reverse('enter_data', - kwargs={"username": username, - "id_string": id_string}) - - data['enketo_add_with_url'] = reverse('add_submission_with', - kwargs={"username": username, - "id_string": id_string}) - data['mongo_api_url'] = reverse('mongo_view_api', - kwargs={"username": username, - "id_string": id_string}) - data['delete_data_url'] = reverse('delete_data', - kwargs={"username": username, - "id_string": id_string}) - audit = { - "xform": xform.id_string - } - audit_log(Actions.FORM_MAP_VIEWED, request.user, owner, - _("Requested map on '%(id_string)s'.") - % {'id_string': xform.id_string}, audit, request) - return render(request, template, data) - - -def map_embed_view(request, username, id_string): - return map_view(request, username, id_string, template='map_embed.html') - - -def add_submission_with(request, username, id_string): - - import uuid - import requests - - from django.template import loader, Context - from dpath import util as dpath_util - from dict2xml import dict2xml - - def geopoint_xpaths(username, id_string): - d = DataDictionary.objects.get( - user__username__iexact=username, id_string__exact=id_string) - return [e.get_abbreviated_xpath() - for e in d.get_survey_elements() - if e.bind.get('type') == 'geopoint'] - - value = request.GET.get('coordinates') - xpaths = geopoint_xpaths(username, id_string) - xml_dict = {} - for path in xpaths: - dpath_util.new(xml_dict, path, value) - - context = {'username': username, - 'id_string': id_string, - 'xml_content': dict2xml(xml_dict)} - instance_xml = loader.get_template("instance_add.xml")\ - .render(Context(context)) - - url = settings.ENKETO_API_INSTANCE_IFRAME_URL - return_url = reverse('thank_you_submission', - kwargs={"username": username, "id_string": id_string}) - if settings.DEBUG: - openrosa_url = "https://dev.formhub.org/{}".format(username) - else: - openrosa_url = request.build_absolute_uri("/{}".format(username)) - payload = {'return_url': return_url, - 'form_id': id_string, - 'server_url': openrosa_url, - 'instance': instance_xml, - 'instance_id': uuid.uuid4().hex} - - r = requests.post(url, data=payload, - auth=(settings.ENKETO_API_TOKEN, ''), verify=False) - - return HttpResponse(r.text, content_type='application/json') - - -def thank_you_submission(request, username, id_string): - return HttpResponse("Thank You") - - def data_export(request, username, id_string, export_type): owner = get_object_or_404(User, username__iexact=username) xform = get_object_or_404(XForm, id_string__exact=id_string, user=owner) @@ -238,8 +80,6 @@ def data_export(request, username, id_string, export_type): force_xlsx = request.GET.get('xls') != 'true' if export_type == Export.XLS_EXPORT and force_xlsx: extension = 'xlsx' - elif export_type in [Export.CSV_ZIP_EXPORT, Export.SAV_ZIP_EXPORT]: - extension = 'zip' audit = { "xform": xform.id_string, @@ -354,38 +194,12 @@ def create_export(request, username, id_string, export_type): ) -def _get_google_token(request, redirect_to_url): - token = None - if request.user.is_authenticated(): - try: - ts = TokenStorageModel.objects.get(id=request.user) - except TokenStorageModel.DoesNotExist: - pass - else: - token = ts.token - elif request.session.get('access_token'): - token = request.session.get('access_token') - if token is None: - request.session["google_redirect_url"] = redirect_to_url - return HttpResponseRedirect(redirect_uri) - return token - - def export_list(request, username, id_string, export_type): try: Export.EXPORT_TYPE_DICT[export_type] except KeyError: return HttpResponseBadRequest(_('Invalid export type')) - if export_type == Export.GDOC_EXPORT: - redirect_url = reverse( - export_list, - kwargs={ - 'username': username, 'id_string': id_string, - 'export_type': export_type}) - token = _get_google_token(request, redirect_url) - if isinstance(token, HttpResponse): - return token owner = get_object_or_404(User, username__iexact=username) xform = get_object_or_404(XForm, id_string__exact=id_string, user=owner) if not has_permission(xform, owner, request): @@ -429,27 +243,6 @@ def export_progress(request, username, id_string, export_type): 'filename': export.filename }) status['filename'] = export.filename - if export.export_type == Export.GDOC_EXPORT and \ - export.export_url is None: - redirect_url = reverse( - export_progress, - kwargs={ - 'username': username, 'id_string': id_string, - 'export_type': export_type}) - token = _get_google_token(request, redirect_url) - if isinstance(token, HttpResponse): - return token - status['url'] = None - try: - url = google_export_xls( - export.full_filepath, xform.title, token, blob=True) - except Exception as e: - status['error'] = True - status['message'] = e.message - else: - export.export_url = url - export.save() - status['url'] = url # mark as complete if it either failed or succeeded but NOT pending if export.status == Export.SUCCESSFUL or export.status == Export.FAILED: @@ -470,9 +263,6 @@ def export_download(request, username, id_string, export_type, filename): # find the export entry in the db export = get_object_or_404(Export, xform=xform, filename=filename) - if export_type == Export.GDOC_EXPORT and export.export_url is not None: - return HttpResponseRedirect(export.export_url) - ext, mime_type = export_def_from_filename(export.filename) audit = { @@ -572,78 +362,6 @@ def kml_export(request, username, id_string): return response -def google_xls_export(request, username, id_string): - token = None - if request.user.is_authenticated(): - try: - ts = TokenStorageModel.objects.get(id=request.user) - except TokenStorageModel.DoesNotExist: - pass - else: - token = ts.token - elif request.session.get('access_token'): - token = request.session.get('access_token') - - if token is None: - request.session["google_redirect_url"] = reverse( - google_xls_export, - kwargs={'username': username, 'id_string': id_string}) - return HttpResponseRedirect(redirect_uri) - - owner = get_object_or_404(User, username__iexact=username) - xform = get_object_or_404(XForm, id_string__exact=id_string, user=owner) - if not has_permission(xform, owner, request): - return HttpResponseForbidden(_('Not shared.')) - - valid, dd = dd_for_params(id_string, owner, request) - if not valid: - return dd - - ddw = XlsWriter() - tmp = NamedTemporaryFile(delete=False) - ddw.set_file(tmp) - ddw.set_data_dictionary(dd) - temp_file = ddw.save_workbook_to_file() - temp_file.close() - url = google_export_xls(tmp.name, xform.title, token, blob=True) - os.unlink(tmp.name) - audit = { - "xform": xform.id_string, - "export_type": "google" - } - audit_log( - Actions.EXPORT_CREATED, request.user, owner, - _("Created Google Docs export on '%(id_string)s'.") % - { - 'id_string': xform.id_string, - }, audit, request) - - return HttpResponseRedirect(url) - - -def data_view(request, username, id_string): - owner = get_object_or_404(User, username__iexact=username) - xform = get_object_or_404(XForm, id_string__exact=id_string, user=owner) - if not has_permission(xform, owner, request): - return HttpResponseForbidden(_('Not shared.')) - - data = { - 'owner': owner, - 'xform': xform - } - audit = { - "xform": xform.id_string, - } - audit_log( - Actions.FORM_DATA_VIEWED, request.user, owner, - _("Requested data view for '%(id_string)s'.") % - { - 'id_string': xform.id_string, - }, audit, request) - - return render(request, "data_view.html", data) - - def attachment_url(request, size='medium'): media_file = request.GET.get('media_file') # TODO: how to make sure we have the right media file, @@ -735,30 +453,3 @@ def attachment_url(request, size='medium'): return response return HttpResponseNotFound(_('Error: Attachment not found')) - - -def instance(request, username, id_string): - xform, is_owner, can_edit, can_view, can_delete_data = get_xform_and_perms( - username, id_string, request) - # no access - if not (xform.shared_data or can_view or - request.session.get('public_link') == xform.uuid): - return HttpResponseForbidden(_('Not shared.')) - - audit = { - "xform": xform.id_string, - } - audit_log( - Actions.FORM_DATA_VIEWED, request.user, xform.user, - _("Requested instance view for '%(id_string)s'.") % - { - 'id_string': xform.id_string, - }, audit, request) - - return render(request, 'instance.html', { - 'username': username, - 'id_string': id_string, - 'xform': xform, - 'can_edit': can_edit, - 'can_delete_data': can_delete_data - }) diff --git a/onadata/koboform/redirect_middleware.py b/onadata/koboform/redirect_middleware.py index 8846a42f2..5f0474f90 100644 --- a/onadata/koboform/redirect_middleware.py +++ b/onadata/koboform/redirect_middleware.py @@ -9,16 +9,6 @@ # regardless of koboform setting DISABLED_VIEWS = [ 'home', - 'form_gallery', - 'faq', - 'support', - 'resources', - 'syntax', - 'members_list', - 'about_us', - 'getting_started', - 'xls2xform', - 'public_profile', ] # if user not logged in: @@ -35,6 +25,7 @@ # /accounts/logout/ => kform/accounts/logout # /accounts/register/ => kform/accounts/register + class ConditionalRedirects(object): def process_view(self, request, view, args, kwargs): view_name = view.__name__ diff --git a/onadata/libs/authentication.py b/onadata/libs/authentication.py index 104fa1f3f..74ded68db 100644 --- a/onadata/libs/authentication.py +++ b/onadata/libs/authentication.py @@ -1,5 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + +from django.conf import settings from django.utils.translation import ugettext as _ from django_digest import HttpDigestAuthenticator from rest_framework.authentication import ( @@ -37,7 +39,8 @@ def authenticate(self, request): # authenticators user_auth = super(HttpsOnlyBasicAuthentication, self).authenticate( request) - if user_auth is not None and not request.is_secure(): + if settings.TESTING_MODE is False and \ + user_auth is not None and not request.is_secure(): # Scold the user if they provided correct credentials for basic # auth but didn't use HTTPS raise AuthenticationFailed(_( diff --git a/onadata/libs/javascript/tests/karma.conf.js b/onadata/libs/javascript/tests/karma.conf.js index c4cb985fc..618dbfdf0 100644 --- a/onadata/libs/javascript/tests/karma.conf.js +++ b/onadata/libs/javascript/tests/karma.conf.js @@ -33,7 +33,6 @@ module.exports = function(config) { 'onadata/apps/viewer/static/js/map.js', 'onadata/apps/viewer/static/js/xform.js', 'onadata/apps/viewer/static/js/data_view.js', - 'onadata/apps/viewer/static/js/stats_tables.js', 'onadata/libs/javascript/tests/spec/*.spec.js', 'onadata/libs/javascript/tests/mock/*.js' ], diff --git a/onadata/libs/javascript/tests/karma_dev.conf.js b/onadata/libs/javascript/tests/karma_dev.conf.js index 725e8666f..9101d6587 100644 --- a/onadata/libs/javascript/tests/karma_dev.conf.js +++ b/onadata/libs/javascript/tests/karma_dev.conf.js @@ -33,7 +33,6 @@ module.exports = function(config) { 'onadata/apps/viewer/static/js/map.js', 'onadata/apps/viewer/static/js/xform.js', 'onadata/apps/viewer/static/js/data_view.js', - 'onadata/apps/viewer/static/js/stats_tables.js', 'onadata/libs/javascript/tests/spec/*.spec.js', 'onadata/libs/javascript/tests/mock/*.js' ], diff --git a/onadata/libs/renderers/renderers.py b/onadata/libs/renderers/renderers.py index 63102ecbd..b3757e62b 100644 --- a/onadata/libs/renderers/renderers.py +++ b/onadata/libs/renderers/renderers.py @@ -30,19 +30,6 @@ class CSVRenderer(BaseRenderer): format = 'csv' charset = 'utf-8' - -class CSVZIPRenderer(BaseRenderer): - media_type = 'application/octet-stream' - format = 'csvzip' - charset = None - - -class SAVZIPRenderer(BaseRenderer): - media_type = 'application/octet-stream' - format = 'savzip' - charset = None - - # TODO add KML, ZIP(attachments) support diff --git a/onadata/libs/serializers/metadata_serializer.py b/onadata/libs/serializers/metadata_serializer.py index 821227bc4..3395c7ff7 100644 --- a/onadata/libs/serializers/metadata_serializer.py +++ b/onadata/libs/serializers/metadata_serializer.py @@ -1,6 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from django.core.exceptions import ValidationError + from django.core.validators import URLValidator from django.utils.translation import ugettext as _ from rest_framework import serializers @@ -26,13 +26,15 @@ class MetaDataSerializer(serializers.HyperlinkedModelSerializer): data_type = serializers.ChoiceField(choices=METADATA_TYPES) data_file = serializers.FileField(required=False) data_file_type = serializers.CharField(max_length=255, required=False) + from_kpi = serializers.BooleanField(required=False) class Meta: model = MetaData # was previously validate_data_value but the signature change in DRF3. def validate(self, attrs): - """Ensure we have a valid url if we are adding a media uri + """ + Ensure we have a valid url if we are adding a media uri instead of a media file """ value = attrs.get("data_value") @@ -50,15 +52,24 @@ def validate(self, attrs): def create(self, validated_data): data_type = validated_data.get('data_type') data_file = validated_data.get('data_file') + data_file_type = validated_data.get('data_file_type') + from_kpi = validated_data.get('from_kpi', False) xform = validated_data.get('xform') - data_value = data_file.name if data_file else validated_data.get('data_value') - data_file_type = data_file.content_type if data_file else None + file_hash = validated_data.get('file_hash') + data_value = ( + data_file.name if data_file else validated_data.get('data_value') + ) + + if not data_file_type: + data_file_type = data_file.content_type if data_file else None return MetaData.objects.create( data_type=data_type, xform=xform, data_value=data_value, data_file=data_file, - data_file_type=data_file_type + data_file_type=data_file_type, + file_hash=file_hash, + from_kpi=from_kpi, ) diff --git a/onadata/libs/serializers/xform_serializer.py b/onadata/libs/serializers/xform_serializer.py index ee48a53da..dd601c7f2 100644 --- a/onadata/libs/serializers/xform_serializer.py +++ b/onadata/libs/serializers/xform_serializer.py @@ -1,6 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import +import os + from rest_framework import serializers from rest_framework.reverse import reverse @@ -50,7 +52,9 @@ class Meta: 'json', 'xml', 'date_created', 'date_modified', 'encrypted', 'last_submission_time') exclude = ('json', 'xml', 'xls', 'user', - 'has_start_time', 'shared', 'shared_data') + 'has_start_time', 'shared', 'shared_data', + 'allows_sms', + 'sms_id_string') # Again, this is to match unit tests @property @@ -70,6 +74,12 @@ def get_xform_metadata(self, obj): return [] + def get_allows_sms(self, obj): + return False + + def get_sms_id_string(self, obj): + return obj.id_string + class XFormListSerializer(serializers.Serializer): formID = serializers.ReadOnlyField(source='id_string') @@ -104,20 +114,34 @@ def get_manifest_url(self, obj): class XFormManifestSerializer(serializers.Serializer): - filename = serializers.ReadOnlyField(source='data_value') + filename = serializers.SerializerMethodField() hash = serializers.SerializerMethodField() downloadUrl = serializers.SerializerMethodField('get_url') + def get_filename(self, obj): + # If file has been synchronized from KPI and it is a remote URL, + # manifest.xml should return only the name, not the full URL. + # See https://github.com/kobotoolbox/kobocat/issues/344 + if obj.from_kpi: + return obj.filename + + # To be retro-compatible, return `data_value` if file has been + # uploaded from KC directly + return obj.data_value + @check_obj def get_url(self, obj): kwargs = {'pk': obj.xform.pk, 'username': obj.xform.user.username, 'metadata': obj.pk} request = self.context.get('request') - format = obj.data_value[obj.data_value.rindex('.') + 1:] + _, extension = os.path.splitext(obj.filename) + # if `obj` is a remote url, it is possible it does not have any + # extensions. Thus, only force format when extension exists. + if extension: + kwargs['format'] = extension[1:].lower() - return reverse('xform-media', kwargs=kwargs, - request=request, format=format.lower()) + return reverse('xform-media', kwargs=kwargs, request=request) @check_obj def get_hash(self, obj): diff --git a/onadata/libs/templates/home_topbar.html b/onadata/libs/templates/home_topbar.html index 0c2364511..d88858608 100644 --- a/onadata/libs/templates/home_topbar.html +++ b/onadata/libs/templates/home_topbar.html @@ -12,11 +12,6 @@
    diff --git a/onadata/libs/templates/topbar.html b/onadata/libs/templates/topbar.html deleted file mode 100644 index af469d488..000000000 --- a/onadata/libs/templates/topbar.html +++ /dev/null @@ -1,97 +0,0 @@ -{% load i18n %} - - diff --git a/onadata/libs/utils/KoBoToolbox_Excel_Data_Analyser_1.23_TEMPLATE.xlsx b/onadata/libs/utils/KoBoToolbox_Excel_Data_Analyser_1.23_TEMPLATE.xlsx deleted file mode 100644 index d94751835..000000000 Binary files a/onadata/libs/utils/KoBoToolbox_Excel_Data_Analyser_1.23_TEMPLATE.xlsx and /dev/null differ diff --git a/onadata/libs/utils/analyser_export.py b/onadata/libs/utils/analyser_export.py deleted file mode 100644 index 654e7411a..000000000 --- a/onadata/libs/utils/analyser_export.py +++ /dev/null @@ -1,233 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -''' -Created on May 14, 2015 - -@author: esmail -''' - - -import tempfile -import io -import shutil -import copy -import os -from zipfile import ( - ZipFile, - ZIP_DEFLATED, -) - -import lxml.etree as etree -import xlrd -import openpyxl -from openpyxl.writer.excel import save_virtual_workbook - - -NAMESPACES= {'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'} - - -def get_worksheet_indices(workbook_file): - workbook_etree= etree.parse(workbook_file) - worksheet_indices= dict() - for sheet_element in workbook_etree.findall('.//xmlns:sheet', NAMESPACES): - sheet_name= sheet_element.attrib['name'] - sheet_index= sheet_element.attrib['sheetId'] - worksheet_indices[sheet_name]= sheet_index - return worksheet_indices - - -# Adapted from http://stackoverflow.com/a/9919409/1877326 -def xls_as_xlsx(xls_file): - # first open using xlrd - source_workbook = xlrd.open_workbook(file_contents=xls_file.read()) - - # Create the destination workbook, deleting and auto-generated worksheets. - destination_workbook = openpyxl.Workbook() # TODO: Would like to figure out how to make appends work with a "write_only" workbook. - for wksht_nm in destination_workbook.get_sheet_names(): - worksheet= destination_workbook.get_sheet_by_name(wksht_nm) - destination_workbook.remove_sheet(worksheet) - - worksheet_names= ['survey', 'choices'] - for wksht_nm in source_workbook.sheet_names(): - source_worksheet= source_workbook.sheet_by_name(wksht_nm) - destination_worksheet= destination_workbook.create_sheet(title=wksht_nm) - - for row in xrange(source_worksheet.nrows): - destination_worksheet.append( [source_worksheet.cell_value(row, col) for col in xrange(source_worksheet.ncols)] ) - - return io.BytesIO(save_virtual_workbook(destination_workbook)) - -def copy_cells(source_worksheet_file, destination_worksheet_file_path, new_string_indices): - destination_worksheet= etree.parse(destination_worksheet_file_path) - - namespace_prefix= '{' + NAMESPACES['xmlns'] + '}' - for _, source_dimension in etree.iterparse(source_worksheet_file, tag=namespace_prefix+'dimension'): - destination_dimension= destination_worksheet.xpath('.//xmlns:dimension', namespaces=NAMESPACES)[0] - destination_dimension.attrib['ref']= source_dimension.attrib['ref'] - source_dimension.getroottree().getroot().clear() # FIXME: Necessary? Enough? - break - else: - raise ValueError('No "dimension" element found in source data file.') - source_worksheet_file.seek(0) - - # Copy the data over row by row, iterating through the (potentially large) - # source instead of loading at once. - destination_sheetData_element= destination_worksheet.xpath('//xmlns:sheetData', namespaces=NAMESPACES)[0] - for _, source_row in etree.iterparse(source_worksheet_file, tag=namespace_prefix+'row'): - # Create a new row element. - destination_row= etree.Element(namespace_prefix+'row', NAMESPACES) - destination_row.attrib.update(source_row.attrib) - - # Copy over the cells one by one. - for source_c in source_row: - destination_c = copy.deepcopy(source_c) - # Remap references to shared strings. - if destination_c.attrib['t'] == 's': - values = destination_c.xpath('.//xmlns:v', namespaces=NAMESPACES) - if values: - destination_v = values[0] - destination_v.text = unicode(new_string_indices[int(destination_v.text)]) - destination_row.append(destination_c) - - # Clean up elements of the source worksheet to save memory. - while source_row.getprevious(): - source_row.getprevious().clean() - print('Previous row cleaned') - source_row.clear() - - # Append in the copied row. - destination_sheetData_element.append(destination_row) - - # Save the changes. - with open(destination_worksheet_file_path, 'w') as destination_worksheet_file: - destination_worksheet.write(destination_worksheet_file, encoding='UTF-8') - -def splice_shared_strings(source_file, destination_etree, new_string_indices): - source_etree= etree.parse(source_file) - original_string_map= dict() - destination_root= destination_etree.getroot() - for i, t_element in enumerate(destination_root.iterfind('.//xmlns:t', NAMESPACES)): - original_string_map[t_element.text]= i - - uniqueCount= len(original_string_map) - for i, t_element in enumerate(source_etree.iterfind('//xmlns:t', NAMESPACES)): - text= t_element.text - if text in original_string_map: - new_string_indices[i]= original_string_map[text] - else: - # Copy the "si" element over to `destination_etree`. - si_element= t_element.getparent() - destination_root.append(si_element) - new_string_indices[i]= uniqueCount - uniqueCount+= 1 - - # Update the "uniqueCount" attribute. - destination_root.attrib['uniqueCount']= unicode(uniqueCount) - - -def insert_xlsform_worksheets(analyser_shared_strings, analyser_survey_worksheet_file_path, analyser_choices_worksheet_file_path, survey_file_xls): - # Create an XLSX copy of the survey file. - survey_file_xlsx= xls_as_xlsx(survey_file_xls) - - with ZipFile(survey_file_xlsx) as survey_zipfile: - # Splice over the shared strings. - with survey_zipfile.open('xl/sharedStrings.xml') as shared_strings_file: - new_string_indices= dict() - splice_shared_strings(shared_strings_file, analyser_shared_strings, new_string_indices) - # Identify the desired worksheet indices. - with survey_zipfile.open('xl/workbook.xml') as workbook_file: - worksheet_indices= get_worksheet_indices(workbook_file) - # Copy over the "survey" sheet. - survey_sheet_path= 'xl/worksheets/sheet' + worksheet_indices['survey'] + '.xml' - with survey_zipfile.open(survey_sheet_path) as source_survey_worksheet_file: - # Create a tempfile that supports seeking. - with tempfile.TemporaryFile('w+') as source_survey_worksheet_tempfile: - source_survey_worksheet_tempfile.write(source_survey_worksheet_file.read()) - source_survey_worksheet_tempfile.seek(0) - copy_cells(source_survey_worksheet_tempfile, analyser_survey_worksheet_file_path, new_string_indices) - # Copy over the "choices" sheet if any. - if 'choices' in worksheet_indices: - choices_sheet_path= 'xl/worksheets/sheet' + worksheet_indices['choices'] + '.xml' - with survey_zipfile.open(choices_sheet_path) as source_choices_worksheet_file: - # Create a tempfile that supports seeking. - with tempfile.TemporaryFile('w+') as source_choices_worksheet_tempfile: - source_choices_worksheet_tempfile.write(source_choices_worksheet_file.read()) - source_choices_worksheet_tempfile.seek(0) - copy_cells(source_choices_worksheet_tempfile, analyser_choices_worksheet_file_path, new_string_indices) - - -def insert_data_sheet(analyser_shared_strings, analyser_data_sheet_file_path, data_file_xlsx): - with ZipFile(data_file_xlsx) as data_zipfile: - with data_zipfile.open('xl/sharedStrings.xml') as shared_strings_file: - new_string_indices= dict() - splice_shared_strings(shared_strings_file, analyser_shared_strings, new_string_indices) - # FIXME: Not safe for multi-sheet data such for repeating groups. - with data_zipfile.open('xl/worksheets/sheet1.xml') as data_worksheet_file: - with tempfile.TemporaryFile('w+') as data_worksheet_tempfile: - data_worksheet_tempfile.write(data_worksheet_file.read()) - data_worksheet_tempfile.seek(0) - copy_cells(data_worksheet_tempfile, analyser_data_sheet_file_path, new_string_indices) - - -def generate_analyser(survey_file_xls, data_file_xlsx, analyser_file_xlsx=None): - ''' - Generate a KoBo Excel Data Analyser pre-populated with survey contents and - data. - - NOTE: Due to the Excel formulas used in the analyser template being incompatible with the - `openpyxl` package, this function relies on low-level manipulation of XLSX features such - as `sharedStrings.xml`, and consequently requires both the data to be provided in an XLSX - file. - - :param survey_file_xls: An XLS-formatted XLSForm containing the "survey" and - "choices" (if present) sheet to be inserted into the analyser. Expected - to be a file-like object that supports the :py:func:`read()` method. - :param data_file_xlsx: An XLSX file containing the data to be inserted into - the analyser's "uncleaned_data" sheet. Per the interface of - :py:class:`zipfile.ZipFile`, this can be either a local path string or a - file-like object. - :param analyser_file_xlsx: The analyser template file, pre-configured with - sheet names and reserved empty sheets. Per the interface of - :py:class:zipfile.ZipFile, this can be either a local path string or a - file-like object. - :rtype: io.BytesIO - ''' - # Use the default analyser template if none was provided. - if not analyser_file_xlsx: - this_scripts_directory= os.path.dirname(__file__) - analyser_filename= 'KoBoToolbox_Excel_Data_Analyser_1.23_TEMPLATE.xlsx' - analyser_file_xlsx= os.path.join(this_scripts_directory, analyser_filename) - - # Create a directory for temporary storage. - temp_directory_path= tempfile.mkdtemp(prefix='analyser_temp_') - try: - # Unzip the analyser in preparation for customization. - with ZipFile(analyser_file_xlsx) as analyser_zipfile: - analyser_zipfile.extractall(temp_directory_path) - zip_contents= [f.filename for f in analyser_zipfile.filelist] - - analyser_shared_strings= etree.parse(temp_directory_path + '/xl/sharedStrings.xml') - - # Copy over the XLSForm's "survey" and "choices" (if present) worksheets. - survey_worksheet_file_path= temp_directory_path + '/xl/worksheets/sheet9.xml' - choices_worksheet_file_path= temp_directory_path + '/xl/worksheets/sheet10.xml' - insert_xlsform_worksheets(analyser_shared_strings, survey_worksheet_file_path, choices_worksheet_file_path, survey_file_xls) - - # Copy the data over to the analyser. - analyser_data_sheet_file_path= temp_directory_path + '/xl/worksheets/sheet8.xml' - insert_data_sheet(analyser_shared_strings, analyser_data_sheet_file_path, data_file_xlsx) - - # Finalize the changes for export. - with open(temp_directory_path + '/xl/sharedStrings.xml', 'wb') as analyser_shared_strings_file: - analyser_shared_strings.write(analyser_shared_strings_file, encoding='UTF-8') - xlsx_out= io.BytesIO() - with ZipFile(xlsx_out, 'w', compression=ZIP_DEFLATED) as zipfile_out: - for file_path in zip_contents: - zipfile_out.write(temp_directory_path + '/' + file_path, file_path) - xlsx_out.seek(0) - - finally: - # Clean the temporary directory. - shutil.rmtree(temp_directory_path) - - return xlsx_out diff --git a/onadata/libs/utils/export_tools.py b/onadata/libs/utils/export_tools.py index d098f7b84..ed5fed1fa 100644 --- a/onadata/libs/utils/export_tools.py +++ b/onadata/libs/utils/export_tools.py @@ -44,7 +44,6 @@ TAGS, NOTES ) -from .analyser_export import generate_analyser # this is Mongo Collection where we will store the parsed submissions @@ -422,74 +421,6 @@ def pre_process_row(self, row, section): return row - def to_zipped_csv(self, path, data, *args): - def write_row(row, csv_writer, fields): - csv_writer.writerow( - [encode_if_str(row, field) for field in fields]) - - csv_defs = {} - for section in self.sections: - csv_file = NamedTemporaryFile(suffix=".csv") - csv_writer = csv.writer(csv_file) - csv_defs[section['name']] = { - 'csv_file': csv_file, 'csv_writer': csv_writer} - - # write headers - for section in self.sections: - fields = [element['title'] for element in section['elements']]\ - + self.EXTRA_FIELDS - csv_defs[section['name']]['csv_writer'].writerow( - [f.encode('utf-8') for f in fields]) - - index = 1 - indices = {} - survey_name = self.survey.name - for d in data: - # decode mongo section names - joined_export = dict_to_joined_export(d, index, indices, - survey_name) - output = ExportBuilder.decode_mongo_encoded_section_names( - joined_export) - # attach meta fields (index, parent_index, parent_table) - # output has keys for every section - if survey_name not in output: - output[survey_name] = {} - output[survey_name][INDEX] = index - output[survey_name][PARENT_INDEX] = -1 - for section in self.sections: - # get data for this section and write to csv - section_name = section['name'] - csv_def = csv_defs[section_name] - fields = [ - element['xpath'] for element in - section['elements']] + self.EXTRA_FIELDS - csv_writer = csv_def['csv_writer'] - # section name might not exist within the output, e.g. data was - # not provided for said repeat - write test to check this - row = output.get(section_name, None) - if type(row) == dict: - write_row( - self.pre_process_row(row, section), - csv_writer, fields) - elif type(row) == list: - for child_row in row: - write_row( - self.pre_process_row(child_row, section), - csv_writer, fields) - index += 1 - - # write zipfile - with ZipFile(path, 'w') as zip_file: - for section_name, csv_def in csv_defs.iteritems(): - csv_file = csv_def['csv_file'] - csv_file.seek(0) - zip_file.write( - csv_file.name, "_".join(section_name.split("/")) + ".csv") - - # close files when we are done - for section_name, csv_def in csv_defs.iteritems(): - csv_def['csv_file'].close() - @classmethod def get_valid_sheet_name(cls, desired_name, existing_names): # a sheet name has to be <= 31 characters and not a duplicate of an @@ -579,29 +510,6 @@ def write_row(data, work_sheet, fields, work_sheet_titles): wb.save(filename=path) - def to_analyser_export(self, path, data, username, xform_id_string, *args): - # Get the XLSForm. - xform = XForm.objects.get(user__username__iexact=username, id_string__exact=xform_id_string) - xlsform_io= xform.to_xlsform() - - if xlsform_io is None: - raise RuntimeError('XLSForm `{}` for user `{}` could not be retrieved from storage.'. - format(xform_id_string, username)) - - prefix = slugify('analyser_data__{}__{}'.format(username, xform_id_string)) - with tempfile.NamedTemporaryFile('w+b', prefix=prefix, suffix='.xlsx',) as xls_data: - # Generate a new XLS export to work from. - self.to_xls_export(xls_data.name, data) - xls_data.file.seek(0) - - # Generate the analyser file. - analyser_io= generate_analyser(xlsform_io, xls_data) - - # Write the generated analyser file to the specified path - # ...which itself points to a temp file. - with open(path, 'wb') as analyser_file: - analyser_file.write(analyser_io.read()) - def to_flat_csv_export( self, path, data, username, id_string, filter_query): # TODO resolve circular import @@ -614,94 +522,6 @@ def to_flat_csv_export( self.SPLIT_SELECT_MULTIPLES, self.BINARY_SELECT_MULTIPLES) csv_builder.export_to(path) - def to_zipped_sav(self, path, data, *args): - def write_row(row, csv_writer, fields): - sav_writer.writerow( - [encode_if_str(row, field, True) for field in fields]) - - sav_defs = {} - - # write headers - for section in self.sections: - fields = [element['title'] for element in section['elements']]\ - + self.EXTRA_FIELDS - c = 0 - var_labels = {} - var_names = [] - tmp_k = {} - for field in fields: - c += 1 - var_name = 'var%d' % c - var_labels[var_name] = field - var_names.append(var_name) - tmp_k[field] = var_name - - var_types = dict( - [(tmp_k[element['title']], - 0 if element['type'] in ['decimal', 'int'] else 255) - for element in section['elements']] - + [(tmp_k[item], - 0 if item in ['_id', '_index', '_parent_index'] else 255) - for item in self.EXTRA_FIELDS] - ) - sav_file = NamedTemporaryFile(suffix=".sav") - sav_writer = SavWriter(sav_file.name, varNames=var_names, - varTypes=var_types, - varLabels=var_labels, ioUtf8=True) - sav_defs[section['name']] = { - 'sav_file': sav_file, 'sav_writer': sav_writer} - - index = 1 - indices = {} - survey_name = self.survey.name - for d in data: - # decode mongo section names - joined_export = dict_to_joined_export(d, index, indices, - survey_name) - output = ExportBuilder.decode_mongo_encoded_section_names( - joined_export) - # attach meta fields (index, parent_index, parent_table) - # output has keys for every section - if survey_name not in output: - output[survey_name] = {} - output[survey_name][INDEX] = index - output[survey_name][PARENT_INDEX] = -1 - for section in self.sections: - # get data for this section and write to csv - section_name = section['name'] - sav_def = sav_defs[section_name] - fields = [ - element['xpath'] for element in - section['elements']] + self.EXTRA_FIELDS - sav_writer = sav_def['sav_writer'] - row = output.get(section_name, None) - if type(row) == dict: - write_row( - self.pre_process_row(row, section), - sav_writer, fields) - elif type(row) == list: - for child_row in row: - write_row( - self.pre_process_row(child_row, section), - sav_writer, fields) - index += 1 - - for section_name, sav_def in sav_defs.iteritems(): - sav_def['sav_writer'].closeSavFile( - sav_def['sav_writer'].fh, mode='wb') - - # write zipfile - with ZipFile(path, 'w') as zip_file: - for section_name, sav_def in sav_defs.iteritems(): - sav_file = sav_def['sav_file'] - sav_file.seek(0) - zip_file.write( - sav_file.name, "_".join(section_name.split("/")) + ".sav") - - # close files when we are done - for section_name, sav_def in sav_defs.iteritems(): - sav_def['sav_file'].close() - def dict_to_flat_export(d, parent_index=0): pass @@ -718,9 +538,6 @@ def generate_export(export_type, extension, username, id_string, export_type_func_map = { Export.XLS_EXPORT: 'to_xls_export', Export.CSV_EXPORT: 'to_flat_csv_export', - Export.CSV_ZIP_EXPORT: 'to_zipped_csv', - Export.SAV_ZIP_EXPORT: 'to_zipped_sav', - Export.ANALYSER_EXPORT: 'to_analyser_export' } xform = XForm.objects.get( @@ -747,9 +564,6 @@ def generate_export(export_type, extension, username, id_string, # generate filename basename = "%s_%s" % ( id_string, datetime.now().strftime("%Y_%m_%d_%H_%M_%S")) - if export_type == Export.ANALYSER_EXPORT: - # Analyser exports should be distinguished by more than just their file extension. - basename= '{}_ANALYSER_{}'.format(id_string, datetime.now().strftime("%Y_%m_%d_%H_%M_%S")) filename = basename + "." + extension # check filename is unique @@ -802,7 +616,7 @@ def query_mongo(username, id_string, query=None, hide_deleted=True): def should_create_new_export(xform, export_type): if Export.objects.filter( - xform=xform, export_type=export_type).count() == 0\ + xform=xform, export_type=export_type).count() == 0 \ or Export.exports_outdated(xform, export_type=export_type): return True return False diff --git a/onadata/libs/utils/google.py b/onadata/libs/utils/google.py deleted file mode 100644 index 55201dbce..000000000 --- a/onadata/libs/utils/google.py +++ /dev/null @@ -1,62 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import json - -import urllib -import urllib2 - -import gdata -import gdata.gauth -import gdata.docs -import gdata.data -import gdata.docs.client -import gdata.docs.data - -from django.conf import settings - -oauth2_token = gdata.gauth.OAuth2Token( - client_id=settings.GOOGLE_CLIENT_ID, - client_secret=settings.GOOGLE_CLIENT_SECRET, - scope=' '.join( - ['https://docs.google.com/feeds/', - 'https://spreadsheets.google.com/feeds/']), - user_agent='formhub') - -redirect_uri = oauth2_token.generate_authorize_url( - redirect_uri=settings.GOOGLE_STEP2_URI, - access_type='offline', approval_prompt='force') - - -def get_refreshed_token(token): - data = urllib.urlencode({ - 'client_id': settings.GOOGLE_CLIENT_ID, - 'client_secret': settings.GOOGLE_CLIENT_SECRET, - 'refresh_token': token.refresh_token, - 'grant_type': 'refresh_token'}) - request = urllib2.Request( - url='https://accounts.google.com/o/oauth2/token', - data=data) - request_open = urllib2.urlopen(request) - response = request_open.read() - request_open.close() - tokens = json.loads(response) - token.access_token = tokens['access_token'] - return token - - -def google_export_xls(filename, title, token, blob=True): - if blob: - token = gdata.gauth.token_from_blob(token) - if token.refresh_token is not None \ - and token.access_token is not None: - oauth2_token.refresh_token = token.refresh_token - working_token = get_refreshed_token(oauth2_token) - docs_client = gdata.docs.client.DocsClient( - source=oauth2_token.user_agent) - docs_client = working_token.authorize(docs_client) - xls_doc = gdata.docs.data.Resource( - type='spreadsheet', title=title) - media = gdata.data.MediaSource() - media.SetFileHandle(filename, 'application/vnd.ms-excel') - xls_doc = docs_client.CreateResource(xls_doc, media=media) - return xls_doc.find_html_link() diff --git a/onadata/libs/utils/log.py b/onadata/libs/utils/log.py index 68051c96f..f950b5312 100644 --- a/onadata/libs/utils/log.py +++ b/onadata/libs/utils/log.py @@ -50,8 +50,6 @@ def __iter__(self): SUBMISSION_DELETED="submission-deleted", SUBMISSION_ACCESSED="submission-accessed", SUBMISSION_EDIT_REQUESTED="submission-edit-requested", - SMS_SUPPORT_ACTIVATED="sms-support-activated", - SMS_SUPPORT_DEACTIVATED="sms-support-deactivated", ) diff --git a/onadata/libs/utils/qrcode.py b/onadata/libs/utils/qrcode.py deleted file mode 100644 index 91e39da92..000000000 --- a/onadata/libs/utils/qrcode.py +++ /dev/null @@ -1,24 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import StringIO -from elaphe import barcode -from base64 import b64encode - - -def generate_qrcode(message, stream=None, - eclevel='M', margin=10, - data_mode='8bits', format='PNG', scale=2.5): - """Generate a QRCode, settings options and output.""" - if stream is None: - stream = StringIO.StringIO() - - img = barcode('qrcode', message, - options=dict(version=9, eclevel=eclevel), - margin=margin, data_mode=data_mode, scale=scale) - - img.save(stream, format) - - datauri = "data:image/png;base64,%s" % b64encode(stream.getvalue()) - stream.close() - - return datauri diff --git a/onadata/libs/utils/tests/test_qrcode.py b/onadata/libs/utils/tests/test_qrcode.py deleted file mode 100644 index 98b13ad28..000000000 --- a/onadata/libs/utils/tests/test_qrcode.py +++ /dev/null @@ -1,17 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import -import os -import unittest - -from onadata.libs.utils.qrcode import generate_qrcode - -url = "https://hmh2a.enketo.formhub.org" - - -class TestGenerateQrCode(unittest.TestCase): - def test_generate_qrcode(self): - path = os.path.join(os.path.dirname(__file__), "fixtures", - "qrcode.txt") - with open(path) as f: - qrcode = f.read() - self.assertEqual(generate_qrcode(url), qrcode.strip()) diff --git a/onadata/settings/common.py b/onadata/settings/common.py index fbbd51bc4..d8cd8300c 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -234,10 +234,7 @@ 'onadata.apps.restservice', 'onadata.apps.api', 'guardian', - 'onadata.apps.sms_support', 'onadata.libs', - 'onadata.apps.survey_report', - 'onadata.apps.export', 'pure_pagination', 'django_celery_beat', 'django_extensions',