diff --git a/abonapp/forms.py b/abonapp/forms.py index 36605a3..eefc816 100644 --- a/abonapp/forms.py +++ b/abonapp/forms.py @@ -226,13 +226,12 @@ class AddIpForm(forms.ModelForm): groups=instance.group ).first() if net is not None: - ips = ( - ip.ip_address for ip in models.Abon.objects.filter( - group__in=net.groups.all() - ).order_by('ip_address').only( - 'ip_address' - ).iterator() - ) + ips = (ip.ip_address for ip in + models.Abon.objects.filter( + group__in=net.groups.all(), + nas=instance.nas + ).order_by('ip_address').only( + 'ip_address').iterator()) free_ip = net.get_free_ip(ips) self.initial['ip_address'] = free_ip else: diff --git a/abonapp/locale/ru/LC_MESSAGES/django.po b/abonapp/locale/ru/LC_MESSAGES/django.po index 2d77f35..758df1c 100644 --- a/abonapp/locale/ru/LC_MESSAGES/django.po +++ b/abonapp/locale/ru/LC_MESSAGES/django.po @@ -1168,3 +1168,6 @@ msgstr "Балланс" msgid "Date joined" msgstr "Дата создания" + +msgid "Update ip address" +msgstr "Обновить ip адрес" diff --git a/abonapp/migrations/0001_squashed_0008_auto_20181115_1206.py b/abonapp/migrations/0001_squashed_0008_auto_20181115_1206.py new file mode 100644 index 0000000..28accc3 --- /dev/null +++ b/abonapp/migrations/0001_squashed_0008_auto_20181115_1206.py @@ -0,0 +1,356 @@ +# Generated by Django 2.1.1 on 2019-03-05 20:00 + +import bitfield.models +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + replaces = [('abonapp', '0001_initial'), ('abonapp', '0002_auto_20180808_1448'), ('abonapp', '0003_abon_nas'), + ('abonapp', '0004_auto_20180918_1734'), ('abonapp', '0005_current_tariff'), + ('abonapp', '0006_change_ip'), ('abonapp', '0007_auto_20181101_1545'), + ('abonapp', '0008_auto_20181115_1206')] + + initial = True + + dependencies = [ + ('tariff_app', '0003_auto_20181115_1206'), + ('gw_app', '0001_initial'), + ('tariff_app', '0001_initial'), + ('ip_pool', '0001_initial'), + ('gw_app', '0002_auto_20181101_1545'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('group_app', '0001_initial'), + ('accounts_app', '0001_initial'), + ('devapp', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AbonTariff', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('tariff', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='linkto_tariff', + to='tariff_app.Tariff' + )), + ('time_start', models.DateTimeField( + blank=True, default=None, null=True + )), + ('deadline', models.DateTimeField( + blank=True, default=None, null=True + )), + ], + options={ + 'ordering': ('time_start',), + 'permissions': (('can_complete_service', 'finish service perm'),), + 'verbose_name': 'Abon service', + 'verbose_name_plural': 'Abon services', + 'db_table': 'abonent_tariff' + }, + ), + migrations.CreateModel( + name='AbonStreet', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64)), + ('group', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='group_app.Group' + )), + ], + options={ + 'verbose_name': 'Street', + 'verbose_name_plural': 'Streets', + 'db_table': 'abon_street', + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='Abon', + fields=[ + ('baseaccount_ptr', models.OneToOneField( + auto_created=True, on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='accounts_app.BaseAccount' + )), + ('current_tariff', models.OneToOneField( + blank=True, default=None, null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='abonapp.AbonTariff' + )), + ('group', models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='group_app.Group', verbose_name='User group' + )), + ('ballance', models.FloatField(default=0.0)), + ('ip_address', models.GenericIPAddressField( + blank=True, null=True, + verbose_name='Ip address' + )), + ('description', models.TextField( + blank=True, null=True, verbose_name='Comment' + )), + ('street', models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='abonapp.AbonStreet', verbose_name='Street' + )), + ('house', models.CharField( + blank=True, max_length=12, + null=True, verbose_name='House' + )), + ('device', models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='devapp.Device' + )), + ('dev_port', models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='devapp.Port' + )), + ('is_dynamic_ip', models.BooleanField( + default=False, verbose_name='Is dynamic ip' + )), + ('nas', models.ForeignKey( + blank=True, default=None, null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='gw_app.NASModel', verbose_name='Network access server' + )), + ('autoconnect_service', models.BooleanField( + default=False, verbose_name='Automatically connect next service' + )), + ('last_connected_tariff', models.ForeignKey( + blank=True, default=None, null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='tariff_app.Tariff', + verbose_name='Last connected service' + )), + ('markers', bitfield.models.BitField( + (('icon_donkey', 'Donkey'), ('icon_fire', 'Fire'), + ('icon_ok', 'Ok'), ('icon_king', 'King'), + ('icon_tv', 'TV'), ('icon_smile', 'Smile'), + ('icon_dollar', 'Dollar'), ('icon_service', 'Service'), + ('icon_mrk', 'Marker')), default=0 + )), + ], + options={ + 'ordering': ('fio',), + 'permissions': ( + ('can_buy_tariff', 'Buy service perm'), + ('can_add_ballance', 'fill account'), + ('can_ping', 'Can ping')), + 'verbose_name': 'Abon', + 'verbose_name_plural': 'Abons', + 'db_table': 'abonent', + 'unique_together': {('ip_address', 'nas')} + }, + bases=('accounts_app.baseaccount',), + ), + migrations.CreateModel( + name='AbonLog', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('abon', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='abonapp.Abon')), + ('amount', models.FloatField(default=0.0)), + ('author', models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='+', to=settings.AUTH_USER_MODEL + )), + ('comment', models.CharField(max_length=128)), + ('date', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'db_table': 'abonent_log', + 'ordering': ('-date',) + }, + ), + migrations.CreateModel( + name='AdditionalTelephone', + fields=[ + ('abon', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='additional_telephones', + to='abonapp.Abon' + )), + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('telephone', models.CharField( + max_length=16, validators=[ + django.core.validators.RegexValidator('^(\\+[7,8,9,3]\\d{10,11})?$') + ], + verbose_name='Telephone') + ), + ('owner_name', models.CharField(max_length=127)), + ], + options={ + 'verbose_name': 'Additional telephone', + 'verbose_name_plural': 'Additional telephones', + 'db_table': 'additional_telephones', + 'ordering': ('owner_name',), + }, + ), + migrations.CreateModel( + name='AllPayLog', + fields=[ + ('pay_id', models.CharField(max_length=64, primary_key=True, serialize=False)), + ('date_action', models.DateTimeField(auto_now_add=True)), + ('summ', models.FloatField(default=0.0)), + ('pay_system_name', models.CharField(max_length=16)), + ], + options={ + 'db_table': 'all_pay_log', + 'ordering': ('-date_action',), + }, + ), + migrations.CreateModel( + name='AllTimePayLog', + fields=[ + ('abon', + models.ForeignKey( + blank=True, default=None, null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to='abonapp.Abon' + )), + ('pay_id', models.CharField( + max_length=36, primary_key=True, + serialize=False, unique=True + )), + ('date_add', models.DateTimeField(auto_now_add=True)), + ('summ', models.FloatField(default=0.0)), + ('trade_point', models.CharField( + blank=True, default=None, max_length=20, + null=True, verbose_name='Trade point' + )), + ('receipt_num', models.BigIntegerField(default=0, verbose_name='Receipt number')), + ], + options={ + 'db_table': 'all_time_pay_log', + 'ordering': ('-date_add',), + }, + ), + migrations.CreateModel( + name='InvoiceForPayment', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('abon', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='abonapp.Abon' + )), + ('status', models.BooleanField(default=False)), + ('amount', models.FloatField(default=0.0)), + ('comment', models.CharField(max_length=128)), + ('date_create', models.DateTimeField(auto_now_add=True)), + ('date_pay', models.DateTimeField(blank=True, null=True)), + ('author', models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='+', to=settings.AUTH_USER_MODEL + )) + ], + options={ + 'verbose_name': 'Debt', + 'verbose_name_plural': 'Debts', + 'db_table': 'abonent_inv_pay', + 'ordering': ('date_create',) + }, + ), + migrations.CreateModel( + name='PassportInfo', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('series', models.CharField( + max_length=4, validators=[django.core.validators.integer_validator], + verbose_name='Pasport serial' + )), + ('number', models.CharField( + max_length=6, validators=[django.core.validators.integer_validator], + verbose_name='Pasport number' + )), + ('distributor', models.CharField( + max_length=64, verbose_name='Distributor' + )), + ('date_of_acceptance', models.DateField( + verbose_name='Date of acceptance' + )), + ('abon', models.OneToOneField( + blank=True, null=True, + on_delete=django.db.models.deletion.CASCADE, + to='abonapp.Abon' + )) + ], + options={ + 'ordering': ('series',), + 'verbose_name': 'Passport Info', + 'verbose_name_plural': 'Passport Info', + 'db_table': 'passport_info' + }, + ), + migrations.CreateModel( + name='PeriodicPayForId', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('periodic_pay', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='tariff_app.PeriodicPay', + verbose_name='Periodic pay' + )), + ('last_pay', models.DateTimeField( + blank=True, null=True, verbose_name='Last pay time' + )), + ('next_pay', models.DateTimeField( + verbose_name='Next time to pay' + )), + ('account', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='abonapp.Abon', + verbose_name='Account' + )) + ], + options={ + 'db_table': 'periodic_pay_for_id', + 'ordering': ('last_pay',) + }, + ), + migrations.CreateModel( + name='AbonRawPassword', + fields=[ + ('account', models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, serialize=False, + to='abonapp.Abon' + )), + ('passw_text', models.CharField(max_length=64)), + ], + options={ + 'db_table': 'abon_raw_password', + }, + ), + ] diff --git a/abonapp/migrations/0002_auto_20180808_1448.py b/abonapp/migrations/0002_auto_20180808_1448.py index 93694e6..71f992f 100644 --- a/abonapp/migrations/0002_auto_20180808_1448.py +++ b/abonapp/migrations/0002_auto_20180808_1448.py @@ -19,12 +19,15 @@ TMP_FILE = '/tmp/djing_ip_field_abonapp_migrate.json' def backup_info(apps, _): Abon = apps.get_model('abonapp', 'Abon') obs = Abon.objects.exclude(ip_address=None).only('ip_address', 'is_dynamic_ip') - with open(TMP_FILE, 'w') as f: - serializers.serialize('json', obs, stream=f, fields=('ip_address', 'is_dynamic_ip')) + if obs.exists(): + with open(TMP_FILE, 'w') as f: + serializers.serialize('json', obs, stream=f, fields=('ip_address', 'is_dynamic_ip')) def restore_info_to_new_scheme(apps, _): Abon = apps.get_model('abonapp', 'Abon') + if not os.path.isfile(TMP_FILE): + return with open(TMP_FILE, 'r') as f: for abon in load(f): ip_addr = abon['fields'].get('ip_address') diff --git a/abonapp/models.py b/abonapp/models.py index 3f58cd8..3e0dd2b 100644 --- a/abonapp/models.py +++ b/abonapp/models.py @@ -7,8 +7,7 @@ from django.conf import settings from django.core import validators from django.core.validators import RegexValidator from django.db import models, transaction -from django.db.models.signals import post_delete, pre_delete, post_init, \ - pre_save +from django.db.models.signals import post_init, pre_save from django.dispatch import receiver from django.shortcuts import resolve_url from django.utils import timezone @@ -349,24 +348,6 @@ class Abon(BaseAccount): except LogicError: pass - def nas_remove_self(self): - """ - Will remove this user to network access server - :return: - """ - if self.nas is None: - raise LogicError(_('gateway required')) - try: - agent_abon = self.build_agent_struct() - if agent_abon is not None: - mngr = self.nas.get_nas_manager() - mngr.remove_user(agent_abon) - except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: - print('ERROR:', e) - return e - except LogicError: - pass - def get_absolute_url(self): return resolve_url('abonapp:abon_home', self.group.id, self.username) @@ -539,17 +520,6 @@ class PeriodicPayForId(models.Model): ordering = ('last_pay',) -@receiver(post_delete, sender=Abon) -def abon_del_signal(sender, **kwargs): - abon = kwargs.get("instance") - if abon is None: - raise ValueError('Instance does not passed to a signal') - try: - abon.nas_remove_self() - except (NasFailedResult, NasNetworkError, LogicError): - return True - - @receiver(post_init, sender=AbonTariff) def abon_tariff_post_init(sender, **kwargs): abon_tariff = kwargs["instance"] @@ -566,17 +536,3 @@ def abon_tariff_pre_save(sender, **kwargs): if getattr(abon_tariff, 'deadline') is None: calc_obj = abon_tariff.tariff.get_calc_type()(abon_tariff) abon_tariff.deadline = calc_obj.calc_deadline() - - -@receiver(pre_delete, sender=AbonTariff) -def abontariff_pre_delete(sender, **kwargs): - abon_tariff = kwargs.get("instance") - if abon_tariff is None: - raise ValueError('Instance does not passed to a signal') - try: - abon = Abon.objects.get(current_tariff=abon_tariff) - abon.nas_remove_self() - except (NasFailedResult, NasNetworkError, LogicError): - return True - except Abon.DoesNotExist: - print('Error: abontariff_pre_delete - user not found') diff --git a/abonapp/tasks.py b/abonapp/tasks.py new file mode 100644 index 0000000..22e8773 --- /dev/null +++ b/abonapp/tasks.py @@ -0,0 +1,47 @@ +from celery import shared_task + +from abonapp.models import Abon +from djing.lib import LogicError +from gw_app.models import NASModel +from gw_app.nas_managers import NasFailedResult, NasNetworkError, SubnetQueue + + +@shared_task +def customer_nas_command(customer_uid: int, command: str): + if command not in ('add', 'sync'): + return 'Command required' + try: + cust = Abon.objects.get(pk=customer_uid) + print(cust, command) + if command == 'sync': + r = cust.nas_sync_self() + if isinstance(r, Exception): + return 'ABONAPP SYNC ERROR: %s' % r + elif command == 'add': + cust.nas_add_self() + else: + return 'ABONAPP SYNC ERROR: Unknown command "%s"' % command + except Abon.DoesNotExist: + pass + except (LogicError, NasFailedResult, NasNetworkError, ConnectionResetError) as e: + return 'ABONAPP ERROR: %s' % e + + +@shared_task +def customer_nas_remove(customer_uid: int, ip_addr: str, speed: tuple, is_access: bool, nas_pk: int): + try: + if not isinstance(ip_addr, (str, int)): + ip_addr = str(ip_addr) + sq = SubnetQueue( + name="uid%d" % customer_uid, + network=ip_addr, + max_limit=speed, + is_access=is_access + ) + nas = NASModel.objects.get(pk=nas_pk) + mngr = nas.get_nas_manager() + mngr.remove_user(sq) + except (ValueError, NasFailedResult, NasNetworkError, LogicError) as e: + return 'ABONAPP ERROR: %s' % e + except NASModel.DoesNotExist: + return 'NASModel.DoesNotExist id=%d' % nas_pk diff --git a/abonapp/templates/abonapp/debtors.html b/abonapp/templates/abonapp/debtors.html index 01d1a2b..dda83d5 100644 --- a/abonapp/templates/abonapp/debtors.html +++ b/abonapp/templates/abonapp/debtors.html @@ -31,13 +31,19 @@ {% for invoice in invoices %} {{ invoice.id }} - {{ invoice.abon.username }} + + + {{ invoice.abon.username }} + + {{ invoice.amount }} {{ invoice.comment }} {{ invoice.date_create|date:'d b H:i' }} - {{ invoice.author.username }} + + + {{ invoice.author.username }} + + {% empty %} diff --git a/abonapp/templates/abonapp/editAbon.html b/abonapp/templates/abonapp/editAbon.html index f7116ed..84432d4 100644 --- a/abonapp/templates/abonapp/editAbon.html +++ b/abonapp/templates/abonapp/editAbon.html @@ -136,7 +136,7 @@
{% if device %} - + {{ device.comment|truncatechars:11 }} {{ device.ip_address|default:'' }} diff --git a/abonapp/templates/abonapp/group_list.html b/abonapp/templates/abonapp/group_list.html index a243df0..b3d671d 100644 --- a/abonapp/templates/abonapp/group_list.html +++ b/abonapp/templates/abonapp/group_list.html @@ -65,7 +65,12 @@ {% endif %} - + {% if request.user.is_superuser %} + + + + {% endif %} + diff --git a/abonapp/templates/abonapp/invoiceForPayment.html b/abonapp/templates/abonapp/invoiceForPayment.html index 34c67a0..320cd12 100644 --- a/abonapp/templates/abonapp/invoiceForPayment.html +++ b/abonapp/templates/abonapp/invoiceForPayment.html @@ -48,7 +48,7 @@ {% endif %} - {{ inv.author.username }} + {{ inv.author.username }} {% empty %} diff --git a/abonapp/templates/abonapp/modal_phonebook.html b/abonapp/templates/abonapp/modal_phonebook.html index 99b4d50..822597c 100644 --- a/abonapp/templates/abonapp/modal_phonebook.html +++ b/abonapp/templates/abonapp/modal_phonebook.html @@ -26,7 +26,7 @@ diff --git a/abonapp/templates/abonapp/payHistory.html b/abonapp/templates/abonapp/payHistory.html index 93e74f9..0099097 100644 --- a/abonapp/templates/abonapp/payHistory.html +++ b/abonapp/templates/abonapp/payHistory.html @@ -14,11 +14,11 @@ {% for ph in pay_history %} - {{ ph.amount }} + {{ ph.amount|floatformat:2 }} {{ ph.date|date:'d F Y, H:i:s' }} {% if ph.author %} - {{ ph.author.username }} + {{ ph.author.username }} {% else %} {% trans 'System' %} {% endif %} diff --git a/abonapp/templates/abonapp/peoples.html b/abonapp/templates/abonapp/peoples.html index a74ca9e..605d336 100644 --- a/abonapp/templates/abonapp/peoples.html +++ b/abonapp/templates/abonapp/peoples.html @@ -24,7 +24,7 @@ - + - + - {% with can_ch_trf=perms.tariff_app.change_tariff can_del_abon=perms.abonapp.delete_abon %} + {% with can_ch_trf=perms.tariff_app.change_tariff %} {% for human in object_list %} {% if human.is_active %} @@ -76,27 +76,26 @@ {% endif %} - + @@ -115,17 +114,17 @@ {% for user_icon in human.get_flag_icons %} {% endfor %} - {% empty %} - {% for usr in users %} - - + + - - + + - - + + - - + + - - + + - - + + {% if request.user.is_superuser %} - - + + {% endif %} diff --git a/accounts_app/templates/accounts/settings/ext.htm b/accounts_app/templates/accounts/settings/ext.htm index c62ea2b..caedf53 100644 --- a/accounts_app/templates/accounts/settings/ext.htm +++ b/accounts_app/templates/accounts/settings/ext.htm @@ -5,7 +5,13 @@ {% endblock %} @@ -17,10 +23,9 @@ {% block main %}
-
{% csrf_token %} + {% csrf_token %} - ava + ava
@@ -30,11 +35,10 @@
-

{{ user.username|default:_('Not assigned') }}

+

{{ object.username|default:_('Not assigned') }}

