From 2816633f547e2c308c77a50dd726f0c2df350190 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Thu, 16 Apr 2020 12:10:33 -0300 Subject: [PATCH 001/240] Replaced basestring with six.string_types --- .../tests/viewsets/test_abstract_viewset.py | 3 +-- .../tests/viewsets/test_attachment_viewset.py | 6 +++-- .../apps/api/viewsets/xform_submission_api.py | 5 +++-- onadata/apps/logger/models/instance.py | 14 ++++++------ onadata/apps/main/models/audit.py | 3 ++- .../apps/main/tests/test_form_enter_data.py | 22 +++++++++---------- onadata/apps/sms_support/tools.py | 17 +++++++------- .../management/commands/set_uuid_in_xml.py | 3 ++- onadata/apps/viewer/models/parsed_instance.py | 17 +++++++------- onadata/apps/viewer/pandas_mongo_bridge.py | 8 ++++--- .../apps/viewer/tests/test_export_builder.py | 6 +++-- onadata/libs/models/signals.py | 4 +++- onadata/libs/serializers/fields/json_field.py | 11 ++++++---- onadata/libs/utils/string.py | 6 ++++- onadata/settings/common.py | 6 +++-- 15 files changed, 75 insertions(+), 56 deletions(-) diff --git a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py index 984ed960d..10b583494 100644 --- a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py @@ -130,8 +130,7 @@ def _set_api_permissions(self, user): user.user_permissions.add(add_userprofile) def _create_user_profile(self, extra_post_data={}): - self.profile_data = dict( - self.profile_data.items() + extra_post_data.items()) + self.profile_data.update(extra_post_data) user, created = User.objects.get_or_create( username=self.profile_data['username'], first_name=self.profile_data['name'], diff --git a/onadata/apps/api/tests/viewsets/test_attachment_viewset.py b/onadata/apps/api/tests/viewsets/test_attachment_viewset.py index a0c6ab4d5..d1ccfb7a5 100644 --- a/onadata/apps/api/tests/viewsets/test_attachment_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_attachment_viewset.py @@ -3,6 +3,8 @@ from os import path +from django.utils.six import string_types + from onadata.apps.api.tests.viewsets.test_abstract_viewset import \ TestAbstractViewSet from onadata.apps.api.viewsets.attachment_viewset import AttachmentViewSet @@ -112,7 +114,7 @@ def test_direct_image_link(self): request = self.factory.get('/', data, **self.extra) response = self.retrieve_view(request, pk=self.attachment.pk) self.assertEqual(response.status_code, 200) - self.assertTrue(isinstance(response.data, basestring)) + self.assertTrue(isinstance(response.data, string_types)) self.assertEqual(response.data, self.attachment.secure_url()) data['filename'] = 10000000 @@ -137,5 +139,5 @@ def test_direct_image_link_uppercase(self): request = self.factory.get('/', data, **self.extra) response = self.retrieve_view(request, pk=self.attachment.pk) self.assertEqual(response.status_code, 200) - self.assertTrue(isinstance(response.data, basestring)) + self.assertTrue(isinstance(response.data, string_types)) self.assertEqual(response.data, self.attachment.secure_url()) diff --git a/onadata/apps/api/viewsets/xform_submission_api.py b/onadata/apps/api/viewsets/xform_submission_api.py index 60b54968c..0c94a3bb7 100644 --- a/onadata/apps/api/viewsets/xform_submission_api.py +++ b/onadata/apps/api/viewsets/xform_submission_api.py @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 +from django.utils.six import string_types from django.utils.translation import ugettext as _ from rest_framework import permissions @@ -44,7 +45,7 @@ def dict_lists2strings(d): :param d: The dict to convert. :returns: The converted dict.""" for k, v in d.items(): - if isinstance(v, list) and all([isinstance(e, basestring) for e in v]): + if isinstance(v, list) and all([isinstance(e, string_types) for e in v]): d[k] = ' '.join(v) elif isinstance(v, dict): d[k] = dict_lists2strings(v) @@ -207,7 +208,7 @@ def error_response(self, error, is_json_request, request): if not error: error_msg = _("Unable to create submission.") status_code = status.HTTP_400_BAD_REQUEST - elif isinstance(error, basestring): + elif isinstance(error, string_types): error_msg = error status_code = status.HTTP_400_BAD_REQUEST elif not is_json_request: diff --git a/onadata/apps/logger/models/instance.py b/onadata/apps/logger/models/instance.py index 61a308969..aa4d75364 100644 --- a/onadata/apps/logger/models/instance.py +++ b/onadata/apps/logger/models/instance.py @@ -13,6 +13,7 @@ from django.db.models.signals import post_delete from django.db.models.signals import post_save from django.utils import timezone +from django.utils.encoding import smart_text from jsonfield import JSONField from taggit.managers import TaggableManager @@ -244,15 +245,15 @@ def _set_uuid(self): set_uuid(self) def _populate_xml_hash(self): - ''' + """ Populate the `xml_hash` attribute of this `Instance` based on the content of the `xml` attribute. - ''' + """ self.xml_hash = self.get_hash(self.xml) @classmethod def populate_xml_hashes_for_instances(cls, usernames=None, pk__in=None, repopulate=False): - ''' + """ Populate the `xml_hash` field for `Instance` instances limited to the specified users and/or DB primary keys. @@ -263,7 +264,7 @@ def populate_xml_hashes_for_instances(cls, usernames=None, pk__in=None, repopula :param bool repopulate: Optional argument to force repopulation of existing hashes. :returns: Total number of `Instance`s updated. :rtype: int - ''' + """ filter_kwargs = dict() if usernames: @@ -359,12 +360,11 @@ def get_hash(input_string): """ Compute the SHA256 hash of the given string. A wrapper to standardize hash computation. - :param basestring input_string: The string to be hashed. + :param string_types input_string: The string to be hashed. :return: The resulting hash. :rtype: str """ - if isinstance(input_string, unicode): - input_string = input_string.encode('utf-8') + input_string = smart_text(input_string) return sha256(input_string).hexdigest() @property diff --git a/onadata/apps/main/models/audit.py b/onadata/apps/main/models/audit.py index a88bcdb20..7d1d0c575 100644 --- a/onadata/apps/main/models/audit.py +++ b/onadata/apps/main/models/audit.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from django.conf import settings +from django.utils.six import string_types from onadata.apps.viewer.models.parsed_instance import DATETIME_FORMAT from onadata.apps.api.mongo_helper import MongoHelper @@ -38,7 +39,7 @@ def query_mongo(cls, username, query=None, fields=None, sort=None, start=0, val, DATETIME_FORMAT) except ValueError: pass - elif isinstance(query[cls.CREATED_ON], basestring): + elif isinstance(query[cls.CREATED_ON], string_types): val = query[cls.CREATED_ON] try: created_on = datetime.strptime(val, DATETIME_FORMAT) diff --git a/onadata/apps/main/tests/test_form_enter_data.py b/onadata/apps/main/tests/test_form_enter_data.py index 92d29c693..9d4506d19 100644 --- a/onadata/apps/main/tests/test_form_enter_data.py +++ b/onadata/apps/main/tests/test_form_enter_data.py @@ -1,25 +1,25 @@ # 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 urlparse import urlparse -from django.test import RequestFactory -from django.core.urlresolvers import reverse -from django.core.validators import URLValidator +import requests from django.conf import settings from django.contrib.auth.models import AnonymousUser - +from django.core.urlresolvers import reverse +from django.core.validators import URLValidator +from django.test import RequestFactory +from django.utils.six import string_types +from httmock import urlmatch, HTTMock 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.apps.main.models import MetaData +from onadata.apps.main.views import set_perm, show, qrcode from onadata.libs.utils.viewer_tools import enketo_url from .test_base import TestBase @@ -68,7 +68,7 @@ def test_enketo_remote_server(self): 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.assertIsInstance(url, string_types) self.assertIsNone(URLValidator()(url)) def _get_grcode_view_response(self): diff --git a/onadata/apps/sms_support/tools.py b/onadata/apps/sms_support/tools.py index 982380546..5e0262474 100644 --- a/onadata/apps/sms_support/tools.py +++ b/onadata/apps/sms_support/tools.py @@ -2,25 +2,24 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import - -import mimetypes -import io import copy +import io +import mimetypes 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.core.files.uploadedfile import InMemoryUploadedFile from django.http import HttpRequest +from django.utils.six import string_types +from django.utils.translation import ugettext as _ -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.apps.logger.xform_instance_parser import InstanceEmptyError, \ + InstanceInvalidUserError, DuplicateInstance 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' @@ -56,7 +55,7 @@ def get_sms_instance_id(instance): def sms_media_to_file(file_object, name): - if isinstance(file_object, basestring): + if isinstance(file_object, string_types): file_object = io.BytesIO(file_object) def getsize(f): diff --git a/onadata/apps/viewer/management/commands/set_uuid_in_xml.py b/onadata/apps/viewer/management/commands/set_uuid_in_xml.py index ffa75c630..62710a638 100644 --- a/onadata/apps/viewer/management/commands/set_uuid_in_xml.py +++ b/onadata/apps/viewer/management/commands/set_uuid_in_xml.py @@ -1,5 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + from django.core.management.base import BaseCommand from django.utils.translation import ugettext as _, ugettext_lazy @@ -19,4 +20,4 @@ def handle(self, *args, **kwargs): dd.set_uuid_in_xml(id_string=dd.id_string) super(DataDictionary, dd).save() if (i + 1) % 10 == 0: - print _('Updated %(nb)d XForms...') % {'nb': i} + print(_('Updated %(nb)d XForms...') % {'nb': i}) diff --git a/onadata/apps/viewer/models/parsed_instance.py b/onadata/apps/viewer/models/parsed_instance.py index 3a3d78ea6..879a50ac2 100644 --- a/onadata/apps/viewer/models/parsed_instance.py +++ b/onadata/apps/viewer/models/parsed_instance.py @@ -1,5 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import datetime import json import logging @@ -9,9 +10,11 @@ from dateutil import parser from django.conf import settings from django.db import models -from django.db.models.signals import post_save, pre_delete +from django.db.models.signals import pre_delete +from django.utils.six import string_types from django.utils.translation import ugettext as _ +from onadata.apps.api.mongo_helper import MongoHelper from onadata.apps.logger.models import Instance from onadata.apps.logger.models import Note from onadata.apps.restservice.utils import call_service @@ -30,8 +33,6 @@ ) from onadata.libs.utils.decorators import apply_form_field_names from onadata.libs.utils.model_tools import queryset_iterator -from onadata.apps.api.mongo_helper import MongoHelper - # this is Mongo Collection where we will store the parsed submissions xform_instances = settings.MONGO_DB.instances @@ -102,7 +103,7 @@ def query_mongo(cls, username, id_string, query, fields, sort, start=0, if count: return [{"count": cursor.count()}] - if isinstance(sort, basestring): + if isinstance(sort, string_types): sort = json.loads(sort, object_hook=json_util.object_hook) sort = sort if sort else {} @@ -119,7 +120,7 @@ def mongo_aggregate(cls, query, pipeline, hide_deleted=True): pipeline - list of dicts or dict of mongodb pipeline operators, http://docs.mongodb.org/manual/reference/operator/aggregation-pipeline """ - if isinstance(query, basestring): + if isinstance(query, string_types): query = json.loads( query, object_hook=json_util.object_hook) if query else {} if not (isinstance(pipeline, dict) or isinstance(pipeline, list)): @@ -153,7 +154,7 @@ def query_mongo_minimal( if count: return [{"count": cursor.count()}] - if isinstance(sort, basestring): + if isinstance(sort, string_types): sort = json.loads(sort, object_hook=json_util.object_hook) sort = sort if sort else {} @@ -191,7 +192,7 @@ def _get_mongo_cursor(cls, query, fields, hide_deleted, username=None, id_string fields_to_select = {cls.USERFORM_ID: 0} # TODO: give more detailed error messages to 3rd parties # using the API when json.loads fails - if isinstance(query, basestring): + if isinstance(query, string_types): query = json.loads(query, object_hook=json_util.object_hook) query = query if query else {} query = MongoHelper.to_safe_dict(query, reading=True) @@ -208,7 +209,7 @@ def _get_mongo_cursor(cls, query, fields, hide_deleted, username=None, id_string query = {"$and": [query, {"_deleted_at": None}]} # fields must be a string array i.e. '["name", "age"]' - if isinstance(fields, basestring): + if isinstance(fields, string_types): fields = json.loads(fields, object_hook=json_util.object_hook) fields = fields if fields else [] diff --git a/onadata/apps/viewer/pandas_mongo_bridge.py b/onadata/apps/viewer/pandas_mongo_bridge.py index e279f86c4..fd15343c6 100644 --- a/onadata/apps/viewer/pandas_mongo_bridge.py +++ b/onadata/apps/viewer/pandas_mongo_bridge.py @@ -1,10 +1,12 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + +import time from collections import OrderedDict from itertools import chain -import time from django.conf import settings +from django.utils.six import string_types from pandas.core.frame import DataFrame # an immediate fix to an error with the installation of pandas v0.15 @@ -187,7 +189,7 @@ def _tag_edit_string(cls, record): def _split_gps_fields(cls, record, gps_fields): updated_gps_fields = {} for key, value in record.iteritems(): - if key in gps_fields and isinstance(value, basestring): + if key in gps_fields and isinstance(value, string_types): gps_xpaths = DataDictionary.get_additional_geopoint_xpaths(key) gps_parts = dict([(xpath, None) for xpath in gps_xpaths]) # hack, check if its a list and grab the object within that @@ -482,7 +484,7 @@ def _add_column_to_section(self, sheet_name, column): xpath = None if isinstance(column, SurveyElement): xpath = column.get_abbreviated_xpath() - elif isinstance(column, basestring): + elif isinstance(column, string_types): xpath = column assert(xpath) # make sure column is not already in list diff --git a/onadata/apps/viewer/tests/test_export_builder.py b/onadata/apps/viewer/tests/test_export_builder.py index 5f803336e..f9c24b0bf 100644 --- a/onadata/apps/viewer/tests/test_export_builder.py +++ b/onadata/apps/viewer/tests/test_export_builder.py @@ -1,5 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import csv import datetime import os @@ -10,12 +11,13 @@ from django.conf import settings from django.core.files.temp import NamedTemporaryFile +from django.utils.six import string_types from openpyxl import load_workbook from pyxform.builder import create_survey_from_xls from savReaderWriter import SavReader -from onadata.apps.main.tests.test_base import TestBase from onadata.apps.api.mongo_helper import MongoHelper +from onadata.apps.main.tests.test_base import TestBase from onadata.apps.viewer.tests.export_helpers import viewer_fixture_path from onadata.libs.utils.export_tools import ( dict_to_joined_export, @@ -914,7 +916,7 @@ def test_type_conversion(self): data = dict_to_joined_export(submission_2, 1, indices, survey_name) new_row = export_builder.pre_process_row(data[survey_name], export_builder.sections[0]) - self.assertIsInstance(new_row['amount'], basestring) + self.assertIsInstance(new_row['amount'], string_types) self.assertEqual(new_row['amount'], '') def test_xls_convert_dates_before_1900(self): diff --git a/onadata/libs/models/signals.py b/onadata/libs/models/signals.py index 878b3e05c..843cb2704 100644 --- a/onadata/libs/models/signals.py +++ b/onadata/libs/models/signals.py @@ -1,6 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import django.dispatch +from django.utils.six import string_types from onadata.apps.logger.models import XForm @@ -26,7 +28,7 @@ def add_tags_to_xform_instances(sender, **kwargs): def delete_tag_from_xform_instances(sender, **kwargs): xform = kwargs.get('xform', None) tag = kwargs.get('tag', None) - if isinstance(xform, XForm) and isinstance(tag, basestring): + if isinstance(xform, XForm) and isinstance(tag, string_types): # update existing instances with the new tag for instance in xform.instances.all(): if tag in instance.tags.names(): diff --git a/onadata/libs/serializers/fields/json_field.py b/onadata/libs/serializers/fields/json_field.py index 13f32b15f..8d2ba5ad0 100644 --- a/onadata/libs/serializers/fields/json_field.py +++ b/onadata/libs/serializers/fields/json_field.py @@ -1,25 +1,28 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from rest_framework import serializers + import json +from django.utils.six import string_types +from rest_framework import serializers + class JsonField(serializers.Field): def to_representation(self, value): - if isinstance(value, basestring): + if isinstance(value, string_types): return json.loads(value) return value def to_internal_value(self, value): - if isinstance(value, basestring): + if isinstance(value, string_types): return json.loads(value) return value @classmethod def to_json(cls, data): - if isinstance(data, basestring): + if isinstance(data, string_types): return json.loads(data) return data diff --git a/onadata/libs/utils/string.py b/onadata/libs/utils/string.py index f7d66aaef..4b8546564 100644 --- a/onadata/libs/utils/string.py +++ b/onadata/libs/utils/string.py @@ -1,5 +1,9 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + +from django.utils.six import string_types + + def str2bool(v): return v.lower() in ( - 'yes', 'true', 't', '1') if isinstance(v, basestring) else v + 'yes', 'true', 't', '1') if isinstance(v, string_types) else v diff --git a/onadata/settings/common.py b/onadata/settings/common.py index c050423a2..af826b534 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -1,6 +1,7 @@ # coding: utf-8 -from __future__ import unicode_literals, print_function, division, absolute_import # vim: set fileencoding=utf-8 +from __future__ import unicode_literals, print_function, division, absolute_import + # this system uses structured settings as defined in # http://www.slideshare.net/jacobian/the-best-and-worst-of-django # @@ -17,6 +18,7 @@ import subprocess # nopep8, used by included files import sys # nopep8, used by included files +from django.utils.six import string_types from django.core.exceptions import SuspiciousOperation from pymongo import MongoClient @@ -511,7 +513,7 @@ def skip_suspicious_operations(record): except ImportError: pass -if isinstance(TEMPLATE_OVERRIDE_ROOT_DIR, basestring): +if isinstance(TEMPLATE_OVERRIDE_ROOT_DIR, string_types): # site templates overrides TEMPLATE_DIRS = ( os.path.join(ONADATA_DIR, TEMPLATE_OVERRIDE_ROOT_DIR, 'templates'), From 478350e2f4140fc6b93d556922940f569d4e0495 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Thu, 16 Apr 2020 12:12:10 -0300 Subject: [PATCH 002/240] Replaced "assertEquals" with "assertEqual" --- .../apps/api/tests/viewsets/test_data_viewset.py | 4 ++-- .../apps/api/tests/viewsets/test_note_viewset.py | 2 +- onadata/apps/logger/tests/test_form_list.py | 2 +- onadata/apps/logger/tests/test_form_submission.py | 8 ++++---- onadata/apps/logger/tests/test_parsing.py | 2 +- .../apps/logger/tests/test_simple_submission.py | 14 +++++++------- onadata/apps/main/tests/test_csv_export.py | 8 ++++---- onadata/apps/main/tests/test_form_auth.py | 2 +- onadata/apps/main/tests/test_form_errors.py | 6 +++--- onadata/apps/main/tests/test_form_metadata.py | 8 ++++---- onadata/apps/main/tests/test_google_doc.py | 2 +- onadata/apps/main/tests/test_process.py | 6 +++--- .../tests/test_user_id_string_unique_together.py | 6 +++--- onadata/apps/restservice/tests/test_restservice.py | 2 +- onadata/apps/viewer/tests/test_mongo_data.py | 12 ++++++------ .../serializers/test_attachment_serializer.py | 2 +- 16 files changed, 43 insertions(+), 43 deletions(-) diff --git a/onadata/apps/api/tests/viewsets/test_data_viewset.py b/onadata/apps/api/tests/viewsets/test_data_viewset.py index 81ddcce37..7d2811742 100644 --- a/onadata/apps/api/tests/viewsets/test_data_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_data_viewset.py @@ -384,7 +384,7 @@ def test_delete_submission(self): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) count = self.xform.instances.all().count() - self.assertEquals(before_count - 1, count) + self.assertEqual(before_count - 1, count) self._create_user_and_login(username='alice', password='alice') # Allow Alice to delete submissions. @@ -398,5 +398,5 @@ def test_delete_submission(self): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) count = self.xform.instances.all().count() - self.assertEquals(before_count - 2, count) + self.assertEqual(before_count - 2, count) diff --git a/onadata/apps/api/tests/viewsets/test_note_viewset.py b/onadata/apps/api/tests/viewsets/test_note_viewset.py index ce3dfd560..805452968 100644 --- a/onadata/apps/api/tests/viewsets/test_note_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_note_viewset.py @@ -98,4 +98,4 @@ def test_delete_note(self): request = self.factory.get('/', **self.extra) response = self.view(request) self.assertEqual(response.status_code, 200) - self.assertEquals(response.data, []) + self.assertEqual(response.data, []) diff --git a/onadata/apps/logger/tests/test_form_list.py b/onadata/apps/logger/tests/test_form_list.py index 02f55e2c6..cde756e2c 100644 --- a/onadata/apps/logger/tests/test_form_list.py +++ b/onadata/apps/logger/tests/test_form_list.py @@ -44,4 +44,4 @@ def test_show_for_anon_when_require_auth_false(self): request = self.factory.get('/') request.user = AnonymousUser() response = formList(request, self.user.username) - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) diff --git a/onadata/apps/logger/tests/test_form_submission.py b/onadata/apps/logger/tests/test_form_submission.py index aac601810..2c27f33d4 100644 --- a/onadata/apps/logger/tests/test_form_submission.py +++ b/onadata/apps/logger/tests/test_form_submission.py @@ -51,7 +51,7 @@ def test_fail_with_ioerror_read(self, mock_pop): mock_pop.side_effect = IOError( 'request data read error') - self.assertEquals(0, self.xform.instances.count()) + self.assertEqual(0, self.xform.instances.count()) xml_submission_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -60,14 +60,14 @@ def test_fail_with_ioerror_read(self, mock_pop): self._make_submission(xml_submission_file_path) self.assertEqual(self.response.status_code, 400) - self.assertEquals(0, self.xform.instances.count()) + self.assertEqual(0, self.xform.instances.count()) @patch('django.utils.datastructures.MultiValueDict.pop') def test_fail_with_ioerror_wsgi(self, mock_pop): mock_pop.side_effect = IOError( 'error during read(65536) on wsgi.input') - self.assertEquals(0, self.xform.instances.count()) + self.assertEqual(0, self.xform.instances.count()) xml_submission_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -76,7 +76,7 @@ def test_fail_with_ioerror_wsgi(self, mock_pop): self._make_submission(xml_submission_file_path) self.assertEqual(self.response.status_code, 400) - self.assertEquals(0, self.xform.instances.count()) + self.assertEqual(0, self.xform.instances.count()) def test_submission_to_require_auth_anon(self): """ diff --git a/onadata/apps/logger/tests/test_parsing.py b/onadata/apps/logger/tests/test_parsing.py index 96f1a0f2c..f245f3b35 100644 --- a/onadata/apps/logger/tests/test_parsing.py +++ b/onadata/apps/logger/tests/test_parsing.py @@ -170,7 +170,7 @@ def test_parse_xform_nested_repeats_multiple_nodes(self): "multiple_nodes_error.xml" ) self._make_submission(xml_submission_file_path) - self.assertEquals(201, self.response.status_code) + self.assertEqual(201, self.response.status_code) def test_xml_repeated_group_to_dict(self): xml_file = os.path.join( diff --git a/onadata/apps/logger/tests/test_simple_submission.py b/onadata/apps/logger/tests/test_simple_submission.py index 3f428eac0..ccc3fe75a 100644 --- a/onadata/apps/logger/tests/test_simple_submission.py +++ b/onadata/apps/logger/tests/test_simple_submission.py @@ -74,31 +74,31 @@ def test_start_time_boolean_properly_set(self): self.assertTrue(self.xform2.has_start_time) def test_simple_yes_submission(self): - self.assertEquals(0, self.xform1.instances.count()) + self.assertEqual(0, self.xform1.instances.count()) self._submit_simple_yes() - self.assertEquals(1, self.xform1.instances.count()) + self.assertEqual(1, self.xform1.instances.count()) self._submit_simple_yes() # a simple "yes" submission *SHOULD* increment the survey count - self.assertEquals(2, self.xform1.instances.count()) + self.assertEqual(2, self.xform1.instances.count()) def test_start_time_submissions(self): """This test checks to make sure that instances *with start_time available* are marked as duplicates when the XML is a direct match. """ - self.assertEquals(0, self.xform2.instances.count()) + self.assertEqual(0, self.xform2.instances.count()) self._submit_at_hour(11) - self.assertEquals(1, self.xform2.instances.count()) + self.assertEqual(1, self.xform2.instances.count()) self._submit_at_hour(12) - self.assertEquals(2, self.xform2.instances.count()) + self.assertEqual(2, self.xform2.instances.count()) # an instance from 11 AM already exists in the database, so it # *SHOULD NOT* increment the survey count. self._submit_at_hour(11) - self.assertEquals(2, self.xform2.instances.count()) + self.assertEqual(2, self.xform2.instances.count()) def test_corrupted_submission(self): """ diff --git a/onadata/apps/main/tests/test_csv_export.py b/onadata/apps/main/tests/test_csv_export.py index 22224d520..de3bae57f 100644 --- a/onadata/apps/main/tests/test_csv_export.py +++ b/onadata/apps/main/tests/test_csv_export.py @@ -41,7 +41,7 @@ def test_csv_export_output(self): with storage.open(export.filepath) as f2: expected_content = f1.read() actual_content = f2.read() - self.assertEquals(actual_content, expected_content) + self.assertEqual(actual_content, expected_content) def test_csv_nested_repeat_output(self): path = os.path.join(self.fixture_dir, 'double_repeat.xls') @@ -58,7 +58,7 @@ def test_csv_nested_repeat_output(self): '/double_repeat/bed_net[2]/member[2]/name', '/double_repeat/meta/instanceID' ] - self.assertEquals(dd.xpaths(repeat_iterations=2), xpaths) + self.assertEqual(dd.xpaths(repeat_iterations=2), xpaths) # test csv export = generate_export(Export.CSV_EXPORT, 'csv', self.user.username, 'double_repeat') @@ -70,7 +70,7 @@ def test_csv_nested_repeat_output(self): with storage.open(export.filepath) as f2: expected_content = f1.read() actual_content = f2.read() - self.assertEquals(actual_content, expected_content) + self.assertEqual(actual_content, expected_content) def test_dotted_fields_csv_export_output(self): path = os.path.join(os.path.dirname(__file__), 'fixtures', 'userone', @@ -93,4 +93,4 @@ def test_dotted_fields_csv_export_output(self): with storage.open(export.filepath) as f2: expected_content = f1.read() actual_content = f2.read() - self.assertEquals(actual_content, expected_content) + self.assertEqual(actual_content, expected_content) diff --git a/onadata/apps/main/tests/test_form_auth.py b/onadata/apps/main/tests/test_form_auth.py index 729f0f2b0..b7ebd162d 100644 --- a/onadata/apps/main/tests/test_form_auth.py +++ b/onadata/apps/main/tests/test_form_auth.py @@ -13,4 +13,4 @@ def setUp(self): def test_login_redirect_redirects(self): response = self.client.get(reverse(login_redirect)) - self.assertEquals(response.status_code, 302) + self.assertEqual(response.status_code, 302) diff --git a/onadata/apps/main/tests/test_form_errors.py b/onadata/apps/main/tests/test_form_errors.py index fb3363a52..b0a90a3c5 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.assertEqual(response.status_code, 200) self.xform = XForm.objects.all()[0] def test_bad_id_string(self): @@ -27,8 +27,8 @@ 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(XForm.objects.count(), count) + self.assertEqual(response.status_code, 200) + self.assertEqual(XForm.objects.count(), count) @skip def test_dl_no_xls(self): diff --git a/onadata/apps/main/tests/test_form_metadata.py b/onadata/apps/main/tests/test_form_metadata.py index 910dbceb2..6ebaa9f2b 100644 --- a/onadata/apps/main/tests/test_form_metadata.py +++ b/onadata/apps/main/tests/test_form_metadata.py @@ -58,14 +58,14 @@ 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( + self.assertEqual(count + 1, len(MetaData.objects.filter( xform=self.xform, data_type='supporting_doc'))) 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.assertEquals(count + 1, len(MetaData.objects.filter( + self.assertEqual(count + 1, len(MetaData.objects.filter( xform=self.xform, data_type='media'))) def test_adds_mapbox_layer_on_submit(self): @@ -75,7 +75,7 @@ def test_adds_mapbox_layer_on_submit(self): self.post_data['map_name'] = 'test_mapbox_layer' self.post_data['link'] = 'http://0.0.0.0:8080' self.client.post(self.edit_url, self.post_data) - self.assertEquals(count + 1, len(MetaData.objects.filter( + self.assertEqual(count + 1, len(MetaData.objects.filter( xform=self.xform, data_type='mapbox_layer'))) def test_shows_supporting_doc_after_submit(self): @@ -257,7 +257,7 @@ def test_add_media_url(self): uri = 'https://devtrac.ona.io/fieldtrips.csv' count = MetaData.objects.filter(data_type='media').count() self.client.post(self.edit_url, {'media_url': uri}) - self.assertEquals(count + 1, + self.assertEqual(count + 1, len(MetaData.objects.filter(data_type='media'))) def test_windows_csv_file_upload(self): diff --git a/onadata/apps/main/tests/test_google_doc.py b/onadata/apps/main/tests/test_google_doc.py index 6b757478e..6b3595faf 100644 --- a/onadata/apps/main/tests/test_google_doc.py +++ b/onadata/apps/main/tests/test_google_doc.py @@ -23,4 +23,4 @@ def test_view(self): 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()) + self.assertEqual(doc._navigation_html(), f.read()) diff --git a/onadata/apps/main/tests/test_process.py b/onadata/apps/main/tests/test_process.py index 7cd003c3d..c82abe1b8 100644 --- a/onadata/apps/main/tests/test_process.py +++ b/onadata/apps/main/tests/test_process.py @@ -283,7 +283,7 @@ def _check_data_for_csv_export(self): del d_from_db[k] self.assertTrue(d_from_db in data) data.remove(d_from_db) - self.assertEquals(data, []) + self.assertEqual(data, []) def _check_group_xpaths_do_not_appear_in_dicts_for_export(self): uuid = 'uuid:f3d8dc65-91a6-4d0f-9e97-802128083390' @@ -443,9 +443,9 @@ def _check_xls_export(self): self.assertEqual(actual_row, expected_row) def _check_delete(self): - self.assertEquals(self.user.xforms.count(), 1) + self.assertEqual(self.user.xforms.count(), 1) self.user.xforms.all()[0].delete() - self.assertEquals(self.user.xforms.count(), 0) + self.assertEqual(self.user.xforms.count(), 0) def test_405_submission(self): url = reverse('submissions') diff --git a/onadata/apps/main/tests/test_user_id_string_unique_together.py b/onadata/apps/main/tests/test_user_id_string_unique_together.py index badfc582b..bb0a3a1fa 100644 --- a/onadata/apps/main/tests/test_user_id_string_unique_together.py +++ b/onadata/apps/main/tests/test_user_id_string_unique_together.py @@ -23,16 +23,16 @@ def test_unique_together(self): # first time response = self._publish_xls_file(xls_path) - self.assertEquals(XForm.objects.count(), 1) + self.assertEqual(XForm.objects.count(), 1) # second time response = self._publish_xls_file(xls_path) response_content = smart_text(response.content) self.assertIn("already exists.", response_content) - self.assertEquals(XForm.objects.count(), 1) + self.assertEqual(XForm.objects.count(), 1) self.client.logout() # first time self._create_user_and_login(username="carl", password="carl") response = self._publish_xls_file(xls_path) - self.assertEquals(XForm.objects.count(), 2) + self.assertEqual(XForm.objects.count(), 2) diff --git a/onadata/apps/restservice/tests/test_restservice.py b/onadata/apps/restservice/tests/test_restservice.py index 8e0bc1fab..02b087dfa 100644 --- a/onadata/apps/restservice/tests/test_restservice.py +++ b/onadata/apps/restservice/tests/test_restservice.py @@ -34,7 +34,7 @@ def _create_rest_service(self): def test_create_rest_service(self): count = RestService.objects.all().count() self._create_rest_service() - self.assertEquals(RestService.objects.all().count(), count + 1) + self.assertEqual(RestService.objects.all().count(), count + 1) def test_service_definition(self): self._create_rest_service() diff --git a/onadata/apps/viewer/tests/test_mongo_data.py b/onadata/apps/viewer/tests/test_mongo_data.py index 4710c4363..96532f8a7 100644 --- a/onadata/apps/viewer/tests/test_mongo_data.py +++ b/onadata/apps/viewer/tests/test_mongo_data.py @@ -18,7 +18,7 @@ def setUp(self): TestBase.setUp(self) self.instances = settings.MONGO_DB.instances self.instances.remove() - self.assertEquals(list(self.instances.find()), []) + self.assertEqual(list(self.instances.find()), []) xls_path = os.path.join(self.this_directory, 'fixtures', 'transportation', 'mongo', 'transportation_with_dirty_mongo_ids.xls') @@ -32,23 +32,23 @@ def setUp(self): self.pi = self.xform.instances.all()[0].parsed_instance def test_mongo_find_one(self): - self.assertEquals(self.pi.to_dict_for_mongo(), + self.assertEqual(self.pi.to_dict_for_mongo(), self.instances.find_one()) def test_mongo_find(self): self.assertNotEquals([self.pi.to_dict()], list(self.instances.find())) - self.assertEquals([self.pi.to_dict_for_mongo()], + self.assertEqual([self.pi.to_dict_for_mongo()], list(self.instances.find())) def test_mongo_find_by_id(self): - self.assertEquals(self.pi.to_dict_for_mongo(), self.instances.find_one( + self.assertEqual(self.pi.to_dict_for_mongo(), self.instances.find_one( {common_tags.ID: self.pi.instance.id})) def test_mongo_find_by_uuid(self): - self.assertEquals(self.pi.to_dict_for_mongo(), self.instances.find_one( + self.assertEqual(self.pi.to_dict_for_mongo(), self.instances.find_one( {common_tags.UUID: self.pi.instance.uuid})) def test_mongo_find_by_key_value_pair(self): for key, value in self.pi.to_dict_for_mongo().items(): - self.assertEquals(self.pi.to_dict_for_mongo(), + self.assertEqual(self.pi.to_dict_for_mongo(), self.instances.find_one({key: value})) diff --git a/onadata/libs/tests/serializers/test_attachment_serializer.py b/onadata/libs/tests/serializers/test_attachment_serializer.py index 955d6615e..ace6be559 100644 --- a/onadata/libs/tests/serializers/test_attachment_serializer.py +++ b/onadata/libs/tests/serializers/test_attachment_serializer.py @@ -61,4 +61,4 @@ def setUp(self): def test_get_field_xpath_of_an_object(self): path = get_path(self.data, self.question) - self.assertEquals(path, "group1/group2/photograph") + self.assertEqual(path, "group1/group2/photograph") From e2b5a38693f9f2425703e1744a9b1687a816ec92 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Thu, 16 Apr 2020 13:47:54 -0300 Subject: [PATCH 003/240] Replaced "unicode()" and "__unicode__" with six utilities --- .../viewsets/test_user_profile_viewset.py | 3 +- onadata/apps/logger/exceptions.py | 7 ++-- onadata/apps/logger/models/survey_type.py | 4 ++- onadata/apps/logger/models/xform.py | 5 +-- onadata/apps/logger/views.py | 6 ++-- onadata/apps/logger/xform_instance_parser.py | 36 ++++++++----------- onadata/apps/main/models/user_profile.py | 15 ++++++-- onadata/apps/restservice/models.py | 4 ++- onadata/apps/sms_support/autodoc.py | 7 ++-- onadata/apps/sms_support/parser.py | 25 ++++++++----- onadata/apps/viewer/models/data_dictionary.py | 6 ++-- onadata/apps/viewer/models/export.py | 8 ++--- onadata/apps/viewer/xls_writer.py | 5 ++- onadata/libs/utils/analyser_export.py | 7 ++-- onadata/libs/utils/logger_tools.py | 28 +++++++-------- 15 files changed, 95 insertions(+), 71 deletions(-) diff --git a/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py b/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py index 93ddb0717..0a030d10a 100644 --- a/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py @@ -4,6 +4,7 @@ import json from django.contrib.auth.models import User +from django.utils.six import text_type from rest_framework import status from onadata.apps.api.tests.viewsets.test_abstract_viewset import \ @@ -162,7 +163,7 @@ def test_profile_create_mixed_case(self): user__username=data['username'].lower()) data['id'] = profile.user.pk - data['gravatar'] = unicode(profile.gravatar) + data['gravatar'] = text_type(profile.gravatar) data['url'] = 'http://testserver/api/v1/profiles/deno' data['user'] = 'http://testserver/api/v1/users/deno' data['username'] = 'deno' diff --git a/onadata/apps/logger/exceptions.py b/onadata/apps/logger/exceptions.py index 07d322eda..1fba01a4d 100644 --- a/onadata/apps/logger/exceptions.py +++ b/onadata/apps/logger/exceptions.py @@ -1,6 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import gettext as _ @@ -8,9 +9,7 @@ class DuplicateUUIDError(Exception): pass +@python_2_unicode_compatible class FormInactiveError(Exception): - def __unicode__(self): - return _("Form is inactive") - def __str__(self): - return unicode(self).encode('utf-8') + return _("Form is inactive") diff --git a/onadata/apps/logger/models/survey_type.py b/onadata/apps/logger/models/survey_type.py index d922a24f4..41bebeeed 100644 --- a/onadata/apps/logger/models/survey_type.py +++ b/onadata/apps/logger/models/survey_type.py @@ -2,13 +2,15 @@ from __future__ import unicode_literals, print_function, division, absolute_import from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class SurveyType(models.Model): slug = models.CharField(max_length=100, unique=True) class Meta: app_label = 'logger' - def __unicode__(self): + def __str__(self): return "SurveyType: %s" % self.slug diff --git a/onadata/apps/logger/models/xform.py b/onadata/apps/logger/models/xform.py index bf46ae1f0..d65eacb6a 100644 --- a/onadata/apps/logger/models/xform.py +++ b/onadata/apps/logger/models/xform.py @@ -20,6 +20,7 @@ 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 \ assign_perm, \ @@ -47,7 +48,7 @@ def upload_to(instance, filename): 'xls', os.path.split(filename)[1]) - +@python_2_unicode_compatible class XForm(BaseModel): CLONED_SUFFIX = '_cloned' MAX_ID_LENGTH = 100 @@ -209,7 +210,7 @@ def save(self, *args, **kwargs): super(XForm, self).save(*args, **kwargs) - def __unicode__(self): + def __str__(self): return getattr(self, "id_string", "") def submission_count(self, force_update=False): diff --git a/onadata/apps/logger/views.py b/onadata/apps/logger/views.py index cf2432ca4..635403bbd 100644 --- a/onadata/apps/logger/views.py +++ b/onadata/apps/logger/views.py @@ -28,7 +28,7 @@ from django.shortcuts import render from django.template import loader from django.template import RequestContext -from django.utils import six +from django.utils.six import string_types, text_type from django.utils.translation import ugettext as _ from django.views.decorators.http import require_GET, require_POST from django.views.decorators.http import require_http_methods @@ -71,7 +71,7 @@ def _bad_request(e): - strerror = unicode(e) + strerror = text_type(e) return strerror and strerror in IO_ERROR_STRINGS @@ -614,7 +614,7 @@ def view_download_submission(request, username): return authenticator.build_challenge_response() data = {} formId = request.GET.get('formId', None) - if not isinstance(formId, six.string_types): + if not isinstance(formId, string_types): return HttpResponseBadRequest() id_string = formId[0:formId.find('[')] diff --git a/onadata/apps/logger/xform_instance_parser.py b/onadata/apps/logger/xform_instance_parser.py index f2e3b1a6f..c889c8685 100644 --- a/onadata/apps/logger/xform_instance_parser.py +++ b/onadata/apps/logger/xform_instance_parser.py @@ -7,8 +7,10 @@ import dateutil.parser import six -from django.utils.encoding import smart_unicode, smart_str +from django.utils.encoding import smart_str, python_2_unicode_compatible +from django.utils.encoding import from django.utils.translation import ugettext as _ +from django.utils.six import text_type from xml.dom import minidom, Node from onadata.libs.utils.common_tags import XFORM_ID_STRING @@ -18,36 +20,28 @@ class XLSFormError(Exception): pass +@python_2_unicode_compatible class DuplicateInstance(Exception): - def __unicode__(self): - return _("Duplicate Instance") - def __str__(self): - return unicode(self).encode('utf-8') + return _("Duplicate Instance") +@python_2_unicode_compatible class InstanceInvalidUserError(Exception): - def __unicode__(self): - return _("Could not determine the user.") - def __str__(self): - return unicode(self).encode('utf-8') + return _("Could not determine the user.") +@python_2_unicode_compatible class InstanceParseError(Exception): - def __unicode__(self): - return _("The instance could not be parsed.") - def __str__(self): - return unicode(self).encode('utf-8') + return _("The instance could not be parsed.") +@python_2_unicode_compatible class InstanceEmptyError(InstanceParseError): - def __unicode__(self): - return _("Empty instance") - def __str__(self): - return unicode(self).encode('utf-8') + return _("Empty instance") class InstanceMultipleNodeError(Exception): @@ -134,7 +128,7 @@ def get_deprecated_uuid_from_xml(xml): def clean_and_parse_xml(xml_string): clean_xml_str = xml_string.strip() - clean_xml_str = re.sub(r">\s+<", "><", smart_unicode(clean_xml_str)) + clean_xml_str = re.sub(r">\s+<", "><", smart_str(clean_xml_str)) xml_obj = minidom.parseString(smart_str(clean_xml_str)) return xml_obj @@ -218,14 +212,14 @@ def _flatten_dict(d, prefix): # hack: removing [1] index to be consistent across # surveys that have a single repitition of the # loop versus mutliple. - item_prefix[-1] += "[%s]" % unicode(i + 1) + item_prefix[-1] += "[%s]" % text_type(i + 1) if type(item) == dict: for pair in _flatten_dict(item, item_prefix): yield pair else: - yield (item_prefix, item) + yield item_prefix, item else: - yield (new_prefix, value) + yield new_prefix, value def _flatten_dict_nest_repeats(d, prefix): diff --git a/onadata/apps/main/models/user_profile.py b/onadata/apps/main/models/user_profile.py index 2137c5407..3023c4b48 100644 --- a/onadata/apps/main/models/user_profile.py +++ b/onadata/apps/main/models/user_profile.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import User from django.db import models from django.db.models.signals import post_save +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy from guardian.shortcuts import get_perms_for_model, assign_perm from rest_framework.authtoken.models import Token @@ -14,6 +15,7 @@ from onadata.apps.main.signals import set_api_permissions +@python_2_unicode_compatible class UserProfile(models.Model): # This field is required. user = models.OneToOneField(User, related_name='profile') @@ -38,7 +40,7 @@ class UserProfile(models.Model): num_of_submissions = models.IntegerField(default=0) metadata = JSONField(default={}, blank=True) - def __unicode__(self): + def __str__(self): return '%s[%s]' % (self.name, self.user.username) @property @@ -66,6 +68,8 @@ class Meta: def create_auth_token(sender, instance=None, created=False, **kwargs): if created: Token.objects.create(user=instance) + + post_save.connect(create_auth_token, sender=User, dispatch_uid='auth_token') post_save.connect(set_api_permissions, sender=User, @@ -79,6 +83,8 @@ def set_object_permissions(sender, instance=None, created=False, **kwargs): if instance.created_by: assign_perm(perm.codename, instance.created_by, instance) + + post_save.connect(set_object_permissions, sender=UserProfile, dispatch_uid='set_object_permissions') @@ -90,5 +96,8 @@ def default_user_profile_require_auth( instance.require_auth = \ settings.REQUIRE_AUTHENTICATION_TO_SEE_FORMS_AND_SUBMIT_DATA_DEFAULT instance.save() -post_save.connect(default_user_profile_require_auth, sender=UserProfile, - dispatch_uid='default_user_profile_require_auth') + + +post_save.connect(default_user_profile_require_auth, + sender=UserProfile, + dispatch_uid='default_user_profile_require_auth') diff --git a/onadata/apps/restservice/models.py b/onadata/apps/restservice/models.py index 7ee3d5299..5751e6183 100644 --- a/onadata/apps/restservice/models.py +++ b/onadata/apps/restservice/models.py @@ -2,12 +2,14 @@ from __future__ import unicode_literals, print_function, division, absolute_import from django.db import models +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy from onadata.apps.logger.models.xform import XForm from onadata.apps.restservice import SERVICE_CHOICES +@python_2_unicode_compatible class RestService(models.Model): class Meta: @@ -18,7 +20,7 @@ class Meta: xform = models.ForeignKey(XForm, related_name="restservices", on_delete=models.CASCADE) name = models.CharField(max_length=50, choices=SERVICE_CHOICES) - def __unicode__(self): + def __str__(self): return "%s:%s - %s" % (self.xform, self.long_name, self.service_url) def get_service_definition(self): diff --git a/onadata/apps/sms_support/autodoc.py b/onadata/apps/sms_support/autodoc.py index 3542a0fbd..1aa71a6c4 100644 --- a/onadata/apps/sms_support/autodoc.py +++ b/onadata/apps/sms_support/autodoc.py @@ -1,6 +1,5 @@ #!/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 @@ -16,6 +15,8 @@ import json import datetime +from django.utils.six import text_type + from .tools import ( DEFAULT_SEPARATOR, DEFAULT_ALLOW_MEDIA, @@ -42,7 +43,7 @@ def get_sample_data_for(question, json_survey, as_names=False): or DEFAULT_DATETIME_FORMAT def safe_wrap(value): - return unicode(value) + return text_type(value) if as_names: return xlsf_name @@ -97,7 +98,7 @@ def safe_wrap(value, xlsf_type=xlsf_type): value = ('%(type)s ' '%(text)s' % {'type': xlsf_type, 'text': value}) - return unicode(value) + return text_type(value) if xlsf_type == 'text': return safe_wrap('Any string (excluding "%s")' % separator) diff --git a/onadata/apps/sms_support/parser.py b/onadata/apps/sms_support/parser.py index b7013a7ef..6c4c978ab 100644 --- a/onadata/apps/sms_support/parser.py +++ b/onadata/apps/sms_support/parser.py @@ -1,19 +1,28 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + +import StringIO import base64 -from datetime import datetime, date import json import re -import StringIO +from datetime import datetime, date +from django.utils.six import text_type 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,\ +from onadata.apps.sms_support.tools import ( + SMS_API_ERROR, + 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 @@ -93,7 +102,7 @@ def media_value(value, medias): % {'except': repr(e)}, xlsf_name) if xlsf_type == 'text': - return safe_wrap(lambda: unicode(value)) + return safe_wrap(lambda: text_type(value)) elif xlsf_type == 'integer': return safe_wrap(lambda: int(value)) elif xlsf_type == 'decimal': @@ -141,7 +150,7 @@ def media_value(value, medias): # Example: hello.jpg;dGhpcyBpcyBteSBwaWN0dXJlIQ== return media_value(value, medias) elif xlsf_type == 'barcode': - return safe_wrap(lambda: unicode(value)) + return safe_wrap(lambda: text_type(value)) elif xlsf_type == 'date': return safe_wrap(lambda: datetime.strptime(value, xlsf_date_fmt).date()) diff --git a/onadata/apps/viewer/models/data_dictionary.py b/onadata/apps/viewer/models/data_dictionary.py index ff07f53da..48915ffdd 100644 --- a/onadata/apps/viewer/models/data_dictionary.py +++ b/onadata/apps/viewer/models/data_dictionary.py @@ -1,10 +1,12 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import os import re from django.db import models from django.db.models.signals import post_save +from django.utils.six import text_type from guardian.shortcuts import assign_perm, get_perms_for_model from pyxform import SurveyElementBuilder from pyxform.builder import create_survey_from_xls @@ -212,7 +214,7 @@ def get_mongo_field_names_dict(self): """ names = {} for elem in self.get_survey_elements(): - names[MongoHelper.encode(unicode(elem.get_abbreviated_xpath()))] = \ + names[MongoHelper.encode(text_type(elem.get_abbreviated_xpath()))] = \ elem.get_abbreviated_xpath() return names @@ -247,7 +249,7 @@ def xpaths(self, prefix='', survey_element=None, result=None, return [] if result is None: result = [] - path = '/'.join([prefix, unicode(survey_element.name)]) + path = '/'.join([prefix, text_type(survey_element.name)]) if survey_element.children is not None: # add xpaths to result for each child indices = [''] if type(survey_element) != RepeatingSection else \ diff --git a/onadata/apps/viewer/models/export.py b/onadata/apps/viewer/models/export.py index 4f1628777..0231478d0 100644 --- a/onadata/apps/viewer/models/export.py +++ b/onadata/apps/viewer/models/export.py @@ -1,11 +1,13 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import os from tempfile import NamedTemporaryFile from django.core.files.storage import get_storage_class from django.db import models from django.db.models.signals import post_delete +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext as _ from onadata.apps.logger.models import XForm @@ -18,13 +20,11 @@ def export_delete_callback(sender, **kwargs): storage.delete(export.filepath) +@python_2_unicode_compatible class Export(models.Model): class ExportTypeError(Exception): - def __unicode__(self): - return _("Invalid export type specified") - def __str__(self): - return unicode(self).encode('utf-8') + return _("Invalid export type specified") XLS_EXPORT = 'xls' CSV_EXPORT = 'csv' diff --git a/onadata/apps/viewer/xls_writer.py b/onadata/apps/viewer/xls_writer.py index 43f7cbc9a..0de291322 100644 --- a/onadata/apps/viewer/xls_writer.py +++ b/onadata/apps/viewer/xls_writer.py @@ -1,6 +1,9 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + from collections import defaultdict + +from django.utils.six import text_type from pyxform import Section, Question from xlwt import Workbook @@ -85,7 +88,7 @@ def write_tables_to_workbook(self, tables): self.add_sheet(table_name) for i, row in enumerate(table): for j, value in enumerate(row): - self._sheets[table_name].write(i, j, unicode(value)) + self._sheets[table_name].write(i, j, text_type(value)) return self._workbook def save_workbook_to_file(self): diff --git a/onadata/libs/utils/analyser_export.py b/onadata/libs/utils/analyser_export.py index 654e7411a..43fd920c9 100644 --- a/onadata/libs/utils/analyser_export.py +++ b/onadata/libs/utils/analyser_export.py @@ -20,10 +20,11 @@ import lxml.etree as etree import xlrd import openpyxl +from django.utils.six import text_type from openpyxl.writer.excel import save_virtual_workbook -NAMESPACES= {'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'} +NAMESPACES = {'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'} def get_worksheet_indices(workbook_file): @@ -86,7 +87,7 @@ def copy_cells(source_worksheet_file, destination_worksheet_file_path, new_strin 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_v.text = text_type(new_string_indices[int(destination_v.text)]) destination_row.append(destination_c) # Clean up elements of the source worksheet to save memory. @@ -122,7 +123,7 @@ def splice_shared_strings(source_file, destination_etree, new_string_indices): uniqueCount+= 1 # Update the "uniqueCount" attribute. - destination_root.attrib['uniqueCount']= unicode(uniqueCount) + destination_root.attrib['uniqueCount']= text_type(uniqueCount) def insert_xlsform_worksheets(analyser_shared_strings, analyser_survey_worksheet_file_path, analyser_choices_worksheet_file_path, survey_file_xls): diff --git a/onadata/libs/utils/logger_tools.py b/onadata/libs/utils/logger_tools.py index d49c70f0d..3c360f3d1 100644 --- a/onadata/libs/utils/logger_tools.py +++ b/onadata/libs/utils/logger_tools.py @@ -1,50 +1,50 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from datetime import date, datetime import os -import pytz import re +import sys import tempfile import traceback -from xml.dom import Node +from datetime import date, datetime from xml.parsers.expat import ExpatError +import pytz from dict2xml import dict2xml from django.conf import settings +from django.contrib.auth.models import User from django.core.exceptions import ValidationError, PermissionDenied from django.core.files.storage import get_storage_class from django.core.mail import mail_admins from django.core.servers.basehttp import FileWrapper -from django.contrib.auth.models import User from django.db import IntegrityError, transaction from django.db.models import Q from django.db.models.signals import pre_delete from django.http import HttpResponse, HttpResponseNotFound, \ StreamingHttpResponse, Http404 from django.shortcuts import get_object_or_404 +from django.utils import timezone from django.utils.encoding import DjangoUnicodeDecodeError +from django.utils.six import text_type from django.utils.translation import ugettext as _ -from django.utils import timezone from modilabs.utils.subprocess_timeout import ProcessTimedOut from pyxform.errors import PyXFormError from pyxform.xform2json import create_survey_element_from_xml -import sys +from xml.dom import Node -from onadata.apps.main.models import UserProfile from onadata.apps.logger.exceptions import FormInactiveError, DuplicateUUIDError from onadata.apps.logger.models import Attachment +from onadata.apps.logger.models import Instance +from onadata.apps.logger.models import XForm from onadata.apps.logger.models.attachment import ( generate_attachment_filename, hash_attachment_contents, ) -from onadata.apps.logger.models import Instance from onadata.apps.logger.models.instance import ( InstanceHistory, get_id_string_from_xml_str, update_xform_submission_count, ) -from onadata.apps.logger.models import XForm from onadata.apps.logger.models.xform import XLSFormError from onadata.apps.logger.xform_instance_parser import ( InstanceEmptyError, @@ -55,13 +55,13 @@ get_uuid_from_xml, get_deprecated_uuid_from_xml, get_submission_date_from_xml) +from onadata.apps.main.models import UserProfile from onadata.apps.viewer.models.data_dictionary import DataDictionary -from onadata.apps.viewer.models.parsed_instance import _remove_from_mongo,\ +from onadata.apps.viewer.models.parsed_instance import _remove_from_mongo, \ xform_instances, ParsedInstance from onadata.libs.utils import common_tags from onadata.libs.utils.model_tools import queryset_iterator, set_uuid - OPEN_ROSA_VERSION_HEADER = 'X-OpenRosa-Version' HTTP_OPEN_ROSA_VERSION_HEADER = 'HTTP_X_OPENROSA_VERSION' OPEN_ROSA_VERSION = '1.0' @@ -458,7 +458,7 @@ def publish_form(callback): except (PyXFormError, XLSFormError) as e: return { 'type': 'alert-error', - 'text': unicode(e) + 'text': text_type(e) } except IntegrityError as e: return { @@ -475,7 +475,7 @@ def publish_form(callback): # form.publish returned None, not sure why... return { 'type': 'alert-error', - 'text': unicode(e) + 'text': text_type(e) } except ProcessTimedOut as e: # catch timeout errors @@ -496,7 +496,7 @@ def publish_form(callback): # error in the XLS file; show an error to the user return { 'type': 'alert-error', - 'text': unicode(e) + 'text': text_type(e) } From 12d90da0ac624c90ee6fc9d0fb79732464b6471c Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Fri, 17 Apr 2020 16:57:18 -0300 Subject: [PATCH 004/240] WIP - 2to3 fixes --- onadata/apps/logger/fields.py | 4 +++- onadata/apps/logger/import_tools.py | 4 ++-- .../commands/create_image_thumbnails.py | 2 +- onadata/libs/utils/viewer_tools.py | 2 +- pytest.ini | 8 +++---- requirements/dev.pip | 16 +++++++------- requirements/py3.pip | 21 ++++++++++++------- 7 files changed, 33 insertions(+), 24 deletions(-) diff --git a/onadata/apps/logger/fields.py b/onadata/apps/logger/fields.py index dfc8e1abf..57d856cd2 100644 --- a/onadata/apps/logger/fields.py +++ b/onadata/apps/logger/fields.py @@ -1,6 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import +import collections + from django.core.exceptions import FieldError from django.db import models @@ -37,7 +39,7 @@ def __init__(self, *args, **kwargs): super(LazyDefaultBooleanField, self).__init__(*args, **kwargs) def _get_lazy_default(self): - if callable(self.lazy_default): + if isinstance(self.lazy_default, collections.Callable): return self.lazy_default() else: return self.lazy_default diff --git a/onadata/apps/logger/import_tools.py b/onadata/apps/logger/import_tools.py index d3daf96e9..60d93ea4d 100644 --- a/onadata/apps/logger/import_tools.py +++ b/onadata/apps/logger/import_tools.py @@ -56,7 +56,7 @@ def iterate_through_instances(dirpath, callback): xfxs = XFormInstanceFS(filepath) try: success_count += callback(xfxs) - except Exception, e: + except Exception as e: errors.append("%s => %s" % (xfxs.filename, str(e))) del(xfxs) total_file_count += 1 @@ -70,7 +70,7 @@ def import_instances_from_zip(zipfile_path, user, status="zip"): zf = zipfile.ZipFile(zipfile_path) zf.extractall(temp_directory) - except zipfile.BadZipfile, e: + except zipfile.BadZipfile as e: errors = ["%s" % e] return 0, 0, errors else: diff --git a/onadata/apps/logger/management/commands/create_image_thumbnails.py b/onadata/apps/logger/management/commands/create_image_thumbnails.py index 1d6155fcd..680173dca 100644 --- a/onadata/apps/logger/management/commands/create_image_thumbnails.py +++ b/onadata/apps/logger/management/commands/create_image_thumbnails.py @@ -77,6 +77,6 @@ def handle(self, *args, **kwargs): else: print(_('Problem with the file %(file)s') % {'file': filename}) - except (IOError, OSError), e: + except (IOError, OSError) as e: print(_('Error on %(filename)s: %(error)s') % {'filename': filename, 'error': e}) diff --git a/onadata/libs/utils/viewer_tools.py b/onadata/libs/utils/viewer_tools.py index 5b2365445..aa1b8a05d 100644 --- a/onadata/libs/utils/viewer_tools.py +++ b/onadata/libs/utils/viewer_tools.py @@ -233,7 +233,7 @@ def create_attachments_zipfile(attachments, output_file=None): try: with storage.open(attachment.media_file.name, 'rb') as source_file: zip_file.writestr(attachment.media_file.name, source_file.read()) - except Exception, e: + except Exception as e: report_exception("Error adding file \"{}\" to archive.".format(attachment.media_file.name), e) return output_file diff --git a/pytest.ini b/pytest.ini index b123210d8..d9e23aa78 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,9 +3,9 @@ testpaths = onadata env = DJANGO_SETTINGS_MODULE=onadata.settings.test_environ filterwarnings = - ignore::django.utils.deprecation.RemovedInDjango110Warning - ignore::django.utils.deprecation.RemovedInDjango19Warning - ignore::exceptions.DeprecationWarning - ignore::exceptions.PendingDeprecationWarning +# ignore::django.utils.deprecation.RemovedInDjango110Warning +# ignore::django.utils.deprecation.RemovedInDjango19Warning +# ignore::exceptions.DeprecationWarning +# ignore::exceptions.PendingDeprecationWarning ignore::exceptions.RuntimeWarning diff --git a/requirements/dev.pip b/requirements/dev.pip index 6c677c786..2d93ea10b 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -1,14 +1,14 @@ -clint -django-kombu -django-snippetscream -django-statsd-mozilla -fabric -gunicorn +#clint +#django-kombu +#django-snippetscream +#django-statsd-mozilla +# fabric +# gunicorn ipdb ipython shell_command -statsd -twill +#statsd +#twill Werkzeug sqlparse pytest==3.10 diff --git a/requirements/py3.pip b/requirements/py3.pip index c79012012..a788f71b8 100644 --- a/requirements/py3.pip +++ b/requirements/py3.pip @@ -1,7 +1,7 @@ pytz -Django>=2.2,<3 +Django>=1.11,<2 dj-database-url -django-guardian +# django-guardian django-registration-redux django-templated-email gdata @@ -20,8 +20,8 @@ openpyxl celery>=4.0,<5.0 celery[redis] -django-celery-beat -# amqp +# django-celery-beat +amqp # vine==1.2.0 django-nose raven @@ -42,7 +42,7 @@ django-pure-pagination path.py # sms support -# dict2xml==1.3 +dict2xml # api support djangorestframework @@ -63,8 +63,8 @@ dpath # tagging django-taggit -# oath2 support -django-oauth-toolkit +# oauth2 support +# django-oauth-toolkit # spss https://bitbucket.org/fomcl/savreaderwriter/downloads/savReaderWriter-3.3.0.zip#egg=savreaderwriter @@ -84,3 +84,10 @@ redis # ssrf -e git+https://github.com/kobotoolbox/ssrf-protect@755efe16694273ce66060a51e04f973dc034ca4e#egg=ssrf_protect + + +# Compatibility Django 1.11 +django-guardian==1.5.1 +django-timezone-field==3.0 +django-celery-beat==1.6.0 +django-oauth-toolkit==1.1.1 From e2e235b459801f5bf32c1015aa7da2c5cf39af13 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Tue, 21 Apr 2020 12:59:50 -0300 Subject: [PATCH 005/240] Replaced "iteritems" with "items" --- onadata/apps/logger/tasks.py | 2 +- onadata/apps/main/models/audit.py | 2 +- onadata/apps/main/tests/test_process.py | 2 +- onadata/apps/main/tests/test_user_settings.py | 2 +- onadata/apps/viewer/pandas_mongo_bridge.py | 14 ++++++------ onadata/libs/renderers/renderers.py | 2 +- onadata/libs/utils/export_tools.py | 22 +++++++++---------- onadata/libs/utils/viewer_tools.py | 2 +- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/onadata/apps/logger/tasks.py b/onadata/apps/logger/tasks.py index 5248caddc..d862e382a 100644 --- a/onadata/apps/logger/tasks.py +++ b/onadata/apps/logger/tasks.py @@ -82,7 +82,7 @@ def list_created_by_month(model, date_field): default_storage = get_storage_class()() with default_storage.open(output_filename, 'wb') as output_file: zip_file = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) - for filename, report_settings in REPORTS.iteritems(): + for filename, report_settings in REPORTS.items(): model_name_plural = report_settings[ 'model']._meta.verbose_name_plural fieldnames = [ diff --git a/onadata/apps/main/models/audit.py b/onadata/apps/main/models/audit.py index 7d1d0c575..882fb85d3 100644 --- a/onadata/apps/main/models/audit.py +++ b/onadata/apps/main/models/audit.py @@ -33,7 +33,7 @@ def query_mongo(cls, username, query=None, fields=None, sort=None, start=0, # check for the created_on key in query and turn its values into dates if type(query) == dict and cls.CREATED_ON in query: if type(query[cls.CREATED_ON]) is dict: - for op, val in query[cls.CREATED_ON].iteritems(): + for op, val in query[cls.CREATED_ON].items(): try: query[cls.CREATED_ON][op] = datetime.strptime( val, DATETIME_FORMAT) diff --git a/onadata/apps/main/tests/test_process.py b/onadata/apps/main/tests/test_process.py index c82abe1b8..29530b9d3 100644 --- a/onadata/apps/main/tests/test_process.py +++ b/onadata/apps/main/tests/test_process.py @@ -66,7 +66,7 @@ def _update_dynamic_data(self): """ Update stuff like submission time so we can compare within out fixtures """ - for uuid, submission_time in self.uuid_to_submission_times.iteritems(): + for uuid, submission_time in self.uuid_to_submission_times.items(): xform_instances.update( {UUID: uuid}, {'$set': {SUBMISSION_TIME: submission_time}}) diff --git a/onadata/apps/main/tests/test_user_settings.py b/onadata/apps/main/tests/test_user_settings.py index 3994d7d75..c7d0e6616 100644 --- a/onadata/apps/main/tests/test_user_settings.py +++ b/onadata/apps/main/tests/test_user_settings.py @@ -42,7 +42,7 @@ def test_update_user_settings(self): response = self.client.post(self.settings_url, post_data) self.assertEqual(response.status_code, 302) self.user = UserProfile.objects.get(pk=self.user.profile.pk).user - for key, value in post_data.iteritems(): + for key, value in post_data.items(): try: self.assertEqual(self.user.profile.__dict__[key], value) except KeyError as e: diff --git a/onadata/apps/viewer/pandas_mongo_bridge.py b/onadata/apps/viewer/pandas_mongo_bridge.py index fd15343c6..330aeec64 100644 --- a/onadata/apps/viewer/pandas_mongo_bridge.py +++ b/onadata/apps/viewer/pandas_mongo_bridge.py @@ -188,7 +188,7 @@ def _tag_edit_string(cls, record): @classmethod def _split_gps_fields(cls, record, gps_fields): updated_gps_fields = {} - for key, value in record.iteritems(): + for key, value in record.items(): if key in gps_fields and isinstance(value, string_types): gps_xpaths = DataDictionary.get_additional_geopoint_xpaths(key) gps_parts = dict([(xpath, None) for xpath in gps_xpaths]) @@ -292,7 +292,7 @@ def export_to(self, file_path, batchsize=1000): data = self._format_for_dataframe(cursor) # write all cursor's data to their respective sheets - for section_name, section in self.sections.iteritems(): + for section_name, section in self.sections.items(): records = data[section_name] # TODO: currently ignoring nested repeats # so ignore sections that have 0 records @@ -335,7 +335,7 @@ def _format_for_dataframe(self, cursor): self.survey_name) parent_index = main_section[self.CURRENT_INDEX_META] - for sheet_name, section in self.sections.iteritems(): + for sheet_name, section in self.sections.items(): # skip default section i.e survey name if sheet_name != self.survey_name: xpath = section["xpath"] @@ -521,7 +521,7 @@ def _reindex(cls, key, value, ordered_columns, parent_prefix=None): # for each list check for dict, we want to transform the key of # this dict if type(item) is dict: - for nested_key, nested_val in item.iteritems(): + for nested_key, nested_val in item.items(): # given the key "children/details" and nested_key/ # abbreviated xpath # "children/details/immunization/polio_1", @@ -617,14 +617,14 @@ def _format_for_dataframe(self, cursor): self._tag_edit_string(record) flat_dict = {} # re index repeats - for key, value in record.iteritems(): + for key, value in record.items(): reindexed = self._reindex(key, value, self.ordered_columns) flat_dict.update(reindexed) # if delimetr is diferent, replace within record as well if self.group_delimiter != DEFAULT_GROUP_DELIMITER: flat_dict = dict((self.group_delimiter.join(k.split('/')), v) - for k, v in flat_dict.iteritems()) + for k, v in flat_dict.items()) data.append(flat_dict) return data @@ -652,7 +652,7 @@ def export_to(self, file_or_path, data_frame_max_size=30000): columns = list(chain.from_iterable( [[xpath] if cols is None else cols - for xpath, cols in self.ordered_columns.iteritems()])) + for xpath, cols in self.ordered_columns.items()])) # use a different group delimiter if needed if self.group_delimiter != DEFAULT_GROUP_DELIMITER: diff --git a/onadata/libs/renderers/renderers.py b/onadata/libs/renderers/renderers.py index 63102ecbd..c0593c016 100644 --- a/onadata/libs/renderers/renderers.py +++ b/onadata/libs/renderers/renderers.py @@ -121,7 +121,7 @@ def _to_xml(self, xml, data): xml.endElement(self.element_node) elif isinstance(data, dict): - for key, value in six.iteritems(data): + for key, value in six.items(data): xml.startElement(key, {}) self._to_xml(xml, value) xml.endElement(key) diff --git a/onadata/libs/utils/export_tools.py b/onadata/libs/utils/export_tools.py index 46fc74559..ce82a214e 100644 --- a/onadata/libs/utils/export_tools.py +++ b/onadata/libs/utils/export_tools.py @@ -148,7 +148,7 @@ def dict_to_joined_export(data, index, indices, name): output = {} # TODO: test for _geolocation and attachment lists if isinstance(data, dict): - for key, val in data.iteritems(): + for key, val in data.items(): if isinstance(val, list) and key not in [NOTES, TAGS]: output[key] = [] for child in val: @@ -162,7 +162,7 @@ def dict_to_joined_export(data, index, indices, name): PARENT_TABLE_NAME: name} # iterate over keys within new_output and append to # main output - for out_key, out_val in new_output.iteritems(): + for out_key, out_val in new_output.items(): if isinstance(out_val, list): if out_key not in output: output[out_key] = [] @@ -336,7 +336,7 @@ def section_by_name(self, name): @classmethod def split_select_multiples(cls, row, select_multiples): # for each select_multiple, get the associated data and split it - for xpath, choices in select_multiples.iteritems(): + for xpath, choices in select_multiples.items(): # get the data matching this xpath data = row.get(xpath) selections = [] @@ -359,7 +359,7 @@ def split_select_multiples(cls, row, select_multiples): @classmethod def split_gps_components(cls, row, gps_fields): # for each gps_field, get associated data and split it - for xpath, gps_components in gps_fields.iteritems(): + for xpath, gps_components in gps_fields.items(): data = row.get(xpath) if data: gps_parts = data.split() @@ -369,7 +369,7 @@ def split_gps_components(cls, row, gps_fields): @classmethod def decode_mongo_encoded_fields(cls, row, encoded_fields): - for xpath, encoded_xpath in encoded_fields.iteritems(): + for xpath, encoded_xpath in encoded_fields.items(): if row.get(encoded_xpath): val = row.pop(encoded_xpath) row.update({xpath: val}) @@ -377,7 +377,7 @@ def decode_mongo_encoded_fields(cls, row, encoded_fields): @classmethod def decode_mongo_encoded_section_names(cls, data): - return dict([(MongoHelper.decode(k), v) for k, v in data.iteritems()]) + return dict([(MongoHelper.decode(k), v) for k, v in data.items()]) @classmethod def convert_type(cls, value, data_type): @@ -484,14 +484,14 @@ def write_row(row, csv_writer, fields): # write zipfile with ZipFile(path, 'w') as zip_file: - for section_name, csv_def in csv_defs.iteritems(): + for section_name, csv_def in csv_defs.items(): 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(): + for section_name, csv_def in csv_defs.items(): csv_def['csv_file'].close() @classmethod @@ -690,20 +690,20 @@ def write_row(row, csv_writer, fields): sav_writer, fields) index += 1 - for section_name, sav_def in sav_defs.iteritems(): + for section_name, sav_def in sav_defs.items(): 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(): + for section_name, sav_def in sav_defs.items(): 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(): + for section_name, sav_def in sav_defs.items(): sav_def['sav_file'].close() diff --git a/onadata/libs/utils/viewer_tools.py b/onadata/libs/utils/viewer_tools.py index aa1b8a05d..e0a6ae62c 100644 --- a/onadata/libs/utils/viewer_tools.py +++ b/onadata/libs/utils/viewer_tools.py @@ -193,7 +193,7 @@ def enketo_url(form_url, id_string, instance_xml=None, 'instance_id': instance_id, 'return_url': return_url }) - for key, value in instance_attachments.iteritems(): + for key, value in instance_attachments.items(): values.update({ 'instance_attachments[' + key + ']': value }) From 865af367ee6f473f80e1dbb43230e993a9c73e0a Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Tue, 21 Apr 2020 13:25:38 -0300 Subject: [PATCH 006/240] Use "bytes" with hashlib --- onadata/apps/logger/models/attachment.py | 2 +- onadata/apps/logger/models/instance.py | 2 +- onadata/apps/logger/models/xform.py | 2 +- onadata/apps/logger/xform_instance_parser.py | 2 +- onadata/apps/main/models/meta_data.py | 4 ++-- onadata/apps/main/tests/test_form_metadata.py | 3 ++- onadata/apps/main/tests/test_process.py | 2 +- onadata/libs/utils/gravatar.py | 7 ++++--- 8 files changed, 13 insertions(+), 11 deletions(-) diff --git a/onadata/apps/logger/models/attachment.py b/onadata/apps/logger/models/attachment.py index b1f6cefdc..f6548f911 100644 --- a/onadata/apps/logger/models/attachment.py +++ b/onadata/apps/logger/models/attachment.py @@ -27,7 +27,7 @@ def upload_to(attachment, filename): def hash_attachment_contents(contents): - return '%s' % md5(contents).hexdigest() + return md5(contents.encode()).hexdigest() class Attachment(models.Model): diff --git a/onadata/apps/logger/models/instance.py b/onadata/apps/logger/models/instance.py index aa4d75364..174bb3ca7 100644 --- a/onadata/apps/logger/models/instance.py +++ b/onadata/apps/logger/models/instance.py @@ -365,7 +365,7 @@ def get_hash(input_string): :rtype: str """ input_string = smart_text(input_string) - return sha256(input_string).hexdigest() + return sha256(input_string.encode()).hexdigest() @property def point(self): diff --git a/onadata/apps/logger/models/xform.py b/onadata/apps/logger/models/xform.py index fe63d6900..5543c5868 100644 --- a/onadata/apps/logger/models/xform.py +++ b/onadata/apps/logger/models/xform.py @@ -263,7 +263,7 @@ def time_of_last_submission_update(self): @property def hash(self): - return '%s' % md5(self.xml.encode('utf8')).hexdigest() + return md5(self.xml.encode()).hexdigest() @property def can_be_replaced(self): diff --git a/onadata/apps/logger/xform_instance_parser.py b/onadata/apps/logger/xform_instance_parser.py index c889c8685..81744154c 100644 --- a/onadata/apps/logger/xform_instance_parser.py +++ b/onadata/apps/logger/xform_instance_parser.py @@ -156,7 +156,7 @@ def _xml_node_to_dict(node, repeats=[]): continue child_name = child.nodeName child_xpath = xpath_from_xml_node(child) - assert d.keys() == [child_name] + assert list(d) == [child_name] node_type = dict # check if name is in list of repeats and make it a list if so if child_xpath in repeats: diff --git a/onadata/apps/main/models/meta_data.py b/onadata/apps/main/models/meta_data.py index 0100ceefe..3b8c5f4ce 100644 --- a/onadata/apps/main/models/meta_data.py +++ b/onadata/apps/main/models/meta_data.py @@ -151,8 +151,8 @@ def _set_hash(self): except IOError: return '' else: - self.file_hash = 'md5:%s' \ - % md5(self.data_file.read()).hexdigest() + md5_hash = md5(self.data_file.read().encode()).hexdigest() + self.file_hash = f'md5:{md5_hash}' return self.file_hash diff --git a/onadata/apps/main/tests/test_form_metadata.py b/onadata/apps/main/tests/test_form_metadata.py index 6ebaa9f2b..a6a293078 100644 --- a/onadata/apps/main/tests/test_form_metadata.py +++ b/onadata/apps/main/tests/test_form_metadata.py @@ -247,7 +247,8 @@ def test_media_file_hash(self): data_file=File(open(media_file), name), data_file_type='image/png') f = open(media_file) - media_hash = 'md5:%s' % hashlib.md5(f.read()).hexdigest() + md5_hash = hashlib.md5(f.read().encode()).hexdigest() + media_hash = f'md5:{md5_hash}' f.close() meta_hash = m.hash self.assertEqual(meta_hash, media_hash) diff --git a/onadata/apps/main/tests/test_process.py b/onadata/apps/main/tests/test_process.py index 29530b9d3..93222ff02 100644 --- a/onadata/apps/main/tests/test_process.py +++ b/onadata/apps/main/tests/test_process.py @@ -190,7 +190,7 @@ def _check_formList(self): self.manifest_url = \ 'http://testserver/%s/xformsManifest/%s'\ % (self.user.username, self.xform.pk) - md5_hash = md5(self.xform.xml).hexdigest() + md5_hash = md5(self.xform.xml.encode()).hexdigest() expected_content = """ transportation_2011_07_25transportation_2011_07_25md5:%(hash)stransportation_2011_07_25%(download_url)s%(manifest_url)s""" # noqa expected_content = expected_content % { diff --git a/onadata/libs/utils/gravatar.py b/onadata/libs/utils/gravatar.py index 27ca75839..a74a5cbc8 100644 --- a/onadata/libs/utils/gravatar.py +++ b/onadata/libs/utils/gravatar.py @@ -1,5 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import hashlib import urllib @@ -10,14 +11,14 @@ def get_gravatar_img_link(user): url = GRAVATAR_ENDPOINT +\ - hashlib.md5(user.email.lower()).hexdigest() + "?" + urllib.urlencode({ - 'd': DEFAULT_GRAVATAR, 's': str(GRAVATAR_SIZE) + hashlib.md5(user.email.lower().encode()).hexdigest() + "?" + urllib.urlencode({ + 'd': DEFAULT_GRAVATAR, 's': GRAVATAR_SIZE }) return url def gravatar_exists(user): url = GRAVATAR_ENDPOINT +\ - hashlib.md5(user.email.lower()).hexdigest() + "?" + "d=404" + hashlib.md5(user.email.lower().encode()).hexdigest() + "?" + "d=404" exists = urllib.urlopen(url).getcode() != 404 return exists From 26d87ddb80d9a35b24bc385d35651393cc1bd530 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Tue, 21 Apr 2020 14:58:34 -0400 Subject: [PATCH 007/240] Updated code for "filter", "itervalues()", "dict_keys()" and "dict_values()" when needed --- onadata/apps/logger/tests/models/test_attachment.py | 4 ++-- onadata/apps/logger/tests/test_instance_creation.py | 4 ++-- onadata/apps/main/models/audit.py | 2 +- onadata/apps/main/tests/test_process.py | 6 ++++-- onadata/apps/viewer/models/data_dictionary.py | 2 +- onadata/apps/viewer/models/parsed_instance.py | 2 +- onadata/apps/viewer/pandas_mongo_bridge.py | 7 +++---- onadata/apps/viewer/tests/test_export_builder.py | 3 +++ onadata/apps/viewer/tests/test_pandas_mongo_bridge.py | 2 +- onadata/apps/viewer/tests/test_remongo.py | 2 +- onadata/libs/utils/export_tools.py | 6 +++--- onadata/libs/utils/log.py | 4 +++- onadata/libs/utils/viewer_tools.py | 1 + onadata/settings/kc_environ.py | 5 +++-- 14 files changed, 29 insertions(+), 21 deletions(-) diff --git a/onadata/apps/logger/tests/models/test_attachment.py b/onadata/apps/logger/tests/models/test_attachment.py index 814fa297d..6837e8b74 100644 --- a/onadata/apps/logger/tests/models/test_attachment.py +++ b/onadata/apps/logger/tests/models/test_attachment.py @@ -38,7 +38,7 @@ def test_thumbnails(self): self.assertNotEqual( url.find(thumbnail), -1) for size in ['small', 'medium', 'large']: - thumbnail = '%s-%s.jpg' % (filename, size) + thumbnail = f'{filename}-{size}.jpg' self.assertTrue( default_storage.exists(thumbnail)) default_storage.delete(thumbnail) @@ -58,7 +58,7 @@ def test_create_thumbnails_command(self): for attachment in Attachment.objects.filter(instance=self.instance): filename = attachment.media_file.name.replace('.jpg', '') for size in settings.THUMB_CONF.keys(): - thumbnail = '%s-%s.jpg' % (filename, size) + thumbnail = f'{filename}-{size}.jpg' self.assertTrue( default_storage.exists(thumbnail)) self.assertTrue( diff --git a/onadata/apps/logger/tests/test_instance_creation.py b/onadata/apps/logger/tests/test_instance_creation.py index a49f0a007..c6dc573f5 100644 --- a/onadata/apps/logger/tests/test_instance_creation.py +++ b/onadata/apps/logger/tests/test_instance_creation.py @@ -91,7 +91,7 @@ def test_data_submission(self): path = get_absolute_path(subdirectory) postdata = create_post_data(path) response = self.client.post('/bob/submission', postdata) - self.failUnlessEqual(response.status_code, 201) + self.assertEqual(response.status_code, 201) def test_submission_for_missing_form(self): xml_file = open(os.path.join( @@ -100,4 +100,4 @@ def test_submission_for_missing_form(self): )) postdata = {"xml_submission_file": xml_file} response = self.client.post('/bob/submission', postdata) - self.failUnlessEqual(response.status_code, 404) + self.assertEqual(response.status_code, 404) diff --git a/onadata/apps/main/models/audit.py b/onadata/apps/main/models/audit.py index 882fb85d3..27cf29e60 100644 --- a/onadata/apps/main/models/audit.py +++ b/onadata/apps/main/models/audit.py @@ -66,7 +66,7 @@ def query_mongo(cls, username, query=None, fields=None, sort=None, start=0, cursor.skip(max(start, 0)).limit(limit) if type(sort) == dict and len(sort) == 1: sort = MongoHelper.to_safe_dict(sort, reading=True) - sort_key = sort.keys()[0] + sort_key = list(sort)[0] sort_dir = int(sort[sort_key]) # -1 for desc, 1 for asc cursor.sort(sort_key, sort_dir) # set batch size for cursor iteration diff --git a/onadata/apps/main/tests/test_process.py b/onadata/apps/main/tests/test_process.py index 93222ff02..ff1c36cce 100644 --- a/onadata/apps/main/tests/test_process.py +++ b/onadata/apps/main/tests/test_process.py @@ -276,7 +276,8 @@ def _check_data_for_csv_export(self): } ] for d_from_db in self.data_dictionary.get_data_for_excel(): - for k, v in d_from_db.items(): + d_from_db_iter = dict(d_from_db) + for k, v in d_from_db_iter.items(): if (k != '_xform_id_string' and k != 'meta/instanceID') and v: new_key = k[len('transport/'):] d_from_db[new_key] = d_from_db[k] @@ -396,7 +397,8 @@ def _check_csv_export_second_pass(self): dd = DataDictionary.objects.get(pk=self.xform.pk) for row, expected_dict in zip(actual_csv, data): d = dict(zip(headers, row)) - for k, v in d.items(): + d_iter = dict(d) + for k, v in d_iter.items(): if v in ["n/a", "False"] or k in dd._additional_headers(): del d[k] l = [] diff --git a/onadata/apps/viewer/models/data_dictionary.py b/onadata/apps/viewer/models/data_dictionary.py index 48915ffdd..90687a812 100644 --- a/onadata/apps/viewer/models/data_dictionary.py +++ b/onadata/apps/viewer/models/data_dictionary.py @@ -201,7 +201,7 @@ def get_choice_label(self, field, choice_value, lang='English'): label = choice.label if isinstance(label, dict): - label = label.get(lang, choice.label.values()[0]) + label = label.get(lang, list(choice.label.values())[0]) return label diff --git a/onadata/apps/viewer/models/parsed_instance.py b/onadata/apps/viewer/models/parsed_instance.py index 879a50ac2..b2985f6c2 100644 --- a/onadata/apps/viewer/models/parsed_instance.py +++ b/onadata/apps/viewer/models/parsed_instance.py @@ -236,7 +236,7 @@ def _get_paginated_and_sorted_cursor(cls, cursor, start, limit, sort): if type(sort) == dict and len(sort) == 1: sort = MongoHelper.to_safe_dict(sort, reading=True) - sort_key = sort.keys()[0] + sort_key = list(sort)[0] sort_dir = int(sort[sort_key]) # -1 for desc, 1 for asc cursor.sort(sort_key, sort_dir) diff --git a/onadata/apps/viewer/pandas_mongo_bridge.py b/onadata/apps/viewer/pandas_mongo_bridge.py index 330aeec64..0cd0b17f1 100644 --- a/onadata/apps/viewer/pandas_mongo_bridge.py +++ b/onadata/apps/viewer/pandas_mongo_bridge.py @@ -319,8 +319,7 @@ def _format_for_dataframe(self, cursor): returns a dictionary with the key being the name of the sheet, and values a list of dicts to feed into a DataFrame """ - data = dict((section_name, []) - for section_name in self.sections.keys()) + data = dict((section_name, []) for section_name in self.sections.keys()) main_section = self.sections[self.survey_name] main_sections_columns = main_section["columns"] @@ -429,7 +428,7 @@ def _build_sections_recursive(self, section_name, element, # if its repeating, build a new section if new_is_repeating: new_section_name = get_valid_sheet_name( - child.name, self.sections.keys()) + child.name, list(self.sections)) self._create_section( new_section_name, child.get_abbreviated_xpath(), True) @@ -467,7 +466,7 @@ def get_exceeds_xls_limits(self): if len(self.sections) > self.XLS_SHEET_COUNT_LIMIT: self.exceeds_xls_limits = True else: - for section in self.sections.itervalues(): + for section in self.sections.values(): if len(section["columns"]) > self.XLS_COLUMN_COUNT_MAX: self.exceeds_xls_limits = True break diff --git a/onadata/apps/viewer/tests/test_export_builder.py b/onadata/apps/viewer/tests/test_export_builder.py index f9c24b0bf..55124f1a6 100644 --- a/onadata/apps/viewer/tests/test_export_builder.py +++ b/onadata/apps/viewer/tests/test_export_builder.py @@ -389,6 +389,9 @@ def test_zipped_csv_export_works_with_unicode(self): '_parent_table_name', '_parent_index', '_tags', '_notes'] rows = [row for row in reader] + + # TODO VALIDATE After UPGRADE TO PYTHON3 + 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])) diff --git a/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py b/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py index f29fb94fa..60a651565 100644 --- a/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py +++ b/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py @@ -426,7 +426,7 @@ def test_split_gps_fields_within_repeats(self): self.assertEqual(expected_result, record) def test_unicode_export(self): - unicode_char = unichr(40960) + unicode_char = chr(40960) # fake data data = [{"key": unicode_char}] columns = ["key"] diff --git a/onadata/apps/viewer/tests/test_remongo.py b/onadata/apps/viewer/tests/test_remongo.py index 8009c7d6f..bc286282f 100644 --- a/onadata/apps/viewer/tests/test_remongo.py +++ b/onadata/apps/viewer/tests/test_remongo.py @@ -83,7 +83,7 @@ def test_indexes_exist(self): # '_id_': {'key': [('_id', 1)], 'v': 1}, # '_userform_id_1': {'key': [('_userform_id', 1)], 'v': 1}} # lets make a list of the indexes - existing_indexes = [v['key'][0][0] for v in index_info.itervalues() + existing_indexes = [v['key'][0][0] for v in index_info.values() if v['key'][0][1] == 1] all_indexes_found = True for index_item in index_list: diff --git a/onadata/libs/utils/export_tools.py b/onadata/libs/utils/export_tools.py index ce82a214e..6d32a0d32 100644 --- a/onadata/libs/utils/export_tools.py +++ b/onadata/libs/utils/export_tools.py @@ -129,7 +129,7 @@ def _build_obs_from_dict(self, d, obs, table_name, def get_observation_from_dict(self, d): result = {} assert len(d.keys()) == 1 - root_name = d.keys()[0] + root_name = list(d)[0] kwargs = { "d": d[root_name], "obs": result, @@ -329,7 +329,7 @@ def _append_xpaths_to_section(current_section_name, field_list, xpath, self.GROUP_DELIMITER) def section_by_name(self, name): - matches = filter(lambda s: s['name'] == name, self.sections) + matches = [s for s in self.sections if s['name'] == name] assert(len(matches) == 1) return matches[0] @@ -530,7 +530,7 @@ def write_row(data, work_sheet, fields, work_sheet_titles): for section in self.sections: section_name = section['name'] work_sheet_title = ExportBuilder.get_valid_sheet_name( - "_".join(section_name.split("/")), work_sheet_titles.values()) + "_".join(section_name.split("/")), list(work_sheet_titles.values())) work_sheet_titles[section_name] = work_sheet_title work_sheets[section_name] = wb.create_sheet( title=work_sheet_title) diff --git a/onadata/libs/utils/log.py b/onadata/libs/utils/log.py index 68051c96f..ac60a4d4a 100644 --- a/onadata/libs/utils/log.py +++ b/onadata/libs/utils/log.py @@ -1,5 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import logging from datetime import datetime from onadata.libs.utils.viewer_tools import get_client_ip @@ -18,7 +19,8 @@ def __getitem__(self, item): return self.__getattr__(item) def __iter__(self): - return self.enums.itervalues() + return iter(self.enums.values()) + Actions = Enum( PROFILE_ACCESSED="profile-accessed", diff --git a/onadata/libs/utils/viewer_tools.py b/onadata/libs/utils/viewer_tools.py index e0a6ae62c..1b2929c0d 100644 --- a/onadata/libs/utils/viewer_tools.py +++ b/onadata/libs/utils/viewer_tools.py @@ -39,6 +39,7 @@ def get_path(path, suffix): return fileName + suffix + fileExtension +# TODO VERIFY IF STILL USED def image_urls(instance): image_urls_dict_ = image_urls_dict(instance) return image_urls_dict_.values() diff --git a/onadata/settings/kc_environ.py b/onadata/settings/kc_environ.py index c032af341..f5e4bbc14 100644 --- a/onadata/settings/kc_environ.py +++ b/onadata/settings/kc_environ.py @@ -206,10 +206,11 @@ def celery_logger_setup_handler(logger, **kwargs): # Set the `server_name` attribute. See https://docs.sentry.io/hosted/clients/python/advanced/ server_name = os.environ.get('RAVEN_SERVER_NAME') - server_name = server_name or '.'.join(filter(None, ( + server_name = server_name or '.'.join([_f for _f in ( os.environ.get('KOBOCAT_PUBLIC_SUBDOMAIN', None), os.environ.get('PUBLIC_DOMAIN_NAME', None) - ))) + ) if _f]) + if server_name: RAVEN_CONFIG.update({'name': server_name}) From 74ffb29e16b22ed8d433c79a98c6e0f639b7583b Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Tue, 21 Apr 2020 16:15:50 -0400 Subject: [PATCH 008/240] Replaced "urllib2" with "urllib" --- .../apps/logger/tests/test_briefcase_client.py | 2 +- onadata/apps/main/forms.py | 8 +++----- onadata/apps/main/google_doc.py | 4 ++-- onadata/apps/main/tests/test_base.py | 7 ++++--- onadata/apps/main/tests/test_form_enter_data.py | 2 +- .../serializers/password_reset_serializer.py | 5 +++-- onadata/libs/utils/briefcase_client.py | 16 ++++++++-------- onadata/libs/utils/decorators.py | 7 ++++--- onadata/libs/utils/export_tools.py | 5 +---- onadata/libs/utils/google.py | 17 ++++++++--------- 10 files changed, 35 insertions(+), 38 deletions(-) diff --git a/onadata/apps/logger/tests/test_briefcase_client.py b/onadata/apps/logger/tests/test_briefcase_client.py index 84a66c91a..4aff60ee0 100644 --- a/onadata/apps/logger/tests/test_briefcase_client.py +++ b/onadata/apps/logger/tests/test_briefcase_client.py @@ -3,7 +3,7 @@ import os.path from cStringIO import StringIO -from urlparse import urljoin +from urllib.parse import urljoin import requests from django.contrib.auth import authenticate diff --git a/onadata/apps/main/forms.py b/onadata/apps/main/forms.py index 1654963dc..9ca9a9f9a 100644 --- a/onadata/apps/main/forms.py +++ b/onadata/apps/main/forms.py @@ -2,13 +2,12 @@ from __future__ import unicode_literals, print_function, division, absolute_import import re -import urllib2 -from urlparse import urlparse +from urllib.request import urlopen +from urllib.parse import urlparse from StringIO import StringIO from django import forms from django.contrib.auth.models import User -from django.contrib.sites.models import Site from django.core.files.base import ContentFile from django.core.files.storage import default_storage from django.core.validators import URLValidator @@ -17,7 +16,6 @@ from django.conf import settings from recaptcha.client import captcha from registration.forms import RegistrationFormUniqueEmail -from registration.models import RegistrationProfile from pyxform.xls2json_backends import csv_to_dict @@ -322,7 +320,7 @@ def publish(self, user, id_string=None): cleaned_xls_file = \ upload_to(None, cleaned_xls_file, user.username) self.validate(cleaned_url) - xls_data = ContentFile(urllib2.urlopen(cleaned_url).read()) + xls_data = ContentFile(urlopen(cleaned_url).read()) cleaned_xls_file = \ default_storage.save(cleaned_xls_file, xls_data) # publish the xls diff --git a/onadata/apps/main/google_doc.py b/onadata/apps/main/google_doc.py index d0b7fb86d..62680c19f 100644 --- a/onadata/apps/main/google_doc.py +++ b/onadata/apps/main/google_doc.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals, print_function, division, absolute_import import re -import urllib2 +from urllib.request import urlopen from django.template.loader import render_to_string from django.template.defaultfilters import slugify @@ -63,7 +63,7 @@ def __init__(self, url=None): self.set_html_from_url(url) def set_html_from_url(self, url): - f = urllib2.urlopen(url) + f = urlopen(url) self.set_html(f.read()) f.close() diff --git a/onadata/apps/main/tests/test_base.py b/onadata/apps/main/tests/test_base.py index bb778174c..eb87587b5 100644 --- a/onadata/apps/main/tests/test_base.py +++ b/onadata/apps/main/tests/test_base.py @@ -5,8 +5,9 @@ import os import re import socket -import urllib2 from cStringIO import StringIO +from urllib.request import urlopen +from urllib.error import URLError from tempfile import NamedTemporaryFile from django.conf import settings @@ -255,9 +256,9 @@ def _make_submissions(self, username=None, add_uuid=False, def _check_url(self, url, timeout=1): try: - urllib2.urlopen(url, timeout=timeout) + urlopen(url, timeout=timeout) return True - except (urllib2.URLError, socket.timeout): + except (URLError, socket.timeout): pass return False diff --git a/onadata/apps/main/tests/test_form_enter_data.py b/onadata/apps/main/tests/test_form_enter_data.py index 9d4506d19..a5fbb4051 100644 --- a/onadata/apps/main/tests/test_form_enter_data.py +++ b/onadata/apps/main/tests/test_form_enter_data.py @@ -5,7 +5,7 @@ import re import unittest from time import time -from urlparse import urlparse +from urllib.parse import urlparse import requests from django.conf import settings diff --git a/onadata/libs/serializers/password_reset_serializer.py b/onadata/libs/serializers/password_reset_serializer.py index 0660bf3f9..2771a1b77 100644 --- a/onadata/libs/serializers/password_reset_serializer.py +++ b/onadata/libs/serializers/password_reset_serializer.py @@ -1,5 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + +from urllib.parse import urlparse + from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.core.mail import send_mail @@ -9,9 +12,7 @@ from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_encode from django.utils.translation import ugettext_lazy as _ - from rest_framework import serializers -from urlparse import urlparse def get_password_reset_email(user, reset_url, diff --git a/onadata/libs/utils/briefcase_client.py b/onadata/libs/utils/briefcase_client.py index d2bd7918a..7c63af9f9 100644 --- a/onadata/libs/utils/briefcase_client.py +++ b/onadata/libs/utils/briefcase_client.py @@ -1,20 +1,20 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -import os -import time -import math + import logging +import math import mimetypes -import requests -from requests.auth import HTTPDigestAuth -from urlparse import urljoin -from xml.parsers.expat import ExpatError - +import os +import time from cStringIO import StringIO +from urllib.parse import urljoin +from xml.parsers.expat import ExpatError +import requests from django.core.files.base import ContentFile from django.core.files.storage import default_storage from django.core.files.uploadedfile import InMemoryUploadedFile +from requests.auth import HTTPDigestAuth from onadata.apps.logger.xform_instance_parser import clean_and_parse_xml from onadata.libs.utils.logger_tools import publish_xml_form, publish_form, \ diff --git a/onadata/libs/utils/decorators.py b/onadata/libs/utils/decorators.py index 96f7e4a22..a839608ba 100644 --- a/onadata/libs/utils/decorators.py +++ b/onadata/libs/utils/decorators.py @@ -1,7 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + from functools import wraps -import urlparse +from urllib.parse import urlparse from django.contrib.auth import REDIRECT_FIELD_NAME from django.utils.decorators import available_attrs @@ -34,8 +35,8 @@ def _wrapped_view(request, *args, **kwargs): login_url = request.build_absolute_uri(settings.LOGIN_URL) # If the login url is the same scheme and net location then just # use the path as the "next" url. - login_scheme, login_netloc = urlparse.urlparse(login_url)[:2] - current_scheme, current_netloc = urlparse.urlparse(path)[:2] + login_scheme, login_netloc = urlparse(login_url)[:2] + current_scheme, current_netloc = urlparse(path)[:2] if ((not login_scheme or login_scheme == current_scheme) and (not login_netloc or login_netloc == current_netloc)): path = request.get_full_path() diff --git a/onadata/libs/utils/export_tools.py b/onadata/libs/utils/export_tools.py index 6d32a0d32..9faf007fd 100644 --- a/onadata/libs/utils/export_tools.py +++ b/onadata/libs/utils/export_tools.py @@ -2,13 +2,12 @@ from __future__ import unicode_literals, print_function, division, absolute_import import csv -from datetime import datetime, date import json import os import re import six import tempfile -from urlparse import urlparse +from datetime import datetime, date from zipfile import ZipFile from bson import json_util @@ -24,10 +23,8 @@ from pyxform.question import Question from pyxform.section import Section, RepeatingSection from savReaderWriter import SavWriter -from json2xlsclient.client import Client from onadata.apps.logger.models import Attachment, Instance, XForm -from onadata.apps.main.models.meta_data import MetaData from onadata.apps.viewer.models.export import Export from onadata.apps.api.mongo_helper import MongoHelper from onadata.libs.utils.viewer_tools import create_attachments_zipfile diff --git a/onadata/libs/utils/google.py b/onadata/libs/utils/google.py index 55201dbce..4e3aef8de 100644 --- a/onadata/libs/utils/google.py +++ b/onadata/libs/utils/google.py @@ -1,18 +1,17 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -import json -import urllib -import urllib2 +import json +from urllib.request import Request, urlopen import gdata -import gdata.gauth -import gdata.docs import gdata.data +import gdata.docs import gdata.docs.client import gdata.docs.data - +import gdata.gauth from django.conf import settings +from django.utils.http import urlencode oauth2_token = gdata.gauth.OAuth2Token( client_id=settings.GOOGLE_CLIENT_ID, @@ -28,15 +27,15 @@ def get_refreshed_token(token): - data = urllib.urlencode({ + data = 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( + request = Request( url='https://accounts.google.com/o/oauth2/token', data=data) - request_open = urllib2.urlopen(request) + request_open = urlopen(request) response = request_open.read() request_open.close() tokens = json.loads(response) From ee50a5ec8de21c6a06b7d5050ff6a415ce5cb5ad Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Tue, 21 Apr 2020 16:29:48 -0400 Subject: [PATCH 009/240] Replaced "obj.next()" with "next(obj)" --- onadata/apps/main/tests/test_audit_log.py | 2 +- onadata/apps/main/tests/test_form_api_delete.py | 2 +- onadata/apps/main/tests/test_process.py | 4 ++-- onadata/apps/viewer/tests/test_exports.py | 2 +- onadata/apps/viewer/tests/test_pandas_mongo_bridge.py | 2 +- onadata/libs/utils/csv_reader.py | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/onadata/apps/main/tests/test_audit_log.py b/onadata/apps/main/tests/test_audit_log.py index e975fa775..225c11098 100644 --- a/onadata/apps/main/tests/test_audit_log.py +++ b/onadata/apps/main/tests/test_audit_log.py @@ -23,7 +23,7 @@ def test_audit_log_call(self): cursor = AuditLog.query_mongo( account_user.username, None, None, sort, 0, 1) self.assertTrue(cursor.count() > 0) - record = cursor.next() + record = next(cursor) self.assertEqual(record['account'], "alice") self.assertEqual(record['user'], "bob") self.assertEqual(record['action'], Actions.FORM_PUBLISHED) diff --git a/onadata/apps/main/tests/test_form_api_delete.py b/onadata/apps/main/tests/test_form_api_delete.py index a509d8e01..266ae099a 100644 --- a/onadata/apps/main/tests/test_form_api_delete.py +++ b/onadata/apps/main/tests/test_form_api_delete.py @@ -109,5 +109,5 @@ def test_delete_updates_mongo(self): cursor = settings.MONGO_DB.instances.find( {common_tags.ID: instance.id}) self.assertEqual(cursor.count(), 1) - record = cursor.next() + record = next(cursor) self.assertIsNotNone(record[common_tags.DELETEDAT]) diff --git a/onadata/apps/main/tests/test_process.py b/onadata/apps/main/tests/test_process.py index ff1c36cce..f244746de 100644 --- a/onadata/apps/main/tests/test_process.py +++ b/onadata/apps/main/tests/test_process.py @@ -250,7 +250,7 @@ def _check_data_dictionary(self): # test to make sure the headers in the actual csv are as expected actual_csv = self._get_csv_() - self.assertEqual(sorted(actual_csv.next()), sorted(expected_list)) + self.assertEqual(sorted(next(actual_csv)), sorted(expected_list)) def _check_data_for_csv_export(self): @@ -354,7 +354,7 @@ def _check_csv_export_second_pass(self): actual_csv = self._get_response_content(response) actual_lines = actual_csv.split("\n") actual_csv = csv.reader(actual_lines) - headers = actual_csv.next() + headers = next(actual_csv) data = [ {'meta/instanceID': 'uuid:5b2cc313-fc09-437e-8149-fcd32f695d41', '_uuid': '5b2cc313-fc09-437e-8149-fcd32f695d41', diff --git a/onadata/apps/viewer/tests/test_exports.py b/onadata/apps/viewer/tests/test_exports.py index a13e489d4..05b18879e 100644 --- a/onadata/apps/viewer/tests/test_exports.py +++ b/onadata/apps/viewer/tests/test_exports.py @@ -691,7 +691,7 @@ def _get_csv_data(self, filepath): storage = get_storage_class()() csv_file = storage.open(filepath) reader = csv.DictReader(csv_file) - data = reader.next() + data = next(reader) csv_file.close() return data diff --git a/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py b/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py index 60a651565..fb8049157 100644 --- a/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py +++ b/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py @@ -576,7 +576,7 @@ def test_csv_export_with_df_size_limit(self): csv_df_builder.export_to(temp_file.name, data_frame_max_size=3) csv_file = open(temp_file.name) csv_reader = csv.reader(csv_file) - header = csv_reader.next() + header = next(csv_reader) self.assertEqual( len(header), 17 + len(AbstractDataFrameBuilder.ADDITIONAL_COLUMNS)) rows = [] diff --git a/onadata/libs/utils/csv_reader.py b/onadata/libs/utils/csv_reader.py index 988c6d1a5..3116ee7d3 100644 --- a/onadata/libs/utils/csv_reader.py +++ b/onadata/libs/utils/csv_reader.py @@ -29,17 +29,17 @@ def close(self): def __iter__(self): return self - def next(self): + def __next__(self): """ A CsvReader object is iterable (since we have defined __iter__ and next methods. Each iteration of this object returns a row of data. """ - row = self._csv_reader.next() + row = next(self._csv_reader) return [cell for cell in row] def _set_headers(self): - self._headers = self.next() + self._headers = next(self) def iter_dicts(self): self._set_headers() From 211932bac9b77be2132e06c43d9c503a34fe9591 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Wed, 22 Apr 2020 10:15:30 -0300 Subject: [PATCH 010/240] Django 1.11 support changes: - Replaced `is_anonymous()` with `is_anonymous` - Replaced `is_authenticated()` with `is_authenticated` - Replaced `detail_route` and `list_route` with `action_route` - Replaced `make_option` in management command with `add_argument` - Use imports in url patterns (instead of string imports) - Changed INSTALLED_APP order --- onadata/apps/api/permissions.py | 2 +- .../tests/viewsets/test_abstract_viewset.py | 2 +- onadata/apps/api/viewsets/briefcase_api.py | 10 +- onadata/apps/api/viewsets/connect_viewset.py | 7 +- onadata/apps/api/viewsets/data_viewset.py | 8 +- onadata/apps/api/viewsets/note_viewset.py | 2 +- .../apps/api/viewsets/user_profile_viewset.py | 4 +- onadata/apps/api/viewsets/xform_list_api.py | 10 +- .../apps/api/viewsets/xform_submission_api.py | 2 +- onadata/apps/api/viewsets/xform_viewset.py | 11 +- .../commands/create_image_thumbnails.py | 20 +- .../management/commands/import_briefcase.py | 25 +- .../commands/populate_media_file_basename.py | 12 +- .../logger/management/commands/publish_xls.py | 11 +- .../commands/pull_from_aggregate.py | 24 +- .../commands/update_is_sync_with_mongo.py | 10 +- .../management/commands/update_xform_uuids.py | 15 +- onadata/apps/main/forms.py | 3 +- onadata/apps/main/google_export.py | 6 +- .../apps/main/management/commands/mailer.py | 6 +- onadata/apps/main/tests/test_base.py | 4 +- onadata/apps/main/urls.py | 289 ++++++++++++------ onadata/apps/main/views.py | 4 +- .../viewer/management/commands/remongo.py | 19 +- .../viewer/management/commands/sync_mongo.py | 36 ++- onadata/apps/viewer/views.py | 11 +- onadata/koboform/redirect_middleware.py | 2 +- onadata/libs/filters.py | 2 +- onadata/libs/mixins/anonymous_user_mixin.py | 2 +- .../anonymous_user_public_forms_mixin.py | 2 +- onadata/libs/mixins/labels_mixin.py | 4 +- .../serializers/user_profile_serializer.py | 2 +- onadata/libs/utils/decorators.py | 2 +- onadata/libs/utils/logger_tools.py | 4 +- onadata/libs/utils/middleware.py | 2 +- onadata/libs/utils/user_auth.py | 2 +- onadata/settings/common.py | 4 +- 37 files changed, 342 insertions(+), 239 deletions(-) diff --git a/onadata/apps/api/permissions.py b/onadata/apps/api/permissions.py index 20895420e..1a8480f7f 100644 --- a/onadata/apps/api/permissions.py +++ b/onadata/apps/api/permissions.py @@ -120,7 +120,7 @@ def has_permission(self, request, view): perms = self.get_required_permissions(request.method, model_cls) if (request.user and - (request.user.is_authenticated() or + (request.user.is_authenticated or not self.authenticated_users_only) and request.user.has_perms(perms)): diff --git a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py index 10b583494..6f0461210 100644 --- a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py @@ -117,7 +117,7 @@ def _add_permissions_to_user(self, user, save=True): # `auth_permission`. Without this, actions # on individual instances are immediately denied and object-level permissions # are never considered. - if user.is_anonymous(): + if user.is_anonymous: user = User.objects.get(id=settings.ANONYMOUS_USER_ID) user.user_permissions = Permission.objects.all() if save: diff --git a/onadata/apps/api/viewsets/briefcase_api.py b/onadata/apps/api/viewsets/briefcase_api.py index 3b44a9b38..589d1b009 100644 --- a/onadata/apps/api/viewsets/briefcase_api.py +++ b/onadata/apps/api/viewsets/briefcase_api.py @@ -16,7 +16,7 @@ from rest_framework.generics import get_object_or_404 from rest_framework.renderers import BrowsableAPIRenderer from rest_framework.response import Response -from rest_framework.decorators import detail_route +from rest_framework.decorators import action_route from onadata.apps.api.tools import get_media_file_response from onadata.apps.api.permissions import ViewDjangoObjectPermissions @@ -117,11 +117,11 @@ def get_object(self): def filter_queryset(self, queryset): username = self.kwargs.get('username') - if username is None and self.request.user.is_anonymous(): + if username is None and self.request.user.is_anonymous: # raises a permission denied exception, forces authentication self.permission_denied(self.request) - if username is not None and self.request.user.is_anonymous(): + if username is not None and self.request.user.is_anonymous: profile = get_object_or_404( UserProfile, user__username=username.lower()) @@ -235,7 +235,7 @@ def retrieve(self, request, *args, **kwargs): location=False), template_name='downloadSubmission.xml') - @detail_route(methods=['GET']) + @action_route(detail=True, methods=['GET']) def manifest(self, request, *args, **kwargs): self.object = self.get_object() object_list = MetaData.objects.filter(data_type='media', @@ -248,7 +248,7 @@ def manifest(self, request, *args, **kwargs): headers=self.get_openrosa_headers(request, location=False)) - @detail_route(methods=['GET']) + @action_route(detail=True, methods=['GET']) def media(self, request, *args, **kwargs): self.object = self.get_object() pk = kwargs.get('metadata') diff --git a/onadata/apps/api/viewsets/connect_viewset.py b/onadata/apps/api/viewsets/connect_viewset.py index 437c2bf9d..90ce223bf 100644 --- a/onadata/apps/api/viewsets/connect_viewset.py +++ b/onadata/apps/api/viewsets/connect_viewset.py @@ -2,8 +2,7 @@ from __future__ import unicode_literals, print_function, division, absolute_import from rest_framework import status from rest_framework import viewsets -from rest_framework.decorators import detail_route -from rest_framework.decorators import list_route +from rest_framework.decorators import action_route from rest_framework.response import Response from onadata.apps.api.permissions import ConnectViewsetPermissions @@ -101,7 +100,7 @@ class ConnectViewSet(ObjectLookupMixin, viewsets.GenericViewSet): def list(self, request, *args, **kwargs): """ Returns authenticated user profile""" - if request and not request.user.is_anonymous(): + if request and not request.user.is_anonymous: session = getattr(request, "session") if not session.session_key: # login user to create session token @@ -116,7 +115,7 @@ def list(self, request, *args, **kwargs): return Response(serializer.data) - @list_route(methods=['POST']) + @action_route(detail=False, methods=['POST']) def reset(self, request, *args, **kwargs): context = {'request': request} data = request.data if request.data is not None else {} diff --git a/onadata/apps/api/viewsets/data_viewset.py b/onadata/apps/api/viewsets/data_viewset.py index 45882f132..82fcffc1e 100644 --- a/onadata/apps/api/viewsets/data_viewset.py +++ b/onadata/apps/api/viewsets/data_viewset.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext as _ from rest_framework import status -from rest_framework.decorators import detail_route, list_route +from rest_framework.decorators import action_route from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework.exceptions import ParseError, PermissionDenied @@ -510,7 +510,7 @@ def filter_queryset(self, queryset, view=None): return qs - @detail_route(methods=["GET", "PATCH", "DELETE"]) + @action_route(detail=True, methods=["GET", "PATCH", "DELETE"]) def validation_status(self, request, *args, **kwargs): """ View or modify validation status of specific instance. @@ -542,7 +542,7 @@ def validation_status(self, request, *args, **kwargs): return Response(data, status=http_status) - @detail_route(methods=['GET', 'POST', 'DELETE'], + @action_route(detail=True, methods=['GET', 'POST', 'DELETE'], extra_lookup_fields=['label', ]) def labels(self, request, *args, **kwargs): http_status = status.HTTP_400_BAD_REQUEST @@ -576,7 +576,7 @@ def labels(self, request, *args, **kwargs): return Response(data, status=http_status) - @detail_route(methods=['GET']) + @action_route(detail=True, methods=['GET']) def enketo(self, request, *args, **kwargs): self.object = self.get_object() data = {} diff --git a/onadata/apps/api/viewsets/note_viewset.py b/onadata/apps/api/viewsets/note_viewset.py index cb34524bb..d47939073 100644 --- a/onadata/apps/api/viewsets/note_viewset.py +++ b/onadata/apps/api/viewsets/note_viewset.py @@ -66,7 +66,7 @@ class NoteViewSet(ModelViewSet): def get_queryset(self): # Allows users to see only notes of instances they're allowed to see - if self.request.user.is_anonymous(): + if self.request.user.is_anonymous: user_id = settings.ANONYMOUS_USER_ID else: user_id = self.request.user.pk diff --git a/onadata/apps/api/viewsets/user_profile_viewset.py b/onadata/apps/api/viewsets/user_profile_viewset.py index d32e18494..310001ba2 100644 --- a/onadata/apps/api/viewsets/user_profile_viewset.py +++ b/onadata/apps/api/viewsets/user_profile_viewset.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.auth.models import User from rest_framework import status -from rest_framework.decorators import detail_route +from rest_framework.decorators import action_route from rest_framework.exceptions import ParseError from rest_framework.generics import get_object_or_404 from rest_framework.response import Response @@ -162,7 +162,7 @@ def get_object(self): return obj - @detail_route(methods=['POST']) + @action_route(detail=True, methods=['POST']) def change_password(self, request, *args, **kwargs): user_profile = self.get_object() current_password = request.data.get('current_password', None) diff --git a/onadata/apps/api/viewsets/xform_list_api.py b/onadata/apps/api/viewsets/xform_list_api.py index 7ff01fd71..c513b22a9 100644 --- a/onadata/apps/api/viewsets/xform_list_api.py +++ b/onadata/apps/api/viewsets/xform_list_api.py @@ -10,7 +10,7 @@ from rest_framework import viewsets from rest_framework import permissions from rest_framework.response import Response -from rest_framework.decorators import detail_route +from rest_framework.decorators import action_route from onadata.apps.api.tools import get_media_file_response from onadata.apps.logger.models.xform import XForm @@ -71,7 +71,7 @@ def filter_queryset(self, queryset): username = self.kwargs.get('username') if username is None: # If no username is specified, the request must be authenticated - if self.request.user.is_anonymous(): + if self.request.user.is_anonymous: # raises a permission denied exception, forces authentication self.permission_denied(self.request) else: @@ -86,7 +86,7 @@ def filter_queryset(self, queryset): if profile.require_auth: # The specified has user ticked "Require authentication to see # forms and submit data"; reject anonymous requests - if self.request.user.is_anonymous(): + if self.request.user.is_anonymous: # raises a permission denied exception, forces authentication self.permission_denied(self.request) else: @@ -110,7 +110,7 @@ def retrieve(self, request, *args, **kwargs): return Response(self.object.xml, headers=self.get_openrosa_headers()) - @detail_route(methods=['GET']) + @action_route(detail=True, methods=['GET']) def manifest(self, request, *args, **kwargs): self.object = self.get_object() object_list = MetaData.objects.filter(data_type='media', @@ -121,7 +121,7 @@ def manifest(self, request, *args, **kwargs): return Response(serializer.data, headers=self.get_openrosa_headers()) - @detail_route(methods=['GET']) + @action_route(detail=True, methods=['GET']) def media(self, request, *args, **kwargs): self.object = self.get_object() pk = kwargs.get('metadata') diff --git a/onadata/apps/api/viewsets/xform_submission_api.py b/onadata/apps/api/viewsets/xform_submission_api.py index 0c94a3bb7..349de7e55 100644 --- a/onadata/apps/api/viewsets/xform_submission_api.py +++ b/onadata/apps/api/viewsets/xform_submission_api.py @@ -166,7 +166,7 @@ def __init__(self, *args, **kwargs): def create(self, request, *args, **kwargs): username = self.kwargs.get('username') - if self.request.user.is_anonymous(): + if self.request.user.is_anonymous: if username is None: # raises a permission denied exception, forces authentication self.permission_denied(self.request) diff --git a/onadata/apps/api/viewsets/xform_viewset.py b/onadata/apps/api/viewsets/xform_viewset.py index f196bf952..029c6abe2 100644 --- a/onadata/apps/api/viewsets/xform_viewset.py +++ b/onadata/apps/api/viewsets/xform_viewset.py @@ -14,7 +14,7 @@ from django.utils.translation import ugettext as _ from rest_framework import exceptions from rest_framework import status -from rest_framework.decorators import detail_route +from rest_framework.decorators import action_route from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.viewsets import ModelViewSet @@ -661,9 +661,6 @@ class XFormViewSet(AnonymousUserPublicFormsMixin, LabelsMixin, ModelViewSet): lookup_field = 'pk' extra_lookup_fields = None permission_classes = [XFormPermissions, ] - # TODO: Figure out what `updatable_fields` does; if nothing, remove it - #updatable_fields = set(('description', 'downloadable', 'require_auth', - # 'shared', 'shared_data', 'title')) filter_backends = (filters.AnonDjangoObjectPermissionFilter, filters.TagFilter, filters.XFormOwnerFilter, @@ -718,7 +715,7 @@ def update(self, request, pk, *args, **kwargs): # Let the superclass handle updates to the other fields return super(XFormViewSet, self).update(request, pk, *args, **kwargs) - @detail_route(methods=['GET']) + @action_route(detail=True, methods=['GET']) def form(self, request, format='json', **kwargs): form = self.get_object() if format not in ['json', 'xml', 'xls', 'csv']: @@ -732,7 +729,7 @@ def form(self, request, format='json', **kwargs): return response - @detail_route(methods=['GET']) + @action_route(detail=True, methods=['GET']) def enketo(self, request, **kwargs): self.object = self.get_object() form_url = _get_form_url(request, self.object.user.username) @@ -765,7 +762,7 @@ def retrieve(self, request, *args, **kwargs): query, export_type) - @detail_route(methods=['POST']) + @action_route(detail=True, methods=['POST']) def csv_import(self, request, *args, **kwargs): """ Endpoint for CSV data imports diff --git a/onadata/apps/logger/management/commands/create_image_thumbnails.py b/onadata/apps/logger/management/commands/create_image_thumbnails.py index 680173dca..b6f32695c 100644 --- a/onadata/apps/logger/management/commands/create_image_thumbnails.py +++ b/onadata/apps/logger/management/commands/create_image_thumbnails.py @@ -3,7 +3,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from optparse import make_option from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError @@ -21,14 +20,17 @@ class Command(BaseCommand): help = ugettext_lazy("Creates thumbnails for " "all form images and stores them") - option_list = BaseCommand.option_list + ( - make_option('-u', '--username', - help=ugettext_lazy("Username of the form user")), - make_option('-i', '--id_string', - help=ugettext_lazy("id string of the form")), - make_option('-f', '--force', action='store_false', - help=ugettext_lazy("regenerate thumbnails if they exist.")) - ) + + def add_arguments(self, parser): + parser.add_argument('-u', '--username', + help=ugettext_lazy("Username of the form user")) + + parser.add_argument('-i', '--id_string', + help=ugettext_lazy("id string of the form")) + + parser.add_argument('-f', '--force', action='store_false', + help=ugettext_lazy("regenerate thumbnails if they " + "exist.")) def handle(self, *args, **kwargs): attachments_qs = Attachment.objects.select_related( diff --git a/onadata/apps/logger/management/commands/import_briefcase.py b/onadata/apps/logger/management/commands/import_briefcase.py index 761ad91a5..55c841da0 100644 --- a/onadata/apps/logger/management/commands/import_briefcase.py +++ b/onadata/apps/logger/management/commands/import_briefcase.py @@ -6,23 +6,26 @@ from django.contrib.auth.models import User from django.core.management.base import BaseCommand from django.utils.translation import ugettext as _ -from optparse import make_option from onadata.libs.utils.briefcase_client import BriefcaseClient class Command(BaseCommand): help = _("Insert all existing parsed instances into MongoDB") - option_list = BaseCommand.option_list + ( - make_option('--url', - help=_("server url to pull forms and submissions")), - make_option('-u', '--username', - help=_("Username")), - make_option('-p', '--password', - help=_("Password")), - make_option('--to', - help=_("username in this server")), - ) + + def add_arguments(self, parser): + parser.add_argument('--url', + help=_("server url to pull forms and submissions")) + + parser.add_argument('-u', '--username', + help=_("Username")) + + parser.add_argument('-p', '--password', + help=_("Password")) + + parser.add_argument('--to', + help=_("username in this server")) + def handle(self, *args, **kwargs): url = kwargs.get('url') diff --git a/onadata/apps/logger/management/commands/populate_media_file_basename.py b/onadata/apps/logger/management/commands/populate_media_file_basename.py index ee190c7f7..30ec6ba98 100644 --- a/onadata/apps/logger/management/commands/populate_media_file_basename.py +++ b/onadata/apps/logger/management/commands/populate_media_file_basename.py @@ -3,8 +3,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from optparse import make_option - from django.core.management.base import BaseCommand from django.db.models import Q, Func from django.utils.translation import ugettext as _, ugettext_lazy @@ -19,13 +17,15 @@ class SubstrFromPattern(Func): class Command(BaseCommand): - help = ugettext_lazy("Updates indexed field `media_file_basename` which is empty or null") - option_list = BaseCommand.option_list + ( - make_option( + help = ugettext_lazy("Updates indexed field `media_file_basename` " + "which is empty or null") + + def add_arguments(self, parser): + parser.add_argument( '--batchsize', type='int', default=1000, - help=ugettext_lazy("Number of records to process per query")),) + help=ugettext_lazy("Number of records to process per query")) def handle(self, *args, **kwargs): batchsize = kwargs.get("batchsize") diff --git a/onadata/apps/logger/management/commands/publish_xls.py b/onadata/apps/logger/management/commands/publish_xls.py index 31f57a919..9d312607a 100644 --- a/onadata/apps/logger/management/commands/publish_xls.py +++ b/onadata/apps/logger/management/commands/publish_xls.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals, print_function, division, absolute_import import os -from optparse import make_option from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError @@ -19,11 +18,11 @@ class Command(BaseCommand): help = ugettext_lazy("Publish an XLS file with the option of replacing an" "existing one") - option_list = BaseCommand.option_list + ( - make_option('-r', '--replace', - action='store_true', - dest='replace', - help=ugettext_lazy("Replace existing form if any")),) + def add_arguments(self, parser): + parser.add_argument('-r', '--replace', + action='store_true', + dest='replace', + help=ugettext_lazy("Replace existing form if any")) def handle(self, *args, **options): try: diff --git a/onadata/apps/logger/management/commands/pull_from_aggregate.py b/onadata/apps/logger/management/commands/pull_from_aggregate.py index f45101a59..3c7565bf2 100644 --- a/onadata/apps/logger/management/commands/pull_from_aggregate.py +++ b/onadata/apps/logger/management/commands/pull_from_aggregate.py @@ -6,23 +6,25 @@ from django.contrib.auth.models import User from django.core.management.base import BaseCommand from django.utils.translation import ugettext as _ -from optparse import make_option from onadata.libs.utils.briefcase_client import BriefcaseClient class Command(BaseCommand): help = _("Insert all existing parsed instances into MongoDB") - option_list = BaseCommand.option_list + ( - make_option('--url', - help=_("server url to pull forms and submissions")), - make_option('-u', '--username', - help=_("Username")), - make_option('-p', '--password', - help=_("Password")), - make_option('--to', - help=_("username in this server")), - ) + + def add_arguments(self, parser): + parser.add_argument('--url', + help=_("server url to pull forms and submissions")) + + parser.add_argument('-u', '--username', + help=_("Username")) + + parser.add_argument('-p', '--password', + help=_("Password")) + + parser.add_argument('--to', + help=_("username in this server")) def handle(self, *args, **kwargs): url = kwargs.get('url') diff --git a/onadata/apps/logger/management/commands/update_is_sync_with_mongo.py b/onadata/apps/logger/management/commands/update_is_sync_with_mongo.py index 8ea9002fa..a62196074 100644 --- a/onadata/apps/logger/management/commands/update_is_sync_with_mongo.py +++ b/onadata/apps/logger/management/commands/update_is_sync_with_mongo.py @@ -5,9 +5,8 @@ from django.conf import settings -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.utils.translation import ugettext as _, ugettext_lazy -from optparse import make_option from onadata.apps.logger.models.instance import Instance @@ -15,12 +14,13 @@ class Command(BaseCommand): help = ugettext_lazy("Updates is_synced_with_mongo property of Instance model") - option_list = BaseCommand.option_list + ( - make_option( + + def add_arguments(self, parser): + parser.add_argument( '--batchsize', type='int', default=100, - help=ugettext_lazy("Number of records to process per query")),) + help=ugettext_lazy("Number of records to process per query")) def handle(self, *args, **kwargs): batchsize = kwargs.get("batchsize", 100) diff --git a/onadata/apps/logger/management/commands/update_xform_uuids.py b/onadata/apps/logger/management/commands/update_xform_uuids.py index 5b175bbb7..77c5e5718 100644 --- a/onadata/apps/logger/management/commands/update_xform_uuids.py +++ b/onadata/apps/logger/management/commands/update_xform_uuids.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals, print_function, division, absolute_import import csv -from optparse import make_option from django.core.management.base import BaseCommand, CommandError from django.utils.translation import ugettext_lazy @@ -15,12 +14,14 @@ class Command(BaseCommand): - help = ugettext_lazy( - "Use a csv file with username, id_string and new_uuid to set new uuids" - ) - option_list = BaseCommand.option_list + ( - make_option('-f', '--file', - help=ugettext_lazy("Path to csv file")),) + + help = ugettext_lazy("Use a csv file with username, " + "id_string and new_uuid to set new uuids") + + def add_arguments(self, parser): + + parser.add_argument('-f', '--file', + help=ugettext_lazy("Path to csv file")) def handle(self, *args, **kwargs): # all options are required diff --git a/onadata/apps/main/forms.py b/onadata/apps/main/forms.py index 9ca9a9f9a..cf8fd1cb3 100644 --- a/onadata/apps/main/forms.py +++ b/onadata/apps/main/forms.py @@ -2,9 +2,10 @@ from __future__ import unicode_literals, print_function, division, absolute_import import re +from StringIO import StringIO from urllib.request import urlopen from urllib.parse import urlparse -from StringIO import StringIO + from django import forms from django.contrib.auth.models import User diff --git a/onadata/apps/main/google_export.py b/onadata/apps/main/google_export.py index 267d78c25..69685bfb2 100644 --- a/onadata/apps/main/google_export.py +++ b/onadata/apps/main/google_export.py @@ -19,7 +19,7 @@ def google_oauth2_request(request): token = None - if request.user.is_authenticated(): + if request.user.is_authenticated: try: ts = TokenStorageModel.objects.get(id=request.user) except TokenStorageModel.DoesNotExist: @@ -50,7 +50,7 @@ def google_oauth2_request(request): def google_auth_return(request): if 'code' not in request.REQUEST: return HttpResponse("Invalid Request") - if request.user.is_authenticated(): + if request.user.is_authenticated: try: ts = TokenStorageModel.objects.get(id=request.user) except TokenStorageModel.DoesNotExist: @@ -69,7 +69,7 @@ def google_auth_return(request): def refresh_access_token(token, user): token = get_refreshed_token(token) - if not user.is_authenticated(): + if not user.is_authenticated: return token try: ts = TokenStorageModel.objects.get(id=user) diff --git a/onadata/apps/main/management/commands/mailer.py b/onadata/apps/main/management/commands/mailer.py index 869aace33..eff4280bb 100644 --- a/onadata/apps/main/management/commands/mailer.py +++ b/onadata/apps/main/management/commands/mailer.py @@ -1,8 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from optparse import make_option - from django.core.management.base import BaseCommand, CommandError from django.contrib.auth.models import User from django.template.loader import get_template @@ -14,8 +12,8 @@ class Command(BaseCommand): help = ugettext_lazy("Send an email to all formhub users") - option_list = BaseCommand.option_list + ( - make_option("-m", "--message", dest="message", default=False)) + def add_arguments(self, parser): + parser.add_argument("-m", "--message", dest="message", default=False) def handle(self, *args, **kwargs): message = kwargs.get('message') diff --git a/onadata/apps/main/tests/test_base.py b/onadata/apps/main/tests/test_base.py index eb87587b5..8b19a22a9 100644 --- a/onadata/apps/main/tests/test_base.py +++ b/onadata/apps/main/tests/test_base.py @@ -5,7 +5,7 @@ import os import re import socket -from cStringIO import StringIO +from io import StringIO from urllib.request import urlopen from urllib.error import URLError from tempfile import NamedTemporaryFile @@ -52,7 +52,7 @@ def _add_permissions_to_user(self, user, save=True): # `auth_permission`. Without this, actions # on individual instances are immediately denied and object-level permissions # are never considered. - if user.is_anonymous(): + if user.is_anonymous: user = User.objects.get(id=settings.ANONYMOUS_USER_ID) user.user_permissions = Permission.objects.all() if save: diff --git a/onadata/apps/main/urls.py b/onadata/apps/main/urls.py index 9f318e955..4890e5eb6 100644 --- a/onadata/apps/main/urls.py +++ b/onadata/apps/main/urls.py @@ -3,15 +3,111 @@ from django.conf.urls import patterns, include, url from django.conf import settings +from django.contrib import admin from django.views.generic import RedirectView +from django.views.i18n import JavaScriptCatalog from onadata.apps.api.urls import router, router_with_patch_list from onadata.apps.api.urls import XFormListApi from onadata.apps.api.urls import XFormSubmissionApi from onadata.apps.api.urls import BriefcaseApi +from onadata.apps.main.service_health import service_health -# Uncomment the next two lines to enable the admin: -from django.contrib import admin +from onadata.apps.main.views import ( + # main website views + home, + tutorial, + about_us, + getting_started, + faq, + syntax, + privacy, + tos, + resources, + form_gallery, + members_list, + xls2xform, + support, + login_redirect, + username_list, + profile, + public_profile, + profile_settings, + clone_xlsform, + activity, + activity_api, + activity_fields, + api_token, + + # form specific + show, + qrcode, + api, + public_api, + delete_data, + edit, + set_perm, + form_photos, + download_metadata, + delete_metadata, + download_media_data, + update_xform, + enketo_preview, + show_form_settings +) + +# exporting stuff +from onadata.apps.viewer.views import ( + attachment_url, + data_export, + google_xls_export, + map_embed_view, + map_view, + instance, + add_submission_with, + thank_you_submission, + data_view, + create_export, + delete_export, + export_progress, + export_list, + export_download, + kml_export +) +from onadata.apps.logger.views import ( + enter_data, + edit_data, + bulksubmission, + bulksubmission_form, + download_xform, + download_xlsform, + download_jsonform, + delete_xform, + toggle_downloadable +) + +# SMS support +from onadata.apps.sms_support.providers import ( + import_submission_for_form, + import_submission +) +from onadata.apps.sms_support.views import ( + import_submission_for_form as view_import_submission_for_form, + import_multiple_submissions_for_form, + import_multiple_submissions, + import_submission as view_import_submission +) +# Statistics for superusers. The username is irrelevant, but leave it as +# the first part of the path to avoid collisions +from onadata.apps.logger.views import ( + superuser_stats, + retrieve_superuser_stats +) + +from onadata.apps.main.google_export import ( + google_auth_return, + google_oauth2_request +) admin.autodiscover() @@ -21,8 +117,7 @@ (r'^i18n/', include('django.conf.urls.i18n')), url('^api/v1/', include(router.urls)), url('^api/v1/', include(router_with_patch_list.urls)), - url(r'^service_health/$', - 'onadata.apps.main.service_health.service_health'), + url(r'^service_health/$', service_health), url(r'^api-docs/', RedirectView.as_view(url='/api/v1/')), url(r'^api/', RedirectView.as_view(url='/api/v1/')), url(r'^api/v1', RedirectView.as_view(url='/api/v1/')), @@ -36,90 +131,87 @@ 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'), + url(r'^gauthtest/$', google_oauth2_request, name='google-auth'), + url(r'^gwelcome/$', 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', + url(r'^$', home), + url(r'^tutorial/$', tutorial, name='tutorial'), + url(r'^about-us/$', about_us, name='about-us'), + url(r'^getting_started/$', 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', + url(r'^faq/$', faq, name='faq'), + url(r'^syntax/$', syntax, name='syntax'), + url(r'^privacy/$', privacy, name='privacy'), + url(r'^tos/$', tos, name='tos'), + url(r'^resources/$', resources, name='resources'), - url(r'^forms/$', 'onadata.apps.main.views.form_gallery', + url(r'^forms/$', 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'), + url(r'^forms/(?P[^/]+)$', show), + url(r'^people/$', members_list), + url(r'^xls2xform/$', xls2xform), + url(r'^support/$', support), + url(r'^login_redirect/$', login_redirect), # Bring back old url because it's still used by `kpi` # ToDo Remove when `kpi#gallery-2` is merged into master - url(r"^attachment/$", 'onadata.apps.viewer.views.attachment_url'), + url(r"^attachment/$", attachment_url), url(r"^attachment/(?P[^/]+)$", - 'onadata.apps.viewer.views.attachment_url'), - url(r"^{}$".format(settings.MEDIA_URL.lstrip('/')), 'onadata.apps.viewer.views.attachment_url'), + attachment_url), + url(r"^{}$".format(settings.MEDIA_URL.lstrip('/')), attachment_url), url(r"^{}(?P[^/]+)$".format(settings.MEDIA_URL.lstrip('/')), - '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', + attachment_url), + url(r'^jsi18n/$', JavaScriptCatalog.as_view(), + {'packages': ('main', 'viewer',)}, + name='javascript-catalog'), + url(r'^typeahead_usernames', username_list, name='username_list'), url(r'^(?P[^/]+)/$', - 'onadata.apps.main.views.profile', name='user_profile'), + profile, name='user_profile'), url(r'^(?P[^/]+)/profile$', - 'onadata.apps.main.views.public_profile', + public_profile, name='public_profile'), url(r'^(?P[^/]+)/settings', - 'onadata.apps.main.views.profile_settings'), + profile_settings), url(r'^(?P[^/]+)/cloneform$', - 'onadata.apps.main.views.clone_xlsform'), + clone_xlsform), url(r'^(?P[^/]+)/activity$', - 'onadata.apps.main.views.activity'), + activity), url(r'^(?P[^/]+)/activity/api$', - 'onadata.apps.main.views.activity_api'), - url(r'^activity/fields$', 'onadata.apps.main.views.activity_fields'), + activity_api), + url(r'^activity/fields$', activity_fields), url(r'^(?P[^/]+)/api-token$', - 'onadata.apps.main.views.api_token'), + api_token), # form specific url(r'^(?P[^/]+)/forms/(?P[^/]+)$', - 'onadata.apps.main.views.show'), + show), url(r'^(?P[^/]+)/forms/(?P[^/]+)/qrcode$', - 'onadata.apps.main.views.qrcode', name='get_qrcode'), + qrcode, name='get_qrcode'), url(r'^(?P[^/]+)/forms/(?P[^/]+)/api$', - 'onadata.apps.main.views.api', name='mongo_view_api'), + api, name='mongo_view_api'), url(r'^(?P[^/]+)/forms/(?P[^/]+)/public_api$', - 'onadata.apps.main.views.public_api', name='public_api'), + public_api, name='public_api'), url(r'^(?P[^/]+)/forms/(?P[^/]+)/delete_data$', - 'onadata.apps.main.views.delete_data', name='delete_data'), + delete_data, name='delete_data'), url(r'^(?P[^/]+)/forms/(?P[^/]+)/edit$', - 'onadata.apps.main.views.edit'), + edit), url(r'^(?P[^/]+)/forms/(?P[^/]+)/perms$', - 'onadata.apps.main.views.set_perm'), + set_perm), url(r'^(?P[^/]+)/forms/(?P[^/]+)/photos', - 'onadata.apps.main.views.form_photos'), + 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'), + '', download_metadata), + url(r'^(?P[^/]+)/forms/(?P[^/]+)/delete-doc/' + r'(?P\d+)', delete_metadata), + url(r'^(?P[^/]+)/forms/(?P[^/]+)/formid-media/' + r'(?P\d+)', download_media_data), url(r'^(?P[^/]+)/forms/(?P[^/]+)/update$', - 'onadata.apps.main.views.update_xform'), + update_xform), url(r'^(?P[^/]+)/forms/(?P[^/]+)/preview$', - 'onadata.apps.main.views.enketo_preview'), + enketo_preview), url(r'^(?P[^/]+)/forms/(?P[^/]+)/form_settings$', - 'onadata.apps.main.views.show_form_settings', name='show_form_settings'), + show_form_settings, name='show_form_settings'), # briefcase api urls url(r"^(?P\w+)/view/submissionList$", @@ -137,50 +229,50 @@ # exporting stuff url(r"^(?P\w+)/forms/(?P[^/]+)/data\.csv$", - 'onadata.apps.viewer.views.data_export', name='csv_export', + data_export, name='csv_export', kwargs={'export_type': 'csv'}), url(r"^(?P\w+)/forms/(?P[^/]+)/data\.xls", - 'onadata.apps.viewer.views.data_export', name='xls_export', + 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', + 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', + 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'), + kml_export), url(r"^(?P\w+)/forms/(?P[^/]+)/gdocs$", - 'onadata.apps.viewer.views.google_xls_export'), + google_xls_export), url(r"^(?P\w+)/forms/(?P[^/]+)/map_embed", - 'onadata.apps.viewer.views.map_embed_view'), + map_embed_view), url(r"^(?P\w+)/forms/(?P[^/]+)/map", - 'onadata.apps.viewer.views.map_view'), + map_view), url(r"^(?P\w+)/forms/(?P[^/]+)/instance", - 'onadata.apps.viewer.views.instance'), + instance), url(r"^(?P\w+)/forms/(?P[^/]+)/enter-data", - 'onadata.apps.logger.views.enter_data', name='enter_data'), + enter_data, name='enter_data'), url(r"^(?P\w+)/forms/(?P[^/]+)/add-submission-with", - 'onadata.apps.viewer.views.add_submission_with', + add_submission_with, name='add_submission_with'), url(r"^(?P\w+)/forms/(?P[^/]+)/thank_you_submission", - 'onadata.apps.viewer.views.thank_you_submission', + 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[^/]+)/edit-data/" + r"(?P\d+)$", edit_data, name='edit_data'), url(r"^(?P\w+)/forms/(?P[^/]+)/view-data", - 'onadata.apps.viewer.views.data_view'), + data_view), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/new$", 'onadata.apps.viewer.views.create_export'), + "/new$", create_export), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/delete$", 'onadata.apps.viewer.views.delete_export'), + "/delete$", delete_export), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/progress$", 'onadata.apps.viewer.views.export_progress'), + "/progress$", export_progress), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/$", 'onadata.apps.viewer.views.export_list'), + "/$", export_list), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" "/(?P[^/]+)$", - 'onadata.apps.viewer.views.export_download'), + export_download), url(r'^(?P\w+)/exports/', include('onadata.apps.export.urls')), url(r'^(?P\w+)/reports/', include('onadata.apps.survey_report.urls')), @@ -200,59 +292,59 @@ XFormListApi.as_view({'get': 'manifest'}), name='manifest-url'), url(r"^(?P\w+)/xformsMedia/(?P[\d+^/]+)" - "/(?P[\d+^/.]+)$", + r"/(?P[\d+^/.]+)$", XFormListApi.as_view({'get': 'media'}), name='xform-media'), url(r"^(?P\w+)/xformsMedia/(?P[\d+^/]+)" - "/(?P[\d+^/.]+)\.(?P[a-z0-9]+)$", + r"/(?P[\d+^/.]+)\.(?P[a-z0-9]+)$", XFormListApi.as_view({'get': 'media'}), name='xform-media'), url(r"^xformsMedia/(?P[\d+^/]+)/(?P[\d+^/.]+)$", XFormListApi.as_view({'get': 'media'}), name='xform-media'), url(r"^xformsMedia/(?P[\d+^/]+)/(?P[\d+^/.]+)\." - "(?P[a-z0-9]+)$", + r"(?P[a-z0-9]+)$", XFormListApi.as_view({'get': 'media'}), name='xform-media'), url(r"^(?P\w+)/submission$", XFormSubmissionApi.as_view({'post': 'create', 'head': 'create'}), name='submissions'), url(r"^(?P\w+)/bulk-submission$", - 'onadata.apps.logger.views.bulksubmission'), + bulksubmission), url(r"^(?P\w+)/bulk-submission-form$", - 'onadata.apps.logger.views.bulksubmission_form'), + bulksubmission_form), url(r"^(?P\w+)/forms/(?P[\d+^/]+)/form\.xml$", XFormListApi.as_view({'get': 'retrieve'}), name="download_xform"), url(r"^(?P\w+)/forms/(?P[^/]+)/form\.xml$", - 'onadata.apps.logger.views.download_xform', name="download_xform"), + download_xform, name="download_xform"), url(r"^(?P\w+)/forms/(?P[^/]+)/form\.xls$", - 'onadata.apps.logger.views.download_xlsform', + download_xlsform, name="download_xlsform"), url(r"^(?P\w+)/forms/(?P[^/]+)/form\.json", - 'onadata.apps.logger.views.download_jsonform', + download_jsonform, name="download_jsonform"), url(r"^(?P\w+)/delete/(?P[^/]+)/$", - 'onadata.apps.logger.views.delete_xform'), + delete_xform), url(r"^(?P\w+)/(?P[^/]+)/toggle_downloadable/$", - 'onadata.apps.logger.views.toggle_downloadable'), + toggle_downloadable), # SMS support - url(r'^(?P[^/]+)/forms/(?P[^/]+)/sms_submission/(?P[a-z]+)/?$', - 'onadata.apps.sms_support.providers.import_submission_for_form', + url(r'^(?P[^/]+)/forms/(?P[^/]+)/sms_submission/' + r'(?P[a-z]+)/?$', + 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', + view_import_submission_for_form, name='sms_submission_form'), url(r"^(?P[^/]+)/sms_submission/(?P[a-z]+)/?$", - 'onadata.apps.sms_support.providers.import_submission', + 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', + url(r'^(?P[^/]+)/forms/(?P[^/]+)/' + r'sms_multiple_submissions$', + import_multiple_submissions_for_form, name='sms_submissions_form'), url(r"^(?P[^/]+)/sms_multiple_submissions$", - 'onadata.apps.sms_support.views.import_multiple_submissions', + import_multiple_submissions, name='sms_submissions'), url(r"^(?P[^/]+)/sms_submission$", - 'onadata.apps.sms_support.views.import_submission', + view_import_submission, name='sms_submission'), # static media @@ -266,10 +358,9 @@ # Statistics for superusers. The username is irrelevant, but leave it as # the first part of the path to avoid collisions url(r"^(?P[^/]+)/superuser_stats/$", - 'onadata.apps.logger.views.superuser_stats'), + superuser_stats), url(r"^(?P[^/]+)/superuser_stats/(?P[^/]+)$", - 'onadata.apps.logger.views.retrieve_superuser_stats'), - + retrieve_superuser_stats), ) urlpatterns += patterns('django.contrib.staticfiles.views', diff --git a/onadata/apps/main/views.py b/onadata/apps/main/views.py index d0fde1075..715b64ed0 100644 --- a/onadata/apps/main/views.py +++ b/onadata/apps/main/views.py @@ -165,7 +165,7 @@ def profile(request, username): data = {'form': form} # xlsform submission... - if request.method == 'POST' and request.user.is_authenticated(): + if request.method == 'POST' and request.user.is_authenticated: def set_form(): form = QuickConverter(request.POST, request.FILES) survey = form.publish(request.user).survey @@ -865,7 +865,7 @@ def form_gallery(request): made a lot prettier. """ data = {} - if request.user.is_authenticated(): + 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 diff --git a/onadata/apps/viewer/management/commands/remongo.py b/onadata/apps/viewer/management/commands/remongo.py index fd140040a..8ff6674f1 100644 --- a/onadata/apps/viewer/management/commands/remongo.py +++ b/onadata/apps/viewer/management/commands/remongo.py @@ -1,9 +1,9 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + from django.conf import settings from django.core.management.base import BaseCommand, CommandError from django.utils.translation import ugettext_lazy -from optparse import make_option from onadata.apps.viewer.models.parsed_instance import ParsedInstance from onadata.libs.utils.common_tags import USERFORM_ID @@ -11,16 +11,19 @@ class Command(BaseCommand): help = ugettext_lazy("Insert all existing parsed instances into MongoDB") - option_list = BaseCommand.option_list + ( - make_option( + + def add_arguments(self, parser): + parser.add_argument( '--batchsize', type='int', default=100, - help=ugettext_lazy("Number of records to process per query")), - make_option('-u', '--username', - help=ugettext_lazy("Username of the form user")), - make_option('-i', '--id_string', - help=ugettext_lazy("id string of the form"))) + help=ugettext_lazy("Number of records to process per query")) + + parser.add_argument('-u', '--username', + help=ugettext_lazy("Username of the form user")) + + parser.add_argument('-i', '--id_string', + help=ugettext_lazy("id string of the form")) def handle(self, *args, **kwargs): ids = None diff --git a/onadata/apps/viewer/management/commands/sync_mongo.py b/onadata/apps/viewer/management/commands/sync_mongo.py index 0dd7457cb..c90b6b1d0 100644 --- a/onadata/apps/viewer/management/commands/sync_mongo.py +++ b/onadata/apps/viewer/management/commands/sync_mongo.py @@ -1,7 +1,6 @@ +#!/usr/bin/env python # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -#!/usr/bin/env python -from optparse import make_option from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError @@ -15,19 +14,26 @@ class Command(BaseCommand): args = '[username] [id_string]' help = ugettext_lazy("Check the count of submissions in sqlite vs the " "mongo db per form and optionally run remongo.") - option_list = BaseCommand.option_list + (make_option( - '-r', '--remongo', - action='store_true', - dest='remongo', - default=False, - help=ugettext_lazy("Whether to run remongo on the found set.")), - make_option( - '-a', '--all', action='store_true', dest='update_all', - default=False, help=ugettext_lazy( - "Update all instances for the selected " - "form(s), including existing ones. " - "Will delete and re-create mongo records. " - "Only makes sense when used with the -r option"))) + + def add_arguments(self, parser): + + parser.add_argument('-r', '--remongo', + action='store_true', + dest='remongo', + default=False, + help=ugettext_lazy("Whether to run remongo on the " + "found set.") + ) + + parser.add_argument('-a', '--all', + action='store_true', dest='update_all', + default=False, + help=ugettext_lazy( + "Update all instances for the selected " + "form(s), including existing ones. " + "Will delete and re-create mongo records. " + "Only makes sense when used with the -r option") + ) def handle(self, *args, **kwargs): user = xform = None diff --git a/onadata/apps/viewer/views.py b/onadata/apps/viewer/views.py index 325e78a87..857e6bd1f 100644 --- a/onadata/apps/viewer/views.py +++ b/onadata/apps/viewer/views.py @@ -180,7 +180,7 @@ def add_submission_with(request, username, id_string): import uuid import requests - from django.template import loader, Context + from django.template import loader from dpath import util as dpath_util from dict2xml import dict2xml @@ -200,8 +200,7 @@ def geopoint_xpaths(username, id_string): context = {'username': username, 'id_string': id_string, 'xml_content': dict2xml(xml_dict)} - instance_xml = loader.get_template("instance_add.xml")\ - .render(Context(context)) + instance_xml = loader.get_template("instance_add.xml").render(context) url = settings.ENKETO_API_INSTANCE_IFRAME_URL return_url = reverse('thank_you_submission', @@ -357,7 +356,7 @@ def create_export(request, username, id_string, export_type): def _get_google_token(request, redirect_to_url): token = None - if request.user.is_authenticated(): + if request.user.is_authenticated: try: ts = TokenStorageModel.objects.get(id=request.user) except TokenStorageModel.DoesNotExist: @@ -575,7 +574,7 @@ def kml_export(request, username, id_string): def google_xls_export(request, username, id_string): token = None - if request.user.is_authenticated(): + if request.user.is_authenticated: try: ts = TokenStorageModel.objects.get(id=request.user) except TokenStorageModel.DoesNotExist: @@ -682,7 +681,7 @@ def attachment_url(request, size='medium'): # the url xform = attachment.instance.xform - if not request.user.is_authenticated(): + if not request.user.is_authenticated: # This is not a DRF view, but we need to honor things like # `DigestAuthentication` (ODK Briefcase uses it!) and # `TokenAuthentication`. Let's try all the DRF authentication diff --git a/onadata/koboform/redirect_middleware.py b/onadata/koboform/redirect_middleware.py index 8846a42f2..fa14dd6fb 100644 --- a/onadata/koboform/redirect_middleware.py +++ b/onadata/koboform/redirect_middleware.py @@ -38,7 +38,7 @@ class ConditionalRedirects(object): def process_view(self, request, view, args, kwargs): view_name = view.__name__ - is_logged_in = request.user.is_authenticated() + is_logged_in = request.user.is_authenticated login_url = reverse('auth_login') if koboform.active and koboform.autoredirect: diff --git a/onadata/libs/filters.py b/onadata/libs/filters.py index c84c65998..fca129783 100644 --- a/onadata/libs/filters.py +++ b/onadata/libs/filters.py @@ -17,7 +17,7 @@ def filter_queryset(self, request, queryset, view): """ Anonymous user has no object permissions, return queryset as it is. """ - if request.user.is_anonymous(): + if request.user.is_anonymous: return queryset return super(AnonDjangoObjectPermissionFilter, self)\ diff --git a/onadata/libs/mixins/anonymous_user_mixin.py b/onadata/libs/mixins/anonymous_user_mixin.py index efd9d3fcd..6ca9ef37f 100644 --- a/onadata/libs/mixins/anonymous_user_mixin.py +++ b/onadata/libs/mixins/anonymous_user_mixin.py @@ -9,7 +9,7 @@ class AnonymousUserMixin(object): def get_queryset(self): """Set AnonymousUser from the database to allow object permissions.""" - if self.request and self.request.user.is_anonymous(): + if self.request and self.request.user.is_anonymous: self.request.user = get_object_or_404( User, pk=settings.ANONYMOUS_USER_ID) diff --git a/onadata/libs/mixins/anonymous_user_public_forms_mixin.py b/onadata/libs/mixins/anonymous_user_public_forms_mixin.py index 83cee603c..3605948c0 100644 --- a/onadata/libs/mixins/anonymous_user_public_forms_mixin.py +++ b/onadata/libs/mixins/anonymous_user_public_forms_mixin.py @@ -10,7 +10,7 @@ def _get_public_forms_queryset(self): def get_queryset(self): """Public forms only for anonymous Users.""" - if self.request and self.request.user.is_anonymous(): + if self.request and self.request.user.is_anonymous: return self._get_public_forms_queryset() return super(AnonymousUserPublicFormsMixin, self).get_queryset() diff --git a/onadata/libs/mixins/labels_mixin.py b/onadata/libs/mixins/labels_mixin.py index 18b8cc1ca..8f2ec04c1 100644 --- a/onadata/libs/mixins/labels_mixin.py +++ b/onadata/libs/mixins/labels_mixin.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals, print_function, division, absolute_import from django import forms from rest_framework import status -from rest_framework.decorators import detail_route +from rest_framework.decorators import action_route from rest_framework.response import Response from taggit.forms import TagField @@ -83,7 +83,7 @@ def process_label_request(request, label, instance): class LabelsMixin(object): - @detail_route(methods=['GET', 'POST', 'DELETE'], extra_lookup_fields=['label', ]) + @action_route(detail=True, methods=['GET', 'POST', 'DELETE'], extra_lookup_fields=['label', ]) def labels(self, request, format='json', **kwargs): xform = self.get_object() label = kwargs.get('label') diff --git a/onadata/libs/serializers/user_profile_serializer.py b/onadata/libs/serializers/user_profile_serializer.py index 377058cb9..8579e558d 100644 --- a/onadata/libs/serializers/user_profile_serializer.py +++ b/onadata/libs/serializers/user_profile_serializer.py @@ -117,7 +117,7 @@ def create(self, validated_data): new_user.save() created_by = self.context['request'].user - created_by = None if created_by.is_anonymous() else created_by + created_by = None if created_by.is_anonymous else created_by profile = UserProfile.objects.create( user=new_user, name=validated_data.get('name', ''), created_by=created_by, diff --git a/onadata/libs/utils/decorators.py b/onadata/libs/utils/decorators.py index a839608ba..788743b97 100644 --- a/onadata/libs/utils/decorators.py +++ b/onadata/libs/utils/decorators.py @@ -25,7 +25,7 @@ def is_owner(view_func): @wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view(request, *args, **kwargs): # assume username is first arg - if request.user.is_authenticated(): + if request.user.is_authenticated: if request.user.username == kwargs['username']: return view_func(request, *args, **kwargs) protocol = "https" if request.is_secure() else "http" diff --git a/onadata/libs/utils/logger_tools.py b/onadata/libs/utils/logger_tools.py index 3c360f3d1..7155d39eb 100644 --- a/onadata/libs/utils/logger_tools.py +++ b/onadata/libs/utils/logger_tools.py @@ -160,7 +160,7 @@ def _has_edit_xform_permission(xform, user): def check_edit_submission_permissions(request_user, xform): - if xform and request_user and request_user.is_authenticated(): + if xform and request_user and request_user.is_authenticated: requires_auth = UserProfile.objects.get_or_create( user=xform.user)[0].require_auth has_edit_perms = _has_edit_xform_permission(xform, request_user) @@ -291,7 +291,7 @@ def create_instance(username, xml_file, media_files, """ instance = None submitted_by = request.user \ - if request and request.user.is_authenticated() else None + if request and request.user.is_authenticated else None if username: username = username.lower() diff --git a/onadata/libs/utils/middleware.py b/onadata/libs/utils/middleware.py index 55acfb016..fa0c0212a 100644 --- a/onadata/libs/utils/middleware.py +++ b/onadata/libs/utils/middleware.py @@ -82,6 +82,6 @@ def process_response(self, request, response): user = request.user except AttributeError: return response - if user.is_authenticated(): + if user.is_authenticated: response['X-KoBoNaUt'] = request.user.username return response diff --git a/onadata/libs/utils/user_auth.py b/onadata/libs/utils/user_auth.py index a27253396..bb0ab00a8 100644 --- a/onadata/libs/utils/user_auth.py +++ b/onadata/libs/utils/user_auth.py @@ -108,7 +108,7 @@ def get_xform_and_perms(username, id_string, request): def helper_auth_helper(request): - if request.user and request.user.is_authenticated(): + if request.user and request.user.is_authenticated: return None # source, http://djangosnippets.org/snippets/243/ if 'HTTP_AUTHORIZATION' in request.META: diff --git a/onadata/settings/common.py b/onadata/settings/common.py index af826b534..2df5af3ab 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -218,8 +218,10 @@ ANONYMOUS_USER_ID = -1 INSTALLED_APPS = ( - 'django.contrib.auth', 'django.contrib.contenttypes', + # Always put `contenttypes` before `auth`; see + # https://code.djangoproject.com/ticket/10827 + 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', From 6a8e0a35fc120d3fa77f960e301c4ee6c3bdbfe7 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Wed, 22 Apr 2020 16:27:04 -0300 Subject: [PATCH 011/240] Add dummy Django KEY for pytest --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index d9e23aa78..0ec4f2d92 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,6 +2,7 @@ testpaths = onadata env = DJANGO_SETTINGS_MODULE=onadata.settings.test_environ + DJANGO_SECRET_KEY=bEW2NgYfhXdRnqkF7J9zPaZkqmTaKN filterwarnings = # ignore::django.utils.deprecation.RemovedInDjango110Warning # ignore::django.utils.deprecation.RemovedInDjango19Warning From ab28fa17f36c50532d0b977d811e855fad37ac69 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Wed, 22 Apr 2020 16:27:36 -0300 Subject: [PATCH 012/240] Fixed version of jsonfield --- requirements/py3.pip | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/py3.pip b/requirements/py3.pip index a788f71b8..93cbf37b0 100644 --- a/requirements/py3.pip +++ b/requirements/py3.pip @@ -91,3 +91,4 @@ django-guardian==1.5.1 django-timezone-field==3.0 django-celery-beat==1.6.0 django-oauth-toolkit==1.1.1 +jsonfield==2.1.1 From b62242895e29b0649ccc83461478da4a10645fdd Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Wed, 22 Apr 2020 18:26:45 -0300 Subject: [PATCH 013/240] Fixed compatibility issues for Python3, Django 1.11 and DRF 3.9 - typo `@action_route` instead of `@action` - Use djangorestframework_guardian package for DjangoObjectPermissionsFilter - Use `io.StringIO` instead of `StringIO.StringIO` - Moved redis session settings to prod settings --- onadata/apps/api/mongo_helper.py | 11 ++++---- onadata/apps/api/viewsets/briefcase_api.py | 6 ++--- onadata/apps/api/viewsets/connect_viewset.py | 4 +-- onadata/apps/api/viewsets/data_viewset.py | 8 +++--- .../apps/api/viewsets/user_profile_viewset.py | 4 +-- onadata/apps/api/viewsets/xform_list_api.py | 6 ++--- .../apps/api/viewsets/xform_submission_api.py | 4 +-- onadata/apps/api/viewsets/xform_viewset.py | 8 +++--- onadata/apps/logger/models/xform.py | 25 +++++++++--------- .../logger/tests/test_briefcase_client.py | 2 +- onadata/apps/logger/xform_instance_parser.py | 1 - onadata/apps/main/forms.py | 2 +- onadata/apps/sms_support/parser.py | 4 +-- .../apps/viewer/tests/test_export_builder.py | 1 - onadata/apps/viewer/tests/test_exports.py | 8 +++--- .../viewer/tests/test_pandas_mongo_bridge.py | 1 + onadata/apps/viewer/xls_writer.py | 2 +- onadata/koboform/pyxform_utils.py | 26 ++++++++++++------- onadata/libs/data/tests/test_tools.py | 2 +- onadata/libs/filters.py | 12 ++++----- onadata/libs/mixins/labels_mixin.py | 4 +-- onadata/libs/renderers/renderers.py | 14 +++++----- onadata/libs/tests/utils/test_csv_import.py | 5 ++-- onadata/libs/utils/briefcase_client.py | 2 +- onadata/libs/utils/csv_import.py | 16 +++++++----- onadata/libs/utils/export_tools.py | 14 ++++++++-- onadata/libs/utils/image_tools.py | 10 ++++--- onadata/libs/utils/logger_tools.py | 10 ++++--- onadata/libs/utils/qrcode.py | 5 ++-- onadata/libs/utils/string.py | 6 +++++ onadata/settings/common.py | 5 +--- onadata/settings/kc_environ.py | 5 +++- onadata/settings/test_environ.py | 2 +- pytest.ini | 4 +-- requirements/dev.pip | 6 ++++- requirements/py3.pip | 12 ++++++--- script/i18ntool.py | 2 +- 37 files changed, 151 insertions(+), 108 deletions(-) diff --git a/onadata/apps/api/mongo_helper.py b/onadata/apps/api/mongo_helper.py index 7b7a35712..1397b9ae4 100644 --- a/onadata/apps/api/mongo_helper.py +++ b/onadata/apps/api/mongo_helper.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals, print_function, division, absolute_import import re -import base64 + from onadata.libs.utils.common_tags import NESTED_RESERVED_ATTRIBUTES +from onadata.libs.utils.string import base64_encodestring class MongoHelper(object): @@ -10,12 +11,12 @@ class MongoHelper(object): KEY_WHITELIST = ['$or', '$and', '$exists', '$in', '$gt', '$gte', '$lt', '$lte', '$regex', '$options', '$all'] ENCODING_SUBSTITUTIONS = [ - (re.compile(r'^\$'), base64.encodestring('$').strip()), - (re.compile(r'\.'), base64.encodestring('.').strip()), + (re.compile(r'^\$'), base64_encodestring('$').strip()), + (re.compile(r'\.'), base64_encodestring('.').strip()), ] DECODING_SUBSTITUTIONS = [ - (re.compile(r'^' + base64.encodestring('$').strip()), '$'), - (re.compile(base64.encodestring('.').strip()), '.'), + (re.compile(r'^' + base64_encodestring('$').strip()), '$'), + (re.compile(base64_encodestring('.').strip()), '.'), ] @classmethod diff --git a/onadata/apps/api/viewsets/briefcase_api.py b/onadata/apps/api/viewsets/briefcase_api.py index 589d1b009..75582217a 100644 --- a/onadata/apps/api/viewsets/briefcase_api.py +++ b/onadata/apps/api/viewsets/briefcase_api.py @@ -16,7 +16,7 @@ from rest_framework.generics import get_object_or_404 from rest_framework.renderers import BrowsableAPIRenderer from rest_framework.response import Response -from rest_framework.decorators import action_route +from rest_framework.decorators import action from onadata.apps.api.tools import get_media_file_response from onadata.apps.api.permissions import ViewDjangoObjectPermissions @@ -235,7 +235,7 @@ def retrieve(self, request, *args, **kwargs): location=False), template_name='downloadSubmission.xml') - @action_route(detail=True, methods=['GET']) + @action(detail=True, methods=['GET']) def manifest(self, request, *args, **kwargs): self.object = self.get_object() object_list = MetaData.objects.filter(data_type='media', @@ -248,7 +248,7 @@ def manifest(self, request, *args, **kwargs): headers=self.get_openrosa_headers(request, location=False)) - @action_route(detail=True, methods=['GET']) + @action(detail=True, methods=['GET']) def media(self, request, *args, **kwargs): self.object = self.get_object() pk = kwargs.get('metadata') diff --git a/onadata/apps/api/viewsets/connect_viewset.py b/onadata/apps/api/viewsets/connect_viewset.py index 90ce223bf..55bfb12bb 100644 --- a/onadata/apps/api/viewsets/connect_viewset.py +++ b/onadata/apps/api/viewsets/connect_viewset.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals, print_function, division, absolute_import from rest_framework import status from rest_framework import viewsets -from rest_framework.decorators import action_route +from rest_framework.decorators import action from rest_framework.response import Response from onadata.apps.api.permissions import ConnectViewsetPermissions @@ -115,7 +115,7 @@ def list(self, request, *args, **kwargs): return Response(serializer.data) - @action_route(detail=False, methods=['POST']) + @action(detail=False, methods=['POST']) def reset(self, request, *args, **kwargs): context = {'request': request} data = request.data if request.data is not None else {} diff --git a/onadata/apps/api/viewsets/data_viewset.py b/onadata/apps/api/viewsets/data_viewset.py index 82fcffc1e..e2a8438ef 100644 --- a/onadata/apps/api/viewsets/data_viewset.py +++ b/onadata/apps/api/viewsets/data_viewset.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext as _ from rest_framework import status -from rest_framework.decorators import action_route +from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework.exceptions import ParseError, PermissionDenied @@ -510,7 +510,7 @@ def filter_queryset(self, queryset, view=None): return qs - @action_route(detail=True, methods=["GET", "PATCH", "DELETE"]) + @action(detail=True, methods=["GET", "PATCH", "DELETE"]) def validation_status(self, request, *args, **kwargs): """ View or modify validation status of specific instance. @@ -542,7 +542,7 @@ def validation_status(self, request, *args, **kwargs): return Response(data, status=http_status) - @action_route(detail=True, methods=['GET', 'POST', 'DELETE'], + @action(detail=True, methods=['GET', 'POST', 'DELETE'], extra_lookup_fields=['label', ]) def labels(self, request, *args, **kwargs): http_status = status.HTTP_400_BAD_REQUEST @@ -576,7 +576,7 @@ def labels(self, request, *args, **kwargs): return Response(data, status=http_status) - @action_route(detail=True, methods=['GET']) + @action(detail=True, methods=['GET']) def enketo(self, request, *args, **kwargs): self.object = self.get_object() data = {} diff --git a/onadata/apps/api/viewsets/user_profile_viewset.py b/onadata/apps/api/viewsets/user_profile_viewset.py index 310001ba2..1d3580dbf 100644 --- a/onadata/apps/api/viewsets/user_profile_viewset.py +++ b/onadata/apps/api/viewsets/user_profile_viewset.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.auth.models import User from rest_framework import status -from rest_framework.decorators import action_route +from rest_framework.decorators import action from rest_framework.exceptions import ParseError from rest_framework.generics import get_object_or_404 from rest_framework.response import Response @@ -162,7 +162,7 @@ def get_object(self): return obj - @action_route(detail=True, methods=['POST']) + @action(detail=True, methods=['POST']) def change_password(self, request, *args, **kwargs): user_profile = self.get_object() current_password = request.data.get('current_password', None) diff --git a/onadata/apps/api/viewsets/xform_list_api.py b/onadata/apps/api/viewsets/xform_list_api.py index c513b22a9..93a043afe 100644 --- a/onadata/apps/api/viewsets/xform_list_api.py +++ b/onadata/apps/api/viewsets/xform_list_api.py @@ -10,7 +10,7 @@ from rest_framework import viewsets from rest_framework import permissions from rest_framework.response import Response -from rest_framework.decorators import action_route +from rest_framework.decorators import action from onadata.apps.api.tools import get_media_file_response from onadata.apps.logger.models.xform import XForm @@ -110,7 +110,7 @@ def retrieve(self, request, *args, **kwargs): return Response(self.object.xml, headers=self.get_openrosa_headers()) - @action_route(detail=True, methods=['GET']) + @action(detail=True, methods=['GET']) def manifest(self, request, *args, **kwargs): self.object = self.get_object() object_list = MetaData.objects.filter(data_type='media', @@ -121,7 +121,7 @@ def manifest(self, request, *args, **kwargs): return Response(serializer.data, headers=self.get_openrosa_headers()) - @action_route(detail=True, methods=['GET']) + @action(detail=True, methods=['GET']) def media(self, request, *args, **kwargs): self.object = self.get_object() pk = kwargs.get('metadata') diff --git a/onadata/apps/api/viewsets/xform_submission_api.py b/onadata/apps/api/viewsets/xform_submission_api.py index 349de7e55..5d6bb4c3a 100644 --- a/onadata/apps/api/viewsets/xform_submission_api.py +++ b/onadata/apps/api/viewsets/xform_submission_api.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals, print_function, division, absolute_import import re -import StringIO +import io from django.conf import settings from django.contrib.auth.models import User @@ -75,7 +75,7 @@ def create_instance_from_json(username, request): submission_joined = dict_lists2strings(submission) xml_string = dict2xform(submission_joined, dict_form.get('id')) - xml_file = StringIO.StringIO(xml_string) + xml_file = io.StringIO(xml_string) return safe_create_instance(username, xml_file, [], None, request) diff --git a/onadata/apps/api/viewsets/xform_viewset.py b/onadata/apps/api/viewsets/xform_viewset.py index 029c6abe2..6819952a1 100644 --- a/onadata/apps/api/viewsets/xform_viewset.py +++ b/onadata/apps/api/viewsets/xform_viewset.py @@ -14,7 +14,7 @@ from django.utils.translation import ugettext as _ from rest_framework import exceptions from rest_framework import status -from rest_framework.decorators import action_route +from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.viewsets import ModelViewSet @@ -715,7 +715,7 @@ def update(self, request, pk, *args, **kwargs): # Let the superclass handle updates to the other fields return super(XFormViewSet, self).update(request, pk, *args, **kwargs) - @action_route(detail=True, methods=['GET']) + @action(detail=True, methods=['GET']) def form(self, request, format='json', **kwargs): form = self.get_object() if format not in ['json', 'xml', 'xls', 'csv']: @@ -729,7 +729,7 @@ def form(self, request, format='json', **kwargs): return response - @action_route(detail=True, methods=['GET']) + @action(detail=True, methods=['GET']) def enketo(self, request, **kwargs): self.object = self.get_object() form_url = _get_form_url(request, self.object.user.username) @@ -762,7 +762,7 @@ def retrieve(self, request, *args, **kwargs): query, export_type) - @action_route(detail=True, methods=['POST']) + @action(detail=True, methods=['POST']) def csv_import(self, request, *args, **kwargs): """ Endpoint for CSV data imports diff --git a/onadata/apps/logger/models/xform.py b/onadata/apps/logger/models/xform.py index 5543c5868..7f845c08b 100644 --- a/onadata/apps/logger/models/xform.py +++ b/onadata/apps/logger/models/xform.py @@ -1,11 +1,10 @@ # 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 io import StringIO, BytesIO from datetime import datetime from hashlib import md5 from xml.sax import saxutils @@ -19,7 +18,7 @@ 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.encoding import smart_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy, ugettext as _ from guardian.shortcuts import ( @@ -50,6 +49,7 @@ def upload_to(instance, filename): 'xls', os.path.split(filename)[1]) + @python_2_unicode_compatible class XForm(BaseModel): CLONED_SUFFIX = '_cloned' @@ -278,11 +278,11 @@ def public_forms(cls): return cls.objects.filter(shared=True) def _xls_file_io(self): - ''' + """ pulls the xls file from remote storage this should be used sparingly - ''' + """ file_path = self.xls.name default_storage = get_storage_class()() @@ -293,12 +293,11 @@ 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()) @@ -307,16 +306,18 @@ def to_kpi_content_schema(self): 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()() + """ + 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()) + xlsform_io = BytesIO(xlsform_file.read()) return xlsform_io else: return None diff --git a/onadata/apps/logger/tests/test_briefcase_client.py b/onadata/apps/logger/tests/test_briefcase_client.py index 4aff60ee0..8521f2435 100644 --- a/onadata/apps/logger/tests/test_briefcase_client.py +++ b/onadata/apps/logger/tests/test_briefcase_client.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals, print_function, division, absolute_import import os.path -from cStringIO import StringIO +from io import StringIO from urllib.parse import urljoin import requests diff --git a/onadata/apps/logger/xform_instance_parser.py b/onadata/apps/logger/xform_instance_parser.py index 81744154c..2e6157670 100644 --- a/onadata/apps/logger/xform_instance_parser.py +++ b/onadata/apps/logger/xform_instance_parser.py @@ -8,7 +8,6 @@ import dateutil.parser import six from django.utils.encoding import smart_str, python_2_unicode_compatible -from django.utils.encoding import from django.utils.translation import ugettext as _ from django.utils.six import text_type from xml.dom import minidom, Node diff --git a/onadata/apps/main/forms.py b/onadata/apps/main/forms.py index cf8fd1cb3..930a0e0ea 100644 --- a/onadata/apps/main/forms.py +++ b/onadata/apps/main/forms.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals, print_function, division, absolute_import import re -from StringIO import StringIO +from io import StringIO from urllib.request import urlopen from urllib.parse import urlparse diff --git a/onadata/apps/sms_support/parser.py b/onadata/apps/sms_support/parser.py index 6c4c978ab..a95d9c21f 100644 --- a/onadata/apps/sms_support/parser.py +++ b/onadata/apps/sms_support/parser.py @@ -1,7 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -import StringIO +import io import base64 import json import re @@ -353,7 +353,7 @@ def process_incoming(incoming, id_string): pass # process_incoming expectes submission to be a file-like object - xforms.append(StringIO.StringIO(xml_submission)) + xforms.append(io.StringIO(xml_submission)) medias.append(medias_submission) json_submissions.append(json_submission) xforms_notes.append(notes) diff --git a/onadata/apps/viewer/tests/test_export_builder.py b/onadata/apps/viewer/tests/test_export_builder.py index 55124f1a6..ead4df980 100644 --- a/onadata/apps/viewer/tests/test_export_builder.py +++ b/onadata/apps/viewer/tests/test_export_builder.py @@ -391,7 +391,6 @@ def test_zipped_csv_export_works_with_unicode(self): rows = [row for row in reader] # TODO VALIDATE After UPGRADE TO PYTHON3 - 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])) diff --git a/onadata/apps/viewer/tests/test_exports.py b/onadata/apps/viewer/tests/test_exports.py index 05b18879e..fe8aede31 100644 --- a/onadata/apps/viewer/tests/test_exports.py +++ b/onadata/apps/viewer/tests/test_exports.py @@ -1,10 +1,11 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import csv import datetime import json import os -import StringIO +import io import unittest from time import sleep @@ -21,7 +22,6 @@ 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 @@ -475,7 +475,7 @@ def test_deleted_submission_not_in_export(self): "id_string": self.xform.id_string}) response = self.client.get(csv_export_url) self.assertEqual(response.status_code, 200) - f = StringIO.StringIO(self._get_response_content(response)) + f = io.StringIO(self._get_response_content(response)) csv_reader = csv.reader(f) num_rows = len([row for row in csv_reader]) f.close() @@ -508,7 +508,7 @@ def test_edited_submissions_in_exports(self): "id_string": self.xform.id_string}) response = self.client.get(csv_export_url) self.assertEqual(response.status_code, 200) - f = StringIO.StringIO(self._get_response_content(response)) + f = io.StringIO(self._get_response_content(response)) csv_reader = csv.DictReader(f) data = [row for row in csv_reader] f.close() diff --git a/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py b/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py index fb8049157..37ba71a3f 100644 --- a/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py +++ b/onadata/apps/viewer/tests/test_pandas_mongo_bridge.py @@ -1,5 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + import csv import os from tempfile import NamedTemporaryFile diff --git a/onadata/apps/viewer/xls_writer.py b/onadata/apps/viewer/xls_writer.py index 0de291322..fa37f1d7d 100644 --- a/onadata/apps/viewer/xls_writer.py +++ b/onadata/apps/viewer/xls_writer.py @@ -25,7 +25,7 @@ def set_file(self, file_object=None): if file_object is not None: self._file = file_object else: - from StringIO import StringIO + from io import StringIO self._file = StringIO() def reset_workbook(self): diff --git a/onadata/koboform/pyxform_utils.py b/onadata/koboform/pyxform_utils.py index 0f82e0a61..323ad5e36 100644 --- a/onadata/koboform/pyxform_utils.py +++ b/onadata/koboform/pyxform_utils.py @@ -1,9 +1,11 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from pyxform import xls2json_backends -import xlwt + +import io import re -import StringIO +import xlwt + +from pyxform import xls2json_backends def convert_csv_to_xls(csv_repr): @@ -17,6 +19,7 @@ def convert_csv_to_xls(csv_repr): csv_repr = ''.join([ line for line in csv_repr.splitlines(True) if line.strip().strip('"') ]) + def _add_contents_to_sheet(sheet, contents): cols = [] for row in contents: @@ -30,16 +33,19 @@ def _add_contents_to_sheet(sheet, contents): val = row.get(col, None) if val: sheet.write(ri+1, ci, val) - encoded_csv = csv_repr.decode("utf-8").encode("utf-8") - dict_repr = xls2json_backends.csv_to_dict(StringIO.StringIO(encoded_csv)) + + encoded_csv = io.BytesIO(csv_repr) + dict_repr = xls2json_backends.csv_to_dict(encoded_csv) + workbook = xlwt.Workbook() for sheet_name in dict_repr.keys(): # pyxform.xls2json_backends adds "_header" items for each sheet..... if not re.match(r".*_header$", sheet_name): cur_sheet = workbook.add_sheet(sheet_name) _add_contents_to_sheet(cur_sheet, dict_repr[sheet_name]) - # TODO: As XLS files are binary, I believe this should be `io.BytesIO()`. - string_io = StringIO.StringIO() - workbook.save(string_io) - string_io.seek(0) - return string_io + + bytes_io = io.BytesIO() + workbook.save(bytes_io) + bytes_io.seek(0) + return bytes_io + diff --git a/onadata/libs/data/tests/test_tools.py b/onadata/libs/data/tests/test_tools.py index eacaef341..c30bc8a83 100644 --- a/onadata/libs/data/tests/test_tools.py +++ b/onadata/libs/data/tests/test_tools.py @@ -38,7 +38,7 @@ def test_get_form_submissions_grouped_by_field(self, mock_time): @patch('onadata.apps.logger.models.instance.submission_time') def test_get_form_submissions_grouped_by_field_datetime_to_date( self, mock_time): - now = datetime(2014, 01, 01) + now = datetime(2014, 1, 1) times = [now, now + timedelta(seconds=1), now + timedelta(seconds=2), now + timedelta(seconds=3)] mock_time.side_effect = times diff --git a/onadata/libs/filters.py b/onadata/libs/filters.py index fca129783..ca915f41b 100644 --- a/onadata/libs/filters.py +++ b/onadata/libs/filters.py @@ -1,18 +1,16 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from django.db.models import Q + from django.shortcuts import get_object_or_404 -from django.core.exceptions import ObjectDoesNotExist -from django.http import Http404 from django.utils import six from rest_framework import filters +from rest_framework_guardian import filters as guardian_filters from rest_framework.exceptions import ParseError from onadata.apps.logger.models import XForm, Instance -from onadata.apps.main.models import MetaData -class AnonDjangoObjectPermissionFilter(filters.DjangoObjectPermissionsFilter): +class AnonDjangoObjectPermissionFilter(guardian_filters.ObjectPermissionsFilter): def filter_queryset(self, request, queryset, view): """ Anonymous user has no object permissions, return queryset as it is. @@ -88,7 +86,7 @@ def _xform_filter_queryset(self, request, queryset, view, keyword): class MetaDataFilter(XFormPermissionFilterMixin, - filters.DjangoObjectPermissionsFilter): + guardian_filters.ObjectPermissionsFilter): def filter_queryset(self, request, queryset, view): queryset = self._xform_filter_queryset(request, queryset, view, 'xform') data_type = request.query_params.get('data_type') @@ -98,7 +96,7 @@ def filter_queryset(self, request, queryset, view): class AttachmentFilter(XFormPermissionFilterMixin, - filters.DjangoObjectPermissionsFilter): + guardian_filters.ObjectPermissionsFilter): def filter_queryset(self, request, queryset, view): queryset = self._xform_filter_queryset(request, queryset, view, 'instance__xform') diff --git a/onadata/libs/mixins/labels_mixin.py b/onadata/libs/mixins/labels_mixin.py index 8f2ec04c1..e0b05da63 100644 --- a/onadata/libs/mixins/labels_mixin.py +++ b/onadata/libs/mixins/labels_mixin.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals, print_function, division, absolute_import from django import forms from rest_framework import status -from rest_framework.decorators import action_route +from rest_framework.decorators import action from rest_framework.response import Response from taggit.forms import TagField @@ -83,7 +83,7 @@ def process_label_request(request, label, instance): class LabelsMixin(object): - @action_route(detail=True, methods=['GET', 'POST', 'DELETE'], extra_lookup_fields=['label', ]) + @action(detail=True, methods=['GET', 'POST', 'DELETE'], extra_lookup_fields=['label', ]) def labels(self, request, format='json', **kwargs): xform = self.get_object() label = kwargs.get('label') diff --git a/onadata/libs/renderers/renderers.py b/onadata/libs/renderers/renderers.py index c0593c016..29a9153ed 100644 --- a/onadata/libs/renderers/renderers.py +++ b/onadata/libs/renderers/renderers.py @@ -1,11 +1,12 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from rest_framework import negotiation -from django.utils.xmlutils import SimplerXMLGenerator +import io + +from django.utils.xmlutils import SimplerXMLGenerator from django.utils import six -from django.utils.six.moves import StringIO from django.utils.encoding import smart_text +from rest_framework.negotiation import DefaultContentNegotiation from rest_framework.renderers import BaseRenderer from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.renderers import StaticHTMLRenderer @@ -55,7 +56,8 @@ def render(self, data, accepted_media_type=None, renderer_context=None): return data -class MediaFileContentNegotiation(negotiation.DefaultContentNegotiation): +class MediaFileContentNegotiation(DefaultContentNegotiation): + def filter_renderers(self, renderers, format): """ If there is a '.json' style format suffix, filter the renderers @@ -101,7 +103,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): elif isinstance(data, six.string_types): return data - stream = StringIO() + stream = io.StringIO() xml = SimplerXMLGenerator(stream, self.charset) xml.startDocument() @@ -161,7 +163,7 @@ class StaticXMLRenderer(StaticHTMLRenderer): media_type = 'text/xml' -class InstanceContentNegotiation(negotiation.DefaultContentNegotiation): +class InstanceContentNegotiation(DefaultContentNegotiation): def filter_renderers(self, renderers, format): """ diff --git a/onadata/libs/tests/utils/test_csv_import.py b/onadata/libs/tests/utils/test_csv_import.py index 196de9378..4e0118ba0 100644 --- a/onadata/libs/tests/utils/test_csv_import.py +++ b/onadata/libs/tests/utils/test_csv_import.py @@ -1,8 +1,9 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -# import mock + import os -from cStringIO import StringIO +from io import StringIO + from django.conf import settings from onadata.libs.utils import csv_import from onadata.apps.logger.models import XForm diff --git a/onadata/libs/utils/briefcase_client.py b/onadata/libs/utils/briefcase_client.py index 7c63af9f9..750f0de0e 100644 --- a/onadata/libs/utils/briefcase_client.py +++ b/onadata/libs/utils/briefcase_client.py @@ -6,7 +6,7 @@ import mimetypes import os import time -from cStringIO import StringIO +from io import StringIO from urllib.parse import urljoin from xml.parsers.expat import ExpatError diff --git a/onadata/libs/utils/csv_import.py b/onadata/libs/utils/csv_import.py index d0a555aa0..07c147e95 100644 --- a/onadata/libs/utils/csv_import.py +++ b/onadata/libs/utils/csv_import.py @@ -1,14 +1,16 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -import unicodecsv as ucsv -import uuid -import json -import cStringIO +import json +import uuid from datetime import datetime + +import io +import unicodecsv as ucsv from django.contrib.auth.models import User -from onadata.libs.utils.logger_tools import dict2xml, safe_create_instance + from onadata.apps.logger.models import Instance +from onadata.libs.utils.logger_tools import dict2xml, safe_create_instance def get_submission_meta_dict(xform, instance_id): @@ -71,7 +73,7 @@ def submit_csv(username, xform, csv_file): :rtype: Dict """ if isinstance(csv_file, (str, unicode)): - csv_file = cStringIO.StringIO(csv_file) + csv_file = io.StringIO(csv_file) elif csv_file is None or not hasattr(csv_file, 'read'): return {'error': ('Invalid param type for `csv_file`. ' 'Expected file or String ' @@ -111,7 +113,7 @@ def submit_csv(username, xform, csv_file): row_uuid = row.get('meta').get('instanceID') rollback_uuids.append(row_uuid.replace('uuid:', '')) - xml_file = cStringIO.StringIO(dict2xmlsubmission(row, xform, row_uuid, + xml_file = io.StringIO(dict2xmlsubmission(row, xform, row_uuid, submission_date)) try: diff --git a/onadata/libs/utils/export_tools.py b/onadata/libs/utils/export_tools.py index 9faf007fd..534695e06 100644 --- a/onadata/libs/utils/export_tools.py +++ b/onadata/libs/utils/export_tools.py @@ -18,7 +18,7 @@ from django.contrib.auth.models import User from django.shortcuts import render_to_response from django.utils.text import slugify -from openpyxl.date_time import SharedDate +from openpyxl.utils.datetime import to_excel, time_to_days, timedelta_to_days from openpyxl.workbook import Workbook from pyxform.question import Question from pyxform.section import Section, RepeatingSection @@ -209,7 +209,17 @@ class ExportBuilder(object): def string_to_date_with_xls_validation(cls, date_str): date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() try: - SharedDate().datetime_to_julian(date_obj) + # SharedDate().datetime_to_julian(date_obj) + # Copy code from v2.0.5. Could not find where SharedDate is in + # latest version of openpyxl (and if it's useful) + if isinstance(date, datetime.datetime): + to_excel(date) + elif isinstance(date, datetime.date): + to_excel(date) + elif isinstance(date, datetime.time): + time_to_days(date) + elif isinstance(date, datetime.timedelta): + timedelta_to_days(date) except ValueError: return date_str else: diff --git a/onadata/libs/utils/image_tools.py b/onadata/libs/utils/image_tools.py index ff5033c76..a014cd2ad 100644 --- a/onadata/libs/utils/image_tools.py +++ b/onadata/libs/utils/image_tools.py @@ -1,7 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from cStringIO import StringIO +from io import StringIO from tempfile import NamedTemporaryFile from PIL import Image @@ -14,15 +14,17 @@ def flat(*nums): - '''Build a tuple of ints from float or integer arguments. + """ + Build a tuple of ints from float or integer arguments. Useful because PIL crop and resize require integer points. source: https://gist.github.com/16a01455 - ''' + """ return tuple(int(round(n)) for n in nums) -def get_dimensions((width, height), longest_side): +def get_dimensions(size_, longest_side): + width, height = size_ if width > height: height = (height / width) * longest_side width = longest_side diff --git a/onadata/libs/utils/logger_tools.py b/onadata/libs/utils/logger_tools.py index 7155d39eb..61404e9d4 100644 --- a/onadata/libs/utils/logger_tools.py +++ b/onadata/libs/utils/logger_tools.py @@ -16,12 +16,15 @@ from django.core.exceptions import ValidationError, PermissionDenied from django.core.files.storage import get_storage_class from django.core.mail import mail_admins -from django.core.servers.basehttp import FileWrapper from django.db import IntegrityError, transaction from django.db.models import Q from django.db.models.signals import pre_delete -from django.http import HttpResponse, HttpResponseNotFound, \ - StreamingHttpResponse, Http404 +from django.http import ( + HttpResponse, + HttpResponseNotFound, + StreamingHttpResponse, + Http404 +) from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.encoding import DjangoUnicodeDecodeError @@ -31,6 +34,7 @@ from pyxform.errors import PyXFormError from pyxform.xform2json import create_survey_element_from_xml from xml.dom import Node +from wsgiref.util import FileWrapper from onadata.apps.logger.exceptions import FormInactiveError, DuplicateUUIDError from onadata.apps.logger.models import Attachment diff --git a/onadata/libs/utils/qrcode.py b/onadata/libs/utils/qrcode.py index 91e39da92..788e5f649 100644 --- a/onadata/libs/utils/qrcode.py +++ b/onadata/libs/utils/qrcode.py @@ -1,6 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -import StringIO + +import io from elaphe import barcode from base64 import b64encode @@ -10,7 +11,7 @@ def generate_qrcode(message, stream=None, data_mode='8bits', format='PNG', scale=2.5): """Generate a QRCode, settings options and output.""" if stream is None: - stream = StringIO.StringIO() + stream = io.StringIO() img = barcode('qrcode', message, options=dict(version=9, eclevel=eclevel), diff --git a/onadata/libs/utils/string.py b/onadata/libs/utils/string.py index 4b8546564..0c1550d75 100644 --- a/onadata/libs/utils/string.py +++ b/onadata/libs/utils/string.py @@ -1,9 +1,15 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import +import base64 + from django.utils.six import string_types def str2bool(v): return v.lower() in ( 'yes', 'true', 't', '1') if isinstance(v, string_types) else v + + +def base64_encodestring(obj): + return base64.encodebytes(obj.encode()).decode() diff --git a/onadata/settings/common.py b/onadata/settings/common.py index 2df5af3ab..11aa1ea2e 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -276,7 +276,7 @@ ], 'DEFAULT_AUTHENTICATION_CLASSES': ( 'onadata.libs.authentication.DigestAuthentication', - 'oauth2_provider.ext.rest_framework.OAuth2Authentication', + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', 'onadata.libs.authentication.HttpsOnlyBasicAuthentication', @@ -565,9 +565,6 @@ def skip_suspicious_operations(record): if os.getenv("USE_X_FORWARDED_HOST", "False") == "True": USE_X_FORWARDED_HOST = True -SESSION_ENGINE = "redis_sessions.session" -SESSION_REDIS = RedisHelper.config(default="redis://redis_cache:6380/2") - # "Although the setting offers little practical benefit, it's sometimes # required by security auditors." # -- https://docs.djangoproject.com/en/2.2/ref/settings/#csrf-cookie-httponly diff --git a/onadata/settings/kc_environ.py b/onadata/settings/kc_environ.py index f5e4bbc14..85cb791df 100644 --- a/onadata/settings/kc_environ.py +++ b/onadata/settings/kc_environ.py @@ -300,5 +300,8 @@ def celery_logger_setup_handler(logger, **kwargs): }, 'options': {'queue': 'kobocat_queue'} } +# ##### END ISSUE 242 FIX ###### + +SESSION_ENGINE = "redis_sessions.session" +SESSION_REDIS = RedisHelper.config(default="redis://redis_cache:6380/2") -###### END ISSUE 242 FIX ###### diff --git a/onadata/settings/test_environ.py b/onadata/settings/test_environ.py index 6ad993884..8141dee91 100644 --- a/onadata/settings/test_environ.py +++ b/onadata/settings/test_environ.py @@ -20,7 +20,7 @@ # Moreover, `apt-get update && apt-get install libsqlite3-mod-spatialite` # should be executed inside the container DATABASES['default']['ENGINE'] = "django.contrib.gis.db.backends.spatialite" -SPATIALITE_LIBRARY_PATH = 'mod_spatialite' +SPATIALITE_LIBRARY_PATH = os.environ.get('SPATIALITE_LIBRARY_PATH', 'mod_spatialite') MONGO_DATABASE = { 'HOST': os.environ.get('KOBOCAT_MONGO_HOST', 'mongo'), diff --git a/pytest.ini b/pytest.ini index 0ec4f2d92..9afb0979a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,10 +3,10 @@ testpaths = onadata env = DJANGO_SETTINGS_MODULE=onadata.settings.test_environ DJANGO_SECRET_KEY=bEW2NgYfhXdRnqkF7J9zPaZkqmTaKN -filterwarnings = +# filterwarnings = # ignore::django.utils.deprecation.RemovedInDjango110Warning # ignore::django.utils.deprecation.RemovedInDjango19Warning # ignore::exceptions.DeprecationWarning # ignore::exceptions.PendingDeprecationWarning - ignore::exceptions.RuntimeWarning + #ignore::exceptions.RuntimeWarning diff --git a/requirements/dev.pip b/requirements/dev.pip index 2d93ea10b..99329a739 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -11,8 +11,12 @@ shell_command #twill Werkzeug sqlparse -pytest==3.10 +pytest pytest-django pytest-env mongomock mock + + +#python 3 +httmock diff --git a/requirements/py3.pip b/requirements/py3.pip index 93cbf37b0..9da62a5a9 100644 --- a/requirements/py3.pip +++ b/requirements/py3.pip @@ -4,7 +4,7 @@ dj-database-url # django-guardian django-registration-redux django-templated-email -gdata +gdata-python3 httplib2 modilabs-python-utils Pillow @@ -33,7 +33,7 @@ raven # new export code relies on pandas>=0.12.0 -elaphe +elaphe3 # request # formpack exports @@ -56,7 +56,9 @@ Markdown django-filter # captcha -recaptcha-client==1.0.6 +# recaptcha-client==1.0.6 +-e git+https://github.com/dresiu/recaptcha-client-1.0.6-py3@48078f8131e2f3c2054d2099ef48cfe9a5745d0c#egg=recaptcha-client + # unicodecsv==0.9.4 dpath @@ -92,3 +94,7 @@ django-timezone-field==3.0 django-celery-beat==1.6.0 django-oauth-toolkit==1.1.1 jsonfield==2.1.1 + +# Added packages +simplejson +djangorestframework-guardian diff --git a/script/i18ntool.py b/script/i18ntool.py index 529010cd7..69cfd1701 100755 --- a/script/i18ntool.py +++ b/script/i18ntool.py @@ -15,7 +15,7 @@ import os import sys -import StringIO +import io import tempfile import types import shutil From 465bf7ada5296724ff649b1853a1d1bce0471ba8 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Thu, 23 Apr 2020 15:12:32 -0300 Subject: [PATCH 014/240] Django 1.11 support: changed deprecated code for urls --- onadata/apps/api/urls.py | 33 ++++++++++++++------- onadata/apps/export/urls.py | 33 ++++++++++----------- onadata/apps/logger/admin.py | 1 + onadata/apps/main/registration_urls.py | 10 +++---- onadata/apps/main/urls.py | 29 +++++++++--------- onadata/apps/survey_report/urls.py | 31 ++++++++++++------- onadata/libs/mixins/anonymous_user_mixin.py | 5 +++- onadata/libs/utils/logger_tools.py | 9 +++--- 8 files changed, 86 insertions(+), 65 deletions(-) diff --git a/onadata/apps/api/urls.py b/onadata/apps/api/urls.py index a0301dfd0..86d1f7049 100644 --- a/onadata/apps/api/urls.py +++ b/onadata/apps/api/urls.py @@ -34,12 +34,15 @@ def __init__(self, *args, **kwargs): 'delete': 'destroy' }, name='{basename}-detail', + detail=True, initkwargs={'suffix': 'Instance'} )) self.lookups_routes.append(self.make_routes('lookup')) self.lookups_routes.append(self.make_routes('lookups')) # Dynamically generated routes. # Generated using @action or @link decorators on methods of the viewset + + # FIXME find out with what value `detail` should be set self.lookups_routes.append(routers.Route( url=[ r'^{prefix}/{lookups}/{methodname}{trailing_slash}$', @@ -48,6 +51,7 @@ def __init__(self, *args, **kwargs): '{httpmethod}': '{methodname}', }, name='{basename}-{methodnamehyphen}', + detail=True, initkwargs={} )) @@ -60,11 +64,11 @@ def make_routes(template_text): 'post': 'create' }, name='{basename}-list', + detail=False, initkwargs={'suffix': 'List'}) def get_extra_lookup_regexes(self, route): - ret = [] base_regex = '(?P<{lookup_field}>[^/]+)' if 'extra_lookup_fields' in route.initkwargs: @@ -75,7 +79,6 @@ def get_extra_lookup_regexes(self, route): def get_lookup_regexes(self, viewset): ret = [] - lookup_fields = getattr(viewset, 'lookup_fields', None) if lookup_fields: for i in range(1, len(lookup_fields)): @@ -112,13 +115,20 @@ def get_lookup_routes(self, viewset): if 'extra_lookup_fields' in initkwargs: uri = route.url[1] uri = routers.replace_methodname(uri, methodname) + + # FIXME routers.Route() expects `detail` parameter ret.append(routers.Route( - url=uri, mapping=mapping, name='%s-extra' % name, + url=uri, + mapping=mapping, + name='%s-extra' % name, initkwargs=initkwargs, )) uri = routers.replace_methodname(route.url[0], methodname) + # FIXME routers.Route() expects `detail` parameter ret.append(routers.Route( - url=uri, mapping=mapping, name=name, + url=uri, + mapping=mapping, + name=name, initkwargs=initkwargs, )) else: @@ -365,6 +375,7 @@ def make_routes(template_text): 'delete': 'bulk_delete' }, name='{basename}-list', + detail=False, initkwargs={'suffix': 'List'}) @@ -373,13 +384,13 @@ def make_routes(template_text): router.register(r'user', ConnectViewSet) router.register(r'profiles', UserProfileViewSet) router.register(r'forms', XFormViewSet) -router.register(r'notes', NoteViewSet, base_name='notes') -router.register(r'metadata', MetaDataViewSet, base_name='metadata') -router.register(r'media', AttachmentViewSet, base_name='attachment') -router.register(r'formlist', XFormListApi, base_name='formlist') -router.register(r'submissions', XFormSubmissionApi, base_name='submissions') -router.register(r'briefcase', BriefcaseApi, base_name='briefcase') +router.register(r'notes', NoteViewSet, basename='notes') +router.register(r'metadata', MetaDataViewSet, basename='metadata') +router.register(r'media', AttachmentViewSet, basename='attachment') +router.register(r'formlist', XFormListApi, basename='formlist') +router.register(r'submissions', XFormSubmissionApi, basename='submissions') +router.register(r'briefcase', BriefcaseApi, basename='briefcase') router_with_patch_list = MultiLookupRouterWithPatchList(trailing_slash=False) -router_with_patch_list.register(r'data', DataViewSet, base_name='data') +router_with_patch_list.register(r'data', DataViewSet, basename='data') diff --git a/onadata/apps/export/urls.py b/onadata/apps/export/urls.py index 5938b71bb..ee2bd0830 100644 --- a/onadata/apps/export/urls.py +++ b/onadata/apps/export/urls.py @@ -5,21 +5,20 @@ # EVERY SINGLE ENDPOINT 500s EXCEPT export_menu # ################################################# -from django.conf.urls import patterns, url +from django.conf.urls import 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') -) +# urlpatterns = [ +# '', +# 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/logger/admin.py b/onadata/apps/logger/admin.py index 50f7d25c9..f08688c49 100644 --- a/onadata/apps/logger/admin.py +++ b/onadata/apps/logger/admin.py @@ -18,4 +18,5 @@ def get_queryset(self, request): return qs return qs.filter(user=request.user) + admin.site.register(XForm, FormAdmin) diff --git a/onadata/apps/main/registration_urls.py b/onadata/apps/main/registration_urls.py index daf2b13bb..5ef3cb019 100644 --- a/onadata/apps/main/registration_urls.py +++ b/onadata/apps/main/registration_urls.py @@ -1,7 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from django.conf.urls import patterns, url, include +from django.conf.urls import url, include from django.views.generic import TemplateView from registration.backends.default.views import ActivationView @@ -17,8 +17,8 @@ """ -urlpatterns = patterns( - '', +urlpatterns = [ + # '', url(r'^activate/complete/$', TemplateView.as_view( template_name='registration/activation_complete.html'), @@ -37,5 +37,5 @@ TemplateView.as_view( template_name='registration/registration_complete.html'), name='registration_complete'), - (r'', include('registration.auth_urls')), -) + url(r'', include('registration.auth_urls')), +] diff --git a/onadata/apps/main/urls.py b/onadata/apps/main/urls.py index 4890e5eb6..b636442fa 100644 --- a/onadata/apps/main/urls.py +++ b/onadata/apps/main/urls.py @@ -1,12 +1,14 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.conf import settings from django.contrib import admin +from django.contrib.staticfiles.views import serve from django.views.generic import RedirectView from django.views.i18n import JavaScriptCatalog + from onadata.apps.api.urls import router, router_with_patch_list from onadata.apps.api.urls import XFormListApi from onadata.apps.api.urls import XFormSubmissionApi @@ -111,10 +113,10 @@ admin.autodiscover() -urlpatterns = patterns( - '', +urlpatterns = [ + # url('') # Same as `url(r'^$', home)`? # change Language - (r'^i18n/', include('django.conf.urls.i18n')), + url(r'^i18n/', include('django.conf.urls.i18n')), url('^api/v1/', include(router.urls)), url('^api/v1/', include(router_with_patch_list.urls)), url(r'^service_health/$', service_health), @@ -263,18 +265,17 @@ url(r"^(?P\w+)/forms/(?P[^/]+)/view-data", data_view), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/new$", create_export), + r"/new$", create_export), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/delete$", delete_export), + r"/delete$", delete_export), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/progress$", export_progress), + r"/progress$", export_progress), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/$", export_list), + r"/$", export_list), url(r"^(?P\w+)/exports/(?P[^/]+)/(?P\w+)" - "/(?P[^/]+)$", + r"/(?P[^/]+)$", export_download), - url(r'^(?P\w+)/exports/', include('onadata.apps.export.urls')), - + #url(r'^(?P\w+)/exports/', include('onadata.apps.export.urls')), url(r'^(?P\w+)/reports/', include('onadata.apps.survey_report.urls')), # odk data urls @@ -351,6 +352,7 @@ # Media are now served by NginX. # url(r'^media/(?P.*)$', 'django.views.static.serve', # {'document_root': settings.MEDIA_ROOT}), + url(r'^static/(?P.*)$', serve), url(r'^favicon\.ico', RedirectView.as_view(url='/static/images/favicon.ico')), @@ -361,7 +363,4 @@ superuser_stats), url(r"^(?P[^/]+)/superuser_stats/(?P[^/]+)$", retrieve_superuser_stats), -) - -urlpatterns += patterns('django.contrib.staticfiles.views', - url(r'^static/(?P.*)$', 'serve')) +] diff --git a/onadata/apps/survey_report/urls.py b/onadata/apps/survey_report/urls.py index 600711f5c..28036e907 100644 --- a/onadata/apps/survey_report/urls.py +++ b/onadata/apps/survey_report/urls.py @@ -1,32 +1,41 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import -from django.conf.urls import patterns, url +from django.conf.urls import url +from onadata.apps.survey_report.views import ( + export_menu, + csv_export, + xlsx_export, + html_export, + view_one_submission, + autoreport_menu, + auto_report +) -urlpatterns = patterns( - '', +urlpatterns = [ + # url('', name='formpack_home'), url(r"(?P[^/]+)/export/$", - 'onadata.apps.survey_report.views.export_menu', + export_menu, name='formpack_export_menu'), url(r"(?P[^/]+)/export.csv$", - 'onadata.apps.survey_report.views.csv_export', + csv_export, name='formpack_csv_export'), url(r"(?P[^/]+)/export.xlsx$", - 'onadata.apps.survey_report.views.xlsx_export', + xlsx_export, name='formpack_xlsx_export'), url(r"(?P[^/]+)/export.html$", - 'onadata.apps.survey_report.views.html_export', + html_export, name='formpack_html_export'), url(r"(?P[^/]+)/submission/(?P\d+).html$", - 'onadata.apps.survey_report.views.view_one_submission', + view_one_submission, name='formpack_one_submission'), url(r"(?P[^/]+)/digest/$", - 'onadata.apps.survey_report.views.autoreport_menu', + autoreport_menu, name='formpack_autoreport_menu'), url(r"(?P[^/]+)/digest.html$", - 'onadata.apps.survey_report.views.auto_report', + auto_report, name='formpack_auto_report') -) +] diff --git a/onadata/libs/mixins/anonymous_user_mixin.py b/onadata/libs/mixins/anonymous_user_mixin.py index 6ca9ef37f..03c2ef9d2 100644 --- a/onadata/libs/mixins/anonymous_user_mixin.py +++ b/onadata/libs/mixins/anonymous_user_mixin.py @@ -1,5 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + from django.conf import settings from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 @@ -8,7 +9,9 @@ class AnonymousUserMixin(object): def get_queryset(self): - """Set AnonymousUser from the database to allow object permissions.""" + """ + Set AnonymousUser from the database to allow object permissions. + """ if self.request and self.request.user.is_anonymous: self.request.user = get_object_or_404( User, pk=settings.ANONYMOUS_USER_ID) diff --git a/onadata/libs/utils/logger_tools.py b/onadata/libs/utils/logger_tools.py index 61404e9d4..ab148e3c9 100644 --- a/onadata/libs/utils/logger_tools.py +++ b/onadata/libs/utils/logger_tools.py @@ -28,7 +28,6 @@ from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.encoding import DjangoUnicodeDecodeError -from django.utils.six import text_type from django.utils.translation import ugettext as _ from modilabs.utils.subprocess_timeout import ProcessTimedOut from pyxform.errors import PyXFormError @@ -462,7 +461,7 @@ def publish_form(callback): except (PyXFormError, XLSFormError) as e: return { 'type': 'alert-error', - 'text': text_type(e) + 'text': str(e) } except IntegrityError as e: return { @@ -479,7 +478,7 @@ def publish_form(callback): # form.publish returned None, not sure why... return { 'type': 'alert-error', - 'text': text_type(e) + 'text': str(e) } except ProcessTimedOut as e: # catch timeout errors @@ -494,13 +493,13 @@ def publish_form(callback): # ODK validation errors are vanilla errors and it masks a lot of regular # errors if we try to catch it so let's catch it, BUT reraise it # if we don't see typical ODK validation error messages in it. - if "ODK Validate Errors" not in e.message: + if "ODK Validate Errors" not in str(e): raise # error in the XLS file; show an error to the user return { 'type': 'alert-error', - 'text': text_type(e) + 'text': str(e) } From a38dd6e2f9dcadac142640cb6463e8e2f28ba90b Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Thu, 23 Apr 2020 15:14:38 -0300 Subject: [PATCH 015/240] Force AnonymousUser() FK to be settings.ANONYMOUS_USER_ID when running tests --- onadata/apps/main/models/user_profile.py | 12 ++++++++++++ onadata/settings/test_environ.py | 2 ++ 2 files changed, 14 insertions(+) diff --git a/onadata/apps/main/models/user_profile.py b/onadata/apps/main/models/user_profile.py index 3023c4b48..6ce4be357 100644 --- a/onadata/apps/main/models/user_profile.py +++ b/onadata/apps/main/models/user_profile.py @@ -1,6 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import +import datetime + from django.conf import settings from django.contrib.auth.models import User from django.db import models @@ -101,3 +103,13 @@ def default_user_profile_require_auth( post_save.connect(default_user_profile_require_auth, sender=UserProfile, dispatch_uid='default_user_profile_require_auth') + + +def get_anonymous_user_instance(User): + """ + Force `AnonymousUser` to be saved with `pk` == `ANONYMOUS_USER_ID` + :param User: User class + :return: User instance + """ + return User(pk=settings.ANONYMOUS_USER_ID, + username='AnonymousUser') diff --git a/onadata/settings/test_environ.py b/onadata/settings/test_environ.py index 8141dee91..a21109067 100644 --- a/onadata/settings/test_environ.py +++ b/onadata/settings/test_environ.py @@ -164,3 +164,5 @@ 'level': 'WARNING', 'propagate': True } + +GUARDIAN_GET_INIT_ANONYMOUS_USER = 'onadata.apps.main.models.user_profile.get_anonymous_user_instance' From 6c48c284a12757157fff30c3e7158cc1773cd1a9 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Thu, 23 Apr 2020 16:30:47 -0300 Subject: [PATCH 016/240] Removed deprecated TEMPLATE_DIRS --- onadata/settings/common.py | 26 +++++++++----------------- onadata/settings/test_environ.py | 13 ++++++++++++- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/onadata/settings/common.py b/onadata/settings/common.py index 11aa1ea2e..3a93e0dd1 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -206,13 +206,15 @@ USE_TZ = True -TEMPLATE_DIRS = ( - os.path.join(ONADATA_DIR, 'libs/templates'), - # Put strings here, like "/home/html/django_templates" - # or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'DIRS': [ + os.path.join(ONADATA_DIR, 'libs/templates') + ] + } +] # needed by guardian ANONYMOUS_USER_ID = -1 @@ -515,16 +517,6 @@ def skip_suspicious_operations(record): except ImportError: pass -if isinstance(TEMPLATE_OVERRIDE_ROOT_DIR, string_types): - # site templates overrides - TEMPLATE_DIRS = ( - os.path.join(ONADATA_DIR, TEMPLATE_OVERRIDE_ROOT_DIR, 'templates'), - ) + TEMPLATE_DIRS - # site static files path - STATICFILES_DIRS += ( - os.path.join(ONADATA_DIR, TEMPLATE_OVERRIDE_ROOT_DIR, 'static'), - ) - # Transition from South to native migrations try: from django.db import migrations diff --git a/onadata/settings/test_environ.py b/onadata/settings/test_environ.py index a21109067..171bc0e27 100644 --- a/onadata/settings/test_environ.py +++ b/onadata/settings/test_environ.py @@ -68,7 +68,18 @@ 'KOBOCAT_TEMPLATES_PATH', os.path.abspath(os.path.join(PROJECT_ROOT, 'kobocat-template')) ) -TEMPLATE_DIRS = (os.path.join(TEMPLATE_OVERRIDE_ROOT_DIR, 'templates'),) + TEMPLATE_DIRS + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'DIRS': [ + os.path.join(ONADATA_DIR, 'libs/templates'), + TEMPLATE_OVERRIDE_ROOT_DIR + ] + } +] + STATICFILES_DIRS += (os.path.join(TEMPLATE_OVERRIDE_ROOT_DIR, 'static'),) KOBOFORM_SERVER = os.environ.get("KOBOFORM_SERVER", "localhost") From 1fc5a2ba2fc76eb5f2d29e75eea22d0ee81ea71a Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Thu, 23 Apr 2020 16:32:47 -0300 Subject: [PATCH 017/240] Make first test run: open files with "rb" mode, fixed bytes and str usage --- .../tests/viewsets/test_abstract_viewset.py | 9 ++-- onadata/apps/api/viewsets/xform_viewset.py | 1 - onadata/apps/logger/views.py | 42 ++++++++++--------- onadata/apps/main/views.py | 6 +-- onadata/apps/viewer/models/data_dictionary.py | 2 +- onadata/apps/viewer/models/parsed_instance.py | 1 - onadata/libs/utils/logger_tools.py | 2 +- 7 files changed, 31 insertions(+), 32 deletions(-) diff --git a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py index 6f0461210..b72b83d75 100644 --- a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py @@ -77,14 +77,13 @@ def publish_xls_form(self, publish_data={}, merge=True, public=False): xform_list_url = reverse('xform-list') - with open(path) as xls_file: + with open(path, 'rb') 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) + 'url': f'http://testserver/api/v1/forms/{self.xform.pk}' }) # Input was a private so change to public if project public @@ -188,7 +187,7 @@ def _make_submission(self, path, username=None, add_uuid=False, if add_uuid: path = self._add_uuid_to_submission_xml(path, self.xform) - with open(path) as f: + with open(path, 'rb') as f: post_data = {'xml_submission_file': f} if media_file is not None: @@ -252,7 +251,7 @@ def _submit_transport_instance_w_attachment(self, media_file = "1335783522563.jpg" path = os.path.join(self.main_directory, 'fixtures', 'transportation', 'instances', s, media_file) - with open(path) as f: + with open(path, 'rb') as f: self._make_submission(os.path.join( self.main_directory, 'fixtures', 'transportation', 'instances', s, s + '.xml'), media_file=f) diff --git a/onadata/apps/api/viewsets/xform_viewset.py b/onadata/apps/api/viewsets/xform_viewset.py index 6819952a1..833cc4192 100644 --- a/onadata/apps/api/viewsets/xform_viewset.py +++ b/onadata/apps/api/viewsets/xform_viewset.py @@ -687,7 +687,6 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) serializer.save() headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) diff --git a/onadata/apps/logger/views.py b/onadata/apps/logger/views.py index 635403bbd..6dae20c68 100644 --- a/onadata/apps/logger/views.py +++ b/onadata/apps/logger/views.py @@ -91,27 +91,29 @@ def _parse_int(num): def _html_submission_response(request, instance): - data = {} - data['username'] = instance.xform.user.username - data['id_string'] = instance.xform.id_string - data['domain'] = Site.objects.get(id=settings.SITE_ID).domain + data = { + 'username': instance.xform.user.username, + 'id_string': instance.xform.id_string, + 'domain': Site.objects.get(id=settings.SITE_ID).domain + } return render(request, "submission.html", data) def _submission_response(request, instance): - data = {} - data['message'] = _("Successful submission.") - data['formid'] = instance.xform.id_string - data['encrypted'] = instance.xform.encrypted - data['instanceID'] = 'uuid:%s' % instance.uuid - data['submissionDate'] = instance.date_created.isoformat() - data['markedAsCompleteDate'] = instance.date_modified.isoformat() - - context = RequestContext(request, data) + data = { + 'message': _("Successful submission."), + 'formid': instance.xform.id_string, + 'encrypted': instance.xform.encrypted, + 'instanceID': f'uuid:{instance.uuid}', + 'submissionDate': instance.date_created.isoformat(), + 'markedAsCompleteDate': instance.date_modified.isoformat() + } + + #context = RequestContext(request, data) t = loader.get_template('submission.xml') - return BaseOpenRosaResponse(t.render(context)) + return BaseOpenRosaResponse(t.render(data, request=request)) @require_POST @@ -283,13 +285,13 @@ def submission(request, username=None): # request.FILES is a django.utils.datastructures.MultiValueDict # for each key we have a list of values try: - xml_file_list = request.FILES.pop("xml_submission_file", []) + xml_file_list = list(request.FILES.pop("xml_submission_file", [])) if len(xml_file_list) != 1: return OpenRosaResponseBadRequest( _("There should be a single XML submission file.") ) # save this XML file and media files as attachments - media_files = request.FILES.values() + media_files = list(request.FILES.values()) # get uuid from post request uuid = request.POST.get('uuid') @@ -332,10 +334,10 @@ def submission(request, username=None): else: raise finally: - if len(xml_file_list): - [_file.close() for _file in xml_file_list] - if len(media_files): - [_file.close() for _file in media_files] + for xml_file in xml_file_list: + xml_file.close() + for media_file in media_files: + media_file.close() def download_xform(request, username, id_string): diff --git a/onadata/apps/main/views.py b/onadata/apps/main/views.py index 715b64ed0..4b3d2983b 100644 --- a/onadata/apps/main/views.py +++ b/onadata/apps/main/views.py @@ -144,13 +144,13 @@ def set_form(): else: message = form_result - context = RequestContext(request, { - 'message': message, 'message_list': message_list}) + context = {'message': message, 'message_list': message_list} if request.is_ajax(): res = loader.render_to_string( 'message.html', - context_instance=context + context=context, + request=request ).replace("'", r"\'").replace('\n', '') return HttpResponse( diff --git a/onadata/apps/viewer/models/data_dictionary.py b/onadata/apps/viewer/models/data_dictionary.py index 90687a812..e1bb1fafa 100644 --- a/onadata/apps/viewer/models/data_dictionary.py +++ b/onadata/apps/viewer/models/data_dictionary.py @@ -133,7 +133,7 @@ def set_uuid_in_xml(self, file_name=None, id_string=None): calculate_node.setAttribute("calculate", "'%s'" % self.uuid) model_node.appendChild(calculate_node) - self.xml = doc.toprettyxml(indent=" ", encoding='utf-8') + self.xml = doc.toprettyxml(indent=" ", encoding='utf-8').decode() # hack # http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-\ # and-silly-whitespace/ diff --git a/onadata/apps/viewer/models/parsed_instance.py b/onadata/apps/viewer/models/parsed_instance.py index b2985f6c2..976db9c30 100644 --- a/onadata/apps/viewer/models/parsed_instance.py +++ b/onadata/apps/viewer/models/parsed_instance.py @@ -44,7 +44,6 @@ class ParseError(Exception): def datetime_from_str(text): - print('DATETIME FROM STR') # Assumes text looks like 2011-01-01T09:50:06.966 if text is None: return None diff --git a/onadata/libs/utils/logger_tools.py b/onadata/libs/utils/logger_tools.py index ab148e3c9..a673981ee 100644 --- a/onadata/libs/utils/logger_tools.py +++ b/onadata/libs/utils/logger_tools.py @@ -299,7 +299,7 @@ def create_instance(username, xml_file, media_files, if username: username = username.lower() - xml = xml_file.read() + xml = xml_file.read().decode() xml_hash = Instance.get_hash(xml) xform = get_xform_from_submission(xml, username, uuid) check_submission_permissions(request, xform) From 51bef8bdf90f7b17d4aa4d0a3701468956937357 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Fri, 24 Apr 2020 11:59:10 -0300 Subject: [PATCH 018/240] Removed `export` app --- onadata/apps/export/__init__.py | 6 - .../export/templates/export/export_html.html | 52 ------ .../export/templates/export/export_menu.html | 163 ----------------- onadata/apps/export/urls.py | 24 --- onadata/apps/export/views.py | 164 ------------------ onadata/settings/common.py | 6 - 6 files changed, 415 deletions(-) delete mode 100644 onadata/apps/export/__init__.py delete mode 100644 onadata/apps/export/templates/export/export_html.html delete mode 100644 onadata/apps/export/templates/export/export_menu.html delete mode 100644 onadata/apps/export/urls.py delete mode 100644 onadata/apps/export/views.py 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 ee2bd0830..000000000 --- a/onadata/apps/export/urls.py +++ /dev/null @@ -1,24 +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 url - -# urlpatterns = [ -# '', -# 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/settings/common.py b/onadata/settings/common.py index 3a93e0dd1..761004b2b 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -193,19 +193,14 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - # Django 1.8 removes TransactionMiddleware (was deprecated in 1.6). See: - # https://docs.djangoproject.com/en/1.6/topics/db/transactions/#transaction-middleware - #'django.middleware.transaction.TransactionMiddleware', 'onadata.libs.utils.middleware.HTTPResponseNotAllowedMiddleware', 'readonly.middleware.DatabaseReadOnlyMiddleware', 'onadata.libs.utils.middleware.UsernameInResponseHeaderMiddleware', ) - ROOT_URLCONF = 'onadata.apps.main.urls' USE_TZ = True - TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -251,7 +246,6 @@ 'onadata.apps.sms_support', 'onadata.libs', 'onadata.apps.survey_report', - 'onadata.apps.export', 'pure_pagination', 'django_celery_beat', 'django_extensions', From 3fd53bcc55471d5ff7406647b5923d48e91359d8 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Fri, 24 Apr 2020 12:02:40 -0300 Subject: [PATCH 019/240] Open files in bytes mode --- .../tests/viewsets/test_abstract_viewset.py | 6 ++-- .../tests/viewsets/test_metadata_viewset.py | 2 +- .../api/tests/viewsets/test_xform_list_api.py | 16 +++++------ .../viewsets/test_xform_submission_api.py | 28 +++++++++---------- .../api/tests/viewsets/test_xform_viewset.py | 6 ++-- onadata/apps/main/tests/test_base.py | 12 ++++---- onadata/apps/main/tests/test_form_metadata.py | 7 +++-- onadata/apps/main/tests/test_process.py | 4 +-- onadata/libs/utils/tests/test_qrcode.py | 4 +-- 9 files changed, 43 insertions(+), 42 deletions(-) diff --git a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py index b72b83d75..53fa1fc2b 100644 --- a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py @@ -162,8 +162,8 @@ def _add_uuid_to_submission_xml(self, path, xform): tmp_file = NamedTemporaryFile(delete=False) split_xml = None - with open(path) as _file: - split_xml = re.split(r'()', _file.read()) + with open(path, 'rb') as _file: + split_xml = re.split(r'()', _file.read().decode()) split_xml[1:1] = [ '%s' % xform.uuid @@ -283,7 +283,7 @@ def _add_form_metadata(self, xform, data_type, data_value, path=None): } if path and data_value: - with open(path) as media_file: + with open(path, 'rb') as media_file: data.update({ 'data_file': media_file, }) diff --git a/onadata/apps/api/tests/viewsets/test_metadata_viewset.py b/onadata/apps/api/tests/viewsets/test_metadata_viewset.py index ea6455462..90f320807 100644 --- a/onadata/apps/api/tests/viewsets/test_metadata_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_metadata_viewset.py @@ -82,7 +82,7 @@ def test_delete_metadata(self): def test_windows_csv_file_upload_to_metadata(self): data_value = 'transportation.csv' path = os.path.join(self.fixture_dir, data_value) - with open(path) as f: + with open(path, 'rb') as f: f = InMemoryUploadedFile( f, 'media', data_value, 'text/csv', 2625, None) data = { diff --git a/onadata/apps/api/tests/viewsets/test_xform_list_api.py b/onadata/apps/api/tests/viewsets/test_xform_list_api.py index 5f8ee6cbd..c1d6e2eed 100644 --- a/onadata/apps/api/tests/viewsets/test_xform_list_api.py +++ b/onadata/apps/api/tests/viewsets/test_xform_list_api.py @@ -37,8 +37,8 @@ def test_get_xform_list(self): os.path.dirname(__file__), '..', 'fixtures', 'formList.xml') - with open(path) as f: - form_list_xml = f.read().strip() + with open(path, 'rb') as f: + form_list_xml = f.read().decode().strip() data = {"hash": self.xform.hash, "pk": self.xform.pk} content = response.render().content self.assertEqual(content, form_list_xml % data) @@ -82,8 +82,8 @@ def test_get_xform_list_anonymous_user(self): os.path.dirname(__file__), '..', 'fixtures', 'formList.xml') - with open(path) as f: - form_list_xml = f.read().strip() + with open(path, 'rb') as f: + form_list_xml = f.read().decode().strip() data = {"hash": self.xform.hash, "pk": self.xform.pk} content = response.render().content self.assertEqual(content, form_list_xml % data) @@ -174,8 +174,8 @@ def test_get_xform_list_other_user_with_dataentry_role(self): os.path.dirname(__file__), '..', 'fixtures', 'formList.xml') - with open(path) as f: - form_list_xml = f.read().strip() + with open(path, 'rb') as f: + form_list_xml = f.read().decode().strip() data = {"hash": self.xform.hash, "pk": self.xform.pk} content = response.render().content self.assertEqual(content, form_list_xml % data) @@ -209,8 +209,8 @@ def test_retrieve_xform_xml(self): os.path.dirname(__file__), '..', 'fixtures', 'Transportation Form.xml') - with open(path) as f: - form_xml = f.read().strip() + with open(path, 'rb') as f: + form_xml = f.read().decode().strip() data = {"form_uuid": self.xform.uuid} content = response.render().content.strip() self.assertEqual(content, form_xml % data) diff --git a/onadata/apps/api/tests/viewsets/test_xform_submission_api.py b/onadata/apps/api/tests/viewsets/test_xform_submission_api.py index 1b47438df..19b7c6712 100644 --- a/onadata/apps/api/tests/viewsets/test_xform_submission_api.py +++ b/onadata/apps/api/tests/viewsets/test_xform_submission_api.py @@ -32,7 +32,7 @@ def test_post_submission_anonymous(self): media_file = "1335783522563.jpg" path = os.path.join(self.main_directory, 'fixtures', 'transportation', 'instances', s, media_file) - with open(path) as f: + with open(path, 'rb') as f: f = InMemoryUploadedFile(f, 'media_file', media_file, 'image/jpg', os.path.getsize(path), None) submission_path = os.path.join( @@ -62,7 +62,7 @@ def test_post_submission_authenticated(self): media_file = "1335783522563.jpg" path = os.path.join(self.main_directory, 'fixtures', 'transportation', 'instances', s, media_file) - with open(path) as f: + with open(path, 'rb') as f: f = InMemoryUploadedFile(f, 'media_file', media_file, 'image/jpg', os.path.getsize(path), None) @@ -70,7 +70,7 @@ def test_post_submission_authenticated(self): self.main_directory, 'fixtures', 'transportation', 'instances', s, s + '.xml') - with open(submission_path) as sf: + with open(submission_path, 'rb') as sf: data = {'xml_submission_file': sf, 'media_file': f} request = self.factory.post('/submission', data) response = self.view(request) @@ -101,7 +101,7 @@ def test_post_submission_uuid_other_user_username_not_provided(self): media_file = "1335783522563.jpg" path = os.path.join(self.main_directory, 'fixtures', 'transportation', 'instances', s, media_file) - with open(path) as f: + with open(path, 'rb') as f: f = InMemoryUploadedFile(f, 'media_file', media_file, 'image/jpg', os.path.getsize(path), None) path = os.path.join( @@ -109,7 +109,7 @@ def test_post_submission_uuid_other_user_username_not_provided(self): 'transportation', 'instances', s, s + '.xml') path = self._add_uuid_to_submission_xml(path, self.xform) - with open(path) as sf: + with open(path, 'rb') as sf: data = {'xml_submission_file': sf, 'media_file': f} request = self.factory.post('/submission', data) response = self.view(request) @@ -130,7 +130,7 @@ def test_post_submission_authenticated_json(self): '..', 'fixtures', 'transport_submission.json') - with open(path) as f: + with open(path, 'rb') as f: data = json.loads(f.read()) request = self.factory.post('/submission', data, format='json') response = self.view(request) @@ -159,8 +159,8 @@ def test_post_submission_authenticated_json_with_geo(self): '..', 'fixtures', 'movie_form_submission.json') - with open(path) as f: - data = json.loads(f.read()) + with open(path, 'rb') as f: + data = json.loads(f.read().decode()) request = self.factory.post('/submission', data, format='json') response = self.view(request) self.assertEqual(response.status_code, 401) @@ -186,8 +186,8 @@ def test_post_submission_authenticated_bad_json(self): '..', 'fixtures', 'transport_submission_bad.json') - with open(path) as f: - data = json.loads(f.read()) + with open(path, 'rb') as f: + data = json.loads(f.read().decode()) request = self.factory.post('/submission', data, format='json') response = self.view(request) self.assertEqual(response.status_code, 401) @@ -215,7 +215,7 @@ def test_post_submission_require_auth(self): media_file = "1335783522563.jpg" path = os.path.join(self.main_directory, 'fixtures', 'transportation', 'instances', s, media_file) - with open(path) as f: + with open(path, 'rb') as f: f = InMemoryUploadedFile(f, 'media_file', media_file, 'image/jpg', os.path.getsize(path), None) submission_path = os.path.join( @@ -257,7 +257,7 @@ def test_post_submission_require_auth_anonymous_user(self): media_file = "1335783522563.jpg" path = os.path.join(self.main_directory, 'fixtures', 'transportation', 'instances', s, media_file) - with open(path) as f: + with open(path, 'rb') as f: f = InMemoryUploadedFile(f, 'media_file', media_file, 'image/jpg', os.path.getsize(path), None) submission_path = os.path.join( @@ -284,7 +284,7 @@ def test_post_submission_require_auth_other_user(self): media_file = "1335783522563.jpg" path = os.path.join(self.main_directory, 'fixtures', 'transportation', 'instances', s, media_file) - with open(path) as f: + with open(path, 'rb') as f: f = InMemoryUploadedFile(f, 'media_file', media_file, 'image/jpg', os.path.getsize(path), None) submission_path = os.path.join( @@ -325,7 +325,7 @@ def test_post_submission_require_auth_data_entry_role(self): media_file = "1335783522563.jpg" path = os.path.join(self.main_directory, 'fixtures', 'transportation', 'instances', s, media_file) - with open(path) as f: + with open(path, 'rb') as f: f = InMemoryUploadedFile(f, 'media_file', media_file, 'image/jpg', os.path.getsize(path), None) submission_path = os.path.join( diff --git a/onadata/apps/api/tests/viewsets/test_xform_viewset.py b/onadata/apps/api/tests/viewsets/test_xform_viewset.py index 8d57396d2..0adacb8ca 100644 --- a/onadata/apps/api/tests/viewsets/test_xform_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_xform_viewset.py @@ -320,7 +320,7 @@ def test_publish_xlsform(self): path = os.path.join( settings.ONADATA_DIR, "apps", "main", "tests", "fixtures", "transportation", "transportation.xls") - with open(path) as xls_file: + with open(path, 'rb') as xls_file: post_data = {'xls_file': xls_file} request = self.factory.post('/', data=post_data, **self.extra) response = view(request) @@ -340,7 +340,7 @@ def test_publish_invalid_xls_form(self): path = os.path.join( settings.ONADATA_DIR, "apps", "main", "tests", "fixtures", "transportation", "transportation.bad_id.xls") - with open(path) as xls_file: + with open(path, 'rb') as xls_file: post_data = {'xls_file': xls_file} request = self.factory.post('/', data=post_data, **self.extra) response = view(request) @@ -355,7 +355,7 @@ def test_publish_invalid_xls_form_no_choices(self): path = os.path.join( settings.ONADATA_DIR, "apps", "main", "tests", "fixtures", "transportation", "transportation.no_choices.xls") - with open(path) as xls_file: + with open(path, 'rb') as xls_file: post_data = {'xls_file': xls_file} request = self.factory.post('/', data=post_data, **self.extra) response = view(request) diff --git a/onadata/apps/main/tests/test_base.py b/onadata/apps/main/tests/test_base.py index 8b19a22a9..aedaa672d 100644 --- a/onadata/apps/main/tests/test_base.py +++ b/onadata/apps/main/tests/test_base.py @@ -93,7 +93,7 @@ def _publish_xls_file(self, path): if not path.startswith('/%s/' % self.user.username): path = os.path.join(self.this_directory, path) - with open(path) as xls_file: + with open(path, 'rb') as xls_file: post_data = {'xls_file': xls_file} return self.client.post('/%s/' % self.user.username, post_data) @@ -172,8 +172,8 @@ def _make_submission(self, path, username=None, add_uuid=False, tmp_file = NamedTemporaryFile(delete=False) split_xml = None - with open(path) as _file: - split_xml = re.split(r'()', _file.read()) + with open(path, 'rb') as _file: + split_xml = re.split(r'()', _file.read().decode()) split_xml[1:1] = [ '%s' % self.xform.uuid @@ -182,7 +182,7 @@ def _make_submission(self, path, username=None, add_uuid=False, path = tmp_file.name tmp_file.close() - with open(path) as f: + with open(path, 'rb') as f: post_data = {'xml_submission_file': f} if username is None: @@ -211,8 +211,8 @@ def _make_submission(self, path, username=None, add_uuid=False, os.unlink(tmp_file.name) def _make_submission_w_attachment(self, path, attachment_path): - with open(path) as f: - a = open(attachment_path) + with open(path, 'rb') as f: + a = open(attachment_path, 'rb') post_data = {'xml_submission_file': f, 'media_file': a} url = '/%s/submission' % self.user.username auth = DigestAuth('bob', 'bob') diff --git a/onadata/apps/main/tests/test_form_metadata.py b/onadata/apps/main/tests/test_form_metadata.py index a6a293078..1a8164663 100644 --- a/onadata/apps/main/tests/test_form_metadata.py +++ b/onadata/apps/main/tests/test_form_metadata.py @@ -36,9 +36,10 @@ def _add_metadata(self, data_type='doc'): name = 'transportation.xls' 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 + with open(path, 'rb') as doc_file: + 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] diff --git a/onadata/apps/main/tests/test_process.py b/onadata/apps/main/tests/test_process.py index f244746de..ecbff6ce0 100644 --- a/onadata/apps/main/tests/test_process.py +++ b/onadata/apps/main/tests/test_process.py @@ -76,7 +76,7 @@ def test_uuid_submit(self): path = os.path.join( self.this_directory, 'fixtures', 'transportation', 'instances', survey, survey + '.xml') - with open(path) as f: + with open(path, 'rb') as f: post_data = {'xml_submission_file': f, 'uuid': self.xform.uuid} url = '/submission' self.response = self.client.post(url, post_data) @@ -151,7 +151,7 @@ def test_not_logged_in_cannot_upload(self): "transportation.xls") if not path.startswith('/%s/' % self.user.username): path = os.path.join(self.this_directory, path) - with open(path) as xls_file: + with open(path, 'rb') as xls_file: post_data = {'xls_file': xls_file} return self.client.post('/%s/' % self.user.username, post_data) diff --git a/onadata/libs/utils/tests/test_qrcode.py b/onadata/libs/utils/tests/test_qrcode.py index 98b13ad28..e61dae68a 100644 --- a/onadata/libs/utils/tests/test_qrcode.py +++ b/onadata/libs/utils/tests/test_qrcode.py @@ -12,6 +12,6 @@ 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() + with open(path, 'rb') as f: + qrcode = f.read().decode() self.assertEqual(generate_qrcode(url), qrcode.strip()) From dc05c80fdae8864a2cc13c75d1b2059e919b8d5d Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Fri, 24 Apr 2020 12:07:32 -0300 Subject: [PATCH 020/240] Replaced reversed dotted path urls with their name - part 1 --- kobocat-template/templates/base.html | 2 +- kobocat-template/templates/data_view.html | 8 +- kobocat-template/templates/export_list.html | 12 +-- kobocat-template/templates/instance.html | 14 +-- kobocat-template/templates/map.html | 6 +- .../templates/published_forms__list.html | 4 +- kobocat-template/templates/show.html | 90 +++++++++---------- .../templates/show_form_settings.html | 6 +- kobocat-template/templates/stats_tables.html | 6 +- kobocat-template/templates/topbar.html | 6 +- onadata/apps/logger/templates/submission.html | 4 +- onadata/apps/logger/views.py | 13 +-- onadata/apps/main/templates/form_gallery.html | 2 +- onadata/apps/main/templates/profile.html | 8 +- .../main/templates/published_surveys.html | 10 ++- onadata/apps/main/templates/settings.html | 2 +- onadata/apps/main/templates/show.html | 72 +++++++-------- .../main/templates/show_form_settings.html | 80 ++++++++--------- onadata/apps/main/urls.py | 73 ++++++++------- .../apps/sms_support/tests/test_base_sms.py | 2 +- .../templates/survey_report/auto_report.html | 2 +- .../survey_report/auto_report_split_by.html | 2 +- .../templates/survey_report/export_html.html | 2 +- .../templates/survey_report/export_menu.html | 2 +- onadata/apps/viewer/templates/charts.html | 2 +- onadata/apps/viewer/templates/data_view.html | 2 +- .../apps/viewer/templates/export_list.html | 2 +- onadata/apps/viewer/templates/instance.html | 2 +- .../apps/viewer/templates/stats_tables.html | 2 +- onadata/apps/viewer/views.py | 2 +- onadata/libs/templates/base.html | 2 +- onadata/libs/templates/base_embed.html | 2 +- onadata/libs/templates/home_topbar.html | 2 +- onadata/libs/templates/topbar.html | 10 +-- onadata/libs/utils/analyser_export.py | 8 +- onadata/libs/utils/gravatar.py | 9 +- 36 files changed, 242 insertions(+), 231 deletions(-) diff --git a/kobocat-template/templates/base.html b/kobocat-template/templates/base.html index 31ef0cf65..60c45cef0 100644 --- a/kobocat-template/templates/base.html +++ b/kobocat-template/templates/base.html @@ -92,7 +92,7 @@ {% block javascript %} - + diff --git a/kobocat-template/templates/data_view.html b/kobocat-template/templates/data_view.html index bc0cc081b..c966fde00 100644 --- a/kobocat-template/templates/data_view.html +++ b/kobocat-template/templates/data_view.html @@ -11,7 +11,7 @@ @@ -48,11 +48,11 @@

{% trans "Data View" %}

{% endblock %} diff --git a/kobocat-template/templates/instance.html b/kobocat-template/templates/instance.html index ddfd82516..011a31237 100644 --- a/kobocat-template/templates/instance.html +++ b/kobocat-template/templates/instance.html @@ -7,8 +7,8 @@ @@ -54,9 +54,9 @@

{% trans "Delete Confirmation" %}

diff --git a/kobocat-template/templates/map.html b/kobocat-template/templates/map.html index c3354b7ec..585d47aac 100644 --- a/kobocat-template/templates/map.html +++ b/kobocat-template/templates/map.html @@ -23,7 +23,7 @@ var customMapBoxTileLayer = {label: '{{mapbox_layer.map_name}}', url: '{{mapbox_layer.link}}'}; {% endif %} - + {% endif %} - + {% csrf_token %} {{ permission_form.as_table }} @@ -160,7 +160,7 @@

{% csrf_token %} - + {% else %}

{{ xform.title }}

@@ -172,7 +172,7 @@

{{ xform.title }}

{% csrf_token %} - + {% else %} {{ xform.description }} @@ -184,16 +184,16 @@

{{ xform.title }}

{% for doc in supporting_docs %}
{% if can_edit %} - + {% endif %} - {{ doc.data_value }} + {{ doc.data_value }}

{% endfor %} {% if can_edit %} -
+ {% csrf_token %}
{{ doc_form.as_table }} @@ -255,16 +255,16 @@

@@ -278,7 +278,7 @@

View gallery - + Download all photos @@ -351,7 +351,7 @@

GPS Points

View on map - + Download GPS points @@ -479,7 +479,7 @@

{{ field }} {% endfor %} {% trans "edit" %} - + {% else %} {{ form_license|default:'No license' }} {% endif %} @@ -508,80 +508,80 @@


@@ -595,7 +595,7 @@

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

{{ field }} {% endfor %} {% trans "edit" %} - + {% else %} {{ data_license|default:'No license' }} {% endif %} @@ -631,16 +631,16 @@

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

{% trans "Source" %}

- + {% if source.data_file %} -

{{ source.data_file.name }}

+

{{ source.data_file.name }}

{% endif %} {% csrf_token %} {% if can_edit %} {% trans "edit" %} - + {% endif %} @@ -671,11 +671,11 @@

{% trans "Mapb

{{ mapbox_layer.map_name }} – {% trans "Attribution:" %} {{ mapbox_layer.attribution }} {% if can_edit %} - – {% trans "remove" %}

+ – {% trans "remove" %}

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

{{ mapbox_layer_form.as_table }} @@ -689,7 +689,7 @@

{% trans "Mapb

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

diff --git a/kobocat-template/templates/show_form_settings.html b/kobocat-template/templates/show_form_settings.html index f2d77323b..64fbf9389 100644 --- a/kobocat-template/templates/show_form_settings.html +++ b/kobocat-template/templates/show_form_settings.html @@ -28,9 +28,9 @@

{% for media in media_upload %}
  • {{ media.data_value }}
    - + {% if can_edit %} - + {% endif %}
  • {% endfor %} @@ -39,7 +39,7 @@

    {% if can_edit %} - + {% csrf_token %}

    diff --git a/onadata/libs/templates/topbar.html b/onadata/libs/templates/topbar.html index bf1a3fb6d..2031a5859 100644 --- a/onadata/libs/templates/topbar.html +++ b/onadata/libs/templates/topbar.html @@ -48,7 +48,7 @@
  • - + {% trans "kml" %}
  • @@ -60,11 +60,11 @@
  • {% trans "Forms" %}
  • {% if user.is_superuser or user.is_staff %} -
  • {% trans "People" %}
  • +
  • {% trans "People" %}
  • {% endif %} -
  • {% trans "Syntax" %}
  • -
  • {% trans "Support" %}
  • +
  • {% trans "Syntax" %}
  • +
  • {% trans "Support" %}
  • {% else %} {% if can_edit %} diff --git a/onadata/apps/main/templates/show_form_settings.html b/onadata/apps/main/templates/show_form_settings.html index 975459c3b..c0d01fb6c 100644 --- a/onadata/apps/main/templates/show_form_settings.html +++ b/onadata/apps/main/templates/show_form_settings.html @@ -245,9 +245,6 @@

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

    {% trans "Browse Data" %} - - {% trans "Statistics" %} - {% else %} {% if can_edit %} From a337288eda4bdd959cee98f5840078d2e1ec739c Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Mon, 29 Jun 2020 16:34:03 -0400 Subject: [PATCH 038/240] Use MIDDLEWARE instead of MIDDLEWARE_CLASSES --- onadata/koboform/redirect_middleware.py | 8 ++++++-- onadata/libs/utils/middleware.py | 12 ++++++------ onadata/settings/common.py | 4 ++-- onadata/settings/kc_environ.py | 6 +++--- onadata/settings/test_environ.py | 4 ++-- onadata/settings/travis_ci.py | 5 ++--- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/onadata/koboform/redirect_middleware.py b/onadata/koboform/redirect_middleware.py index fa14dd6fb..bac46efa6 100644 --- a/onadata/koboform/redirect_middleware.py +++ b/onadata/koboform/redirect_middleware.py @@ -1,9 +1,11 @@ # coding: utf-8 from __future__ import unicode_literals, print_function, division, absolute_import + from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse -from onadata import koboform +from django.utils.deprecation import MiddlewareMixin +from onadata import koboform # Always redirect to user profile, # regardless of koboform setting @@ -35,7 +37,9 @@ # /accounts/logout/ => kform/accounts/logout # /accounts/register/ => kform/accounts/register -class ConditionalRedirects(object): + +class ConditionalRedirects(MiddlewareMixin): + def process_view(self, request, view, args, kwargs): view_name = view.__name__ is_logged_in = request.user.is_authenticated diff --git a/onadata/libs/utils/middleware.py b/onadata/libs/utils/middleware.py index 8a64a8868..314b411f2 100644 --- a/onadata/libs/utils/middleware.py +++ b/onadata/libs/utils/middleware.py @@ -7,17 +7,17 @@ from django.template import RequestContext from django.template import loader from django.middleware.locale import LocaleMiddleware +from django.utils.deprecation import MiddlewareMixin from django.utils.translation.trans_real import parse_accept_lang_header -from django.utils.encoding import smart_str -class ExceptionLoggingMiddleware(object): +class ExceptionLoggingMiddleware(MiddlewareMixin): def process_exception(self, request, exception): print(traceback.format_exc()) -class HTTPResponseNotAllowedMiddleware(object): +class HTTPResponseNotAllowedMiddleware(MiddlewareMixin): def process_response(self, request, response): if isinstance(response, HttpResponseNotAllowed): @@ -48,7 +48,7 @@ def process_request(self, request): super(LocaleMiddlewareWithTweaks, self).process_request(request) -class SqlLogging: +class SqlLogging(MiddlewareMixin): def process_response(self, request, response): from sys import stdout if stdout.isatty(): @@ -59,7 +59,7 @@ def process_response(self, request, response): return response -class BrokenClientMiddleware(object): +class BrokenClientMiddleware(MiddlewareMixin): """ ODK Collect sends HTTP-violating localized date strings, e.g. 'mar., 25 ao\xfbt 2015 07:11:56 GMT+00:00', which wreak havoc on oauthlib. @@ -74,7 +74,7 @@ def process_request(self, request): del request.META['HTTP_DATE'] -class UsernameInResponseHeaderMiddleware(object): +class UsernameInResponseHeaderMiddleware(MiddlewareMixin): """ Record the authenticated user (if any) in the `X-KoBoNaUt` HTTP header """ diff --git a/onadata/settings/common.py b/onadata/settings/common.py index 0decf7f99..f9f808bb8 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -181,7 +181,7 @@ 'onadata.apps.main.context_processors.base_url' ) -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = [ 'reversion.middleware.RevisionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', @@ -197,7 +197,7 @@ 'onadata.libs.utils.middleware.HTTPResponseNotAllowedMiddleware', 'readonly.middleware.DatabaseReadOnlyMiddleware', 'onadata.libs.utils.middleware.UsernameInResponseHeaderMiddleware', -) +] ROOT_URLCONF = 'onadata.apps.main.urls' USE_TZ = True diff --git a/onadata/settings/kc_environ.py b/onadata/settings/kc_environ.py index bf302cc22..4f2378ea8 100644 --- a/onadata/settings/kc_environ.py +++ b/onadata/settings/kc_environ.py @@ -69,7 +69,7 @@ def celery_logger_setup_handler(logger, **kwargs): else: TESTING_MODE = False -MEDIA_URL = '/' + os.environ.get('KOBOCAT_MEDIA_URL', 'media').strip('/') + '/' +MEDIA_URL = f"/{os.environ.get('KOBOCAT_MEDIA_URL', 'media').strip('/')}/" STATIC_URL = '/static/' LOGIN_URL = '/accounts/login/' LOGIN_REDIRECT_URL = '/login_redirect/' @@ -93,7 +93,7 @@ def celery_logger_setup_handler(logger, **kwargs): MEDIA_ROOT = os.path.join(PROJECT_ROOT, MEDIA_URL.lstrip('/')) if PRINT_EXCEPTION and DEBUG: - MIDDLEWARE_CLASSES += ('utils.middleware.ExceptionLoggingMiddleware',) + MIDDLEWARE.append('onadata.libs.utils.middleware.ExceptionLoggingMiddleware') # Clear out the test database if TESTING_MODE: @@ -119,7 +119,7 @@ def celery_logger_setup_handler(logger, **kwargs): 'onadata.koboform.context_processors.koboform_integration', ) + TEMPLATE_CONTEXT_PROCESSORS -MIDDLEWARE_CLASSES = ('onadata.koboform.redirect_middleware.ConditionalRedirects', ) + MIDDLEWARE_CLASSES +MIDDLEWARE.insert(0, 'onadata.koboform.redirect_middleware.ConditionalRedirects') # Domain must not exclude KPI when sharing sessions if os.environ.get('SESSION_COOKIE_DOMAIN'): diff --git a/onadata/settings/test_environ.py b/onadata/settings/test_environ.py index 2de500e9e..573f7e816 100644 --- a/onadata/settings/test_environ.py +++ b/onadata/settings/test_environ.py @@ -61,7 +61,7 @@ ENKETO_API_TOKEN = 'abc' if PRINT_EXCEPTION and DEBUG: - MIDDLEWARE_CLASSES += ('utils.middleware.ExceptionLoggingMiddleware',) + MIDDLEWARE.append('onadata.libs.utils.middleware.ExceptionLoggingMiddleware') # include the kobocat-template directory TEMPLATE_OVERRIDE_ROOT_DIR = os.environ.get( @@ -92,7 +92,7 @@ 'onadata.koboform.context_processors.koboform_integration', ) + TEMPLATE_CONTEXT_PROCESSORS -# MIDDLEWARE_CLASSES = ('onadata.koboform.redirect_middleware.ConditionalRedirects', ) + MIDDLEWARE_CLASSES +# MIDDLEWARE.insert(0, 'onadata.koboform.redirect_middleware.ConditionalRedirects') # Domain must not exclude KPI when sharing sessions if os.environ.get('SESSION_COOKIE_DOMAIN'): diff --git a/onadata/settings/travis_ci.py b/onadata/settings/travis_ci.py index 6ca4d20b6..1857fcd20 100644 --- a/onadata/settings/travis_ci.py +++ b/onadata/settings/travis_ci.py @@ -15,8 +15,7 @@ } } -### MONGO ### - +# ## MONGO ### MONGO_DATABASE = { 'HOST': os.environ.get('KOBOCAT_MONGO_HOST', 'localhost'), 'PORT': int(os.environ.get('KOBOCAT_MONGO_PORT', 27017)), @@ -51,7 +50,7 @@ ############# if PRINT_EXCEPTION and DEBUG: - MIDDLEWARE_CLASSES += ('utils.middleware.ExceptionLoggingMiddleware',) + MIDDLEWARE.append('onadata.libs.utils.middleware.ExceptionLoggingMiddleware') if len(sys.argv) >= 2 and (sys.argv[1] == "test" or sys.argv[1] == "test_all"): # This trick works only when we run tests from the command line. From 72e24da59bdc19c292a5abfe27927e663b2b8f56 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Mon, 29 Jun 2020 17:05:13 -0400 Subject: [PATCH 039/240] Fixed form permissions tests --- kobocat-template/templates/show.html | 2 +- onadata/apps/main/templates/show.html | 2 +- .../apps/main/tests/test_form_permissions.py | 20 +++++++------------ 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/kobocat-template/templates/show.html b/kobocat-template/templates/show.html index a56f604f5..b87470bdb 100644 --- a/kobocat-template/templates/show.html +++ b/kobocat-template/templates/show.html @@ -211,7 +211,7 @@

    {{ xform.title }}

    {% if is_owner %}
    - + {% csrf_token %}

    {% trans "Delete Confirmation" %}

    diff --git a/onadata/apps/main/templates/show.html b/onadata/apps/main/templates/show.html index 10b93a167..66e48805f 100644 --- a/onadata/apps/main/templates/show.html +++ b/onadata/apps/main/templates/show.html @@ -487,7 +487,7 @@

    {% trans "Settings" %}

    - {{ 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 %} diff --git a/onadata/apps/main/templates/show_form_settings.html b/onadata/apps/main/templates/show_form_settings.html index 3d1196eb1..95a6d7f79 100644 --- a/onadata/apps/main/templates/show_form_settings.html +++ b/onadata/apps/main/templates/show_form_settings.html @@ -452,65 +452,6 @@

    {% trans {% 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 %} diff --git a/onadata/apps/main/urls.py b/onadata/apps/main/urls.py index ad9315f11..908fb7960 100644 --- a/onadata/apps/main/urls.py +++ b/onadata/apps/main/urls.py @@ -180,8 +180,6 @@ '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'}), @@ -231,26 +229,26 @@ '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'), + # 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. diff --git a/onadata/apps/main/views.py b/onadata/apps/main/views.py index e1c68b70d..3f78c3818 100644 --- a/onadata/apps/main/views.py +++ b/onadata/apps/main/views.py @@ -36,18 +36,12 @@ 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 + QuickConverter, SourceForm, PermissionForm, MediaForm 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.apps.viewer.views import attachment_url from onadata.libs.utils.decorators import is_owner from onadata.libs.utils.log import audit_log, Actions from onadata.libs.utils.logger_tools import response_with_mimetype_and_name, \ @@ -132,17 +126,7 @@ def set_form(): 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 + message = form_result context = RequestContext(request, { 'message': message, 'message_list': message_list}) @@ -199,17 +183,7 @@ def set_form(): # 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 + data['message'] = form_result # profile view... # for the same user -> dashboard @@ -352,20 +326,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( @@ -422,9 +382,6 @@ def show(request, username=None, id_string=None, uuid=None): 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) - return render(request, "show.html", data) @@ -461,9 +418,6 @@ def show_form_settings(request, username=None, id_string=None, uuid=None): 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) - return render(request, "show_form_settings.html", data) @@ -689,43 +643,6 @@ def edit(request, username, id_string): }, 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 = _(u"SMS Support Activated on") - else: - audit_action = Actions.SMS_SUPPORT_DEACTIVATED - audit_message = _(u"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'): uri = request.POST.get('media_url') try: diff --git a/onadata/apps/sms_support/__init__.py b/onadata/apps/sms_support/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/onadata/apps/sms_support/autodoc.py b/onadata/apps/sms_support/autodoc.py deleted file mode 100644 index 4899bd680..000000000 --- a/onadata/apps/sms_support/autodoc.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim: ai ts=4 sts=4 et sw=4 nu - -""" 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(u' '.join([c.get('sms_option') - for c in xlsf_choices][:1])) - elif xlsf_type == 'select all that apply': - return safe_wrap(u' '.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 = (u'%(type)s ' - u'%(text)s' - % {'type': xlsf_type, 'text': value}) - return unicode(value) - - if xlsf_type == 'text': - return safe_wrap(u'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 = u'Select one of the following:' - helper += u'
      ' - helper += u''.join([u'
    • ' - u'%(sms_option)s %(label)s
    • ' - % {'sms_option': c.get('sms_option'), - 'label': c.get('label')} - for c in xlsf_choices]) - helper += u'
    ' - return safe_wrap(helper) - elif xlsf_type == 'select all that apply': - helper = u'Select none, one or more in:' - helper += u'
      ' - helper += u''.join([u'
    • ' - u'%(sms_option)s %(label)s
    • ' - % {'sms_option': c.get('sms_option'), - 'label': c.get('label')} - for c in xlsf_choices]) - helper += u'
    ' - return safe_wrap(helper) - elif xlsf_type == 'geopoint': - helper = (u'GPS coordinates as ' - u'latitude longitude.' - u'
    Optionnaly add ' - u'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 = (u'File name and base64 data of the file as in ' - u'x.%s;dGhpc.' - u'
    It is not intented to be filled by ' - u'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 = (u'%(keyword)s' - u'%(qid)d ' - % {'keyword': xform.sms_id_string, 'qid': len(helpers)}) - line_values = (u'%(keyword)s ' - % {'keyword': xform.sms_id_string}) - helpers.append(('keyword', u'The keyword used to identify the form.' - u'
    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 += (u'' - u'%(sep)s%(sms_field)s ' - % {'sep': separator, - 'sms_field': group.get('sms_field')}) - line_names += (u'' - u'%(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 += (u'%(value)s ' - % {'type': type_id, 'value': sample}) - line_names += (u'%(value)s' - u'%(h)s ' - % {'type': type_id, - 'value': sample_name, 'h': qid}) - helpers.append((sample_name, - get_helper_text(question, json_survey))) - - line_values += u'' - - 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 e69de29bb..000000000 diff --git a/onadata/apps/sms_support/parser.py b/onadata/apps/sms_support/parser.py deleted file mode 100644 index 1d9cd1194..000000000 --- a/onadata/apps/sms_support/parser.py +++ /dev/null @@ -1,379 +0,0 @@ -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 = _(u"%(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(_(u"Required field missing"), xlsf_name) - - def safe_wrap(func): - try: - return func() - except Exception as e: - raise SMSCastingError(_(u"%(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(_(u"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(_(u"No matching choice " - u"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 u" ".join(ret_values) - elif xlsf_type == 'geopoint': - err_msg = _(u"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(_(u"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 = u" ".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': _(u"[SUCCESS] Your submission has been accepted. " - u"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': _(u"Missing 'identity' " - u"or 'text' field.")}) - return - - if not len(identity.strip()) or not len(text.strip()): - responses.append({'code': SMS_API_ERROR, - 'text': _(u"'identity' and 'text' fields can " - u"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': _(u"The form '%(id_string)s' does not " - u"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': _(u"There must be at least one group of " - u"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': _(u"Required field `%(field)s` is " - u"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 dc359d025..000000000 --- a/onadata/apps/sms_support/providers/__init__.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim: ai ts=4 sts=4 et sw=4 nu - -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': u"SMS Sync", - 'imp': imp_sub_smssync, - 'imp_form': imp_sub_form_smssync, - 'doc': autodoc_smssync}, - TELERIVET: {'name': u"Telerivet", - 'imp': imp_sub_telerivet, - 'imp_form': imp_sub_form_telerivet, - 'doc': autodoc_telerivet}, - TWILIO: {'name': u"Twilio", - 'imp': imp_sub_twilio, - 'imp_form': imp_sub_form_twilio, - 'doc': autodoc_twilio}, - TEXTIT: {'name': u"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(u"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 9bbfaab78..000000000 --- a/onadata/apps/sms_support/providers/smssync.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" 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 = (u'

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

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

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

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

    ') % {'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 = _(u"[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': _(u"`identity` and `message` are " - u"both required and must not be " - u"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 08df429a3..000000000 --- a/onadata/apps/sms_support/providers/telerivet.py +++ /dev/null @@ -1,108 +0,0 @@ -""" 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 = (u'

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

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

      ' - + u'

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

    ') % {'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 = _(u"[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': _(u"`identity` and `message` are " - u"both required and must not be " - u"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 7801f0071..000000000 --- a/onadata/apps/sms_support/providers/textit.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" 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 = (u'

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

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

      ' - + u'

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

    ') % {'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 = _(u"[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': _(u"`identity` and `message` are " - u"both required and must not be " - u"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 b7f585fe7..000000000 --- a/onadata/apps/sms_support/providers/twilio.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" 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 = (u'

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

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

      ' - + u'

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

    ') % {'urla': urla, 'urlb': urlb} - return doc - - -def get_response(data): - - xml_head = u'' - response_dict = {'Response': {}} - message = data.get('text') - - if data.get('code') == SMS_API_ERROR: - message = None - elif data.get('code') != SMS_SUBMISSION_ACCEPTED: - message = _(u"[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': _(u"`identity` and `message` are " - u"both required and must not be " - u"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 6e1142f89..000000000 --- a/onadata/apps/sms_support/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ - -# 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 5ad97a2a81c8966800e0cc7299d687578fdd573b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10240 zcmeHNU2Ggz6+W}}I(F@3oism?k}z?w^Xt$wE&WMT+ligT5GTfVsgNqU^RO+1>Cfe`aq(0z~?z#8ed+s^so|*L@e!uqm>p$82uGEx+QYSZOo1~$NZsB=tgj+?{ z;F+|Wv$L}i8ILRF6QqF~;auymf%T|KRPOmI)JD`M)O%2yQCm<~qjJAlVFLPjU3mWl zEqi6Ep$3M;!(Sk_^hp7CU&`3crI(Gcm}+G%rDt`#SPK8Gcvj?BG;k|jvAtUVO^xv4 zhNS#r^)^EQLL52=V zs|P;diX%CxX$8ZbZf%i;A7Vbs_${mwp*KwYj{oM0(z1 z^e|IP-?a#R?;`Zwi_n`Eqzmjs&xc2~E@nQo2TOK}d}<-OZI1dm{BM|Xm=aXclX@I` z4*#nOFip!W@2%5y`o4C$LEqO+*X#Se)2sA-{d98n8Tg+i7vRHLCKre}rAhycDqoP4 zyuy?Mww#{u+_WlPkXAWQYep(kQbFJr#!KHqD6;T()oMf)8M$c5`|>P0%gh*ArC|mO z6$>ih?3viG()3wry1YlU1JXRh@bN3iXq4wI%&GB;)s1VnZ2f@qLCVvJ!5TNAekjuV zvDDXRK_5begrHnhxD}kdLf=xpG@5l)E`vlW=ZvWwk|(V~(kNMtI zN%@pDwmsH}lgrrwr|J}oxw27C#0m9Gsj<-rZV49!njO*Xj8WKRNpVhj6*y3n12ymw z8&{lw&I^1kD-!tOx|QArgY-w8Y)#!ZY( z^QK0P>0qKH(Ktsrh7;Udp#)G;NQKtP=c`iDE0m~_;ela$M3pe#Pz($u(A>}`pP`mL z(0Th;JKOV}?HRnbMvtBC#~*7Scn~^Le%iH*bb>Qp0Dt3v+w8CCXE&n?ftz)mLSTbz zKlhxQId^W~EMAO;qzk)>IZ3*411cWEg5z431ZF5E%W9pIWwjs_We~O)4cF?h)QSM~ z@ee=v|#59c?2}SIIu9rtBdv z?-cf)q&{DpEG{JOMZVHs5ZotM4VUMuP!-7R?%V>in=twAK3TVMq!#O#$^UBL-(&a_l;9U}Pq zRs2wn^_^zZE(v~`5J7lD!3IfVLZ2*rhBcsADNQ4QLC>M_gy3joi}=7ZkH+n&<)f@x z8E_R2HfKCI$~?K;>e}(({PSousM_)PdS1H@T4n8c@Q`_ZsjJ$))MIU5YOt2Z&q1{` zo~E@l`0_j&5B6F!qXkaAPb1fG$^56C_;5W+$+;o4+IGsu}4bV0^4X^ zDgClz5J%yj5OM!u9Wf#&_rbJL0(XFjl2ZE9h(Y9})(s*j^-aWCr1S?fgUHD>8AMKM zmxvaW(x2H3B4@PPBqXNv%Z_QQ$4dbC~TyYpJYum0m4%YudP4`UVl#nG)nS zA)-8HiKdP6kWOes9i;HW6d`in7b9}w@YbBvPh~E&&B|Pw^;~cUYtCk9J0)8~MBSz2 zvoWFsJ4TeSEkx97MFK6jB4Le|&=zv?r7wlhXyj4n8|8B`qJ;JsQ9?(ExF0Fm5hHR| z5IJ{-ZS;YZ?1~XND~O!C!!~+GO7_HvoE1dQy zCqL6n-BqkxI3C~IDS&^8#5fY^aY_se&Od&>Afny;A0L%QM~S5zmGh1|qfmyK=%~Z9 z-qYv^BdDbfF1Qq>V5~~Hp+WEM(lSeL?&QI{rGh&B~Qd{>{mhT*A=#z&>r;Scd8sNFrZc?RW^y9t$~5aJ_a$a- zT+Lq(r@Y=;;ZI@%f3v-U8<0JGxUPa>V)FtF-3q~lHO!^)OdrLyl1o?9D$aidOcm$v zjS-EtFGe)hqcLLcb}U9opyM%W0P2s?Dxfch$OMfq#fVOIB1V)q5F^Sv3Dkgm$AX4< zWtp8dYfT^+m|&Ac4s;4F22uK8j41ufK-DgGf3=Gtsadv*p-=2$j1$t$OIY-on$Rg8 z;~<_^-QrA*dMQ3zco~(GeeZ_o zR~Ei@^c3*Lb;=%a{EYH}`6|N<=0PMp-MGtN-+G9zQ~H@0rJEP`wZ{*hyJqou`r9qv z{lVeiaZ1ZBi>;sYOuYTuw=eCxul3c}u>QRt{g{C_SwnjoG|rF0S()myFW0yj9nCA{ zF44f9{QY0sPfZM2NKH&Mx@lx6PvO50d^s&+`u`A48{#sq>tL-Mihj%(wv?{q{~Nz} zpjdJQlJoe5`xNHztRXJrO8NA6%4S(=SEes47Ubqc8E_j{ey%c-&2=e5aQ*yjz><_< znZ)|@_{E$5`_Yg80?Fz" % form['text'] - else: - form_text = u"" - - def prep_return(msg, comp=None): - - from django.core.urlresolvers import reverse - - error = 'alert-info' - warning = 'alert-info' - success = 'alert-success' - outro = (u"
    Please check the " - u"SMS Syntax Page." % {'syntax_url': reverse('syntax')}) - - # no compatibility at all - if not comp: - alert = error - msg = (u"%(prefix)s %(msg)s" - % {'prefix': u"Your Form is not SMS-compatible" - u". If you want to later enable " - u"SMS Support, please fix:
    ", - 'msg': msg}) - # no blocker but could be improved - elif comp == 1: - alert = warning - msg = (u"%(prefix)s
      %(msg)s
    " - % {'prefix': u"Your form can be used with SMS, " - u"knowing that:", 'msg': msg}) - # SMS compatible - else: - outro = u"" - alert = success - - return {'type': alert, - 'text': u"%(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(_(u"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(_(u"All your groups must have an 'sms_field' " - u"(use 'meta' prefixed ones for non-fillable " - u"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( - _(u"Not all options in the choices list for " - u"%s have an " - u"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(_(u"Questions for which values can contain " - u"spaces are only allowed on last " - u"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(_(u"When allowing medias ('sms_allow_media'), your " - u"separator (%s) must be outside Base64 alphabet " - u"(letters, digits and +/=). " - u"You case use '#' instead." % separator)) - - # WARNINGS - warnings = [] - # sms_separator not set - if not json_survey.get('sms_separator', ''): - warnings.append(u"
  • You have not set a separator. Default '+' " - u"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(u"
  • You have 'date' fields without " - u"explicitly setting a date format. " - u"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(u"
  • You have 'datetime' fields without " - u"explicitly setting a datetime format. " - u"Default (%s) will be used.
  • " - % DEFAULT_DATETIME_FORMAT) - break - - # date or datetime format contain space - if 'date' in sensitive_fields: - warnings.append(u"
  • 'sms_date_format' contains space which will " - u"require 'date' questions to be positioned at " - u"the end of groups (%s).
  • " % date_format) - if 'datetime' in sensitive_fields: - warnings.append(u"
  • 'sms_datetime_format' contains space which will " - u"require 'datetime' questions to be positioned at " - u"the end of groups (%s).
  • " % datetime_format) - - if len(warnings): - return prep_return(u"".join(warnings), comp=1) - - # Good to go - return prep_return(_(u"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 486ae8653..000000000 --- a/onadata/apps/sms_support/views.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim: ai ts=4 sts=4 et sw=4 nu - -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': _(u"`identity` and `message` are " - u"both required and must not be " - u"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 e69de29bb..000000000 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 e69de29bb..000000000 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 51cddbc7a..000000000 --- a/onadata/apps/survey_report/templatetags/data_manipulation.py +++ /dev/null @@ -1,14 +0,0 @@ -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 6eab3aa2f..000000000 --- a/onadata/apps/survey_report/urls.py +++ /dev/null @@ -1,30 +0,0 @@ -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 bbb492fa3..000000000 --- a/onadata/apps/survey_report/views.py +++ /dev/null @@ -1,290 +0,0 @@ -# coding: utf-8 - -from __future__ import (unicode_literals, print_function, absolute_import, - division) - -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(_(u'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/libs/serializers/xform_serializer.py b/onadata/libs/serializers/xform_serializer.py index c6509209c..985600f9c 100644 --- a/onadata/libs/serializers/xform_serializer.py +++ b/onadata/libs/serializers/xform_serializer.py @@ -49,7 +49,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 @@ -69,6 +71,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') diff --git a/onadata/libs/utils/log.py b/onadata/libs/utils/log.py index 06e4364cb..c05983095 100644 --- a/onadata/libs/utils/log.py +++ b/onadata/libs/utils/log.py @@ -48,8 +48,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/settings/common.py b/onadata/settings/common.py index 4001608c7..ce2791b5b 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -241,9 +241,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', From e926a07b0923248e055fdd22e1cbb611e5110198 Mon Sep 17 00:00:00 2001 From: Olivier Leger Date: Wed, 29 Jul 2020 15:38:29 -0400 Subject: [PATCH 104/240] Remove GoogleDoc exports --- onadata/apps/main/google_doc.py | 201 ------------------ onadata/apps/main/google_export.py | 77 ------- onadata/apps/main/models/__init__.py | 1 - onadata/apps/main/models/google_oath.py | 10 - onadata/apps/main/tests/test_google_doc.py | 23 -- .../main/tests/test_google_docs_export.py | 51 ----- onadata/apps/main/urls.py | 10 - onadata/apps/viewer/models/export.py | 2 - onadata/apps/viewer/tasks.py | 7 +- onadata/apps/viewer/tests/test_export_list.py | 8 - onadata/apps/viewer/views.py | 123 +---------- onadata/libs/utils/google.py | 60 ------ 12 files changed, 12 insertions(+), 561 deletions(-) delete mode 100644 onadata/apps/main/google_doc.py delete mode 100644 onadata/apps/main/google_export.py delete mode 100644 onadata/apps/main/models/google_oath.py delete mode 100644 onadata/apps/main/tests/test_google_doc.py delete mode 100644 onadata/apps/main/tests/test_google_docs_export.py delete mode 100644 onadata/libs/utils/google.py diff --git a/onadata/apps/main/google_doc.py b/onadata/apps/main/google_doc.py deleted file mode 100644 index 9f1016184..000000000 --- a/onadata/apps/main/google_doc.py +++ /dev/null @@ -1,201 +0,0 @@ -import re -import urllib2 - -from django.template.loader import render_to_string -from django.template.defaultfilters import slugify - - -class Section(dict): - """ - A class used to represent a section of a page. A section should - have certain fields. 'level' denotes how nested this section is in - the document, like h1, h2, etc. 'id' is a string used to link to - this section. 'title' will be printed at the top the - section. 'content' is the html that will be printed as the meat of - the section. Notice that we use the 'section.html' template to - render a section as html, and the url provides a link that will be - used in the page's table of contents. - """ - - FIELDS = ['level', 'id', 'title', 'content'] - - def to_html(self): - return render_to_string('section.html', self) - - def url(self): - return u'%(title)s' % self - - -class TreeNode(list): - """ - This simple tree class will be used to construct the table of - contents for the page. - """ - - def __init__(self, value=None, parent=None): - self.value = value - self.parent = parent - list.__init__(self) - - def add_child(self, value): - child = TreeNode(value, self) - self.append(child) - return child - - -class GoogleDoc(object): - """ - This class provides a structure for dealing with a Google - Document. Most use cases will initialize a GoogleDoc by passing a - url to the init. This should be a public url that links to an html - version of the document. You can find this url by publishing your - Google Document to the web and copying the url. - - The primary method this class provides is 'to_html' which renders - this document as html in the Twitter Bootstrap format. - """ - - def __init__(self, url=None): - if url is not None: - self.set_html_from_url(url) - - def set_html_from_url(self, url): - f = urllib2.urlopen(url) - self.set_html(f.read()) - f.close() - - def set_html(self, html): - """ - When setting the html for this Google Document we do two - things: - - 1. We extract the content from the html. Using a regular - expression we pull the meat of the document out of the body - of the html, we also cut off the footer Google adds on - automatically. - - 2. We extract the various sections from the content of the - document. Again using a regular expression, we look for h1, - h2, ... tags to split the document up into sections. Note: - it is important when you are writing your Google Document - to use the heading text styles, so this code will split - things correctly. - """ - self._html = html - self._extract_content() - self._extract_sections() - - def _extract_content(self): - m = re.search(r'(.*)