diff --git a/.github/workflows/flask.yml b/.github/workflows/flask.yml index d94c15d8..16086b5b 100644 --- a/.github/workflows/flask.yml +++ b/.github/workflows/flask.yml @@ -45,11 +45,8 @@ jobs: - name: Install dependencies run: | sudo apt-get update -qy - sudo apt-get -y install postgresql-client + sudo apt-get -y install postgresql-client --no-install-recommends python -m pip install --upgrade pip - pip install virtualenv - virtualenv env - source env/bin/activate pip install flake8 pytest coverage pip install -r requirements.txt @@ -65,10 +62,21 @@ jobs: psql -h "localhost" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION citext SCHEMA public;" psql -h "localhost" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION pg_trgm SCHEMA public;" + - name: Lint with flake8 + run: | + # stop the build if: + # - E9,F63,F7,F82: Python syntax errors or undefined names + # - E501: line longer than 120 characters + # - C901: complexity greater than 10 + # - F401: modules imported but unused + # See: https://flake8.pycqa.org/en/latest/user/error-codes.html + flake8 . --select=E9,F63,F7,F82,E501,C901,F401 + flake8 . --exit-zero + - name: Run Tests run: | - source env/bin/activate - coverage run --source='ereuse_devicehub' env/bin/pytest -m mvp --maxfail=5 tests/ + export SECRET_KEY=`python3 -c 'import secrets; print(secrets.token_hex())'` + coverage run --source='ereuse_devicehub' -m pytest -m mvp --maxfail=5 tests/ coverage report --include='ereuse_devicehub/*' coverage xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 83520944..5eb71013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,27 +6,38 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ml). ## master - [1.0.12-beta] ## testing - [1.0.13-beta] -## [1.0.13-beta] +## [2.0.0-alpha] +First server render HTML version. Completely rewrites views of angular JS client on flask. +- [added] #193 render on backend devices and lots +- [added] #195 render on backend tags system +- [added] #196 render on backend action system +- [added] #201 render on backend Data Wipe action +- [added] #203 render on backend Trade action +- [added] #204 render on backend export files +- [added] #205 UX improvements +- [added] #208 render on backend filter for type of devices in the general list +- [changed] #191 pass to drop teal and use the pure flask and use render from flask +- [changed] #207 Create automatic tag only for Computers. +- [changed] #209 adding a new device in a lot if it is created from a lot +- [fixed] #206 fix 2 bugs about visibility devices when you are not the owner ## [1.0.12-beta] -- [changes] #187 now is possible duplicate slots of RAM. -- [changes] #188 Excel report devices allow to see device to old owners. +- [changed] #187 now is possible duplicate slots of RAM. +- [changed] #188 Excel report devices allow to see device to old owners. ## [1.0.11-beta] -- [addend] #186 adding property power_on_hours. +- [added] #186 adding property power_on_hours. ## [1.0.10-beta] -- [addend] #170 can delete/deactivate devices. -- [bugfix] #168 can to do a trade without devices. +- [added] #170 can delete/deactivate devices. - [added] #167 new actions of status devices: use, recycling, refurbish and management. -- [changes] #177 new structure of trade. -- [bugfix] #184 clean nested of schemas of lot - [added] #182 adding power on hours +- [changed] #177 new structure of trade. +- [fixed] #168 can to do a trade without devices. +- [fixed] #184 clean nested of schemas of lot ## [1.0.9-beta] - [added] #159 external document as proof of erase of disk @@ -34,7 +45,7 @@ ml). ## [1.0.8-beta] -- [bugfix] #161 fixing DataStorage with bigInteger +- [fixed] #161 fixing DataStorage with bigInteger ## [1.0.7-beta] - [added] #158 support for encrypted snapshots data @@ -42,26 +53,26 @@ ml). - [added] #140 adding endpoint for download the settings for usb workbench ## [1.0.6-beta] -- [bugfix] #143 biginteger instead of integer in TestDataStorage +- [fixed] #143 biginteger instead of integer in TestDataStorage ## [1.0.5-beta] - [added] #124 adding endpoint for extract the internal stats of use - [added] #122 system for verify all documents that it's produced from devicehub - [added] #127 add one code for every named tag - [added] #131 add one code for every device -- [bugfix] #138 search device with devicehubId +- [fixed] #138 search device with devicehubId ## [1.0.4-beta] - [added] #95 adding endpoint for check the hash of one report - [added] #98 adding endpoint for insert a new live - [added] #98 adding endpoint for get all licences in one query - [added] #102 adding endpoint for download metrics -- [bugfix] #100 fixing bug of scheme live -- [bugfix] #101 fixing bug when 2 users have one device and launch one live -- [changes] #114 clean blockchain of all models -- [changes] #118 deactivate manual merge -- [changes] #118 clean datas of public information of devices -- [remove] #114 remove proof system +- [changed] #114 clean blockchain of all models +- [changed] #118 deactivate manual merge +- [changed] #118 clean datas of public information of devices +- [fixed] #100 fixing bug of scheme live +- [fixed] #101 fixing bug when 2 users have one device and launch one live +- [removed] #114 remove proof system ## [1.0.3-beta] - [added] #85 add mac of network adapter to device hid @@ -69,6 +80,6 @@ ml). ## [1.0.2-beta] - [added] #87 allocate, deallocate and live actions -- [fixed] #89 save json on disk only for shapshots - [added] #83 add owner_id in all kind of device +- [fixed] #89 save json on disk only for shapshots - [fixed] #91 The most old time allow is 1970-01-01 diff --git a/ereuse_devicehub/__init__.py b/ereuse_devicehub/__init__.py index c69e4574..688b5e56 100644 --- a/ereuse_devicehub/__init__.py +++ b/ereuse_devicehub/__init__.py @@ -1 +1 @@ -__version__ = "1.0.12-beta" +__version__ = "2.0.0-alpha" diff --git a/ereuse_devicehub/cli.py b/ereuse_devicehub/cli.py index 4c253235..67054095 100644 --- a/ereuse_devicehub/cli.py +++ b/ereuse_devicehub/cli.py @@ -1,8 +1,8 @@ import os import click.testing -import ereuse_utils import flask.cli +import ereuse_utils from ereuse_devicehub.config import DevicehubConfig from ereuse_devicehub.devicehub import Devicehub @@ -11,7 +11,7 @@ import sys sys.ps1 = '\001\033[92m\002>>> \001\033[0m\002' sys.ps2= '\001\033[94m\002... \001\033[0m\002' -import os, readline, rlcompleter, atexit +import os, readline, atexit history_file = os.path.join(os.environ['HOME'], '.python_history') try: readline.read_history_file(history_file) diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index 01a98292..d1f89896 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -35,6 +35,7 @@ class DevicehubConfig(Config): import_resource(metric_def), ),) PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str] + SECRET_KEY = config('SECRET_KEY') DB_USER = config('DB_USER', 'dhub') DB_PASSWORD = config('DB_PASSWORD', 'ereuse') DB_HOST = config('DB_HOST', 'localhost') diff --git a/ereuse_devicehub/devicehub.py b/ereuse_devicehub/devicehub.py index a15e484a..7163173d 100644 --- a/ereuse_devicehub/devicehub.py +++ b/ereuse_devicehub/devicehub.py @@ -7,7 +7,8 @@ import click import click_spinner import ereuse_utils.cli from ereuse_utils.session import DevicehubClient -from flask.globals import _app_ctx_stack, g +from flask import _app_ctx_stack, g +from flask_login import LoginManager, current_user from flask_sqlalchemy import SQLAlchemy from teal.db import SchemaSQLAlchemy from teal.teal import Teal @@ -19,6 +20,7 @@ from ereuse_devicehub.db import db from ereuse_devicehub.dummy.dummy import Dummy from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.inventory import Inventory, InventoryDef +from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.templating import Environment @@ -61,6 +63,17 @@ class Devicehub(Teal): inv.command('search')(self.regenerate_search) self.before_request(self._prepare_request) + self.configure_extensions() + + def configure_extensions(self): + # configure Flask-Login + login_manager = LoginManager() + login_manager.init_app(self) + + @login_manager.user_loader + def load_user(user_id): + return User.query.get(user_id) + # noinspection PyMethodOverriding @click.option('--name', '-n', default='Test 1', @@ -150,6 +163,9 @@ class Devicehub(Teal): inv = g.inventory = Inventory.current # type: Inventory g.tag_provider = DevicehubClient(base_url=inv.tag_provider, token=DevicehubClient.encode_token(inv.tag_token)) + # NOTE: models init methods expects that current user is + # available on g.user (e.g. to initialize object owner) + g.user = current_user def create_client(self, email='user@dhub.com', password='1234'): client = UserClient(self, email, password, response_wrapper=self.response_class) diff --git a/ereuse_devicehub/forms.py b/ereuse_devicehub/forms.py new file mode 100644 index 00000000..d88c9cf1 --- /dev/null +++ b/ereuse_devicehub/forms.py @@ -0,0 +1,61 @@ +from flask_wtf import FlaskForm +from werkzeug.security import generate_password_hash +from wtforms import BooleanField, EmailField, PasswordField, validators + +from ereuse_devicehub.resources.user.models import User + + +class LoginForm(FlaskForm): + email = EmailField('Email Address', [validators.Length(min=6, max=35)]) + password = PasswordField('Password', [validators.DataRequired()]) + remember = BooleanField('Remember me') + + error_messages = { + 'invalid_login': ( + "Please enter a correct email and password. Note that both " + "fields may be case-sensitive." + ), + 'inactive': "This account is inactive.", + } + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + return False + + email = self.email.data + password = self.password.data + self.user_cache = self.authenticate(email, password) + + if self.user_cache is None: + self.form_errors.append(self.error_messages['invalid_login']) + return False + + return self.confirm_login_allowed(self.user_cache) + + def authenticate(self, email, password): + if email is None or password is None: + return + user = User.query.filter_by(email=email).first() + if user is None: + # Run the default password hasher once to reduce the timing + # difference between an existing and a nonexistent user (#20760). + generate_password_hash(password) + else: + if user.check_password(password): + return user + + def confirm_login_allowed(self, user): + """ + Controls whether the given User may log in. This is a policy setting, + independent of end-user authentication. This default behavior is to + allow login by active users, and reject login by inactive users. + If the given user cannot log in, this method should raise a + ``ValidationError``. + If the given user may log in, this method should return None. + """ + if not user.is_active: + self.form_errors.append(self.error_messages['inactive']) + + return user.is_active diff --git a/ereuse_devicehub/inventory/__init__.py b/ereuse_devicehub/inventory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py new file mode 100644 index 00000000..5eab3236 --- /dev/null +++ b/ereuse_devicehub/inventory/forms.py @@ -0,0 +1,1009 @@ +import copy +import json +from json.decoder import JSONDecodeError + +from boltons.urlutils import URL +from flask import g, request +from flask_wtf import FlaskForm +from sqlalchemy import or_ +from sqlalchemy.util import OrderedSet +from wtforms import ( + BooleanField, + DateField, + FileField, + FloatField, + Form, + HiddenField, + IntegerField, + MultipleFileField, + SelectField, + StringField, + TextAreaField, + URLField, + validators, +) +from wtforms.fields import FormField + +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.action.models import RateComputer, Snapshot, Trade +from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate +from ereuse_devicehub.resources.action.schemas import Snapshot as SnapshotSchema +from ereuse_devicehub.resources.action.views.snapshot import move_json, save_json +from ereuse_devicehub.resources.device.models import ( + SAI, + Cellphone, + Computer, + Device, + Keyboard, + MemoryCardReader, + Monitor, + Mouse, + Smartphone, + Tablet, +) +from ereuse_devicehub.resources.device.sync import Sync +from ereuse_devicehub.resources.documents.models import DataWipeDocument +from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware +from ereuse_devicehub.resources.hash_reports import insert_hash +from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.resources.tag.model import Tag +from ereuse_devicehub.resources.tradedocument.models import TradeDocument +from ereuse_devicehub.resources.user.exceptions import InsufficientPermission +from ereuse_devicehub.resources.user.models import User + +DEVICES = { + "All": ["All"], + "Computer": [ + "Desktop", + "Laptop", + "Server", + ], + "Monitor": ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"], + "Mobile, tablet & smartphone": ["Mobile", "Tablet", "Smartphone", "Cellphone"], + "DataStorage": ["HardDrive", "SolidStateDrive"], + "Accessories & Peripherals": [ + "GraphicCard", + "Motherboard", + "NetworkAdapter", + "Processor", + "RamModule", + "SoundCard", + "Battery", + "Keyboard", + "Mouse", + "MemoryCardReader", + ], +} + + +class FilterForm(FlaskForm): + filter = SelectField( + '', choices=DEVICES, default="Computer", render_kw={'class': "form-select"} + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + types_of_devices = [item for sublist in DEVICES.values() for item in sublist] + dev = request.args.get('filter') + self.device = dev if dev in types_of_devices else None + if self.device: + self.filter.data = self.device + + def search(self): + + if self.device: + return [self.device] + + return ['Desktop', 'Laptop', 'Server'] + + +class LotDeviceForm(FlaskForm): + lot = StringField('Lot', [validators.UUID()]) + devices = StringField('Devices', [validators.length(min=1)]) + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + return False + + self._lot = ( + Lot.query.outerjoin(Trade) + .filter(Lot.id == self.lot.data) + .filter( + or_( + Trade.user_from == g.user, + Trade.user_to == g.user, + Lot.owner_id == g.user.id, + ) + ) + .one() + ) + + devices = set(self.devices.data.split(",")) + self._devices = ( + Device.query.filter(Device.id.in_(devices)) + .filter(Device.owner_id == g.user.id) + .distinct() + .all() + ) + + return bool(self._devices) + + def save(self, commit=True): + trade = self._lot.trade + if trade: + for dev in self._devices: + if trade not in dev.actions: + trade.devices.add(dev) + + if self._devices: + self._lot.devices.update(self._devices) + db.session.add(self._lot) + + if commit: + db.session.commit() + + def remove(self, commit=True): + if self._devices: + self._lot.devices.difference_update(self._devices) + db.session.add(self._lot) + + if commit: + db.session.commit() + + +class LotForm(FlaskForm): + name = StringField('Name', [validators.length(min=1)]) + + def __init__(self, *args, **kwargs): + self.id = kwargs.pop('id', None) + self.instance = None + if self.id: + self.instance = ( + Lot.query.filter(Lot.id == self.id) + .filter(Lot.owner_id == g.user.id) + .one() + ) + super().__init__(*args, **kwargs) + if self.instance and not self.name.data: + self.name.data = self.instance.name + + def save(self): + if not self.id: + self.instance = Lot(name=self.name.data) + + self.populate_obj(self.instance) + + if not self.id: + self.id = self.instance.id + db.session.add(self.instance) + db.session.commit() + return self.id + + db.session.commit() + return self.id + + def remove(self): + if self.instance and not self.instance.trade: + self.instance.delete() + db.session.commit() + return self.instance + + +class UploadSnapshotForm(FlaskForm): + snapshot = MultipleFileField('Select a Snapshot File', [validators.DataRequired()]) + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + return False + + data = request.files.getlist(self.snapshot.name) + if not data: + return False + self.snapshots = [] + self.result = {} + for d in data: + filename = d.filename + self.result[filename] = 'Not processed' + d = d.stream.read() + if not d: + self.result[filename] = 'Error this snapshot is empty' + continue + + try: + d_json = json.loads(d) + except JSONDecodeError: + self.result[filename] = 'Error this snapshot is not a json' + continue + + uuid_snapshot = d_json.get('uuid') + if Snapshot.query.filter(Snapshot.uuid == uuid_snapshot).all(): + self.result[filename] = 'Error this snapshot exist' + continue + + self.snapshots.append((filename, d_json)) + + if not self.snapshots: + return False + + return True + + def save(self, commit=True): + if any([x == 'Error' for x in self.result.values()]): + return + # result = [] + self.sync = Sync() + schema = SnapshotSchema() + # self.tmp_snapshots = app.config['TMP_SNAPSHOTS'] + # TODO @cayop get correct var config + self.tmp_snapshots = '/tmp/' + for filename, snapshot_json in self.snapshots: + path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email) + snapshot_json.pop('debug', None) + snapshot_json = schema.load(snapshot_json) + response = self.build(snapshot_json) + + if hasattr(response, 'type'): + self.result[filename] = 'Ok' + else: + self.result[filename] = 'Error' + + move_json(self.tmp_snapshots, path_snapshot, g.user.email) + + if commit: + db.session.commit() + return response + + def build(self, snapshot_json): # noqa: C901 + # this is a copy adaptated from ereuse_devicehub.resources.action.views.snapshot + device = snapshot_json.pop('device') # type: Computer + components = None + if snapshot_json['software'] == ( + SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid + ): + components = snapshot_json.pop('components', None) + if isinstance(device, Computer) and device.hid: + device.add_mac_to_hid(components_snap=components) + snapshot = Snapshot(**snapshot_json) + + # Remove new actions from devices so they don't interfere with sync + actions_device = set(e for e in device.actions_one) + device.actions_one.clear() + if components: + actions_components = tuple( + set(e for e in c.actions_one) for c in components + ) + for component in components: + component.actions_one.clear() + + assert not device.actions_one + assert all(not c.actions_one for c in components) if components else True + db_device, remove_actions = self.sync.run(device, components) + + del device # Do not use device anymore + snapshot.device = db_device + snapshot.actions |= remove_actions | actions_device # Set actions to snapshot + # commit will change the order of the components by what + # the DB wants. Let's get a copy of the list so we preserve order + ordered_components = OrderedSet(x for x in snapshot.components) + + # Add the new actions to the db-existing devices and components + db_device.actions_one |= actions_device + if components: + for component, actions in zip(ordered_components, actions_components): + component.actions_one |= actions + snapshot.actions |= actions + + if snapshot.software == SnapshotSoftware.Workbench: + # Check ownership of (non-component) device to from current.user + if db_device.owner_id != g.user.id: + raise InsufficientPermission() + # Compute ratings + try: + rate_computer, price = RateComputer.compute(db_device) + except CannotRate: + pass + else: + snapshot.actions.add(rate_computer) + if price: + snapshot.actions.add(price) + elif snapshot.software == SnapshotSoftware.WorkbenchAndroid: + pass # TODO try except to compute RateMobile + # Check if HID is null and add Severity:Warning to Snapshot + if snapshot.device.hid is None: + snapshot.severity = Severity.Warning + + db.session.add(snapshot) + return snapshot + + +class NewDeviceForm(FlaskForm): + type = StringField('Type', [validators.DataRequired()]) + label = StringField('Label') + serial_number = StringField('Seria Number', [validators.DataRequired()]) + model = StringField('Model', [validators.DataRequired()]) + manufacturer = StringField('Manufacturer', [validators.DataRequired()]) + appearance = StringField('Appearance', [validators.Optional()]) + functionality = StringField('Functionality', [validators.Optional()]) + brand = StringField('Brand') + generation = IntegerField('Generation') + version = StringField('Version') + weight = FloatField('Weight', [validators.DataRequired()]) + width = FloatField('Width', [validators.DataRequired()]) + height = FloatField('Height', [validators.DataRequired()]) + depth = FloatField('Depth', [validators.DataRequired()]) + variant = StringField('Variant', [validators.Optional()]) + sku = StringField('SKU', [validators.Optional()]) + image = StringField('Image', [validators.Optional(), validators.URL()]) + imei = IntegerField('IMEI', [validators.Optional()]) + meid = StringField('MEID', [validators.Optional()]) + resolution = IntegerField('Resolution width', [validators.Optional()]) + screen = FloatField('Screen size', [validators.Optional()]) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.devices = { + "Smartphone": Smartphone, + "Tablet": Tablet, + "Cellphone": Cellphone, + "Monitor": Monitor, + "Mouse": Mouse, + "Keyboard": Keyboard, + "SAI": SAI, + "MemoryCardReader": MemoryCardReader, + } + + if not self.generation.data: + self.generation.data = 1 + + if not self.weight.data: + self.weight.data = 0.1 + + if not self.height.data: + self.height.data = 0.1 + + if not self.width.data: + self.width.data = 0.1 + + if not self.depth.data: + self.depth.data = 0.1 + + def validate(self, extra_validators=None): # noqa: C901 + error = ["Not a correct value"] + is_valid = super().validate(extra_validators) + + if self.generation.data < 1: + self.generation.errors = error + is_valid = False + + if self.weight.data < 0.1: + self.weight.errors = error + is_valid = False + + if self.height.data < 0.1: + self.height.errors = error + is_valid = False + + if self.width.data < 0.1: + self.width.errors = error + is_valid = False + + if self.depth.data < 0.1: + self.depth.errors = error + is_valid = False + + if self.imei.data: + if not 13 < len(str(self.imei.data)) < 17: + self.imei.errors = error + is_valid = False + + if self.meid.data: + meid = self.meid.data + if not 13 < len(meid) < 17: + is_valid = False + try: + int(meid, 16) + except ValueError: + self.meid.errors = error + is_valid = False + + if not is_valid: + return False + + if self.image.data == '': + self.image.data = None + if self.manufacturer.data: + self.manufacturer.data = self.manufacturer.data.lower() + if self.model.data: + self.model.data = self.model.data.lower() + if self.serial_number.data: + self.serial_number.data = self.serial_number.data.lower() + + return True + + def save(self, commit=True): + + json_snapshot = { + 'type': 'Snapshot', + 'software': 'Web', + 'version': '11.0', + 'device': { + 'type': self.type.data, + 'model': self.model.data, + 'manufacturer': self.manufacturer.data, + 'serialNumber': self.serial_number.data, + 'brand': self.brand.data, + 'version': self.version.data, + 'generation': self.generation.data, + 'sku': self.sku.data, + 'weight': self.weight.data, + 'width': self.width.data, + 'height': self.height.data, + 'depth': self.depth.data, + 'variant': self.variant.data, + 'image': self.image.data, + }, + } + + if self.appearance.data or self.functionality.data: + json_snapshot['device']['actions'] = [ + { + 'type': 'VisualTest', + 'appearanceRange': self.appearance.data, + 'functionalityRange': self.functionality.data, + } + ] + + upload_form = UploadSnapshotForm() + upload_form.sync = Sync() + + schema = SnapshotSchema() + self.tmp_snapshots = '/tmp/' + path_snapshot = save_json(json_snapshot, self.tmp_snapshots, g.user.email) + snapshot_json = schema.load(json_snapshot) + + if self.type.data == 'Monitor': + snapshot_json['device'].resolution_width = self.resolution.data + snapshot_json['device'].size = self.screen.data + + if self.type.data in ['Smartphone', 'Tablet', 'Cellphone']: + snapshot_json['device'].imei = self.imei.data + snapshot_json['device'].meid = self.meid.data + + snapshot = upload_form.build(snapshot_json) + + move_json(self.tmp_snapshots, path_snapshot, g.user.email) + if self.type.data == 'Monitor': + snapshot.device.resolution = self.resolution.data + snapshot.device.screen = self.screen.data + + if commit: + db.session.commit() + return snapshot + + +class TagForm(FlaskForm): + code = StringField('Code', [validators.length(min=1)]) + + def validate(self, extra_validators=None): + error = ["This value is being used"] + is_valid = super().validate(extra_validators) + if not is_valid: + return False + tag = Tag.query.filter(Tag.id == self.code.data).all() + if tag: + self.code.errors = error + return False + + return True + + def save(self): + self.instance = Tag(id=self.code.data) + db.session.add(self.instance) + db.session.commit() + return self.instance + + def remove(self): + if not self.instance.device and not self.instance.provider: + self.instance.delete() + db.session.commit() + return self.instance + + +class TagUnnamedForm(FlaskForm): + amount = IntegerField('amount') + + def save(self): + num = self.amount.data + tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)]) + tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id] + db.session.add_all(tags) + db.session.commit() + return tags + + +class TagDeviceForm(FlaskForm): + tag = SelectField('Tag', choices=[]) + device = StringField('Device', [validators.Optional()]) + + def __init__(self, *args, **kwargs): + self.delete = kwargs.pop('delete', None) + self.device_id = kwargs.pop('device', None) + + super().__init__(*args, **kwargs) + + if self.delete: + tags = ( + Tag.query.filter(Tag.owner_id == g.user.id) + .filter_by(device_id=self.device_id) + .order_by(Tag.id) + ) + else: + tags = ( + Tag.query.filter(Tag.owner_id == g.user.id) + .filter_by(device_id=None) + .order_by(Tag.id) + ) + + self.tag.choices = [(tag.id, tag.id) for tag in tags] + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + return False + + self._tag = ( + Tag.query.filter(Tag.id == self.tag.data) + .filter(Tag.owner_id == g.user.id) + .one() + ) + + if not self.delete and self._tag.device_id: + self.tag.errors = [("This tag is actualy in use.")] + return False + + if self.device.data: + try: + self.device.data = int(self.device.data.split(',')[-1]) + except: # noqa: E722 + self.device.data = None + + if self.device_id or self.device.data: + self.device_id = self.device_id or self.device.data + self._device = ( + Device.query.filter(Device.id == self.device_id) + .filter(Device.owner_id == g.user.id) + .one() + ) + + return True + + def save(self): + self._tag.device_id = self._device.id + db.session.add(self._tag) + db.session.commit() + + def remove(self): + self._tag.device = None + db.session.add(self._tag) + db.session.commit() + + +class NewActionForm(FlaskForm): + name = StringField( + 'Name', + [validators.length(max=50)], + description="A name or title of the event. Something to look for.", + ) + devices = HiddenField() + date = DateField( + 'Date', + [validators.Optional()], + description="""When the action ends. For some actions like booking + the time when it expires, for others like renting the + time that the end rents. For specific actions, it is the + time in which they are carried out; differs from created + in that created is where the system receives the action.""", + ) + severity = SelectField( + 'Severity', + choices=[(v.name, v.name) for v in Severity], + description="""An indicator that evaluates the execution of the event. + For example, failed events are set to Error""", + ) + description = TextAreaField('Description') + lot = HiddenField() + type = HiddenField() + + def validate(self, extra_validators=None): + is_valid = self.generic_validation(extra_validators=extra_validators) + + if not is_valid: + return False + + self._devices = OrderedSet() + if self.devices.data: + devices = set(self.devices.data.split(",")) + self._devices = OrderedSet( + Device.query.filter(Device.id.in_(devices)) + .filter(Device.owner_id == g.user.id) + .all() + ) + + if not self._devices: + return False + + return True + + def generic_validation(self, extra_validators=None): + # Some times we want check validations without devices list + return super().validate(extra_validators) + + def save(self): + Model = db.Model._decl_class_registry.data[self.type.data]() + self.instance = Model() + devices = self.devices.data + severity = self.severity.data + self.devices.data = self._devices + self.severity.data = Severity[self.severity.data] + + self.populate_obj(self.instance) + db.session.add(self.instance) + db.session.commit() + + self.devices.data = devices + self.severity.data = severity + + return self.instance + + def check_valid(self): + if self.type.data in ['', None]: + return + + if not self.validate(): + return self.type.data + + +class AllocateForm(NewActionForm): + start_time = DateField('Start time') + end_time = DateField('End time') + final_user_code = StringField('Final user code', [validators.length(max=50)]) + transaction = StringField('Transaction', [validators.length(max=50)]) + end_users = IntegerField('End users') + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + start_time = self.start_time.data + end_time = self.end_time.data + if start_time and end_time and end_time < start_time: + error = ['The action cannot finish before it starts.'] + self.start_time.errors = error + self.end_time.errors = error + is_valid = False + + if not self.end_users.data: + self.end_users.errors = ["You need to specify a number of users"] + is_valid = False + + return is_valid + + +class DataWipeDocumentForm(Form): + date = DateField( + 'Date', [validators.Optional()], description="Date when was data wipe" + ) + url = URLField( + 'Url', [validators.Optional()], description="Url where the document resides" + ) + success = BooleanField( + 'Success', [validators.Optional()], description="The erase was success or not?" + ) + software = StringField( + 'Software', + [validators.Optional()], + description="Which software has you use for erase the disks", + ) + id_document = StringField( + 'Document Id', + [validators.Optional()], + description="Identification number of document", + ) + file_name = FileField( + 'File', + [validators.DataRequired()], + description="""This file is not stored on our servers, it is only used to + generate a digital signature and obtain the name of the file.""", + ) + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + return is_valid + + def save(self, commit=True): + file_name = '' + file_hash = '' + if self.file_name.data: + file_name = self.file_name.data.filename + file_hash = insert_hash(self.file_name.data.read(), commit=False) + + self.url.data = URL(self.url.data) + self._obj = DataWipeDocument( + document_type='DataWipeDocument', + ) + self.populate_obj(self._obj) + self._obj.file_name = file_name + self._obj.file_hash = file_hash + db.session.add(self._obj) + if commit: + db.session.commit() + + return self._obj + + +class DataWipeForm(NewActionForm): + document = FormField(DataWipeDocumentForm) + + def save(self): + self.document.form.save(commit=False) + + Model = db.Model._decl_class_registry.data[self.type.data]() + self.instance = Model() + devices = self.devices.data + severity = self.severity.data + self.devices.data = self._devices + self.severity.data = Severity[self.severity.data] + + document = copy.copy(self.document) + del self.document + self.populate_obj(self.instance) + self.instance.document = document.form._obj + db.session.add(self.instance) + db.session.commit() + + self.devices.data = devices + self.severity.data = severity + self.document = document + + return self.instance + + +class TradeForm(NewActionForm): + user_from = StringField( + 'Supplier', + [validators.Optional()], + description="Please enter the supplier's email address", + render_kw={'data-email': ""}, + ) + user_to = StringField( + 'Receiver', + [validators.Optional()], + description="Please enter the receiver's email address", + render_kw={'data-email': ""}, + ) + confirm = BooleanField( + 'Confirm', + [validators.Optional()], + default=True, + description="I need confirmation from the other user for every device and document.", + ) + code = StringField( + 'Code', + [validators.Optional()], + description="If you don't need confirm, you need put a code for trace the user in the statistics.", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user_from.render_kw['data-email'] = g.user.email + self.user_to.render_kw['data-email'] = g.user.email + self._lot = ( + Lot.query.outerjoin(Trade) + .filter(Lot.id == self.lot.data) + .filter( + or_( + Trade.user_from == g.user, + Trade.user_to == g.user, + Lot.owner_id == g.user.id, + ) + ) + .one() + ) + + def validate(self, extra_validators=None): + is_valid = self.generic_validation(extra_validators=extra_validators) + email_from = self.user_from.data + email_to = self.user_to.data + + if not self.confirm.data and not self.code.data: + self.code.errors = ["If you don't want to confirm, you need a code"] + is_valid = False + + if ( + self.confirm.data + and not (email_from and email_to) + or email_to == email_from + or g.user.email not in [email_from, email_to] + ): + + errors = ["If you want confirm, you need a correct email"] + self.user_to.errors = errors + self.user_from.errors = errors + + is_valid = False + + if self.confirm.data and is_valid: + user_to = User.query.filter_by(email=email_to).first() or g.user + user_from = User.query.filter_by(email=email_from).first() or g.user + if user_to == user_from: + is_valid = False + else: + self.db_user_to = user_to + self.db_user_from = user_from + + self.has_errors = not is_valid + return is_valid + + def save(self, commit=True): + if self.has_errors: + raise ValueError( + "The %s could not be saved because the data didn't validate." + % (self.instance._meta.object_name) + ) + if not self.confirm.data: + self.create_phantom_account() + self.prepare_instance() + self.create_automatic_trade() + + if commit: + db.session.commit() + + return self.instance + + def prepare_instance(self): + Model = db.Model._decl_class_registry.data['Trade']() + self.instance = Model() + self.instance.user_from = self.db_user_from + self.instance.user_to = self.db_user_to + self.instance.lot_id = self._lot.id + self.instance.devices = self._lot.devices + self.instance.code = self.code.data + self.instance.confirm = self.confirm.data + self.instance.date = self.date.data + self.instance.name = self.name.data + self.instance.description = self.description.data + db.session.add(self.instance) + + def create_phantom_account(self): + """ + If exist both users not to do nothing + If exist from but not to: + search if exist in the DB + if exist use it + else create new one + The same if exist to but not from + + """ + user_from = self.user_from.data + user_to = self.user_to.data + code = self.code.data + + if user_from and user_to: + # both users exist, no further action is necessary + return + + # Create receiver (to) phantom account + if user_from and not user_to: + assert g.user.email == user_from + + self.user_from = g.user + self.user_to = self.get_or_create_user(code) + + # Create supplier (from) phantom account + if not user_from and user_to: + assert g.user.email == user_to + + self.user_from = self.get_or_create_user(code) + self.user_to = g.user + + self.db_user_to = self.user_to + self.db_user_from = self.user_from + + def get_or_create_user(self, code): + email = "{}_{}@dhub.com".format(str(g.user.id), code) + user = User.query.filter_by(email=email).first() + if not user: + user = User(email=email, password='', active=False, phantom=True) + db.session.add(user) + return user + + def create_automatic_trade(self): + # This method change the ownership of devices + # do nothing if an explicit confirmation is required + if self.confirm.data: + return + + # Change the owner for every devices + for dev in self._lot.devices: + dev.change_owner(self.db_user_to) + + def check_valid(self): + if self.user_from.data == self.user_to.data: + return + + if self.user_from.data == g.user.email: + return 'user_to' + + if self.user_to.data == g.user.email: + return 'user_from' + + +class TradeDocumentForm(FlaskForm): + url = URLField( + 'Url', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="Url where the document resides", + ) + description = StringField( + 'Description', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="", + ) + id_document = StringField( + 'Document Id', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="Identification number of document", + ) + date = DateField( + 'Date', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="", + ) + file_name = FileField( + 'File', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + description="""This file is not stored on our servers, it is only used to + generate a digital signature and obtain the name of the file.""", + ) + + def __init__(self, *args, **kwargs): + lot_id = kwargs.pop('lot') + super().__init__(*args, **kwargs) + self._lot = Lot.query.filter(Lot.id == lot_id).one() + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if g.user not in [self._lot.trade.user_from, self._lot.trade.user_to]: + is_valid = False + + return is_valid + + def save(self, commit=True): + file_name = '' + file_hash = '' + if self.file_name.data: + file_name = self.file_name.data.filename + file_hash = insert_hash(self.file_name.data.read(), commit=False) + + self.url.data = URL(self.url.data) + self._obj = TradeDocument(lot_id=self._lot.id) + self.populate_obj(self._obj) + self._obj.file_name = file_name + self._obj.file_hash = file_hash + db.session.add(self._obj) + self._lot.trade.documents.add(self._obj) + if commit: + db.session.commit() + + return self._obj diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py new file mode 100644 index 00000000..4e827be4 --- /dev/null +++ b/ereuse_devicehub/inventory/views.py @@ -0,0 +1,736 @@ +import csv +import logging +from io import StringIO + +import flask +import flask_weasyprint +from flask import Blueprint, g, make_response, request, url_for +from flask.views import View +from flask_login import current_user, login_required +from requests.exceptions import ConnectionError +from sqlalchemy import or_ +from werkzeug.exceptions import NotFound + +from ereuse_devicehub import __version__, messages +from ereuse_devicehub.db import db +from ereuse_devicehub.inventory.forms import ( + AllocateForm, + DataWipeForm, + FilterForm, + LotDeviceForm, + LotForm, + NewActionForm, + NewDeviceForm, + TagDeviceForm, + TagForm, + TagUnnamedForm, + TradeDocumentForm, + TradeForm, + UploadSnapshotForm, +) +from ereuse_devicehub.resources.action.models import Trade +from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device +from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow +from ereuse_devicehub.resources.hash_reports import insert_hash +from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.resources.tag.model import Tag + +devices = Blueprint('inventory', __name__, url_prefix='/inventory') + +logger = logging.getLogger(__name__) + + +class GenericMixView(View): + def get_lots(self): + return ( + Lot.query.outerjoin(Trade) + .filter( + or_( + Trade.user_from == g.user, + Trade.user_to == g.user, + Lot.owner_id == g.user.id, + ) + ) + .distinct() + ) + + +class DeviceListMix(GenericMixView): + decorators = [login_required] + template_name = 'inventory/device_list.html' + + def get_context(self, lot_id): + form_filter = FilterForm() + filter_types = form_filter.search() + lots = self.get_lots() + lot = None + tags = ( + Tag.query.filter(Tag.owner_id == current_user.id) + .filter(Tag.device_id.is_(None)) + .order_by(Tag.id.asc()) + ) + + if lot_id: + lot = lots.filter(Lot.id == lot_id).one() + devices = lot.devices + if "All" not in filter_types: + devices = [dev for dev in lot.devices if dev.type in filter_types] + devices = sorted(devices, key=lambda x: x.updated, reverse=True) + form_new_action = NewActionForm(lot=lot.id) + form_new_allocate = AllocateForm(lot=lot.id) + form_new_datawipe = DataWipeForm(lot=lot.id) + form_new_trade = TradeForm( + lot=lot.id, + user_to=g.user.email, + user_from=g.user.email, + ) + else: + if "All" in filter_types: + devices = ( + Device.query.filter(Device.owner_id == current_user.id) + .filter_by(lots=None) + .order_by(Device.updated.desc()) + ) + else: + devices = ( + Device.query.filter(Device.owner_id == current_user.id) + .filter_by(lots=None) + .filter(Device.type.in_(filter_types)) + .order_by(Device.updated.desc()) + ) + + form_new_action = NewActionForm() + form_new_allocate = AllocateForm() + form_new_datawipe = DataWipeForm() + form_new_trade = '' + action_devices = form_new_action.devices.data + list_devices = [] + if action_devices: + list_devices.extend([int(x) for x in action_devices.split(",")]) + + self.context = { + 'devices': devices, + 'lots': lots, + 'form_lot_device': LotDeviceForm(), + 'form_tag_device': TagDeviceForm(), + 'form_new_action': form_new_action, + 'form_new_allocate': form_new_allocate, + 'form_new_datawipe': form_new_datawipe, + 'form_new_trade': form_new_trade, + 'form_filter': form_filter, + 'lot': lot, + 'tags': tags, + 'list_devices': list_devices, + 'version': __version__, + } + + return self.context + + +class DeviceListView(DeviceListMix): + def dispatch_request(self, lot_id=None): + self.get_context(lot_id) + return flask.render_template(self.template_name, **self.context) + + +class DeviceDetailView(GenericMixView): + decorators = [login_required] + template_name = 'inventory/device_detail.html' + + def dispatch_request(self, id): + lots = self.get_lots() + device = ( + Device.query.filter(Device.owner_id == current_user.id) + .filter(Device.devicehub_id == id) + .one() + ) + + context = { + 'device': device, + 'lots': lots, + 'page_title': 'Device {}'.format(device.devicehub_id), + 'version': __version__, + } + return flask.render_template(self.template_name, **context) + + +class LotDeviceAddView(View): + methods = ['POST'] + decorators = [login_required] + template_name = 'inventory/device_list.html' + + def dispatch_request(self): + form = LotDeviceForm() + if form.validate_on_submit(): + form.save(commit=False) + messages.success( + 'Add devices to lot "{}" successfully!'.format(form._lot.name) + ) + db.session.commit() + else: + messages.error('Error adding devices to lot!') + + next_url = request.referrer or url_for('inventory.devicelist') + return flask.redirect(next_url) + + +class LotDeviceDeleteView(View): + methods = ['POST'] + decorators = [login_required] + template_name = 'inventory/device_list.html' + + def dispatch_request(self): + form = LotDeviceForm() + if form.validate_on_submit(): + form.remove(commit=False) + messages.success( + 'Remove devices from lot "{}" successfully!'.format(form._lot.name) + ) + db.session.commit() + else: + messages.error('Error removing devices from lot!') + + next_url = request.referrer or url_for('inventory.devicelist') + return flask.redirect(next_url) + + +class LotCreateView(GenericMixView): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/lot.html' + title = "Add a new lot" + + def dispatch_request(self): + form = LotForm() + if form.validate_on_submit(): + form.save() + next_url = url_for('inventory.lotdevicelist', lot_id=form.id) + return flask.redirect(next_url) + + lots = self.get_lots() + context = { + 'form': form, + 'title': self.title, + 'lots': lots, + 'version': __version__, + } + return flask.render_template(self.template_name, **context) + + +class LotUpdateView(View): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/lot.html' + title = "Edit a new lot" + + def dispatch_request(self, id): + form = LotForm(id=id) + if form.validate_on_submit(): + form.save() + next_url = url_for('inventory.lotdevicelist', lot_id=id) + return flask.redirect(next_url) + + lots = Lot.query.filter(Lot.owner_id == current_user.id) + context = { + 'form': form, + 'title': self.title, + 'lots': lots, + 'version': __version__, + } + return flask.render_template(self.template_name, **context) + + +class LotDeleteView(View): + methods = ['GET'] + decorators = [login_required] + template_name = 'inventory/device_list.html' + + def dispatch_request(self, id): + form = LotForm(id=id) + if form.instance.trade: + msg = "Sorry, the lot cannot be deleted because have a trade action " + messages.error(msg) + next_url = url_for('inventory.lotdevicelist', lot_id=id) + return flask.redirect(next_url) + + form.remove() + next_url = url_for('inventory.devicelist') + return flask.redirect(next_url) + + +class UploadSnapshotView(GenericMixView): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/upload_snapshot.html' + + def dispatch_request(self, lot_id=None): + lots = self.get_lots() + form = UploadSnapshotForm() + context = { + 'page_title': 'Upload Snapshot', + 'lots': lots, + 'form': form, + 'lot_id': lot_id, + 'version': __version__, + } + if form.validate_on_submit(): + snapshot = form.save(commit=False) + if lot_id: + lot = lots.filter(Lot.id == lot_id).one() + lot.devices.add(snapshot.device) + db.session.add(lot) + db.session.commit() + + return flask.render_template(self.template_name, **context) + + +class DeviceCreateView(GenericMixView): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/device_create.html' + + def dispatch_request(self, lot_id=None): + lots = self.get_lots() + form = NewDeviceForm() + context = { + 'page_title': 'New Device', + 'lots': lots, + 'form': form, + 'lot_id': lot_id, + 'version': __version__, + } + if form.validate_on_submit(): + snapshot = form.save(commit=False) + next_url = url_for('inventory.devicelist') + if lot_id: + next_url = url_for('inventory.lotdevicelist', lot_id=lot_id) + lot = lots.filter(Lot.id == lot_id).one() + lot.devices.add(snapshot.device) + db.session.add(lot) + + db.session.commit() + messages.success('Device "{}" created successfully!'.format(form.type.data)) + return flask.redirect(next_url) + + return flask.render_template(self.template_name, **context) + + +class TagListView(View): + methods = ['GET'] + decorators = [login_required] + template_name = 'inventory/tag_list.html' + + def dispatch_request(self): + lots = Lot.query.filter(Lot.owner_id == current_user.id) + tags = Tag.query.filter(Tag.owner_id == current_user.id).order_by(Tag.id) + context = { + 'lots': lots, + 'tags': tags, + 'page_title': 'Tags Management', + 'version': __version__, + } + return flask.render_template(self.template_name, **context) + + +class TagAddView(View): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/tag_create.html' + + def dispatch_request(self): + lots = Lot.query.filter(Lot.owner_id == current_user.id) + context = {'page_title': 'New Tag', 'lots': lots, 'version': __version__} + form = TagForm() + if form.validate_on_submit(): + form.save() + next_url = url_for('inventory.taglist') + return flask.redirect(next_url) + + return flask.render_template(self.template_name, form=form, **context) + + +class TagAddUnnamedView(View): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/tag_create_unnamed.html' + + def dispatch_request(self): + lots = Lot.query.filter(Lot.owner_id == current_user.id) + context = { + 'page_title': 'New Unnamed Tag', + 'lots': lots, + 'version': __version__, + } + form = TagUnnamedForm() + if form.validate_on_submit(): + try: + form.save() + except ConnectionError as e: + logger.error( + "Error while trying to connect to tag server: {}".format(e) + ) + msg = ( + "Sorry, we cannot create the unnamed tags requested because " + "some error happens while connecting to the tag server!" + ) + messages.error(msg) + + next_url = url_for('inventory.taglist') + return flask.redirect(next_url) + + return flask.render_template(self.template_name, form=form, **context) + + +class TagDetailView(View): + decorators = [login_required] + template_name = 'inventory/tag_detail.html' + + def dispatch_request(self, id): + lots = Lot.query.filter(Lot.owner_id == current_user.id) + tag = ( + Tag.query.filter(Tag.owner_id == current_user.id).filter(Tag.id == id).one() + ) + + context = { + 'lots': lots, + 'tag': tag, + 'page_title': '{} Tag'.format(tag.code), + 'version': __version__, + } + return flask.render_template(self.template_name, **context) + + +class TagLinkDeviceView(View): + methods = ['POST'] + decorators = [login_required] + # template_name = 'inventory/device_list.html' + + def dispatch_request(self): + form = TagDeviceForm() + if form.validate_on_submit(): + form.save() + + return flask.redirect(request.referrer) + + +class TagUnlinkDeviceView(View): + methods = ['POST', 'GET'] + decorators = [login_required] + template_name = 'inventory/tag_unlink_device.html' + + def dispatch_request(self, id): + lots = Lot.query.filter(Lot.owner_id == current_user.id) + form = TagDeviceForm(delete=True, device=id) + if form.validate_on_submit(): + form.remove() + + next_url = url_for('inventory.devicelist') + return flask.redirect(next_url) + + return flask.render_template( + self.template_name, + form=form, + lots=lots, + referrer=request.referrer, + version=__version__, + ) + + +class NewActionView(View): + methods = ['POST'] + decorators = [login_required] + form_class = NewActionForm + + def dispatch_request(self): + self.form = self.form_class() + + if self.form.validate_on_submit(): + self.form.save() + messages.success( + 'Action "{}" created successfully!'.format(self.form.type.data) + ) + + next_url = self.get_next_url() + return flask.redirect(next_url) + + def get_next_url(self): + lot_id = self.form.lot.data + + if lot_id: + return url_for('inventory.lotdevicelist', lot_id=lot_id) + + return url_for('inventory.devicelist') + + +class NewAllocateView(NewActionView, DeviceListMix): + methods = ['POST'] + form_class = AllocateForm + + def dispatch_request(self): + self.form = self.form_class() + + if self.form.validate_on_submit(): + self.form.save() + messages.success( + 'Action "{}" created successfully!'.format(self.form.type.data) + ) + + next_url = self.get_next_url() + return flask.redirect(next_url) + + lot_id = self.form.lot.data + self.get_context(lot_id) + self.context['form_new_allocate'] = self.form + return flask.render_template(self.template_name, **self.context) + + +class NewDataWipeView(NewActionView, DeviceListMix): + methods = ['POST'] + form_class = DataWipeForm + + def dispatch_request(self): + self.form = self.form_class() + + if self.form.validate_on_submit(): + self.form.save() + messages.success( + 'Action "{}" created successfully!'.format(self.form.type.data) + ) + + next_url = self.get_next_url() + return flask.redirect(next_url) + + lot_id = self.form.lot.data + self.get_context(lot_id) + self.context['form_new_datawipe'] = self.form + return flask.render_template(self.template_name, **self.context) + + +class NewTradeView(NewActionView, DeviceListMix): + methods = ['POST'] + form_class = TradeForm + + def dispatch_request(self): + self.form = self.form_class() + + if self.form.validate_on_submit(): + self.form.save() + messages.success( + 'Action "{}" created successfully!'.format(self.form.type.data) + ) + + next_url = self.get_next_url() + return flask.redirect(next_url) + + lot_id = self.form.lot.data + self.get_context(lot_id) + self.context['form_new_trade'] = self.form + return flask.render_template(self.template_name, **self.context) + + +class NewTradeDocumentView(View): + methods = ['POST', 'GET'] + decorators = [login_required] + template_name = 'inventory/trade_document.html' + form_class = TradeDocumentForm + title = "Add new document" + + def dispatch_request(self, lot_id): + self.form = self.form_class(lot=lot_id) + + if self.form.validate_on_submit(): + self.form.save() + messages.success('Document created successfully!') + next_url = url_for('inventory.lotdevicelist', lot_id=lot_id) + return flask.redirect(next_url) + + return flask.render_template( + self.template_name, form=self.form, title=self.title, version=__version__ + ) + + +class ExportsView(View): + methods = ['GET'] + decorators = [login_required] + + def dispatch_request(self, export_id): + export_ids = { + 'metrics': self.metrics, + 'devices': self.devices_list, + 'certificates': self.erasure, + 'links': self.public_links, + } + + if export_id not in export_ids: + return NotFound() + return export_ids[export_id]() + + def find_devices(self): + args = request.args.get('ids') + ids = args.split(',') if args else [] + query = Device.query.filter(Device.owner == g.user) + return query.filter(Device.devicehub_id.in_(ids)) + + def response_csv(self, data, name): + bfile = data.getvalue().encode('utf-8') + # insert proof + insert_hash(bfile) + output = make_response(bfile) + output.headers['Content-Disposition'] = 'attachment; filename={}'.format(name) + output.headers['Content-type'] = 'text/csv' + return output + + def devices_list(self): + """Get device query and put information in csv format.""" + data = StringIO() + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') + first = True + + for device in self.find_devices(): + d = DeviceRow(device, {}) + if first: + cw.writerow(d.keys()) + first = False + cw.writerow(d.values()) + + return self.response_csv(data, "export.csv") + + def metrics(self): + """Get device query and put information in csv format.""" + data = StringIO() + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') + first = True + devs_id = [] + # Get the allocate info + for device in self.find_devices(): + devs_id.append(device.id) + for allocate in device.get_metrics(): + d = ActionRow(allocate) + if first: + cw.writerow(d.keys()) + first = False + cw.writerow(d.values()) + + # Get the trade info + query_trade = Trade.query.filter( + Trade.devices.any(Device.id.in_(devs_id)) + ).all() + + lot_id = request.args.get('lot') + if lot_id and not query_trade: + lot = Lot.query.filter_by(id=lot_id).one() + if hasattr(lot, "trade") and lot.trade: + if g.user in [lot.trade.user_from, lot.trade.user_to]: + query_trade = [lot.trade] + + for trade in query_trade: + data_rows = trade.get_metrics() + for row in data_rows: + d = ActionRow(row) + if first: + cw.writerow(d.keys()) + first = False + cw.writerow(d.values()) + + return self.response_csv(data, "actions_export.csv") + + def public_links(self): + # get a csv with the publink links of this devices + data = StringIO() + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') + cw.writerow(['links']) + host_url = request.host_url + for dev in self.find_devices(): + code = dev.devicehub_id + link = [f"{host_url}devices/{code}"] + cw.writerow(link) + + return self.response_csv(data, "links.csv") + + def erasure(self): + template = self.build_erasure_certificate() + res = flask_weasyprint.render_pdf( + flask_weasyprint.HTML(string=template), + download_filename='erasure-certificate.pdf', + ) + insert_hash(res.data) + return res + + def build_erasure_certificate(self): + erasures = [] + for device in self.find_devices(): + if isinstance(device, Computer): + for privacy in device.privacy: + erasures.append(privacy) + elif isinstance(device, DataStorage): + if device.privacy: + erasures.append(device.privacy) + + params = { + 'title': 'Erasure Certificate', + 'erasures': tuple(erasures), + 'url_pdf': '', + } + return flask.render_template('inventory/erasure.html', **params) + + +devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add')) +devices.add_url_rule('/action/trade/add/', view_func=NewTradeView.as_view('trade_add')) +devices.add_url_rule( + '/action/allocate/add/', view_func=NewAllocateView.as_view('allocate_add') +) +devices.add_url_rule( + '/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add') +) +devices.add_url_rule( + '/lot//trade-document/add/', + view_func=NewTradeDocumentView.as_view('trade_document_add'), +) +devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist')) +devices.add_url_rule( + '/device//', view_func=DeviceDetailView.as_view('device_details') +) +devices.add_url_rule( + '/lot//device/', view_func=DeviceListView.as_view('lotdevicelist') +) +devices.add_url_rule( + '/lot/devices/add/', view_func=LotDeviceAddView.as_view('lot_devices_add') +) +devices.add_url_rule( + '/lot/devices/del/', view_func=LotDeviceDeleteView.as_view('lot_devices_del') +) +devices.add_url_rule('/lot/add/', view_func=LotCreateView.as_view('lot_add')) +devices.add_url_rule( + '/lot//del/', view_func=LotDeleteView.as_view('lot_del') +) +devices.add_url_rule('/lot//', view_func=LotUpdateView.as_view('lot_edit')) +devices.add_url_rule( + '/upload-snapshot/', view_func=UploadSnapshotView.as_view('upload_snapshot') +) +devices.add_url_rule( + '/lot//upload-snapshot/', + view_func=UploadSnapshotView.as_view('lot_upload_snapshot'), +) +devices.add_url_rule('/device/add/', view_func=DeviceCreateView.as_view('device_add')) +devices.add_url_rule( + '/lot//device/add/', + view_func=DeviceCreateView.as_view('lot_device_add'), +) +devices.add_url_rule('/tag/', view_func=TagListView.as_view('taglist')) +devices.add_url_rule('/tag/add/', view_func=TagAddView.as_view('tag_add')) +devices.add_url_rule( + '/tag/unnamed/add/', view_func=TagAddUnnamedView.as_view('tag_unnamed_add') +) +devices.add_url_rule( + '/tag//', view_func=TagDetailView.as_view('tag_details') +) +devices.add_url_rule( + '/tag/devices/add/', view_func=TagLinkDeviceView.as_view('tag_devices_add') +) +devices.add_url_rule( + '/tag/devices//del/', + view_func=TagUnlinkDeviceView.as_view('tag_devices_del'), +) +devices.add_url_rule( + '/export//', view_func=ExportsView.as_view('export') +) diff --git a/ereuse_devicehub/messages.py b/ereuse_devicehub/messages.py new file mode 100644 index 00000000..ba3bf089 --- /dev/null +++ b/ereuse_devicehub/messages.py @@ -0,0 +1,64 @@ +from flask import flash, session + +DEBUG = 10 +INFO = 20 +SUCCESS = 25 +WARNING = 30 +ERROR = 40 + +DEFAULT_LEVELS = { + 'DEBUG': DEBUG, + 'INFO': INFO, + 'SUCCESS': SUCCESS, + 'WARNING': WARNING, + 'ERROR': ERROR, +} + +DEFAULT_TAGS = { + DEBUG: 'light', + INFO: 'info', + SUCCESS: 'success', + WARNING: 'warning', + ERROR: 'danger', +} + +DEFAULT_ICONS = { + DEFAULT_TAGS[DEBUG]: 'tools', + DEFAULT_TAGS[INFO]: 'info-circle', + DEFAULT_TAGS[SUCCESS]: 'check-circle', + DEFAULT_TAGS[WARNING]: 'exclamation-triangle', + DEFAULT_TAGS[ERROR]: 'exclamation-octagon', +} + + +def add_message(level, message): + level_tag = DEFAULT_TAGS[level] + if '_message_icon' not in session: + session['_message_icon'] = DEFAULT_ICONS + + flash(message, level_tag) + + +def debug(message): + """Add a message with the ``DEBUG`` level.""" + add_message(DEBUG, message) + + +def info(message): + """Add a message with the ``INFO`` level.""" + add_message(INFO, message) + + +def success(message): + """Add a message with the ``SUCCESS`` level.""" + add_message(SUCCESS, message) + + +def warning(message): + """Add a message with the ``WARNING`` level.""" + add_message(WARNING, message) + + +def error(message): + """Add a message with the ``ERROR`` level.""" + add_message(ERROR, message) diff --git a/ereuse_devicehub/resources/action/models.py b/ereuse_devicehub/resources/action/models.py index 6fc039aa..f9c7a4b0 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -213,18 +213,6 @@ class Action(Thing): args[INHERIT_COND] = cls.id == Action.id return args - @validates('end_time') - def validate_end_time(self, _, end_time: datetime): - if self.start_time and end_time <= self.start_time: - raise ValidationError('The action cannot finish before it starts.') - return end_time - - @validates('start_time') - def validate_start_time(self, _, start_time: datetime): - if self.end_time and start_time >= self.end_time: - raise ValidationError('The action cannot start after it finished.') - return start_time - @property def date_str(self): return '{:%c}'.format(self.end_time) diff --git a/ereuse_devicehub/resources/action/schemas.py b/ereuse_devicehub/resources/action/schemas.py index bca1d8db..df93b107 100644 --- a/ereuse_devicehub/resources/action/schemas.py +++ b/ereuse_devicehub/resources/action/schemas.py @@ -55,6 +55,10 @@ class Action(Thing): if 'start_time' in data and data['start_time'] < unix_time: data['start_time'] = unix_time + if data.get('end_time') and data.get('start_time'): + if data['start_time'] > data['end_time']: + raise ValidationError('The action cannot finish before it starts.') + class ActionWithOneDevice(Action): __doc__ = m.ActionWithOneDevice.__doc__ diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 7c230f57..98227c4c 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -164,6 +164,10 @@ class Device(Thing): super().__init__(**kw) self.set_hid() + @property + def reverse_actions(self) -> list: + return reversed(self.actions) + @property def actions(self) -> list: """All the actions where the device participated, including: @@ -470,6 +474,13 @@ class Device(Thing): key=attrgetter('type')) # last test of each type return self._warning_actions(current_tests) + @property + def verbose_name(self): + type = self.type or '' + manufacturer = self.manufacturer or '' + model = self.model or '' + return f'{type} {manufacturer} {model}' + @declared_attr def __mapper_args__(cls): """Defines inheritance. diff --git a/ereuse_devicehub/resources/hash_reports.py b/ereuse_devicehub/resources/hash_reports.py index f44a79be..1cb7f491 100644 --- a/ereuse_devicehub/resources/hash_reports.py +++ b/ereuse_devicehub/resources/hash_reports.py @@ -26,12 +26,15 @@ class ReportHash(db.Model): hash3.comment = """The normalized name of the hash.""" -def insert_hash(bfile): +def insert_hash(bfile, commit=True): hash3 = hashlib.sha3_256(bfile).hexdigest() db_hash = ReportHash(hash3=hash3) db.session.add(db_hash) - db.session.commit() - db.session.flush() + if commit: + db.session.commit() + db.session.flush() + + return hash3 def verify_hash(bfile): diff --git a/ereuse_devicehub/resources/lot/models.py b/ereuse_devicehub/resources/lot/models.py index 9699c969..88c68626 100644 --- a/ereuse_devicehub/resources/lot/models.py +++ b/ereuse_devicehub/resources/lot/models.py @@ -5,6 +5,7 @@ from typing import Union from boltons import urlutils from citext import CIText from flask import g +from flask_login import current_user from sqlalchemy import TEXT from sqlalchemy.dialects.postgresql import UUID from sqlalchemy_utils import LtreeType @@ -101,7 +102,15 @@ class Lot(Thing): @property def is_temporary(self): - return False if self.trade else True + return not bool(self.trade) + + @property + def is_incoming(self): + return bool(self.trade and self.trade.user_to == current_user) + + @property + def is_outgoing(self): + return bool(self.trade and self.trade.user_from == current_user) @classmethod def descendantsq(cls, id): diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 92c8d5d4..9c672f8c 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -136,6 +136,10 @@ class Tag(Thing): def code(self) -> str: return hashcode.encode(self.internal_id) + @property + def get_provider(self) -> str: + return self.provider.to_text() if self.provider else '' + def delete(self): """Deletes the tag. diff --git a/ereuse_devicehub/resources/user/models.py b/ereuse_devicehub/resources/user/models.py index 79525286..70f14e00 100644 --- a/ereuse_devicehub/resources/user/models.py +++ b/ereuse_devicehub/resources/user/models.py @@ -1,7 +1,7 @@ from uuid import uuid4 -from citext import CIText from flask import current_app as app +from flask_login import UserMixin from sqlalchemy import Column, Boolean, BigInteger, Sequence from sqlalchemy.dialects.postgresql import UUID from sqlalchemy_utils import EmailType, PasswordType @@ -13,7 +13,7 @@ from ereuse_devicehub.resources.models import STR_SIZE, Thing from ereuse_devicehub.resources.enums import SessionType -class User(Thing): +class User(UserMixin, Thing): __table_args__ = {'schema': 'common'} id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True) email = Column(EmailType, nullable=False, unique=True) @@ -66,6 +66,21 @@ class User(Thing): return return self.email.split('@')[0].split('_')[1] + @property + def is_active(self): + """Alias because flask-login expects `is_active` attribute""" + return self.active + + @property + def get_full_name(self): + # TODO(@slamora) create first_name & last_name fields and use + # them to generate user full name + return self.email + + def check_password(self, password): + # take advantage of SQL Alchemy PasswordType to verify password + return self.password == password + class UserInventory(db.Model): """Relationship between users and their inventories.""" diff --git a/ereuse_devicehub/static/css/style.css b/ereuse_devicehub/static/css/style.css new file mode 100644 index 00000000..8e263853 --- /dev/null +++ b/ereuse_devicehub/static/css/style.css @@ -0,0 +1,1084 @@ +/** +* Template Name: NiceAdmin - v2.2.0 +* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ +* Author: BootstrapMade.com +* License: https://bootstrapmade.com/license/ +*/ + +/*-------------------------------------------------------------- +# General +--------------------------------------------------------------*/ +:root { + scroll-behavior: smooth; +} + +body { + font-family: "Open Sans", sans-serif; + background: #f6f9ff; + color: #444444; +} + +a { + color: #4154f1; + text-decoration: none; +} + +a:hover { + color: #717ff5; + text-decoration: none; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "Nunito", sans-serif; +} + +/*-------------------------------------------------------------- +# Main +--------------------------------------------------------------*/ +#main { + margin-top: 60px; + padding: 20px 30px; + transition: all 0.3s; +} +@media (max-width: 1199px) { + #main { + padding: 20px; + } +} + +/*-------------------------------------------------------------- +# Page Title +--------------------------------------------------------------*/ +.pagetitle { + margin-bottom: 10px; +} +.pagetitle h1 { + font-size: 24px; + margin-bottom: 0; + font-weight: 600; + color: #012970; +} + +/*-------------------------------------------------------------- +# Back to top button +--------------------------------------------------------------*/ +.back-to-top { + position: fixed; + visibility: hidden; + opacity: 0; + right: 15px; + bottom: 15px; + z-index: 99999; + background: #4154f1; + width: 40px; + height: 40px; + border-radius: 4px; + transition: all 0.4s; +} +.back-to-top i { + font-size: 24px; + color: #fff; + line-height: 0; +} +.back-to-top:hover { + background: #6776f4; + color: #fff; +} +.back-to-top.active { + visibility: visible; + opacity: 1; +} + +/*-------------------------------------------------------------- +# Override some default Bootstrap stylings +--------------------------------------------------------------*/ +/* Dropdown menus */ +.dropdown-menu { + border-radius: 4px; + padding: 10px 0; + -webkit-animation-name: dropdown-animate; + animation-name: dropdown-animate; + -webkit-animation-duration: 0.2s; + animation-duration: 0.2s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + border: 0; + box-shadow: 0 5px 30px 0 rgba(82, 63, 105, 0.2); +} +@media (max-width: 480px) { + .dropdown-menu { + width: 100vw !important; + } +} +.dropdown-menu .dropdown-header, .dropdown-menu .dropdown-footer { + text-align: center; + font-size: 15px; + padding: 10px 25px; +} +.dropdown-menu .dropdown-footer a { + color: #444444; + text-decoration: underline; +} +.dropdown-menu .dropdown-footer a:hover { + text-decoration: none; +} +.dropdown-menu .dropdown-divider { + color: #a5c5fe; + margin: 0; +} +.dropdown-menu .dropdown-item { + font-size: 14px; + padding: 10px 15px; + transition: 0.3s; +} +.dropdown-menu .dropdown-item i { + margin-right: 10px; + font-size: 18px; + line-height: 0; +} +.dropdown-menu .dropdown-item:hover { + background-color: #f6f9ff; +} + +@media (min-width: 768px) { + .dropdown-menu-arrow::before { + content: ""; + width: 13px; + height: 13px; + background: #fff; + position: absolute; + top: -7px; + right: 20px; + transform: rotate(45deg); + border-top: 1px solid #eaedf1; + border-left: 1px solid #eaedf1; + } +} +@-webkit-keyframes dropdown-animate { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + 0% { + opacity: 0; + } +} +@keyframes dropdown-animate { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + 0% { + opacity: 0; + } +} +/* Light Backgrounds */ +.bg-primary-light { + background-color: #cfe2ff; + border-color: #cfe2ff; +} + +.bg-secondary-light { + background-color: #e2e3e5; + border-color: #e2e3e5; +} + +.bg-success-light { + background-color: #d1e7dd; + border-color: #d1e7dd; +} + +.bg-danger-light { + background-color: #f8d7da; + border-color: #f8d7da; +} + +.bg-warning-light { + background-color: #fff3cd; + border-color: #fff3cd; +} + +.bg-info-light { + background-color: #cff4fc; + border-color: #cff4fc; +} + +.bg-dark-light { + background-color: #d3d3d4; + border-color: #d3d3d4; +} + +/* Card */ +.card { + margin-bottom: 30px; + border: none; + border-radius: 5px; + box-shadow: 0px 0 30px rgba(1, 41, 112, 0.1); + overflow: hidden; +} + +.card-header, .card-footer { + border-color: #ebeef4; + background-color: #fff; + color: #798eb3; + padding: 15px; +} + +.card-title { + padding: 20px 0 15px 0; + font-size: 18px; + font-weight: 500; + color: #012970; + font-family: "Poppins", sans-serif; +} +.card-title span { + color: #899bbd; + font-size: 14px; + font-weight: 400; +} + +.card-body { + padding: 0 20px 20px 20px; +} + +.card-img-overlay { + background-color: rgba(255, 255, 255, 0.6); +} + +/* Alerts */ +.alert-heading { + font-weight: 500; + font-family: "Poppins", sans-serif; + font-size: 20px; +} + +/* Close Button */ +.btn-close { + background-size: 25%; +} +.btn-close:focus { + outline: 0; + box-shadow: none; +} + +/* Accordion */ +.accordion-item { + border: 1px solid #ebeef4; +} + +.accordion-button:focus { + outline: 0; + box-shadow: none; +} +.accordion-button:not(.collapsed) { + color: #012970; + background-color: #f6f9ff; +} + +.accordion-flush .accordion-button { + padding: 15px 0; + background: none; + border: 0; +} +.accordion-flush .accordion-button:not(.collapsed) { + box-shadow: none; + color: #4154f1; +} +.accordion-flush .accordion-body { + padding: 0 0 15px 0; + color: #3e4f6f; + font-size: 15px; +} + +/* Breadcrumbs */ +.breadcrumb { + font-size: 14px; + font-family: "Nunito", sans-serif; + color: #899bbd; + font-weight: 600; +} +.breadcrumb a { + color: #899bbd; + transition: 0.3s; +} +.breadcrumb a:hover { + color: #51678f; +} +.breadcrumb .breadcrumb-item::before { + color: #899bbd; +} +.breadcrumb .active { + color: #51678f; + font-weight: 600; +} + +/* Bordered Tabs */ +.nav-tabs-bordered { + border-bottom: 2px solid #ebeef4; +} +.nav-tabs-bordered .nav-link { + margin-bottom: -2px; + border: none; + color: #2c384e; +} +.nav-tabs-bordered .nav-link:hover, .nav-tabs-bordered .nav-link:focus { + color: #4154f1; +} +.nav-tabs-bordered .nav-link.active { + background-color: #fff; + color: #4154f1; + border-bottom: 2px solid #4154f1; +} + +/*-------------------------------------------------------------- +# Header +--------------------------------------------------------------*/ +.logo { + line-height: 1; +} +@media (min-width: 1200px) { + .logo { + width: 280px; + } +} +.logo img { + max-height: 26px; + margin-right: 6px; +} +.logo span { + font-size: 26px; + font-weight: 700; + color: #012970; + font-family: "Nunito", sans-serif; +} + +.header { + transition: all 0.5s; + z-index: 997; + height: 60px; + box-shadow: 0px 2px 20px rgba(1, 41, 112, 0.1); + background-color: #fff; + padding-left: 20px; + /* Toggle Sidebar Button */ + /* Search Bar */ +} +.header .toggle-sidebar-btn { + font-size: 32px; + padding-left: 10px; + cursor: pointer; + color: #012970; +} +.header .search-bar { + min-width: 360px; + padding: 0 20px; +} +@media (max-width: 1199px) { + .header .search-bar { + position: fixed; + top: 50px; + left: 0; + right: 0; + padding: 20px; + box-shadow: 0px 0px 15px 0px rgba(1, 41, 112, 0.1); + background: white; + z-index: 9999; + transition: 0.3s; + visibility: hidden; + opacity: 0; + } + .header .search-bar-show { + top: 60px; + visibility: visible; + opacity: 1; + } +} +.header .search-form { + width: 100%; +} +.header .search-form input { + border: 0; + font-size: 14px; + color: #012970; + border: 1px solid rgba(1, 41, 112, 0.2); + padding: 7px 38px 7px 8px; + border-radius: 3px; + transition: 0.3s; + width: 100%; +} +.header .search-form input:focus, .header .search-form input:hover { + outline: none; + box-shadow: 0 0 10px 0 rgba(1, 41, 112, 0.15); + border: 1px solid rgba(1, 41, 112, 0.3); +} +.header .search-form button { + border: 0; + padding: 0; + margin-left: -30px; + background: none; +} +.header .search-form button i { + color: #012970; +} + +/*-------------------------------------------------------------- +# Header Nav +--------------------------------------------------------------*/ +.header-nav ul { + list-style: none; +} +.header-nav > ul { + margin: 0; + padding: 0; +} +.header-nav .nav-icon { + font-size: 20px; + color: #012970; +} +.header-nav .nav-profile { + color: #012970; +} +.header-nav .nav-profile img { + max-height: 36px; +} +.header-nav .nav-profile span { + font-size: 14px; + font-weight: 600; +} +.header-nav .badge-number { + position: absolute; + inset: 4px 6px auto auto; + font-weight: normal; + font-size: 11px; + padding: 3px 6px; +} +.header-nav .notifications .notification-item { + display: flex; + align-items: center; + padding: 15px 10px; + transition: 0.3s; +} +.header-nav .notifications .notification-item i { + margin: 0 20px 0 10px; + font-size: 24px; +} +.header-nav .notifications .notification-item h4 { + font-size: 16px; + font-weight: 600; + margin-bottom: 5px; +} +.header-nav .notifications .notification-item p { + font-size: 13px; + margin-bottom: 3px; + color: #919191; +} +.header-nav .notifications .notification-item:hover { + background-color: #f6f9ff; +} +.header-nav .messages .message-item { + padding: 15px 10px; + transition: 0.3s; +} +.header-nav .messages .message-item a { + display: flex; +} +.header-nav .messages .message-item img { + margin: 0 20px 0 10px; + max-height: 40px; +} +.header-nav .messages .message-item h4 { + font-size: 16px; + font-weight: 600; + margin-bottom: 5px; + color: #444444; +} +.header-nav .messages .message-item p { + font-size: 13px; + margin-bottom: 3px; + color: #919191; +} +.header-nav .messages .message-item:hover { + background-color: #f6f9ff; +} +.header-nav .profile { + min-width: 240px; + padding-bottom: 0; +} +.header-nav .profile .dropdown-header h6 { + font-size: 18px; + margin-bottom: 0; + font-weight: 600; + color: #444444; +} +.header-nav .profile .dropdown-header span { + font-size: 14px; +} +.header-nav .profile .dropdown-item { + font-size: 14px; + padding: 10px 15px; + transition: 0.3s; +} +.header-nav .profile .dropdown-item i { + margin-right: 10px; + font-size: 18px; + line-height: 0; +} +.header-nav .profile .dropdown-item:hover { + background-color: #f6f9ff; +} + +/*-------------------------------------------------------------- +# Sidebar +--------------------------------------------------------------*/ +.sidebar { + position: fixed; + top: 60px; + left: 0; + bottom: 0; + width: 300px; + z-index: 996; + transition: all 0.3s; + padding: 20px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #aab7cf transparent; + box-shadow: 0px 0px 20px rgba(1, 41, 112, 0.1); + background-color: #fff; +} +@media (max-width: 1199px) { + .sidebar { + left: -300px; + } +} +.sidebar::-webkit-scrollbar { + width: 5px; + height: 8px; + background-color: #fff; +} +.sidebar::-webkit-scrollbar-thumb { + background-color: #aab7cf; +} + +@media (min-width: 1200px) { + #main, #footer { + margin-left: 300px; + } +} +@media (max-width: 1199px) { + .toggle-sidebar .sidebar { + left: 0; + } +} +@media (min-width: 1200px) { + .toggle-sidebar #main, .toggle-sidebar #footer { + margin-left: 0; + } + .toggle-sidebar .sidebar { + left: -300px; + } +} + +.sidebar-nav { + padding: 0; + margin: 0; + list-style: none; +} +.sidebar-nav li { + padding: 0; + margin: 0; + list-style: none; +} +.sidebar-nav .nav-item { + margin-bottom: 5px; +} +.sidebar-nav .nav-heading { + font-size: 11px; + text-transform: uppercase; + color: #899bbd; + font-weight: 600; + margin: 10px 0 5px 15px; +} +.sidebar-nav .nav-link { + display: flex; + align-items: center; + font-size: 15px; + font-weight: 600; + color: #4154f1; + transition: 0.3; + background: #f6f9ff; + padding: 10px 15px; + border-radius: 4px; +} +.sidebar-nav .nav-link i { + font-size: 16px; + margin-right: 10px; + color: #4154f1; +} +.sidebar-nav .nav-link.collapsed { + color: #012970; + background: #fff; +} +.sidebar-nav .nav-link.collapsed i { + color: #899bbd; +} +.sidebar-nav .nav-link:hover { + color: #4154f1; + background: #f6f9ff; +} +.sidebar-nav .nav-link:hover i { + color: #4154f1; +} +.sidebar-nav .nav-link .bi-chevron-down { + margin-right: 0; + transition: transform 0.2s ease-in-out; +} +.sidebar-nav .nav-link:not(.collapsed) .bi-chevron-down { + transform: rotate(180deg); +} +.sidebar-nav .nav-content { + padding: 5px 0 0 0; + margin: 0; + list-style: none; +} +.sidebar-nav .nav-content a { + display: flex; + align-items: center; + font-size: 14px; + font-weight: 600; + color: #012970; + transition: 0.3; + padding: 10px 0 10px 40px; + transition: 0.3s; +} +.sidebar-nav .nav-content a i { + font-size: 6px; + margin-right: 8px; + line-height: 0; + border-radius: 50%; +} +.sidebar-nav .nav-content a:hover, .sidebar-nav .nav-content a.active { + color: #4154f1; +} +.sidebar-nav .nav-content a.active i { + background-color: #4154f1; +} + +/*-------------------------------------------------------------- +# Dashboard +--------------------------------------------------------------*/ +/* Filter dropdown */ +.dashboard .filter { + position: absolute; + right: 0px; + top: 15px; +} +.dashboard .filter .icon { + color: #aab7cf; + padding-right: 20px; + padding-bottom: 5px; + transition: 0.3s; + font-size: 16px; +} +.dashboard .filter .icon:hover, .dashboard .filter .icon:focus { + color: #4154f1; +} +.dashboard .filter .dropdown-header { + padding: 8px 15px; +} +.dashboard .filter .dropdown-header h6 { + text-transform: uppercase; + font-size: 14px; + font-weight: 600; + letter-spacing: 1px; + color: #aab7cf; + margin-bottom: 0; + padding: 0; +} +.dashboard .filter .dropdown-item { + padding: 8px 15px; +} + +/* Info Cards */ +.dashboard .info-card { + padding-bottom: 10px; +} +.dashboard .info-card h6 { + font-size: 28px; + color: #012970; + font-weight: 700; + margin: 0; + padding: 0; +} +.dashboard .card-icon { + font-size: 32px; + line-height: 0; + width: 64px; + height: 64px; + flex-shrink: 0; + flex-grow: 0; +} +.dashboard .sales-card .card-icon { + color: #4154f1; + background: #f6f6fe; +} +.dashboard .revenue-card .card-icon { + color: #2eca6a; + background: #e0f8e9; +} +.dashboard .customers-card .card-icon { + color: #ff771d; + background: #ffecdf; +} + +/* Activity */ +.dashboard .activity { + font-size: 14px; +} +.dashboard .activity .activity-item .activite-label { + color: #888; + position: relative; + flex-shrink: 0; + flex-grow: 0; + min-width: 64px; +} +.dashboard .activity .activity-item .activite-label::before { + content: ""; + position: absolute; + right: -11px; + width: 4px; + top: 0; + bottom: 0; + background-color: #eceefe; +} +.dashboard .activity .activity-item .activity-badge { + margin-top: 3px; + z-index: 1; + font-size: 11px; + line-height: 0; + border-radius: 50%; + flex-shrink: 0; + border: 3px solid #fff; + flex-grow: 0; +} +.dashboard .activity .activity-item .activity-content { + padding-left: 10px; + padding-bottom: 20px; +} +.dashboard .activity .activity-item:first-child .activite-label::before { + top: 5px; +} +.dashboard .activity .activity-item:last-child .activity-content { + padding-bottom: 0; +} + +/* News & Updates */ +.dashboard .news .post-item + .post-item { + margin-top: 15px; +} +.dashboard .news img { + width: 80px; + float: left; + border-radius: 5px; +} +.dashboard .news h4 { + font-size: 15px; + margin-left: 95px; + font-weight: bold; + margin-bottom: 5px; +} +.dashboard .news h4 a { + color: #012970; + transition: 0.3s; +} +.dashboard .news h4 a:hover { + color: #4154f1; +} +.dashboard .news p { + font-size: 14px; + color: #777777; + margin-left: 95px; +} + +/* Recent Sales */ +.dashboard .recent-sales { + font-size: 14px; +} +.dashboard .recent-sales .table thead { + background: #f6f6fe; +} +.dashboard .recent-sales .table thead th { + border: 0; +} +.dashboard .recent-sales .dataTable-top { + padding: 0 0 10px 0; +} +.dashboard .recent-sales .dataTable-bottom { + padding: 10px 0 0 0; +} + +/* Top Selling */ +.dashboard .top-selling { + font-size: 14px; +} +.dashboard .top-selling .table thead { + background: #f6f6fe; +} +.dashboard .top-selling .table thead th { + border: 0; +} +.dashboard .top-selling .table tbody td { + vertical-align: middle; +} +.dashboard .top-selling img { + border-radius: 5px; + max-width: 60px; +} + +/*-------------------------------------------------------------- +# Icons list page +--------------------------------------------------------------*/ +.iconslist { + display: grid; + max-width: 100%; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1.25rem; + padding-top: 15px; +} +.iconslist .icon { + background-color: #fff; + border-radius: 0.25rem; + text-align: center; + color: #012970; + padding: 15px 0; +} +.iconslist i { + margin: 0.25rem; + font-size: 2.5rem; +} +.iconslist .label { + font-family: var(--bs-font-monospace); + display: inline-block; + width: 100%; + overflow: hidden; + padding: 0.25rem; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; + color: #666; +} + +/*-------------------------------------------------------------- +# Profie Page +--------------------------------------------------------------*/ +.profile .profile-card img { + max-width: 120px; +} +.profile .profile-card h2 { + font-size: 24px; + font-weight: 700; + color: #2c384e; + margin: 10px 0 0 0; +} +.profile .profile-card h3 { + font-size: 18px; +} +.profile .profile-card .social-links a { + font-size: 20px; + display: inline-block; + color: rgba(1, 41, 112, 0.5); + line-height: 0; + margin-right: 10px; + transition: 0.3s; +} +.profile .profile-card .social-links a:hover { + color: #012970; +} +.profile .profile-overview .row { + margin-bottom: 20px; + font-size: 15px; +} +.profile .profile-overview .card-title { + color: #012970; +} +.profile .profile-overview .label { + font-weight: 600; + color: rgba(1, 41, 112, 0.6); +} +.profile .profile-edit label { + font-weight: 600; + color: rgba(1, 41, 112, 0.6); +} +.profile .profile-edit img { + max-width: 120px; +} + +/*-------------------------------------------------------------- +# F.A.Q Page +--------------------------------------------------------------*/ +.faq .basic h6 { + font-size: 18px; + font-weight: 600; + color: #4154f1; +} +.faq .basic p { + color: #6980aa; +} + +/*-------------------------------------------------------------- +# Contact +--------------------------------------------------------------*/ +.contact .info-box { + padding: 28px 30px; +} +.contact .info-box i { + font-size: 38px; + line-height: 0; + color: #4154f1; +} +.contact .info-box h3 { + font-size: 20px; + color: #012970; + font-weight: 700; + margin: 20px 0 10px 0; +} +.contact .info-box p { + padding: 0; + line-height: 24px; + font-size: 14px; + margin-bottom: 0; +} +.contact .php-email-form .error-message { + display: none; + color: #fff; + background: #ed3c0d; + text-align: left; + padding: 15px; + margin-bottom: 24px; + font-weight: 600; +} +.contact .php-email-form .sent-message { + display: none; + color: #fff; + background: #18d26e; + text-align: center; + padding: 15px; + margin-bottom: 24px; + font-weight: 600; +} +.contact .php-email-form .loading { + display: none; + background: #fff; + text-align: center; + padding: 15px; + margin-bottom: 24px; +} +.contact .php-email-form .loading:before { + content: ""; + display: inline-block; + border-radius: 50%; + width: 24px; + height: 24px; + margin: 0 10px -6px 0; + border: 3px solid #18d26e; + border-top-color: #eee; + -webkit-animation: animate-loading 1s linear infinite; + animation: animate-loading 1s linear infinite; +} +.contact .php-email-form input, .contact .php-email-form textarea { + border-radius: 0; + box-shadow: none; + font-size: 14px; + border-radius: 0; +} +.contact .php-email-form input:focus, .contact .php-email-form textarea:focus { + border-color: #4154f1; +} +.contact .php-email-form input { + padding: 10px 15px; +} +.contact .php-email-form textarea { + padding: 12px 15px; +} +.contact .php-email-form button[type=submit] { + background: #4154f1; + border: 0; + padding: 10px 30px; + color: #fff; + transition: 0.4s; + border-radius: 4px; +} +.contact .php-email-form button[type=submit]:hover { + background: #5969f3; +} +@-webkit-keyframes animate-loading { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +@keyframes animate-loading { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/*-------------------------------------------------------------- +# Error 404 +--------------------------------------------------------------*/ +.error-404 { + padding: 30px; +} +.error-404 h1 { + font-size: 180px; + font-weight: 700; + color: #4154f1; + margin-bottom: 0; + line-height: 150px; +} +.error-404 h2 { + font-size: 24px; + font-weight: 700; + color: #012970; + margin-bottom: 30px; +} +.error-404 .btn { + background: #51678f; + color: #fff; + padding: 8px 30px; +} +.error-404 .btn:hover { + background: #3e4f6f; +} +@media (min-width: 992px) { + .error-404 img { + max-width: 50%; + } +} + +/*-------------------------------------------------------------- +# Footer +--------------------------------------------------------------*/ +.footer { + padding: 20px 0; + font-size: 14px; + transition: all 0.3s; + border-top: 1px solid #cddfff; +} +.footer .copyright { + text-align: center; + color: #012970; +} +.footer .credits { + padding-top: 5px; + text-align: center; + font-size: 13px; + color: #012970; +} \ No newline at end of file diff --git a/ereuse_devicehub/static/img/apple-touch-icon.png b/ereuse_devicehub/static/img/apple-touch-icon.png new file mode 100644 index 00000000..126f0035 Binary files /dev/null and b/ereuse_devicehub/static/img/apple-touch-icon.png differ diff --git a/ereuse_devicehub/static/img/favicon.png b/ereuse_devicehub/static/img/favicon.png new file mode 100644 index 00000000..ca34de6f Binary files /dev/null and b/ereuse_devicehub/static/img/favicon.png differ diff --git a/ereuse_devicehub/static/img/messages-1.jpg b/ereuse_devicehub/static/img/messages-1.jpg new file mode 100644 index 00000000..a337d972 Binary files /dev/null and b/ereuse_devicehub/static/img/messages-1.jpg differ diff --git a/ereuse_devicehub/static/img/messages-2.jpg b/ereuse_devicehub/static/img/messages-2.jpg new file mode 100644 index 00000000..1d75b4e6 Binary files /dev/null and b/ereuse_devicehub/static/img/messages-2.jpg differ diff --git a/ereuse_devicehub/static/img/messages-3.jpg b/ereuse_devicehub/static/img/messages-3.jpg new file mode 100644 index 00000000..390712bc Binary files /dev/null and b/ereuse_devicehub/static/img/messages-3.jpg differ diff --git a/ereuse_devicehub/static/img/usody-logo-black.svg b/ereuse_devicehub/static/img/usody-logo-black.svg new file mode 100644 index 00000000..9266981e --- /dev/null +++ b/ereuse_devicehub/static/img/usody-logo-black.svg @@ -0,0 +1,47 @@ + + + + + + + image/svg+xml + + + + + + + USOdy + + diff --git a/ereuse_devicehub/static/js/create_device.js b/ereuse_devicehub/static/js/create_device.js new file mode 100644 index 00000000..1c9e0655 --- /dev/null +++ b/ereuse_devicehub/static/js/create_device.js @@ -0,0 +1,23 @@ +$(document).ready(function() { + $("#type").on("change", deviceInputs); + deviceInputs(); +}) + +function deviceInputs() { + if ($("#type").val() == 'Monitor') { + $("#screen").show(); + $("#resolution").show(); + $("#imei").hide(); + $("#meid").hide(); + } else if (['Smartphone', 'Cellphone', 'Tablet'].includes($("#type").val())) { + $("#screen").hide(); + $("#resolution").hide(); + $("#imei").show(); + $("#meid").show(); + } else { + $("#screen").hide(); + $("#resolution").hide(); + $("#imei").hide(); + $("#meid").hide(); + } +} diff --git a/ereuse_devicehub/static/js/jquery-3.6.0.min.js b/ereuse_devicehub/static/js/jquery-3.6.0.min.js new file mode 100644 index 00000000..c4c6022f --- /dev/null +++ b/ereuse_devicehub/static/js/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0>"),G("endobj"),n=R[t].join("\n"),J(),k){for(i=[],o=n.length;o--;)i[o]=n.charCodeAt(o);c=l.from(n),s=new a(6),s.append(new Uint8Array(i)),n=s.flush(),i=new Uint8Array(n.length+6),i.set(new Uint8Array([120,156])),i.set(n,2),i.set(new Uint8Array([255&c,c>>8&255,c>>16&255,c>>24&255]),n.length+2),n=String.fromCharCode.apply(null,i),G("<>")}else G("<>");Z(n),G("endobj")}P[1]=M,G("1 0 obj"),G("<>"),G("endobj"),W.publish("postPutPages")},et=function(t){t.objectNumber=J(),G("<>"),G("endobj")},nt=function(){for(var t in E)E.hasOwnProperty(t)&&et(E[t])},rt=function(){W.publish("putXobjectDict")},it=function(){G("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]"),G("/Font <<");for(var t in E)E.hasOwnProperty(t)&&G("/"+t+" "+E[t].objectNumber+" 0 R");G(">>"),G("/XObject <<"),rt(),G(">>")},ot=function(){nt(),W.publish("putResources"),P[2]=M,G("2 0 obj"),G("<<"),it(),G(">>"),G("endobj"),W.publish("postPutResources")},at=function(){W.publish("putAdditionalObjects");for(var t=0;t>8&&(c=!0);t=s.join("")}for(n=t.length;void 0===c&&0!==n;)t.charCodeAt(n-1)>>8&&(c=!0),n--;if(!c)return t;for(s=e.noBOM?[]:[254,255],n=0,r=t.length;n>8,u>>8)throw new Error("Character at position "+n+" of string '"+t+"' exceeds 16bits. Cannot be encoded into UCS-2 BE");s.push(u),s.push(l-(u<<8))}return String.fromCharCode.apply(void 0,s)},ft=function(t,e){return ht(t,e).replace(/\\/g,"\\\\").replace(/\(/g,"\\(").replace(/\)/g,"\\)")},dt=function(){G("/Producer (jsPDF "+r.version+")");for(var t in U)U.hasOwnProperty(t)&&U[t]&&G("/"+t.substr(0,1).toUpperCase()+t.substr(1)+" ("+ft(U[t])+")");var e=new Date,n=e.getTimezoneOffset(),i=n<0?"+":"-",o=Math.floor(Math.abs(n/60)),a=Math.abs(n%60),s=[i,Y(o),"'",Y(a),"'"].join("");G(["/CreationDate (D:",e.getFullYear(),Y(e.getMonth()+1),Y(e.getDate()),Y(e.getHours()),Y(e.getMinutes()),Y(e.getSeconds()),s,")"].join(""))},pt=function(){switch(G("/Type /Catalog"),G("/Pages 1 0 R"),b||(b="fullwidth"),b){case"fullwidth":G("/OpenAction [3 0 R /FitH null]");break;case"fullheight":G("/OpenAction [3 0 R /FitV null]");break;case"fullpage":G("/OpenAction [3 0 R /Fit]");break;case"original":G("/OpenAction [3 0 R /XYZ null null 1]");break;default:var t=""+b;"%"===t.substr(t.length-1)&&(b=parseInt(b)/100),"number"==typeof b&&G("/OpenAction [3 0 R /XYZ null null "+X(b)+"]")}switch(x||(x="continuous"),x){case"continuous":G("/PageLayout /OneColumn");break;case"single":G("/PageLayout /SinglePage");break;case"two":case"twoleft":G("/PageLayout /TwoColumnLeft");break;case"tworight":G("/PageLayout /TwoColumnRight")}v&&G("/PageMode /"+v),W.publish("putCatalog")},gt=function(){G("/Size "+(T+1)),G("/Root "+T+" 0 R"),G("/Info "+(T-1)+" 0 R")},mt=function(t,e){var n="string"==typeof e&&e.toLowerCase();if("string"==typeof t){var r=t.toLowerCase();s.hasOwnProperty(r)&&(t=s[r][0]/p,e=s[r][1]/p)}if(Array.isArray(t)&&(e=t[1],t=t[0]),n){switch(n.substr(0,1)){case"l":e>t&&(n="s");break;case"p":t>e&&(n="s")}"s"===n&&(g=t,t=e,e=g)}I=!0,R[++F]=[],D[F]={width:Number(t)||w,height:Number(e)||y},B[F]={},vt(F)},wt=function(){mt.apply(this,arguments),G(X(q*p)+" w"),G(C),0!==z&&G(z+" J"),0!==L&&G(L+" j"),W.publish("addPage",{pageNumber:F})},yt=function(t){t>0&&t<=F&&(R.splice(t,1),D.splice(t,1),F--,m>F&&(m=F),this.setPage(m))},vt=function(t){t>0&&t<=F&&(m=t,w=D[t].width,y=D[t].height)},bt=function(t,e){var n;switch(t=void 0!==t?t:E[d].fontName,e=void 0!==e?e:E[d].fontStyle,void 0!==t&&(t=t.toLowerCase()),t){case"sans-serif":case"verdana":case"arial":case"helvetica":t="helvetica";break;case"fixed":case"monospace":case"terminal":case"courier":t="courier";break;case"serif":case"cursive":case"fantasy":default:t="times"}try{n=O[t][e]}catch(t){}return n||(n=O.times[e],null==n&&(n=O.times.normal)),n},xt=function(){I=!1,T=2,M=0,j=[],P=[],N=[],W.publish("buildDocument"),G("%PDF-"+o),tt(),at(),ot(),J(),G("<<"),dt(),G(">>"),G("endobj"),J(),G("<<"),pt(),G(">>"),G("endobj");var t,e=M,n="0000000000";for(G("xref"),G("0 "+(T+1)),G(n+" 65535 f "),t=1;t<=T;t++){var r=P[t];G("function"==typeof r?(n+P[t]()).slice(-10)+" 00000 n ":(n+P[t]).slice(-10)+" 00000 n ")}return G("trailer"),G("<<"),gt(),G(">>"),G("startxref"),G(""+e),G("%%EOF"),I=!0,j.join("\n")},kt=function(t){var e="S";return"F"===t?e="f":"FD"===t||"DF"===t?e="B":"f"!==t&&"f*"!==t&&"B"!==t&&"B*"!==t||(e=t),e},_t=function(){for(var t=xt(),e=t.length,n=new ArrayBuffer(e),r=new Uint8Array(n);e--;)r[e]=t.charCodeAt(e);return n},Ct=function(){return new Blob([_t()],{type:"application/pdf"})},At=ut(function(t,n){var r="dataur"===(""+t).substr(0,6)?"data:application/pdf;base64,"+btoa(xt()):0;switch(t){case void 0:return xt();case"save":if(navigator.getUserMedia&&(void 0===e.URL||void 0===e.URL.createObjectURL))return H.output("dataurlnewwindow");i(Ct(),n),"function"==typeof i.unload&&e.setTimeout&&setTimeout(i.unload,911);break;case"arraybuffer":return _t();case"blob":return Ct();case"bloburi":case"bloburl":return e.URL&&e.URL.createObjectURL(Ct())||void 0;case"datauristring":case"dataurlstring":return r;case"dataurlnewwindow":var o=e.open(r);if(o||"undefined"==typeof safari)return o;case"datauri":case"dataurl":return e.document.location.href=r;default:throw new Error('Output type "'+t+'" is not supported.')}});switch(l){case"pt":p=1;break;case"mm":p=72/25.4000508;break;case"cm":p=72/2.54000508;break;case"in":p=72;break;case"px":p=96/72;break;case"pc":p=12;break;case"em":p=12;break;case"ex":p=6;break;default:throw"Invalid unit: "+l}H.internal={pdfEscape:ft,getStyle:kt,getFont:function(){return E[bt.apply(H,arguments)]},getFontSize:function(){return A},getLineHeight:function(){return A*S},write:function(t){G(1===arguments.length?t:Array.prototype.join.call(arguments," "))},getCoordinateString:function(t){return X(t*p)},getVerticalCoordinateString:function(t){return X((y-t)*p)},collections:{},newObject:J,newAdditionalObject:Q,newObjectDeferred:K,newObjectDeferredBegin:$,putStream:Z,events:W,scaleFactor:p,pageSize:{get width(){return w},get height(){return y}},output:function(t,e){return At(t,e)},getNumberOfPages:function(){return R.length-1},pages:R,out:G,f2:X,getPageInfo:function(t){var e=2*(t-1)+3;return{objId:e,pageNumber:t,pageContext:B[t]}},getCurrentPageInfo:function(){var t=2*(m-1)+3;return{objId:t,pageNumber:m,pageContext:B[m]}},getPDFVersion:function(){return o}},H.addPage=function(){return wt.apply(this,arguments),this},H.setPage=function(){return vt.apply(this,arguments),this},H.insertPage=function(t){return this.addPage(),this.movePage(m,t),this},H.movePage=function(t,e){if(t>e){for(var n=R[t],r=D[t],i=B[t],o=t;o>e;o--)R[o]=R[o-1],D[o]=D[o-1],B[o]=B[o-1];R[e]=n,D[e]=r,B[e]=i,this.setPage(e)}else if(t>16&255,e=r>>8&255,n=255&r}return _=0===t&&0===e&&0===n||"undefined"==typeof e?V(t/255)+" g":[V(t/255),V(e/255),V(n/255),"rg"].join(" "),this},H.CapJoinStyles={0:0,butt:0,but:0,miter:0,1:1,round:1,rounded:1,circle:1,2:2,projecting:2,project:2,square:2,bevel:2},H.setLineCap=function(t){var e=this.CapJoinStyles[t];if(void 0===e)throw new Error("Line cap style of '"+t+"' is not recognized. See or extend .CapJoinStyles property for valid styles");return z=e,G(e+" J"),this},H.setLineJoin=function(t){var e=this.CapJoinStyles[t];if(void 0===e)throw new Error("Line join style of '"+t+"' is not recognized. See or extend .CapJoinStyles property for valid styles");return L=e,G(e+" j"),this},H.output=At,H.save=function(t){H.output("save",t)};for(var St in r.API)r.API.hasOwnProperty(St)&&("events"===St&&r.API.events.length?!function(t,e){var n,r,i;for(i=e.length-1;i!==-1;i--)n=e[i][0],r=e[i][1],t.subscribe.apply(t,[n].concat("function"==typeof r?[r]:r))}(W,r.API.events):H[St]=r.API[St]);return lt(),d="F1",wt(u,c),W.publish("initialized"),H}var o="1.3",s={a0:[2383.94,3370.39],a1:[1683.78,2383.94],a2:[1190.55,1683.78],a3:[841.89,1190.55],a4:[595.28,841.89],a5:[419.53,595.28],a6:[297.64,419.53],a7:[209.76,297.64],a8:[147.4,209.76],a9:[104.88,147.4],a10:[73.7,104.88],b0:[2834.65,4008.19],b1:[2004.09,2834.65],b2:[1417.32,2004.09],b3:[1000.63,1417.32],b4:[708.66,1000.63],b5:[498.9,708.66],b6:[354.33,498.9],b7:[249.45,354.33],b8:[175.75,249.45],b9:[124.72,175.75],b10:[87.87,124.72],c0:[2599.37,3676.54],c1:[1836.85,2599.37],c2:[1298.27,1836.85],c3:[918.43,1298.27],c4:[649.13,918.43],c5:[459.21,649.13],c6:[323.15,459.21],c7:[229.61,323.15],c8:[161.57,229.61],c9:[113.39,161.57],c10:[79.37,113.39],dl:[311.81,623.62],letter:[612,792],"government-letter":[576,756],legal:[612,1008],"junior-legal":[576,360],ledger:[1224,792],tabloid:[792,1224],"credit-card":[153,243]};return r.API={events:[]},r.version="1.3.2 2016-09-30T20:33:18.867Z:jameshall","function"==typeof define&&define.amd?define("jsPDF",function(){return r}):"undefined"!=typeof module&&module.exports?module.exports=r:e.jsPDF=r,r}("undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0);window.tmp=e,/** + * jsPDF AcroForm Plugin + * Copyright (c) 2016 Alexander Weidt, https://github.com/BiggA94 + * + * Licensed under the MIT License. + * http://opensource.org/licenses/mit-license + */ +(window.AcroForm=function(t){var n=window.AcroForm;n.scale=function(t){return t*(r.internal.scaleFactor/1)},n.antiScale=function(t){return 1/r.internal.scaleFactor*t};var r={fields:[],xForms:[],acroFormDictionaryRoot:null,printedOut:!1,internal:null};e.API.acroformPlugin=r;var i=function(){for(var t in this.acroformPlugin.acroFormDictionaryRoot.Fields){var e=this.acroformPlugin.acroFormDictionaryRoot.Fields[t];e.hasAnnotation&&a.call(this,e)}},o=function(){if(this.acroformPlugin.acroFormDictionaryRoot)throw new Error("Exception while creating AcroformDictionary");this.acroformPlugin.acroFormDictionaryRoot=new n.AcroFormDictionary,this.acroformPlugin.internal=this.internal,this.acroformPlugin.acroFormDictionaryRoot._eventID=this.internal.events.subscribe("postPutResources",l),this.internal.events.subscribe("buildDocument",i),this.internal.events.subscribe("putCatalog",c),this.internal.events.subscribe("postPutPages",u)},a=function(t){var n={type:"reference",object:t};e.API.annotationPlugin.annotations[this.internal.getPageInfo(t.page).pageNumber].push(n)},s=function(t){this.acroformPlugin.printedOut&&(this.acroformPlugin.printedOut=!1,this.acroformPlugin.acroFormDictionaryRoot=null),this.acroformPlugin.acroFormDictionaryRoot||o.call(this),this.acroformPlugin.acroFormDictionaryRoot.Fields.push(t)},c=function(){"undefined"!=typeof this.acroformPlugin.acroFormDictionaryRoot?this.internal.write("/AcroForm "+this.acroformPlugin.acroFormDictionaryRoot.objId+" 0 R"):console.log("Root missing...")},l=function(){this.internal.events.unsubscribe(this.acroformPlugin.acroFormDictionaryRoot._eventID),delete this.acroformPlugin.acroFormDictionaryRoot._eventID,this.acroformPlugin.printedOut=!0},u=function(t){var e=!t;t||(this.internal.newObjectDeferredBegin(this.acroformPlugin.acroFormDictionaryRoot.objId),this.internal.out(this.acroformPlugin.acroFormDictionaryRoot.getString()));var t=t||this.acroformPlugin.acroFormDictionaryRoot.Kids;for(var r in t){var i=t[r],o=i.Rect;i.Rect&&(i.Rect=n.internal.calculateCoordinates.call(this,i.Rect)),this.internal.newObjectDeferredBegin(i.objId);var a="";if(a+=i.objId+" 0 obj\n",a+="<<\n"+i.getContent(),i.Rect=o,i.hasAppearanceStream&&!i.appearanceStreamContent){var s=n.internal.calculateAppearanceStream.call(this,i);a+="/AP << /N "+s+" >>\n",this.acroformPlugin.xForms.push(s)}if(i.appearanceStreamContent){a+="/AP << ";for(var c in i.appearanceStreamContent){var l=i.appearanceStreamContent[c];if(a+="/"+c+" ",a+="<< ",Object.keys(l).length>=1||Array.isArray(l))for(var r in l){var u=l[r];"function"==typeof u&&(u=u.call(this,i)),a+="/"+r+" "+u+" ",this.acroformPlugin.xForms.indexOf(u)>=0||this.acroformPlugin.xForms.push(u)}else{var u=l;"function"==typeof u&&(u=u.call(this,i)),a+="/"+r+" "+u+" \n",this.acroformPlugin.xForms.indexOf(u)>=0||this.acroformPlugin.xForms.push(u)}a+=" >>\n"}a+=">>\n"}a+=">>\nendobj\n",this.internal.out(a)}e&&h.call(this,this.acroformPlugin.xForms)},h=function(t){for(var e in t){var n=e,r=t[e];this.internal.newObjectDeferredBegin(r&&r.objId);var i="";i+=r?r.getString():"",this.internal.out(i),delete t[n]}};t.addField=function(t){return t instanceof n.TextField?d.call(this,t):t instanceof n.ChoiceField?p.call(this,t):t instanceof n.Button?f.call(this,t):t instanceof n.ChildClass?s.call(this,t):t&&s.call(this,t),t.page=this.acroformPlugin.internal.getCurrentPageInfo().pageNumber,this};var f=function(t){var t=t||new n.Field;t.FT="/Btn";var e=t.Ff||0;t.pushbutton&&(e=n.internal.setBitPosition(e,17),delete t.pushbutton),t.radio&&(e=n.internal.setBitPosition(e,16),delete t.radio),t.noToggleToOff&&(e=n.internal.setBitPosition(e,15)),t.Ff=e,s.call(this,t)},d=function(t){var t=t||new n.Field;t.FT="/Tx";var e=t.Ff||0;t.multiline&&(e=4096|e),t.password&&(e=8192|e),t.fileSelect&&(e|=1<<20),t.doNotSpellCheck&&(e|=1<<22),t.doNotScroll&&(e|=1<<23),t.Ff=t.Ff||e,s.call(this,t)},p=function(t){var e=t||new n.Field;e.FT="/Ch";var r=e.Ff||0;e.combo&&(r=n.internal.setBitPosition(r,18),delete e.combo),e.edit&&(r=n.internal.setBitPosition(r,19),delete e.edit),e.sort&&(r=n.internal.setBitPosition(r,20),delete e.sort),e.multiSelect&&this.internal.getPDFVersion()>=1.4&&(r=n.internal.setBitPosition(r,22),delete e.multiSelect),e.doNotSpellCheck&&this.internal.getPDFVersion()>=1.4&&(r=n.internal.setBitPosition(r,23),delete e.doNotSpellCheck),e.Ff=r,s.call(this,e)}})(e.API);var n=window.AcroForm;n.internal={},n.createFormXObject=function(t){var e=new n.FormXObject,r=n.Appearance.internal.getHeight(t)||0,i=n.Appearance.internal.getWidth(t)||0;return e.BBox=[0,0,i,r],e},n.Appearance={CheckBox:{createAppearanceStream:function(){var t={N:{On:n.Appearance.CheckBox.YesNormal},D:{On:n.Appearance.CheckBox.YesPushDown,Off:n.Appearance.CheckBox.OffPushDown}};return t},createMK:function(){return"<< /CA (3)>>"},YesPushDown:function(t){var e=n.createFormXObject(t),r="";t.Q=1;var i=n.internal.calculateX(t,"3","ZapfDingbats",50);return r+="0.749023 g\n 0 0 "+n.Appearance.internal.getWidth(t)+" "+n.Appearance.internal.getHeight(t)+" re\n f\n BMC\n q\n 0 0 1 rg\n /F13 "+i.fontSize+" Tf 0 g\n BT\n",r+=i.text,r+="ET\n Q\n EMC\n",e.stream=r,e},YesNormal:function(t){var e=n.createFormXObject(t),r="";t.Q=1;var i=n.internal.calculateX(t,"3","ZapfDingbats",.9*n.Appearance.internal.getHeight(t));return r+="1 g\n0 0 "+n.Appearance.internal.getWidth(t)+" "+n.Appearance.internal.getHeight(t)+" re\nf\nq\n0 0 1 rg\n0 0 "+(n.Appearance.internal.getWidth(t)-1)+" "+(n.Appearance.internal.getHeight(t)-1)+" re\nW\nn\n0 g\nBT\n/F13 "+i.fontSize+" Tf 0 g\n",r+=i.text,r+="ET\n Q\n",e.stream=r,e},OffPushDown:function(t){var e=n.createFormXObject(t),r="";return r+="0.749023 g\n 0 0 "+n.Appearance.internal.getWidth(t)+" "+n.Appearance.internal.getHeight(t)+" re\n f\n",e.stream=r,e}},RadioButton:{Circle:{createAppearanceStream:function(t){var e={D:{Off:n.Appearance.RadioButton.Circle.OffPushDown},N:{}};return e.N[t]=n.Appearance.RadioButton.Circle.YesNormal,e.D[t]=n.Appearance.RadioButton.Circle.YesPushDown,e},createMK:function(){return"<< /CA (l)>>"},YesNormal:function(t){var e=n.createFormXObject(t),r="",i=n.Appearance.internal.getWidth(t)<=n.Appearance.internal.getHeight(t)?n.Appearance.internal.getWidth(t)/4:n.Appearance.internal.getHeight(t)/4;i*=.9;var o=n.Appearance.internal.Bezier_C;return r+="q\n1 0 0 1 "+n.Appearance.internal.getWidth(t)/2+" "+n.Appearance.internal.getHeight(t)/2+" cm\n"+i+" 0 m\n"+i+" "+i*o+" "+i*o+" "+i+" 0 "+i+" c\n-"+i*o+" "+i+" -"+i+" "+i*o+" -"+i+" 0 c\n-"+i+" -"+i*o+" -"+i*o+" -"+i+" 0 -"+i+" c\n"+i*o+" -"+i+" "+i+" -"+i*o+" "+i+" 0 c\nf\nQ\n",e.stream=r,e},YesPushDown:function(t){var e=n.createFormXObject(t),r="",i=n.Appearance.internal.getWidth(t)<=n.Appearance.internal.getHeight(t)?n.Appearance.internal.getWidth(t)/4:n.Appearance.internal.getHeight(t)/4;i*=.9;var o=2*i,a=o*n.Appearance.internal.Bezier_C,s=i*n.Appearance.internal.Bezier_C;return r+="0.749023 g\n q\n 1 0 0 1 "+n.Appearance.internal.getWidth(t)/2+" "+n.Appearance.internal.getHeight(t)/2+" cm\n"+o+" 0 m\n"+o+" "+a+" "+a+" "+o+" 0 "+o+" c\n-"+a+" "+o+" -"+o+" "+a+" -"+o+" 0 c\n-"+o+" -"+a+" -"+a+" -"+o+" 0 -"+o+" c\n"+a+" -"+o+" "+o+" -"+a+" "+o+" 0 c\n f\n Q\n 0 g\n q\n 1 0 0 1 "+n.Appearance.internal.getWidth(t)/2+" "+n.Appearance.internal.getHeight(t)/2+" cm\n"+i+" 0 m\n"+i+" "+s+" "+s+" "+i+" 0 "+i+" c\n-"+s+" "+i+" -"+i+" "+s+" -"+i+" 0 c\n-"+i+" -"+s+" -"+s+" -"+i+" 0 -"+i+" c\n"+s+" -"+i+" "+i+" -"+s+" "+i+" 0 c\n f\n Q\n",e.stream=r,e},OffPushDown:function(t){var e=n.createFormXObject(t),r="",i=n.Appearance.internal.getWidth(t)<=n.Appearance.internal.getHeight(t)?n.Appearance.internal.getWidth(t)/4:n.Appearance.internal.getHeight(t)/4;i*=.9;var o=2*i,a=o*n.Appearance.internal.Bezier_C;return r+="0.749023 g\n q\n 1 0 0 1 "+n.Appearance.internal.getWidth(t)/2+" "+n.Appearance.internal.getHeight(t)/2+" cm\n"+o+" 0 m\n"+o+" "+a+" "+a+" "+o+" 0 "+o+" c\n-"+a+" "+o+" -"+o+" "+a+" -"+o+" 0 c\n-"+o+" -"+a+" -"+a+" -"+o+" 0 -"+o+" c\n"+a+" -"+o+" "+o+" -"+a+" "+o+" 0 c\n f\n Q\n",e.stream=r,e}},Cross:{createAppearanceStream:function(t){var e={D:{Off:n.Appearance.RadioButton.Cross.OffPushDown},N:{}};return e.N[t]=n.Appearance.RadioButton.Cross.YesNormal,e.D[t]=n.Appearance.RadioButton.Cross.YesPushDown,e},createMK:function(){return"<< /CA (8)>>"},YesNormal:function(t){var e=n.createFormXObject(t),r="",i=n.Appearance.internal.calculateCross(t);return r+="q\n 1 1 "+(n.Appearance.internal.getWidth(t)-2)+" "+(n.Appearance.internal.getHeight(t)-2)+" re\n W\n n\n "+i.x1.x+" "+i.x1.y+" m\n "+i.x2.x+" "+i.x2.y+" l\n "+i.x4.x+" "+i.x4.y+" m\n "+i.x3.x+" "+i.x3.y+" l\n s\n Q\n",e.stream=r,e},YesPushDown:function(t){var e=n.createFormXObject(t),r=n.Appearance.internal.calculateCross(t),i="";return i+="0.749023 g\n 0 0 "+n.Appearance.internal.getWidth(t)+" "+n.Appearance.internal.getHeight(t)+" re\n f\n q\n 1 1 "+(n.Appearance.internal.getWidth(t)-2)+" "+(n.Appearance.internal.getHeight(t)-2)+" re\n W\n n\n "+r.x1.x+" "+r.x1.y+" m\n "+r.x2.x+" "+r.x2.y+" l\n "+r.x4.x+" "+r.x4.y+" m\n "+r.x3.x+" "+r.x3.y+" l\n s\n Q\n",e.stream=i,e},OffPushDown:function(t){var e=n.createFormXObject(t),r="";return r+="0.749023 g\n 0 0 "+n.Appearance.internal.getWidth(t)+" "+n.Appearance.internal.getHeight(t)+" re\n f\n",e.stream=r,e}}},createDefaultAppearanceStream:function(t){var e="";return e+="/Helv 0 Tf 0 g"}},n.Appearance.internal={Bezier_C:.551915024494,calculateCross:function(t){var e=function(t,e){return t>e?e:t},r=n.Appearance.internal.getWidth(t),i=n.Appearance.internal.getHeight(t),o=e(r,i),a={x1:{x:(r-o)/2,y:(i-o)/2+o},x2:{x:(r-o)/2+o,y:(i-o)/2},x3:{x:(r-o)/2,y:(i-o)/2},x4:{x:(r-o)/2+o,y:(i-o)/2+o}};return a}},n.Appearance.internal.getWidth=function(t){return t.Rect[2]},n.Appearance.internal.getHeight=function(t){return t.Rect[3]},n.internal.inherit=function(t,e){Object.create||function(t){var e=function(){};return e.prototype=t,new e};t.prototype=Object.create(e.prototype),t.prototype.constructor=t},n.internal.arrayToPdfArray=function(t){if(Array.isArray(t)){var e=" [";for(var n in t){var r=t[n].toString();e+=r,e+=n>\n",this.stream&&(t+="stream\n",t+=this.stream,t+="endstream\n"),t+="endobj\n"},n.PDFObject.prototype.getContent=function(){var t=function(t){var e="",r=Object.keys(t).filter(function(t){return"content"!=t&&"appearanceStreamContent"!=t&&"_"!=t.substring(0,1)});for(var i in r){var o=r[i],a=t[o];a&&(e+=Array.isArray(a)?"/"+o+" "+n.internal.arrayToPdfArray(a)+"\n":a instanceof n.PDFObject?"/"+o+" "+a.objId+" 0 R\n":"/"+o+" "+a+"\n")}return e},e="";return e+=t(this)},n.FormXObject=function(){n.PDFObject.call(this),this.Type="/XObject",this.Subtype="/Form",this.FormType=1,this.BBox,this.Matrix,this.Resources="2 0 R",this.PieceInfo;var t;Object.defineProperty(this,"Length",{enumerable:!0,get:function(){return void 0!==t?t.length:0}}),Object.defineProperty(this,"stream",{enumerable:!1,set:function(e){t=e},get:function(){return t?t:null}})},n.internal.inherit(n.FormXObject,n.PDFObject),n.AcroFormDictionary=function(){n.PDFObject.call(this);var t=[];Object.defineProperty(this,"Kids",{enumerable:!1,configurable:!0,get:function(){return t.length>0?t:void 0}}),Object.defineProperty(this,"Fields",{enumerable:!0,configurable:!0,get:function(){return t}}),this.DA},n.internal.inherit(n.AcroFormDictionary,n.PDFObject),n.Field=function(){n.PDFObject.call(this);var t;Object.defineProperty(this,"Rect",{enumerable:!0,configurable:!1,get:function(){if(t){var e=t;return e}},set:function(e){t=e}});var e="";Object.defineProperty(this,"FT",{enumerable:!0,set:function(t){e=t},get:function(){return e}});var r;Object.defineProperty(this,"T",{enumerable:!0,configurable:!1,set:function(t){r=t},get:function(){if(!r||r.length<1){if(this instanceof n.ChildClass)return;return"(FieldObject"+n.Field.FieldNum++ +")"}return"("==r.substring(0,1)&&r.substring(r.length-1)?r:"("+r+")"}});var i;Object.defineProperty(this,"DA",{enumerable:!0,get:function(){if(i)return"("+i+")"},set:function(t){i=t}});var o;Object.defineProperty(this,"DV",{enumerable:!0,configurable:!0,get:function(){if(o)return o},set:function(t){o=t}}),Object.defineProperty(this,"Type",{enumerable:!0,get:function(){return this.hasAnnotation?"/Annot":null}}),Object.defineProperty(this,"Subtype",{enumerable:!0,get:function(){return this.hasAnnotation?"/Widget":null}}),this.BG,Object.defineProperty(this,"hasAnnotation",{enumerable:!1,get:function(){return!!(this.Rect||this.BC||this.BG)}}),Object.defineProperty(this,"hasAppearanceStream",{enumerable:!1,configurable:!0,writable:!0}),Object.defineProperty(this,"page",{enumerable:!1,configurable:!0,writable:!0})},n.Field.FieldNum=0,n.internal.inherit(n.Field,n.PDFObject),n.ChoiceField=function(){n.Field.call(this),this.FT="/Ch",this.Opt=[],this.V="()",this.TI=0,this.combo=!1,Object.defineProperty(this,"edit",{enumerable:!0,set:function(t){1==t?(this._edit=!0,this.combo=!0):this._edit=!1},get:function(){return!!this._edit&&this._edit},configurable:!1}),this.hasAppearanceStream=!0,Object.defineProperty(this,"V",{get:function(){n.internal.toPdfString()}})},n.internal.inherit(n.ChoiceField,n.Field),window.ChoiceField=n.ChoiceField,n.ListBox=function(){n.ChoiceField.call(this)},n.internal.inherit(n.ListBox,n.ChoiceField),window.ListBox=n.ListBox,n.ComboBox=function(){n.ListBox.call(this),this.combo=!0},n.internal.inherit(n.ComboBox,n.ListBox),window.ComboBox=n.ComboBox,n.EditBox=function(){n.ComboBox.call(this),this.edit=!0},n.internal.inherit(n.EditBox,n.ComboBox),window.EditBox=n.EditBox,n.Button=function(){n.Field.call(this),this.FT="/Btn"},n.internal.inherit(n.Button,n.Field),window.Button=n.Button,n.PushButton=function(){n.Button.call(this),this.pushbutton=!0},n.internal.inherit(n.PushButton,n.Button),window.PushButton=n.PushButton,n.RadioButton=function(){n.Button.call(this),this.radio=!0;var t=[];Object.defineProperty(this,"Kids",{enumerable:!0,get:function(){if(t.length>0)return t}}),Object.defineProperty(this,"__Kids",{get:function(){return t}});var e;Object.defineProperty(this,"noToggleToOff",{enumerable:!1,get:function(){return e},set:function(t){e=t}})},n.internal.inherit(n.RadioButton,n.Button),window.RadioButton=n.RadioButton,n.ChildClass=function(t,e){n.Field.call(this),this.Parent=t,this._AppearanceType=n.Appearance.RadioButton.Circle,this.appearanceStreamContent=this._AppearanceType.createAppearanceStream(e),this.F=n.internal.setBitPosition(this.F,3,1),this.MK=this._AppearanceType.createMK(),this.AS="/Off",this._Name=e},n.internal.inherit(n.ChildClass,n.Field),n.RadioButton.prototype.setAppearance=function(t){if(!("createAppearanceStream"in t&&"createMK"in t))return void console.log("Couldn't assign Appearance to RadioButton. Appearance was Invalid!");for(var e in this.__Kids){var n=this.__Kids[e];n.appearanceStreamContent=t.createAppearanceStream(n._Name),n.MK=t.createMK()}},n.RadioButton.prototype.createOption=function(t){var r=this,i=(this.__Kids.length,new n.ChildClass(r,t));return this.__Kids.push(i),e.API.addField(i),i},n.CheckBox=function(){Button.call(this),this.appearanceStreamContent=n.Appearance.CheckBox.createAppearanceStream(),this.MK=n.Appearance.CheckBox.createMK(),this.AS="/On",this.V="/On"},n.internal.inherit(n.CheckBox,n.Button),window.CheckBox=n.CheckBox,n.TextField=function(){n.Field.call(this),this.DA=n.Appearance.createDefaultAppearanceStream(),this.F=4;var t;Object.defineProperty(this,"V",{get:function(){return t?"("+t+")":t},enumerable:!0,set:function(e){t=e}});var e;Object.defineProperty(this,"DV",{get:function(){return e?"("+e+")":e},enumerable:!0,set:function(t){e=t}});var r=!1;Object.defineProperty(this,"multiline",{enumerable:!1,get:function(){return r},set:function(t){r=t}});var i=!1;Object.defineProperty(this,"MaxLen",{enumerable:!0,get:function(){return i},set:function(t){i=t}}),Object.defineProperty(this,"hasAppearanceStream",{enumerable:!1,get:function(){return this.V||this.DV}})},n.internal.inherit(n.TextField,n.Field),window.TextField=n.TextField,n.PasswordField=function(){TextField.call(this),Object.defineProperty(this,"password",{value:!0,enumerable:!1,configurable:!1,writable:!1})},n.internal.inherit(n.PasswordField,n.TextField),window.PasswordField=n.PasswordField,n.internal.calculateFontSpace=function(t,e,r){var r=r||"helvetica",i=n.internal.calculateFontSpace.canvas||(n.internal.calculateFontSpace.canvas=document.createElement("canvas")),o=i.getContext("2d");o.save();var a=e+" "+r;o.font=a;var s=o.measureText(t);o.fontcolor="black";var o=i.getContext("2d");s.height=1.5*o.measureText("3").width,o.restore();s.width;return s},n.internal.calculateX=function(t,e,r,i){var i=i||12,r=r||"helvetica",o={text:"",fontSize:""};e="("==e.substr(0,1)?e.substr(1):e,e=")"==e.substr(e.length-1)?e.substr(0,e.length-1):e;var a=e.split(" "),s=i,c=2,l=2,u=n.Appearance.internal.getHeight(t)||0;u=u<0?-u:u;var h=n.Appearance.internal.getWidth(t)||0;h=h<0?-h:h;var f=function(t,e,i){if(t+1=a.length-1;if(!A||S){if(A||S){if(S)v=C;else if(t.multiline&&(d+c)*(k+2)+c>u)continue t}else{if(!t.multiline)continue t;if((d+c)*(k+2)+c>u)continue t;v=C}for(var q="",T=y;T<=v;T++)q+=a[T]+" ";switch(q=" "==q.substr(q.length-1)?q.substr(0,q.length-1):q,b=n.internal.calculateFontSpace(q,s+"px",r).width,t.Q){case 2:g=h-b-l;break;case 1:g=(h-b)/2;break;case 0:default:g=l}e+=g+" "+w+" Td\n",e+="("+q+") Tj\n",e+=-g+" 0 Td\n",w=-(s+c),m=g,b=0,y=v+1,k++,x=""}else x+=" "}break}return o.text=e,o.fontSize=s,o},n.internal.calculateAppearanceStream=function(t){if(t.appearanceStreamContent)return t.appearanceStreamContent;if(t.V||t.DV){var e="",r=t.V||t.DV,i=n.internal.calculateX(t,r);e+="/Tx BMC\nq\n/F1 "+i.fontSize+" Tf\n1 0 0 1 0 0 Tm\n",e+="BT\n",e+=i.text,e+="ET\n",e+="Q\nEMC\n";var o=new n.createFormXObject(t);o.stream=e;return o}},n.internal.calculateCoordinates=function(t,e,r,i){var o={};if(this.internal){var a=function(t){return t*this.internal.scaleFactor};Array.isArray(t)?(t[0]=n.scale(t[0]),t[1]=n.scale(t[1]),t[2]=n.scale(t[2]),t[3]=n.scale(t[3]),o.lowerLeft_X=t[0]||0,o.lowerLeft_Y=a.call(this,this.internal.pageSize.height)-t[3]-t[1]||0,o.upperRight_X=t[0]+t[2]||0,o.upperRight_Y=a.call(this,this.internal.pageSize.height)-t[1]||0):(t=n.scale(t),e=n.scale(e),r=n.scale(r),i=n.scale(i),o.lowerLeft_X=t||0,o.lowerLeft_Y=this.internal.pageSize.height-e||0,o.upperRight_X=t+r||0,o.upperRight_Y=this.internal.pageSize.height-e+i||0)}else Array.isArray(t)?(o.lowerLeft_X=t[0]||0,o.lowerLeft_Y=t[1]||0,o.upperRight_X=t[0]+t[2]||0,o.upperRight_Y=t[1]+t[3]||0):(o.lowerLeft_X=t||0,o.lowerLeft_Y=e||0,o.upperRight_X=t+r||0,o.upperRight_Y=e+i||0);return[o.lowerLeft_X,o.lowerLeft_Y,o.upperRight_X,o.upperRight_Y]},n.internal.calculateColor=function(t,e,n){var r=new Array(3);return r.r=0|t,r.g=0|e,r.b=0|n,r},n.internal.getBitPosition=function(t,e){t=t||0;var n=1;return n<<=e-1,t|n},n.internal.setBitPosition=function(t,e,n){t=t||0,n=n||1;var r=1;if(r<<=e-1,1==n)var t=t|r;else var t=t&~r;return t},/** + * jsPDF addHTML PlugIn + * Copyright (c) 2014 Diego Casorran + * + * Licensed under the MIT License. + * http://opensource.org/licenses/mit-license + */ +function(t){t.addHTML=function(t,e,n,r,i){if("undefined"==typeof html2canvas&&"undefined"==typeof rasterizeHTML)throw new Error("You need either https://github.com/niklasvh/html2canvas or https://github.com/cburgmer/rasterizeHTML.js");"number"!=typeof e&&(r=e,i=n),"function"==typeof r&&(i=r,r=null);var o=this.internal,a=o.scaleFactor,s=o.pageSize.width,c=o.pageSize.height;if(r=r||{},r.onrendered=function(t){e=parseInt(e)||0,n=parseInt(n)||0;var o=r.dim||{},l=o.h||0,u=o.w||Math.min(s,t.width/a)-e,h="JPEG";if(r.format&&(h=r.format),t.height>c&&r.pagesplit){var f=function(){for(var r=0;;){var o=document.createElement("canvas");o.width=Math.min(s*a,t.width),o.height=Math.min(c*a,t.height-r);var l=o.getContext("2d");l.drawImage(t,0,r,t.width,o.height,0,0,o.width,o.height);var f=[o,e,r?0:n,o.width/a,o.height/a,h,null,"SLOW"];if(this.addImage.apply(this,f),r+=o.height,r>=t.height)break;this.addPage()}i(u,r,null,f)}.bind(this);if("CANVAS"===t.nodeName){var d=new Image;d.onload=f,d.src=t.toDataURL("image/png"),t=d}else f()}else{var p=Math.random().toString(35),g=[t,e,n,u,l,h,p,"SLOW"];this.addImage.apply(this,g),i(u,l,p,g)}}.bind(this),"undefined"!=typeof html2canvas&&!r.rstz)return html2canvas(t,r);if("undefined"!=typeof rasterizeHTML){var l="drawDocument";return"string"==typeof t&&(l=/^http/.test(t)?"drawURL":"drawHTML"),r.width=r.width||s*a,rasterizeHTML[l](t,void 0,r).then(function(t){r.onrendered(t.image)},function(t){i(null,t)})}return null}}(e.API),function(e){var n="addImage_",r=["jpeg","jpg","png"],i=function t(e){var n=this.internal.newObject(),r=this.internal.write,i=this.internal.putStream;if(e.n=n,r("<>"),"trns"in e&&e.trns.constructor==Array){for(var o="",a=0,s=e.trns.length;a>"),i(e.data),r("endobj"),"smask"in e){var c="/Predictor "+e.p+" /Colors 1 /BitsPerComponent "+e.bpc+" /Columns "+e.w,l={w:e.w,h:e.h,cs:"DeviceGray",bpc:e.bpc,dp:c,data:e.smask};"f"in e&&(l.f=e.f),t.call(this,l)}e.cs===this.color_spaces.INDEXED&&(this.internal.newObject(),r("<< /Length "+e.pal.length+">>"),i(this.arrayBufferToBinaryString(new Uint8Array(e.pal))),r("endobj"))},o=function(){var t=this.internal.collections[n+"images"];for(var e in t)i.call(this,t[e])},a=function(){var t,e=this.internal.collections[n+"images"],r=this.internal.write;for(var i in e)t=e[i],r("/I"+t.i,t.n,"0","R")},s=function(t){return t&&"string"==typeof t&&(t=t.toUpperCase()),t in e.image_compression?t:e.image_compression.NONE},c=function(){var t=this.internal.collections[n+"images"];return t||(this.internal.collections[n+"images"]=t={},this.internal.events.subscribe("putResources",o),this.internal.events.subscribe("putXobjectDict",a)),t},l=function(t){var e=0;return t&&(e=Object.keys?Object.keys(t).length:function(t){var e=0;for(var n in t)t.hasOwnProperty(n)&&e++;return e}(t)),e},u=function(t){return"undefined"==typeof t||null===t},h=function(t){return"string"==typeof t&&e.sHashCode(t)},f=function(t){return r.indexOf(t)===-1},d=function(t){return"function"!=typeof e["process"+t.toUpperCase()]},p=function(e){return"object"===("undefined"==typeof e?"undefined":t(e))&&1===e.nodeType},g=function(e,n,r){if("IMG"===e.nodeName&&e.hasAttribute("src")){var i=""+e.getAttribute("src");if(!r&&0===i.indexOf("data:image/"))return i;!n&&/\.png(?:[?#].*)?$/i.test(i)&&(n="png")}if("CANVAS"===e.nodeName)var o=e;else{var o=document.createElement("canvas");o.width=e.clientWidth||e.width,o.height=e.clientHeight||e.height;var a=o.getContext("2d");if(!a)throw"addImage requires canvas to be supported by browser.";if(r){var s,c,l,u,h,f,d,p,g=Math.PI/180;"object"===("undefined"==typeof r?"undefined":t(r))&&(s=r.x,c=r.y,l=r.bg,r=r.angle),p=r*g,u=Math.abs(Math.cos(p)),h=Math.abs(Math.sin(p)),f=o.width,d=o.height,o.width=d*h+f*u,o.height=d*u+f*h,isNaN(s)&&(s=o.width/2),isNaN(c)&&(c=o.height/2),a.clearRect(0,0,o.width,o.height),a.fillStyle=l||"white",a.fillRect(0,0,o.width,o.height),a.save(),a.translate(s,c),a.rotate(p),a.drawImage(e,-(f/2),-(d/2)),a.rotate(-p),a.translate(-s,-c),a.restore()}else a.drawImage(e,0,0,o.width,o.height)}return o.toDataURL("png"==(""+n).toLowerCase()?"image/png":"image/jpeg")},m=function(t,e){var n;if(e)for(var r in e)if(t===e[r].alias){n=e[r];break}return n},w=function(t,e,n){return t||e||(t=-96,e=-96),t<0&&(t=-1*n.w*72/t/this.internal.scaleFactor),e<0&&(e=-1*n.h*72/e/this.internal.scaleFactor),0===t&&(t=e*n.w/n.h),0===e&&(e=t*n.h/n.w),[t,e]},y=function(t,e,n,r,i,o,a){var s=w.call(this,n,r,i),c=this.internal.getCoordinateString,l=this.internal.getVerticalCoordinateString;n=s[0],r=s[1],a[o]=i,this.internal.write("q",c(n),"0 0",c(r),c(t),l(e+r),"cm /I"+i.i,"Do Q")};e.color_spaces={DEVICE_RGB:"DeviceRGB",DEVICE_GRAY:"DeviceGray",DEVICE_CMYK:"DeviceCMYK",CAL_GREY:"CalGray",CAL_RGB:"CalRGB",LAB:"Lab",ICC_BASED:"ICCBased",INDEXED:"Indexed",PATTERN:"Pattern",SEPARATION:"Separation",DEVICE_N:"DeviceN"},e.decode={DCT_DECODE:"DCTDecode",FLATE_DECODE:"FlateDecode",LZW_DECODE:"LZWDecode",JPX_DECODE:"JPXDecode",JBIG2_DECODE:"JBIG2Decode",ASCII85_DECODE:"ASCII85Decode",ASCII_HEX_DECODE:"ASCIIHexDecode",RUN_LENGTH_DECODE:"RunLengthDecode",CCITT_FAX_DECODE:"CCITTFaxDecode"},e.image_compression={NONE:"NONE",FAST:"FAST",MEDIUM:"MEDIUM",SLOW:"SLOW"},e.sHashCode=function(t){return Array.prototype.reduce&&t.split("").reduce(function(t,e){return t=(t<<5)-t+e.charCodeAt(0),t&t},0)},e.isString=function(t){return"string"==typeof t},e.extractInfoFromBase64DataURI=function(t){return/^data:([\w]+?\/([\w]+?));base64,(.+?)$/g.exec(t)},e.supportsArrayBuffer=function(){return"undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8Array},e.isArrayBuffer=function(t){return!!this.supportsArrayBuffer()&&t instanceof ArrayBuffer},e.isArrayBufferView=function(t){return!!this.supportsArrayBuffer()&&("undefined"!=typeof Uint32Array&&(t instanceof Int8Array||t instanceof Uint8Array||"undefined"!=typeof Uint8ClampedArray&&t instanceof Uint8ClampedArray||t instanceof Int16Array||t instanceof Uint16Array||t instanceof Int32Array||t instanceof Uint32Array||t instanceof Float32Array||t instanceof Float64Array))},e.binaryStringToUint8Array=function(t){for(var e=t.length,n=new Uint8Array(e),r=0;r>18,n=(258048&o)>>12,r=(4032&o)>>6,i=63&o,a+=s[e]+s[n]+s[r]+s[i];return 1==u?(o=c[h],e=(252&o)>>2,n=(3&o)<<4,a+=s[e]+s[n]+"=="):2==u&&(o=c[h]<<8|c[h+1],e=(64512&o)>>10,n=(1008&o)>>4,r=(15&o)<<2,a+=s[e]+s[n]+s[r]+"="),a},e.createImageInfo=function(t,e,n,r,i,o,a,s,c,l,u,h,f){var d={alias:s,w:e,h:n,cs:r,bpc:i,i:a,data:t};return o&&(d.f=o),c&&(d.dp=c),l&&(d.trns=l),u&&(d.pal=u),h&&(d.smask=h),f&&(d.p=f),d},e.addImage=function(e,n,i,o,a,w,v,b,x){if("string"!=typeof n){var k=w;w=a,a=o,o=i,i=n,n=k}if("object"===("undefined"==typeof e?"undefined":t(e))&&!p(e)&&"imageData"in e){var _=e;e=_.imageData,n=_.format||n,i=_.x||i||0,o=_.y||o||0,a=_.w||a,w=_.h||w,v=_.alias||v,b=_.compression||b,x=_.rotation||_.angle||x}if(isNaN(i)||isNaN(o))throw console.error("jsPDF.addImage: Invalid coordinates",arguments),new Error("Invalid coordinates passed to jsPDF.addImage");var C,A=c.call(this);if(!(C=m(e,A))){var S;if(p(e)&&(e=g(e,n,x)),u(v)&&(v=h(e)),!(C=m(v,A))){if(this.isString(e)){var q=this.extractInfoFromBase64DataURI(e);q?(n=q[2],e=atob(q[3])):137===e.charCodeAt(0)&&80===e.charCodeAt(1)&&78===e.charCodeAt(2)&&71===e.charCodeAt(3)&&(n="png")}if(n=(n||"JPEG").toLowerCase(),f(n))throw new Error("addImage currently only supports formats "+r+", not '"+n+"'");if(d(n))throw new Error("please ensure that the plugin for '"+n+"' support is added");if(this.supportsArrayBuffer()&&(e instanceof Uint8Array||(S=e,e=this.binaryStringToUint8Array(e))),C=this["process"+n.toUpperCase()](e,l(A),v,s(b),S),!C)throw new Error("An unkwown error occurred whilst processing the image")}}return y.call(this,i,o,a,w,C,C.i,A),this};var v=function(t){var e,n,r;if(255===!t.charCodeAt(0)||216===!t.charCodeAt(1)||255===!t.charCodeAt(2)||224===!t.charCodeAt(3)||!t.charCodeAt(6)==="J".charCodeAt(0)||!t.charCodeAt(7)==="F".charCodeAt(0)||!t.charCodeAt(8)==="I".charCodeAt(0)||!t.charCodeAt(9)==="F".charCodeAt(0)||0===!t.charCodeAt(10))throw new Error("getJpegSize requires a binary string jpeg file");for(var i=256*t.charCodeAt(4)+t.charCodeAt(5),o=4,a=t.length;o7)return n=x(t,c+5),r=(n[2]<<8)+n[3],i=(n[0]<<8)+n[1],o=n[4],{width:r,height:i,numcomponents:o};c+=2}throw new Error("getJpegSizeFromBytes could not find the size of the image")},x=function(t,e){return t.subarray(e,e+5)};e.processJPEG=function(t,e,n,r,i){var o,a=this.color_spaces.DEVICE_RGB,s=this.decode.DCT_DECODE,c=8;return this.isString(t)?(o=v(t),this.createImageInfo(t,o[0],o[1],1==o[3]?this.color_spaces.DEVICE_GRAY:a,c,s,e,n)):(this.isArrayBuffer(t)&&(t=new Uint8Array(t)),this.isArrayBufferView(t)?(o=b(t),t=i||this.arrayBufferToBinaryString(t),this.createImageInfo(t,o.width,o.height,1==o.numcomponents?this.color_spaces.DEVICE_GRAY:a,c,s,e,n)):null)},e.processJPG=function(){return this.processJPEG.apply(this,arguments)}}(e.API),/** + * jsPDF Annotations PlugIn + * Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv + * + * Licensed under the MIT License. + * http://opensource.org/licenses/mit-license + */ +function(t){var n={annotations:[],f2:function(t){return t.toFixed(2)},notEmpty:function(t){if("undefined"!=typeof t&&""!=t)return!0}};return e.API.annotationPlugin=n,e.API.events.push(["addPage",function(t){this.annotationPlugin.annotations[t.pageNumber]=[]}]),t.events.push(["putPage",function(t){for(var e=this.annotationPlugin.annotations[t.pageNumber],r=!1,i=0;i>",u.content=y;var p=u.objId+" 0 R",g=30,d="/Rect ["+a((o.bounds.x+g)*s)+" "+a(c-(o.bounds.y+o.bounds.h)*s)+" "+a((o.bounds.x+o.bounds.w+g)*s)+" "+a((c-o.bounds.y)*s)+"] ";y="<>";else if(o.options.pageNumber){var t=this.internal.getPageInfo(o.options.pageNumber);switch(y="<>",this.internal.write(y))}}this.internal.write("]")}}]),t.createAnnotation=function(t){switch(t.type){case"link":this.link(t.bounds.x,t.bounds.y,t.bounds.w,t.bounds.h,t);break;case"text":case"freetext":this.annotationPlugin.annotations[this.internal.getCurrentPageInfo().pageNumber].push(t)}},t.link=function(t,e,n,r,i){this.annotationPlugin.annotations[this.internal.getCurrentPageInfo().pageNumber].push({x:t,y:e,w:n,h:r,options:i,type:"link"})},t.link=function(t,e,n,r,i){this.annotationPlugin.annotations[this.internal.getCurrentPageInfo().pageNumber].push({x:t,y:e,w:n,h:r,options:i,type:"link"})},t.textWithLink=function(t,e,n,r){var i=this.getTextWidth(t),o=this.internal.getLineHeight();return this.text(t,e,n),n+=.2*o,this.link(e,n-o,i,o,r),i},t.getTextWidth=function(t){var e=this.internal.getFontSize(),n=this.getStringUnitWidth(t)*e/this.internal.scaleFactor;return n},t.getLineHeight=function(){return this.internal.getLineHeight()},this}(e.API),function(t){t.autoPrint=function(){var t;return this.internal.events.subscribe("postPutResources",function(){t=this.internal.newObject(),this.internal.write("<< /S/Named /Type/Action /N/Print >>","endobj")}),this.internal.events.subscribe("putCatalog",function(){this.internal.write("/OpenAction "+t+" 0 R")}),this}}(e.API),/** + * jsPDF Canvas PlugIn + * Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv + * + * Licensed under the MIT License. + * http://opensource.org/licenses/mit-license + */ +function(t){return t.events.push(["initialized",function(){this.canvas.pdf=this}]),t.canvas={getContext:function(t){return this.pdf.context2d._canvas=this,this.pdf.context2d},style:{}},Object.defineProperty(t.canvas,"width",{get:function(){return this._width},set:function(t){this._width=t,this.getContext("2d").pageWrapX=t+1}}),Object.defineProperty(t.canvas,"height",{get:function(){return this._height},set:function(t){this._height=t,this.getContext("2d").pageWrapY=t+1}}),this}(e.API),/** ==================================================================== + * jsPDF Cell plugin + * Copyright (c) 2013 Youssef Beddad, youssef.beddad@gmail.com + * 2013 Eduardo Menezes de Morais, eduardo.morais@usp.br + * 2013 Lee Driscoll, https://github.com/lsdriscoll + * 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria + * 2014 James Hall, james@parall.ax + * 2014 Diego Casorran, https://github.com/diegocr + * + * + * ==================================================================== + */ +function(t){var e,n,r,i,o=3,a=13,s={x:void 0,y:void 0,w:void 0,h:void 0,ln:void 0},c=1,l=function(t,e,n,r,i){s={x:t,y:e,w:n,h:r,ln:i}},u=function(){return s},h={left:0,top:0,bottom:0};t.setHeaderFunction=function(t){i=t},t.getTextDimensions=function(t){e=this.internal.getFont().fontName,n=this.table_font_size||this.internal.getFontSize(),r=this.internal.getFont().fontStyle;var i,o,a=19.049976/25.4;o=document.createElement("font"),o.id="jsPDFCell";try{o.style.fontStyle=r}catch(t){o.style.fontWeight=r}o.style.fontName=e,o.style.fontSize=n+"pt";try{o.textContent=t}catch(e){o.innerText=t}return document.body.appendChild(o),i={w:(o.offsetWidth+1)*a,h:(o.offsetHeight+1)*a},document.body.removeChild(o),i},t.cellAddPage=function(){var t=this.margins||h;this.addPage(),l(t.left,t.top,void 0,void 0),c+=1},t.cellInitialize=function(){s={x:void 0,y:void 0,w:void 0,h:void 0,ln:void 0},c=1},t.cell=function(t,e,n,r,i,s,c){var f=u(),d=!1;if(void 0!==f.ln)if(f.ln===s)t=f.x+f.w,e=f.y;else{var p=this.margins||h;f.y+f.h+r+a>=this.internal.pageSize.height-p.bottom&&(this.cellAddPage(),d=!0,this.printHeaders&&this.tableHeaderRow&&this.printHeaderRow(s,!0)),e=u().y+u().h,d&&(e=a+10)}if(void 0!==i[0])if(this.printingHeaderRow?this.rect(t,e,n,r,"FD"):this.rect(t,e,n,r),"right"===c){i instanceof Array||(i=[i]);for(var g=0;go&&(o=i);return o},t.table=function(e,n,r,i,o){if(!r)throw"No data for PDF table";var a,l,u,f,d,p,g,m,w,y,v=[],b=[],x={},k={},_=[],C=[],A=!1,S=!0,q=12,T=h;if(T.width=this.internal.pageSize.width,o&&(o.autoSize===!0&&(A=!0),o.printHeaders===!1&&(S=!1),o.fontSize&&(q=o.fontSize),o.css&&"undefined"!=typeof o.css["font-size"]&&(q=16*o.css["font-size"]),o.margins&&(T=o.margins)),this.lnMod=0,s={x:void 0,y:void 0,w:void 0,h:void 0,ln:void 0},c=1,this.printHeaders=S,this.margins=T,this.setFontSize(q),this.table_font_size=q,void 0===i||null===i)v=Object.keys(r[0]);else if(i[0]&&"string"!=typeof i[0]){var I=19.049976/25.4;for(l=0,u=i.length;li&&(i=s)}return i},t.setTableHeaderRow=function(t){this.tableHeaderRow=t},t.printHeaderRow=function(t,e){if(!this.tableHeaderRow)throw"Property tableHeaderRow does not exist.";var n,r,o,s;if(this.printingHeaderRow=!0,void 0!==i){var u=i(this,c);l(u[0],u[1],u[2],u[3],-1)}this.setFontStyle("bold");var h=[];for(o=0,s=this.tableHeaderRow.length;o0&&this.setTableHeaderRow(h),this.setFontStyle("normal"),this.printingHeaderRow=!1}}(e.API),/** + * jsPDF Context2D PlugIn Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv + * + * Licensed under the MIT License. http://opensource.org/licenses/mit-license + */ +function(t){function e(){this._isStrokeTransparent=!1,this._strokeOpacity=1,this.strokeStyle="#000000",this.fillStyle="#000000",this._isFillTransparent=!1,this._fillOpacity=1,this.font="12pt times",this.textBaseline="alphabetic",this.textAlign="start",this.lineWidth=1,this.lineJoin="miter",this.lineCap="butt",this._transform=[1,0,0,1,0,0],this.globalCompositeOperation="normal",this.globalAlpha=1,this._clip_path=[],this.ignoreClearRect=!1,this.copy=function(t){this._isStrokeTransparent=t._isStrokeTransparent,this._strokeOpacity=t._strokeOpacity,this.strokeStyle=t.strokeStyle,this._isFillTransparent=t._isFillTransparent,this._fillOpacity=t._fillOpacity,this.fillStyle=t.fillStyle,this.font=t.font,this.lineWidth=t.lineWidth,this.lineJoin=t.lineJoin,this.lineCap=t.lineCap,this.textBaseline=t.textBaseline,this.textAlign=t.textAlign,this._fontSize=t._fontSize,this._transform=t._transform.slice(0),this.globalCompositeOperation=t.globalCompositeOperation,this.globalAlpha=t.globalAlpha,this._clip_path=t._clip_path.slice(0),this.ignoreClearRect=t.ignoreClearRect}}t.events.push(["initialized",function(){this.context2d.pdf=this,this.context2d.internal.pdf=this,this.context2d.ctx=new e,this.context2d.ctxStack=[],this.context2d.path=[]}]),t.context2d={pageWrapXEnabled:!1,pageWrapYEnabled:!1,pageWrapX:9999999,pageWrapY:9999999,ctx:new e,f2:function(t){return t.toFixed(2)},fillRect:function(t,e,n,r){if(!this._isFillTransparent()){t=this._wrapX(t),e=this._wrapY(e);var i=this._matrix_map_rect(this.ctx._transform,{x:t,y:e,w:n,h:r});this.pdf.rect(i.x,i.y,i.w,i.h,"f")}},strokeRect:function(t,e,n,r){if(!this._isStrokeTransparent()){t=this._wrapX(t),e=this._wrapY(e);var i=this._matrix_map_rect(this.ctx._transform,{x:t,y:e,w:n,h:r});this.pdf.rect(i.x,i.y,i.w,i.h,"s")}},clearRect:function(t,e,n,r){if(!this.ctx.ignoreClearRect){t=this._wrapX(t),e=this._wrapY(e);var i=this._matrix_map_rect(this.ctx._transform,{x:t,y:e,w:n,h:r});this.save(),this.setFillStyle("#ffffff"),this.pdf.rect(i.x,i.y,i.w,i.h,"f"),this.restore()}},save:function(){this.ctx._fontSize=this.pdf.internal.getFontSize();var t=new e;t.copy(this.ctx),this.ctxStack.push(this.ctx),this.ctx=t},restore:function(){this.ctx=this.ctxStack.pop(),this.setFillStyle(this.ctx.fillStyle),this.setStrokeStyle(this.ctx.strokeStyle),this.setFont(this.ctx.font),this.pdf.setFontSize(this.ctx._fontSize),this.setLineCap(this.ctx.lineCap),this.setLineWidth(this.ctx.lineWidth),this.setLineJoin(this.ctx.lineJoin)},rect:function(t,e,n,r){this.moveTo(t,e),this.lineTo(t+n,e),this.lineTo(t+n,e+r),this.lineTo(t,e+r),this.lineTo(t,e),this.closePath()},beginPath:function(){this.path=[]},closePath:function(){this.path.push({type:"close"})},_getRgba:function(t){var e={};if(this.internal.rxTransparent.test(t))e.r=0,e.g=0,e.b=0,e.a=0;else{var n=this.internal.rxRgb.exec(t);null!=n?(e.r=parseInt(n[1]),e.g=parseInt(n[2]),e.b=parseInt(n[3]),e.a=1):(n=this.internal.rxRgba.exec(t),null!=n?(e.r=parseInt(n[1]),e.g=parseInt(n[2]),e.b=parseInt(n[3]),e.a=parseFloat(n[4])):(e.a=1,"#"!=t.charAt(0)&&(t=o.colorNameToHex(t),t||(t="#000000")),4===t.length?(e.r=t.substring(1,2),e.r+=r,e.g=t.substring(2,3),e.g+=g,e.b=t.substring(3,4),e.b+=b):(e.r=t.substring(1,3),e.g=t.substring(3,5),e.b=t.substring(5,7)),e.r=parseInt(e.r,16),e.g=parseInt(e.g,16),e.b=parseInt(e.b,16)))}return e.style=t,e},setFillStyle:function(t){var e,n,r,i;if(this.internal.rxTransparent.test(t))e=0,n=0,r=0,i=0;else{var a=this.internal.rxRgb.exec(t);null!=a?(e=parseInt(a[1]),n=parseInt(a[2]),r=parseInt(a[3]),i=1):(a=this.internal.rxRgba.exec(t),null!=a?(e=parseInt(a[1]),n=parseInt(a[2]),r=parseInt(a[3]),i=parseFloat(a[4])):(i=1,"#"!=t.charAt(0)&&(t=o.colorNameToHex(t),t||(t="#000000")),4===t.length?(e=t.substring(1,2),e+=e,n=t.substring(2,3),n+=n,r=t.substring(3,4),r+=r):(e=t.substring(1,3),n=t.substring(3,5),r=t.substring(5,7)),e=parseInt(e,16),n=parseInt(n,16),r=parseInt(r,16)))}this.ctx.fillStyle=t,this.ctx._isFillTransparent=0==i,this.ctx._fillOpacity=i,this.pdf.setFillColor(e,n,r,{a:i}),this.pdf.setTextColor(e,n,r,{a:i})},setStrokeStyle:function(t){var e=this._getRgba(t);this.ctx.strokeStyle=e.style,this.ctx._isStrokeTransparent=0==e.a,this.ctx._strokeOpacity=e.a,0===e.a?this.pdf.setDrawColor(255,255,255):1===e.a?this.pdf.setDrawColor(e.r,e.g,e.b):this.pdf.setDrawColor(e.r,e.g,e.b)},fillText:function(t,e,n,r){if(!this._isFillTransparent()){e=this._wrapX(e),n=this._wrapY(n);var i=this._matrix_map_point(this.ctx._transform,[e,n]);e=i[0],n=i[1];var o=this._matrix_rotation(this.ctx._transform),a=57.2958*o;if(this.ctx._clip_path.length>0){var s;s=window.outIntercept?"group"===window.outIntercept.type?window.outIntercept.stream:window.outIntercept:this.pdf.internal.pages[1],s.push("q");var c=this.path;this.path=this.ctx._clip_path,this.ctx._clip_path=[],this._fill(null,!0),this.ctx._clip_path=this.path,this.path=c}this.pdf.text(t,e,this._getBaseline(n),null,a),this.ctx._clip_path.length>0&&s.push("Q")}},strokeText:function(t,e,n,r){if(!this._isStrokeTransparent()){e=this._wrapX(e),n=this._wrapY(n);var i=this._matrix_map_point(this.ctx._transform,[e,n]);e=i[0],n=i[1];var o=this._matrix_rotation(this.ctx._transform),a=57.2958*o;if(this.ctx._clip_path.length>0){var s;s=window.outIntercept?"group"===window.outIntercept.type?window.outIntercept.stream:window.outIntercept:this.pdf.internal.pages[1],s.push("q");var c=this.path;this.path=this.ctx._clip_path,this.ctx._clip_path=[],this._fill(null,!0),this.ctx._clip_path=this.path,this.path=c}this.pdf.text(t,e,this._getBaseline(n),{stroke:!0},a),this.ctx._clip_path.length>0&&s.push("Q")}},setFont:function(t){this.ctx.font=t;var e=/\s*(\w+)\s+(\w+)\s+(\w+)\s+([\d\.]+)(px|pt|em)\s+(.*)?/;if(h=e.exec(t),null!=h){var n=h[1],r=(h[2],h[3]),i=h[4],o=h[5],a=h[6];i="px"===o?Math.floor(parseFloat(i)):"em"===o?Math.floor(parseFloat(i)*this.pdf.getFontSize()):Math.floor(parseFloat(i)),this.pdf.setFontSize(i),"bold"===r||"700"===r?this.pdf.setFontStyle("bold"):"italic"===n?this.pdf.setFontStyle("italic"):this.pdf.setFontStyle("normal");var s,c=a,l=c.toLowerCase().split(/\s*,\s*/);s=l.indexOf("arial")!=-1?"Arial":l.indexOf("verdana")!=-1?"Verdana":l.indexOf("helvetica")!=-1?"Helvetica":l.indexOf("sans-serif")!=-1?"sans-serif":l.indexOf("fixed")!=-1?"Fixed":l.indexOf("monospace")!=-1?"Monospace":l.indexOf("terminal")!=-1?"Terminal":l.indexOf("courier")!=-1?"Courier":l.indexOf("times")!=-1?"Times":l.indexOf("cursive")!=-1?"Cursive":l.indexOf("fantasy")!=-1?"Fantasy":(l.indexOf("serif")!=-1,"Serif");var u;u="bold"===r?"bold":"normal",this.pdf.setFont(s,u)}else{var e=/(\d+)(pt|px|em)\s+(\w+)\s*(\w+)?/,h=e.exec(t);if(null!=h){var f=h[1],c=(h[2],h[3]),u=h[4];u||(u="normal"),f="em"===o?Math.floor(parseFloat(i)*this.pdf.getFontSize()):Math.floor(parseFloat(f)),this.pdf.setFontSize(f),this.pdf.setFont(c,u)}}},setTextBaseline:function(t){this.ctx.textBaseline=t},getTextBaseline:function(){return this.ctx.textBaseline},setTextAlign:function(t){this.ctx.textAlign=t},getTextAlign:function(){return this.ctx.textAlign},setLineWidth:function(t){this.ctx.lineWidth=t,this.pdf.setLineWidth(t)},setLineCap:function(t){this.ctx.lineCap=t,this.pdf.setLineCap(t)},setLineJoin:function(t){this.ctx.lineJoin=t,this.pdf.setLineJoin(t)},moveTo:function(t,e){t=this._wrapX(t),e=this._wrapY(e);var n=this._matrix_map_point(this.ctx._transform,[t,e]);t=n[0],e=n[1];var r={type:"mt",x:t,y:e};this.path.push(r)},_wrapX:function(t){return this.pageWrapXEnabled?t%this.pageWrapX:t},_wrapY:function(t){return this.pageWrapYEnabled?(this._gotoPage(this._page(t)),(t-this.lastBreak)%this.pageWrapY):t},transform:function(t,e,n,r,i,o){this.ctx._transform=[t,e,n,r,i,o]},setTransform:function(t,e,n,r,i,o){this.ctx._transform=[t,e,n,r,i,o]},_getTransform:function(){return this.ctx._transform},lastBreak:0,pageBreaks:[],_page:function(t){if(this.pageWrapYEnabled){this.lastBreak=0;for(var e=0,n=0,r=0;r=this.pageBreaks[r]){e++,0===this.lastBreak&&n++;var i=this.pageBreaks[r]-this.lastBreak;this.lastBreak=this.pageBreaks[r];var o=Math.floor(i/this.pageWrapY);n+=o}if(0===this.lastBreak){var o=Math.floor(t/this.pageWrapY)+1;n+=o}return n+e}return this.pdf.internal.getCurrentPageInfo().pageNumber},_gotoPage:function(t){},lineTo:function(t,e){t=this._wrapX(t),e=this._wrapY(e);var n=this._matrix_map_point(this.ctx._transform,[t,e]);t=n[0],e=n[1];var r={type:"lt",x:t,y:e};this.path.push(r)},bezierCurveTo:function(t,e,n,r,i,o){t=this._wrapX(t),e=this._wrapY(e),n=this._wrapX(n),r=this._wrapY(r),i=this._wrapX(i),o=this._wrapY(o);var a;a=this._matrix_map_point(this.ctx._transform,[i,o]),i=a[0],o=a[1],a=this._matrix_map_point(this.ctx._transform,[t,e]),t=a[0],e=a[1],a=this._matrix_map_point(this.ctx._transform,[n,r]),n=a[0],r=a[1];var s={type:"bct",x1:t,y1:e,x2:n,y2:r,x:i,y:o};this.path.push(s)},quadraticCurveTo:function(t,e,n,r){t=this._wrapX(t),e=this._wrapY(e),n=this._wrapX(n),r=this._wrapY(r);var i;i=this._matrix_map_point(this.ctx._transform,[n,r]),n=i[0],r=i[1],i=this._matrix_map_point(this.ctx._transform,[t,e]),t=i[0],e=i[1];var o={type:"qct",x1:t,y1:e,x:n,y:r};this.path.push(o)},arc:function(t,e,n,r,i,o){t=this._wrapX(t),e=this._wrapY(e);var a=this._matrix_map_point(this.ctx._transform,[t,e]);t=a[0],e=a[1];var s={type:"arc",x:t,y:e,radius:n,startAngle:r,endAngle:i,anticlockwise:o};this.path.push(s)},drawImage:function(t,e,n,r,i,o,a,s,c){void 0!==o&&(e=o,n=a,r=s,i=c),e=this._wrapX(e),n=this._wrapY(n);var l,u=this._matrix_map_rect(this.ctx._transform,{x:e,y:n,w:r,h:i}),h=(this._matrix_map_rect(this.ctx._transform,{x:o,y:a,w:s,h:c}),/data:image\/(\w+).*/i),f=h.exec(t);l=null!=f?f[1]:"png",this.pdf.addImage(t,l,u.x,u.y,u.w,u.h)},_matrix_multiply:function(t,e){var n=e[0],r=e[1],i=e[2],o=e[3],a=e[4],s=e[5],c=n*t[0]+r*t[2],l=i*t[0]+o*t[2],u=a*t[0]+s*t[2]+t[4];return r=n*t[1]+r*t[3],o=i*t[1]+o*t[3],s=a*t[1]+s*t[3]+t[5],n=c,i=l,a=u,[n,r,i,o,a,s]},_matrix_rotation:function(t){return Math.atan2(t[2],t[0])},_matrix_decompose:function(t){var e=t[0],n=t[1],r=t[2],i=t[3],o=Math.sqrt(e*e+n*n);e/=o,n/=o;var a=e*r+n*i;r-=e*a,i-=n*a;var s=Math.sqrt(r*r+i*i);return r/=s,i/=s,a/=s,e*i0){var t;t=window.outIntercept?"group"===window.outIntercept.type?window.outIntercept.stream:window.outIntercept:this.pdf.internal.pages[1],t.push("q");var e=this.path;this.path=this.ctx._clip_path,this.ctx._clip_path=[],this._stroke(!0),this.ctx._clip_path=this.path,this.path=e,this._stroke(!1),t.push("Q")}else this._stroke(!1)},_stroke:function(t){if(t||!this._isStrokeTransparent()){for(var e=[],n=!1,r=this.path,i=0;i0){var e;e=window.outIntercept?"group"===window.outIntercept.type?window.outIntercept.stream:window.outIntercept:this.pdf.internal.pages[1],e.push("q");var n=this.path;this.path=this.ctx._clip_path,this.ctx._clip_path=[],this._fill(t,!0),this.ctx._clip_path=this.path,this.path=n,this._fill(t,!1),e.push("Q")}else this._fill(t,!1)},_fill:function(t,e){if(!this._isFillTransparent()){var r,i="function"==typeof this.pdf.internal.newObject2;r=window.outIntercept?"group"===window.outIntercept.type?window.outIntercept.stream:window.outIntercept:this.pdf.internal.pages[1];var o=[],a=window.outIntercept;if(i)switch(this.ctx.globalCompositeOperation){case"normal":case"source-over":break;case"destination-in":case"destination-out":var s=this.pdf.internal.newStreamObject(),c=this.pdf.internal.newObject2();c.push("<>"),c.push(">>");var l="MASK"+c.objId;this.pdf.internal.addGraphicsState(l,c.objId);var u="/"+l+" gs";r.splice(0,0,"q"),r.splice(1,0,u),r.push("Q"),window.outIntercept=s;break;default:var h="/"+this.pdf.internal.blendModeMap[this.ctx.globalCompositeOperation.toUpperCase()];h&&this.pdf.internal.out(h+" gs")}var f=this.ctx.globalAlpha;if(this.ctx._fillOpacity<1&&(f=this.ctx._fillOpacity),i){var d=this.pdf.internal.newObject2();d.push("<>");var l="GS_O_"+d.objId;this.pdf.internal.addGraphicsState(l,d.objId),this.pdf.internal.out("/"+l+" gs")}for(var p=this.path,g=0;g>"),n.push(">>");var r="MASK"+n.objId;this.pdf.internal.addGraphicsState(r,n.objId);var i="/"+r+" gs";this.pdf.internal.out(i)},clip:function(){if(this.ctx._clip_path.length>0)for(var t=0;to)&&(a%=o);var s=n;(so)&&(s%=o);for(var c=[],l=Math.PI/2,u=r?-1:1,h=e,f=Math.min(o,Math.abs(s-a));f>i;){var d=h+u*Math.min(f,l);c.push(this.createSmallArc(t,h,d)),f-=Math.abs(d-h),h=d}return c},n.internal.createSmallArc=function(t,e,n){var r=(n-e)/2,i=t*Math.cos(r),o=t*Math.sin(r),a=i,s=-o,c=a*a+s*s,l=c+a*i+s*o,u=4/3*(Math.sqrt(2*c*l)-l)/(a*o-s*i),h=a-u*s,f=s+u*a,d=h,p=-f,g=r+e,m=Math.cos(g),w=Math.sin(g);return{x1:t*Math.cos(e),y1:t*Math.sin(e),x2:h*m-f*w,y2:h*w+f*m,x3:d*m-p*w,y3:d*w+p*m,x4:t*Math.cos(n),y4:t*Math.sin(n)}},this}(e.API),/** @preserve + * jsPDF fromHTML plugin. BETA stage. API subject to change. Needs browser + * Copyright (c) 2012 Willow Systems Corporation, willow-systems.com + * 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria + * 2014 Diego Casorran, https://github.com/diegocr + * 2014 Daniel Husar, https://github.com/danielhusar + * 2014 Wolfgang Gassler, https://github.com/woolfg + * 2014 Steven Spungin, https://github.com/flamenco + * + * + * ==================================================================== + */ +function(e){var n,r,i,a,s,c,l,u,h,f,d,p,g,m,w,y,v,b,x,k;n=function(){function t(){}return function(e){return t.prototype=e,new t}}(),f=function(t){var e,n,r,i,o,a,s;for(n=0,r=t.length,e=void 0,i=!1,a=!1;!i&&n!==r;)e=t[n]=t[n].trimLeft(),e&&(i=!0),n++;for(n=r-1;r&&!a&&n!==-1;)e=t[n]=t[n].trimRight(),e&&(a=!0),n--;for(o=/\s+$/g,s=!0,n=0;n!==r;)"\u2028"!=t[n]&&(e=t[n].replace(/\s+/g," "),s&&(e=e.trimLeft()),e&&(s=o.test(e)),t[n]=e),n++;return t},d=function(t,e,n,r){return this.pdf=t,this.x=e,this.y=n,this.settings=r,this.watchFunctions=[],this.init(),this},p=function(t){var e,n,r;for(e=void 0,r=t.split(","),n=r.shift();!e&&n;)e=i[n.trim().toLowerCase()],n=r.shift();return e},g=function(t){t="auto"===t?"0px":t,t.indexOf("em")>-1&&!isNaN(Number(t.replace("em","")))&&(t=18.719*Number(t.replace("em",""))+"px"),t.indexOf("pt")>-1&&!isNaN(Number(t.replace("pt","")))&&(t=1.333*Number(t.replace("pt",""))+"px");var e,n,r;return n=void 0,e=16,(r=m[t])?r:(r={"xx-small":9,"x-small":11,small:13,medium:16,large:19,"x-large":23,"xx-large":28,auto:0}[{css_line_height_string:t}],r!==n?m[t]=r/e:(r=parseFloat(t))?m[t]=r/e:(r=t.match(/([\d\.]+)(px)/),3===r.length?m[t]=parseFloat(r[1])/e:m[t]=1))},h=function(t){var e,n,r;return r=function(t){var e;return e=function(t){return document.defaultView&&document.defaultView.getComputedStyle?document.defaultView.getComputedStyle(t,null):t.currentStyle?t.currentStyle:t.style}(t),function(t){return t=t.replace(/-\D/g,function(t){return t.charAt(1).toUpperCase()}),e[t]}}(t),e={},n=void 0,e["font-family"]=p(r("font-family"))||"times",e["font-style"]=a[r("font-style")]||"normal",e["text-align"]=s[r("text-align")]||"left",n=c[r("font-weight")]||"normal","bold"===n&&("normal"===e["font-style"]?e["font-style"]=n:e["font-style"]=n+e["font-style"]),e["font-size"]=g(r("font-size"))||1,e["line-height"]=g(r("line-height"))||1,e.display="inline"===r("display")?"inline":"block",n="block"===e.display,e["margin-top"]=n&&g(r("margin-top"))||0,e["margin-bottom"]=n&&g(r("margin-bottom"))||0,e["padding-top"]=n&&g(r("padding-top"))||0,e["padding-bottom"]=n&&g(r("padding-bottom"))||0,e["margin-left"]=n&&g(r("margin-left"))||0,e["margin-right"]=n&&g(r("margin-right"))||0,e["padding-left"]=n&&g(r("padding-left"))||0,e["padding-right"]=n&&g(r("padding-right"))||0,e["page-break-before"]=r("page-break-before")||"auto",e.float=l[r("cssFloat")]||"none",e.clear=u[r("clear")]||"none",e.color=r("color"),e},w=function(t,e,n){var r,i,o,a,s;if(o=!1,i=void 0,a=void 0,s=void 0,r=n["#"+t.id])if("function"==typeof r)o=r(t,e);else for(i=0,a=r.length;!o&&i!==a;)o=r[i](t,e),i++;if(r=n[t.nodeName],!o&&r)if("function"==typeof r)o=r(t,e);else for(i=0,a=r.length;!o&&i!==a;)o=r[i](t,e),i++;return o},k=function(t,e){var n,r,i,o,a,s,c,l,u,h;for(n=[],r=[],i=0,h=t.rows[0].cells.length,l=t.clientWidth;ii.pdf.margins_doc.top&&(i.pdf.addPage(),i.y=i.pdf.margins_doc.top,i.executeWatchFunctions(a));var P=h(a),E=i.x,O=12/i.pdf.internal.scaleFactor,F=(P["margin-left"]+P["padding-left"])*O,R=(P["margin-right"]+P["padding-right"])*O,B=(P["margin-top"]+P["padding-top"])*O,D=(P["margin-bottom"]+P["padding-bottom"])*O;E+=void 0!==P.float&&"right"===P.float?i.settings.width-a.width-R:F,i.pdf.addImage(T,E,i.y+B,a.width,a.height),T=void 0,"right"===P.float||"left"===P.float?(i.watchFunctions.push(function(t,e,n,r){return i.y>=e?(i.x+=t,i.settings.width+=n,!0):!!(r&&1===r.nodeType&&!_[r.nodeName]&&i.x+r.width>i.pdf.margins_doc.left+i.pdf.margins_doc.width)&&(i.x+=t,i.y=e,i.settings.width+=n,!0)}.bind(this,"left"===P.float?-a.width-F-R:0,i.y+a.height+B+D,a.width)),i.watchFunctions.push(function(t,e,n){return!(i.y0){i=i[0];var o=e.pdf.internal.write,a=e.y;e.pdf.internal.write=function(){},r(i,e,n);var s=Math.ceil(e.y-a)+5;e.y=a,e.pdf.internal.write=o,e.pdf.margins_doc.bottom+=s;for(var c=function(t){var o=void 0!==t?t.pageNumber:1,a=e.y;e.y=e.pdf.internal.pageSize.height-e.pdf.margins_doc.bottom,e.pdf.margins_doc.bottom-=s;for(var c=i.getElementsByTagName("span"),l=0;l-1&&(c[l].innerHTML=o),(" "+c[l].className+" ").replace(/[\n\t]/g," ").indexOf(" totalPages ")>-1&&(c[l].innerHTML="###jsPDFVarTotalPages###");r(i,e,n),e.pdf.margins_doc.bottom+=s,e.y=a},l=i.getElementsByTagName("span"),u=0;u-1&&e.pdf.internal.events.subscribe("htmlRenderingFinished",e.pdf.putTotalPages.bind(e.pdf,"###jsPDFVarTotalPages###"),!0);e.pdf.internal.events.subscribe("addPage",c,!1),c(),_.FOOTER=1}},x=function(t,e,n,i,o,a){if(!e)return!1;"string"==typeof e||e.parentNode||(e=""+e.innerHTML),"string"==typeof e&&(e=function(t){var e,n,r,i;return r="jsPDFhtmlText"+Date.now().toString()+(1e3*Math.random()).toFixed(0),i="position: absolute !important;clip: rect(1px 1px 1px 1px); /* IE6, IE7 */clip: rect(1px, 1px, 1px, 1px);padding:0 !important;border:0 !important;height: 1px !important;width: 1px !important; top:auto;left:-100px;overflow: hidden;",n=document.createElement("div"),n.style.cssText=i,n.innerHTML='