diff --git a/devapp/templates/devapp/custom_dev_page/onu.html b/devapp/templates/devapp/custom_dev_page/onu.html index ae164fe..fd6258a 100644 --- a/devapp/templates/devapp/custom_dev_page/onu.html +++ b/devapp/templates/devapp/custom_dev_page/onu.html @@ -21,8 +21,7 @@ {% for da in dev_accs %}
  • {% trans 'Attached user' %}: {% if da.group %} - {{ da.get_full_name }} + {{ da.get_full_name }} {% else %} {{ da.get_full_name }} {% endif %} @@ -32,7 +31,7 @@
  • {% with pdev=dev.parent_dev pdgrp=dev.parent_dev.group %} {% trans 'Parent device' %}: - + {{ pdev.ip_address|default:'-' }} {{ pdev.comment }} {% endwith %} diff --git a/devapp/templates/devapp/custom_dev_page/onu_for_zte.html b/devapp/templates/devapp/custom_dev_page/onu_for_zte.html index fa01ee6..50cf3ee 100644 --- a/devapp/templates/devapp/custom_dev_page/onu_for_zte.html +++ b/devapp/templates/devapp/custom_dev_page/onu_for_zte.html @@ -22,8 +22,7 @@ {% for da in dev_accs %}
  • {% trans 'Attached user' %}: {% if da.group %} - {{ da.get_full_name }} + {{ da.get_full_name }} {% else %} {{ da.get_full_name }} {% endif %} @@ -33,9 +32,7 @@
  • {% with pdev=dev.parent_dev pdgrp=dev.parent_dev.group %} {% trans 'Parent device' %}: - + {{ pdev.ip_address|default:'-' }} {{ pdev.comment }} {% endwith %} @@ -73,15 +70,17 @@
  • - {% trans 'Name on OLT' %}: {{ onu_details.name }}
    - {% trans 'Distance(m)' %}: {{ onu_details.distance }}
    + {% trans 'Distance(m)' %}: {{ onu_details.distance|default:'-' }}
    {% trans 'Signal' %}: {{ onu_details.signal }}
    {% if onu_details.ip_addr %} {% trans 'Ip addr' %}: {{ onu_details.ip_addr }}
    {% endif %} {% if onu_details.vlans %} - {% trans 'VLan list' %}: {{ onu_details.vlans }} + {% trans 'VLan list' %}: {{ onu_details.vlans }}
    {% endif %} + {% trans 'Serial' %}: {{ onu_details.serial|default:'-' }}
    + {% trans 'Onu type' %}: {{ onu_details.onu_type|default:'-' }}
    + {% trans 'Name' %}: {{ onu_details.int_name|default:'-' }}
    diff --git a/devapp/templates/devapp/group_list.html b/devapp/templates/devapp/group_list.html index e124712..a3f2c9d 100644 --- a/devapp/templates/devapp/group_list.html +++ b/devapp/templates/devapp/group_list.html @@ -35,7 +35,7 @@ {% trans 'Devices without group' %} - + {% trans 'Export to nagios objects' %} diff --git a/devapp/urls.py b/devapp/urls.py index fd84e72..c3d9daf 100644 --- a/devapp/urls.py +++ b/devapp/urls.py @@ -5,52 +5,32 @@ app_name = 'devapp' urlpatterns = [ path('', views.GroupsListView.as_view(), name='group_list'), - path('devices_without_groups/', - views.DevicesWithoutGroupsListView.as_view(), - name='devices_null_group'), + path('devices_without_groups/', views.DevicesWithoutGroupsListView.as_view(), name='devices_null_group'), path('fix_onu/', views.fix_onu, name='fix_onu'), path('/', views.DevicesListView.as_view(), name='devs'), path('/add/', views.DeviceCreateView.as_view(), name='add'), path('//', views.devview, name='view'), - path('//del/', - views.DeviceDeleteView.as_view(), name='del'), - path('//add/', views.add_single_port, - name='add_port'), - path('//edit/', views.DeviceUpdate.as_view(), - name='edit'), - path('//edit_extra/', - views.DeviceUpdateExtra.as_view(), name='extra_data_edit'), - path( - '//ports//fix_port_conflict/', - views.fix_port_conflict, - name='fix_port_conflict'), - path( - '//ports//show_subscriber_on_port/', - views.ShowSubscriberOnPort.as_view(), name='show_subscriber_on_port'), - path('//ports_add/', views.add_ports, - name='add_ports'), - path('//register_device/', - views.register_device, name='dev_register'), - re_path('^(\d+)/(?P\d+)/(?P\d+)_(?P[0-1]{1})$', - views.toggle_port, name='port_toggle'), - path('///del/', - views.delete_single_port, name='del_port'), - path('///edit/', - views.EditSinglePort.as_view(), name='edit_port'), - path('fix_device_group//', views.fix_device_group, - name='fix_device_group'), + path('//del/', views.DeviceDeleteView.as_view(), name='del'), + path('//add/', views.add_single_port, name='add_port'), + path('//edit/', views.DeviceUpdate.as_view(), name='edit'), + path('//edit_extra/', views.DeviceUpdateExtra.as_view(), name='extra_data_edit'), + path('//ports//fix_port_conflict/', views.fix_port_conflict, name='fix_port_conflict'), + path('//ports//show_subscriber_on_port/', views.ShowSubscriberOnPort.as_view(), name='show_subscriber_on_port'), + path('//ports_add/', views.add_ports, name='add_ports'), + path('//register_device/', views.register_device, name='dev_register'), + re_path('^(\d+)/(?P\d+)/(?P\d+)_(?P[0-1]{1})$', views.toggle_port, name='port_toggle'), + path('///del/', views.delete_single_port, name='del_port'), + path('///edit/', views.EditSinglePort.as_view(), name='edit_port'), + path('fix_device_group//', views.fix_device_group, name='fix_device_group'), path('search_dev/', views.search_dev), # ZTE ports under fibers - path('///', - views.zte_port_view_uncfg, name='zte_port_view_uncfg'), + path('///', views.zte_port_view_uncfg, name='zte_port_view_uncfg'), # Monitoring api path('on_device_event/', views.OnDeviceMonitoringEvent.as_view()), # Nagios mon generate - path('nagios/hosts/', views.nagios_objects_conf, - name='nagios_objects_conf'), - path('api/getall/', views.DevicesGetListView.as_view(), - name='nagios_get_all_hosts') + path('nagios/hosts/', views.nagios_objects_conf, name='nagios_objects_conf'), + path('api/getall/', views.DevicesGetListView.as_view(), name='nagios_get_all_hosts') ] diff --git a/devapp/views.py b/devapp/views.py index 3613f52..aa8129c 100644 --- a/devapp/views.py +++ b/devapp/views.py @@ -3,7 +3,6 @@ from ipaddress import ip_address from abonapp.models import Abon from accounts_app.models import UserProfile -from chatbot.models import ChatException from devapp.base_intr import DeviceImplementationError from django.conf import settings from django.contrib import messages @@ -22,17 +21,16 @@ from djing.lib import safe_int, ProcessLocked, DuplicateEntry from djing.lib.decorators import json_view from djing.lib.decorators import only_admins, hash_auth_view from djing.lib.mixins import LoginAdminPermissionMixin, LoginAdminMixin -from djing.lib.tln import ZteOltConsoleError, OnuZteRegisterError, \ - ZteOltLoginFailed from djing.tasks import multicast_email_notify from easysnmp import EasySNMPTimeoutError, EasySNMPError from group_app.models import Group -from guardian.decorators import \ - permission_required_or_403 as permission_required +from messenger.tasks import multicast_viber_notify +from guardian.decorators import permission_required_or_403 as permission_required from guardian.shortcuts import get_objects_for_user -from .forms import DeviceForm, PortForm, DeviceExtraDataForm -from .models import Device, Port, DeviceDBException, DeviceMonitoringException -from .tasks import onu_register +from devapp.forms import DeviceForm, PortForm, DeviceExtraDataForm +from devapp.models import Device, Port, DeviceDBException, DeviceMonitoringException +from devapp.tasks import onu_register +from devapp import onu_config class DevicesListView(LoginAdminPermissionMixin, @@ -485,9 +483,8 @@ def devview(request, group_id: int, device_id: int): template_name = 'generic_switch.html' try: - if device.ip_address: - if not ping(str(device.ip_address)): - messages.error(request, _('Dot was not pinged')) + if device.ip_address and not ping(str(device.ip_address)): + messages.error(request, _('Dot was not pinged')) if device.man_passw: manager = device.get_manager_object() ports = tuple(manager.get_ports()) @@ -498,13 +495,15 @@ def devview(request, group_id: int, device_id: int): template_name = manager.get_template_name() else: messages.warning(request, _('Not Set snmp device password')) + return render(request, 'devapp/custom_dev_page/' + template_name, { 'dev': device, 'ports': ports, 'dev_accs': Abon.objects.filter(device=device), 'dev_manager': manager, 'ports_db': Port.objects.filter(device=device).annotate( - num_abons=Count('abon')), + num_abons=Count('abon') + ), }) except EasySNMPError as e: messages.error(request, @@ -726,21 +725,22 @@ class OnDeviceMonitoringEvent(global_base_views.SecureApiView): } recipients = UserProfile.objects.get_profiles_by_group( - device_down.group.pk) + device_down.group.pk).filter(flags=UserProfile.flags.notify_mon) - multicast_email_notify.delay(msg_text=gettext(notify_text) % { + user_ids = tuple(recipient.pk for recipient in recipients.only('pk').iterator()) + text = gettext(notify_text) % { 'device_name': "%s(%s) %s" % ( device_down.ip_address, device_down.mac_addr, device_down.comment ) - }, account_ids=( - recipient.pk for recipient in recipients.only('pk').iterator() - )) + } + multicast_email_notify.delay(msg_text=text, account_ids=user_ids) + multicast_viber_notify.delay(None, account_id_list=user_ids, message_text=text) return { 'text': 'notification successfully sent' } - except ChatException as e: + except ValueError as e: return { 'text': str(e) } @@ -802,17 +802,25 @@ def register_device(request, group_id: int, device_id: int): try: device.register_device() status = 0 - except OnuZteRegisterError: + except onu_config.OnuZteRegisterError: text = format_msg(gettext('Unregistered onu not found'), 'eye-close') - except ZteOltLoginFailed: - text = format_msg(gettext('Wrong login or password for telnet access'), - 'lock') - except (ConnectionRefusedError, ZteOltConsoleError) as e: + except onu_config.ZteOltLoginFailed: + text = format_msg( + gettext('Wrong login or password for telnet access'), + 'lock' + ) + except ( + ConnectionRefusedError, onu_config.ZteOltConsoleError, + onu_config.ExpectValidationError, onu_config.ZTEFiberIsFull + ) as e: text = format_msg(e, 'exclamation-sign') except DeviceImplementationError as e: - text = format_msg(e, 'wrench') + text = format_msg(str(e), 'wrench') except ProcessLocked: - text = format_msg(gettext('Process locked by another process'), 'time') + text = format_msg( + gettext('Process locked by another process'), + 'time' + ) else: text = format_msg(msg='ok', icon='ok') return { diff --git a/dialing_app/templates/index.html b/dialing_app/templates/index.html index 1a12cb7..1bccf3a 100644 --- a/dialing_app/templates/index.html +++ b/dialing_app/templates/index.html @@ -60,7 +60,7 @@ - + diff --git a/dialing_app/templatetags/telephone_filters.py b/dialing_app/templatetags/telephone_filters.py index 29cf37c..64a6fa6 100644 --- a/dialing_app/templatetags/telephone_filters.py +++ b/dialing_app/templatetags/telephone_filters.py @@ -14,7 +14,7 @@ def abon_if_telephone(value): if value[0] != '+': value = '+' + value url = resolve_url('dialapp:to_abon', tel=value) - a = '%s' % (url, value) + a = '%s' % (url, value) return a else: return value diff --git a/djing/fields.py b/djing/fields.py index b8effee..58bbd10 100644 --- a/djing/fields.py +++ b/djing/fields.py @@ -111,6 +111,7 @@ except ImportError: pass +# DEPRECATED: remove after clean old migrations class MyGenericIPAddressField(models.GenericIPAddressField): description = "Int32 notation ip address" diff --git a/djing/lib/decorators.py b/djing/lib/decorators.py index cbdf61f..9895929 100644 --- a/djing/lib/decorators.py +++ b/djing/lib/decorators.py @@ -1,29 +1,11 @@ from functools import wraps from django.conf import settings -from django.http import HttpResponseRedirect, HttpResponseForbidden, JsonResponse +from django.http import HttpResponseForbidden, JsonResponse from django.shortcuts import redirect from djing.lib import check_sign -def require_ssl(view): - """ - Decorator that requires an SSL connection. If the current connection is not SSL, we redirect to the SSL version of - the page. - from: https://gist.github.com/ckinsey/9709984 - """ - - @wraps(view) - def wrapper(request, *args, **kwargs): - debug = getattr(settings, 'DEBUG', False) - if not debug and not request.is_secure(): - target_url = "https://%s%s" % (request.META['HTTP_HOST'], request.path_info) - return HttpResponseRedirect(target_url) - return view(request, *args, **kwargs) - - return wrapper - - # Allow to view only admins def only_admins(fn): @wraps(fn) @@ -99,6 +81,8 @@ def json_view(fn): @wraps(fn) def wrapped(request, *args, **kwargs): r = fn(request, *args, **kwargs) + if isinstance(r, dict) and not isinstance(r.get('text'), str): + r['text'] = str(r.get('text')) return JsonResponse(r, safe=False, json_dumps_params={ 'ensure_ascii': False }) diff --git a/djing/lib/mixins.py b/djing/lib/mixins.py index c4326bf..23cc635 100644 --- a/djing/lib/mixins.py +++ b/djing/lib/mixins.py @@ -2,6 +2,14 @@ from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin from guardian.mixins import PermissionRequiredMixin +class OnlySuperUserMixin(AccessMixin): + """Verify that the current user is superuser.""" + def dispatch(self, request, *args, **kwargs): + if not request.user.is_superuser: + return self.handle_no_permission() + return super().dispatch(request, *args, **kwargs) + + class OnlyAdminsMixin(AccessMixin): """Verify that the current user is admin.""" def dispatch(self, request, *args, **kwargs): diff --git a/djing/lib/tln/__init__.py b/djing/lib/tln/__init__.py deleted file mode 100644 index afd3a90..0000000 --- a/djing/lib/tln/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .tln import * - -__all__ = ('TelnetApi', 'ValidationError', 'ZTEFiberIsFull', 'ZteOltLoginFailed', - 'OnuZteRegisterError', 'ZteOltConsoleError', 'register_onu_ZTE_F660') diff --git a/djing/lib/tln/tln.py b/djing/lib/tln/tln.py deleted file mode 100755 index 5d4d347..0000000 --- a/djing/lib/tln/tln.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python3 -import re -import struct -from telnetlib import Telnet -from time import sleep -from typing import Generator, Dict, Optional, Tuple - -from djing.lib import process_lock - - -class ZteOltConsoleError(Exception): - pass - - -class OnuZteRegisterError(ZteOltConsoleError): - pass - - -class ZTEFiberIsFull(ZteOltConsoleError): - pass - - -class ZteOltLoginFailed(ZteOltConsoleError): - pass - - -class ValidationError(ValueError): - pass - - -MAC_ADDR_REGEX = b'^([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]{1,2})$' -IP_ADDR_REGEX = ( - '^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' - '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' - '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' - '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' -) -ONU_SN_REGEX = b'^ZTEG[A-F\d]{8}$' - - -class TelnetApi(Telnet): - - def __init__(self, prompt_string: bytes, *args, **kwargs): - timeout = kwargs.get('timeout') - if timeout: - self._timeout = timeout - self._prompt_string = prompt_string or b'ZTE#' - self.config_level = [] - super().__init__(*args, **kwargs) - - def write(self, buffer: bytes) -> None: - buffer = buffer + b'\n' - print('>>', buffer) - super().write(buffer) - - def resize_screen(self, width: int, height: int): - naws_cmd = struct.pack('>BBBHHBB', - 255, 250, 31, # IAC SB NAWS - width, height, - 255, 240 # IAC SE - ) - sock = self.get_socket() - sock.send(naws_cmd) - - def read_lines(self) -> Generator: - while True: - line = self.read_until(b'\r\n', timeout=self._timeout) - line = line.replace(b'\r\n', b'') - if self._prompt_string == line: - break - if line == b'': - continue - yield line - - def command_to(self, cmd: bytes) -> Generator: - self.write(cmd) - return self.read_lines() - - def set_prompt_string(self, prompt_string: bytes) -> None: - self.config_level.append(prompt_string) - self._prompt_string = prompt_string - - def level_exit(self) -> Optional[Tuple]: - if len(self.config_level) < 2: - print('We are in root') - return - self.config_level.pop() - self.set_prompt_string(self.config_level[-1]) - return tuple(self.command_to(b'exit')) - - def __del__(self): - if self.sock: - self.write(b'exit') - super().__del__() - - -def parse_onu_name(onu_name: bytes, name_regexp=re.compile(b'[/:_]')) -> Dict[str, bytes]: - gpon_onu, stack_num, rack_num, fiber_num, onu_num = name_regexp.split(onu_name) - return { - 'stack_num': stack_num, - 'rack_num': rack_num, - 'fiber_num': fiber_num, - 'onu_num': onu_num - } - - -class OltZTERegister(TelnetApi): - - def __init__(self, screen_size: Tuple[int, int], prompt_title: bytes, *args, **kwargs): - super().__init__(prompt_string=prompt_title, *args, **kwargs) - self.prompt_title = prompt_title - self.set_prompt_string(b'%s#' % prompt_title) - self.resize_screen(*screen_size) - - def enter(self, username: bytes, passw: bytes) -> None: - self.read_until(b'Username:') - self.write(username) - self.read_until(b'Password:') - self.write(passw) - for l in self.read_lines(): - if b'bad password' in l: - raise ZteOltLoginFailed - - def get_unregistered_onu(self, sn: bytes) -> Optional[Dict]: - lines = tuple(self.command_to(b'show gpon onu uncfg')) - if len(lines) > 3: - # devices available - # find onu by sn - line = tuple(ln for ln in lines if sn.lower() in ln.lower()) - if len(line) > 0: - line = line[0] - onu_name, onu_sn, onu_state = line.split() - onu_numbers = parse_onu_name(onu_name) - onu_numbers.update({ - 'onu_name': onu_name, - 'onu_sn': onu_sn, - 'onu_state': onu_state - }) - return onu_numbers - - def get_last_registered_onu_number(self, stack_num: int, rack_num: int, fiber_num: int) -> int: - registered_lines = self.command_to(b'show run int gpon-olt_%d/%d/%d' % ( - stack_num, - rack_num, - fiber_num - )) - onu_type_regexp = re.compile(b'^\s{2}onu \d{1,3} type [-\w\d]{4,64} sn \w{4,64}$') - last_onu = 0 - for rl in registered_lines: - if rl == b' --More--': - self.write(b' ') - if onu_type_regexp.match(rl): - _onu, num, _type, onu_type, _sn, onu_sn = rl.split() - last_onu = int(num) - return last_onu - - def enter_to_config_mode(self) -> bool: - prompt = b'%s(config)#' % self.prompt_title - self.set_prompt_string(prompt) - res = tuple(self.command_to(b'config terminal')) - if res[1].startswith(b'Enter configuration commands'): - # ok, we in the config mode - return True - return False - - def go_to_olt_interface(self, stack_num: int, rack_num: int, fiber_num: int) -> Tuple: - self.set_prompt_string(b'%s(config-if)#' % self.prompt_title) - return tuple(self.command_to(b'interface gpon-olt_%d/%d/%d' % ( - stack_num, - rack_num, - fiber_num - ))) - - def go_to_onu_interface(self, stack_num: int, rack_num: int, fiber_num: int, onu_port_num: int) -> Tuple: - self.set_prompt_string(b'%s(config-if)#' % self.prompt_title) - return tuple(self.command_to(b'interface gpon-onu_%d/%d/%d:%d' % ( - stack_num, - rack_num, - fiber_num, - onu_port_num - ))) - - def apply_conf_to_onu(self, mac_addr: bytes, vlan_id: int) -> None: - tmpl = ( - b'switchport vlan %d tag vport 1' % vlan_id, - b'port-location format flexible-syntax vport 1', - b'port-location sub-option remote-id enable vport 1', - b'port-location sub-option remote-id name %s vport 1' % mac_addr, - b'dhcp-option82 enable vport 1', - b'dhcp-option82 trust true replace vport 1', - b'ip dhcp snooping enable vport 1' - ) - for conf_line in tmpl: - self.write(conf_line) - - def register_onu_on_olt_fiber(self, onu_type: bytes, new_onu_num: int, onu_sn: bytes, line_profile: bytes, - remote_profile: bytes) -> Tuple: - # ok, we in interface - tpl = b'onu %d type %s sn %s' % (new_onu_num, onu_type, onu_sn) - r = tuple(self.command_to(tpl)) - return tuple(self.command_to(b'onu %d profile line %s remote %s' % ( - new_onu_num, - line_profile, - remote_profile - ))) + r - - -@process_lock -def register_onu_ZTE_F660(olt_ip: str, onu_sn: bytes, login_passwd: Tuple[bytes, bytes], onu_mac: bytes, prompt_title: bytes, vlan_id: int) -> Tuple: - onu_type = b'ZTE-F660' - line_profile = b'ZTE-F660-LINE' - remote_profile = b'ZTE-F660-ROUTER' - if not re.match(MAC_ADDR_REGEX, onu_mac): - raise ValidationError - if not re.match(IP_ADDR_REGEX, olt_ip): - raise ValidationError - if not re.match(ONU_SN_REGEX, onu_sn): - raise ValidationError - - tn = OltZTERegister(host=olt_ip, timeout=2, screen_size=(120, 128), prompt_title=prompt_title) - tn.enter(*login_passwd) - - unregistered_onu = tn.get_unregistered_onu(onu_sn) - if unregistered_onu is None: - raise OnuZteRegisterError('unregistered onu not found, sn=%s' % onu_sn.decode('utf-8')) - - stack_num = int(unregistered_onu['stack_num']) - rack_num = int(unregistered_onu['rack_num']) - fiber_num = int(unregistered_onu['fiber_num']) - - last_onu_number = tn.get_last_registered_onu_number( - stack_num, rack_num, fiber_num - ) - - if last_onu_number > 126: - raise ZTEFiberIsFull('olt fiber %d is full' % fiber_num) - - # enter to config - if not tn.enter_to_config_mode(): - raise ZteOltConsoleError('Failed to enter to config mode') - - # go to olt interface - if not tn.go_to_olt_interface(stack_num, rack_num, fiber_num): - raise ZteOltConsoleError('Failed to enter in olt fiber port') - - # new onu port number - new_onu_port_num = last_onu_number + 1 - - # register onu on olt interface - r = tn.register_onu_on_olt_fiber(onu_type, new_onu_port_num, onu_sn, line_profile, remote_profile) - print(r) - - # exit from olt interface - tn.level_exit() - - r = tn.go_to_onu_interface(stack_num, rack_num, fiber_num, new_onu_port_num) - print(r) - - tn.apply_conf_to_onu(onu_mac, vlan_id) - sleep(1) - return stack_num, rack_num, fiber_num, new_onu_port_num - - -if __name__ == '__main__': - ip = '192.168.0.100' - try: - register_onu_ZTE_F660( - olt_ip=ip, onu_sn=b'ZTEG^#*$&@&', login_passwd=(b'login', b'password'), - onu_mac=b'MAC' - ) - except ZteOltConsoleError as e: - print(e) - except ConnectionRefusedError: - print('ERROR: connection refused', ip) diff --git a/djing/local_settings.py.example b/djing/local_settings.py.example index 26330b4..45d66fc 100644 --- a/djing/local_settings.py.example +++ b/djing/local_settings.py.example @@ -66,6 +66,9 @@ EMAIL_PORT = 587 EMAIL_HOST_PASSWORD = 'password' EMAIL_USE_TLS = True +# public url for Viber Bot +VIBER_BOT_PUBLIC_URL = 'https://your_domain.name' + # Encrypted fields # https://pypi.org/project/django-encrypted-model-fields/ # You must change this value diff --git a/djing/settings.py b/djing/settings.py index 16580aa..3e008d2 100644 --- a/djing/settings.py +++ b/djing/settings.py @@ -42,6 +42,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'encrypted_model_fields', + 'django_cleanup.apps.CleanupConfig', 'ip_pool', 'accounts_app', 'gw_app', @@ -50,10 +51,11 @@ INSTALLED_APPS = [ 'searchapp', 'devapp', 'mapapp', + 'traf_stat', 'finapp', 'taskapp', 'clientsideapp', - 'chatbot', + 'messenger', 'msg_app', 'dialing_app', 'group_app', @@ -182,7 +184,7 @@ DEFAULT_PICTURE = '/static/img/user_ava.gif' AUTH_USER_MODEL = 'accounts_app.UserProfile' LOGIN_URL = reverse_lazy('acc_app:login') -LOGIN_REDIRECT_URL = reverse_lazy('acc_app:profile') +LOGIN_REDIRECT_URL = reverse_lazy('acc_app:setup_info') LOGOUT_URL = reverse_lazy('acc_app:logout') PAGINATION_ITEMS_PER_PAGE = local_settings.PAGINATION_ITEMS_PER_PAGE @@ -234,6 +236,10 @@ BROKER_TRANSPORT_OPTIONS = {'visibility_timeout': 3600} CELERY_RESULT_BACKEND = 'redis://' + REDIS_HOST + ':' + REDIS_PORT + '/0' +# public url for Viber Bot +VIBER_BOT_PUBLIC_URL = local_settings.VIBER_BOT_PUBLIC_URL + + # Encrypted fields # https://pypi.org/project/django-encrypted-model-fields/ FIELD_ENCRYPTION_KEY = getattr( diff --git a/djing/tasks.py b/djing/tasks.py index 6f5a9e3..9188574 100644 --- a/djing/tasks.py +++ b/djing/tasks.py @@ -37,6 +37,8 @@ def send_email_notify(msg_text: str, account_id: int): def multicast_email_notify(msg_text: str, account_ids: Iterable): text_content = strip_tags(msg_text) for acc_id in account_ids: + if not acc_id: + continue try: account = UserProfile.objects.get(pk=acc_id) target_email = account.email diff --git a/djing/urls.py b/djing/urls.py index 54bf7b8..31f7dfa 100644 --- a/djing/urls.py +++ b/djing/urls.py @@ -11,13 +11,14 @@ urlpatterns = [ path('search/', include('searchapp.urls', namespace='searchapp')), path('dev/', include('devapp.urls', namespace='devapp')), path('map/', include('mapapp.urls', namespace='mapapp')), - # path('statistic/', include('statistics.urls', namespace='statistics')), + path('statistic/', include('traf_stat.urls', namespace='traf_stat')), path('tasks/', include('taskapp.urls', namespace='taskapp')), path('client/', include('clientsideapp.urls', namespace='client_side')), path('msg/', include('msg_app.urls', namespace='msg_app')), path('dialing/', include('dialing_app.urls', namespace='dialapp')), path('groups/', include('group_app.urls', namespace='group_app')), path('ip_pool/', include('ip_pool.urls', namespace='ip_pool')), + path('messenger/', include('messenger.urls', namespace='messenger')), path('gw/', include('gw_app.urls', namespace='gw_app')), path('fin/', include('finapp.urls', namespace='finapp')) diff --git a/djing/views.py b/djing/views.py index aedcbb5..1a9af95 100644 --- a/djing/views.py +++ b/djing/views.py @@ -5,6 +5,6 @@ from django.shortcuts import redirect @login_required def home(request): if request.user.is_staff: - return redirect('acc_app:profile') + return redirect('acc_app:setup_info') else: return redirect('client_side:home') diff --git a/docs/extra_func.md b/docs/extra_func.md index fb253b7..190790d 100644 --- a/docs/extra_func.md +++ b/docs/extra_func.md @@ -3,13 +3,10 @@ Его совсем не много, но без внимания оставить нельзя. Все вспомогательные модули можно найти в пакете **djing.lib**. -### tln -Это модуль работы по *telnet* ### messaging Этот модуль помогает работать с форматами СМС сообщений. - ### init Содержит всякие мелкие примочки, код прост и с комментариями, зайдите посмотрите. diff --git a/group_app/views.py b/group_app/views.py index e8da0e4..b8e2520 100644 --- a/group_app/views.py +++ b/group_app/views.py @@ -9,6 +9,7 @@ from django.contrib import messages from django.conf import settings from djing.lib.decorators import only_admins from guardian.decorators import permission_required_or_403 as permission_required +from guardian.shortcuts import get_objects_for_user from djing.global_base_views import OrderedFilteredList from . import models @@ -27,6 +28,12 @@ class GroupListView(OrderedFilteredList): model = models.Group context_object_name = 'groups' + def get_queryset(self): + queryset = get_objects_for_user(self.request.user, + 'group_app.view_group', klass=self.model, + accept_global_perms=False) + return queryset + @method_decorator(login_decs, name='dispatch') @method_decorator(permission_required('group_app.change_group'), name='dispatch') diff --git a/gw_app/nas_managers/core.py b/gw_app/nas_managers/core.py index d50e9d2..9eb08f2 100644 --- a/gw_app/nas_managers/core.py +++ b/gw_app/nas_managers/core.py @@ -1,4 +1,4 @@ -from abc import ABC, abstractmethod, abstractproperty +from abc import ABC, abstractmethod from typing import Iterator, Tuple, Optional from djing import ping from gw_app.nas_managers.structs import SubnetQueue, VectorQueue @@ -16,7 +16,8 @@ class NasNetworkError(Exception): # Communicate with gw class BaseTransmitter(ABC): - @abstractproperty + @property + @abstractmethod def description(self): """ :return: Returnd a description of nas implementation diff --git a/gw_app/nas_managers/mod_mikrotik.py b/gw_app/nas_managers/mod_mikrotik.py index f702544..7bd102c 100644 --- a/gw_app/nas_managers/mod_mikrotik.py +++ b/gw_app/nas_managers/mod_mikrotik.py @@ -272,8 +272,9 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos, '=target=%s' % queue.network, '=max-limit=%.3fM/%.3fM' % queue.max_limit, '=queue=Djing_pcq_up/Djing_pcq_down', - '=burst-time=1/5', - #'=total-queue=Djing_pcq_down' + '=burst-time=5/5', + '=burst-limit=%.3fM/%.3fM' % tuple(i * 2 for i in queue.max_limit), + '=burst-threshold=%.3fM/%.3fM' % tuple(i / 1.2 for i in queue.max_limit) )) def remove_queue(self, queue: i_structs.SubnetQueue) -> None: @@ -300,7 +301,7 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos, if queue_gw is None: return self.add_queue(queue) else: - cmd = [ + cmd = ( '/queue/simple/set', '=name=%s' % queue.name, '=max-limit=%.3fM/%.3fM' % queue.max_limit, @@ -308,10 +309,11 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos, # или =target-addresses или =target '=target=%s' % queue.network, '=queue=Djing_pcq_up/Djing_pcq_down', - '=burst-time=1/1' - ] - if queue.queue_id: - cmd.insert(1, '=.id=%s' % queue.queue_id) + '=burst-time=5/5', + '=burst-limit=%.3fM/%.3fM' % tuple(i * 2 for i in queue.max_limit), + '=burst-threshold=%.3fM/%.3fM' % tuple(i / 1.2 for i in queue.max_limit), + '=numbers=%s' % queue_gw.queue_id + ) r = self._exec_cmd(cmd) return r diff --git a/gw_app/nas_managers/structs.py b/gw_app/nas_managers/structs.py index 7cd8230..d880808 100644 --- a/gw_app/nas_managers/structs.py +++ b/gw_app/nas_managers/structs.py @@ -23,7 +23,7 @@ class SubnetQueue(BaseStruct): return self._max_limit def set_max_limit(self, v): - if isinstance(v, tuple): + if isinstance(v, (tuple, list)): self._max_limit = v elif isinstance(v, str): s_in, s_out = v.split('/') @@ -32,7 +32,7 @@ class SubnetQueue(BaseStruct): sp = float(v) self._max_limit = sp, sp else: - raise ValueError('Unexpected format for max_limit') + raise ValueError('Unexpected format for max_limit %s' % v) max_limit = property(get_max_limit, set_max_limit) diff --git a/ip_pool/migrations/0001_squashed_0004_auto_20190305_1243.py b/ip_pool/migrations/0001_squashed_0004_auto_20190305_1243.py new file mode 100644 index 0000000..cc71ae3 --- /dev/null +++ b/ip_pool/migrations/0001_squashed_0004_auto_20190305_1243.py @@ -0,0 +1,36 @@ +# Generated by Django 2.1.1 on 2019-03-05 12:47 + +from django.db import migrations, models +import ip_pool.fields + + +class Migration(migrations.Migration): + + replaces = [('ip_pool', '0001_initial'), ('ip_pool', '0002_change_unique'), ('ip_pool', '0003_auto_20181019_1230'), ('ip_pool', '0004_auto_20190305_1243')] + + initial = True + + dependencies = [ + ('group_app', '0002_group_code'), + ] + + operations = [ + migrations.CreateModel( + name='NetworkModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('network', ip_pool.fields.GenericIpAddressWithPrefix(help_text='Ip address of network. For example: 192.168.1.0 or fde8:6789:1234:1::', unique=True, verbose_name='IP network')), + ('kind', models.CharField(choices=[('inet', 'Internet'), ('guest', 'Guest'), ('trust', 'Trusted'), ('device', 'Devices'), ('admin', 'Admin')], default='guest', max_length=6, verbose_name='Kind of network')), + ('description', models.CharField(max_length=64, verbose_name='Description')), + ('ip_start', models.GenericIPAddressField(verbose_name='Start work ip range')), + ('ip_end', models.GenericIPAddressField(verbose_name='End work ip range')), + ('groups', models.ManyToManyField(to='group_app.Group', verbose_name='Groups')), + ], + options={ + 'verbose_name': 'Network', + 'verbose_name_plural': 'Networks', + 'db_table': 'ip_pool_network', + 'ordering': ('network',), + }, + ) + ] diff --git a/ip_pool/migrations/0004_auto_20190305_1243.py b/ip_pool/migrations/0004_auto_20190305_1243.py new file mode 100644 index 0000000..5514733 --- /dev/null +++ b/ip_pool/migrations/0004_auto_20190305_1243.py @@ -0,0 +1,15 @@ +# Generated by Django 2.1.1 on 2019-03-05 12:43 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ip_pool', '0003_auto_20181019_1230'), + ] + + operations = [ + migrations.DeleteModel(name='IpLeaseModel'), + migrations.DeleteModel(name='LeasesHistory') + ] diff --git a/ip_pool/models.py b/ip_pool/models.py index bc6f771..e5f9a10 100644 --- a/ip_pool/models.py +++ b/ip_pool/models.py @@ -160,6 +160,7 @@ class NetworkModel(models.Model): ordering = ('network',) +# Deprecated. Remove after migrations squashed class IpLeaseManager(models.Manager): def get_free_ip(self, network: NetworkModel): @@ -234,6 +235,7 @@ class IpLeaseModel(models.Model): unique_together = ('ip', 'network', 'mac_addr') +# Deprecated. Remove after migrations squashed class LeasesHistory(models.Model): ip = models.GenericIPAddressField(verbose_name=_('Ip address')) lease_time = models.DateTimeField(_('Lease time'), auto_now_add=True) diff --git a/locale/ru/LC_MESSAGES/django.po b/locale/ru/LC_MESSAGES/django.po index 54a1c4d..8942dc0 100644 --- a/locale/ru/LC_MESSAGES/django.po +++ b/locale/ru/LC_MESSAGES/django.po @@ -128,3 +128,6 @@ msgstr "Вы уверены в этом?" msgid "Finance" msgstr "Финансы" + +msgid "Traffic" +msgstr "Траффик" diff --git a/messenger/__init__.py b/messenger/__init__.py new file mode 100644 index 0000000..4407c25 --- /dev/null +++ b/messenger/__init__.py @@ -0,0 +1 @@ +default_app_config = 'messenger.apps.messengerConfig' \ No newline at end of file diff --git a/messenger/admin.py b/messenger/admin.py new file mode 100644 index 0000000..5e2afdc --- /dev/null +++ b/messenger/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from messenger import models + +admin.site.register(models.Messenger) +admin.site.register(models.ViberMessenger) +admin.site.register(models.ViberSubscriber) +admin.site.register(models.ViberMessage) diff --git a/messenger/apps.py b/messenger/apps.py new file mode 100644 index 0000000..786128b --- /dev/null +++ b/messenger/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class messengerConfig(AppConfig): + name = 'messenger' diff --git a/messenger/forms.py b/messenger/forms.py new file mode 100644 index 0000000..f85aabd --- /dev/null +++ b/messenger/forms.py @@ -0,0 +1,28 @@ +from django import forms +from messenger import models + + +class MessengerForm(forms.ModelForm): + class Meta: + model = models.Messenger + fields = ('bot_type',) + + +class MessengerViberForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + kwargs['initial']['bot_type'] = 1 + super().__init__(*args, **kwargs) + inst = getattr(self, 'instance') + if inst: + self.fields['bot_type'].disabled = True + #self.fields['bot_type'].widget.attrs['disabled'] = True + + class Meta: + model = models.ViberMessenger + fields = '__all__' + + +class MessengerViberMessageForm(forms.ModelForm): + class Meta: + model = models.ViberMessage + fields = '__all__' diff --git a/messenger/locale/ru/LC_MESSAGES/django.po b/messenger/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000..951a44d --- /dev/null +++ b/messenger/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,190 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Dmitry Novikov nerosketch@gmail.com, 2019. +# +#, fuzzy +msgid "" +msgstr "" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-02-06 13:45+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Dmitry Novikov nerosketch@gmail.com\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" + +#: models.py:16 templates/messenger/messenger_list.html:20 +msgid "Title" +msgstr "Название" + +#: models.py:18 +msgid "Viber" +msgstr "Вайбер" + +#: models.py:20 +msgid "Bot type" +msgstr "Тип бота" + +#: models.py:21 templates/messenger/messenger_list.html:22 +msgid "Slug" +msgstr "Ссыль" + +#: models.py:28 models.py:92 +msgid "messenger" +msgstr "Мэссенджер" + +#: models.py:29 templates/messenger/messenger_list.html:7 +#: templates/messenger/messenger_list.html:12 +#: templates/messenger/vibermessenger_form.html:8 +msgid "Messengers" +msgstr "Мэссенджэры" + +#: models.py:48 +msgid "Bot secret token" +msgstr "Секретный токен viber" + +#: models.py:49 models.py:108 +msgid "Avatar" +msgstr "Аватар" + +#: models.py:83 +msgid "Viber messenger" +msgstr "Viber мэссенджэр" + +#: models.py:84 +msgid "Viber messengers" +msgstr "Viber мэссенджэры" + +#: models.py:89 +msgid "Message" +msgstr "Сообщение" + +#: models.py:90 +msgid "Date" +msgstr "Дата" + +#: models.py:91 +msgid "Sender" +msgstr "Отправитель" + +#: models.py:93 +msgid "Subscriber" +msgstr "Подписчик" + +#: models.py:100 +msgid "Viber message" +msgstr "Сообщение viber" + +#: models.py:101 +msgid "Viber messages" +msgstr "Сообщения viber" + +#: models.py:106 +msgid "User unique id in viber" +msgstr "Уникальный id viber" + +#: models.py:107 +msgid "Name" +msgstr "Имя" + +#: models.py:109 +msgid "System account" +msgstr "Системная учётная запись" + +#: models.py:116 +msgid "Viber subscriber" +msgstr "Подписчик viber" + +#: models.py:117 +msgid "Viber subscribers" +msgstr "Подписчики viber" + +#: templates/messenger/add_messenger.html:5 +msgid "Select bot type" +msgstr "Выберите тип бота" + +#: templates/messenger/add_messenger.html:11 +msgid "Add" +msgstr "Добавить" + +#: templates/messenger/messenger_list.html:21 +msgid "Type" +msgstr "Тип" + +#: templates/messenger/messenger_list.html:33 +msgid "Edit" +msgstr "Изменить" + +#: templates/messenger/messenger_list.html:40 +msgid "messengers was not found" +msgstr "Мэссенджеры не найдены" + +#: templates/messenger/messenger_list.html:49 +msgid "New" +msgstr "Новый" + +#: templates/messenger/vibermessenger_form.html:10 +msgid "Update messenger" +msgstr "Обновить мэссенджэр" + +#: templates/messenger/vibermessenger_form.html:11 +msgid "Change viber" +msgstr "Изменить viber" + +#: templates/messenger/vibermessenger_form.html:13 +msgid "Add messenger" +msgstr "Добавить мэссенджэр" + +#: templates/messenger/vibermessenger_form.html:14 +msgid "Add viber" +msgstr "Добавить viber" + +#: templates/messenger/vibermessenger_form.html:25 +msgid "Change messenger" +msgstr "Изменить мэссенджэр" + +#: templates/messenger/vibermessenger_form.html:28 +msgid "Add new messenger" +msgstr "Добавить мэссенджэр" + +#: templates/messenger/vibermessenger_form.html:39 +msgid "Save" +msgstr "Сохранить" + +#: templates/messenger/vibermessenger_form.html:43 +msgid "Send webhook" +msgstr "Отправить webhook" + +#: views.py:38 +msgid "Unexpected bot type" +msgstr "Не известный тип бота" + +#: views.py:51 +msgid "New viber messenger successfully created" +msgstr "Новый viber мэссенджэр успешно создан" + +#: views.py:62 +msgid "Viber messenger successfully updated" +msgstr "viber мэссенджэр успешно обновлён" + +#: views.py:73 +msgid "Viber messenger successfully deleted" +msgstr "viber мэссенджэр успешно удалён" + +#: views.py:132 +msgid "My telephone number" +msgstr "Мой номер телефона" + +#: views.py:150 +msgid "" +"Telephone not found, please specify telephone number in account in billing" +msgstr "" +"Номер телефона не найден. Укажите свой номер телефона в учётке в биллинге" + +msgid "Your account is attached. Now you will be receive notifications from billing" +msgstr "Ваша учётка из биллинга привязана. Теперь вы будете получать оповещения из биллинга." diff --git a/messenger/migrations/0001_initial.py b/messenger/migrations/0001_initial.py new file mode 100644 index 0000000..39dcd17 --- /dev/null +++ b/messenger/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 2.1.3 on 2019-02-07 12:31 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Messenger', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=64, verbose_name='Title')), + ('bot_type', models.PositiveSmallIntegerField(blank=True, choices=[(1, 'Viber')], verbose_name='Bot type')), + ('slug', models.SlugField(verbose_name='Slug')), + ], + options={ + 'verbose_name': 'messenger', + 'verbose_name_plural': 'Messengers', + 'db_table': 'messengers', + 'ordering': ('title',), + }, + ), + migrations.CreateModel( + name='ViberMessage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('msg', models.TextField(verbose_name='Message')), + ('date', models.DateTimeField(auto_now_add=True, verbose_name='Date')), + ('sender', models.CharField(max_length=32, verbose_name='Sender')), + ], + options={ + 'verbose_name': 'Viber message', + 'verbose_name_plural': 'Viber messages', + 'db_table': 'viber_messages_notifications', + 'ordering': ('-date',), + }, + ), + migrations.CreateModel( + name='ViberSubscriber', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uid', models.CharField(max_length=32, verbose_name='User unique id in viber')), + ('name', models.CharField(blank=True, max_length=32, null=True, verbose_name='Name')), + ('avatar', models.URLField(blank=True, max_length=250, null=True, verbose_name='Avatar')), + ('account', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='System account')), + ], + options={ + 'verbose_name': 'Viber subscriber', + 'verbose_name_plural': 'Viber subscribers', + 'db_table': 'viber_subscriber', + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='ViberMessenger', + fields=[ + ('messenger_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='messenger.Messenger')), + ('token', models.CharField(max_length=64, verbose_name='Bot secret token')), + ('avatar', models.ImageField(null=True, upload_to='viber_avatar', verbose_name='Avatar')), + ], + options={ + 'verbose_name': 'Viber messenger', + 'verbose_name_plural': 'Viber messengers', + 'db_table': 'viber_messenger_notifications', + 'ordering': ('title',), + }, + bases=('messenger.messenger',), + ), + migrations.AddField( + model_name='vibermessage', + name='subscriber', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='messenger.ViberSubscriber', verbose_name='Subscriber'), + ), + migrations.AddField( + model_name='vibermessage', + name='messenger', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='messenger.ViberMessenger', verbose_name='messenger'), + ), + ] diff --git a/chatbot/__init__.py b/messenger/migrations/__init__.py similarity index 100% rename from chatbot/__init__.py rename to messenger/migrations/__init__.py diff --git a/messenger/models.py b/messenger/models.py new file mode 100644 index 0000000..685aad2 --- /dev/null +++ b/messenger/models.py @@ -0,0 +1,128 @@ +from urllib.parse import urljoin + +from django.conf import settings +from django.shortcuts import resolve_url +from django.utils.translation import gettext_lazy as _ +from django.db import models +from viberbot import Api, BotConfiguration +from viberbot.api.messages import TextMessage +from viberbot.api.messages.message import Message + +from accounts_app.models import UserProfile + + +class Messenger(models.Model): + title = models.CharField(_('Title'), max_length=64) + CHAT_TYPES = ( + (1, _('Viber')), + ) + bot_type = models.PositiveSmallIntegerField(_('Bot type'), choices=CHAT_TYPES, blank=True) + slug = models.SlugField(_('Slug')) + + def __str__(self): + return self.title + + class Meta: + db_table = 'messengers' + verbose_name = _('messenger') + verbose_name_plural = _('Messengers') + ordering = ('title',) + + def get_absolute_url(self): + if self.bot_type == 1: + return resolve_url('messenger:update_viber_messenger', self.slug) + + def get_next_url(self): + if self.bot_type == 1: # Viber + return resolve_url('messenger:update_viber_messenger', self.slug) + else: + return resolve_url('messenger:messengers_list') + + +class ViberMessenger(Messenger): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._viber_cache = None + + token = models.CharField(_('Bot secret token'), max_length=64) + avatar = models.ImageField(_('Avatar'), upload_to='viber_avatar', null=True) + + def get_viber(self): + if self._viber_cache is None: + self._viber_cache = Api(BotConfiguration( + name=str(self.slug), + avatar=self.avatar.url, + auth_token=str(self.token) + )) + return self._viber_cache + + def send_message(self, to: UserProfile, msg): + try: + viber = self.get_viber() + vs = to.vibersubscriber + if issubclass(msg.__class__, Message): + viber.send_messages(str(vs.uid), msg) + else: + viber.send_messages(str(vs.uid), TextMessage(text=msg)) + except ViberSubscriber.DoesNotExist: + pass + + def send_messages(self, receivers, msg_text: str): + """ + :param receivers: QuerySet of accounts_app.UserProfile + :param msg_text: text message + :return: nothing + """ + viber = self.get_viber() + msg = TextMessage(text=msg_text) + for vs in ViberSubscriber.objects.filter(account__in=receivers).iterator(): + viber.send_messages(str(vs.uid), msg) + + def send_webhook(self): + pub_url = getattr(settings, 'VIBER_BOT_PUBLIC_URL') + listen_url = resolve_url('messenger:listen_viber_bot', self.slug) + public_url = urljoin(pub_url, listen_url) + viber = self.get_viber() + viber.set_webhook(public_url, ['failed', 'subscribed', 'unsubscribed', 'conversation_started']) + + def __str__(self): + return self.title + + class Meta: + db_table = 'viber_messenger_notifications' + verbose_name = _('Viber messenger') + verbose_name_plural = _('Viber messengers') + ordering = ('title',) + + +class ViberMessage(models.Model): + msg = models.TextField(_('Message')) + date = models.DateTimeField(_('Date'), auto_now_add=True) + sender = models.CharField(_('Sender'), max_length=32) + messenger = models.ForeignKey(ViberMessenger, verbose_name=_('messenger'), on_delete=models.CASCADE) + subscriber = models.ForeignKey('ViberSubscriber', on_delete=models.SET_NULL, verbose_name=_('Subscriber'), null=True) + + def __str__(self): + return self.msg + + class Meta: + db_table = 'viber_messages_notifications' + verbose_name = _('Viber message') + verbose_name_plural = _('Viber messages') + ordering = ('-date',) + + +class ViberSubscriber(models.Model): + uid = models.CharField(_('User unique id in viber'), max_length=32) + name = models.CharField(_('Name'), max_length=32, null=True, blank=True) + avatar = models.URLField(_('Avatar'), max_length=250, null=True, blank=True) + account = models.OneToOneField(UserProfile, on_delete=models.CASCADE, verbose_name=_('System account'), blank=True, null=True) + + def __str__(self): + return self.name or 'no' + + class Meta: + db_table = 'viber_subscriber' + verbose_name = _('Viber subscriber') + verbose_name_plural = _('Viber subscribers') + ordering = ('name',) diff --git a/messenger/tasks.py b/messenger/tasks.py new file mode 100644 index 0000000..39f103f --- /dev/null +++ b/messenger/tasks.py @@ -0,0 +1,56 @@ +from typing import Optional, Iterable + +from celery import shared_task + +from accounts_app.models import UserProfile +from messenger.models import ViberMessenger + + +@shared_task +def send_viber_message(messenger_id: Optional[int], account_id: int, message_text: str) -> Optional[str]: + """ + Send text message via viber + :param messenger_id: Primary key UID for messanger.ViberMessenger + :param account_id: User id from accounts_app.UserProfile + :param message_text: + :return: Optional text for log + """ + if not message_text: + return 'ERROR: empty message text' + try: + sp = UserProfile.objects.get(pk=account_id) + if messenger_id is None: + for vm in ViberMessenger.objects.all().iterator(): + vm.send_message(sp, message_text) + else: + vm = ViberMessenger.objects.get(pk=messenger_id) + vm.send_message(sp, message_text) + except ViberMessenger.DoesNotExist: + return 'ERROR: Viber messanger with id=%d not found' % messenger_id + except UserProfile.DoesNotExist: + return 'ERROR: accounts_app.UserProfile with pk=%d does not exist' % account_id + + +@shared_task +def multicast_viber_notify(messenger_id: Optional[int], account_id_list: Iterable[int], message_text: str): + """ + Send multiple message via Viber to several addresses + :param messenger_id: Primary key UID for messanger.ViberMessenger + :param account_id_list: list of account ids from accounts_app.UserProfile + :param message_text: + :return: Optional text for log + """ + if not message_text: + return 'ERROR: empty message text' + account_id_list = tuple(account_id_list) + recipients = UserProfile.objects.filter(pk__in=account_id_list) + if not recipients.exists(): + return 'No recipients found from ids: %s' % ','.join(str(i) for i in account_id_list) + if messenger_id is None: + for vm in ViberMessenger.objects.all().iterator(): + vm.send_messages(recipients, message_text) + else: + vm = ViberMessenger.objects.filter(pk=messenger_id).first() + if vm is None: + return 'ERROR ViberMessenger with pk=%d does not exist' % messenger_id + vm.send_messages(recipients, message_text) diff --git a/messenger/templates/messenger/add_messenger.html b/messenger/templates/messenger/add_messenger.html new file mode 100644 index 0000000..b90e83d --- /dev/null +++ b/messenger/templates/messenger/add_messenger.html @@ -0,0 +1,15 @@ +{% load i18n bootstrap3 %} +{% csrf_token %} + + + + + diff --git a/messenger/templates/messenger/messenger_list.html b/messenger/templates/messenger/messenger_list.html new file mode 100644 index 0000000..1495ae4 --- /dev/null +++ b/messenger/templates/messenger/messenger_list.html @@ -0,0 +1,57 @@ +{% extends 'base.html' %} +{% load dpagination i18n %} + +{% block breadcrumb %} + +{% endblock %} + +{% block page-header %} + {% trans 'Messengers' %} +{% endblock %} + +{% block main %} +
    +
    ## {% trans 'Sub' %} @@ -64,11 +64,11 @@ {% if order_by == 'ballance' %}{% endif %} #Ping
    - -{# {% if human.statcache.is_online %}#} -{# #} -{# {% else %}#} -{# #} -{# {% endif %}#} + {% if human.statcache.is_online %} + + {% else %} + + {% endif %} {{ human.username }} {{ human.ip_address|default_if_none:'—' }}{{ human.fio|default:'—' }}{{ human.fio|default:'—' }} {{ human.street|default:_('Not assigned') }} {{ human.house|default:'—' }} {{ human.telephone }} - {% if can_del_abon %} - - + + {% if perms.abonapp.can_ping %} + + {% endif %}
    + {% trans 'Subscribers not found' %}. {% if perms.abonapp.add_abon %} {% trans 'Add abon' %} diff --git a/abonapp/templates/abonapp/service.html b/abonapp/templates/abonapp/service.html index dd1326c..d04019e 100644 --- a/abonapp/templates/abonapp/service.html +++ b/abonapp/templates/abonapp/service.html @@ -117,7 +117,7 @@
    {% trans 'Auto continue service.' %}
    - ? + ?
    diff --git a/abonapp/views.py b/abonapp/views.py index 379336d..d92a956 100644 --- a/abonapp/views.py +++ b/abonapp/views.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import Dict, Optional +from abonapp.tasks import customer_nas_command, customer_nas_remove from agent.commands.dhcp import dhcp_commit, dhcp_expiry, dhcp_release from devapp.models import Device, Port as DevPort from dialing_app.models import AsteriskCDR @@ -51,11 +52,11 @@ class PeoplesListView(LoginRequiredMixin, OnlyAdminsMixin, if street_id > 0: peoples_list = peoples_list.filter(street=street_id) peoples_list = peoples_list.select_related( - 'group', 'street', 'current_tariff' + 'group', 'street', 'current_tariff__tariff', 'statcache' ).only( - 'group', 'street', 'fio', + 'group', 'street', 'fio', 'birth_day', 'street', 'house', 'telephone', 'ballance', 'markers', - 'username', 'is_active', 'current_tariff' + 'username', 'is_active', 'current_tariff', 'ip_address' ) ordering = self.get_ordering() if ordering and isinstance(ordering, str): @@ -84,16 +85,15 @@ class PeoplesListView(LoginRequiredMixin, OnlyAdminsMixin, class GroupListView(LoginRequiredMixin, OnlyAdminsMixin, OrderedFilteredList): context_object_name = 'groups' template_name = 'abonapp/group_list.html' - queryset = Group.objects.annotate(usercount=Count('abon')) def get_queryset(self): - queryset = super(GroupListView, self).get_queryset() queryset = get_objects_for_user( self.request.user, - 'group_app.view_group', klass=queryset, + 'group_app.view_group', klass=Group, + use_groups=False, accept_global_perms=False ) - return queryset + return queryset.annotate(usercount=Count('abon')) class AbonCreateView(LoginRequiredMixin, OnlyAdminsMixin, @@ -175,6 +175,13 @@ class DelAbonDeleteView(LoginAdminMixin, PermissionRequiredMixin, DeleteView): try: abon = self.get_object() gid = abon.group.id + if abon.current_tariff: + abon_tariff = abon.current_tariff.tariff + customer_nas_remove.delay( + customer_uid=abon.pk, ip_addr=abon.ip_address, + speed=(abon_tariff.speedIn, abon_tariff.speedOut), + is_access=abon.is_access(), nas_pk=abon.nas_id + ) abon.delete() request.user.log(request.META, 'dusr', ( '%(uname)s, "%(fio)s", %(group)s %(street)s %(house)s' % { @@ -320,9 +327,7 @@ class AbonHomeUpdateView(LoginAdminMixin, PermissionRequiredMixin, UpdateView): def form_valid(self, form): r = super(AbonHomeUpdateView, self).form_valid(form) abon = self.object - res = abon.nas_sync_self() - if isinstance(res, Exception): - messages.warning(self.request, res) + customer_nas_command.delay(abon.pk, 'sync') messages.success(self.request, _('edit abon success msg')) return r @@ -428,11 +433,8 @@ def pick_tariff(request, gid: int, uname): comment=log_comment) else: abon.pick_tariff(trf, request.user, comment=log_comment) - r = abon.nas_sync_self() - if r is None: - messages.success(request, _('Tariff has been picked')) - else: - messages.error(request, r) + customer_nas_command.delay(abon.pk, 'sync') + messages.success(request, _('Tariff has been picked')) return redirect('abonapp:abon_services', gid=gid, uname=abon.username) except (lib.LogicError, NasFailedResult) as e: @@ -467,6 +469,13 @@ def unsubscribe_service(request, gid: int, uname, abon_tariff_id: int): try: abon_tariff = get_object_or_404(models.AbonTariff, pk=int(abon_tariff_id)) + abon = abon_tariff.abon + trf = abon_tariff.tariff + customer_nas_remove.delay( + customer_uid=abon.pk, ip_addr=abon.ip_address, + speed=(trf.speedIn, trf.speedOut), + is_access=abon.is_access(), nas_pk=abon.nas_id + ) abon_tariff.delete() messages.success(request, _('User has been detached from service')) except NasFailedResult as e: @@ -594,9 +603,7 @@ class IpUpdateView(LoginAdminPermissionMixin, UpdateView): def form_valid(self, form): r = super(IpUpdateView, self).form_valid(form) abon = self.object - res = abon.nas_sync_self() - if isinstance(res, Exception): - messages.warning(self.request, res) + customer_nas_command.delay(abon.pk, 'sync') messages.success(self.request, _('Ip successfully updated')) return r @@ -1214,6 +1221,7 @@ def user_session_free(request, gid: int, uname): return redirect('abonapp:abon_home', gid, uname) if abon.ip_address: abon.free_ip_addr() + customer_nas_command.delay(abon.pk, 'remove') messages.success(request, _('Ip lease has been freed')) else: messages.error(request, _('User not have ip')) @@ -1228,9 +1236,9 @@ def attach_nas(request, gid): gateway_id = lib.safe_int(request.POST.get('gateway')) if gateway_id: nas = get_object_or_404(NASModel, pk=gateway_id) - abons = models.Abon.objects.filter(group__id=gid) - if abons.exists(): - abons.update(nas=nas) + customers = models.Abon.objects.filter(group__id=gid) + if customers.exists(): + customers.update(nas=nas) messages.success( request, _('Network access server for users in this ' diff --git a/accounts_app/forms.py b/accounts_app/forms.py index cc5dfba..84ffb6b 100644 --- a/accounts_app/forms.py +++ b/accounts_app/forms.py @@ -46,10 +46,11 @@ class UserPermissionsForm(forms.ModelForm): class Meta: model = UserProfile - fields = ('avatar', 'password', 'groups', 'user_permissions', 'responsibility_groups', 'is_superuser') + fields = ('user_permissions', 'is_superuser') class UserProfileForm(forms.ModelForm): class Meta: model = UserProfile - exclude = ('avatar', 'password', 'groups', 'user_permissions', 'responsibility_groups', 'is_superuser') + exclude = ('avatar', 'password', 'groups', 'user_permissions', + 'responsibility_groups', 'is_admin', 'is_superuser', 'last_login') diff --git a/accounts_app/locale/ru/LC_MESSAGES/django.po b/accounts_app/locale/ru/LC_MESSAGES/django.po index 3e62963..22ee614 100644 --- a/accounts_app/locale/ru/LC_MESSAGES/django.po +++ b/accounts_app/locale/ru/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-31 16:28+0300\n" +"POT-Creation-Date: 2018-12-24 15:52+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Dmitry Novikov nerosketch@gmail.com\n" "Language: ru\n" @@ -18,114 +18,129 @@ msgstr "" "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" "%100>=11 && n%100<=14)? 2 : 3);\n" -#: models.py:22 +#: models.py:23 msgid "Users must have an telephone number" msgstr "У пользователей должен быть номер телефона" -#: models.py:50 templates/accounts/acc_list.html:21 +#: models.py:51 templates/accounts/acc_list.html:21 views.py:31 msgid "profile username" msgstr "Логин" -#: models.py:55 +#: models.py:56 msgid "fio" msgstr "ФИО" -#: models.py:56 +#: models.py:57 msgid "birth day" msgstr "дата рождения" -#: models.py:57 +#: models.py:58 msgid "Is active" msgstr "Активен" -#: models.py:61 templates/accounts/acc_list.html:23 -#: templates/accounts/create_acc.html:62 templates/accounts/index.html:9 -#: templates/accounts/settings/ch_info.html:38 +#: models.py:62 templates/accounts/acc_list.html:23 +#: templates/accounts/create_acc.html:61 templates/accounts/index.html:9 msgid "Telephone" msgstr "Телефон" -#: models.py:95 +#: models.py:96 msgid "Author" msgstr "Автор" -#: models.py:96 templates/accounts/action_log.html:12 +#: models.py:97 templates/accounts/action_log.html:12 msgid "Meta information" msgstr "Мета информация" -#: models.py:98 +#: models.py:99 msgid "Create user" msgstr "Создание абонента" -#: models.py:99 +#: models.py:100 msgid "Delete user" msgstr "Удаление абонента" -#: models.py:100 +#: models.py:101 msgid "Create device" msgstr "Создание устройства" -#: models.py:101 +#: models.py:102 msgid "Delete device" msgstr "Удаление устройства" -#: models.py:102 +#: models.py:103 msgid "Create NAS" msgstr "Создание NAS" -#: models.py:103 +#: models.py:104 msgid "Delete NAS" msgstr "Удаление NAS" -#: models.py:104 +#: models.py:105 msgid "Create service" msgstr "Создание тарифа" -#: models.py:105 +#: models.py:106 msgid "Delete service" msgstr "Удаление тарифа" -#: models.py:107 +#: models.py:108 msgid "Action type" msgstr "Тип действия" -#: models.py:108 +#: models.py:109 msgid "Additional info" msgstr "Дополнительная информация" -#: models.py:109 +#: models.py:110 msgid "Action date" msgstr "Дата действия" -#: models.py:116 +#: models.py:117 msgid "User profile log" msgstr "Лог действий учётной записи" -#: models.py:117 +#: models.py:118 msgid "User profile logs" msgstr "Логи действий учётной записи" -#: models.py:126 +#: models.py:127 msgid "Avatar" msgstr "Аватар" -#: models.py:128 +#: models.py:129 msgid "Responsibility groups" msgstr "Группы администратора" -#: models.py:142 +#: models.py:131 +msgid "Notification about tasks" +msgstr "Оповещения о задачах" + +#: models.py:132 +msgid "Notification about messages" +msgstr "Оповещения о сообщениях" + +#: models.py:133 +msgid "Notification from monitoring" +msgstr "Оповещения из мониторинга" + +#: models.py:135 +msgid "Settings flags" +msgstr "Флаги настройки" + +#: models.py:149 msgid "Staff account profile" msgstr "Учётная запись работника" -#: models.py:143 +#: models.py:150 msgid "Staff account profiles" msgstr "Учётные записи работников" -#: templates/accounts/acc_list.html:7 templates/accounts/create_acc.html:8 -#: templates/accounts/perms/change_global_perms.html:8 +#: templates/accounts/acc_list.html:7 templates/accounts/create_acc.html:7 +#: templates/accounts/perms/change_global_perms.html:7 #: templates/accounts/perms/ext.html:7 #: templates/accounts/perms/object/objects_of_type.html:7 -#: templates/accounts/perms/object/objects_types.html:8 -#: templates/accounts/perms/object/perms_edit.html:8 +#: templates/accounts/perms/object/objects_types.html:7 +#: templates/accounts/perms/object/perms_edit.html:7 msgid "Administrators" msgstr "Сотрудники" @@ -142,7 +157,6 @@ msgid "Fullname, or login if name is empty" msgstr "ФИО (или ник если нет)" #: templates/accounts/acc_list.html:24 -#: templates/accounts/settings/ch_info.html:28 msgid "Email" msgstr "Адрес электронной почты" @@ -178,61 +192,63 @@ msgstr "Описание" msgid "That admin has no logs" msgstr "Эта учётная запись не имеет логов" -#: templates/accounts/create_acc.html:9 +#: templates/accounts/create_acc.html:8 msgid "Add" msgstr "Добавить" -#: templates/accounts/create_acc.html:16 +#: templates/accounts/create_acc.html:15 msgid "Create new account" msgstr "Создать новую учётную запись" -#: templates/accounts/create_acc.html:33 templates/accounts/create_acc.html:37 +#: templates/accounts/create_acc.html:32 templates/accounts/create_acc.html:36 msgid "Username" msgstr "Логин" -#: templates/accounts/create_acc.html:42 templates/accounts/create_acc.html:47 +#: templates/accounts/create_acc.html:41 templates/accounts/create_acc.html:46 msgid "Fullname" msgstr "Полное имя" -#: templates/accounts/create_acc.html:52 +#: templates/accounts/create_acc.html:51 msgid "EMail" msgstr "Адрес электронной почты" -#: templates/accounts/create_acc.html:67 -#: templates/accounts/settings/ch_info.html:43 +#: templates/accounts/create_acc.html:66 msgid "+[7,8,9,3] and 10,11 digits" msgstr "+[7,8,9,3] и 10,11 цифр" -#: templates/accounts/create_acc.html:72 +#: templates/accounts/create_acc.html:71 msgid "Type password" msgstr "Введите пароль" -#: templates/accounts/create_acc.html:80 +#: templates/accounts/create_acc.html:79 msgid "Repeat password" msgstr "Повторите пароль" -#: templates/accounts/create_acc.html:89 +#: templates/accounts/create_acc.html:88 #: templates/accounts/manage_responsibility_groups.html:20 -#: templates/accounts/perms/change_global_perms.html:22 -#: templates/accounts/perms/object/perms_edit.html:43 +#: templates/accounts/perms/change_global_perms.html:21 +#: templates/accounts/perms/object/perms_edit.html:42 #: templates/accounts/set_abon_groups_permission.html:20 -#: templates/accounts/settings/ch_info.html:67 +#: templates/accounts/settings/userprofile_form.html:11 msgid "Save" msgstr "Сохранить" -#: templates/accounts/create_acc.html:92 +#: templates/accounts/create_acc.html:91 #: templates/accounts/manage_responsibility_groups.html:21 -#: templates/accounts/perms/object/perms_edit.html:46 +#: templates/accounts/perms/object/perms_edit.html:45 #: templates/accounts/set_abon_groups_permission.html:21 -#: templates/accounts/settings/ch_info.html:70 +#: templates/accounts/settings/userprofile_form.html:14 msgid "Reset" msgstr "Сбросить" -#: templates/accounts/index.html:13 templates/accounts/settings/ch_info.html:9 -#: templates/accounts/settings/ch_info.html:13 +#: templates/accounts/index.html:13 msgid "User name" msgstr "Логин" +#: templates/accounts/index.html:17 +msgid "Name and surname" +msgstr "Имя и отчество" + #: templates/accounts/index.html:21 msgid "Is enable" msgstr "Включён-ли" @@ -242,8 +258,8 @@ msgid "Last login" msgstr "Последняя авторизация" #: templates/accounts/index.html:30 -msgid "All permissions" -msgstr "Административный доступ (все права)" +msgid "Is superuser" +msgstr "Является суперпользователем" #: templates/accounts/login.html:5 msgid "Auth" @@ -261,47 +277,47 @@ msgstr "Войти по местоположению" msgid "The responsibility of the administrator of the group of subscribers" msgstr "Ответственность администратора за группы абонентов" -#: templates/accounts/perms/change_global_perms.html:10 +#: templates/accounts/perms/change_global_perms.html:9 #: templates/accounts/perms/ext.html:9 templates/accounts/perms/ext.html:14 msgid "Permission options" msgstr "Права" -#: templates/accounts/perms/change_global_perms.html:11 +#: templates/accounts/perms/change_global_perms.html:10 #: templates/accounts/perms/ext.html:22 msgid "Global permission options" msgstr "Глобальные права" -#: templates/accounts/perms/change_global_perms.html:16 +#: templates/accounts/perms/change_global_perms.html:15 msgid "Select permissions for picked account" msgstr "Отметьте права для выбранной учётной записи" #: templates/accounts/perms/ext.html:27 #: templates/accounts/perms/object/objects_of_type.html:9 +#: templates/accounts/perms/object/objects_types.html:9 #: templates/accounts/perms/object/objects_types.html:10 -#: templates/accounts/perms/object/objects_types.html:11 -#: templates/accounts/perms/object/perms_edit.html:10 +#: templates/accounts/perms/object/perms_edit.html:9 msgid "Object permission options" msgstr "Права для каждого объекта" #: templates/accounts/perms/object/objects_of_type.html:16 -#: templates/accounts/perms/object/perms_edit.html:18 +#: templates/accounts/perms/object/perms_edit.html:17 msgid "Pick object for edit permissions" msgstr "Выберите объект для редактирования прав доступа" -#: templates/accounts/perms/object/objects_types.html:16 +#: templates/accounts/perms/object/objects_types.html:15 msgid "Pick the type of object" msgstr "Выберите тип объекта" -#: templates/accounts/perms/object/objects_types.html:24 +#: templates/accounts/perms/object/objects_types.html:23 msgid "Group" msgstr "Группа" -#: templates/accounts/perms/object/perms_edit.html:27 +#: templates/accounts/perms/object/perms_edit.html:26 msgid "Profile is superuser, permissions to change it makes no sense" msgstr "" -"Учётная запись является суперпользователем. Разрешения менять нет смысла.," +"Учётная запись является суперпользователем. Разрешения менять нет смысла." -#: templates/accounts/perms/object/perms_edit.html:33 +#: templates/accounts/perms/object/perms_edit.html:32 msgid "Change permission for that object" msgstr "Изменение прав доступа для выбранного объекта" @@ -309,47 +325,35 @@ msgstr "Изменение прав доступа для выбранного msgid "The list of user groups to which the account has access" msgstr "Список групп абонентов, к которым учётка имеет доступ" -#: views.py:33 -msgid "Wrong login or password, please try again" -msgstr "Неправильный логин или пароль, попробуйте ещё раз" - -#: views.py:121 -msgid "New password is empty, fill it" -msgstr "Новый пароль пустой, придумайте себе пароль" - -#: views.py:123 -msgid "Wrong password" -msgstr "Неправильный пароль" - -#: views.py:125 -msgid "Empty password, fill it" -msgstr "Пустой пароль, впишите что-то в пароль" +#: views.py:104 views.py:126 +msgid "Saved successfully" +msgstr "Успешно сохранено" -#: views.py:149 +#: views.py:154 msgid "You forget specify a password for the new account" msgstr "Забыли указать пароль для нового аккаунта" -#: views.py:152 +#: views.py:157 msgid "You forget to repeat a password for the new account" msgstr "Забыли повторить пароль для нового аккаунта" -#: views.py:161 +#: views.py:166 msgid "Subscriber with this name already exist" msgstr "Пользователь с таким именем уже есть" -#: views.py:163 +#: views.py:168 msgid "Passwords does not match, try again" msgstr "Пароли не совпадают, попробуйте ещё раз" -#: views.py:178 +#: views.py:183 msgid "Profile has been deleted" msgstr "Учётная запись удалена" -#: views.py:240 +#: views.py:244 views.py:288 msgid "Permissions has successfully updated" msgstr "Права успешно обновлены" -#: views.py:352 +#: views.py:354 msgid "Responsibilities has been updated" msgstr "Ответственность за группы обновлена" @@ -374,5 +378,5 @@ msgstr "Лог действий" msgid "Administrator" msgstr "Сотрудник" -msgid "Saved successfully" -msgstr "Успешно сохранено" +msgid "Options" +msgstr "Настройки" diff --git a/accounts_app/migrations/0004_userprofile_flags.py b/accounts_app/migrations/0004_userprofile_flags.py new file mode 100644 index 0000000..9917033 --- /dev/null +++ b/accounts_app/migrations/0004_userprofile_flags.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.3 on 2018-12-24 15:52 + +import bitfield.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts_app', '0003_new_user_profile_log'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='flags', + field=bitfield.models.BitField((('notify_task', 'Notification about tasks'), ('notify_msg', 'Notification about messages'), ('notify_mon', 'Notification from monitoring')), default=0, verbose_name='Flags'), + ), + ] diff --git a/accounts_app/models.py b/accounts_app/models.py index 4d1d734..825bd1a 100644 --- a/accounts_app/models.py +++ b/accounts_app/models.py @@ -1,6 +1,7 @@ # -*- coding:utf-8 -*- import os from PIL import Image +from bitfield.models import BitField from jsonfield import JSONField from django.db import models @@ -124,8 +125,14 @@ class UserProfileManager(MyUserManager): class UserProfile(BaseAccount): avatar = models.ImageField(_('Avatar'), upload_to=os.path.join('user', 'avatar'), null=True, default=None, blank=True) - email = models.EmailField(default='') + email = models.EmailField(default='', blank=True) responsibility_groups = models.ManyToManyField(Group, blank=True, verbose_name=_('Responsibility groups')) + USER_PROFILE_FLAGS = ( + ('notify_task', _('Notification about tasks')), + ('notify_msg', _('Notification about messages')), + ('notify_mon', _('Notification from monitoring')) + ) + flags = BitField(flags=USER_PROFILE_FLAGS, default=0, verbose_name=_('Settings flags')) objects = UserProfileManager() diff --git a/accounts_app/templates/accounts/acc_list.html b/accounts_app/templates/accounts/acc_list.html index d13190f..f150a78 100644 --- a/accounts_app/templates/accounts/acc_list.html +++ b/accounts_app/templates/accounts/acc_list.html @@ -27,11 +27,12 @@
    - {{ usr.username }} -
    + + {{ usr.username }} + + {{ usr.username }} {{ usr.get_full_name }}
    {% trans 'Telephone' %}{{ userprofile.telephone }}{% trans 'Telephone' %}{{ userprofile.telephone }}
    {% trans 'User name' %}{{ userprofile.username }}{% trans 'User name' %}{{ userprofile.username }}
    {% trans 'Name and surname' %}{{ userprofile.fio }}{% trans 'Name and surname' %}{{ userprofile.fio }}
    {% trans 'Is enable' %}{% trans 'Is enable' %}
    {% trans 'Last login' %}{{ userprofile.last_login|date:"l d E Y H:i" }}{% trans 'Last login' %}{{ userprofile.last_login|date:"l d E Y H:i" }}
    {% trans 'All permissions' %}{% trans 'Is superuser' %}
    + + + + + + + + + + {% for messenger in object_list %} + + + + + + + {% empty %} + + + + {% endfor %} + + {% if perms.messenger.add_messenger %} + + + + + + {% endif %} +
    {% trans 'Title' %}{% trans 'Type' %}{% trans 'Slug' %}#
    {{ messenger.title }}{{ messenger.get_bot_type_display }}{{ messenger.slug }} + + + +
    {% trans 'messengers was not found' %}
    + + + +
    +
    +{% endblock %} diff --git a/messenger/templates/messenger/vibermessenger_form.html b/messenger/templates/messenger/vibermessenger_form.html new file mode 100644 index 0000000..888f1ce --- /dev/null +++ b/messenger/templates/messenger/vibermessenger_form.html @@ -0,0 +1,49 @@ +{% extends request.is_ajax|yesno:'bajax.html,base.html' %} +{% load i18n bootstrap3 %} + + +{% block breadcrumb %} + +{% endblock %} + + +{% block main %} + + {% if object %} + {% url 'messenger:update_viber_messenger' object.slug as form_url %} + {% trans 'Change messenger' as panel_title %} + {% else %} + {% url 'messenger:add_viber_messenger' as form_url %} + {% trans 'Add new messenger' as panel_title %} + {% endif %} + +
    +
    +

    {{ panel_title }}

    +
    +
    +
    {% csrf_token %} + {% bootstrap_form form %} + + {% if object %} + + {% trans 'Send webhook' %} + + {% endif %} +
    +
    +
    +{% endblock %} diff --git a/messenger/tests.py b/messenger/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/messenger/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/messenger/urls.py b/messenger/urls.py new file mode 100644 index 0000000..46acab3 --- /dev/null +++ b/messenger/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from django.views.decorators.csrf import csrf_exempt + +from messenger import views + + +app_name = 'messenger' + +urlpatterns = [ + path('', views.messengerListView.as_view(), name='messengers_list'), + path('new/', views.AddmessengerCreateView.as_view(), name='add_messenger'), + path('viber/new/', views.AddmessengerViberCreateView.as_view(), name='add_viber_messenger'), + path('viber//update/', views.UpdateVibermessengerUpdateView.as_view(), name='update_viber_messenger'), + path('viber//delete/', views.RemoveVibermessengerDeleteView.as_view(), name='delete_viber_messenger'), + path('viber//listen/', csrf_exempt(views.ListenViberView.as_view()), name='listen_viber_bot'), + path('viber//set_webhook/', views.SetWebhook.as_view(), name='webhook_viber_bot'), +] diff --git a/messenger/views.py b/messenger/views.py new file mode 100644 index 0000000..c5f6cf6 --- /dev/null +++ b/messenger/views.py @@ -0,0 +1,161 @@ +from django.contrib import messages +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.http import HttpResponseForbidden, HttpResponse, HttpResponseNotFound +from django.shortcuts import resolve_url +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _, gettext +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import ListView, CreateView, UpdateView, DeleteView, FormView, View +from django.views.generic.detail import SingleObjectMixin +from viberbot.api.messages import KeyboardMessage, ContactMessage +from viberbot.api.user_profile import UserProfile as ViberUserProfile +from viberbot.api.viber_requests import ViberMessageRequest, ViberSubscribedRequest, ViberFailedRequest, \ + ViberUnsubscribedRequest + +from accounts_app.models import UserProfile +from djing.lib.mixins import LoginAdminPermissionMixin, LoginAdminMixin +from messenger import forms, models + +from messenger.models import ViberMessage, ViberSubscriber + + +class messengerListView(LoginAdminPermissionMixin, ListView): + model = models.Messenger + permission_required = 'messenger.view_messenger' + + +class AddmessengerCreateView(LoginAdminMixin, FormView): + template_name = 'messenger/add_messenger.html' + form_class = forms.MessengerForm + + def form_valid(self, form): + bot_type = form.cleaned_data.get('bot_type') + if isinstance(bot_type, int) and bot_type > 0: + if bot_type == 1: + self.success_url = resolve_url('messenger:add_viber_messenger') + return super().form_valid(form) + messages.info(self.request, _('Unexpected bot type')) + self.success_url = resolve_url('messenger:messengers_list') + return super().form_valid(form) + + +class AddmessengerViberCreateView(LoginAdminMixin, PermissionRequiredMixin, CreateView): + model = models.ViberMessenger + form_class = forms.MessengerViberForm + permission_required = 'messenger.add_vibermessenger' + success_url = reverse_lazy('messenger:messengers_list') + + def form_valid(self, form): + r = super().form_valid(form) + messages.success(self.request, _('New viber messenger successfully created')) + return r + + +class UpdateVibermessengerUpdateView(LoginAdminPermissionMixin, UpdateView): + model = models.ViberMessenger + form_class = forms.MessengerViberForm + permission_required = 'messenger.change_vibermessenger' + + def form_valid(self, form): + r = super().form_valid(form) + messages.success(self.request, _('Viber messenger successfully updated')) + return r + + +class RemoveVibermessengerDeleteView(LoginAdminPermissionMixin, DeleteView): + model = models.ViberMessenger + permission_required = 'messenger.delete_vibermessenger' + success_url = reverse_lazy('messenger:messengers_list') + + def delete(self, request, *args, **kwargs): + r = super().delete(request, *args, **kwargs) + messages.success(request, _('Viber messenger successfully deleted')) + return r + + +@method_decorator(csrf_exempt, name='post') +class ListenViberView(SingleObjectMixin, View): + http_method_names = 'post', + model = models.ViberMessenger + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if not obj: + return HttpResponseNotFound() + self.object = obj + viber = obj.get_viber() + if not viber.verify_signature(request.body, request.META.get('HTTP_X_VIBER_CONTENT_SIGNATURE')): + return HttpResponseForbidden() + # this library supplies a simple way to receive a request object + vr = viber.parse_request(request.body) + if isinstance(vr, ViberMessageRequest): + in_msg = vr.message + if isinstance(in_msg, ContactMessage): + self.inbox_contact(in_msg, vr.sender) + subscriber, created = self.make_subscriber(vr.sender) + if not created: + ViberMessage.objects.create( + msg=vr.message, + sender=vr.sender.id, + messenger=obj, + subscriber=subscriber + ) + elif isinstance(vr, ViberSubscribedRequest): + self.make_subscriber(vr.user) + elif isinstance(vr, ViberFailedRequest): + print("client failed receiving message. failure: {0}".format(vr)) + elif isinstance(vr, ViberUnsubscribedRequest): + ViberSubscriber.objects.filter( + uid=vr.user_id + ).delete() + return HttpResponse(status=200) + + def make_subscriber(self, viber_user_profile: ViberUserProfile): + subscriber, created = ViberSubscriber.objects.get_or_create( + uid=viber_user_profile.id, + defaults={ + 'name': viber_user_profile.name, + 'avatar': viber_user_profile.avatar + } + ) + if created and hasattr(self, 'object'): + msg = KeyboardMessage(keyboard={ + 'Type': 'keyboard', + 'DefaultHeight': True, + 'Buttons': ({ + 'ActionType': 'share-phone', + 'ActionBody': 'reply to me', + "Text": gettext('My telephone number'), + "TextSize": "medium" + },) + }, min_api_version=3) + viber = self.object.get_viber() + viber.send_messages(viber_user_profile.id, msg) + return subscriber, created + + def inbox_contact(self, msg, sender: ViberUserProfile): + tel = msg.contact.phone_number + accs = UserProfile.objects.filter(telephone__icontains=tel) + viber = self.object.get_viber() + if accs.exists(): + subs = ViberSubscriber.objects.filter(uid=sender.id) + if subs.exists(): + subs.update(account=accs.first()) + viber.send_messages(sender.id, gettext( + 'Your account is attached. Now you will be receive notifications from billing' + )) + else: + viber.send_messages(sender.id, gettext('Telephone not found, please specify telephone number in account in billing')) + + +class SetWebhook(LoginAdminMixin, SingleObjectMixin, View): + http_method_names = 'get', + model = models.ViberMessenger + + def get(self, request, *args, **kwargs): + obj = self.get_object() + if not obj: + return HttpResponseNotFound + obj.send_webhook() + return HttpResponse(b'ok', status=200) diff --git a/msg_app/forms.py b/msg_app/forms.py index ff8c81b..af6a8cd 100644 --- a/msg_app/forms.py +++ b/msg_app/forms.py @@ -7,7 +7,7 @@ from accounts_app.models import UserProfile class ConversationForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(ConversationForm, self).__init__(*args, **kwargs) - user_profile_queryset = UserProfile.objects.filter(is_admin=True) + user_profile_queryset = UserProfile.objects.filter(is_admin=True, is_active=True) if user_profile_queryset is not None: self.fields['participants'].choices = [(up.pk, up.get_full_name()) for up in user_profile_queryset] diff --git a/msg_app/models.py b/msg_app/models.py index c8f12e9..7a06148 100644 --- a/msg_app/models.py +++ b/msg_app/models.py @@ -2,7 +2,6 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from accounts_app.models import UserProfile from djing.tasks import send_email_notify -from chatbot.models import ChatException class MessageError(Exception): @@ -203,23 +202,21 @@ class Conversation(models.Model): return messages[0] def new_message(self, text, attachment, author, with_status=True): - try: - msg = Message.objects.create( - text=text, conversation=self, - attachment=attachment, author=author - ) - if with_status: - for participant in self.participants.all(): - if participant == author: - continue - MessageStatus.objects.create(msg=msg, user=participant) + msg = Message.objects.create( + text=text, conversation=self, + attachment=attachment, author=author + ) + if with_status: + for participant in self.participants.filter(is_active=True): + if participant == author: + continue + MessageStatus.objects.create(msg=msg, user=participant) + if participant.flags.notify_msg: send_email_notify.delay( msg_text=text, account_id=participant.pk ) - return msg - except ChatException as e: - raise MessageError(e) + return msg @staticmethod def remove_message(msg): diff --git a/msg_app/templates/msg_app/chat.html b/msg_app/templates/msg_app/chat.html index 48a4488..6434a15 100644 --- a/msg_app/templates/msg_app/chat.html +++ b/msg_app/templates/msg_app/chat.html @@ -37,7 +37,7 @@
    {% if can_view_profile %} - + ava {% else %} @@ -51,7 +51,7 @@
    {{ msg.text }}
    {% if msg.attachment %} - + {{ msg.attachment }} {% endif %} diff --git a/msg_app/urls.py b/msg_app/urls.py index bfcd91f..eabb0d7 100644 --- a/msg_app/urls.py +++ b/msg_app/urls.py @@ -7,6 +7,5 @@ urlpatterns = [ path('', views.ConversationsListView.as_view(), name='home'), path('new/', views.new_conversation, name='new_conversation'), path('/', views.to_conversation, name='to_conversation'), - path('//del/', views.remove_msg, name='remove_msg'), - path('check_news/', views.check_news, name='check_news') + path('//del/', views.remove_msg, name='remove_msg') ] diff --git a/msg_app/views.py b/msg_app/views.py index 2310ab3..80d9573 100644 --- a/msg_app/views.py +++ b/msg_app/views.py @@ -1,15 +1,12 @@ -from json import dumps from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied -from django.http import HttpResponse from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.contrib import messages from django.shortcuts import render, redirect, get_object_or_404 from django.views.generic import ListView -from chatbot.models import MessageQueue from djing.lib.decorators import only_admins from guardian.decorators import permission_required_or_403 as permission_required @@ -87,22 +84,3 @@ def remove_msg(request, conv_id, msg_id): conversation_id = msg.conversation.pk msg.delete() return redirect('msg_app:to_conversation', conversation_id) - - -@login_required -@only_admins -def check_news(request): - if request.user.is_authenticated: - msg = MessageQueue.objects.pop(user=request.user, tag='msgapp') - if msg is None: - r = {'auth': True, 'exist': False} - else: - r = { - 'auth': True, - 'exist': True, - 'content': msg, - 'title': "%s" % _('Message') - } - else: - r = {'auth': False} - return HttpResponse(dumps(r)) diff --git a/periodic.py b/periodic.py index 36d2705..90e378e 100755 --- a/periodic.py +++ b/periodic.py @@ -7,9 +7,8 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") django.setup() from django.utils import timezone from django.db import transaction -from django.db.models import signals, Count -from abonapp.models import Abon, AbonTariff, abontariff_pre_delete, \ - PeriodicPayForId, AbonLog +from django.db.models import Count +from abonapp.models import Abon, AbonTariff, PeriodicPayForId, AbonLog from gw_app.nas_managers import NasNetworkError, NasFailedResult from gw_app.models import NASModel from djing.lib import LogicError @@ -35,7 +34,6 @@ class NasSyncThread(Thread): def main(): - signals.pre_delete.disconnect(abontariff_pre_delete, sender=AbonTariff) AbonTariff.objects.filter(abon=None).delete() now = timezone.now() fields = ('id', 'tariff__title', 'abon__id', 'abon__username') @@ -79,7 +77,7 @@ def main(): # make log about it l = AbonLog.objects.create( abon=abon, amount=-amount, - comment="Автоматическое продление услуги '%s'" % trf.title + comment="Автоматическое продление услуги '%s' для %s" % (trf.title, abon) ) print(l.comment) else: @@ -101,7 +99,8 @@ def main(): # connect service when autoconnect is True, and user have enough money for ab in Abon.objects.filter( is_active=True, - current_tariff=None + current_tariff=None, + autoconnect_service=True ).exclude(last_connected_tariff=None).iterator(): try: tariff = ab.last_connected_tariff diff --git a/requirements.txt b/requirements.txt index 1de9427..2a218a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,12 @@ urllib3 Django>=2 Pillow -telepot # for mac address field netaddr # for testing required xmltodict -xmltodict +#xmltodict dicttoxml # db client for Postgres @@ -18,11 +17,13 @@ pid django-guardian pinax-theme-bootstrap django-bootstrap3 -django-jsonfield + +# django-jsonfield +-e git://github.com/dmkoch/django-jsonfield.git#egg=django-jsonfield + requests webdavclient pyst2 -django-bitfield transliterate asterisk django-encrypted-model-fields @@ -30,6 +31,18 @@ django-encrypted-model-fields # django-xmlview for pay system allpay -e git://github.com/nerosketch/django-xmlview.git#egg=django-xmlview +# django-bitfield +-e git://github.com/disqus/django-bitfield.git#egg=django-bitfield + +# django_cleanup for clean unused media +-e git://github.com/un1t/django-cleanup.git#egg=django_cleanup + +# viberbot +-e git://github.com/Viber/viber-bot-python.git#egg=viberbot + +# pexpect +-e git://github.com/pexpect/pexpect.git#egg=pexpect + Celery redis==2.10.6 celery[redis] diff --git a/searchapp/templates/searchapp/index.html b/searchapp/templates/searchapp/index.html index 94b4b96..999debf 100644 --- a/searchapp/templates/searchapp/index.html +++ b/searchapp/templates/searchapp/index.html @@ -38,7 +38,7 @@
    {% for ab in abons %} - +

    {{ ab.username_display|safe }} @@ -70,7 +70,7 @@ {% else %} {% url 'devapp:fix_device_group' dev.pk as devviewlink %} {% endif %} - +

    {{ dev.comment|safe }} diff --git a/static/bad_ie.html b/static/bad_ie.html index b125096..6f670ed 100644 --- a/static/bad_ie.html +++ b/static/bad_ie.html @@ -1,11 +1,18 @@ - + - Старый браузер - + Старый браузер + -

    У вас старый ослик, обновитесь хотяб до IE10

    +

    Ваш InternetExplorer устарел, обновите ваш браузер на более современный

    +

    Можете воспользоваться ссылками ниже:

    +
    diff --git a/static/css/all.min.css b/static/css/all.min.css index 3ce7493..fc25529 100644 --- a/static/css/all.min.css +++ b/static/css/all.min.css @@ -10,7 +10,3 @@ * version : 4.17.43 * https://github.com/Eonasdan/bootstrap-datetimepicker/ */.bootstrap-datetimepicker-widget{list-style:none}.bootstrap-datetimepicker-widget.dropdown-menu{margin:2px 0;padding:4px;width:19em}@media (min-width:768px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:992px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:1200px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}.bootstrap-datetimepicker-widget.dropdown-menu:before,.bootstrap-datetimepicker-widget.dropdown-menu:after{content:'';display:inline-block;position:absolute}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before{border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);top:-7px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid white;top:-6px;left:8px}.bootstrap-datetimepicker-widget.dropdown-menu.top:before{border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #ccc;border-top-color:rgba(0,0,0,0.2);bottom:-7px;left:6px}.bootstrap-datetimepicker-widget.dropdown-menu.top:after{border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid white;bottom:-6px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget .list-unstyled{margin:0}.bootstrap-datetimepicker-widget a[data-action]{padding:6px 0}.bootstrap-datetimepicker-widget a[data-action]:active{box-shadow:none}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:54px;font-weight:bold;font-size:1.2em;margin:0}.bootstrap-datetimepicker-widget button[data-action]{padding:6px}.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Hours"}.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Hours"}.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Hours"}.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle AM/PM"}.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Clear the picker"}.bootstrap-datetimepicker-widget .btn[data-action="today"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Set the date to today"}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget .picker-switch::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle Date and Time Screens"}.bootstrap-datetimepicker-widget .picker-switch td{padding:0;margin:0;height:auto;width:auto;line-height:inherit}.bootstrap-datetimepicker-widget .picker-switch td span{line-height:2.5;height:2.5em;width:100%}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget table td,.bootstrap-datetimepicker-widget table th{text-align:center;border-radius:4px}.bootstrap-datetimepicker-widget table th{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table th.picker-switch{width:145px}.bootstrap-datetimepicker-widget table th.disabled,.bootstrap-datetimepicker-widget table th.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table th.prev::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Previous Month"}.bootstrap-datetimepicker-widget table th.next::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Next Month"}.bootstrap-datetimepicker-widget table thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#eee}.bootstrap-datetimepicker-widget table td{height:54px;line-height:54px;width:54px}.bootstrap-datetimepicker-widget table td.cw{font-size:.8em;height:20px;line-height:20px;color:#777}.bootstrap-datetimepicker-widget table td.day{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table td.day:hover,.bootstrap-datetimepicker-widget table td.hour:hover,.bootstrap-datetimepicker-widget table td.minute:hover,.bootstrap-datetimepicker-widget table td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget table td.old,.bootstrap-datetimepicker-widget table td.new{color:#777}.bootstrap-datetimepicker-widget table td.today{position:relative}.bootstrap-datetimepicker-widget table td.today:before{content:'';display:inline-block;border:solid transparent;border-width:0 0 7px 7px;border-bottom-color:#337ab7;border-top-color:rgba(0,0,0,0.2);position:absolute;bottom:4px;right:4px}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td.active.today:before{border-bottom-color:#fff}.bootstrap-datetimepicker-widget table td.disabled,.bootstrap-datetimepicker-widget table td.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table td span{display:inline-block;width:54px;height:54px;line-height:54px;margin:2px 1.5px;cursor:pointer;border-radius:4px}.bootstrap-datetimepicker-widget table td span:hover{background:#eee}.bootstrap-datetimepicker-widget table td span.active{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td span.old{color:#777}.bootstrap-datetimepicker-widget table td span.disabled,.bootstrap-datetimepicker-widget table td span.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget.usetwentyfour td.hour{height:27px;line-height:27px}.bootstrap-datetimepicker-widget.wider{width:21em}.bootstrap-datetimepicker-widget .datepicker-decades .decade{line-height:1.8em !important}.input-group.date .input-group-addon{cursor:pointer}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0} - - -/* Chartist.min.css */ -.ct-double-octave:after,.ct-major-eleventh:after,.ct-major-second:after,.ct-major-seventh:after,.ct-major-sixth:after,.ct-major-tenth:after,.ct-major-third:after,.ct-major-twelfth:after,.ct-minor-second:after,.ct-minor-seventh:after,.ct-minor-sixth:after,.ct-minor-third:after,.ct-octave:after,.ct-perfect-fifth:after,.ct-perfect-fourth:after,.ct-square:after{content:"";clear:both}.ct-label{fill:rgba(0,0,0,.4);color:rgba(0,0,0,.4);font-size:.75rem;line-height:1}.ct-grid-background,.ct-line{fill:none}.ct-chart-bar .ct-label,.ct-chart-line .ct-label{display:block;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}.ct-chart-donut .ct-label,.ct-chart-pie .ct-label{dominant-baseline:central}.ct-label.ct-horizontal.ct-start{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-label.ct-horizontal.ct-end{-webkit-box-align:flex-start;-webkit-align-items:flex-start;-ms-flex-align:flex-start;align-items:flex-start;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-label.ct-vertical.ct-start{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:flex-end;-webkit-justify-content:flex-end;-ms-flex-pack:flex-end;justify-content:flex-end;text-align:right;text-anchor:end}.ct-label.ct-vertical.ct-end{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-chart-bar .ct-label.ct-horizontal.ct-start{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center;text-anchor:start}.ct-chart-bar .ct-label.ct-horizontal.ct-end{-webkit-box-align:flex-start;-webkit-align-items:flex-start;-ms-flex-align:flex-start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center;text-anchor:start}.ct-chart-bar.ct-horizontal-bars .ct-label.ct-horizontal.ct-start{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-chart-bar.ct-horizontal-bars .ct-label.ct-horizontal.ct-end{-webkit-box-align:flex-start;-webkit-align-items:flex-start;-ms-flex-align:flex-start;align-items:flex-start;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-chart-bar.ct-horizontal-bars .ct-label.ct-vertical.ct-start{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:flex-end;-webkit-justify-content:flex-end;-ms-flex-pack:flex-end;justify-content:flex-end;text-align:right;text-anchor:end}.ct-chart-bar.ct-horizontal-bars .ct-label.ct-vertical.ct-end{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:end}.ct-grid{stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:2px}.ct-point{stroke-width:10px;stroke-linecap:round}.ct-line{stroke-width:4px}.ct-area{stroke:none;fill-opacity:.1}.ct-bar{fill:none;stroke-width:10px}.ct-slice-donut{fill:none;stroke-width:60px}.ct-series-a .ct-bar,.ct-series-a .ct-line,.ct-series-a .ct-point,.ct-series-a .ct-slice-donut{stroke:#d70206}.ct-series-a .ct-area,.ct-series-a .ct-slice-donut-solid,.ct-series-a .ct-slice-pie{fill:#d70206}.ct-series-b .ct-bar,.ct-series-b .ct-line,.ct-series-b .ct-point,.ct-series-b .ct-slice-donut{stroke:#f05b4f}.ct-series-b .ct-area,.ct-series-b .ct-slice-donut-solid,.ct-series-b .ct-slice-pie{fill:#f05b4f}.ct-series-c .ct-bar,.ct-series-c .ct-line,.ct-series-c .ct-point,.ct-series-c .ct-slice-donut{stroke:#f4c63d}.ct-series-c .ct-area,.ct-series-c .ct-slice-donut-solid,.ct-series-c .ct-slice-pie{fill:#f4c63d}.ct-series-d .ct-bar,.ct-series-d .ct-line,.ct-series-d .ct-point,.ct-series-d .ct-slice-donut{stroke:#d17905}.ct-series-d .ct-area,.ct-series-d .ct-slice-donut-solid,.ct-series-d .ct-slice-pie{fill:#d17905}.ct-series-e .ct-bar,.ct-series-e .ct-line,.ct-series-e .ct-point,.ct-series-e .ct-slice-donut{stroke:#453d3f}.ct-series-e .ct-area,.ct-series-e .ct-slice-donut-solid,.ct-series-e .ct-slice-pie{fill:#453d3f}.ct-series-f .ct-bar,.ct-series-f .ct-line,.ct-series-f .ct-point,.ct-series-f .ct-slice-donut{stroke:#59922b}.ct-series-f .ct-area,.ct-series-f .ct-slice-donut-solid,.ct-series-f .ct-slice-pie{fill:#59922b}.ct-series-g .ct-bar,.ct-series-g .ct-line,.ct-series-g .ct-point,.ct-series-g .ct-slice-donut{stroke:#0544d3}.ct-series-g .ct-area,.ct-series-g .ct-slice-donut-solid,.ct-series-g .ct-slice-pie{fill:#0544d3}.ct-series-h .ct-bar,.ct-series-h .ct-line,.ct-series-h .ct-point,.ct-series-h .ct-slice-donut{stroke:#6b0392}.ct-series-h .ct-area,.ct-series-h .ct-slice-donut-solid,.ct-series-h .ct-slice-pie{fill:#6b0392}.ct-series-i .ct-bar,.ct-series-i .ct-line,.ct-series-i .ct-point,.ct-series-i .ct-slice-donut{stroke:#f05b4f}.ct-series-i .ct-area,.ct-series-i .ct-slice-donut-solid,.ct-series-i .ct-slice-pie{fill:#f05b4f}.ct-series-j .ct-bar,.ct-series-j .ct-line,.ct-series-j .ct-point,.ct-series-j .ct-slice-donut{stroke:#dda458}.ct-series-j .ct-area,.ct-series-j .ct-slice-donut-solid,.ct-series-j .ct-slice-pie{fill:#dda458}.ct-series-k .ct-bar,.ct-series-k .ct-line,.ct-series-k .ct-point,.ct-series-k .ct-slice-donut{stroke:#eacf7d}.ct-series-k .ct-area,.ct-series-k .ct-slice-donut-solid,.ct-series-k .ct-slice-pie{fill:#eacf7d}.ct-series-l .ct-bar,.ct-series-l .ct-line,.ct-series-l .ct-point,.ct-series-l .ct-slice-donut{stroke:#86797d}.ct-series-l .ct-area,.ct-series-l .ct-slice-donut-solid,.ct-series-l .ct-slice-pie{fill:#86797d}.ct-series-m .ct-bar,.ct-series-m .ct-line,.ct-series-m .ct-point,.ct-series-m .ct-slice-donut{stroke:#b2c326}.ct-series-m .ct-area,.ct-series-m .ct-slice-donut-solid,.ct-series-m .ct-slice-pie{fill:#b2c326}.ct-series-n .ct-bar,.ct-series-n .ct-line,.ct-series-n .ct-point,.ct-series-n .ct-slice-donut{stroke:#6188e2}.ct-series-n .ct-area,.ct-series-n .ct-slice-donut-solid,.ct-series-n .ct-slice-pie{fill:#6188e2}.ct-series-o .ct-bar,.ct-series-o .ct-line,.ct-series-o .ct-point,.ct-series-o .ct-slice-donut{stroke:#a748ca}.ct-series-o .ct-area,.ct-series-o .ct-slice-donut-solid,.ct-series-o .ct-slice-pie{fill:#a748ca}.ct-square{display:block;position:relative;width:100%}.ct-square:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:100%}.ct-square:after{display:table}.ct-square>svg{display:block;position:absolute;top:0;left:0}.ct-minor-second{display:block;position:relative;width:100%}.ct-minor-second:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:93.75%}.ct-minor-second:after{display:table}.ct-minor-second>svg{display:block;position:absolute;top:0;left:0}.ct-major-second{display:block;position:relative;width:100%}.ct-major-second:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:88.8888888889%}.ct-major-second:after{display:table}.ct-major-second>svg{display:block;position:absolute;top:0;left:0}.ct-minor-third{display:block;position:relative;width:100%}.ct-minor-third:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:83.3333333333%}.ct-minor-third:after{display:table}.ct-minor-third>svg{display:block;position:absolute;top:0;left:0}.ct-major-third{display:block;position:relative;width:100%}.ct-major-third:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:80%}.ct-major-third:after{display:table}.ct-major-third>svg{display:block;position:absolute;top:0;left:0}.ct-perfect-fourth{display:block;position:relative;width:100%}.ct-perfect-fourth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:75%}.ct-perfect-fourth:after{display:table}.ct-perfect-fourth>svg{display:block;position:absolute;top:0;left:0}.ct-perfect-fifth{display:block;position:relative;width:100%}.ct-perfect-fifth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:66.6666666667%}.ct-perfect-fifth:after{display:table}.ct-perfect-fifth>svg{display:block;position:absolute;top:0;left:0}.ct-minor-sixth{display:block;position:relative;width:100%}.ct-minor-sixth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:62.5%}.ct-minor-sixth:after{display:table}.ct-minor-sixth>svg{display:block;position:absolute;top:0;left:0}.ct-golden-section{display:block;position:relative;width:100%}.ct-golden-section:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:61.804697157%}.ct-golden-section:after{content:"";display:table;clear:both}.ct-golden-section>svg{display:block;position:absolute;top:0;left:0}.ct-major-sixth{display:block;position:relative;width:100%}.ct-major-sixth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:60%}.ct-major-sixth:after{display:table}.ct-major-sixth>svg{display:block;position:absolute;top:0;left:0}.ct-minor-seventh{display:block;position:relative;width:100%}.ct-minor-seventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:56.25%}.ct-minor-seventh:after{display:table}.ct-minor-seventh>svg{display:block;position:absolute;top:0;left:0}.ct-major-seventh{display:block;position:relative;width:100%}.ct-major-seventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:53.3333333333%}.ct-major-seventh:after{display:table}.ct-major-seventh>svg{display:block;position:absolute;top:0;left:0}.ct-octave{display:block;position:relative;width:100%}.ct-octave:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:50%}.ct-octave:after{display:table}.ct-octave>svg{display:block;position:absolute;top:0;left:0}.ct-major-tenth{display:block;position:relative;width:100%}.ct-major-tenth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:40%}.ct-major-tenth:after{display:table}.ct-major-tenth>svg{display:block;position:absolute;top:0;left:0}.ct-major-eleventh{display:block;position:relative;width:100%}.ct-major-eleventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:37.5%}.ct-major-eleventh:after{display:table}.ct-major-eleventh>svg{display:block;position:absolute;top:0;left:0}.ct-major-twelfth{display:block;position:relative;width:100%}.ct-major-twelfth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:33.3333333333%}.ct-major-twelfth:after{display:table}.ct-major-twelfth>svg{display:block;position:absolute;top:0;left:0}.ct-double-octave{display:block;position:relative;width:100%}.ct-double-octave:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:25%}.ct-double-octave:after{display:table}.ct-double-octave>svg{display:block;position:absolute;top:0;left:0} \ No newline at end of file diff --git a/static/css/bootstrap-datetimepicker.min.css b/static/css/bootstrap-datetimepicker.min.css deleted file mode 100644 index 1b22fb7..0000000 --- a/static/css/bootstrap-datetimepicker.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Datetimepicker for Bootstrap 3 - * version : 4.17.43 - * https://github.com/Eonasdan/bootstrap-datetimepicker/ - */.bootstrap-datetimepicker-widget{list-style:none}.bootstrap-datetimepicker-widget.dropdown-menu{margin:2px 0;padding:4px;width:19em}@media (min-width:768px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:992px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:1200px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}.bootstrap-datetimepicker-widget.dropdown-menu:before,.bootstrap-datetimepicker-widget.dropdown-menu:after{content:'';display:inline-block;position:absolute}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before{border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);top:-7px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid white;top:-6px;left:8px}.bootstrap-datetimepicker-widget.dropdown-menu.top:before{border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #ccc;border-top-color:rgba(0,0,0,0.2);bottom:-7px;left:6px}.bootstrap-datetimepicker-widget.dropdown-menu.top:after{border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid white;bottom:-6px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget .list-unstyled{margin:0}.bootstrap-datetimepicker-widget a[data-action]{padding:6px 0}.bootstrap-datetimepicker-widget a[data-action]:active{box-shadow:none}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:54px;font-weight:bold;font-size:1.2em;margin:0}.bootstrap-datetimepicker-widget button[data-action]{padding:6px}.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Hours"}.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Hours"}.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Hours"}.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle AM/PM"}.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Clear the picker"}.bootstrap-datetimepicker-widget .btn[data-action="today"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Set the date to today"}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget .picker-switch::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle Date and Time Screens"}.bootstrap-datetimepicker-widget .picker-switch td{padding:0;margin:0;height:auto;width:auto;line-height:inherit}.bootstrap-datetimepicker-widget .picker-switch td span{line-height:2.5;height:2.5em;width:100%}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget table td,.bootstrap-datetimepicker-widget table th{text-align:center;border-radius:4px}.bootstrap-datetimepicker-widget table th{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table th.picker-switch{width:145px}.bootstrap-datetimepicker-widget table th.disabled,.bootstrap-datetimepicker-widget table th.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table th.prev::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Previous Month"}.bootstrap-datetimepicker-widget table th.next::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Next Month"}.bootstrap-datetimepicker-widget table thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#eee}.bootstrap-datetimepicker-widget table td{height:54px;line-height:54px;width:54px}.bootstrap-datetimepicker-widget table td.cw{font-size:.8em;height:20px;line-height:20px;color:#777}.bootstrap-datetimepicker-widget table td.day{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table td.day:hover,.bootstrap-datetimepicker-widget table td.hour:hover,.bootstrap-datetimepicker-widget table td.minute:hover,.bootstrap-datetimepicker-widget table td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget table td.old,.bootstrap-datetimepicker-widget table td.new{color:#777}.bootstrap-datetimepicker-widget table td.today{position:relative}.bootstrap-datetimepicker-widget table td.today:before{content:'';display:inline-block;border:solid transparent;border-width:0 0 7px 7px;border-bottom-color:#337ab7;border-top-color:rgba(0,0,0,0.2);position:absolute;bottom:4px;right:4px}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td.active.today:before{border-bottom-color:#fff}.bootstrap-datetimepicker-widget table td.disabled,.bootstrap-datetimepicker-widget table td.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table td span{display:inline-block;width:54px;height:54px;line-height:54px;margin:2px 1.5px;cursor:pointer;border-radius:4px}.bootstrap-datetimepicker-widget table td span:hover{background:#eee}.bootstrap-datetimepicker-widget table td span.active{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td span.old{color:#777}.bootstrap-datetimepicker-widget table td span.disabled,.bootstrap-datetimepicker-widget table td span.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget.usetwentyfour td.hour{height:27px;line-height:27px}.bootstrap-datetimepicker-widget.wider{width:21em}.bootstrap-datetimepicker-widget .datepicker-decades .decade{line-height:1.8em !important}.input-group.date .input-group-addon{cursor:pointer}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0} \ No newline at end of file diff --git a/static/css/chartist.min.css b/static/css/chartist.min.css new file mode 100644 index 0000000..6a23d47 --- /dev/null +++ b/static/css/chartist.min.css @@ -0,0 +1 @@ +.ct-double-octave:after,.ct-major-eleventh:after,.ct-major-second:after,.ct-major-seventh:after,.ct-major-sixth:after,.ct-major-tenth:after,.ct-major-third:after,.ct-major-twelfth:after,.ct-minor-second:after,.ct-minor-seventh:after,.ct-minor-sixth:after,.ct-minor-third:after,.ct-octave:after,.ct-perfect-fifth:after,.ct-perfect-fourth:after,.ct-square:after{content:"";clear:both}.ct-label{fill:rgba(0,0,0,.4);color:rgba(0,0,0,.4);font-size:.75rem;line-height:1}.ct-grid-background,.ct-line{fill:none}.ct-chart-bar .ct-label,.ct-chart-line .ct-label{display:block;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}.ct-chart-donut .ct-label,.ct-chart-pie .ct-label{dominant-baseline:central}.ct-label.ct-horizontal.ct-start{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-label.ct-horizontal.ct-end{-webkit-box-align:flex-start;-webkit-align-items:flex-start;-ms-flex-align:flex-start;align-items:flex-start;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-label.ct-vertical.ct-start{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:flex-end;-webkit-justify-content:flex-end;-ms-flex-pack:flex-end;justify-content:flex-end;text-align:right;text-anchor:end}.ct-label.ct-vertical.ct-end{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-chart-bar .ct-label.ct-horizontal.ct-start{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center;text-anchor:start}.ct-chart-bar .ct-label.ct-horizontal.ct-end{-webkit-box-align:flex-start;-webkit-align-items:flex-start;-ms-flex-align:flex-start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center;text-anchor:start}.ct-chart-bar.ct-horizontal-bars .ct-label.ct-horizontal.ct-start{-webkit-box-align:flex-end;-webkit-align-items:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-chart-bar.ct-horizontal-bars .ct-label.ct-horizontal.ct-end{-webkit-box-align:flex-start;-webkit-align-items:flex-start;-ms-flex-align:flex-start;align-items:flex-start;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:start}.ct-chart-bar.ct-horizontal-bars .ct-label.ct-vertical.ct-start{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:flex-end;-webkit-justify-content:flex-end;-ms-flex-pack:flex-end;justify-content:flex-end;text-align:right;text-anchor:end}.ct-chart-bar.ct-horizontal-bars .ct-label.ct-vertical.ct-end{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:flex-start;-webkit-justify-content:flex-start;-ms-flex-pack:flex-start;justify-content:flex-start;text-align:left;text-anchor:end}.ct-grid{stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:2px}.ct-point{stroke-width:10px;stroke-linecap:round}.ct-line{stroke-width:4px}.ct-area{stroke:none;fill-opacity:.1}.ct-bar{fill:none;stroke-width:10px}.ct-slice-donut{fill:none;stroke-width:60px}.ct-series-a .ct-bar,.ct-series-a .ct-line,.ct-series-a .ct-point,.ct-series-a .ct-slice-donut{stroke:#d70206}.ct-series-a .ct-area,.ct-series-a .ct-slice-donut-solid,.ct-series-a .ct-slice-pie{fill:#d70206}.ct-series-b .ct-bar,.ct-series-b .ct-line,.ct-series-b .ct-point,.ct-series-b .ct-slice-donut{stroke:#f05b4f}.ct-series-b .ct-area,.ct-series-b .ct-slice-donut-solid,.ct-series-b .ct-slice-pie{fill:#f05b4f}.ct-series-c .ct-bar,.ct-series-c .ct-line,.ct-series-c .ct-point,.ct-series-c .ct-slice-donut{stroke:#f4c63d}.ct-series-c .ct-area,.ct-series-c .ct-slice-donut-solid,.ct-series-c .ct-slice-pie{fill:#f4c63d}.ct-series-d .ct-bar,.ct-series-d .ct-line,.ct-series-d .ct-point,.ct-series-d .ct-slice-donut{stroke:#d17905}.ct-series-d .ct-area,.ct-series-d .ct-slice-donut-solid,.ct-series-d .ct-slice-pie{fill:#d17905}.ct-series-e .ct-bar,.ct-series-e .ct-line,.ct-series-e .ct-point,.ct-series-e .ct-slice-donut{stroke:#453d3f}.ct-series-e .ct-area,.ct-series-e .ct-slice-donut-solid,.ct-series-e .ct-slice-pie{fill:#453d3f}.ct-series-f .ct-bar,.ct-series-f .ct-line,.ct-series-f .ct-point,.ct-series-f .ct-slice-donut{stroke:#59922b}.ct-series-f .ct-area,.ct-series-f .ct-slice-donut-solid,.ct-series-f .ct-slice-pie{fill:#59922b}.ct-series-g .ct-bar,.ct-series-g .ct-line,.ct-series-g .ct-point,.ct-series-g .ct-slice-donut{stroke:#0544d3}.ct-series-g .ct-area,.ct-series-g .ct-slice-donut-solid,.ct-series-g .ct-slice-pie{fill:#0544d3}.ct-series-h .ct-bar,.ct-series-h .ct-line,.ct-series-h .ct-point,.ct-series-h .ct-slice-donut{stroke:#6b0392}.ct-series-h .ct-area,.ct-series-h .ct-slice-donut-solid,.ct-series-h .ct-slice-pie{fill:#6b0392}.ct-series-i .ct-bar,.ct-series-i .ct-line,.ct-series-i .ct-point,.ct-series-i .ct-slice-donut{stroke:#f05b4f}.ct-series-i .ct-area,.ct-series-i .ct-slice-donut-solid,.ct-series-i .ct-slice-pie{fill:#f05b4f}.ct-series-j .ct-bar,.ct-series-j .ct-line,.ct-series-j .ct-point,.ct-series-j .ct-slice-donut{stroke:#dda458}.ct-series-j .ct-area,.ct-series-j .ct-slice-donut-solid,.ct-series-j .ct-slice-pie{fill:#dda458}.ct-series-k .ct-bar,.ct-series-k .ct-line,.ct-series-k .ct-point,.ct-series-k .ct-slice-donut{stroke:#eacf7d}.ct-series-k .ct-area,.ct-series-k .ct-slice-donut-solid,.ct-series-k .ct-slice-pie{fill:#eacf7d}.ct-series-l .ct-bar,.ct-series-l .ct-line,.ct-series-l .ct-point,.ct-series-l .ct-slice-donut{stroke:#86797d}.ct-series-l .ct-area,.ct-series-l .ct-slice-donut-solid,.ct-series-l .ct-slice-pie{fill:#86797d}.ct-series-m .ct-bar,.ct-series-m .ct-line,.ct-series-m .ct-point,.ct-series-m .ct-slice-donut{stroke:#b2c326}.ct-series-m .ct-area,.ct-series-m .ct-slice-donut-solid,.ct-series-m .ct-slice-pie{fill:#b2c326}.ct-series-n .ct-bar,.ct-series-n .ct-line,.ct-series-n .ct-point,.ct-series-n .ct-slice-donut{stroke:#6188e2}.ct-series-n .ct-area,.ct-series-n .ct-slice-donut-solid,.ct-series-n .ct-slice-pie{fill:#6188e2}.ct-series-o .ct-bar,.ct-series-o .ct-line,.ct-series-o .ct-point,.ct-series-o .ct-slice-donut{stroke:#a748ca}.ct-series-o .ct-area,.ct-series-o .ct-slice-donut-solid,.ct-series-o .ct-slice-pie{fill:#a748ca}.ct-square{display:block;position:relative;width:100%}.ct-square:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:100%}.ct-square:after{display:table}.ct-square>svg{display:block;position:absolute;top:0;left:0}.ct-minor-second{display:block;position:relative;width:100%}.ct-minor-second:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:93.75%}.ct-minor-second:after{display:table}.ct-minor-second>svg{display:block;position:absolute;top:0;left:0}.ct-major-second{display:block;position:relative;width:100%}.ct-major-second:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:88.8888888889%}.ct-major-second:after{display:table}.ct-major-second>svg{display:block;position:absolute;top:0;left:0}.ct-minor-third{display:block;position:relative;width:100%}.ct-minor-third:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:83.3333333333%}.ct-minor-third:after{display:table}.ct-minor-third>svg{display:block;position:absolute;top:0;left:0}.ct-major-third{display:block;position:relative;width:100%}.ct-major-third:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:80%}.ct-major-third:after{display:table}.ct-major-third>svg{display:block;position:absolute;top:0;left:0}.ct-perfect-fourth{display:block;position:relative;width:100%}.ct-perfect-fourth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:75%}.ct-perfect-fourth:after{display:table}.ct-perfect-fourth>svg{display:block;position:absolute;top:0;left:0}.ct-perfect-fifth{display:block;position:relative;width:100%}.ct-perfect-fifth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:66.6666666667%}.ct-perfect-fifth:after{display:table}.ct-perfect-fifth>svg{display:block;position:absolute;top:0;left:0}.ct-minor-sixth{display:block;position:relative;width:100%}.ct-minor-sixth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:62.5%}.ct-minor-sixth:after{display:table}.ct-minor-sixth>svg{display:block;position:absolute;top:0;left:0}.ct-golden-section{display:block;position:relative;width:100%}.ct-golden-section:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:61.804697157%}.ct-golden-section:after{content:"";display:table;clear:both}.ct-golden-section>svg{display:block;position:absolute;top:0;left:0}.ct-major-sixth{display:block;position:relative;width:100%}.ct-major-sixth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:60%}.ct-major-sixth:after{display:table}.ct-major-sixth>svg{display:block;position:absolute;top:0;left:0}.ct-minor-seventh{display:block;position:relative;width:100%}.ct-minor-seventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:56.25%}.ct-minor-seventh:after{display:table}.ct-minor-seventh>svg{display:block;position:absolute;top:0;left:0}.ct-major-seventh{display:block;position:relative;width:100%}.ct-major-seventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:53.3333333333%}.ct-major-seventh:after{display:table}.ct-major-seventh>svg{display:block;position:absolute;top:0;left:0}.ct-octave{display:block;position:relative;width:100%}.ct-octave:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:50%}.ct-octave:after{display:table}.ct-octave>svg{display:block;position:absolute;top:0;left:0}.ct-major-tenth{display:block;position:relative;width:100%}.ct-major-tenth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:40%}.ct-major-tenth:after{display:table}.ct-major-tenth>svg{display:block;position:absolute;top:0;left:0}.ct-major-eleventh{display:block;position:relative;width:100%}.ct-major-eleventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:37.5%}.ct-major-eleventh:after{display:table}.ct-major-eleventh>svg{display:block;position:absolute;top:0;left:0}.ct-major-twelfth{display:block;position:relative;width:100%}.ct-major-twelfth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:33.3333333333%}.ct-major-twelfth:after{display:table}.ct-major-twelfth>svg{display:block;position:absolute;top:0;left:0}.ct-double-octave{display:block;position:relative;width:100%}.ct-double-octave:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:25%}.ct-double-octave:after{display:table}.ct-double-octave>svg{display:block;position:absolute;top:0;left:0} \ No newline at end of file diff --git a/static/img/gmarkers/dev.png b/static/img/gmarkers/dev.png deleted file mode 100644 index 81ebf83..0000000 Binary files a/static/img/gmarkers/dev.png and /dev/null differ diff --git a/static/img/gmarkers/dev_bug.png b/static/img/gmarkers/dev_bug.png deleted file mode 100644 index 1c1e867..0000000 Binary files a/static/img/gmarkers/dev_bug.png and /dev/null differ diff --git a/static/img/gmarkers/dev_ok.png b/static/img/gmarkers/dev_ok.png deleted file mode 100644 index b2639cf..0000000 Binary files a/static/img/gmarkers/dev_ok.png and /dev/null differ diff --git a/static/img/gmarkers/flag_black.png b/static/img/gmarkers/flag_black.png deleted file mode 100644 index 0d98e16..0000000 Binary files a/static/img/gmarkers/flag_black.png and /dev/null differ diff --git a/static/img/gmarkers/relay_rack.png b/static/img/gmarkers/relay_rack.png deleted file mode 100644 index b537df0..0000000 Binary files a/static/img/gmarkers/relay_rack.png and /dev/null differ diff --git a/static/js/all.min.js b/static/js/all.min.js index 2cec58b..ea60342 100644 --- a/static/js/all.min.js +++ b/static/js/all.min.js @@ -21,14 +21,3 @@ return k({},n(this))}function Bc(){return n(this).overflow}function Cc(){return{ /* Datetime picker */ !function(a){"use strict";if("function"==typeof define&&define.amd)define(["jquery","moment"],a);else if("object"==typeof exports)module.exports=a(require("jquery"),require("moment"));else{if("undefined"==typeof jQuery)throw"bootstrap-datetimepicker requires jQuery to be loaded first";if("undefined"==typeof moment)throw"bootstrap-datetimepicker requires Moment.js to be loaded first";a(jQuery,moment)}}(function(a,b){"use strict";if(!b)throw new Error("bootstrap-datetimepicker requires Moment.js to be loaded first");var c=function(c,d){var e,f,g,h,i,j,k,l={},m=!0,n=!1,o=!1,p=0,q=[{clsName:"days",navFnc:"M",navStep:1},{clsName:"months",navFnc:"y",navStep:1},{clsName:"years",navFnc:"y",navStep:10},{clsName:"decades",navFnc:"y",navStep:100}],r=["days","months","years","decades"],s=["top","bottom","auto"],t=["left","right","auto"],u=["default","top","bottom"],v={up:38,38:"up",down:40,40:"down",left:37,37:"left",right:39,39:"right",tab:9,9:"tab",escape:27,27:"escape",enter:13,13:"enter",pageUp:33,33:"pageUp",pageDown:34,34:"pageDown",shift:16,16:"shift",control:17,17:"control",space:32,32:"space",t:84,84:"t",delete:46,46:"delete"},w={},x=function(){return void 0!==b.tz&&void 0!==d.timeZone&&null!==d.timeZone&&""!==d.timeZone},y=function(a){var c;return c=void 0===a||null===a?b():b.isDate(a)||b.isMoment(a)?b(a):x()?b.tz(a,j,d.useStrict,d.timeZone):b(a,j,d.useStrict),x()&&c.tz(d.timeZone),c},z=function(a){if("string"!=typeof a||a.length>1)throw new TypeError("isEnabled expects a single character string parameter");switch(a){case"y":return i.indexOf("Y")!==-1;case"M":return i.indexOf("M")!==-1;case"d":return i.toLowerCase().indexOf("d")!==-1;case"h":case"H":return i.toLowerCase().indexOf("h")!==-1;case"m":return i.indexOf("m")!==-1;case"s":return i.indexOf("s")!==-1;default:return!1}},A=function(){return z("h")||z("m")||z("s")},B=function(){return z("y")||z("M")||z("d")},C=function(){var b=a("").append(a("").append(a("").addClass("prev").attr("data-action","previous").append(a("").addClass(d.icons.previous))).append(a("").addClass("picker-switch").attr("data-action","pickerSwitch").attr("colspan",d.calendarWeeks?"6":"5")).append(a("").addClass("next").attr("data-action","next").append(a("").addClass(d.icons.next)))),c=a("").append(a("").append(a("").attr("colspan",d.calendarWeeks?"8":"7")));return[a("
    ").addClass("datepicker-days").append(a("").addClass("table-condensed").append(b).append(a(""))),a("
    ").addClass("datepicker-months").append(a("
    ").addClass("table-condensed").append(b.clone()).append(c.clone())),a("
    ").addClass("datepicker-years").append(a("
    ").addClass("table-condensed").append(b.clone()).append(c.clone())),a("
    ").addClass("datepicker-decades").append(a("
    ").addClass("table-condensed").append(b.clone()).append(c.clone()))]},D=function(){var b=a(""),c=a(""),e=a("");return z("h")&&(b.append(a("
    ").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementHour}).addClass("btn").attr("data-action","incrementHours").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-hour").attr({"data-time-component":"hours",title:d.tooltips.pickHour}).attr("data-action","showHours"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementHour}).addClass("btn").attr("data-action","decrementHours").append(a("").addClass(d.icons.down))))),z("m")&&(z("h")&&(b.append(a("").addClass("separator")),c.append(a("").addClass("separator").html(":")),e.append(a("").addClass("separator"))),b.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementMinute}).addClass("btn").attr("data-action","incrementMinutes").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-minute").attr({"data-time-component":"minutes",title:d.tooltips.pickMinute}).attr("data-action","showMinutes"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementMinute}).addClass("btn").attr("data-action","decrementMinutes").append(a("").addClass(d.icons.down))))),z("s")&&(z("m")&&(b.append(a("").addClass("separator")),c.append(a("").addClass("separator").html(":")),e.append(a("").addClass("separator"))),b.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementSecond}).addClass("btn").attr("data-action","incrementSeconds").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-second").attr({"data-time-component":"seconds",title:d.tooltips.pickSecond}).attr("data-action","showSeconds"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementSecond}).addClass("btn").attr("data-action","decrementSeconds").append(a("").addClass(d.icons.down))))),h||(b.append(a("").addClass("separator")),c.append(a("").append(a("").addClass("separator"))),a("
    ").addClass("timepicker-picker").append(a("").addClass("table-condensed").append([b,c,e]))},E=function(){var b=a("
    ").addClass("timepicker-hours").append(a("
    ").addClass("table-condensed")),c=a("
    ").addClass("timepicker-minutes").append(a("
    ").addClass("table-condensed")),d=a("
    ").addClass("timepicker-seconds").append(a("
    ").addClass("table-condensed")),e=[D()];return z("h")&&e.push(b),z("m")&&e.push(c),z("s")&&e.push(d),e},F=function(){var b=[];return d.showTodayButton&&b.push(a("
    ").append(a("").attr({"data-action":"today",title:d.tooltips.today}).append(a("").addClass(d.icons.today)))),!d.sideBySide&&B()&&A()&&b.push(a("").append(a("").attr({"data-action":"togglePicker",title:d.tooltips.selectTime}).append(a("").addClass(d.icons.time)))),d.showClear&&b.push(a("").append(a("").attr({"data-action":"clear",title:d.tooltips.clear}).append(a("").addClass(d.icons.clear)))),d.showClose&&b.push(a("").append(a("").attr({"data-action":"close",title:d.tooltips.close}).append(a("").addClass(d.icons.close)))),a("").addClass("table-condensed").append(a("").append(a("").append(b)))},G=function(){var b=a("
    ").addClass("bootstrap-datetimepicker-widget dropdown-menu"),c=a("
    ").addClass("datepicker").append(C()),e=a("
    ").addClass("timepicker").append(E()),f=a("
      ").addClass("list-unstyled"),g=a("
    • ").addClass("picker-switch"+(d.collapse?" accordion-toggle":"")).append(F());return d.inline&&b.removeClass("dropdown-menu"),h&&b.addClass("usetwentyfour"),z("s")&&!h&&b.addClass("wider"),d.sideBySide&&B()&&A()?(b.addClass("timepicker-sbs"),"top"===d.toolbarPlacement&&b.append(g),b.append(a("
      ").addClass("row").append(c.addClass("col-md-6")).append(e.addClass("col-md-6"))),"bottom"===d.toolbarPlacement&&b.append(g),b):("top"===d.toolbarPlacement&&f.append(g),B()&&f.append(a("
    • ").addClass(d.collapse&&A()?"collapse in":"").append(c)),"default"===d.toolbarPlacement&&f.append(g),A()&&f.append(a("
    • ").addClass(d.collapse&&B()?"collapse":"").append(e)),"bottom"===d.toolbarPlacement&&f.append(g),b.append(f))},H=function(){var b,e={};return b=c.is("input")||d.inline?c.data():c.find("input").data(),b.dateOptions&&b.dateOptions instanceof Object&&(e=a.extend(!0,e,b.dateOptions)),a.each(d,function(a){var c="date"+a.charAt(0).toUpperCase()+a.slice(1);void 0!==b[c]&&(e[a]=b[c])}),e},I=function(){var b,e=(n||c).position(),f=(n||c).offset(),g=d.widgetPositioning.vertical,h=d.widgetPositioning.horizontal;if(d.widgetParent)b=d.widgetParent.append(o);else if(c.is("input"))b=c.after(o).parent();else{if(d.inline)return void(b=c.append(o));b=c,c.children().first().after(o)}if("auto"===g&&(g=f.top+1.5*o.height()>=a(window).height()+a(window).scrollTop()&&o.height()+c.outerHeight()a(window).width()?"right":"left"),"top"===g?o.addClass("top").removeClass("bottom"):o.addClass("bottom").removeClass("top"),"right"===h?o.addClass("pull-right"):o.removeClass("pull-right"),"static"===b.css("position")&&(b=b.parents().filter(function(){return"static"!==a(this).css("position")}).first()),0===b.length)throw new Error("datetimepicker component should be placed within a non-static positioned container");o.css({top:"top"===g?"auto":e.top+c.outerHeight(),bottom:"top"===g?b.outerHeight()-(b===c?0:e.top):"auto",left:"left"===h?b===c?0:e.left:"auto",right:"left"===h?"auto":b.outerWidth()-c.outerWidth()-(b===c?0:e.left)})},J=function(a){"dp.change"===a.type&&(a.date&&a.date.isSame(a.oldDate)||!a.date&&!a.oldDate)||c.trigger(a)},K=function(a){"y"===a&&(a="YYYY"),J({type:"dp.update",change:a,viewDate:f.clone()})},L=function(a){o&&(a&&(k=Math.max(p,Math.min(3,k+a))),o.find(".datepicker > div").hide().filter(".datepicker-"+q[k].clsName).show())},M=function(){var b=a("
    "),c=f.clone().startOf("w").startOf("d");for(d.calendarWeeks===!0&&b.append(a(""),d.calendarWeeks&&c.append('"),j.push(c)),k=["day"],b.isBefore(f,"M")&&k.push("old"),b.isAfter(f,"M")&&k.push("new"),b.isSame(e,"d")&&!m&&k.push("active"),R(b,"d")||k.push("disabled"),b.isSame(y(),"d")&&k.push("today"),0!==b.day()&&6!==b.day()||k.push("weekend"),J({type:"dp.classify",date:b,classNames:k}),c.append('"),b.add(1,"d");h.find("tbody").empty().append(j),T(),U(),V()}},X=function(){var b=o.find(".timepicker-hours table"),c=f.clone().startOf("d"),d=[],e=a("");for(f.hour()>11&&!h&&c.hour(12);c.isSame(f,"d")&&(h||f.hour()<12&&c.hour()<12||f.hour()>11);)c.hour()%4===0&&(e=a(""),d.push(e)),e.append('"),c.add(1,"h");b.empty().append(d)},Y=function(){for(var b=o.find(".timepicker-minutes table"),c=f.clone().startOf("h"),e=[],g=a(""),h=1===d.stepping?5:d.stepping;f.isSame(c,"h");)c.minute()%(4*h)===0&&(g=a(""),e.push(g)),g.append('"),c.add(h,"m");b.empty().append(e)},Z=function(){for(var b=o.find(".timepicker-seconds table"),c=f.clone().startOf("m"),d=[],e=a("");f.isSame(c,"m");)c.second()%20===0&&(e=a(""),d.push(e)),e.append('"),c.add(5,"s");b.empty().append(d)},$=function(){var a,b,c=o.find(".timepicker span[data-time-component]");h||(a=o.find(".timepicker [data-action=togglePeriod]"),b=e.clone().add(e.hours()>=12?-12:12,"h"),a.text(e.format("A")),R(b,"h")?a.removeClass("disabled"):a.addClass("disabled")),c.filter("[data-time-component=hours]").text(e.format(h?"HH":"hh")),c.filter("[data-time-component=minutes]").text(e.format("mm")),c.filter("[data-time-component=seconds]").text(e.format("ss")),X(),Y(),Z()},_=function(){o&&(W(),$())},aa=function(a){var b=m?null:e;if(!a)return m=!0,g.val(""),c.data("date",""),J({type:"dp.change",date:!1,oldDate:b}),void _();if(a=a.clone().locale(d.locale),x()&&a.tz(d.timeZone),1!==d.stepping)for(a.minutes(Math.round(a.minutes()/d.stepping)*d.stepping).seconds(0);d.minDate&&a.isBefore(d.minDate);)a.add(d.stepping,"minutes");R(a)?(e=a,f=e.clone(),g.val(e.format(i)),c.data("date",e.format(i)),m=!1,_(),J({type:"dp.change",date:e.clone(),oldDate:b})):(d.keepInvalid?J({type:"dp.change",date:a,oldDate:b}):g.val(m?"":e.format(i)),J({type:"dp.error",date:a,oldDate:b}))},ba=function(){var b=!1;return o?(o.find(".collapse").each(function(){var c=a(this).data("collapse");return!c||!c.transitioning||(b=!0,!1)}),b?l:(n&&n.hasClass("btn")&&n.toggleClass("active"),o.hide(),a(window).off("resize",I),o.off("click","[data-action]"),o.off("mousedown",!1),o.remove(),o=!1,J({type:"dp.hide",date:e.clone()}),g.blur(),f=e.clone(),l)):l},ca=function(){aa(null)},da=function(a){return void 0===d.parseInputDate?(!b.isMoment(a)||a instanceof Date)&&(a=y(a)):a=d.parseInputDate(a),a},ea={next:function(){var a=q[k].navFnc;f.add(q[k].navStep,a),W(),K(a)},previous:function(){var a=q[k].navFnc;f.subtract(q[k].navStep,a),W(),K(a)},pickerSwitch:function(){L(1)},selectMonth:function(b){var c=a(b.target).closest("tbody").find("span").index(a(b.target));f.month(c),k===p?(aa(e.clone().year(f.year()).month(f.month())),d.inline||ba()):(L(-1),W()),K("M")},selectYear:function(b){var c=parseInt(a(b.target).text(),10)||0;f.year(c),k===p?(aa(e.clone().year(f.year())),d.inline||ba()):(L(-1),W()),K("YYYY")},selectDecade:function(b){var c=parseInt(a(b.target).data("selection"),10)||0;f.year(c),k===p?(aa(e.clone().year(f.year())),d.inline||ba()):(L(-1),W()),K("YYYY")},selectDay:function(b){var c=f.clone();a(b.target).is(".old")&&c.subtract(1,"M"),a(b.target).is(".new")&&c.add(1,"M"),aa(c.date(parseInt(a(b.target).text(),10))),A()||d.keepOpen||d.inline||ba()},incrementHours:function(){var a=e.clone().add(1,"h");R(a,"h")&&aa(a)},incrementMinutes:function(){var a=e.clone().add(d.stepping,"m");R(a,"m")&&aa(a)},incrementSeconds:function(){var a=e.clone().add(1,"s");R(a,"s")&&aa(a)},decrementHours:function(){var a=e.clone().subtract(1,"h");R(a,"h")&&aa(a)},decrementMinutes:function(){var a=e.clone().subtract(d.stepping,"m");R(a,"m")&&aa(a)},decrementSeconds:function(){var a=e.clone().subtract(1,"s");R(a,"s")&&aa(a)},togglePeriod:function(){aa(e.clone().add(e.hours()>=12?-12:12,"h"))},togglePicker:function(b){var c,e=a(b.target),f=e.closest("ul"),g=f.find(".in"),h=f.find(".collapse:not(.in)");if(g&&g.length){if(c=g.data("collapse"),c&&c.transitioning)return;g.collapse?(g.collapse("hide"),h.collapse("show")):(g.removeClass("in"),h.addClass("in")),e.is("span")?e.toggleClass(d.icons.time+" "+d.icons.date):e.find("span").toggleClass(d.icons.time+" "+d.icons.date)}},showPicker:function(){o.find(".timepicker > div:not(.timepicker-picker)").hide(),o.find(".timepicker .timepicker-picker").show()},showHours:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-hours").show()},showMinutes:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-seconds").show()},selectHour:function(b){var c=parseInt(a(b.target).text(),10);h||(e.hours()>=12?12!==c&&(c+=12):12===c&&(c=0)),aa(e.clone().hours(c)),ea.showPicker.call(l)},selectMinute:function(b){aa(e.clone().minutes(parseInt(a(b.target).text(),10))),ea.showPicker.call(l)},selectSecond:function(b){aa(e.clone().seconds(parseInt(a(b.target).text(),10))),ea.showPicker.call(l)},clear:ca,today:function(){var a=y();R(a,"d")&&aa(a)},close:ba},fa=function(b){return!a(b.currentTarget).is(".disabled")&&(ea[a(b.currentTarget).data("action")].apply(l,arguments),!1)},ga=function(){var b,c={year:function(a){return a.month(0).date(1).hours(0).seconds(0).minutes(0)},month:function(a){return a.date(1).hours(0).seconds(0).minutes(0)},day:function(a){return a.hours(0).seconds(0).minutes(0)},hour:function(a){return a.seconds(0).minutes(0)},minute:function(a){return a.seconds(0)}};return g.prop("disabled")||!d.ignoreReadonly&&g.prop("readonly")||o?l:(void 0!==g.val()&&0!==g.val().trim().length?aa(da(g.val().trim())):m&&d.useCurrent&&(d.inline||g.is("input")&&0===g.val().trim().length)&&(b=y(),"string"==typeof d.useCurrent&&(b=c[d.useCurrent](b)),aa(b)),o=G(),M(),S(),o.find(".timepicker-hours").hide(),o.find(".timepicker-minutes").hide(),o.find(".timepicker-seconds").hide(),_(),L(),a(window).on("resize",I),o.on("click","[data-action]",fa),o.on("mousedown",!1),n&&n.hasClass("btn")&&n.toggleClass("active"),I(),o.show(),d.focusOnShow&&!g.is(":focus")&&g.focus(),J({type:"dp.show"}),l)},ha=function(){return o?ba():ga()},ia=function(a){var b,c,e,f,g=null,h=[],i={},j=a.which,k="p";w[j]=k;for(b in w)w.hasOwnProperty(b)&&w[b]===k&&(h.push(b),parseInt(b,10)!==j&&(i[b]=!0));for(b in d.keyBinds)if(d.keyBinds.hasOwnProperty(b)&&"function"==typeof d.keyBinds[b]&&(e=b.split(" "),e.length===h.length&&v[j]===e[e.length-1])){for(f=!0,c=e.length-2;c>=0;c--)if(!(v[e[c]]in i)){f=!1;break}if(f){g=d.keyBinds[b];break}}g&&(g.call(l,o),a.stopPropagation(),a.preventDefault())},ja=function(a){w[a.which]="r",a.stopPropagation(),a.preventDefault()},ka=function(b){var c=a(b.target).val().trim(),d=c?da(c):null;return aa(d),b.stopImmediatePropagation(),!1},la=function(){g.on({change:ka,blur:d.debug?"":ba,keydown:ia,keyup:ja,focus:d.allowInputToggle?ga:""}),c.is("input")?g.on({focus:ga}):n&&(n.on("click",ha),n.on("mousedown",!1))},ma=function(){g.off({change:ka,blur:blur,keydown:ia,keyup:ja,focus:d.allowInputToggle?ba:""}),c.is("input")?g.off({focus:ga}):n&&(n.off("click",ha),n.off("mousedown",!1))},na=function(b){var c={};return a.each(b,function(){var a=da(this);a.isValid()&&(c[a.format("YYYY-MM-DD")]=!0)}),!!Object.keys(c).length&&c},oa=function(b){var c={};return a.each(b,function(){c[this]=!0}),!!Object.keys(c).length&&c},pa=function(){var a=d.format||"L LT";i=a.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(a){var b=e.localeData().longDateFormat(a)||a;return b.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(a){return e.localeData().longDateFormat(a)||a})}),j=d.extraFormats?d.extraFormats.slice():[],j.indexOf(a)<0&&j.indexOf(i)<0&&j.push(i),h=i.toLowerCase().indexOf("a")<1&&i.replace(/\[.*?\]/g,"").indexOf("h")<1,z("y")&&(p=2),z("M")&&(p=1),z("d")&&(p=0),k=Math.max(p,k),m||aa(e)};if(l.destroy=function(){ba(),ma(),c.removeData("DateTimePicker"),c.removeData("date")},l.toggle=ha,l.show=ga,l.hide=ba,l.disable=function(){return ba(),n&&n.hasClass("btn")&&n.addClass("disabled"),g.prop("disabled",!0),l},l.enable=function(){return n&&n.hasClass("btn")&&n.removeClass("disabled"),g.prop("disabled",!1),l},l.ignoreReadonly=function(a){if(0===arguments.length)return d.ignoreReadonly;if("boolean"!=typeof a)throw new TypeError("ignoreReadonly () expects a boolean parameter");return d.ignoreReadonly=a,l},l.options=function(b){if(0===arguments.length)return a.extend(!0,{},d);if(!(b instanceof Object))throw new TypeError("options() options parameter should be an object");return a.extend(!0,d,b),a.each(d,function(a,b){if(void 0===l[a])throw new TypeError("option "+a+" is not recognized!");l[a](b)}),l},l.date=function(a){if(0===arguments.length)return m?null:e.clone();if(!(null===a||"string"==typeof a||b.isMoment(a)||a instanceof Date))throw new TypeError("date() parameter must be one of [null, string, moment or Date]");return aa(null===a?null:da(a)),l},l.format=function(a){if(0===arguments.length)return d.format;if("string"!=typeof a&&("boolean"!=typeof a||a!==!1))throw new TypeError("format() expects a string or boolean:false parameter "+a);return d.format=a,i&&pa(),l},l.timeZone=function(a){if(0===arguments.length)return d.timeZone;if("string"!=typeof a)throw new TypeError("newZone() expects a string parameter");return d.timeZone=a,l},l.dayViewHeaderFormat=function(a){if(0===arguments.length)return d.dayViewHeaderFormat;if("string"!=typeof a)throw new TypeError("dayViewHeaderFormat() expects a string parameter");return d.dayViewHeaderFormat=a,l},l.extraFormats=function(a){if(0===arguments.length)return d.extraFormats;if(a!==!1&&!(a instanceof Array))throw new TypeError("extraFormats() expects an array or false parameter");return d.extraFormats=a,j&&pa(),l},l.disabledDates=function(b){if(0===arguments.length)return d.disabledDates?a.extend({},d.disabledDates):d.disabledDates;if(!b)return d.disabledDates=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledDates() expects an array parameter");return d.disabledDates=na(b),d.enabledDates=!1,_(),l},l.enabledDates=function(b){if(0===arguments.length)return d.enabledDates?a.extend({},d.enabledDates):d.enabledDates;if(!b)return d.enabledDates=!1,_(),l;if(!(b instanceof Array))throw new TypeError("enabledDates() expects an array parameter");return d.enabledDates=na(b),d.disabledDates=!1,_(),l},l.daysOfWeekDisabled=function(a){if(0===arguments.length)return d.daysOfWeekDisabled.splice(0);if("boolean"==typeof a&&!a)return d.daysOfWeekDisabled=!1,_(),l;if(!(a instanceof Array))throw new TypeError("daysOfWeekDisabled() expects an array parameter");if(d.daysOfWeekDisabled=a.reduce(function(a,b){return b=parseInt(b,10),b>6||b<0||isNaN(b)?a:(a.indexOf(b)===-1&&a.push(b),a)},[]).sort(),d.useCurrent&&!d.keepInvalid){for(var b=0;!R(e,"d");){if(e.add(1,"d"),31===b)throw"Tried 31 times to find a valid date";b++}aa(e)}return _(),l},l.maxDate=function(a){if(0===arguments.length)return d.maxDate?d.maxDate.clone():d.maxDate;if("boolean"==typeof a&&a===!1)return d.maxDate=!1,_(),l;"string"==typeof a&&("now"!==a&&"moment"!==a||(a=y()));var b=da(a);if(!b.isValid())throw new TypeError("maxDate() Could not parse date parameter: "+a);if(d.minDate&&b.isBefore(d.minDate))throw new TypeError("maxDate() date parameter is before options.minDate: "+b.format(i));return d.maxDate=b,d.useCurrent&&!d.keepInvalid&&e.isAfter(a)&&aa(d.maxDate),f.isAfter(b)&&(f=b.clone().subtract(d.stepping,"m")),_(),l},l.minDate=function(a){if(0===arguments.length)return d.minDate?d.minDate.clone():d.minDate;if("boolean"==typeof a&&a===!1)return d.minDate=!1,_(),l;"string"==typeof a&&("now"!==a&&"moment"!==a||(a=y()));var b=da(a);if(!b.isValid())throw new TypeError("minDate() Could not parse date parameter: "+a);if(d.maxDate&&b.isAfter(d.maxDate))throw new TypeError("minDate() date parameter is after options.maxDate: "+b.format(i));return d.minDate=b,d.useCurrent&&!d.keepInvalid&&e.isBefore(a)&&aa(d.minDate),f.isBefore(b)&&(f=b.clone().add(d.stepping,"m")),_(),l},l.defaultDate=function(a){if(0===arguments.length)return d.defaultDate?d.defaultDate.clone():d.defaultDate;if(!a)return d.defaultDate=!1,l;"string"==typeof a&&(a="now"===a||"moment"===a?y():y(a));var b=da(a);if(!b.isValid())throw new TypeError("defaultDate() Could not parse date parameter: "+a);if(!R(b))throw new TypeError("defaultDate() date passed is invalid according to component setup validations");return d.defaultDate=b,(d.defaultDate&&d.inline||""===g.val().trim())&&aa(d.defaultDate),l},l.locale=function(a){if(0===arguments.length)return d.locale;if(!b.localeData(a))throw new TypeError("locale() locale "+a+" is not loaded from moment locales!");return d.locale=a,e.locale(d.locale),f.locale(d.locale),i&&pa(),o&&(ba(),ga()),l},l.stepping=function(a){return 0===arguments.length?d.stepping:(a=parseInt(a,10),(isNaN(a)||a<1)&&(a=1),d.stepping=a,l)},l.useCurrent=function(a){var b=["year","month","day","hour","minute"];if(0===arguments.length)return d.useCurrent;if("boolean"!=typeof a&&"string"!=typeof a)throw new TypeError("useCurrent() expects a boolean or string parameter");if("string"==typeof a&&b.indexOf(a.toLowerCase())===-1)throw new TypeError("useCurrent() expects a string parameter of "+b.join(", "));return d.useCurrent=a,l},l.collapse=function(a){if(0===arguments.length)return d.collapse;if("boolean"!=typeof a)throw new TypeError("collapse() expects a boolean parameter");return d.collapse===a?l:(d.collapse=a,o&&(ba(),ga()),l)},l.icons=function(b){if(0===arguments.length)return a.extend({},d.icons);if(!(b instanceof Object))throw new TypeError("icons() expects parameter to be an Object");return a.extend(d.icons,b),o&&(ba(),ga()),l},l.tooltips=function(b){if(0===arguments.length)return a.extend({},d.tooltips);if(!(b instanceof Object))throw new TypeError("tooltips() expects parameter to be an Object");return a.extend(d.tooltips,b),o&&(ba(),ga()),l},l.useStrict=function(a){if(0===arguments.length)return d.useStrict;if("boolean"!=typeof a)throw new TypeError("useStrict() expects a boolean parameter");return d.useStrict=a,l},l.sideBySide=function(a){if(0===arguments.length)return d.sideBySide;if("boolean"!=typeof a)throw new TypeError("sideBySide() expects a boolean parameter");return d.sideBySide=a,o&&(ba(),ga()),l},l.viewMode=function(a){if(0===arguments.length)return d.viewMode;if("string"!=typeof a)throw new TypeError("viewMode() expects a string parameter");if(r.indexOf(a)===-1)throw new TypeError("viewMode() parameter must be one of ("+r.join(", ")+") value");return d.viewMode=a,k=Math.max(r.indexOf(a),p),L(),l},l.toolbarPlacement=function(a){if(0===arguments.length)return d.toolbarPlacement;if("string"!=typeof a)throw new TypeError("toolbarPlacement() expects a string parameter");if(u.indexOf(a)===-1)throw new TypeError("toolbarPlacement() parameter must be one of ("+u.join(", ")+") value");return d.toolbarPlacement=a,o&&(ba(),ga()),l},l.widgetPositioning=function(b){if(0===arguments.length)return a.extend({},d.widgetPositioning);if("[object Object]"!=={}.toString.call(b))throw new TypeError("widgetPositioning() expects an object variable");if(b.horizontal){if("string"!=typeof b.horizontal)throw new TypeError("widgetPositioning() horizontal variable must be a string");if(b.horizontal=b.horizontal.toLowerCase(),t.indexOf(b.horizontal)===-1)throw new TypeError("widgetPositioning() expects horizontal parameter to be one of ("+t.join(", ")+")");d.widgetPositioning.horizontal=b.horizontal}if(b.vertical){if("string"!=typeof b.vertical)throw new TypeError("widgetPositioning() vertical variable must be a string");if(b.vertical=b.vertical.toLowerCase(),s.indexOf(b.vertical)===-1)throw new TypeError("widgetPositioning() expects vertical parameter to be one of ("+s.join(", ")+")");d.widgetPositioning.vertical=b.vertical}return _(),l},l.calendarWeeks=function(a){if(0===arguments.length)return d.calendarWeeks;if("boolean"!=typeof a)throw new TypeError("calendarWeeks() expects parameter to be a boolean value");return d.calendarWeeks=a,_(),l},l.showTodayButton=function(a){if(0===arguments.length)return d.showTodayButton;if("boolean"!=typeof a)throw new TypeError("showTodayButton() expects a boolean parameter");return d.showTodayButton=a,o&&(ba(),ga()),l},l.showClear=function(a){if(0===arguments.length)return d.showClear;if("boolean"!=typeof a)throw new TypeError("showClear() expects a boolean parameter");return d.showClear=a,o&&(ba(),ga()),l},l.widgetParent=function(b){if(0===arguments.length)return d.widgetParent;if("string"==typeof b&&(b=a(b)),null!==b&&"string"!=typeof b&&!(b instanceof a))throw new TypeError("widgetParent() expects a string or a jQuery object parameter");return d.widgetParent=b,o&&(ba(),ga()),l},l.keepOpen=function(a){if(0===arguments.length)return d.keepOpen;if("boolean"!=typeof a)throw new TypeError("keepOpen() expects a boolean parameter");return d.keepOpen=a,l},l.focusOnShow=function(a){if(0===arguments.length)return d.focusOnShow;if("boolean"!=typeof a)throw new TypeError("focusOnShow() expects a boolean parameter");return d.focusOnShow=a,l},l.inline=function(a){if(0===arguments.length)return d.inline;if("boolean"!=typeof a)throw new TypeError("inline() expects a boolean parameter");return d.inline=a,l},l.clear=function(){return ca(),l},l.keyBinds=function(a){return 0===arguments.length?d.keyBinds:(d.keyBinds=a,l)},l.getMoment=function(a){return y(a)},l.debug=function(a){if("boolean"!=typeof a)throw new TypeError("debug() expects a boolean parameter");return d.debug=a,l},l.allowInputToggle=function(a){if(0===arguments.length)return d.allowInputToggle;if("boolean"!=typeof a)throw new TypeError("allowInputToggle() expects a boolean parameter");return d.allowInputToggle=a,l},l.showClose=function(a){if(0===arguments.length)return d.showClose;if("boolean"!=typeof a)throw new TypeError("showClose() expects a boolean parameter");return d.showClose=a,l},l.keepInvalid=function(a){if(0===arguments.length)return d.keepInvalid;if("boolean"!=typeof a)throw new TypeError("keepInvalid() expects a boolean parameter"); return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)return d.datepickerInput;if("string"!=typeof a)throw new TypeError("datepickerInput() expects a string parameter");return d.datepickerInput=a,l},l.parseInputDate=function(a){if(0===arguments.length)return d.parseInputDate;if("function"!=typeof a)throw new TypeError("parseInputDate() sholud be as function");return d.parseInputDate=a,l},l.disabledTimeIntervals=function(b){if(0===arguments.length)return d.disabledTimeIntervals?a.extend({},d.disabledTimeIntervals):d.disabledTimeIntervals;if(!b)return d.disabledTimeIntervals=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledTimeIntervals() expects an array parameter");return d.disabledTimeIntervals=b,_(),l},l.disabledHours=function(b){if(0===arguments.length)return d.disabledHours?a.extend({},d.disabledHours):d.disabledHours;if(!b)return d.disabledHours=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledHours() expects an array parameter");if(d.disabledHours=oa(b),d.enabledHours=!1,d.useCurrent&&!d.keepInvalid){for(var c=0;!R(e,"h");){if(e.add(1,"h"),24===c)throw"Tried 24 times to find a valid date";c++}aa(e)}return _(),l},l.enabledHours=function(b){if(0===arguments.length)return d.enabledHours?a.extend({},d.enabledHours):d.enabledHours;if(!b)return d.enabledHours=!1,_(),l;if(!(b instanceof Array))throw new TypeError("enabledHours() expects an array parameter");if(d.enabledHours=oa(b),d.disabledHours=!1,d.useCurrent&&!d.keepInvalid){for(var c=0;!R(e,"h");){if(e.add(1,"h"),24===c)throw"Tried 24 times to find a valid date";c++}aa(e)}return _(),l},l.viewDate=function(a){if(0===arguments.length)return f.clone();if(!a)return f=e.clone(),l;if(!("string"==typeof a||b.isMoment(a)||a instanceof Date))throw new TypeError("viewDate() parameter must be one of [string, moment or Date]");return f=da(a),K(),l},c.is("input"))g=c;else if(g=c.find(d.datepickerInput),0===g.length)g=c.find("input");else if(!g.is("input"))throw new Error('CSS class "'+d.datepickerInput+'" cannot be applied to non input element');if(c.hasClass("input-group")&&(n=0===c.find(".datepickerbutton").length?c.find(".input-group-addon"):c.find(".datepickerbutton")),!d.inline&&!g.is("input"))throw new Error("Could not initialize DateTimePicker without an input element");return e=y(),f=e.clone(),a.extend(!0,d,H()),l.options(d),pa(),la(),g.prop("disabled")&&l.disable(),g.is("input")&&0!==g.val().trim().length?aa(da(g.val().trim())):d.defaultDate&&void 0===g.attr("placeholder")&&aa(d.defaultDate),d.inline&&ga(),l};return a.fn.datetimepicker=function(b){b=b||{};var d,e=Array.prototype.slice.call(arguments,1),f=!0,g=["destroy","hide","show","toggle"];if("object"==typeof b)return this.each(function(){var d,e=a(this);e.data("DateTimePicker")||(d=a.extend(!0,{},a.fn.datetimepicker.defaults,b),e.data("DateTimePicker",c(e,d)))});if("string"==typeof b)return this.each(function(){var c=a(this),g=c.data("DateTimePicker");if(!g)throw new Error('bootstrap-datetimepicker("'+b+'") method was called on an element that is not using DateTimePicker');d=g[b].apply(g,e),f=d===g}),f||a.inArray(b,g)>-1?this:d;throw new TypeError("Invalid arguments for DateTimePicker: "+b)},a.fn.datetimepicker.defaults={timeZone:"",format:!1,dayViewHeaderFormat:"MMMM YYYY",extraFormats:!1,stepping:1,minDate:!1,maxDate:!1,useCurrent:!0,collapse:!0,locale:b.locale(),defaultDate:!1,disabledDates:!1,enabledDates:!1,icons:{time:"glyphicon glyphicon-time",date:"glyphicon glyphicon-calendar",up:"glyphicon glyphicon-chevron-up",down:"glyphicon glyphicon-chevron-down",previous:"glyphicon glyphicon-chevron-left",next:"glyphicon glyphicon-chevron-right",today:"glyphicon glyphicon-screenshot",clear:"glyphicon glyphicon-trash",close:"glyphicon glyphicon-remove"},tooltips:{today:"Go to today",clear:"Clear selection",close:"Close the picker",selectMonth:"Select Month",prevMonth:"Previous Month",nextMonth:"Next Month",selectYear:"Select Year",prevYear:"Previous Year",nextYear:"Next Year",selectDecade:"Select Decade",prevDecade:"Previous Decade",nextDecade:"Next Decade",prevCentury:"Previous Century",nextCentury:"Next Century",pickHour:"Pick Hour",incrementHour:"Increment Hour",decrementHour:"Decrement Hour",pickMinute:"Pick Minute",incrementMinute:"Increment Minute",decrementMinute:"Decrement Minute",pickSecond:"Pick Second",incrementSecond:"Increment Second",decrementSecond:"Decrement Second",togglePeriod:"Toggle Period",selectTime:"Select Time"},useStrict:!1,sideBySide:!1,daysOfWeekDisabled:!1,calendarWeeks:!1,viewMode:"days",toolbarPlacement:"default",showTodayButton:!1,showClear:!1,showClose:!1,widgetPositioning:{horizontal:"auto",vertical:"auto"},widgetParent:null,ignoreReadonly:!1,keepOpen:!1,focusOnShow:!0,inline:!1,keepInvalid:!1,datepickerInput:".datepickerinput",keyBinds:{up:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().subtract(7,"d")):this.date(b.clone().add(this.stepping(),"m"))}},down:function(a){if(!a)return void this.show();var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().add(7,"d")):this.date(b.clone().subtract(this.stepping(),"m"))},"control up":function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().subtract(1,"y")):this.date(b.clone().add(1,"h"))}},"control down":function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().add(1,"y")):this.date(b.clone().subtract(1,"h"))}},left:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().subtract(1,"d"))}},right:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().add(1,"d"))}},pageUp:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().subtract(1,"M"))}},pageDown:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().add(1,"M"))}},enter:function(){this.hide()},escape:function(){this.hide()},"control space":function(a){a&&a.find(".timepicker").is(":visible")&&a.find('.btn[data-action="togglePeriod"]').click()},t:function(){this.date(this.getMoment())},delete:function(){this.clear()}},debug:!1,allowInputToggle:!1,disabledTimeIntervals:!1,disabledHours:!1,enabledHours:!1,viewDate:!1},a.fn.datetimepicker}); - - -/* Chartist.js 0.11.0 - * Copyright © 2017 Gion Kunz - * Free to use under either the WTFPL license or the MIT license. - * https://raw.githubusercontent.com/gionkunz/chartist-js/master/LICENSE-WTFPL - * https://raw.githubusercontent.com/gionkunz/chartist-js/master/LICENSE-MIT - */ -!function(a,b){"function"==typeof define&&define.amd?define("Chartist",[],function(){return a.Chartist=b()}):"object"==typeof module&&module.exports?module.exports=b():a.Chartist=b()}(this,function(){var a={version:"0.11.0"};return function(a,b,c){"use strict";c.namespaces={svg:"http://www.w3.org/2000/svg",xmlns:"http://www.w3.org/2000/xmlns/",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",ct:"http://gionkunz.github.com/chartist-js/ct"},c.noop=function(a){return a},c.alphaNumerate=function(a){return String.fromCharCode(97+a%26)},c.extend=function(a){var b,d,e;for(a=a||{},b=1;b":">",'"':""","'":"'"},c.serialize=function(a){return null===a||void 0===a?a:("number"==typeof a?a=""+a:"object"==typeof a&&(a=JSON.stringify({data:a})),Object.keys(c.escapingMap).reduce(function(a,b){return c.replaceAll(a,b,c.escapingMap[b])},a))},c.deserialize=function(a){if("string"!=typeof a)return a;a=Object.keys(c.escapingMap).reduce(function(a,b){return c.replaceAll(a,c.escapingMap[b],b)},a);try{a=JSON.parse(a),a=void 0!==a.data?a.data:a}catch(b){}return a},c.createSvg=function(a,b,d,e){var f;return b=b||"100%",d=d||"100%",Array.prototype.slice.call(a.querySelectorAll("svg")).filter(function(a){return a.getAttributeNS(c.namespaces.xmlns,"ct")}).forEach(function(b){a.removeChild(b)}),f=new c.Svg("svg").attr({width:b,height:d}).addClass(e),f._node.style.width=b,f._node.style.height=d,a.appendChild(f._node),f},c.normalizeData=function(a,b,d){var e,f={raw:a,normalized:{}};return f.normalized.series=c.getDataArray({series:a.series||[]},b,d),e=f.normalized.series.every(function(a){return a instanceof Array})?Math.max.apply(null,f.normalized.series.map(function(a){return a.length})):f.normalized.series.length,f.normalized.labels=(a.labels||[]).slice(),Array.prototype.push.apply(f.normalized.labels,c.times(Math.max(0,e-f.normalized.labels.length)).map(function(){return""})),b&&c.reverseData(f.normalized),f},c.safeHasProperty=function(a,b){return null!==a&&"object"==typeof a&&a.hasOwnProperty(b)},c.isDataHoleValue=function(a){return null===a||void 0===a||"number"==typeof a&&isNaN(a)},c.reverseData=function(a){a.labels.reverse(),a.series.reverse();for(var b=0;bf.high&&(f.high=c),h&&c0?f.low=0:(f.high=1,f.low=0)),f},c.isNumeric=function(a){return null!==a&&isFinite(a)},c.isFalseyButZero=function(a){return!a&&0!==a},c.getNumberOrUndefined=function(a){return c.isNumeric(a)?+a:void 0},c.isMultiValue=function(a){return"object"==typeof a&&("x"in a||"y"in a)},c.getMultiValue=function(a,b){return c.isMultiValue(a)?c.getNumberOrUndefined(a[b||"y"]):c.getNumberOrUndefined(a)},c.rho=function(a){function b(a,c){return a%c===0?c:b(c,a%c)}function c(a){return a*a+1}if(1===a)return a;var d,e=2,f=2;if(a%2===0)return 2;do e=c(e)%a,f=c(c(f))%a,d=b(Math.abs(e-f),a);while(1===d);return d},c.getBounds=function(a,b,d,e){function f(a,b){return a===(a+=b)&&(a*=1+(b>0?o:-o)),a}var g,h,i,j=0,k={high:b.high,low:b.low};k.valueRange=k.high-k.low,k.oom=c.orderOfMagnitude(k.valueRange),k.step=Math.pow(10,k.oom),k.min=Math.floor(k.low/k.step)*k.step,k.max=Math.ceil(k.high/k.step)*k.step,k.range=k.max-k.min,k.numberOfSteps=Math.round(k.range/k.step);var l=c.projectLength(a,k.step,k),m=l=d)k.step=1;else if(e&&n=d)k.step=n;else for(;;){if(m&&c.projectLength(a,k.step,k)<=d)k.step*=2;else{if(m||!(c.projectLength(a,k.step/2,k)>=d))break;if(k.step/=2,e&&k.step%1!==0){k.step*=2;break}}if(j++>1e3)throw new Error("Exceeded maximum number of iterations while optimizing scale step!")}var o=2.221e-16;for(k.step=Math.max(k.step,o),h=k.min,i=k.max;h+k.step<=k.low;)h=f(h,k.step);for(;i-k.step>=k.high;)i=f(i,-k.step);k.min=h,k.max=i,k.range=k.max-k.min;var p=[];for(g=k.min;g<=k.max;g=f(g,k.step)){var q=c.roundWithPrecision(g);q!==p[p.length-1]&&p.push(q)}return k.values=p,k},c.polarToCartesian=function(a,b,c,d){var e=(d-90)*Math.PI/180;return{x:a+c*Math.cos(e),y:b+c*Math.sin(e)}},c.createChartRect=function(a,b,d){var e=!(!b.axisX&&!b.axisY),f=e?b.axisY.offset:0,g=e?b.axisX.offset:0,h=a.width()||c.quantity(b.width).value||0,i=a.height()||c.quantity(b.height).value||0,j=c.normalizePadding(b.chartPadding,d);h=Math.max(h,f+j.left+j.right),i=Math.max(i,g+j.top+j.bottom);var k={padding:j,width:function(){return this.x2-this.x1},height:function(){return this.y1-this.y2}};return e?("start"===b.axisX.position?(k.y2=j.top+g,k.y1=Math.max(i-j.bottom,k.y2+1)):(k.y2=j.top,k.y1=Math.max(i-j.bottom-g,k.y2+1)),"start"===b.axisY.position?(k.x1=j.left+f,k.x2=Math.max(h-j.right,k.x1+1)):(k.x1=j.left,k.x2=Math.max(h-j.right-f,k.x1+1))):(k.x1=j.left,k.x2=Math.max(h-j.right,k.x1+1),k.y2=j.top,k.y1=Math.max(i-j.bottom,k.y2+1)),k},c.createGrid=function(a,b,d,e,f,g,h,i){var j={};j[d.units.pos+"1"]=a,j[d.units.pos+"2"]=a,j[d.counterUnits.pos+"1"]=e,j[d.counterUnits.pos+"2"]=e+f;var k=g.elem("line",j,h.join(" "));i.emit("draw",c.extend({type:"grid",axis:d,index:b,group:g,element:k},j))},c.createGridBackground=function(a,b,c,d){var e=a.elem("rect",{x:b.x1,y:b.y2,width:b.width(),height:b.height()},c,!0);d.emit("draw",{type:"gridBackground",group:a,element:e})},c.createLabel=function(a,d,e,f,g,h,i,j,k,l,m){var n,o={};if(o[g.units.pos]=a+i[g.units.pos],o[g.counterUnits.pos]=i[g.counterUnits.pos],o[g.units.len]=d,o[g.counterUnits.len]=Math.max(0,h-10),l){var p=b.createElement("span");p.className=k.join(" "),p.setAttribute("xmlns",c.namespaces.xhtml),p.innerText=f[e],p.style[g.units.len]=Math.round(o[g.units.len])+"px",p.style[g.counterUnits.len]=Math.round(o[g.counterUnits.len])+"px",n=j.foreignObject(p,c.extend({style:"overflow: visible;"},o))}else n=j.elem("text",o,k.join(" ")).text(f[e]);m.emit("draw",c.extend({type:"label",axis:g,index:e,group:j,element:n,text:f[e]},o))},c.getSeriesOption=function(a,b,c){if(a.name&&b.series&&b.series[a.name]){var d=b.series[a.name];return d.hasOwnProperty(c)?d[c]:b[c]}return b[c]},c.optionsProvider=function(b,d,e){function f(b){var f=h;if(h=c.extend({},j),d)for(i=0;i=2&&a[h]<=a[h-2]&&(g=!0),g&&(f.push({pathCoordinates:[],valueData:[]}),g=!1),f[f.length-1].pathCoordinates.push(a[h],a[h+1]),f[f.length-1].valueData.push(b[h/2]));return f}}(window,document,a),function(a,b,c){"use strict";c.Interpolation={},c.Interpolation.none=function(a){var b={fillHoles:!1};return a=c.extend({},b,a),function(b,d){for(var e=new c.Svg.Path,f=!0,g=0;g1){var i=[];return h.forEach(function(a){i.push(f(a.pathCoordinates,a.valueData))}),c.Svg.Path.join(i)}if(b=h[0].pathCoordinates,g=h[0].valueData,b.length<=4)return c.Interpolation.none()(b,g);for(var j,k=(new c.Svg.Path).move(b[0],b[1],!1,g[0]),l=0,m=b.length;m-2*!j>l;l+=2){var n=[{x:+b[l-2],y:+b[l-1]},{x:+b[l],y:+b[l+1]},{x:+b[l+2],y:+b[l+3]},{x:+b[l+4],y:+b[l+5]}];j?l?m-4===l?n[3]={x:+b[0],y:+b[1]}:m-2===l&&(n[2]={x:+b[0],y:+b[1]},n[3]={x:+b[2],y:+b[3]}):n[0]={x:+b[m-2],y:+b[m-1]}:m-4===l?n[3]=n[2]:l||(n[0]={x:+b[l],y:+b[l+1]}),k.curve(d*(-n[0].x+6*n[1].x+n[2].x)/6+e*n[2].x,d*(-n[0].y+6*n[1].y+n[2].y)/6+e*n[2].y,d*(n[1].x+6*n[2].x-n[3].x)/6+e*n[2].x,d*(n[1].y+6*n[2].y-n[3].y)/6+e*n[2].y,n[2].x,n[2].y,!1,g[(l+2)/2])}return k}return c.Interpolation.none()([])}},c.Interpolation.monotoneCubic=function(a){var b={fillHoles:!1};return a=c.extend({},b,a),function d(b,e){var f=c.splitIntoSegments(b,e,{fillHoles:a.fillHoles,increasingX:!0});if(f.length){if(f.length>1){var g=[];return f.forEach(function(a){g.push(d(a.pathCoordinates,a.valueData))}),c.Svg.Path.join(g)}if(b=f[0].pathCoordinates,e=f[0].valueData,b.length<=4)return c.Interpolation.none()(b,e);var h,i,j=[],k=[],l=b.length/2,m=[],n=[],o=[],p=[];for(h=0;h0!=n[h]>0?m[h]=0:(m[h]=3*(p[h-1]+p[h])/((2*p[h]+p[h-1])/n[h-1]+(p[h]+2*p[h-1])/n[h]),isFinite(m[h])||(m[h]=0));for(i=(new c.Svg.Path).move(j[0],k[0],!1,e[0]),h=0;h1}).map(function(a){var b=a.pathElements[0],c=a.pathElements[a.pathElements.length-1];return a.clone(!0).position(0).remove(1).move(b.x,r).line(b.x,b.y).position(a.pathElements.length+1).line(c.x,r)}).forEach(function(c){var h=i.elem("path",{d:c.stringify()},a.classNames.area,!0);this.eventEmitter.emit("draw",{type:"area",values:b.normalized.series[g],path:c.clone(),series:f,seriesIndex:g,axisX:d,axisY:e,chartRect:j,index:g,group:i,element:h})}.bind(this))}}.bind(this)),this.eventEmitter.emit("created",{bounds:e.bounds,chartRect:j,axisX:d,axisY:e,svg:this.svg,options:a})}function e(a,b,d,e){c.Line["super"].constructor.call(this,a,b,f,c.extend({},f,d),e)}var f={axisX:{offset:30,position:"end",labelOffset:{x:0,y:0},showLabel:!0,showGrid:!0,labelInterpolationFnc:c.noop,type:void 0},axisY:{offset:40,position:"start",labelOffset:{x:0,y:0},showLabel:!0,showGrid:!0,labelInterpolationFnc:c.noop,type:void 0,scaleMinSpace:20,onlyInteger:!1},width:void 0,height:void 0,showLine:!0,showPoint:!0,showArea:!1,areaBase:0,lineSmooth:!0,showGridBackground:!1,low:void 0,high:void 0,chartPadding:{top:15,right:15,bottom:5,left:10},fullWidth:!1,reverseData:!1,classNames:{chart:"ct-chart-line",label:"ct-label",labelGroup:"ct-labels",series:"ct-series",line:"ct-line",point:"ct-point",area:"ct-area",grid:"ct-grid",gridGroup:"ct-grids",gridBackground:"ct-grid-background",vertical:"ct-vertical",horizontal:"ct-horizontal",start:"ct-start",end:"ct-end"}};c.Line=c.Base.extend({constructor:e,createChart:d})}(window,document,a),function(a,b,c){"use strict";function d(a){var b,d;a.distributeSeries?(b=c.normalizeData(this.data,a.reverseData,a.horizontalBars?"x":"y"),b.normalized.series=b.normalized.series.map(function(a){return[a]})):b=c.normalizeData(this.data,a.reverseData,a.horizontalBars?"x":"y"),this.svg=c.createSvg(this.container,a.width,a.height,a.classNames.chart+(a.horizontalBars?" "+a.classNames.horizontalBars:""));var e=this.svg.elem("g").addClass(a.classNames.gridGroup),g=this.svg.elem("g"),h=this.svg.elem("g").addClass(a.classNames.labelGroup);if(a.stackBars&&0!==b.normalized.series.length){var i=c.serialMap(b.normalized.series,function(){ -return Array.prototype.slice.call(arguments).map(function(a){return a}).reduce(function(a,b){return{x:a.x+(b&&b.x)||0,y:a.y+(b&&b.y)||0}},{x:0,y:0})});d=c.getHighLow([i],a,a.horizontalBars?"x":"y")}else d=c.getHighLow(b.normalized.series,a,a.horizontalBars?"x":"y");d.high=+a.high||(0===a.high?0:d.high),d.low=+a.low||(0===a.low?0:d.low);var j,k,l,m,n,o=c.createChartRect(this.svg,a,f.padding);k=a.distributeSeries&&a.stackBars?b.normalized.labels.slice(0,1):b.normalized.labels,a.horizontalBars?(j=m=void 0===a.axisX.type?new c.AutoScaleAxis(c.Axis.units.x,b.normalized.series,o,c.extend({},a.axisX,{highLow:d,referenceValue:0})):a.axisX.type.call(c,c.Axis.units.x,b.normalized.series,o,c.extend({},a.axisX,{highLow:d,referenceValue:0})),l=n=void 0===a.axisY.type?new c.StepAxis(c.Axis.units.y,b.normalized.series,o,{ticks:k}):a.axisY.type.call(c,c.Axis.units.y,b.normalized.series,o,a.axisY)):(l=m=void 0===a.axisX.type?new c.StepAxis(c.Axis.units.x,b.normalized.series,o,{ticks:k}):a.axisX.type.call(c,c.Axis.units.x,b.normalized.series,o,a.axisX),j=n=void 0===a.axisY.type?new c.AutoScaleAxis(c.Axis.units.y,b.normalized.series,o,c.extend({},a.axisY,{highLow:d,referenceValue:0})):a.axisY.type.call(c,c.Axis.units.y,b.normalized.series,o,c.extend({},a.axisY,{highLow:d,referenceValue:0})));var p=a.horizontalBars?o.x1+j.projectValue(0):o.y1-j.projectValue(0),q=[];l.createGridAndLabels(e,h,this.supportsForeignObject,a,this.eventEmitter),j.createGridAndLabels(e,h,this.supportsForeignObject,a,this.eventEmitter),a.showGridBackground&&c.createGridBackground(e,o,a.classNames.gridBackground,this.eventEmitter),b.raw.series.forEach(function(d,e){var f,h,i=e-(b.raw.series.length-1)/2;f=a.distributeSeries&&!a.stackBars?l.axisLength/b.normalized.series.length/2:a.distributeSeries&&a.stackBars?l.axisLength/2:l.axisLength/b.normalized.series[e].length/2,h=g.elem("g"),h.attr({"ct:series-name":d.name,"ct:meta":c.serialize(d.meta)}),h.addClass([a.classNames.series,d.className||a.classNames.series+"-"+c.alphaNumerate(e)].join(" ")),b.normalized.series[e].forEach(function(g,k){var r,s,t,u;if(u=a.distributeSeries&&!a.stackBars?e:a.distributeSeries&&a.stackBars?0:k,r=a.horizontalBars?{x:o.x1+j.projectValue(g&&g.x?g.x:0,k,b.normalized.series[e]),y:o.y1-l.projectValue(g&&g.y?g.y:0,u,b.normalized.series[e])}:{x:o.x1+l.projectValue(g&&g.x?g.x:0,u,b.normalized.series[e]),y:o.y1-j.projectValue(g&&g.y?g.y:0,k,b.normalized.series[e])},l instanceof c.StepAxis&&(l.options.stretch||(r[l.units.pos]+=f*(a.horizontalBars?-1:1)),r[l.units.pos]+=a.stackBars||a.distributeSeries?0:i*a.seriesBarDistance*(a.horizontalBars?-1:1)),t=q[k]||p,q[k]=t-(p-r[l.counterUnits.pos]),void 0!==g){var v={};v[l.units.pos+"1"]=r[l.units.pos],v[l.units.pos+"2"]=r[l.units.pos],!a.stackBars||"accumulate"!==a.stackMode&&a.stackMode?(v[l.counterUnits.pos+"1"]=p,v[l.counterUnits.pos+"2"]=r[l.counterUnits.pos]):(v[l.counterUnits.pos+"1"]=t,v[l.counterUnits.pos+"2"]=q[k]),v.x1=Math.min(Math.max(v.x1,o.x1),o.x2),v.x2=Math.min(Math.max(v.x2,o.x1),o.x2),v.y1=Math.min(Math.max(v.y1,o.y2),o.y1),v.y2=Math.min(Math.max(v.y2,o.y2),o.y1);var w=c.getMetaData(d,k);s=h.elem("line",v,a.classNames.bar).attr({"ct:value":[g.x,g.y].filter(c.isNumeric).join(","),"ct:meta":c.serialize(w)}),this.eventEmitter.emit("draw",c.extend({type:"bar",value:g,index:k,meta:w,series:d,seriesIndex:e,axisX:m,axisY:n,chartRect:o,group:h,element:s},v))}}.bind(this))}.bind(this)),this.eventEmitter.emit("created",{bounds:j.bounds,chartRect:o,axisX:m,axisY:n,svg:this.svg,options:a})}function e(a,b,d,e){c.Bar["super"].constructor.call(this,a,b,f,c.extend({},f,d),e)}var f={axisX:{offset:30,position:"end",labelOffset:{x:0,y:0},showLabel:!0,showGrid:!0,labelInterpolationFnc:c.noop,scaleMinSpace:30,onlyInteger:!1},axisY:{offset:40,position:"start",labelOffset:{x:0,y:0},showLabel:!0,showGrid:!0,labelInterpolationFnc:c.noop,scaleMinSpace:20,onlyInteger:!1},width:void 0,height:void 0,high:void 0,low:void 0,referenceValue:0,chartPadding:{top:15,right:15,bottom:5,left:10},seriesBarDistance:15,stackBars:!1,stackMode:"accumulate",horizontalBars:!1,distributeSeries:!1,reverseData:!1,showGridBackground:!1,classNames:{chart:"ct-chart-bar",horizontalBars:"ct-horizontal-bars",label:"ct-label",labelGroup:"ct-labels",series:"ct-series",bar:"ct-bar",grid:"ct-grid",gridGroup:"ct-grids",gridBackground:"ct-grid-background",vertical:"ct-vertical",horizontal:"ct-horizontal",start:"ct-start",end:"ct-end"}};c.Bar=c.Base.extend({constructor:e,createChart:d})}(window,document,a),function(a,b,c){"use strict";function d(a,b,c){var d=b.x>a.x;return d&&"explode"===c||!d&&"implode"===c?"start":d&&"implode"===c||!d&&"explode"===c?"end":"middle"}function e(a){var b,e,f,h,i,j=c.normalizeData(this.data),k=[],l=a.startAngle;this.svg=c.createSvg(this.container,a.width,a.height,a.donut?a.classNames.chartDonut:a.classNames.chartPie),e=c.createChartRect(this.svg,a,g.padding),f=Math.min(e.width()/2,e.height()/2),i=a.total||j.normalized.series.reduce(function(a,b){return a+b},0);var m=c.quantity(a.donutWidth);"%"===m.unit&&(m.value*=f/100),f-=a.donut&&!a.donutSolid?m.value/2:0,h="outside"===a.labelPosition||a.donut&&!a.donutSolid?f:"center"===a.labelPosition?0:a.donutSolid?f-m.value/2:f/2,h+=a.labelOffset;var n={x:e.x1+e.width()/2,y:e.y2+e.height()/2},o=1===j.raw.series.filter(function(a){return a.hasOwnProperty("value")?0!==a.value:0!==a}).length;j.raw.series.forEach(function(a,b){k[b]=this.svg.elem("g",null,null)}.bind(this)),a.showLabel&&(b=this.svg.elem("g",null,null)),j.raw.series.forEach(function(e,g){if(0!==j.normalized.series[g]||!a.ignoreEmptyValues){k[g].attr({"ct:series-name":e.name}),k[g].addClass([a.classNames.series,e.className||a.classNames.series+"-"+c.alphaNumerate(g)].join(" "));var p=i>0?l+j.normalized.series[g]/i*360:0,q=Math.max(0,l-(0===g||o?0:.2));p-q>=359.99&&(p=q+359.99);var r,s,t,u=c.polarToCartesian(n.x,n.y,f,q),v=c.polarToCartesian(n.x,n.y,f,p),w=new c.Svg.Path(!a.donut||a.donutSolid).move(v.x,v.y).arc(f,f,0,p-l>180,0,u.x,u.y);a.donut?a.donutSolid&&(t=f-m.value,r=c.polarToCartesian(n.x,n.y,t,l-(0===g||o?0:.2)),s=c.polarToCartesian(n.x,n.y,t,p),w.line(r.x,r.y),w.arc(t,t,0,p-l>180,1,s.x,s.y)):w.line(n.x,n.y);var x=a.classNames.slicePie;a.donut&&(x=a.classNames.sliceDonut,a.donutSolid&&(x=a.classNames.sliceDonutSolid));var y=k[g].elem("path",{d:w.stringify()},x);if(y.attr({"ct:value":j.normalized.series[g],"ct:meta":c.serialize(e.meta)}),a.donut&&!a.donutSolid&&(y._node.style.strokeWidth=m.value+"px"),this.eventEmitter.emit("draw",{type:"slice",value:j.normalized.series[g],totalDataSum:i,index:g,meta:e.meta,series:e,group:k[g],element:y,path:w.clone(),center:n,radius:f,startAngle:l,endAngle:p}),a.showLabel){var z;z=1===j.raw.series.length?{x:n.x,y:n.y}:c.polarToCartesian(n.x,n.y,h,l+(p-l)/2);var A;A=j.normalized.labels&&!c.isFalseyButZero(j.normalized.labels[g])?j.normalized.labels[g]:j.normalized.series[g];var B=a.labelInterpolationFnc(A,g);if(B||0===B){var C=b.elem("text",{dx:z.x,dy:z.y,"text-anchor":d(n,z,a.labelDirection)},a.classNames.label).text(""+B);this.eventEmitter.emit("draw",{type:"label",index:g,group:b,element:C,text:""+B,x:z.x,y:z.y})}}l=p}}.bind(this)),this.eventEmitter.emit("created",{chartRect:e,svg:this.svg,options:a})}function f(a,b,d,e){c.Pie["super"].constructor.call(this,a,b,g,c.extend({},g,d),e)}var g={width:void 0,height:void 0,chartPadding:5,classNames:{chartPie:"ct-chart-pie",chartDonut:"ct-chart-donut",series:"ct-series",slicePie:"ct-slice-pie",sliceDonut:"ct-slice-donut",sliceDonutSolid:"ct-slice-donut-solid",label:"ct-label"},startAngle:0,total:void 0,donut:!1,donutSolid:!1,donutWidth:60,showLabel:!0,labelOffset:0,labelPosition:"inside",labelInterpolationFnc:c.noop,labelDirection:"neutral",reverseData:!1,ignoreEmptyValues:!1};c.Pie=c.Base.extend({constructor:f,createChart:e,determineAnchorPosition:d})}(window,document,a),a}); -//# sourceMappingURL=chartist.min.js.map diff --git a/static/js/bootstrap-datetimepicker.min.js b/static/js/bootstrap-datetimepicker.min.js deleted file mode 100644 index 724db76..0000000 --- a/static/js/bootstrap-datetimepicker.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(a){"use strict";if("function"==typeof define&&define.amd)define(["jquery","moment"],a);else if("object"==typeof exports)module.exports=a(require("jquery"),require("moment"));else{if("undefined"==typeof jQuery)throw"bootstrap-datetimepicker requires jQuery to be loaded first";if("undefined"==typeof moment)throw"bootstrap-datetimepicker requires Moment.js to be loaded first";a(jQuery,moment)}}(function(a,b){"use strict";if(!b)throw new Error("bootstrap-datetimepicker requires Moment.js to be loaded first");var c=function(c,d){var e,f,g,h,i,j,k,l={},m=!0,n=!1,o=!1,p=0,q=[{clsName:"days",navFnc:"M",navStep:1},{clsName:"months",navFnc:"y",navStep:1},{clsName:"years",navFnc:"y",navStep:10},{clsName:"decades",navFnc:"y",navStep:100}],r=["days","months","years","decades"],s=["top","bottom","auto"],t=["left","right","auto"],u=["default","top","bottom"],v={up:38,38:"up",down:40,40:"down",left:37,37:"left",right:39,39:"right",tab:9,9:"tab",escape:27,27:"escape",enter:13,13:"enter",pageUp:33,33:"pageUp",pageDown:34,34:"pageDown",shift:16,16:"shift",control:17,17:"control",space:32,32:"space",t:84,84:"t",delete:46,46:"delete"},w={},x=function(){return void 0!==b.tz&&void 0!==d.timeZone&&null!==d.timeZone&&""!==d.timeZone},y=function(a){var c;return c=void 0===a||null===a?b():b.isDate(a)||b.isMoment(a)?b(a):x()?b.tz(a,j,d.useStrict,d.timeZone):b(a,j,d.useStrict),x()&&c.tz(d.timeZone),c},z=function(a){if("string"!=typeof a||a.length>1)throw new TypeError("isEnabled expects a single character string parameter");switch(a){case"y":return i.indexOf("Y")!==-1;case"M":return i.indexOf("M")!==-1;case"d":return i.toLowerCase().indexOf("d")!==-1;case"h":case"H":return i.toLowerCase().indexOf("h")!==-1;case"m":return i.indexOf("m")!==-1;case"s":return i.indexOf("s")!==-1;default:return!1}},A=function(){return z("h")||z("m")||z("s")},B=function(){return z("y")||z("M")||z("d")},C=function(){var b=a("").append(a("").append(a("").append(a("").append(a("
    ").addClass("cw").text("#"));c.isBefore(f.clone().endOf("w"));)b.append(a("").addClass("dow").text(c.format("dd"))),c.add(1,"d");o.find(".datepicker-days thead").append(b)},N=function(a){return d.disabledDates[a.format("YYYY-MM-DD")]===!0},O=function(a){return d.enabledDates[a.format("YYYY-MM-DD")]===!0},P=function(a){return d.disabledHours[a.format("H")]===!0},Q=function(a){return d.enabledHours[a.format("H")]===!0},R=function(b,c){if(!b.isValid())return!1;if(d.disabledDates&&"d"===c&&N(b))return!1;if(d.enabledDates&&"d"===c&&!O(b))return!1;if(d.minDate&&b.isBefore(d.minDate,c))return!1;if(d.maxDate&&b.isAfter(d.maxDate,c))return!1;if(d.daysOfWeekDisabled&&"d"===c&&d.daysOfWeekDisabled.indexOf(b.day())!==-1)return!1;if(d.disabledHours&&("h"===c||"m"===c||"s"===c)&&P(b))return!1;if(d.enabledHours&&("h"===c||"m"===c||"s"===c)&&!Q(b))return!1;if(d.disabledTimeIntervals&&("h"===c||"m"===c||"s"===c)){var e=!1;if(a.each(d.disabledTimeIntervals,function(){if(b.isBetween(this[0],this[1]))return e=!0,!1}),e)return!1}return!0},S=function(){for(var b=[],c=f.clone().startOf("y").startOf("d");c.isSame(f,"y");)b.push(a("").attr("data-action","selectMonth").addClass("month").text(c.format("MMM"))),c.add(1,"M");o.find(".datepicker-months td").empty().append(b)},T=function(){var b=o.find(".datepicker-months"),c=b.find("th"),g=b.find("tbody").find("span");c.eq(0).find("span").attr("title",d.tooltips.prevYear),c.eq(1).attr("title",d.tooltips.selectYear),c.eq(2).find("span").attr("title",d.tooltips.nextYear),b.find(".disabled").removeClass("disabled"),R(f.clone().subtract(1,"y"),"y")||c.eq(0).addClass("disabled"),c.eq(1).text(f.year()),R(f.clone().add(1,"y"),"y")||c.eq(2).addClass("disabled"),g.removeClass("active"),e.isSame(f,"y")&&!m&&g.eq(e.month()).addClass("active"),g.each(function(b){R(f.clone().month(b),"M")||a(this).addClass("disabled")})},U=function(){var a=o.find(".datepicker-years"),b=a.find("th"),c=f.clone().subtract(5,"y"),g=f.clone().add(6,"y"),h="";for(b.eq(0).find("span").attr("title",d.tooltips.prevDecade),b.eq(1).attr("title",d.tooltips.selectDecade),b.eq(2).find("span").attr("title",d.tooltips.nextDecade),a.find(".disabled").removeClass("disabled"),d.minDate&&d.minDate.isAfter(c,"y")&&b.eq(0).addClass("disabled"),b.eq(1).text(c.year()+"-"+g.year()),d.maxDate&&d.maxDate.isBefore(g,"y")&&b.eq(2).addClass("disabled");!c.isAfter(g,"y");)h+=''+c.year()+"",c.add(1,"y");a.find("td").html(h)},V=function(){var a,c=o.find(".datepicker-decades"),g=c.find("th"),h=b({y:f.year()-f.year()%100-1}),i=h.clone().add(100,"y"),j=h.clone(),k=!1,l=!1,m="";for(g.eq(0).find("span").attr("title",d.tooltips.prevCentury),g.eq(2).find("span").attr("title",d.tooltips.nextCentury),c.find(".disabled").removeClass("disabled"),(h.isSame(b({y:1900}))||d.minDate&&d.minDate.isAfter(h,"y"))&&g.eq(0).addClass("disabled"),g.eq(1).text(h.year()+"-"+i.year()),(h.isSame(b({y:2e3}))||d.maxDate&&d.maxDate.isBefore(i,"y"))&&g.eq(2).addClass("disabled");!h.isAfter(i,"y");)a=h.year()+12,k=d.minDate&&d.minDate.isAfter(h,"y")&&d.minDate.year()<=a,l=d.maxDate&&d.maxDate.isAfter(h,"y")&&d.maxDate.year()<=a,m+=''+(h.year()+1)+" - "+(h.year()+12)+"",h.add(12,"y");m+="",c.find("td").html(m),g.eq(1).text(j.year()+1+"-"+h.year())},W=function(){var b,c,g,h=o.find(".datepicker-days"),i=h.find("th"),j=[],k=[];if(B()){for(i.eq(0).find("span").attr("title",d.tooltips.prevMonth),i.eq(1).attr("title",d.tooltips.selectMonth),i.eq(2).find("span").attr("title",d.tooltips.nextMonth),h.find(".disabled").removeClass("disabled"),i.eq(1).text(f.format(d.dayViewHeaderFormat)),R(f.clone().subtract(1,"M"),"M")||i.eq(0).addClass("disabled"),R(f.clone().add(1,"M"),"M")||i.eq(2).addClass("disabled"),b=f.clone().startOf("M").startOf("w").startOf("d"),g=0;g<42;g++)0===b.weekday()&&(c=a("
    '+b.week()+"'+b.date()+"
    '+c.format(h?"HH":"hh")+"
    '+c.format("mm")+"
    '+c.format("ss")+"
    ").addClass("prev").attr("data-action","previous").append(a("").addClass(d.icons.previous))).append(a("").addClass("picker-switch").attr("data-action","pickerSwitch").attr("colspan",d.calendarWeeks?"6":"5")).append(a("").addClass("next").attr("data-action","next").append(a("").addClass(d.icons.next)))),c=a("
    ").attr("colspan",d.calendarWeeks?"8":"7")));return[a("
    ").addClass("datepicker-days").append(a("").addClass("table-condensed").append(b).append(a(""))),a("
    ").addClass("datepicker-months").append(a("
    ").addClass("table-condensed").append(b.clone()).append(c.clone())),a("
    ").addClass("datepicker-years").append(a("
    ").addClass("table-condensed").append(b.clone()).append(c.clone())),a("
    ").addClass("datepicker-decades").append(a("
    ").addClass("table-condensed").append(b.clone()).append(c.clone()))]},D=function(){var b=a(""),c=a(""),e=a("");return z("h")&&(b.append(a("
    ").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementHour}).addClass("btn").attr("data-action","incrementHours").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-hour").attr({"data-time-component":"hours",title:d.tooltips.pickHour}).attr("data-action","showHours"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementHour}).addClass("btn").attr("data-action","decrementHours").append(a("").addClass(d.icons.down))))),z("m")&&(z("h")&&(b.append(a("").addClass("separator")),c.append(a("").addClass("separator").html(":")),e.append(a("").addClass("separator"))),b.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementMinute}).addClass("btn").attr("data-action","incrementMinutes").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-minute").attr({"data-time-component":"minutes",title:d.tooltips.pickMinute}).attr("data-action","showMinutes"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementMinute}).addClass("btn").attr("data-action","decrementMinutes").append(a("").addClass(d.icons.down))))),z("s")&&(z("m")&&(b.append(a("").addClass("separator")),c.append(a("").addClass("separator").html(":")),e.append(a("").addClass("separator"))),b.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementSecond}).addClass("btn").attr("data-action","incrementSeconds").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-second").attr({"data-time-component":"seconds",title:d.tooltips.pickSecond}).attr("data-action","showSeconds"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementSecond}).addClass("btn").attr("data-action","decrementSeconds").append(a("").addClass(d.icons.down))))),h||(b.append(a("").addClass("separator")),c.append(a("").append(a("").addClass("separator"))),a("
    ").addClass("timepicker-picker").append(a("").addClass("table-condensed").append([b,c,e]))},E=function(){var b=a("
    ").addClass("timepicker-hours").append(a("
    ").addClass("table-condensed")),c=a("
    ").addClass("timepicker-minutes").append(a("
    ").addClass("table-condensed")),d=a("
    ").addClass("timepicker-seconds").append(a("
    ").addClass("table-condensed")),e=[D()];return z("h")&&e.push(b),z("m")&&e.push(c),z("s")&&e.push(d),e},F=function(){var b=[];return d.showTodayButton&&b.push(a("
    ").append(a("").attr({"data-action":"today",title:d.tooltips.today}).append(a("").addClass(d.icons.today)))),!d.sideBySide&&B()&&A()&&b.push(a("").append(a("").attr({"data-action":"togglePicker",title:d.tooltips.selectTime}).append(a("").addClass(d.icons.time)))),d.showClear&&b.push(a("").append(a("").attr({"data-action":"clear",title:d.tooltips.clear}).append(a("").addClass(d.icons.clear)))),d.showClose&&b.push(a("").append(a("").attr({"data-action":"close",title:d.tooltips.close}).append(a("").addClass(d.icons.close)))),a("").addClass("table-condensed").append(a("").append(a("").append(b)))},G=function(){var b=a("
    ").addClass("bootstrap-datetimepicker-widget dropdown-menu"),c=a("
    ").addClass("datepicker").append(C()),e=a("
    ").addClass("timepicker").append(E()),f=a("
      ").addClass("list-unstyled"),g=a("
    • ").addClass("picker-switch"+(d.collapse?" accordion-toggle":"")).append(F());return d.inline&&b.removeClass("dropdown-menu"),h&&b.addClass("usetwentyfour"),z("s")&&!h&&b.addClass("wider"),d.sideBySide&&B()&&A()?(b.addClass("timepicker-sbs"),"top"===d.toolbarPlacement&&b.append(g),b.append(a("
      ").addClass("row").append(c.addClass("col-md-6")).append(e.addClass("col-md-6"))),"bottom"===d.toolbarPlacement&&b.append(g),b):("top"===d.toolbarPlacement&&f.append(g),B()&&f.append(a("
    • ").addClass(d.collapse&&A()?"collapse in":"").append(c)),"default"===d.toolbarPlacement&&f.append(g),A()&&f.append(a("
    • ").addClass(d.collapse&&B()?"collapse":"").append(e)),"bottom"===d.toolbarPlacement&&f.append(g),b.append(f))},H=function(){var b,e={};return b=c.is("input")||d.inline?c.data():c.find("input").data(),b.dateOptions&&b.dateOptions instanceof Object&&(e=a.extend(!0,e,b.dateOptions)),a.each(d,function(a){var c="date"+a.charAt(0).toUpperCase()+a.slice(1);void 0!==b[c]&&(e[a]=b[c])}),e},I=function(){var b,e=(n||c).position(),f=(n||c).offset(),g=d.widgetPositioning.vertical,h=d.widgetPositioning.horizontal;if(d.widgetParent)b=d.widgetParent.append(o);else if(c.is("input"))b=c.after(o).parent();else{if(d.inline)return void(b=c.append(o));b=c,c.children().first().after(o)}if("auto"===g&&(g=f.top+1.5*o.height()>=a(window).height()+a(window).scrollTop()&&o.height()+c.outerHeight()a(window).width()?"right":"left"),"top"===g?o.addClass("top").removeClass("bottom"):o.addClass("bottom").removeClass("top"),"right"===h?o.addClass("pull-right"):o.removeClass("pull-right"),"static"===b.css("position")&&(b=b.parents().filter(function(){return"static"!==a(this).css("position")}).first()),0===b.length)throw new Error("datetimepicker component should be placed within a non-static positioned container");o.css({top:"top"===g?"auto":e.top+c.outerHeight(),bottom:"top"===g?b.outerHeight()-(b===c?0:e.top):"auto",left:"left"===h?b===c?0:e.left:"auto",right:"left"===h?"auto":b.outerWidth()-c.outerWidth()-(b===c?0:e.left)})},J=function(a){"dp.change"===a.type&&(a.date&&a.date.isSame(a.oldDate)||!a.date&&!a.oldDate)||c.trigger(a)},K=function(a){"y"===a&&(a="YYYY"),J({type:"dp.update",change:a,viewDate:f.clone()})},L=function(a){o&&(a&&(k=Math.max(p,Math.min(3,k+a))),o.find(".datepicker > div").hide().filter(".datepicker-"+q[k].clsName).show())},M=function(){var b=a("
    "),c=f.clone().startOf("w").startOf("d");for(d.calendarWeeks===!0&&b.append(a(""),d.calendarWeeks&&c.append('"),j.push(c)),k=["day"],b.isBefore(f,"M")&&k.push("old"),b.isAfter(f,"M")&&k.push("new"),b.isSame(e,"d")&&!m&&k.push("active"),R(b,"d")||k.push("disabled"),b.isSame(y(),"d")&&k.push("today"),0!==b.day()&&6!==b.day()||k.push("weekend"),J({type:"dp.classify",date:b,classNames:k}),c.append('"),b.add(1,"d");h.find("tbody").empty().append(j),T(),U(),V()}},X=function(){var b=o.find(".timepicker-hours table"),c=f.clone().startOf("d"),d=[],e=a("");for(f.hour()>11&&!h&&c.hour(12);c.isSame(f,"d")&&(h||f.hour()<12&&c.hour()<12||f.hour()>11);)c.hour()%4===0&&(e=a(""),d.push(e)),e.append('"),c.add(1,"h");b.empty().append(d)},Y=function(){for(var b=o.find(".timepicker-minutes table"),c=f.clone().startOf("h"),e=[],g=a(""),h=1===d.stepping?5:d.stepping;f.isSame(c,"h");)c.minute()%(4*h)===0&&(g=a(""),e.push(g)),g.append('"),c.add(h,"m");b.empty().append(e)},Z=function(){for(var b=o.find(".timepicker-seconds table"),c=f.clone().startOf("m"),d=[],e=a("");f.isSame(c,"m");)c.second()%20===0&&(e=a(""),d.push(e)),e.append('"),c.add(5,"s");b.empty().append(d)},$=function(){var a,b,c=o.find(".timepicker span[data-time-component]");h||(a=o.find(".timepicker [data-action=togglePeriod]"),b=e.clone().add(e.hours()>=12?-12:12,"h"),a.text(e.format("A")),R(b,"h")?a.removeClass("disabled"):a.addClass("disabled")),c.filter("[data-time-component=hours]").text(e.format(h?"HH":"hh")),c.filter("[data-time-component=minutes]").text(e.format("mm")),c.filter("[data-time-component=seconds]").text(e.format("ss")),X(),Y(),Z()},_=function(){o&&(W(),$())},aa=function(a){var b=m?null:e;if(!a)return m=!0,g.val(""),c.data("date",""),J({type:"dp.change",date:!1,oldDate:b}),void _();if(a=a.clone().locale(d.locale),x()&&a.tz(d.timeZone),1!==d.stepping)for(a.minutes(Math.round(a.minutes()/d.stepping)*d.stepping).seconds(0);d.minDate&&a.isBefore(d.minDate);)a.add(d.stepping,"minutes");R(a)?(e=a,f=e.clone(),g.val(e.format(i)),c.data("date",e.format(i)),m=!1,_(),J({type:"dp.change",date:e.clone(),oldDate:b})):(d.keepInvalid?J({type:"dp.change",date:a,oldDate:b}):g.val(m?"":e.format(i)),J({type:"dp.error",date:a,oldDate:b}))},ba=function(){var b=!1;return o?(o.find(".collapse").each(function(){var c=a(this).data("collapse");return!c||!c.transitioning||(b=!0,!1)}),b?l:(n&&n.hasClass("btn")&&n.toggleClass("active"),o.hide(),a(window).off("resize",I),o.off("click","[data-action]"),o.off("mousedown",!1),o.remove(),o=!1,J({type:"dp.hide",date:e.clone()}),g.blur(),f=e.clone(),l)):l},ca=function(){aa(null)},da=function(a){return void 0===d.parseInputDate?(!b.isMoment(a)||a instanceof Date)&&(a=y(a)):a=d.parseInputDate(a),a},ea={next:function(){var a=q[k].navFnc;f.add(q[k].navStep,a),W(),K(a)},previous:function(){var a=q[k].navFnc;f.subtract(q[k].navStep,a),W(),K(a)},pickerSwitch:function(){L(1)},selectMonth:function(b){var c=a(b.target).closest("tbody").find("span").index(a(b.target));f.month(c),k===p?(aa(e.clone().year(f.year()).month(f.month())),d.inline||ba()):(L(-1),W()),K("M")},selectYear:function(b){var c=parseInt(a(b.target).text(),10)||0;f.year(c),k===p?(aa(e.clone().year(f.year())),d.inline||ba()):(L(-1),W()),K("YYYY")},selectDecade:function(b){var c=parseInt(a(b.target).data("selection"),10)||0;f.year(c),k===p?(aa(e.clone().year(f.year())),d.inline||ba()):(L(-1),W()),K("YYYY")},selectDay:function(b){var c=f.clone();a(b.target).is(".old")&&c.subtract(1,"M"),a(b.target).is(".new")&&c.add(1,"M"),aa(c.date(parseInt(a(b.target).text(),10))),A()||d.keepOpen||d.inline||ba()},incrementHours:function(){var a=e.clone().add(1,"h");R(a,"h")&&aa(a)},incrementMinutes:function(){var a=e.clone().add(d.stepping,"m");R(a,"m")&&aa(a)},incrementSeconds:function(){var a=e.clone().add(1,"s");R(a,"s")&&aa(a)},decrementHours:function(){var a=e.clone().subtract(1,"h");R(a,"h")&&aa(a)},decrementMinutes:function(){var a=e.clone().subtract(d.stepping,"m");R(a,"m")&&aa(a)},decrementSeconds:function(){var a=e.clone().subtract(1,"s");R(a,"s")&&aa(a)},togglePeriod:function(){aa(e.clone().add(e.hours()>=12?-12:12,"h"))},togglePicker:function(b){var c,e=a(b.target),f=e.closest("ul"),g=f.find(".in"),h=f.find(".collapse:not(.in)");if(g&&g.length){if(c=g.data("collapse"),c&&c.transitioning)return;g.collapse?(g.collapse("hide"),h.collapse("show")):(g.removeClass("in"),h.addClass("in")),e.is("span")?e.toggleClass(d.icons.time+" "+d.icons.date):e.find("span").toggleClass(d.icons.time+" "+d.icons.date)}},showPicker:function(){o.find(".timepicker > div:not(.timepicker-picker)").hide(),o.find(".timepicker .timepicker-picker").show()},showHours:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-hours").show()},showMinutes:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-seconds").show()},selectHour:function(b){var c=parseInt(a(b.target).text(),10);h||(e.hours()>=12?12!==c&&(c+=12):12===c&&(c=0)),aa(e.clone().hours(c)),ea.showPicker.call(l)},selectMinute:function(b){aa(e.clone().minutes(parseInt(a(b.target).text(),10))),ea.showPicker.call(l)},selectSecond:function(b){aa(e.clone().seconds(parseInt(a(b.target).text(),10))),ea.showPicker.call(l)},clear:ca,today:function(){var a=y();R(a,"d")&&aa(a)},close:ba},fa=function(b){return!a(b.currentTarget).is(".disabled")&&(ea[a(b.currentTarget).data("action")].apply(l,arguments),!1)},ga=function(){var b,c={year:function(a){return a.month(0).date(1).hours(0).seconds(0).minutes(0)},month:function(a){return a.date(1).hours(0).seconds(0).minutes(0)},day:function(a){return a.hours(0).seconds(0).minutes(0)},hour:function(a){return a.seconds(0).minutes(0)},minute:function(a){return a.seconds(0)}};return g.prop("disabled")||!d.ignoreReadonly&&g.prop("readonly")||o?l:(void 0!==g.val()&&0!==g.val().trim().length?aa(da(g.val().trim())):m&&d.useCurrent&&(d.inline||g.is("input")&&0===g.val().trim().length)&&(b=y(),"string"==typeof d.useCurrent&&(b=c[d.useCurrent](b)),aa(b)),o=G(),M(),S(),o.find(".timepicker-hours").hide(),o.find(".timepicker-minutes").hide(),o.find(".timepicker-seconds").hide(),_(),L(),a(window).on("resize",I),o.on("click","[data-action]",fa),o.on("mousedown",!1),n&&n.hasClass("btn")&&n.toggleClass("active"),I(),o.show(),d.focusOnShow&&!g.is(":focus")&&g.focus(),J({type:"dp.show"}),l)},ha=function(){return o?ba():ga()},ia=function(a){var b,c,e,f,g=null,h=[],i={},j=a.which,k="p";w[j]=k;for(b in w)w.hasOwnProperty(b)&&w[b]===k&&(h.push(b),parseInt(b,10)!==j&&(i[b]=!0));for(b in d.keyBinds)if(d.keyBinds.hasOwnProperty(b)&&"function"==typeof d.keyBinds[b]&&(e=b.split(" "),e.length===h.length&&v[j]===e[e.length-1])){for(f=!0,c=e.length-2;c>=0;c--)if(!(v[e[c]]in i)){f=!1;break}if(f){g=d.keyBinds[b];break}}g&&(g.call(l,o),a.stopPropagation(),a.preventDefault())},ja=function(a){w[a.which]="r",a.stopPropagation(),a.preventDefault()},ka=function(b){var c=a(b.target).val().trim(),d=c?da(c):null;return aa(d),b.stopImmediatePropagation(),!1},la=function(){g.on({change:ka,blur:d.debug?"":ba,keydown:ia,keyup:ja,focus:d.allowInputToggle?ga:""}),c.is("input")?g.on({focus:ga}):n&&(n.on("click",ha),n.on("mousedown",!1))},ma=function(){g.off({change:ka,blur:blur,keydown:ia,keyup:ja,focus:d.allowInputToggle?ba:""}),c.is("input")?g.off({focus:ga}):n&&(n.off("click",ha),n.off("mousedown",!1))},na=function(b){var c={};return a.each(b,function(){var a=da(this);a.isValid()&&(c[a.format("YYYY-MM-DD")]=!0)}),!!Object.keys(c).length&&c},oa=function(b){var c={};return a.each(b,function(){c[this]=!0}),!!Object.keys(c).length&&c},pa=function(){var a=d.format||"L LT";i=a.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(a){var b=e.localeData().longDateFormat(a)||a;return b.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(a){return e.localeData().longDateFormat(a)||a})}),j=d.extraFormats?d.extraFormats.slice():[],j.indexOf(a)<0&&j.indexOf(i)<0&&j.push(i),h=i.toLowerCase().indexOf("a")<1&&i.replace(/\[.*?\]/g,"").indexOf("h")<1,z("y")&&(p=2),z("M")&&(p=1),z("d")&&(p=0),k=Math.max(p,k),m||aa(e)};if(l.destroy=function(){ba(),ma(),c.removeData("DateTimePicker"),c.removeData("date")},l.toggle=ha,l.show=ga,l.hide=ba,l.disable=function(){return ba(),n&&n.hasClass("btn")&&n.addClass("disabled"),g.prop("disabled",!0),l},l.enable=function(){return n&&n.hasClass("btn")&&n.removeClass("disabled"),g.prop("disabled",!1),l},l.ignoreReadonly=function(a){if(0===arguments.length)return d.ignoreReadonly;if("boolean"!=typeof a)throw new TypeError("ignoreReadonly () expects a boolean parameter");return d.ignoreReadonly=a,l},l.options=function(b){if(0===arguments.length)return a.extend(!0,{},d);if(!(b instanceof Object))throw new TypeError("options() options parameter should be an object");return a.extend(!0,d,b),a.each(d,function(a,b){if(void 0===l[a])throw new TypeError("option "+a+" is not recognized!");l[a](b)}),l},l.date=function(a){if(0===arguments.length)return m?null:e.clone();if(!(null===a||"string"==typeof a||b.isMoment(a)||a instanceof Date))throw new TypeError("date() parameter must be one of [null, string, moment or Date]");return aa(null===a?null:da(a)),l},l.format=function(a){if(0===arguments.length)return d.format;if("string"!=typeof a&&("boolean"!=typeof a||a!==!1))throw new TypeError("format() expects a string or boolean:false parameter "+a);return d.format=a,i&&pa(),l},l.timeZone=function(a){if(0===arguments.length)return d.timeZone;if("string"!=typeof a)throw new TypeError("newZone() expects a string parameter");return d.timeZone=a,l},l.dayViewHeaderFormat=function(a){if(0===arguments.length)return d.dayViewHeaderFormat;if("string"!=typeof a)throw new TypeError("dayViewHeaderFormat() expects a string parameter");return d.dayViewHeaderFormat=a,l},l.extraFormats=function(a){if(0===arguments.length)return d.extraFormats;if(a!==!1&&!(a instanceof Array))throw new TypeError("extraFormats() expects an array or false parameter");return d.extraFormats=a,j&&pa(),l},l.disabledDates=function(b){if(0===arguments.length)return d.disabledDates?a.extend({},d.disabledDates):d.disabledDates;if(!b)return d.disabledDates=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledDates() expects an array parameter");return d.disabledDates=na(b),d.enabledDates=!1,_(),l},l.enabledDates=function(b){if(0===arguments.length)return d.enabledDates?a.extend({},d.enabledDates):d.enabledDates;if(!b)return d.enabledDates=!1,_(),l;if(!(b instanceof Array))throw new TypeError("enabledDates() expects an array parameter");return d.enabledDates=na(b),d.disabledDates=!1,_(),l},l.daysOfWeekDisabled=function(a){if(0===arguments.length)return d.daysOfWeekDisabled.splice(0);if("boolean"==typeof a&&!a)return d.daysOfWeekDisabled=!1,_(),l;if(!(a instanceof Array))throw new TypeError("daysOfWeekDisabled() expects an array parameter");if(d.daysOfWeekDisabled=a.reduce(function(a,b){return b=parseInt(b,10),b>6||b<0||isNaN(b)?a:(a.indexOf(b)===-1&&a.push(b),a)},[]).sort(),d.useCurrent&&!d.keepInvalid){for(var b=0;!R(e,"d");){if(e.add(1,"d"),31===b)throw"Tried 31 times to find a valid date";b++}aa(e)}return _(),l},l.maxDate=function(a){if(0===arguments.length)return d.maxDate?d.maxDate.clone():d.maxDate;if("boolean"==typeof a&&a===!1)return d.maxDate=!1,_(),l;"string"==typeof a&&("now"!==a&&"moment"!==a||(a=y()));var b=da(a);if(!b.isValid())throw new TypeError("maxDate() Could not parse date parameter: "+a);if(d.minDate&&b.isBefore(d.minDate))throw new TypeError("maxDate() date parameter is before options.minDate: "+b.format(i));return d.maxDate=b,d.useCurrent&&!d.keepInvalid&&e.isAfter(a)&&aa(d.maxDate),f.isAfter(b)&&(f=b.clone().subtract(d.stepping,"m")),_(),l},l.minDate=function(a){if(0===arguments.length)return d.minDate?d.minDate.clone():d.minDate;if("boolean"==typeof a&&a===!1)return d.minDate=!1,_(),l;"string"==typeof a&&("now"!==a&&"moment"!==a||(a=y()));var b=da(a);if(!b.isValid())throw new TypeError("minDate() Could not parse date parameter: "+a);if(d.maxDate&&b.isAfter(d.maxDate))throw new TypeError("minDate() date parameter is after options.maxDate: "+b.format(i));return d.minDate=b,d.useCurrent&&!d.keepInvalid&&e.isBefore(a)&&aa(d.minDate),f.isBefore(b)&&(f=b.clone().add(d.stepping,"m")),_(),l},l.defaultDate=function(a){if(0===arguments.length)return d.defaultDate?d.defaultDate.clone():d.defaultDate;if(!a)return d.defaultDate=!1,l;"string"==typeof a&&(a="now"===a||"moment"===a?y():y(a));var b=da(a);if(!b.isValid())throw new TypeError("defaultDate() Could not parse date parameter: "+a);if(!R(b))throw new TypeError("defaultDate() date passed is invalid according to component setup validations");return d.defaultDate=b,(d.defaultDate&&d.inline||""===g.val().trim())&&aa(d.defaultDate),l},l.locale=function(a){if(0===arguments.length)return d.locale;if(!b.localeData(a))throw new TypeError("locale() locale "+a+" is not loaded from moment locales!");return d.locale=a,e.locale(d.locale),f.locale(d.locale),i&&pa(),o&&(ba(),ga()),l},l.stepping=function(a){return 0===arguments.length?d.stepping:(a=parseInt(a,10),(isNaN(a)||a<1)&&(a=1),d.stepping=a,l)},l.useCurrent=function(a){var b=["year","month","day","hour","minute"];if(0===arguments.length)return d.useCurrent;if("boolean"!=typeof a&&"string"!=typeof a)throw new TypeError("useCurrent() expects a boolean or string parameter");if("string"==typeof a&&b.indexOf(a.toLowerCase())===-1)throw new TypeError("useCurrent() expects a string parameter of "+b.join(", "));return d.useCurrent=a,l},l.collapse=function(a){if(0===arguments.length)return d.collapse;if("boolean"!=typeof a)throw new TypeError("collapse() expects a boolean parameter");return d.collapse===a?l:(d.collapse=a,o&&(ba(),ga()),l)},l.icons=function(b){if(0===arguments.length)return a.extend({},d.icons);if(!(b instanceof Object))throw new TypeError("icons() expects parameter to be an Object");return a.extend(d.icons,b),o&&(ba(),ga()),l},l.tooltips=function(b){if(0===arguments.length)return a.extend({},d.tooltips);if(!(b instanceof Object))throw new TypeError("tooltips() expects parameter to be an Object");return a.extend(d.tooltips,b),o&&(ba(),ga()),l},l.useStrict=function(a){if(0===arguments.length)return d.useStrict;if("boolean"!=typeof a)throw new TypeError("useStrict() expects a boolean parameter");return d.useStrict=a,l},l.sideBySide=function(a){if(0===arguments.length)return d.sideBySide;if("boolean"!=typeof a)throw new TypeError("sideBySide() expects a boolean parameter");return d.sideBySide=a,o&&(ba(),ga()),l},l.viewMode=function(a){if(0===arguments.length)return d.viewMode;if("string"!=typeof a)throw new TypeError("viewMode() expects a string parameter");if(r.indexOf(a)===-1)throw new TypeError("viewMode() parameter must be one of ("+r.join(", ")+") value");return d.viewMode=a,k=Math.max(r.indexOf(a),p),L(),l},l.toolbarPlacement=function(a){if(0===arguments.length)return d.toolbarPlacement;if("string"!=typeof a)throw new TypeError("toolbarPlacement() expects a string parameter");if(u.indexOf(a)===-1)throw new TypeError("toolbarPlacement() parameter must be one of ("+u.join(", ")+") value");return d.toolbarPlacement=a,o&&(ba(),ga()),l},l.widgetPositioning=function(b){if(0===arguments.length)return a.extend({},d.widgetPositioning);if("[object Object]"!=={}.toString.call(b))throw new TypeError("widgetPositioning() expects an object variable");if(b.horizontal){if("string"!=typeof b.horizontal)throw new TypeError("widgetPositioning() horizontal variable must be a string");if(b.horizontal=b.horizontal.toLowerCase(),t.indexOf(b.horizontal)===-1)throw new TypeError("widgetPositioning() expects horizontal parameter to be one of ("+t.join(", ")+")");d.widgetPositioning.horizontal=b.horizontal}if(b.vertical){if("string"!=typeof b.vertical)throw new TypeError("widgetPositioning() vertical variable must be a string");if(b.vertical=b.vertical.toLowerCase(),s.indexOf(b.vertical)===-1)throw new TypeError("widgetPositioning() expects vertical parameter to be one of ("+s.join(", ")+")");d.widgetPositioning.vertical=b.vertical}return _(),l},l.calendarWeeks=function(a){if(0===arguments.length)return d.calendarWeeks;if("boolean"!=typeof a)throw new TypeError("calendarWeeks() expects parameter to be a boolean value");return d.calendarWeeks=a,_(),l},l.showTodayButton=function(a){if(0===arguments.length)return d.showTodayButton;if("boolean"!=typeof a)throw new TypeError("showTodayButton() expects a boolean parameter");return d.showTodayButton=a,o&&(ba(),ga()),l},l.showClear=function(a){if(0===arguments.length)return d.showClear;if("boolean"!=typeof a)throw new TypeError("showClear() expects a boolean parameter");return d.showClear=a,o&&(ba(),ga()),l},l.widgetParent=function(b){if(0===arguments.length)return d.widgetParent;if("string"==typeof b&&(b=a(b)),null!==b&&"string"!=typeof b&&!(b instanceof a))throw new TypeError("widgetParent() expects a string or a jQuery object parameter");return d.widgetParent=b,o&&(ba(),ga()),l},l.keepOpen=function(a){if(0===arguments.length)return d.keepOpen;if("boolean"!=typeof a)throw new TypeError("keepOpen() expects a boolean parameter");return d.keepOpen=a,l},l.focusOnShow=function(a){if(0===arguments.length)return d.focusOnShow;if("boolean"!=typeof a)throw new TypeError("focusOnShow() expects a boolean parameter");return d.focusOnShow=a,l},l.inline=function(a){if(0===arguments.length)return d.inline;if("boolean"!=typeof a)throw new TypeError("inline() expects a boolean parameter");return d.inline=a,l},l.clear=function(){return ca(),l},l.keyBinds=function(a){return 0===arguments.length?d.keyBinds:(d.keyBinds=a,l)},l.getMoment=function(a){return y(a)},l.debug=function(a){if("boolean"!=typeof a)throw new TypeError("debug() expects a boolean parameter");return d.debug=a,l},l.allowInputToggle=function(a){if(0===arguments.length)return d.allowInputToggle;if("boolean"!=typeof a)throw new TypeError("allowInputToggle() expects a boolean parameter");return d.allowInputToggle=a,l},l.showClose=function(a){if(0===arguments.length)return d.showClose;if("boolean"!=typeof a)throw new TypeError("showClose() expects a boolean parameter");return d.showClose=a,l},l.keepInvalid=function(a){if(0===arguments.length)return d.keepInvalid;if("boolean"!=typeof a)throw new TypeError("keepInvalid() expects a boolean parameter"); -return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)return d.datepickerInput;if("string"!=typeof a)throw new TypeError("datepickerInput() expects a string parameter");return d.datepickerInput=a,l},l.parseInputDate=function(a){if(0===arguments.length)return d.parseInputDate;if("function"!=typeof a)throw new TypeError("parseInputDate() sholud be as function");return d.parseInputDate=a,l},l.disabledTimeIntervals=function(b){if(0===arguments.length)return d.disabledTimeIntervals?a.extend({},d.disabledTimeIntervals):d.disabledTimeIntervals;if(!b)return d.disabledTimeIntervals=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledTimeIntervals() expects an array parameter");return d.disabledTimeIntervals=b,_(),l},l.disabledHours=function(b){if(0===arguments.length)return d.disabledHours?a.extend({},d.disabledHours):d.disabledHours;if(!b)return d.disabledHours=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledHours() expects an array parameter");if(d.disabledHours=oa(b),d.enabledHours=!1,d.useCurrent&&!d.keepInvalid){for(var c=0;!R(e,"h");){if(e.add(1,"h"),24===c)throw"Tried 24 times to find a valid date";c++}aa(e)}return _(),l},l.enabledHours=function(b){if(0===arguments.length)return d.enabledHours?a.extend({},d.enabledHours):d.enabledHours;if(!b)return d.enabledHours=!1,_(),l;if(!(b instanceof Array))throw new TypeError("enabledHours() expects an array parameter");if(d.enabledHours=oa(b),d.disabledHours=!1,d.useCurrent&&!d.keepInvalid){for(var c=0;!R(e,"h");){if(e.add(1,"h"),24===c)throw"Tried 24 times to find a valid date";c++}aa(e)}return _(),l},l.viewDate=function(a){if(0===arguments.length)return f.clone();if(!a)return f=e.clone(),l;if(!("string"==typeof a||b.isMoment(a)||a instanceof Date))throw new TypeError("viewDate() parameter must be one of [string, moment or Date]");return f=da(a),K(),l},c.is("input"))g=c;else if(g=c.find(d.datepickerInput),0===g.length)g=c.find("input");else if(!g.is("input"))throw new Error('CSS class "'+d.datepickerInput+'" cannot be applied to non input element');if(c.hasClass("input-group")&&(n=0===c.find(".datepickerbutton").length?c.find(".input-group-addon"):c.find(".datepickerbutton")),!d.inline&&!g.is("input"))throw new Error("Could not initialize DateTimePicker without an input element");return e=y(),f=e.clone(),a.extend(!0,d,H()),l.options(d),pa(),la(),g.prop("disabled")&&l.disable(),g.is("input")&&0!==g.val().trim().length?aa(da(g.val().trim())):d.defaultDate&&void 0===g.attr("placeholder")&&aa(d.defaultDate),d.inline&&ga(),l};return a.fn.datetimepicker=function(b){b=b||{};var d,e=Array.prototype.slice.call(arguments,1),f=!0,g=["destroy","hide","show","toggle"];if("object"==typeof b)return this.each(function(){var d,e=a(this);e.data("DateTimePicker")||(d=a.extend(!0,{},a.fn.datetimepicker.defaults,b),e.data("DateTimePicker",c(e,d)))});if("string"==typeof b)return this.each(function(){var c=a(this),g=c.data("DateTimePicker");if(!g)throw new Error('bootstrap-datetimepicker("'+b+'") method was called on an element that is not using DateTimePicker');d=g[b].apply(g,e),f=d===g}),f||a.inArray(b,g)>-1?this:d;throw new TypeError("Invalid arguments for DateTimePicker: "+b)},a.fn.datetimepicker.defaults={timeZone:"",format:!1,dayViewHeaderFormat:"MMMM YYYY",extraFormats:!1,stepping:1,minDate:!1,maxDate:!1,useCurrent:!0,collapse:!0,locale:b.locale(),defaultDate:!1,disabledDates:!1,enabledDates:!1,icons:{time:"glyphicon glyphicon-time",date:"glyphicon glyphicon-calendar",up:"glyphicon glyphicon-chevron-up",down:"glyphicon glyphicon-chevron-down",previous:"glyphicon glyphicon-chevron-left",next:"glyphicon glyphicon-chevron-right",today:"glyphicon glyphicon-screenshot",clear:"glyphicon glyphicon-trash",close:"glyphicon glyphicon-remove"},tooltips:{today:"Go to today",clear:"Clear selection",close:"Close the picker",selectMonth:"Select Month",prevMonth:"Previous Month",nextMonth:"Next Month",selectYear:"Select Year",prevYear:"Previous Year",nextYear:"Next Year",selectDecade:"Select Decade",prevDecade:"Previous Decade",nextDecade:"Next Decade",prevCentury:"Previous Century",nextCentury:"Next Century",pickHour:"Pick Hour",incrementHour:"Increment Hour",decrementHour:"Decrement Hour",pickMinute:"Pick Minute",incrementMinute:"Increment Minute",decrementMinute:"Decrement Minute",pickSecond:"Pick Second",incrementSecond:"Increment Second",decrementSecond:"Decrement Second",togglePeriod:"Toggle Period",selectTime:"Select Time"},useStrict:!1,sideBySide:!1,daysOfWeekDisabled:!1,calendarWeeks:!1,viewMode:"days",toolbarPlacement:"default",showTodayButton:!1,showClear:!1,showClose:!1,widgetPositioning:{horizontal:"auto",vertical:"auto"},widgetParent:null,ignoreReadonly:!1,keepOpen:!1,focusOnShow:!0,inline:!1,keepInvalid:!1,datepickerInput:".datepickerinput",keyBinds:{up:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().subtract(7,"d")):this.date(b.clone().add(this.stepping(),"m"))}},down:function(a){if(!a)return void this.show();var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().add(7,"d")):this.date(b.clone().subtract(this.stepping(),"m"))},"control up":function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().subtract(1,"y")):this.date(b.clone().add(1,"h"))}},"control down":function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().add(1,"y")):this.date(b.clone().subtract(1,"h"))}},left:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().subtract(1,"d"))}},right:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().add(1,"d"))}},pageUp:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().subtract(1,"M"))}},pageDown:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().add(1,"M"))}},enter:function(){this.hide()},escape:function(){this.hide()},"control space":function(a){a&&a.find(".timepicker").is(":visible")&&a.find('.btn[data-action="togglePeriod"]').click()},t:function(){this.date(this.getMoment())},delete:function(){this.clear()}},debug:!1,allowInputToggle:!1,disabledTimeIntervals:!1,disabledHours:!1,enabledHours:!1,viewDate:!1},a.fn.datetimepicker}); \ No newline at end of file diff --git a/static/js/chartist.min.js b/static/js/chartist.min.js new file mode 100644 index 0000000..b54d35a --- /dev/null +++ b/static/js/chartist.min.js @@ -0,0 +1,10 @@ +/* Chartist.js 0.11.0 + * Copyright © 2017 Gion Kunz + * Free to use under either the WTFPL license or the MIT license. + * https://raw.githubusercontent.com/gionkunz/chartist-js/master/LICENSE-WTFPL + * https://raw.githubusercontent.com/gionkunz/chartist-js/master/LICENSE-MIT + */ + +!function(a,b){"function"==typeof define&&define.amd?define("Chartist",[],function(){return a.Chartist=b()}):"object"==typeof module&&module.exports?module.exports=b():a.Chartist=b()}(this,function(){var a={version:"0.11.0"};return function(a,b,c){"use strict";c.namespaces={svg:"http://www.w3.org/2000/svg",xmlns:"http://www.w3.org/2000/xmlns/",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",ct:"http://gionkunz.github.com/chartist-js/ct"},c.noop=function(a){return a},c.alphaNumerate=function(a){return String.fromCharCode(97+a%26)},c.extend=function(a){var b,d,e;for(a=a||{},b=1;b":">",'"':""","'":"'"},c.serialize=function(a){return null===a||void 0===a?a:("number"==typeof a?a=""+a:"object"==typeof a&&(a=JSON.stringify({data:a})),Object.keys(c.escapingMap).reduce(function(a,b){return c.replaceAll(a,b,c.escapingMap[b])},a))},c.deserialize=function(a){if("string"!=typeof a)return a;a=Object.keys(c.escapingMap).reduce(function(a,b){return c.replaceAll(a,c.escapingMap[b],b)},a);try{a=JSON.parse(a),a=void 0!==a.data?a.data:a}catch(b){}return a},c.createSvg=function(a,b,d,e){var f;return b=b||"100%",d=d||"100%",Array.prototype.slice.call(a.querySelectorAll("svg")).filter(function(a){return a.getAttributeNS(c.namespaces.xmlns,"ct")}).forEach(function(b){a.removeChild(b)}),f=new c.Svg("svg").attr({width:b,height:d}).addClass(e),f._node.style.width=b,f._node.style.height=d,a.appendChild(f._node),f},c.normalizeData=function(a,b,d){var e,f={raw:a,normalized:{}};return f.normalized.series=c.getDataArray({series:a.series||[]},b,d),e=f.normalized.series.every(function(a){return a instanceof Array})?Math.max.apply(null,f.normalized.series.map(function(a){return a.length})):f.normalized.series.length,f.normalized.labels=(a.labels||[]).slice(),Array.prototype.push.apply(f.normalized.labels,c.times(Math.max(0,e-f.normalized.labels.length)).map(function(){return""})),b&&c.reverseData(f.normalized),f},c.safeHasProperty=function(a,b){return null!==a&&"object"==typeof a&&a.hasOwnProperty(b)},c.isDataHoleValue=function(a){return null===a||void 0===a||"number"==typeof a&&isNaN(a)},c.reverseData=function(a){a.labels.reverse(),a.series.reverse();for(var b=0;bf.high&&(f.high=c),h&&c0?f.low=0:(f.high=1,f.low=0)),f},c.isNumeric=function(a){return null!==a&&isFinite(a)},c.isFalseyButZero=function(a){return!a&&0!==a},c.getNumberOrUndefined=function(a){return c.isNumeric(a)?+a:void 0},c.isMultiValue=function(a){return"object"==typeof a&&("x"in a||"y"in a)},c.getMultiValue=function(a,b){return c.isMultiValue(a)?c.getNumberOrUndefined(a[b||"y"]):c.getNumberOrUndefined(a)},c.rho=function(a){function b(a,c){return a%c===0?c:b(c,a%c)}function c(a){return a*a+1}if(1===a)return a;var d,e=2,f=2;if(a%2===0)return 2;do e=c(e)%a,f=c(c(f))%a,d=b(Math.abs(e-f),a);while(1===d);return d},c.getBounds=function(a,b,d,e){function f(a,b){return a===(a+=b)&&(a*=1+(b>0?o:-o)),a}var g,h,i,j=0,k={high:b.high,low:b.low};k.valueRange=k.high-k.low,k.oom=c.orderOfMagnitude(k.valueRange),k.step=Math.pow(10,k.oom),k.min=Math.floor(k.low/k.step)*k.step,k.max=Math.ceil(k.high/k.step)*k.step,k.range=k.max-k.min,k.numberOfSteps=Math.round(k.range/k.step);var l=c.projectLength(a,k.step,k),m=l=d)k.step=1;else if(e&&n=d)k.step=n;else for(;;){if(m&&c.projectLength(a,k.step,k)<=d)k.step*=2;else{if(m||!(c.projectLength(a,k.step/2,k)>=d))break;if(k.step/=2,e&&k.step%1!==0){k.step*=2;break}}if(j++>1e3)throw new Error("Exceeded maximum number of iterations while optimizing scale step!")}var o=2.221e-16;for(k.step=Math.max(k.step,o),h=k.min,i=k.max;h+k.step<=k.low;)h=f(h,k.step);for(;i-k.step>=k.high;)i=f(i,-k.step);k.min=h,k.max=i,k.range=k.max-k.min;var p=[];for(g=k.min;g<=k.max;g=f(g,k.step)){var q=c.roundWithPrecision(g);q!==p[p.length-1]&&p.push(q)}return k.values=p,k},c.polarToCartesian=function(a,b,c,d){var e=(d-90)*Math.PI/180;return{x:a+c*Math.cos(e),y:b+c*Math.sin(e)}},c.createChartRect=function(a,b,d){var e=!(!b.axisX&&!b.axisY),f=e?b.axisY.offset:0,g=e?b.axisX.offset:0,h=a.width()||c.quantity(b.width).value||0,i=a.height()||c.quantity(b.height).value||0,j=c.normalizePadding(b.chartPadding,d);h=Math.max(h,f+j.left+j.right),i=Math.max(i,g+j.top+j.bottom);var k={padding:j,width:function(){return this.x2-this.x1},height:function(){return this.y1-this.y2}};return e?("start"===b.axisX.position?(k.y2=j.top+g,k.y1=Math.max(i-j.bottom,k.y2+1)):(k.y2=j.top,k.y1=Math.max(i-j.bottom-g,k.y2+1)),"start"===b.axisY.position?(k.x1=j.left+f,k.x2=Math.max(h-j.right,k.x1+1)):(k.x1=j.left,k.x2=Math.max(h-j.right-f,k.x1+1))):(k.x1=j.left,k.x2=Math.max(h-j.right,k.x1+1),k.y2=j.top,k.y1=Math.max(i-j.bottom,k.y2+1)),k},c.createGrid=function(a,b,d,e,f,g,h,i){var j={};j[d.units.pos+"1"]=a,j[d.units.pos+"2"]=a,j[d.counterUnits.pos+"1"]=e,j[d.counterUnits.pos+"2"]=e+f;var k=g.elem("line",j,h.join(" "));i.emit("draw",c.extend({type:"grid",axis:d,index:b,group:g,element:k},j))},c.createGridBackground=function(a,b,c,d){var e=a.elem("rect",{x:b.x1,y:b.y2,width:b.width(),height:b.height()},c,!0);d.emit("draw",{type:"gridBackground",group:a,element:e})},c.createLabel=function(a,d,e,f,g,h,i,j,k,l,m){var n,o={};if(o[g.units.pos]=a+i[g.units.pos],o[g.counterUnits.pos]=i[g.counterUnits.pos],o[g.units.len]=d,o[g.counterUnits.len]=Math.max(0,h-10),l){var p=b.createElement("span");p.className=k.join(" "),p.setAttribute("xmlns",c.namespaces.xhtml),p.innerText=f[e],p.style[g.units.len]=Math.round(o[g.units.len])+"px",p.style[g.counterUnits.len]=Math.round(o[g.counterUnits.len])+"px",n=j.foreignObject(p,c.extend({style:"overflow: visible;"},o))}else n=j.elem("text",o,k.join(" ")).text(f[e]);m.emit("draw",c.extend({type:"label",axis:g,index:e,group:j,element:n,text:f[e]},o))},c.getSeriesOption=function(a,b,c){if(a.name&&b.series&&b.series[a.name]){var d=b.series[a.name];return d.hasOwnProperty(c)?d[c]:b[c]}return b[c]},c.optionsProvider=function(b,d,e){function f(b){var f=h;if(h=c.extend({},j),d)for(i=0;i=2&&a[h]<=a[h-2]&&(g=!0),g&&(f.push({pathCoordinates:[],valueData:[]}),g=!1),f[f.length-1].pathCoordinates.push(a[h],a[h+1]),f[f.length-1].valueData.push(b[h/2]));return f}}(window,document,a),function(a,b,c){"use strict";c.Interpolation={},c.Interpolation.none=function(a){var b={fillHoles:!1};return a=c.extend({},b,a),function(b,d){for(var e=new c.Svg.Path,f=!0,g=0;g1){var i=[];return h.forEach(function(a){i.push(f(a.pathCoordinates,a.valueData))}),c.Svg.Path.join(i)}if(b=h[0].pathCoordinates,g=h[0].valueData,b.length<=4)return c.Interpolation.none()(b,g);for(var j,k=(new c.Svg.Path).move(b[0],b[1],!1,g[0]),l=0,m=b.length;m-2*!j>l;l+=2){var n=[{x:+b[l-2],y:+b[l-1]},{x:+b[l],y:+b[l+1]},{x:+b[l+2],y:+b[l+3]},{x:+b[l+4],y:+b[l+5]}];j?l?m-4===l?n[3]={x:+b[0],y:+b[1]}:m-2===l&&(n[2]={x:+b[0],y:+b[1]},n[3]={x:+b[2],y:+b[3]}):n[0]={x:+b[m-2],y:+b[m-1]}:m-4===l?n[3]=n[2]:l||(n[0]={x:+b[l],y:+b[l+1]}),k.curve(d*(-n[0].x+6*n[1].x+n[2].x)/6+e*n[2].x,d*(-n[0].y+6*n[1].y+n[2].y)/6+e*n[2].y,d*(n[1].x+6*n[2].x-n[3].x)/6+e*n[2].x,d*(n[1].y+6*n[2].y-n[3].y)/6+e*n[2].y,n[2].x,n[2].y,!1,g[(l+2)/2])}return k}return c.Interpolation.none()([])}},c.Interpolation.monotoneCubic=function(a){var b={fillHoles:!1};return a=c.extend({},b,a),function d(b,e){var f=c.splitIntoSegments(b,e,{fillHoles:a.fillHoles,increasingX:!0});if(f.length){if(f.length>1){var g=[];return f.forEach(function(a){g.push(d(a.pathCoordinates,a.valueData))}),c.Svg.Path.join(g)}if(b=f[0].pathCoordinates,e=f[0].valueData,b.length<=4)return c.Interpolation.none()(b,e);var h,i,j=[],k=[],l=b.length/2,m=[],n=[],o=[],p=[];for(h=0;h0!=n[h]>0?m[h]=0:(m[h]=3*(p[h-1]+p[h])/((2*p[h]+p[h-1])/n[h-1]+(p[h]+2*p[h-1])/n[h]),isFinite(m[h])||(m[h]=0));for(i=(new c.Svg.Path).move(j[0],k[0],!1,e[0]),h=0;h1}).map(function(a){var b=a.pathElements[0],c=a.pathElements[a.pathElements.length-1];return a.clone(!0).position(0).remove(1).move(b.x,r).line(b.x,b.y).position(a.pathElements.length+1).line(c.x,r)}).forEach(function(c){var h=i.elem("path",{d:c.stringify()},a.classNames.area,!0);this.eventEmitter.emit("draw",{type:"area",values:b.normalized.series[g],path:c.clone(),series:f,seriesIndex:g,axisX:d,axisY:e,chartRect:j,index:g,group:i,element:h})}.bind(this))}}.bind(this)),this.eventEmitter.emit("created",{bounds:e.bounds,chartRect:j,axisX:d,axisY:e,svg:this.svg,options:a})}function e(a,b,d,e){c.Line["super"].constructor.call(this,a,b,f,c.extend({},f,d),e)}var f={axisX:{offset:30,position:"end",labelOffset:{x:0,y:0},showLabel:!0,showGrid:!0,labelInterpolationFnc:c.noop,type:void 0},axisY:{offset:40,position:"start",labelOffset:{x:0,y:0},showLabel:!0,showGrid:!0,labelInterpolationFnc:c.noop,type:void 0,scaleMinSpace:20,onlyInteger:!1},width:void 0,height:void 0,showLine:!0,showPoint:!0,showArea:!1,areaBase:0,lineSmooth:!0,showGridBackground:!1,low:void 0,high:void 0,chartPadding:{top:15,right:15,bottom:5,left:10},fullWidth:!1,reverseData:!1,classNames:{chart:"ct-chart-line",label:"ct-label",labelGroup:"ct-labels",series:"ct-series",line:"ct-line",point:"ct-point",area:"ct-area",grid:"ct-grid",gridGroup:"ct-grids",gridBackground:"ct-grid-background",vertical:"ct-vertical",horizontal:"ct-horizontal",start:"ct-start",end:"ct-end"}};c.Line=c.Base.extend({constructor:e,createChart:d})}(window,document,a),function(a,b,c){"use strict";function d(a){var b,d;a.distributeSeries?(b=c.normalizeData(this.data,a.reverseData,a.horizontalBars?"x":"y"),b.normalized.series=b.normalized.series.map(function(a){return[a]})):b=c.normalizeData(this.data,a.reverseData,a.horizontalBars?"x":"y"),this.svg=c.createSvg(this.container,a.width,a.height,a.classNames.chart+(a.horizontalBars?" "+a.classNames.horizontalBars:""));var e=this.svg.elem("g").addClass(a.classNames.gridGroup),g=this.svg.elem("g"),h=this.svg.elem("g").addClass(a.classNames.labelGroup);if(a.stackBars&&0!==b.normalized.series.length){var i=c.serialMap(b.normalized.series,function(){ +return Array.prototype.slice.call(arguments).map(function(a){return a}).reduce(function(a,b){return{x:a.x+(b&&b.x)||0,y:a.y+(b&&b.y)||0}},{x:0,y:0})});d=c.getHighLow([i],a,a.horizontalBars?"x":"y")}else d=c.getHighLow(b.normalized.series,a,a.horizontalBars?"x":"y");d.high=+a.high||(0===a.high?0:d.high),d.low=+a.low||(0===a.low?0:d.low);var j,k,l,m,n,o=c.createChartRect(this.svg,a,f.padding);k=a.distributeSeries&&a.stackBars?b.normalized.labels.slice(0,1):b.normalized.labels,a.horizontalBars?(j=m=void 0===a.axisX.type?new c.AutoScaleAxis(c.Axis.units.x,b.normalized.series,o,c.extend({},a.axisX,{highLow:d,referenceValue:0})):a.axisX.type.call(c,c.Axis.units.x,b.normalized.series,o,c.extend({},a.axisX,{highLow:d,referenceValue:0})),l=n=void 0===a.axisY.type?new c.StepAxis(c.Axis.units.y,b.normalized.series,o,{ticks:k}):a.axisY.type.call(c,c.Axis.units.y,b.normalized.series,o,a.axisY)):(l=m=void 0===a.axisX.type?new c.StepAxis(c.Axis.units.x,b.normalized.series,o,{ticks:k}):a.axisX.type.call(c,c.Axis.units.x,b.normalized.series,o,a.axisX),j=n=void 0===a.axisY.type?new c.AutoScaleAxis(c.Axis.units.y,b.normalized.series,o,c.extend({},a.axisY,{highLow:d,referenceValue:0})):a.axisY.type.call(c,c.Axis.units.y,b.normalized.series,o,c.extend({},a.axisY,{highLow:d,referenceValue:0})));var p=a.horizontalBars?o.x1+j.projectValue(0):o.y1-j.projectValue(0),q=[];l.createGridAndLabels(e,h,this.supportsForeignObject,a,this.eventEmitter),j.createGridAndLabels(e,h,this.supportsForeignObject,a,this.eventEmitter),a.showGridBackground&&c.createGridBackground(e,o,a.classNames.gridBackground,this.eventEmitter),b.raw.series.forEach(function(d,e){var f,h,i=e-(b.raw.series.length-1)/2;f=a.distributeSeries&&!a.stackBars?l.axisLength/b.normalized.series.length/2:a.distributeSeries&&a.stackBars?l.axisLength/2:l.axisLength/b.normalized.series[e].length/2,h=g.elem("g"),h.attr({"ct:series-name":d.name,"ct:meta":c.serialize(d.meta)}),h.addClass([a.classNames.series,d.className||a.classNames.series+"-"+c.alphaNumerate(e)].join(" ")),b.normalized.series[e].forEach(function(g,k){var r,s,t,u;if(u=a.distributeSeries&&!a.stackBars?e:a.distributeSeries&&a.stackBars?0:k,r=a.horizontalBars?{x:o.x1+j.projectValue(g&&g.x?g.x:0,k,b.normalized.series[e]),y:o.y1-l.projectValue(g&&g.y?g.y:0,u,b.normalized.series[e])}:{x:o.x1+l.projectValue(g&&g.x?g.x:0,u,b.normalized.series[e]),y:o.y1-j.projectValue(g&&g.y?g.y:0,k,b.normalized.series[e])},l instanceof c.StepAxis&&(l.options.stretch||(r[l.units.pos]+=f*(a.horizontalBars?-1:1)),r[l.units.pos]+=a.stackBars||a.distributeSeries?0:i*a.seriesBarDistance*(a.horizontalBars?-1:1)),t=q[k]||p,q[k]=t-(p-r[l.counterUnits.pos]),void 0!==g){var v={};v[l.units.pos+"1"]=r[l.units.pos],v[l.units.pos+"2"]=r[l.units.pos],!a.stackBars||"accumulate"!==a.stackMode&&a.stackMode?(v[l.counterUnits.pos+"1"]=p,v[l.counterUnits.pos+"2"]=r[l.counterUnits.pos]):(v[l.counterUnits.pos+"1"]=t,v[l.counterUnits.pos+"2"]=q[k]),v.x1=Math.min(Math.max(v.x1,o.x1),o.x2),v.x2=Math.min(Math.max(v.x2,o.x1),o.x2),v.y1=Math.min(Math.max(v.y1,o.y2),o.y1),v.y2=Math.min(Math.max(v.y2,o.y2),o.y1);var w=c.getMetaData(d,k);s=h.elem("line",v,a.classNames.bar).attr({"ct:value":[g.x,g.y].filter(c.isNumeric).join(","),"ct:meta":c.serialize(w)}),this.eventEmitter.emit("draw",c.extend({type:"bar",value:g,index:k,meta:w,series:d,seriesIndex:e,axisX:m,axisY:n,chartRect:o,group:h,element:s},v))}}.bind(this))}.bind(this)),this.eventEmitter.emit("created",{bounds:j.bounds,chartRect:o,axisX:m,axisY:n,svg:this.svg,options:a})}function e(a,b,d,e){c.Bar["super"].constructor.call(this,a,b,f,c.extend({},f,d),e)}var f={axisX:{offset:30,position:"end",labelOffset:{x:0,y:0},showLabel:!0,showGrid:!0,labelInterpolationFnc:c.noop,scaleMinSpace:30,onlyInteger:!1},axisY:{offset:40,position:"start",labelOffset:{x:0,y:0},showLabel:!0,showGrid:!0,labelInterpolationFnc:c.noop,scaleMinSpace:20,onlyInteger:!1},width:void 0,height:void 0,high:void 0,low:void 0,referenceValue:0,chartPadding:{top:15,right:15,bottom:5,left:10},seriesBarDistance:15,stackBars:!1,stackMode:"accumulate",horizontalBars:!1,distributeSeries:!1,reverseData:!1,showGridBackground:!1,classNames:{chart:"ct-chart-bar",horizontalBars:"ct-horizontal-bars",label:"ct-label",labelGroup:"ct-labels",series:"ct-series",bar:"ct-bar",grid:"ct-grid",gridGroup:"ct-grids",gridBackground:"ct-grid-background",vertical:"ct-vertical",horizontal:"ct-horizontal",start:"ct-start",end:"ct-end"}};c.Bar=c.Base.extend({constructor:e,createChart:d})}(window,document,a),function(a,b,c){"use strict";function d(a,b,c){var d=b.x>a.x;return d&&"explode"===c||!d&&"implode"===c?"start":d&&"implode"===c||!d&&"explode"===c?"end":"middle"}function e(a){var b,e,f,h,i,j=c.normalizeData(this.data),k=[],l=a.startAngle;this.svg=c.createSvg(this.container,a.width,a.height,a.donut?a.classNames.chartDonut:a.classNames.chartPie),e=c.createChartRect(this.svg,a,g.padding),f=Math.min(e.width()/2,e.height()/2),i=a.total||j.normalized.series.reduce(function(a,b){return a+b},0);var m=c.quantity(a.donutWidth);"%"===m.unit&&(m.value*=f/100),f-=a.donut&&!a.donutSolid?m.value/2:0,h="outside"===a.labelPosition||a.donut&&!a.donutSolid?f:"center"===a.labelPosition?0:a.donutSolid?f-m.value/2:f/2,h+=a.labelOffset;var n={x:e.x1+e.width()/2,y:e.y2+e.height()/2},o=1===j.raw.series.filter(function(a){return a.hasOwnProperty("value")?0!==a.value:0!==a}).length;j.raw.series.forEach(function(a,b){k[b]=this.svg.elem("g",null,null)}.bind(this)),a.showLabel&&(b=this.svg.elem("g",null,null)),j.raw.series.forEach(function(e,g){if(0!==j.normalized.series[g]||!a.ignoreEmptyValues){k[g].attr({"ct:series-name":e.name}),k[g].addClass([a.classNames.series,e.className||a.classNames.series+"-"+c.alphaNumerate(g)].join(" "));var p=i>0?l+j.normalized.series[g]/i*360:0,q=Math.max(0,l-(0===g||o?0:.2));p-q>=359.99&&(p=q+359.99);var r,s,t,u=c.polarToCartesian(n.x,n.y,f,q),v=c.polarToCartesian(n.x,n.y,f,p),w=new c.Svg.Path(!a.donut||a.donutSolid).move(v.x,v.y).arc(f,f,0,p-l>180,0,u.x,u.y);a.donut?a.donutSolid&&(t=f-m.value,r=c.polarToCartesian(n.x,n.y,t,l-(0===g||o?0:.2)),s=c.polarToCartesian(n.x,n.y,t,p),w.line(r.x,r.y),w.arc(t,t,0,p-l>180,1,s.x,s.y)):w.line(n.x,n.y);var x=a.classNames.slicePie;a.donut&&(x=a.classNames.sliceDonut,a.donutSolid&&(x=a.classNames.sliceDonutSolid));var y=k[g].elem("path",{d:w.stringify()},x);if(y.attr({"ct:value":j.normalized.series[g],"ct:meta":c.serialize(e.meta)}),a.donut&&!a.donutSolid&&(y._node.style.strokeWidth=m.value+"px"),this.eventEmitter.emit("draw",{type:"slice",value:j.normalized.series[g],totalDataSum:i,index:g,meta:e.meta,series:e,group:k[g],element:y,path:w.clone(),center:n,radius:f,startAngle:l,endAngle:p}),a.showLabel){var z;z=1===j.raw.series.length?{x:n.x,y:n.y}:c.polarToCartesian(n.x,n.y,h,l+(p-l)/2);var A;A=j.normalized.labels&&!c.isFalseyButZero(j.normalized.labels[g])?j.normalized.labels[g]:j.normalized.series[g];var B=a.labelInterpolationFnc(A,g);if(B||0===B){var C=b.elem("text",{dx:z.x,dy:z.y,"text-anchor":d(n,z,a.labelDirection)},a.classNames.label).text(""+B);this.eventEmitter.emit("draw",{type:"label",index:g,group:b,element:C,text:""+B,x:z.x,y:z.y})}}l=p}}.bind(this)),this.eventEmitter.emit("created",{chartRect:e,svg:this.svg,options:a})}function f(a,b,d,e){c.Pie["super"].constructor.call(this,a,b,g,c.extend({},g,d),e)}var g={width:void 0,height:void 0,chartPadding:5,classNames:{chartPie:"ct-chart-pie",chartDonut:"ct-chart-donut",series:"ct-series",slicePie:"ct-slice-pie",sliceDonut:"ct-slice-donut",sliceDonutSolid:"ct-slice-donut-solid",label:"ct-label"},startAngle:0,total:void 0,donut:!1,donutSolid:!1,donutWidth:60,showLabel:!0,labelOffset:0,labelPosition:"inside",labelInterpolationFnc:c.noop,labelDirection:"neutral",reverseData:!1,ignoreEmptyValues:!1};c.Pie=c.Base.extend({constructor:f,createChart:e,determineAnchorPosition:d})}(window,document,a),a}); +//# sourceMappingURL=chartist.min.js.map \ No newline at end of file diff --git a/static/js/cidr.js b/static/js/cidr.js deleted file mode 100644 index 8cd4071..0000000 --- a/static/js/cidr.js +++ /dev/null @@ -1,55 +0,0 @@ -(function ($){ - $.fn.cidr_validator = function(opts){ - var IP4_REG = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/s; - var IP6_REG = /^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?$/s; - var settings = $.extend( { - res_label: this.find('.panel-title>span') - }, opts); - - var net_inp = this.find('#id_network'); - var mask_inp = this.find('#id_mask'); - - var validate_ip_by_key = function(){ - var v = this.value; - if(v === undefined) - return; - var o = $(this).closest('.form-group-sm,.form-group'); - o.removeClass('has-error has-success'); - if(v.match(IP4_REG) !== null){ - mask_inp.val('24'); - o.addClass('has-success'); - }else - if(v.match(IP6_REG) !== null){ - mask_inp.val('64'); - o.addClass('has-success'); - }else - o.addClass('has-error'); - }; - var validate_ip_by_focus = function(){ - var v = this.value; - if(v.includes('/')){ - var chunks = v.split('/'); - if(chunks[1] !== ""){ - net_inp.val(chunks[0]); - mask_inp.val(chunks[1]); - settings.res_label.text(v); - } - }else { - settings.res_label.text(v + '/' + mask_inp.val()); - } - $(this).trigger('keyup'); - }; - net_inp.on('keyup focusin', validate_ip_by_key); - net_inp.on('focusout', validate_ip_by_focus); - - var validate_mask = function(){ - - }; - mask_inp.on('change', validate_mask); - }; -})(jQuery); - - -$(document).ready(function () { - $('div.cidr-contain').cidr_validator(); -}); diff --git a/static/js/my.js b/static/js/my.js index 82a5def..c785ca9 100644 --- a/static/js/my.js +++ b/static/js/my.js @@ -305,8 +305,4 @@ $(document).ready(function () { $('[data-toggle="tooltip"]').tooltip({container:'body'}); $('.btn_ajloader').ajloader({'dst_block': '#id_block_devices'}); - - $(document).notifys({news_url: '/tasks/check_news', check_interval: 50}); - $(document).notifys({news_url: '/msg/check_news', check_interval: 55}); - }); diff --git a/systemd_units/djing_telebot.service b/systemd_units/djing_telebot.service deleted file mode 100644 index eb0de74..0000000 --- a/systemd_units/djing_telebot.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=Djing telegram bot - -[Service] -Type=simple -ExecStart=/usr/bin/python3 ./telebot.py -PIDFile=/run/djing_telebot.pid -WorkingDirectory=/var/www/djing -TimeoutSec=9 -Restart=always -User=www-data -Group=www-data - -[Install] -WantedBy=multi-user.target diff --git a/systemd_units/do_backup.sh b/systemd_units/do_backup.sh index 9ba0bbf..829d016 100644 --- a/systemd_units/do_backup.sh +++ b/systemd_units/do_backup.sh @@ -1,18 +1,19 @@ #!/bin/bash -PATH=/usr/bin:/usr/sbin:/bin +PATH=/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin cd /var/backups file="djing`date "+%Y-%m-%d_%H.%M.%S"`.sql.gz" -export PGPASSWORD=POSTGRES ROOT PASSWORD +mysql_passw=MYSQL ROOT PASSWORD -pg_dump -O -d djing -h localhost -U djing | gzip > $file +echo show tables | mysql -uroot -p$mysql_passw djingdb | \ + grep -v '^flowstat' | grep -v 'traflost' | grep -v '^Tables' | \ + xargs mysqldump -R -Q --add-locks -uroot --password=$mysql_passw djingdb $1 | gzip > $file chmod 400 $file ./webdav_backup.py $file # удаляем старые find . -name "djing20??-??-??_??.??.??.sql.gz" -mtime +30 -type f -delete - diff --git a/taskapp/forms.py b/taskapp/forms.py index ca9e8e1..e7ac2b8 100644 --- a/taskapp/forms.py +++ b/taskapp/forms.py @@ -11,7 +11,7 @@ class TaskFrm(forms.ModelForm): 'out_date': delta_add_days().strftime("%Y-%m-%d") }}) super(TaskFrm, self).__init__(*args, **kwargs) - self.fields['recipients'].queryset = UserProfile.objects.filter(is_admin=True) + self.fields['recipients'].queryset = UserProfile.objects.filter(is_admin=True, is_active=True) if initial_abon is not None: # fetch profiles that has been attached on group of selected subscriber diff --git a/taskapp/handle.py b/taskapp/handle.py index a492bc0..2285258 100644 --- a/taskapp/handle.py +++ b/taskapp/handle.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- from django.template.loader import render_to_string from django.utils.translation import gettext as _ -from djing.tasks import send_email_notify -from chatbot.models import ChatException -from djing.lib import MultipleException +from djing.tasks import send_email_notify, multicast_email_notify +from messenger.tasks import multicast_viber_notify, send_viber_message class TaskException(Exception): @@ -11,29 +10,31 @@ class TaskException(Exception): def handle(task, author, recipients): - errors = [] + profile_ids = [] for recipient in recipients: - try: - task_status = _('Task') - # If signal to myself then quietly - if author == recipient: - return - # If task completed or failed - elif task.state == 'F' or task.state == 'C': - task_status = _('Task completed') - - fulltext = render_to_string('taskapp/notification.html', { - 'task': task, - 'abon': task.abon, - 'task_status': task_status - }) - - if task.state == 'F' or task.state == 'C': - # If task completed or failed than send one message to author - send_email_notify.delay(fulltext, author.pk) - else: - send_email_notify.delay(fulltext, recipient.pk) - except ChatException as e: - errors.append(e) - if len(errors) > 0: - raise MultipleException(errors) + if not recipient.flags.notify_task: + continue + # If signal to myself then quietly + if author == recipient: + return + profile_ids.append(recipient.pk) + + task_status = _('Task') + + # If task completed or failed + if task.state == 'F' or task.state == 'C': + task_status = _('Task completed') + + fulltext = render_to_string('taskapp/notification.html', { + 'task': task, + 'abon': task.abon, + 'task_status': task_status + }) + + if task.state == 'F' or task.state == 'C': + # If task completed or failed than send one message to author + send_email_notify.delay(fulltext, author.pk) + send_viber_message.delay(None, author.pk, fulltext) + else: + multicast_email_notify.delay(fulltext, profile_ids) + multicast_viber_notify.delay(None, profile_ids, fulltext) diff --git a/taskapp/locale/ru/LC_MESSAGES/django.po b/taskapp/locale/ru/LC_MESSAGES/django.po index 1cbbb57..af02fc2 100644 --- a/taskapp/locale/ru/LC_MESSAGES/django.po +++ b/taskapp/locale/ru/LC_MESSAGES/django.po @@ -505,3 +505,9 @@ msgstr "Задачи, которые необходимо выполнить" msgid "Task has been reminded" msgstr "Напоминание о задаче отправлено" + +msgid "View all new tasks" +msgstr "Все незавершённые задачи" + +msgid "New task with this user already exists. You are redirected to it." +msgstr "Заявка для этого абонента уже создана. Вы перенаправлены к ней." diff --git a/taskapp/models.py b/taskapp/models.py index 3b25a3d..2ddfc5d 100644 --- a/taskapp/models.py +++ b/taskapp/models.py @@ -134,7 +134,7 @@ class Task(models.Model): def send_notification(self): task_handle( self, self.author, - self.recipients.all() + self.recipients.filter(is_active=True) ) def get_attachment_fname(self): diff --git a/taskapp/templates/taskapp/details.html b/taskapp/templates/taskapp/details.html index a70ab55..f1b92ae 100644 --- a/taskapp/templates/taskapp/details.html +++ b/taskapp/templates/taskapp/details.html @@ -9,7 +9,7 @@ {% trans 'Task author' %}: {% if task and task.author %} - {{ task.author.username }} + {{ task.author.username }} {% else %} {% trans 'Not assigned' %} {% endif %}
    @@ -17,7 +17,7 @@ {% trans 'Implementers' %}: {% trans 'A priority' %}: {{ task.get_priority_display }}
    @@ -28,13 +28,13 @@ {% trans 'Condition' %}: {{ task.get_state_display }}
    {% trans 'Subscriber' %} {% if task.abon %} - {{ task.abon.get_full_name }} + {{ task.abon.get_full_name }} {% else %} {% trans 'Not assigned' %} {% endif %}
    {% if task.attachment %} {% trans 'Attachment' %}: - + {% endif %} diff --git a/taskapp/templates/taskapp/footer_btns.html b/taskapp/templates/taskapp/footer_btns.html index a9ed49d..5b80bc4 100644 --- a/taskapp/templates/taskapp/footer_btns.html +++ b/taskapp/templates/taskapp/footer_btns.html @@ -1,16 +1,23 @@ {% load i18n %}
    + {% if perms.taskapp.add_task %} {% trans 'Add new task' %} {% endif %} + {% if perms.taskapp.can_viewall %} {% trans 'View all tasks' %} + + {% trans 'View all new tasks' %} + {% endif %} + {% trans 'View empty tasks' %} +
    diff --git a/taskapp/urls.py b/taskapp/urls.py index ce5f7e6..ad07312 100644 --- a/taskapp/urls.py +++ b/taskapp/urls.py @@ -20,6 +20,6 @@ urlpatterns = [ path('own/', views.OwnTaskListView.as_view(), name='own_tasks'), path('my/', views.MyTaskListView.as_view(), name='my_tasks'), path('all/', views.AllTasksListView.as_view(), name='all_tasks'), - path('empty/', views.EmptyTasksListView.as_view(), name='empty_tasks'), - path('check_news/', views.check_news, name='check_news') + path('all_new/', views.AllNewTasksListView.as_view(), name='all_new_tasks'), + path('empty/', views.EmptyTasksListView.as_view(), name='empty_tasks') ] diff --git a/taskapp/views.py b/taskapp/views.py index 03e9d45..93f27a3 100644 --- a/taskapp/views.py +++ b/taskapp/views.py @@ -1,34 +1,27 @@ -from django.contrib.auth.decorators import login_required +from datetime import datetime +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.db.models import Count from django.shortcuts import redirect, get_object_or_404, resolve_url from django.contrib import messages -from django.utils.decorators import method_decorator from django.views.generic import ListView, CreateView from django.utils.translation import ugettext as _ from django.conf import settings - -from datetime import datetime - from django.views.generic.edit import FormMixin, DeleteView, UpdateView -from guardian.decorators import permission_required_or_403 as permission_required -from chatbot.models import MessageQueue +from guardian.shortcuts import assign_perm from abonapp.models import Abon from djing import httpresponse_to_referrer from djing.lib import safe_int, MultipleException, RuTimedelta -from djing.lib.decorators import only_admins, json_view +from djing.lib.decorators import only_admins +from djing.lib.mixins import LoginAdminMixin, LoginAdminPermissionMixin from .handle import TaskException from .models import Task, ExtraComment from .forms import TaskFrm, ExtraCommentForm -login_decs = login_required, only_admins - - -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.view_task'), name='dispatch') -class NewTasksView(ListView): +class NewTasksView(LoginAdminPermissionMixin, ListView): """ Show new tasks """ @@ -36,6 +29,7 @@ class NewTasksView(ListView): paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10) template_name = 'taskapp/tasklist.html' context_object_name = 'tasks' + permission_required = 'taskapp.view_task' def get_queryset(self): return Task.objects.filter( @@ -47,8 +41,6 @@ class NewTasksView(ListView): ) -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.view_task'), name='dispatch') class FailedTasksView(NewTasksView): """ Show crashed tasks @@ -64,8 +56,6 @@ class FailedTasksView(NewTasksView): ) -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.view_task'), name='dispatch') class FinishedTaskListView(NewTasksView): template_name = 'taskapp/tasklist_finish.html' @@ -77,8 +67,6 @@ class FinishedTaskListView(NewTasksView): ) -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.view_task'), name='dispatch') class OwnTaskListView(NewTasksView): template_name = 'taskapp/tasklist_own.html' @@ -91,8 +79,6 @@ class OwnTaskListView(NewTasksView): ) -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.view_task'), name='dispatch') class MyTaskListView(NewTasksView): template_name = 'taskapp/tasklist.html' @@ -105,13 +91,12 @@ class MyTaskListView(NewTasksView): ) -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.can_viewall'), name='dispatch') -class AllTasksListView(ListView): +class AllTasksListView(LoginAdminMixin, LoginRequiredMixin, ListView): http_method_names = ('get',) paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10) template_name = 'taskapp/tasklist_all.html' context_object_name = 'tasks' + permission_required = 'taskapp.can_viewall' def get_queryset(self): return Task.objects.annotate( @@ -121,8 +106,12 @@ class AllTasksListView(ListView): ) -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.view_task'), name='dispatch') +class AllNewTasksListView(AllTasksListView): + + def get_queryset(self): + return super(AllNewTasksListView, self).get_queryset().filter(state='S') + + class EmptyTasksListView(NewTasksView): template_name = 'taskapp/tasklist_empty.html' @@ -147,8 +136,7 @@ def task_delete(request, task_id): return redirect('taskapp:home') -@method_decorator(login_decs, name='dispatch') -class TaskUpdateView(UpdateView): +class TaskUpdateView(LoginAdminMixin, UpdateView): http_method_names = ('get', 'post') template_name = 'taskapp/add_edit_task.html' form_class = TaskFrm @@ -174,6 +162,16 @@ class TaskUpdateView(UpdateView): else: if not request.user.has_perm('taskapp.change_task'): raise PermissionDenied + + # check if new task with user already exists + uname = request.GET.get('uname') + if uname and self.kwargs.get('task_id') is None: + exists_task = Task.objects.filter(abon__username=uname, state='S') + if exists_task.exists(): + messages.info(request, _('New task with this user already exists.' + ' You are redirected to it.')) + return redirect('taskapp:edit', exists_task.first().pk) + try: return super(TaskUpdateView, self).dispatch(request, *args, **kwargs) except TaskException as e: @@ -187,6 +185,14 @@ class TaskUpdateView(UpdateView): return kwargs def form_valid(self, form): + # check if new task with picked user already exists + if form.cleaned_data['state'] == 'S' and self.kwargs.get('task_id') is None: + exists_task = Task.objects.filter(abon=form.cleaned_data['abon'], state='S') + if exists_task.exists(): + messages.info(self.request, _('New task with this user already exists.' + ' You are redirected to it.')) + return redirect('taskapp:edit', exists_task.first().pk) + try: self.object = form.save() if self.object.author is None: @@ -292,30 +298,11 @@ def remind(request, task_id): return redirect('taskapp:home') -@json_view -def check_news(request): - if request.user.is_authenticated and request.user.is_admin: - msg = MessageQueue.objects.pop(user=request.user, tag='taskap') - if msg is not None: - r = { - 'auth': True, - 'exist': True, - 'content': msg, - 'title': _('Task') - } - else: - r = {'auth': True, 'exist': False} - else: - r = {'auth': False} - return r - - -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.add_extracomment'), name='dispatch') -class NewCommentView(CreateView): +class NewCommentView(LoginAdminMixin, LoginRequiredMixin, CreateView): form_class = ExtraCommentForm model = ExtraComment http_method_names = ('get', 'post') + permission_required = 'taskapp.add_extracomment' def form_valid(self, form): self.task = get_object_or_404(Task, pk=self.kwargs.get('task_id')) @@ -323,16 +310,19 @@ class NewCommentView(CreateView): author=self.request.user, task=self.task ) + author = self.object.author + assign_perm('taskapp.change_extracomment', author, self.object) + assign_perm('taskapp.delete_extracomment', author, self.object) + assign_perm('taskapp.view_extracomment', author, self.object) return FormMixin.form_valid(self, form) -@method_decorator(login_decs, name='dispatch') -@method_decorator(permission_required('taskapp.delete_extracomment'), name='dispatch') -class DeleteCommentView(DeleteView): +class DeleteCommentView(LoginAdminPermissionMixin, DeleteView): model = ExtraComment pk_url_kwarg = 'comment_id' http_method_names = ('get', 'post') template_name = 'taskapp/comments/extracomment_confirm_delete.html' + permission_required = 'taskapp.delete_extracomment' def get_context_data(self, **kwargs): context = { diff --git a/telebot.py b/telebot.py deleted file mode 100755 index 0111c53..0000000 --- a/telebot.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -import os -from pid.decorator import pidfile -import django -from telepot import DelegatorBot -from telepot.exception import BadHTTPResponse -from telepot.delegate import per_chat_id, create_open, pave_event_space - - -@pidfile(pidname='djing_telebot.pid', piddir='/run') -def main(): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") - django.setup() - from chatbot.telebot import token, DjingTelebot - while True: - try: - bot = DelegatorBot(token, [ - pave_event_space()( - per_chat_id(), create_open, DjingTelebot, timeout=300 - ), - ]) - bot.message_loop(run_forever='Listening ...') - except BadHTTPResponse as e: - print(e) - - -if __name__ == '__main__': - main() diff --git a/templates/all_base.html b/templates/all_base.html index 7f99d4a..0cc5e9d 100644 --- a/templates/all_base.html +++ b/templates/all_base.html @@ -46,25 +46,10 @@ {% trans 'Map page' %} {% endif %} - {% comment %} {% endcomment %} - diff --git a/templates/base.html b/templates/base.html index 937955f..7c80932 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,7 +8,7 @@
    - + profile image @@ -106,6 +106,15 @@ {% endif %} + {% if perms.traf_stat.statcache_view %} + {% url 'traf_stat:home' as stathome %} + + + {% trans 'Traffic' %} + + + {% endif %} + {% url 'devapp:group_list' as devapp_groups %} @@ -122,6 +131,15 @@ {% endif %} + {% if perms.gw_app.view_nasmodel %} + {% url 'messenger:messengers_list' as mesngrhome %} + + + {% trans 'Messengers' %} + + + {% endif %} +
    diff --git a/chatbot/migrations/__init__.py b/traf_stat/__init__.py similarity index 100% rename from chatbot/migrations/__init__.py rename to traf_stat/__init__.py diff --git a/traf_stat/admin.py b/traf_stat/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/traf_stat/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/traf_stat/apps.py b/traf_stat/apps.py new file mode 100644 index 0000000..9c1b437 --- /dev/null +++ b/traf_stat/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TrafStatConfig(AppConfig): + name = 'traf_stat' diff --git a/traf_stat/fields.py b/traf_stat/fields.py new file mode 100644 index 0000000..6a6f78a --- /dev/null +++ b/traf_stat/fields.py @@ -0,0 +1,50 @@ +# +# Get from https://github.com/Niklas9/django-unixdatetimefield +# +import datetime +import time + +import django.db.models as models + + +class UnixDateTimeField(models.DateTimeField): + + # TODO(niklas9): + # * should we take care of transforming between time zones in any way here ? + # * get default datetime format from settings ? + DEFAULT_DATETIME_FMT = '%Y-%m-%d %H:%M:%S' + TZ_CONST = '+' + # TODO(niklas9): + # * metaclass below just for Django < 1.9, fix a if stmt for it? + #__metaclass__ = models.SubfieldBase + description = "Unix timestamp integer to datetime object" + + def get_internal_type(self): + return 'PositiveIntegerField' + + def to_python(self, val): + if val is None or isinstance(val, datetime.datetime): + return val + if isinstance(val, datetime.date): + return datetime.datetime(val.year, val.month, val.day) + elif isinstance(val, str): + # TODO(niklas9): + # * not addressing time zone support as todo above for now + if self.TZ_CONST in val: + val = val.split(self.TZ_CONST)[0] + return datetime.datetime.strptime(val, self.DEFAULT_DATETIME_FMT) + else: + return datetime.datetime.fromtimestamp(float(val)) + + def get_db_prep_value(self, val, *args, **kwargs): + if val is None: + if self.default == models.fields.NOT_PROVIDED: return None + return self.default + return int(time.mktime(val.timetuple())) + + def value_to_string(self, obj): + val = self._get_val_from_obj(obj) + return self.to_python(val).strftime(self.DEFAULT_DATETIME_FMT) + + def from_db_value(self, val, *args, **kwargs): + return self.to_python(val) diff --git a/traf_stat/migrations/0001_initial.py b/traf_stat/migrations/0001_initial.py new file mode 100644 index 0000000..50183b4 --- /dev/null +++ b/traf_stat/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.1 on 2019-03-06 18:07 + +from django.db import migrations, models +import django.db.models.deletion +import traf_stat.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('abonapp', '0001_squashed_0008_auto_20181115_1206'), + ] + + operations = [ + migrations.CreateModel( + name='StatCache', + fields=[ + ('last_time', traf_stat.fields.UnixDateTimeField()), + ('abon', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='abonapp.Abon')), + ('octets', models.PositiveIntegerField(default=0)), + ('packets', models.PositiveIntegerField(default=0)), + ], + options={ + 'db_table': 'flowcache', + 'ordering': ('-last_time',), + }, + ), + migrations.RunSQL(sql=(r'ALTER TABLE flowcache ENGINE=MEMORY;',)) + ] diff --git a/traf_stat/migrations/__init__.py b/traf_stat/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/traf_stat/models.py b/traf_stat/models.py new file mode 100644 index 0000000..c2dbc0d --- /dev/null +++ b/traf_stat/models.py @@ -0,0 +1,129 @@ +from datetime import datetime, timedelta, date, time +import math + +from django.db import models, connection, ProgrammingError +from django.utils.timezone import now +from .fields import UnixDateTimeField + + +def get_dates(): + tables = connection.introspection.table_names() + tables = (t.replace('flowstat_', '') for t in tables if t.startswith('flowstat_')) + return tuple(datetime.strptime(t, '%d%m%Y').date() for t in tables) + + +class StatManager(models.Manager): + def chart(self, user, count_of_parts=12, want_date=date.today()): + def byte_to_mbit(x): + return ((x / 60) * 8) / 2 ** 20 + + def split_list(lst, chunk_count): + chunk_size = len(lst) // chunk_count + if chunk_size == 0: + chunk_size = 1 + return tuple(lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)) + + def avarage(elements): + return sum(elements) / len(elements) + + try: + charts_data = self.filter(abon=user) + charts_times = tuple(cd.cur_time.timestamp() * 1000 for cd in charts_data) + charts_octets = tuple(cd.octets for cd in charts_data) + if len(charts_octets) > 0 and len(charts_octets) == len(charts_times): + charts_octets = split_list(charts_octets, count_of_parts) + charts_octets = (byte_to_mbit(avarage(c)) for c in charts_octets) + + charts_times = split_list(charts_times, count_of_parts) + charts_times = tuple(avarage(t) for t in charts_times) + + charts_data = zip(charts_times, charts_octets) + charts_data = ["{x: new Date(%d), y: %.2f}" % (cd[0], cd[1]) for cd in charts_data] + midnight = datetime.combine(want_date, time.min) + charts_data.append("{x:new Date(%d),y:0}" % (int(charts_times[-1:][0]) + 1)) + charts_data.append("{x:new Date(%d),y:0}" % (int((midnight + timedelta(days=1)).timestamp()) * 1000)) + return charts_data + else: + return + except ProgrammingError as e: + if "flowstat" in str(e): + return + + +class StatElem(models.Model): + cur_time = UnixDateTimeField(primary_key=True) + abon = models.ForeignKey('abonapp.Abon', on_delete=models.CASCADE, null=True, default=None, blank=True) + ip = models.PositiveIntegerField() + octets = models.PositiveIntegerField(default=0) + packets = models.PositiveIntegerField(default=0) + + objects = StatManager() + + # ReadOnly + def save(self, *args, **kwargs): + pass + + # ReadOnly + def delete(self, *args, **kwargs): + pass + + @property + def table_name(self): + return self._meta.db_table + + def delete_month(self): + cursor = connection.cursor() + table_name = self._meta.db_table + sql = "DROP TABLE %s;" % table_name + cursor.execute(sql) + + @staticmethod + def percentile(N, percent, key=lambda x: x): + """ + Find the percentile of a list of values. + + @parameter N - is a list of values. Note N MUST BE already sorted. + @parameter percent - a float value from 0.0 to 1.0. + @parameter key - optional key function to compute value from each element of N. + + @return - the percentile of the values + """ + if not N: + return None + k = (len(N) - 1) * percent + f = math.floor(k) + c = math.ceil(k) + if f == c: + return key(N[int(k)]) + d0 = key(N[int(f)]) * (c - k) + d1 = key(N[int(c)]) * (k - f) + return d0 + d1 + + class Meta: + abstract = True + + +def getModel(want_date=now()): + class DynamicStatElem(StatElem): + class Meta: + db_table = 'flowstat_%s' % want_date.strftime("%d%m%Y") + abstract = False + + return DynamicStatElem + + +class StatCache(models.Model): + last_time = UnixDateTimeField() + abon = models.OneToOneField('abonapp.Abon', on_delete=models.CASCADE, primary_key=True) + octets = models.PositiveIntegerField(default=0) + packets = models.PositiveIntegerField(default=0) + + def is_online(self): + return self.last_time > now() - timedelta(minutes=55) + + def is_today(self): + return date.today() == self.last_time.date() + + class Meta: + db_table = 'flowcache' + ordering = ('-last_time',) diff --git a/traf_stat/templates/statistics/index.html b/traf_stat/templates/statistics/index.html new file mode 100644 index 0000000..2c0dc43 --- /dev/null +++ b/traf_stat/templates/statistics/index.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block additional_link %} + + +{% endblock %} + +{% block page-header %}{% trans 'Traffic' %}{% endblock %} + +{% block main %} + +
    +{% endblock %} \ No newline at end of file diff --git a/traf_stat/tests.py b/traf_stat/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/traf_stat/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/traf_stat/urls.py b/traf_stat/urls.py new file mode 100644 index 0000000..c9887c7 --- /dev/null +++ b/traf_stat/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from traf_stat.views import home + +app_name = 'traf_stat' + +urlpatterns = [ + path('', home, name='home'), +] diff --git a/traf_stat/views.py b/traf_stat/views.py new file mode 100644 index 0000000..d5eb1b8 --- /dev/null +++ b/traf_stat/views.py @@ -0,0 +1,9 @@ +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from djing.lib.decorators import only_admins + + +@login_required +@only_admins +def home(request): + return render(request, 'statistics/index.html')
    ").addClass("cw").text("#"));c.isBefore(f.clone().endOf("w"));)b.append(a("").addClass("dow").text(c.format("dd"))),c.add(1,"d");o.find(".datepicker-days thead").append(b)},N=function(a){return d.disabledDates[a.format("YYYY-MM-DD")]===!0},O=function(a){return d.enabledDates[a.format("YYYY-MM-DD")]===!0},P=function(a){return d.disabledHours[a.format("H")]===!0},Q=function(a){return d.enabledHours[a.format("H")]===!0},R=function(b,c){if(!b.isValid())return!1;if(d.disabledDates&&"d"===c&&N(b))return!1;if(d.enabledDates&&"d"===c&&!O(b))return!1;if(d.minDate&&b.isBefore(d.minDate,c))return!1;if(d.maxDate&&b.isAfter(d.maxDate,c))return!1;if(d.daysOfWeekDisabled&&"d"===c&&d.daysOfWeekDisabled.indexOf(b.day())!==-1)return!1;if(d.disabledHours&&("h"===c||"m"===c||"s"===c)&&P(b))return!1;if(d.enabledHours&&("h"===c||"m"===c||"s"===c)&&!Q(b))return!1;if(d.disabledTimeIntervals&&("h"===c||"m"===c||"s"===c)){var e=!1;if(a.each(d.disabledTimeIntervals,function(){if(b.isBetween(this[0],this[1]))return e=!0,!1}),e)return!1}return!0},S=function(){for(var b=[],c=f.clone().startOf("y").startOf("d");c.isSame(f,"y");)b.push(a("").attr("data-action","selectMonth").addClass("month").text(c.format("MMM"))),c.add(1,"M");o.find(".datepicker-months td").empty().append(b)},T=function(){var b=o.find(".datepicker-months"),c=b.find("th"),g=b.find("tbody").find("span");c.eq(0).find("span").attr("title",d.tooltips.prevYear),c.eq(1).attr("title",d.tooltips.selectYear),c.eq(2).find("span").attr("title",d.tooltips.nextYear),b.find(".disabled").removeClass("disabled"),R(f.clone().subtract(1,"y"),"y")||c.eq(0).addClass("disabled"),c.eq(1).text(f.year()),R(f.clone().add(1,"y"),"y")||c.eq(2).addClass("disabled"),g.removeClass("active"),e.isSame(f,"y")&&!m&&g.eq(e.month()).addClass("active"),g.each(function(b){R(f.clone().month(b),"M")||a(this).addClass("disabled")})},U=function(){var a=o.find(".datepicker-years"),b=a.find("th"),c=f.clone().subtract(5,"y"),g=f.clone().add(6,"y"),h="";for(b.eq(0).find("span").attr("title",d.tooltips.prevDecade),b.eq(1).attr("title",d.tooltips.selectDecade),b.eq(2).find("span").attr("title",d.tooltips.nextDecade),a.find(".disabled").removeClass("disabled"),d.minDate&&d.minDate.isAfter(c,"y")&&b.eq(0).addClass("disabled"),b.eq(1).text(c.year()+"-"+g.year()),d.maxDate&&d.maxDate.isBefore(g,"y")&&b.eq(2).addClass("disabled");!c.isAfter(g,"y");)h+=''+c.year()+"",c.add(1,"y");a.find("td").html(h)},V=function(){var a,c=o.find(".datepicker-decades"),g=c.find("th"),h=b({y:f.year()-f.year()%100-1}),i=h.clone().add(100,"y"),j=h.clone(),k=!1,l=!1,m="";for(g.eq(0).find("span").attr("title",d.tooltips.prevCentury),g.eq(2).find("span").attr("title",d.tooltips.nextCentury),c.find(".disabled").removeClass("disabled"),(h.isSame(b({y:1900}))||d.minDate&&d.minDate.isAfter(h,"y"))&&g.eq(0).addClass("disabled"),g.eq(1).text(h.year()+"-"+i.year()),(h.isSame(b({y:2e3}))||d.maxDate&&d.maxDate.isBefore(i,"y"))&&g.eq(2).addClass("disabled");!h.isAfter(i,"y");)a=h.year()+12,k=d.minDate&&d.minDate.isAfter(h,"y")&&d.minDate.year()<=a,l=d.maxDate&&d.maxDate.isAfter(h,"y")&&d.maxDate.year()<=a,m+=''+(h.year()+1)+" - "+(h.year()+12)+"",h.add(12,"y");m+="",c.find("td").html(m),g.eq(1).text(j.year()+1+"-"+h.year())},W=function(){var b,c,g,h=o.find(".datepicker-days"),i=h.find("th"),j=[],k=[];if(B()){for(i.eq(0).find("span").attr("title",d.tooltips.prevMonth),i.eq(1).attr("title",d.tooltips.selectMonth),i.eq(2).find("span").attr("title",d.tooltips.nextMonth),h.find(".disabled").removeClass("disabled"),i.eq(1).text(f.format(d.dayViewHeaderFormat)),R(f.clone().subtract(1,"M"),"M")||i.eq(0).addClass("disabled"),R(f.clone().add(1,"M"),"M")||i.eq(2).addClass("disabled"),b=f.clone().startOf("M").startOf("w").startOf("d"),g=0;g<42;g++)0===b.weekday()&&(c=a("
    '+b.week()+"'+b.date()+"
    '+c.format(h?"HH":"hh")+"
    '+c.format("mm")+"
    '+c.format("ss")+"