diff --git a/Doc.txt b/Doc.txt index c361607..0b24ce0 100644 --- a/Doc.txt +++ b/Doc.txt @@ -13,3 +13,6 @@ и ваш класс для своей логики расчёта тарифа ВАЖНО! Для отработки своевременного выключения услуги, время на сервере биллинга и NAS должно быть настроено точно. + +Таблицу кеша статистики лучше сделать в памяти т.к. будет часто обновляться +ALTER TABLE flowcache ENGINE=MEMORY; diff --git a/README.md b/README.md index 637f293..01b8a50 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,6 @@ Попытка интернет биллинга. djing сокращение от django-billing. Это web интерфейс управления абонентами интернет-провайдера. Сейчас идёт тестирвоание работы на Mikrotik, функционал пока минимальный, т.к. пишу в свободное время. Работа планируется в реальных условиях и на реальных абонентах. Использовано python 3, django 1.9, bootstrap 3, и другое в файле requirements.txt -Может: - Наблюдать за устройствами по snmp - Отправлять изменения мгновенно на mikrotik - Привязывать на карте к точкам топологии устройства - Есть привязка монтажника к группам абонентов - Есть менеджер задач на абонентов. Это оператор может выбрать абонента и описать проблему. Система отправит оповещение через telegram ответственному за групу указанного абонента монтажнику с текстом проблемы, адресом и телефоном абонента. -## Установка(не завершил описание): -На ArchLinux нужые пакеты я устанавливаю так: -``` -# pacman -Sy mariadb-clients python3 python-pip nginx uwsgi -``` -Дальше ставим всё для python через pip: -``` -# pip install git+https://github.com/nerosketch/djing.git -``` - -На Fedora ставил так -``` -# dnf install uwsgi python3 python3-devel nginx python-pip git redis mariadb mariadb-devel net-snmp net-snmp-devel gcc redhat-rpm-config uwsgi-plugin-python3 -# pip3 install rq mysqlclient easysnmp xmltodict netaddr telepot Pillow Django==1.9 uwsgi -``` +## Содержание +* [Установка](./docs/install.md) diff --git a/abonapp/__init__.py b/abonapp/__init__.py index 8e82010..e69de29 100644 --- a/abonapp/__init__.py +++ b/abonapp/__init__.py @@ -1,59 +0,0 @@ -from django.conf import settings - -from netaddr import mac_unix, mac_eui48 - -import importlib -import warnings - -class mac_linux(mac_unix): - """MAC format with zero-padded all upper-case hex and colon separated""" - word_fmt = '%.2X' - -def default_dialect(eui_obj=None): - # Check to see if a default dialect class has been specified in settings, - # using 'module.dialect_cls' string and use importlib and getattr to retrieve dialect class. 'module' is the module and - # 'dialect_cls' is the class name of the custom dialect. The dialect must either be defined or imported by the module's - # __init__.py if the module is a package. - from .fields import MACAddressField # Remove import at v1.4 - if hasattr(settings, 'MACADDRESS_DEFAULT_DIALECT') and not MACAddressField.dialect: - module, dialect_cls = settings.MACADDRESS_DEFAULT_DIALECT.split('.') - dialect = getattr(importlib.import_module(module), dialect_cls, mac_linux) - return dialect - else: - if MACAddressField.dialect: # Remove this "if" statement at v1.4 - warnings.warn( - "The set_dialect class method on MACAddressField has been deprecated, in favor of the default_dialect " - "utility function and settings.MACADDRESS_DEFAULT_DIALECT. See macaddress.__init__.py source or the " - "project README for more information.", - DeprecationWarning, - ) - return MACAddressField.dialect - if eui_obj: - return eui_obj.dialect - else: - return mac_linux - -def format_mac(eui_obj, dialect): - # Format a EUI instance as a string using the supplied dialect class, allowing custom string classes by - # passing directly or as a string, a la 'module.dialect_cls', where 'module' is the module and 'dialect_cls' - # is the class name of the custom dialect. The dialect must either be defined or imported by the module's __init__.py if - # the module is a package. - if not isinstance(dialect, mac_eui48): - if isinstance(dialect, str): - module, dialect_cls = dialect.split('.') - dialect = getattr(importlib.import_module(module), dialect_cls) - eui_obj.dialect = dialect - return str(eui_obj) - - -from pkg_resources import get_distribution, DistributionNotFound -import os.path - -try: - _dist = get_distribution('django-macaddress') -except DistributionNotFound: - __version__ = 'Please install this project with setup.py' -else: - __version__ = _dist.version -VERSION = __version__ # synonym -default_app_config = 'abonapp.apps.AbonappConfig' diff --git a/abonapp/admin.py b/abonapp/admin.py index 64a0547..c3e76ef 100644 --- a/abonapp/admin.py +++ b/abonapp/admin.py @@ -13,6 +13,4 @@ admin.site.register(models.AllTimePayLog) admin.site.register(models.AbonRawPassword) admin.site.register(models.ExtraFieldsModel) admin.site.register(models.AllPayLog) -admin.site.register(models.Opt82) admin.site.register(models.PassportInfo) -admin.site.register(models.AbonDevice) diff --git a/abonapp/forms.py b/abonapp/forms.py index cf31d19..1666c8a 100644 --- a/abonapp/forms.py +++ b/abonapp/forms.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from django.http import QueryDict from datetime import datetime from django.utils.translation import ugettext as _ from django import forms @@ -7,7 +6,6 @@ from django.contrib.auth.hashers import make_password from random import choice from string import digits, ascii_lowercase from . import models -from .formfields import MACAddressField def generate_random_username(length=6, chars=digits, split=2, delimiter=''): @@ -88,19 +86,6 @@ class AbonForm(forms.ModelForm): return acc -class Opt82Form(forms.ModelForm): - mac = MACAddressField(widget=forms.TextInput(attrs={'class': 'form-control', 'pattern': r'^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$', 'required': ''})) - class Meta: - model = models.Opt82 - fields = '__all__' - widgets = { - 'port': forms.NumberInput(attrs={'class': 'form-control', 'required': ''}) - } - - #def save(self, commit=True): - # super().save(commit=commit) - - class AbonGroupForm(forms.ModelForm): class Meta: model = models.AbonGroup @@ -136,3 +121,13 @@ class ExtraFieldForm(forms.ModelForm): 'field_type': forms.Select(attrs={'class': 'form-control'}), 'data': forms.TextInput(attrs={'class': 'form-control'}) } + + +class AbonStreetForm(forms.ModelForm): + class Meta: + model = models.AbonStreet + fields = '__all__' + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control', 'required':'', 'autofocus':''}), + 'group': forms.Select(attrs={'class': 'form-control'}) + } diff --git a/abonapp/locale/ru/LC_MESSAGES/django.po b/abonapp/locale/ru/LC_MESSAGES/django.po index 40513ea..63f19d5 100644 --- a/abonapp/locale/ru/LC_MESSAGES/django.po +++ b/abonapp/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-03-25 00:40+0300\n" +"POT-Creation-Date: 2017-06-15 13:35+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Dmitry Novikov nerosketch@gmail.com\n" "Language: ru\n" @@ -19,27 +19,35 @@ 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" -#: abonapp/forms.py:29 abonapp/templates/abonapp/addAbon.html.py:21 +#: abonapp/formfields.py:12 +msgid "Enter a valid integer." +msgstr "Введите верное число" + +#: abonapp/formfields.py:21 +msgid "Enter a valid MAC Address." +msgstr "Введите валидный mac адрес" + +#: abonapp/forms.py:42 abonapp/templates/abonapp/addAbon.html.py:21 #: abonapp/templates/abonapp/editAbon.html:15 #: abonapp/templates/abonapp/viewAbon.html:28 msgid "login" msgstr "Логин" -#: abonapp/forms.py:42 abonapp/templates/abonapp/editAbon.html.py:22 -#: abonapp/templates/abonapp/peoples.html:33 +#: abonapp/forms.py:55 abonapp/templates/abonapp/editAbon.html.py:22 +#: abonapp/templates/abonapp/peoples.html:37 #: abonapp/templates/abonapp/viewAbon.html:32 msgid "fio" msgstr "ФИО" -#: abonapp/forms.py:47 +#: abonapp/forms.py:60 msgid "telephone placeholder" msgstr "+[7,8,9,3] и 10,11 цифр" -#: abonapp/models.py:26 +#: abonapp/models.py:23 msgid "fill account" msgstr "Пополнение счёта" -#: abonapp/models.py:119 +#: abonapp/models.py:116 msgid "not enough money" msgstr "Не хватает денег на счету" @@ -47,10 +55,6 @@ msgstr "Не хватает денег на счету" msgid "finish service perm" msgstr "Снятие со счёта средств" -#: abonapp/models.py:145 -msgid "activate service perm" -msgstr "Активация услуги абонента" - #: abonapp/models.py:162 msgid "Digital field" msgstr "Цифровое поле" @@ -63,31 +67,35 @@ msgstr "Текстовое поле" msgid "Floating field" msgstr "Дробное с плавающей точкой" -#: abonapp/models.py:178 +#: abonapp/models.py:165 abonapp/templates/abonapp/editAbon.html.py:43 +#: abonapp/templates/abonapp/viewAbon.html:50 +msgid "Ip Address" +msgstr "IP Адрес" + +#: abonapp/models.py:190 msgid "Double invalid value" msgstr "Введите число с плавающей запятой" -#: abonapp/models.py:219 +#: abonapp/models.py:244 msgid "Buy service perm" msgstr "Покупка тарифа абоненту" -#: abonapp/models.py:220 +#: abonapp/models.py:245 msgid "Can view passport" msgstr "Может просматривать паспортные данные" -#: abonapp/models.py:224 -msgid "pay log" -msgstr "Снятие со счёта средств" - -#: abonapp/models.py:270 +#: abonapp/models.py:292 msgid "Buy service default log" msgstr "Покупка тарифного плана через админку" -#: abonapp/models.py:285 +#: abonapp/models.py:307 msgid "service overdue log" msgstr "Услуга просрочена, отключаем, и подключаем новую" -#: abonapp/templates/abonapp/activate_service.html:8 +#: abonapp/models.py:347 +msgid "Ip address already exist" +msgstr "Такой ip уже у кого-то есть" + #: abonapp/templates/abonapp/addAbon.html:7 #: abonapp/templates/abonapp/addGroup.html:7 #: abonapp/templates/abonapp/addInvoice.html:7 @@ -101,49 +109,32 @@ msgstr "Услуга просрочена, отключаем, и подключ #: abonapp/templates/abonapp/invoiceForPayment.html:7 #: abonapp/templates/abonapp/log.html:7 #: abonapp/templates/abonapp/peoples.html:8 -#: abonapp/templates/abonapp/peoples.html:114 +#: abonapp/templates/abonapp/peoples.html:132 msgid "User groups" msgstr "Группы абонентов" -#: abonapp/templates/abonapp/activate_service.html:11 -#: abonapp/templates/abonapp/activate_service.html:18 #: abonapp/templates/abonapp/services.html:44 msgid "Activate service" msgstr "Активировать услугу" -#: abonapp/templates/abonapp/activate_service.html:26 -#, python-format -msgid "" -"\n" -"Are you sure that you want activate service for the user?
\n" -"Note that the account will be removed from his money and open access to the " -"resources of the paid services.
\n" -"Maintenance cost is %(amount)s. On account of %(ballance)s, will be " -"%(diff)s
\n" -"It will work until %(deadline)s" -msgstr "" -"\n" -"Вы уверены что хотите активировать абоненту эту услугу?
Обратите внимание " -"что с его счёта снимутся деньги и откроется доступ к ресурсам " -"оплаченной услуги.
Стоимость услуги: %(amount)sруб. На счету %(ballance)s " -"руб, останется %(diff)s руб.
Услуга будет действовать до %(deadline)s" - -#: abonapp/templates/abonapp/activate_service.html:34 -#: abonapp/templates/abonapp/addAbon.html:86 +#: abonapp/templates/abonapp/addAbon.html:91 #: abonapp/templates/abonapp/addGroup.html:29 #: abonapp/templates/abonapp/addInvoice.html:46 -#: abonapp/templates/abonapp/buy_tariff.html:38 +#: abonapp/templates/abonapp/buy_tariff.html:57 #: abonapp/templates/abonapp/complete_service.html:49 -#: abonapp/templates/abonapp/editAbon.html:96 +#: abonapp/templates/abonapp/editAbon.html:103 +#: abonapp/templates/abonapp/editAbon.html:192 +#: abonapp/templates/abonapp/editAbon.html:238 #: abonapp/templates/abonapp/group_tariffs.html:29 -#: abonapp/templates/abonapp/passport_view.html:42 +#: abonapp/templates/abonapp/modal_dev.html:29 +#: abonapp/templates/abonapp/passport_view.html:49 msgid "Save" msgstr "Сохранить" #: abonapp/templates/abonapp/addAbon.html:9 #: abonapp/templates/abonapp/addAbon.html:16 -#: abonapp/templates/abonapp/peoples.html:97 -#: abonapp/templates/abonapp/peoples.html:108 +#: abonapp/templates/abonapp/peoples.html:115 +#: abonapp/templates/abonapp/peoples.html:126 msgid "Add abon" msgstr "Добавить абонента" @@ -153,13 +144,13 @@ msgstr "Фамилия и Имя" #: abonapp/templates/abonapp/addAbon.html:37 #: abonapp/templates/abonapp/editAbon.html:29 -#: abonapp/templates/abonapp/peoples.html:49 +#: abonapp/templates/abonapp/peoples.html:53 #: abonapp/templates/abonapp/viewAbon.html:36 msgid "Telephone" msgstr "Телефон" #: abonapp/templates/abonapp/addAbon.html:45 -#: abonapp/templates/abonapp/editAbon.html:66 +#: abonapp/templates/abonapp/editAbon.html:73 #: abonapp/templates/abonapp/viewAbon.html:16 msgid "User group" msgstr "Группа" @@ -167,7 +158,7 @@ msgstr "Группа" #: abonapp/templates/abonapp/addAbon.html:53 #: abonapp/templates/abonapp/addInvoice.html:40 #: abonapp/templates/abonapp/debtors.html:22 -#: abonapp/templates/abonapp/editAbon.html:87 +#: abonapp/templates/abonapp/editAbon.html:94 #: abonapp/templates/abonapp/invoiceForPayment.html:23 #: abonapp/templates/abonapp/log.html:20 #: abonapp/templates/abonapp/payHistory.html:12 @@ -176,29 +167,31 @@ msgid "Comment" msgstr "Комментарий" #: abonapp/templates/abonapp/addAbon.html:59 -#: abonapp/templates/abonapp/editAbon.html:43 -#: abonapp/templates/abonapp/peoples.html:39 +#: abonapp/templates/abonapp/editAbon.html:50 +#: abonapp/templates/abonapp/peoples.html:43 #: abonapp/templates/abonapp/viewAbon.html:40 msgid "Street" msgstr "Улица" #: abonapp/templates/abonapp/addAbon.html:67 -#: abonapp/templates/abonapp/peoples.html:45 +#: abonapp/templates/abonapp/peoples.html:49 msgid "Apartment" msgstr "Квартира" #: abonapp/templates/abonapp/addAbon.html:76 -#: abonapp/templates/abonapp/editAbon.html:73 +#: abonapp/templates/abonapp/editAbon.html:80 #: abonapp/templates/abonapp/viewAbon.html:54 msgid "Password" msgstr "Пароль" -#: abonapp/templates/abonapp/addAbon.html:89 +#: abonapp/templates/abonapp/addAbon.html:94 #: abonapp/templates/abonapp/addGroup.html:32 #: abonapp/templates/abonapp/addInvoice.html:49 -#: abonapp/templates/abonapp/buy_tariff.html:41 +#: abonapp/templates/abonapp/buy_tariff.html:60 #: abonapp/templates/abonapp/group_tariffs.html:29 -#: abonapp/templates/abonapp/modal_abonamount.html:26 +#: abonapp/templates/abonapp/modal_abonamount.html:25 +#: abonapp/templates/abonapp/modal_dev.html:32 +#: abonapp/templates/abonapp/modal_extra_field.html:40 msgid "Reset" msgstr "Сбросить" @@ -245,10 +238,28 @@ msgstr "Купить новую услугу (заказать тариф) дл #: abonapp/templates/abonapp/debtors.html:20 #: abonapp/templates/abonapp/log.html:19 #: abonapp/templates/abonapp/payHistory.html:8 -#: abonapp/templates/abonapp/peoples.html:21 +#: abonapp/templates/abonapp/peoples.html:24 msgid "Sub" msgstr "Абонент" +#: abonapp/templates/abonapp/buy_tariff.html:32 +#: abonapp/templates/abonapp/group_tariffs.html:24 +msgid "currency" +msgstr "руб" + +#: abonapp/templates/abonapp/charts.html:9 +msgid "Graph of use" +msgstr "График использования" + +#: abonapp/templates/abonapp/charts.html:44 +#: abonapp/templates/abonapp/charts.html:56 +msgid "Static info was Not found" +msgstr "Статистика не найдена" + +#: abonapp/templates/abonapp/charts.html:51 +msgid "Graphs by dates" +msgstr "Графики по датам" + #: abonapp/templates/abonapp/complete_service.html:10 #: abonapp/templates/abonapp/complete_service.html:17 #: abonapp/templates/abonapp/services.html:72 @@ -310,36 +321,133 @@ msgstr "Автор" msgid "Debts not found" msgstr "Нет должников" +#: abonapp/templates/abonapp/dial_log.html:8 +msgid "Play" +msgstr "Слушать" + +#: abonapp/templates/abonapp/dial_log.html:9 +msgid "calldate" +msgstr "дата звонка" + +#: abonapp/templates/abonapp/dial_log.html:10 +msgid "src" +msgstr "кто" + +#: abonapp/templates/abonapp/dial_log.html:11 +msgid "dst" +msgstr "куда" + +#: abonapp/templates/abonapp/dial_log.html:12 +msgid "duration" +msgstr "продолжительность" + +#: abonapp/templates/abonapp/dial_log.html:13 +msgid "start" +msgstr "начало" + +#: abonapp/templates/abonapp/dial_log.html:14 +msgid "answer" +msgstr "ответ" + +#: abonapp/templates/abonapp/dial_log.html:15 +msgid "end" +msgstr "конец" + +#: abonapp/templates/abonapp/dial_log.html:16 +msgid "disposition" +msgstr "состояние" + +#: abonapp/templates/abonapp/dial_log.html:38 +msgid "Calls was not found" +msgstr "Звонки не найдены" + #: abonapp/templates/abonapp/editAbon.html:9 msgid "Change subscriber" msgstr "Изменение абонента" -#: abonapp/templates/abonapp/editAbon.html:36 -#: abonapp/templates/abonapp/editAbon.html:117 -#: abonapp/templates/abonapp/editAbon.html:135 -#: abonapp/templates/abonapp/viewAbon.html:50 -msgid "Ip Address" -msgstr "IP Адрес" +#: abonapp/templates/abonapp/editAbon.html:45 +#: abonapp/templates/abonapp/editAbon.html:216 +#: abonapp/templates/abonapp/peoples.html:86 +#: abonapp/templates/abonapp/peoples.html:88 +#: abonapp/templates/abonapp/viewAbon.html:18 +#: abonapp/templates/abonapp/viewAbon.html:29 +#: abonapp/templates/abonapp/viewAbon.html:33 +#: abonapp/templates/abonapp/viewAbon.html:37 +#: abonapp/templates/abonapp/viewAbon.html:42 +#: abonapp/templates/abonapp/viewAbon.html:47 +#: abonapp/templates/abonapp/viewAbon.html:51 +msgid "Not assigned" +msgstr "<Не назначен>" -#: abonapp/templates/abonapp/editAbon.html:51 -#: abonapp/templates/abonapp/peoples.html:45 +#: abonapp/templates/abonapp/editAbon.html:58 #: abonapp/templates/abonapp/viewAbon.html:46 msgid "House" msgstr "Дом" -#: abonapp/templates/abonapp/editAbon.html:59 +#: abonapp/templates/abonapp/editAbon.html:66 #: abonapp/templates/abonapp/viewAbon.html:22 msgid "Is active" msgstr "Активен" -#: abonapp/templates/abonapp/editAbon.html:99 +#: abonapp/templates/abonapp/editAbon.html:106 msgid "Send account info to user" msgstr "Отправить данные абоненту" -#: abonapp/templates/abonapp/editAbon.html:111 +#: abonapp/templates/abonapp/editAbon.html:109 +#: abonapp/templates/abonapp/editAbon.html:110 +#, fuzzy +#| msgid "Add debt" +msgid "Add new task" +msgstr "Добавить квитанцию" + +#: abonapp/templates/abonapp/editAbon.html:123 +msgid "Remove clutch" +msgstr "Удалить муфту" + +#: abonapp/templates/abonapp/editAbon.html:127 +#: abonapp/templates/abonapp/modal_dev.html:6 +msgid "Add clutch" +msgstr "Добавить муфту" + +#: abonapp/templates/abonapp/editAbon.html:170 msgid "DHCP information" msgstr "DHCP информация" +#: abonapp/templates/abonapp/editAbon.html:176 +msgid "Mac Address" +msgstr "Мак" + +#: abonapp/templates/abonapp/editAbon.html:183 +msgid "Port" +msgstr "Порт" + +#: abonapp/templates/abonapp/editAbon.html:194 +msgid "Reset option82" +msgstr "Сбросить option82" + +#: abonapp/templates/abonapp/editAbon.html:195 +#: abonapp/templates/abonapp/editAbon.html:219 +msgid "Delete" +msgstr "Удалить" + +#: abonapp/templates/abonapp/editAbon.html:178 +msgid "Extra fields" +msgstr "Динамические записи" + +#: abonapp/templates/abonapp/editAbon.html:201 abonapp/views.py:824 +msgid "Extra field does not exist" +msgstr "Поле не найдено" + +#: abonapp/templates/abonapp/editAbon.html:206 +#: abonapp/templates/abonapp/modal_extra_field.html:6 +msgid "Add extra field" +msgstr "Добавить новое динамическое поле" + +#: abonapp/templates/abonapp/editAbon.html:207 +#: abonapp/templates/abonapp/modal_extra_field.html:37 +msgid "Add" +msgstr "Добавить" + #: abonapp/templates/abonapp/group_list.html:27 msgid "Number of subscribers" msgstr "Количество абонентов" @@ -420,10 +528,26 @@ msgstr "Начисление средств на счёт" msgid "Amount of money" msgstr "Количество денег" -#: abonapp/templates/abonapp/modal_abonamount.html:23 +#: abonapp/templates/abonapp/modal_abonamount.html:22 msgid "Refill" msgstr "Пополнить" +#: abonapp/templates/abonapp/modal_dev.html:13 +msgid "Select the device" +msgstr "Выберите устройство" + +#: abonapp/templates/abonapp/modal_extra_field.html:11 +msgid "Field title" +msgstr "Название поля" + +#: abonapp/templates/abonapp/modal_extra_field.html:20 +msgid "Field type" +msgstr "Тип динамического поля" + +#: abonapp/templates/abonapp/modal_extra_field.html:28 +msgid "Field content" +msgstr "Содержимое динамического поля" + #: abonapp/templates/abonapp/passport_view.html:9 #: abonapp/templates/abonapp/viewAbon.html:67 msgid "Passport information" @@ -444,8 +568,8 @@ msgid "Distributor" msgstr "Кем выдан" #: abonapp/templates/abonapp/passport_view.html:33 -msgid "Birthday" -msgstr "Дата рождения" +msgid "Date of acceptance" +msgstr "Дата выдачи" #: abonapp/templates/abonapp/payHistory.html:26 msgid "Payment history is empty" @@ -459,49 +583,72 @@ msgstr "Пополнить счёт" msgid "The people in the selected group" msgstr "Народ в выбранной группе" -#: abonapp/templates/abonapp/peoples.html:27 +#: abonapp/templates/abonapp/peoples.html:28 +msgid "Last traffic" +msgstr "Траф." + +#: abonapp/templates/abonapp/peoples.html:31 #, fuzzy #| msgid "Ip Address" msgid "Ip address" msgstr "IP Адрес" -#: abonapp/templates/abonapp/peoples.html:50 +#: abonapp/templates/abonapp/peoples.html:54 #: abonapp/templates/abonapp/services.html:10 msgid "Service" msgstr "Услуга" -#: abonapp/templates/abonapp/peoples.html:53 +#: abonapp/templates/abonapp/peoples.html:57 msgid "Ballance" msgstr "Балланс" -#: abonapp/templates/abonapp/peoples.html:68 -#: abonapp/templates/abonapp/peoples.html:70 -#: abonapp/templates/abonapp/peoples.html:71 -#: abonapp/templates/abonapp/viewAbon.html:18 -#: abonapp/templates/abonapp/viewAbon.html:29 -#: abonapp/templates/abonapp/viewAbon.html:33 -#: abonapp/templates/abonapp/viewAbon.html:37 -#: abonapp/templates/abonapp/viewAbon.html:42 -#: abonapp/templates/abonapp/viewAbon.html:47 -#: abonapp/templates/abonapp/viewAbon.html:51 -msgid "Not assigned" -msgstr "<Не назначен>" - -#: abonapp/templates/abonapp/peoples.html:95 +#: abonapp/templates/abonapp/peoples.html:113 msgid "Subscribers not found" msgstr "Абоненты не найдены" -#: abonapp/templates/abonapp/peoples.html:112 +#: abonapp/templates/abonapp/peoples.html:130 msgid "Refresh subscribers on NAS" msgstr "Обновить абонентов в NAS" -#: abonapp/templates/abonapp/peoples.html:115 +#: abonapp/templates/abonapp/peoples.html:133 msgid "Tariffs in groups" msgstr "Тарифы в группах" + +#: abonapp/templates/abonapp/peoples.html:148 +msgid "No streets found for that group" +msgstr "Не найдены улицы для группы" + #: abonapp/templates/abonapp/services.html:5 -msgid "Services of subscriber" -msgstr "Купленные абонентом услуги (назначенные тарифные планы)" +msgid "Subscriber's service" +msgstr "Текущая услуга абонента" + +msgid "Add street" +msgstr "Добавить улицу" + +msgid "Edit streets" +msgstr "Редактировать улицы" + +msgid "Street successfully saved" +msgstr "Улица успешно сохранена" + +msgid "Streets has been saved" +msgstr "Улицы сохранены" + +msgid "Street title" +msgstr "Название улицы" + +msgid "One of these streets has not been found" +msgstr "Одна из этих улиц не была найдена" + +msgid "The street has not been found" +msgstr "Улица не найдена" + +msgid "The street successfully deleted" +msgstr "Улица успешно удалена" + +msgid "Streets has not been found" +msgstr "Улицы не найдены" #: abonapp/templates/abonapp/services.html:9 msgid "Priority" @@ -515,13 +662,9 @@ msgstr "Входящая скорость" msgid "Output speed" msgstr "Исходящая скорость" -#: abonapp/templates/abonapp/services.html:52 -msgid "Priority up" -msgstr "Повысить приоритет" - -#: abonapp/templates/abonapp/services.html:58 -msgid "Priority down" -msgstr "Понизить приоритет" +#: abonapp/templates/abonapp/services.html:14 +msgid "Works until" +msgstr "Действует до" #: abonapp/templates/abonapp/services.html:64 msgid "Delete service" @@ -567,231 +710,241 @@ msgstr "Просмотр абонента" msgid "yes,no" msgstr "Да,Нет" -#: abonapp/views.py:51 +#: abonapp/views.py:75 msgid "create group success msg" msgstr "Группа успешно создана" -#: abonapp/views.py:54 abonapp/views.py:112 abonapp/views.py:253 +#: abonapp/views.py:78 abonapp/views.py:138 abonapp/views.py:284 +#: abonapp/views.py:342 abonapp/views.py:432 abonapp/views.py:648 +#: abonapp/views.py:791 msgid "fix form errors" msgstr "Некоторые поля заполнены не правильно, проверте ещё раз" -#: abonapp/views.py:89 abonapp/views.py:154 +#: abonapp/views.py:114 abonapp/views.py:177 msgid "delete group success msg" msgstr "Группа успешно удалена" -#: abonapp/views.py:109 +#: abonapp/views.py:135 msgid "create abon success msg" msgstr "Абонент успешно создан" -#: abonapp/views.py:126 +#: abonapp/views.py:149 msgid "Address" msgstr "Адрес" -#: abonapp/views.py:148 +#: abonapp/views.py:171 msgid "delete abon success msg" msgstr "Абонент успешно удалён" -#: abonapp/views.py:157 +#: abonapp/views.py:180 msgid "I not know what to delete" msgstr "Не понятно что удалять" -#: abonapp/views.py:161 +#: abonapp/views.py:184 #, python-format msgid "NAS says: '%s'" msgstr "NAS сказал: '%s'" -#: abonapp/views.py:174 +#: abonapp/views.py:201 msgid "fill account through admin side" msgstr "Пополнение счёта через админку" -#: abonapp/views.py:176 +#: abonapp/views.py:203 #, python-format msgid "Account filled successfully on %.2f" msgstr "" -#: abonapp/views.py:179 +#: abonapp/views.py:206 msgid "I not know the account id" msgstr "Счёт успешно пополнен на %.2f" -#: abonapp/views.py:251 +#: abonapp/views.py:282 msgid "edit abon success msg" msgstr "Абонент успешно изменён" -#: abonapp/views.py:258 -#, python-format -msgid "Ip address already exist" -msgstr "Такой ip уже у кого-то есть" - -#: abonapp/views.py:268 +#: abonapp/views.py:297 msgid "User has not have password, and cannot login" msgstr "Для абонента не задан пароль, он не сможет войти в учётку" -#: abonapp/views.py:315 +#: abonapp/views.py:299 abonapp/views.py:703 +msgid "User device was not found" +msgstr "Пользовательское устройство не найдено" + +#: abonapp/views.py:355 +#, fuzzy +#| msgid "Abon does not exist" +msgid "User does not exist" +msgstr "Абонент не найден" + +#: abonapp/views.py:388 msgid "Receipt has been created" msgstr "Квитанция на оплату была создана" -#: abonapp/views.py:339 +#: abonapp/views.py:419 msgid "Tariff has been picked" msgstr "Тариф успешно выбран" -#: abonapp/views.py:349 +#: abonapp/views.py:427 msgid "Tariff your picked does not exist" msgstr "Тариф, который вы выбрали, не существует" -#: abonapp/views.py:406 +#: abonapp/views.py:491 msgid "Refunds for unused resources" msgstr "Возврат средств за неиспользованные ресурсы" -#: abonapp/views.py:412 +#: abonapp/views.py:497 msgid "Service has been finished successfully" msgstr "Услуга успешно завершена" -#: abonapp/views.py:415 abonapp/views.py:446 +#: abonapp/views.py:500 abonapp/views.py:533 msgid "Not confirmed" msgstr "Действие не подтверждено" -#: abonapp/views.py:449 +#: abonapp/views.py:536 msgid "Service has been activated successfully" msgstr "Услуга успешно активирована" -#: abonapp/views.py:474 +#: abonapp/views.py:562 msgid "User has been detached from service" msgstr "Абонент отвязан от услуги" -msgid "Sub information" -msgstr "Информация абонента" - -msgid "Services" -msgstr "Услуги" +#: abonapp/views.py:645 +msgid "Passport information has been saved" +msgstr "Информация о паспорте сохранена" -msgid "Payment history" -msgstr "История платежей" +#: abonapp/views.py:653 abonapp/views.py:700 abonapp/views.py:720 +#: abonapp/views.py:761 +msgid "Abon does not exist" +msgstr "Абонент не найден" -msgid "Payments" -msgstr "Финансы" +#: abonapp/views.py:656 +msgid "Passport info for the user does not exist" +msgstr "Для абонента не найдены паспортные данные" -msgid "History of tasks" -msgstr "История задач" +#: abonapp/views.py:693 +msgid "Device has successfully attached" +msgstr "Устройство успешно прикреплено" -msgid "Dynamic Field" -msgstr "Динамическое поле" +#: abonapp/views.py:698 +msgid "Device your selected already does not exist" +msgstr "Устройство, выбранное вами, уже не существует" -msgid "Mac Address" -msgstr "Мак" +#: abonapp/views.py:718 +msgid "Device has successfully unattached" +msgstr "Устройство успешно откреплено" -msgid "Port" -msgstr "Порт" +#: abonapp/views.py:764 +msgid "Group what you want doesn't exist" +msgstr "Указанная вами группа не найдена" -msgid "Enter a valid MAC Address." -msgstr "Введите валидный mac адрес" +#: abonapp/views.py:789 +msgid "Extra field successfully created" +msgstr "Динамичесое поле добавлено успешно" -msgid "Reset option82" -msgstr "Сбросить option82" +#: abonapp/views.py:819 +msgid "Extra fields has been saved" +msgstr "Динамические поля сохранены" -msgid "Delete" -msgstr "Удалить" +#: abonapp/views.py:821 +msgid "One or more extra fields has not been saved" +msgstr "Поле или одно из полей не найдено" -msgid "Instance of a option82 unexpectiadly disappeared" -msgstr "Экземпляр option82 неожиданно исчез из базы" +#: abonapp/views.py:833 +msgid "Extra field successfully deleted" +msgstr "Динамическое поле успешно удалено" -msgid "SNMP error on device" -msgstr "Ошибка в SNMP на устройстве" +#: abonapp/views.py:843 +msgid "no ping" +msgstr "не пингуется" -msgid "No streets found for that group" -msgstr "Не найдены улицы для группы" +#: abonapp/views.py:850 abonapp/views.py:860 +msgid "ping ok" +msgstr "пингуется" -msgid "Date of acceptance" -msgstr "Дата выдачи" +#: abonapp/views.py:855 +#, python-format +msgid "ok ping, %d/%d loses" +msgstr "пингуется, %d/%d" -msgid "Passport information has been saved" -msgstr "Информация о паспорте сохранена" +#: abonapp/views.py:858 +#, python-format +msgid "no ping, %d/%d loses" +msgstr "не пингуется, %d/%d" -msgid "Abon does not exist" -msgstr "Абонент не найден" +#: abonapp/templates/abonapp/ext.html:31 +msgid "Services" +msgstr "Услуги" -msgid "Passport info for the user does not exist" -msgstr "Для абонента не найдены паспортные данные" +#: abonapp/templates/abonapp/ext.html:43 +msgid "Payments" +msgstr "Финансы" -msgid "currency" -msgstr "руб" +#: abonapp/templates/abonapp/ext.html:48 +msgid "History of tasks" +msgstr "История задач" +#: abonapp/templates/abonapp/ext.html:53 msgid "Charts" msgstr "Графики" -msgid "Group what you want doesn't exist" -msgstr "Указанная вами группа не найдена" +#: abonapp/templates/abonapp/ext.html:26 +msgid "Sub information" +msgstr "Информация абонента" -msgid "User device was not found" -msgstr "Пользовательское устройство не найдено" +msgid "Streets" +msgstr "Улицы" -msgid "Add clutch" -msgstr "Добавить муфту" +msgid "Dialing" +msgstr "Звонки" -msgid "Remove clutch" -msgstr "Удалить муфту" +msgid "Device port" +msgstr "Порт устройства" -msgid "Select a device" -msgstr "Выберите устройство" -msgid "Device your selected already does not exist" -msgstr "Устройство, выбранное вами, уже не существует" +msgid "Ports does not exist" +msgstr "Порты не найдены" -msgid "Device has successfully attached" -msgstr "Устройство успешно прикреплено" +msgid "User port has been saved" +msgstr "Порт абонента успешно выбран" -msgid "Device has successfully unattached" -msgstr "Устройство успешно откреплено" +msgid "Selected port does not exist" +msgstr "Выбранный порт не существует" -msgid "Works until" -msgstr "Действует до" +msgid "Device" +msgstr "Устройство" -msgid "Do" -msgstr "Действия" +msgid "Is dynamic network settings" +msgstr "Динамические настройки по dhcp" -msgid "Last traffic" -msgstr "Последний траффик" +msgid "Method is not POST" +msgstr "Метод не POST" -msgid "Extra field successfully created" -msgstr "Динамичесое поле добавлено успешно" +msgid "Call to" +msgstr "Позвонить" -msgid "Extra field successfully deleted" -msgstr "Динамическое поле успешно удалено" - -msgid "Extra field does not exist" -msgstr "Поле не найдено" - -msgid "Extra fields" -msgstr "Динамические записи" - -msgid "Add extra field" -msgstr "Добавить новое динамическое поле" - -msgid "Add" -msgstr "Добавить" - -msgid "Field title" -msgstr "Название поля" +msgid "That service already activated" +msgstr "Эта услуга уже подключена" -msgid "Field type" -msgstr "Тип динамического поля" +msgid "Service already activated" +msgstr "Услуга уже подключена" -msgid "Field content" -msgstr "Содержимое динамического поля" +msgid "We have a problem in DB: AbonTariff instance has no related to service" +msgstr "У нас проблема с БД: экземпляр AbonTariff не имеет отношения к тарифу" -msgid "Extra fields has been saved" -msgstr "Динамические поля сохранены" +msgid "Date of start" +msgstr "Дата начала" -msgid "One or more extra fields has not been saved" -msgstr "Поле или одно из полей не найдено" +msgid "Subscriber has no service" +msgstr "У абонента нет услуги" -msgid "ok ping, %d/%d loses" -msgstr "пингуется, %d/%d" +msgid "This group has no services" +msgstr "У этой группы нет услуг" -msgid "no ping, %d/%d loses" -msgstr "не пингуется, %d/%d" +msgid "Attach serices to groups" +msgstr "Привязать услуги к группам" -msgid "no ping" -msgstr "не пингуется" +msgid "Attach services to group" +msgstr "Привязать услуги к этой группе" -msgid "ping ok" -msgstr "пингуется" +msgid "User that is no staff can not buy admin services" +msgstr "Пользователь, который не является персоналом не может покупать услуги для внутренних нужд" diff --git a/abonapp/migrations/0014_auto_20170330_1452.py b/abonapp/migrations/0014_auto_20170330_1452.py index a21e8cc..c0985ab 100644 --- a/abonapp/migrations/0014_auto_20170330_1452.py +++ b/abonapp/migrations/0014_auto_20170330_1452.py @@ -2,9 +2,9 @@ # Generated by Django 1.9 on 2017-03-30 11:52 from __future__ import unicode_literals -import abonapp.fields -from django.db import migrations, models import django.db.models.deletion +import djing.fields +from django.db import migrations, models class Migration(migrations.Migration): @@ -18,7 +18,7 @@ class Migration(migrations.Migration): name='Opt82', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('mac', abonapp.fields.MACAddressField(integer=True)), + ('mac', djing.fields.MACAddressField(integer=True)), ('port', models.PositiveSmallIntegerField(default=0)), ], options={ diff --git a/abonapp/migrations/0021_auto_20170705_1403.py b/abonapp/migrations/0021_auto_20170705_1403.py new file mode 100644 index 0000000..16a2a15 --- /dev/null +++ b/abonapp/migrations/0021_auto_20170705_1403.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-07-05 14:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('devapp', '0006_auto_20170705_1403'), + ('abonapp', '0020_auto_20170517_1655'), + ] + + operations = [ + migrations.RemoveField( + model_name='abon', + name='opt82', + ), + migrations.DeleteModel( + name='Opt82', + ), + migrations.AddField( + model_name='abon', + name='dev_port', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='devapp.Port'), + ), + migrations.AddField( + model_name='abon', + name='device', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='devapp.Device'), + ), + migrations.AddField( + model_name='abon', + name='is_dynamic_ip', + field=models.BooleanField(default=False), + ), + migrations.DeleteModel( + name='AbonDevice', + ), + ] diff --git a/abonapp/migrations/0022_auto_20170816_1109.py b/abonapp/migrations/0022_auto_20170816_1109.py new file mode 100644 index 0000000..d514365 --- /dev/null +++ b/abonapp/migrations/0022_auto_20170816_1109.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-08-16 11:09 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('abonapp', '0021_auto_20170705_1403'), + ] + + operations = [ + migrations.AlterModelOptions( + name='abontariff', + options={'permissions': (('can_complete_service', 'Снятие со счёта средств'),)}, + ), + migrations.RemoveField( + model_name='abon', + name='current_tariffs', + ), + migrations.AddField( + model_name='abon', + name='current_tariff', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='abonapp.AbonTariff'), + ), + migrations.AlterUniqueTogether( + name='abontariff', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='abontariff', + name='abon', + ), + migrations.RemoveField( + model_name='abontariff', + name='tariff_priority', + ), + ] diff --git a/abonapp/models.py b/abonapp/models.py index 0ac2a8e..9b79aa9 100644 --- a/abonapp/models.py +++ b/abonapp/models.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from datetime import datetime from django.core.exceptions import ValidationError from django.utils import timezone from django.db import models @@ -8,8 +7,8 @@ from django.utils.translation import ugettext as _ from agent import Transmitter, AbonStruct, TariffStruct, NasFailedResult, NasNetworkError from tariff_app.models import Tariff from accounts_app.models import UserProfile -from .fields import MACAddressField from mydefs import MyGenericIPAddressField, ip2int, LogicError, ip_addr_regex +from djing import settings class AbonGroup(models.Model): @@ -42,107 +41,32 @@ class AbonLog(models.Model): class AbonTariff(models.Model): - abon = models.ForeignKey('Abon') tariff = models.ForeignKey(Tariff, related_name='linkto_tariff') - tariff_priority = models.PositiveSmallIntegerField(default=0) - # время начала действия, остальные что не начали действие - NULL + # время начала действия услуги time_start = models.DateTimeField(null=True, blank=True, default=None) # время завершения услуги deadline = models.DateTimeField(null=True, blank=True, default=None) - def priority_up(self): - # ищем услугу с большим приоритетом(число приоритета меньше) - target_abtar = AbonTariff.objects.filter( - abon=self.abon, - tariff_priority__lt=self.tariff_priority - ).order_by('-tariff_priority')[:1] - if target_abtar.count() > 0: - target_abtar = target_abtar[0] - else: - return - - # Ищем текущий тариф абонента - active_abtar = AbonTariff.objects.filter( - abon=self.abon - )[:1] - if active_abtar.count() > 0: - active_abtar = active_abtar[0] - else: - return - - # Если услуга с которой хотим поменяться приоритетом является текущей то нельзя меняться - if active_abtar == target_abtar: - return - - # Swap приоритетов у текущего и найденного с меньшим tariff_priority (большим приоритетом) - tmp_prior = target_abtar.tariff_priority - target_abtar.tariff_priority = self.tariff_priority - target_abtar.save(update_fields=['tariff_priority']) - self.tariff_priority = tmp_prior - self.save(update_fields=['tariff_priority']) - - def priority_down(self): - # ищем услугу с меньшим приоритетом - target_abtar = AbonTariff.objects.filter( - abon=self.abon, - tariff_priority__gt=self.tariff_priority - )[:1] - if target_abtar.count() > 0: - target_abtar = target_abtar[0] - else: - # меньше нет, это самая последняя услуга - return - - # Swap приоритетов у текущего и найденного с большим tariff_priority (меньшим приоритетом) - tmp_pr = self.tariff_priority - self.tariff_priority = target_abtar.tariff_priority - target_abtar.tariff_priority = tmp_pr - target_abtar.save(update_fields=['tariff_priority']) - self.save(update_fields=['tariff_priority']) - - # Считает текущую стоимость услуг согласно выбранной для тарифа логики оплаты (см. в документации) def calc_amount_service(self): amount = self.tariff.amount return round(amount, 2) - # Активируем тариф - def activate(self, current_user, deadline=None): - calc_obj = self.tariff.get_calc_type()(self) - amnt = self.tariff.amount - # если не хватает денег - if self.abon.ballance < amnt: - raise LogicError(_('not enough money')) - # считаем дату активации услуги - self.time_start = timezone.now() - # считаем дату завершения услуги - if deadline is None: - self.deadline = calc_obj.calc_deadline() - else: - self.deadline = deadline - # снимаем деньги за услугу - self.abon.make_pay(current_user, amnt) - self.save() - # Используется-ли услуга сейчас, если время старта есть то он активирован def is_started(self): - return True if self.time_start is not None else False + return False if self.time_start is None else True def __str__(self): - return "%d: '%s' - '%s'" % ( - self.tariff_priority, - self.tariff.title, - self.abon.get_short_name() + return "%d: %s" % ( + self.pk or 0, + self.tariff.title ) class Meta: - ordering = ('tariff_priority',) db_table = 'abonent_tariff' - unique_together = (('abon', 'tariff', 'tariff_priority'),) permissions = ( ('can_complete_service', _('finish service perm')), - ('can_activate_service', _('activate service perm')) ) @@ -199,20 +123,8 @@ class ExtraFieldsModel(models.Model): db_table = 'abon_extra_fields' -class Opt82(models.Model): - mac = MACAddressField() - port = models.PositiveSmallIntegerField(default=0) - - def __str__(self): - return "%s-%d" % (self.mac, self.port) - - class Meta: - db_table = 'opt_82' - unique_together = (('mac', 'port'),) - - class Abon(UserProfile): - current_tariffs = models.ManyToManyField(Tariff, through=AbonTariff) + current_tariff = models.ForeignKey(AbonTariff, null=True, blank=True, on_delete=models.SET_NULL) group = models.ForeignKey(AbonGroup, models.SET_NULL, blank=True, null=True) ballance = models.FloatField(default=0.0) ip_address = MyGenericIPAddressField(blank=True, null=True) @@ -220,22 +132,13 @@ class Abon(UserProfile): street = models.ForeignKey(AbonStreet, on_delete=models.SET_NULL, null=True, blank=True) house = models.CharField(max_length=12, null=True, blank=True) extra_fields = models.ManyToManyField(ExtraFieldsModel, blank=True) - opt82 = models.ForeignKey(Opt82, null=True, blank=True, on_delete=models.SET_NULL) - - _act_tar_cache = None + device = models.ForeignKey('devapp.Device', null=True, blank=True, on_delete=models.SET_NULL) + dev_port = models.ForeignKey('devapp.Port', null=True, blank=True, on_delete=models.SET_NULL) + is_dynamic_ip = models.BooleanField(default=False) - # возвращает текущий тариф для абонента - def active_tariff(self, use_cache=True): - if self._act_tar_cache and use_cache: - return self._act_tar_cache - - ats = AbonTariff.objects.filter(abon=self).exclude(time_start=None) - - if ats.count() > 0: - self._act_tar_cache = ats[0].tariff - return ats[0].tariff - else: - self._act_tar_cache = None + # возвращает связь с текущим тарифом для абонента + def active_tariff(self): + return self.current_tariff class Meta: db_table = 'abonent' @@ -263,23 +166,31 @@ class Abon(UserProfile): def pick_tariff(self, tariff, author, comment=None, deadline=None): assert isinstance(tariff, Tariff) - # выбераем связь ТарифАбонент с самым низким приоритетом - abtrf = AbonTariff.objects.filter(abon=self).order_by('-tariff_priority')[:1] - abtrf = abtrf[0] if abtrf.count() > 0 else None + amount = round(tariff.amount, 2) - # создаём новую связь с приоритетом ещё ниже - new_abtar = AbonTariff( - abon=self, - tariff=tariff, - tariff_priority=abtrf.tariff_priority + 1 if abtrf else -1 - ) + if not author.is_staff and tariff.is_admin: + raise LogicError(_('User that is no staff can not buy admin services')) - # Если это первая услуга в списке (фильтр по приоритету ничего не вернул) - if not abtrf: - # значит пробуем её активировать - new_abtar.activate(author, deadline) - else: - new_abtar.save() + if self.current_tariff is not None: + if self.current_tariff.tariff == tariff: + # Эта услуга уже подключена + raise LogicError(_('That service already activated')) + else: + # Не надо молча заменять услугу если какая-то уже есть + raise LogicError(_('Service already activated')) + + # если не хватает денег + if self.ballance < amount: + raise LogicError(_('not enough money')) + + new_abtar = AbonTariff(deadline=deadline, tariff=tariff) + new_abtar.save() + self.current_tariff = new_abtar + + # снимаем деньги за услугу + self.ballance -= amount + + self.save() # Запись об этом в лог AbonLog.objects.create( @@ -288,43 +199,23 @@ class Abon(UserProfile): comment=comment or _('Buy service default log') ) - # Пробует подключить новую услугу если пришло время - def activate_next_tariff(self, author): - ats = AbonTariff.objects.filter(abon=self).order_by('tariff_priority') - - nw = timezone.make_aware(datetime.now()) - - for at in ats: - # услуга не активна, продолжаем - if at.deadline is None: - continue - # если услуга просрочена - if nw > at.deadline: - print(_('service overdue log')) - # выберем следующую по приоритету - # next_tarifs = AbonTariff.objects.filter(tariff_priority__gt = self.tariff_priority, abon=self.abon) - next_tarifs = [tr for tr in ats if tr.tariff_priority > at.tariff_priority][:2] - #next_tarifs = filter(lambda tr: tr.tariff_priority > at.tariff_priority, ats)[:2] - - # и если что-нибудь из списка следующих услуг вернулось - то активируем - if len(next_tarifs) > 0: - next_tarifs[0].activate(author) - - # удаляем запись о текущей услугу. - at.delete() - return + # Производим расчёт услуги абонента, т.е. завершаем если пришло время + def bill_service(self, author): + abon_tariff = self.active_tariff() + nw = timezone.now() + # если услуга просрочена + if nw > abon_tariff.deadline: + print("Service %s for user %s is overdued, end service" % (abon_tariff.tariff, self)) + abon_tariff.delete() # есть-ли доступ у абонента к услуге, смотрим в tariff_app.custom_tariffs..manage_access() def is_access(self): - ats = AbonTariff.objects.filter(abon=self).exclude(time_start=None) - if not ats or ats.count() < 1: - return False - trf = ats[0].tariff - ct = trf.get_calc_type()(ats[0]) - if ct.manage_access(self): - return True - else: + abon_tariff = self.active_tariff() + if abon_tariff is None: return False + trf = abon_tariff.tariff + ct = trf.get_calc_type()(abon_tariff) + return ct.manage_access(self) # создаём абонента из структуры агента def build_agent_struct(self): @@ -332,33 +223,22 @@ class Abon(UserProfile): user_ip = ip2int(self.ip_address) else: return - inst_tariff = self.active_tariff() - if inst_tariff: - agent_trf = TariffStruct(inst_tariff.id, inst_tariff.speedIn, inst_tariff.speedOut) - else: + abon_tariff = self.active_tariff() + if abon_tariff is None: agent_trf = TariffStruct() + else: + trf = abon_tariff.tariff + agent_trf = TariffStruct(trf.id, trf.speedIn, trf.speedOut) return AbonStruct(self.pk, user_ip, agent_trf, bool(self.is_active)) def save(self, *args, **kwargs): # проверяем не-ли у кого такого-же ip - if Abon.objects.filter(ip_address=self.ip_address).exclude(pk=self.pk).count() > 0: + if self.ip_address is not None and Abon.objects.filter(ip_address=self.ip_address).exclude(pk=self.pk).count() > 0: self.is_bad_ip = True raise LogicError(_('Ip address already exist')) super(Abon, self).save(*args, **kwargs) -class AbonDevice(models.Model): - abon = models.ForeignKey(Abon) - device = models.ForeignKey('devapp.Device') - - def __str__(self): - return "%s - %s" % (self.abon, self.device) - - class Meta: - db_table = 'abon_device' - unique_together = ('abon', 'device') - - class PassportInfo(models.Model): series = models.CharField(max_length=4, validators=[validators.integer_validator]) number = models.CharField(max_length=6, validators=[validators.integer_validator]) @@ -437,7 +317,7 @@ class AbonRawPassword(models.Model): def abon_post_save(sender, instance, **kwargs): timeout = None if hasattr(instance, 'is_dhcp') and instance.is_dhcp: - timeout = 14400 + timeout = getattr(settings, 'DHCP_TIMEOUT', 14400) agent_abon = instance.build_agent_struct() if agent_abon is None: return True @@ -468,40 +348,14 @@ def abon_del_signal(sender, instance, **kwargs): return True -def abontariff_post_save(sender, instance, **kwargs): - # Тут или подключение абону услуги, или изменение приоритета - if not kwargs['created']: - # если изменение приоритета то не говорим об этом NAS'у - return - if instance.abon.ip_address is None: - return - try: - agent_abon = instance.abon.build_agent_struct() - if agent_abon is None: - return True - tm = Transmitter() - tm.update_user(agent_abon) - except (NasFailedResult, NasNetworkError): - return True - - -def abontariff_del_signal(sender, instance, **kwargs): - if not instance.is_started(): - # если удаляем не активную услугу то говорить об этом NAS'у не обязательно - return - if instance.abon.ip_address is None: - # если у абонента нет ip то и создавать правило не на кого - return - try: - agent_abon = instance.abon.build_agent_struct() - tm = Transmitter() - tm.pause_user(agent_abon) - except (NasFailedResult, NasNetworkError): - return True +def abon_tariff_post_init(sender, instance, **kwargs): + if getattr(instance, 'time_start') is None: + instance.time_start = timezone.now() + calc_obj = instance.tariff.get_calc_type()(instance) + if getattr(instance, 'deadline') is None: + instance.deadline = calc_obj.calc_deadline() models.signals.post_save.connect(abon_post_save, sender=Abon) models.signals.post_delete.connect(abon_del_signal, sender=Abon) - -models.signals.post_save.connect(abontariff_post_save, sender=AbonTariff) -models.signals.post_delete.connect(abontariff_del_signal, sender=AbonTariff) +models.signals.post_init.connect(abon_tariff_post_init, sender=AbonTariff) diff --git a/abonapp/pay_systems.py b/abonapp/pay_systems.py index f0f0606..4835348 100644 --- a/abonapp/pay_systems.py +++ b/abonapp/pay_systems.py @@ -62,13 +62,15 @@ def allpay(request): pays = AllTimePayLog.objects.filter(pay_id=pay_id) if pays.count() > 0: return bad_ret(-100) + + # тут в author передаём учётку абонента, т.к. это он сам через терминал пополняет + abon.add_ballance(abon, pay_amount, comment='AllPay %.2f' % pay_amount) + abon.save(update_fields=['ballance']) + AllTimePayLog.objects.create( pay_id=pay_id, summ=pay_amount ) - # тут в author передаём учётку абонента, т.к. это он сам через терминал пополняет - abon.add_ballance(abon, pay_amount, comment='AllPay %.2f' % pay_amount) - abon.save(update_fields=['ballance']) current_date = timezone.now().strftime("%d.%m.%Y %H:%M:%S") return "" \ "\n" +\ diff --git a/abonapp/templates/abonapp/activate_service.html b/abonapp/templates/abonapp/activate_service.html deleted file mode 100644 index 040b669..0000000 --- a/abonapp/templates/abonapp/activate_service.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends request.is_ajax|yesno:'bajax.html,base.html' %} -{% load i18n %} -{% block main %} - - - - - {% include 'message_block.html' %} - -
-
-

{% trans 'Activate service' %}

-
-
-
{% csrf_token %} - - -

-{% blocktrans with ballance=abon.ballance deadline=deadline|date:'d F Y, H:i:s' %} -Are you sure that you want activate service for the user?
-Note that the account will be removed from his money and open access to the resources of the paid services.
-Maintenance cost is {{ amount }}. On account of {{ ballance }}, will be {{ diff }}
-It will work until {{ deadline }}{% endblocktrans %} -

- - -
-
-
- -{% endblock %} \ No newline at end of file diff --git a/abonapp/templates/abonapp/buy_tariff.html b/abonapp/templates/abonapp/buy_tariff.html index ab4aad9..a8cce4c 100644 --- a/abonapp/templates/abonapp/buy_tariff.html +++ b/abonapp/templates/abonapp/buy_tariff.html @@ -22,41 +22,55 @@
{% csrf_token %}
- + {% if tariffs %} + +
+ + +
+ {% if not abon.active_tariff %} +
+ + + +
+ {% endif %} + {% else %} + -
- - -
- {% if not abon.active_tariff %} -
- - - -
{% endif %}
- -
diff --git a/abonapp/templates/abonapp/charts.html b/abonapp/templates/abonapp/charts.html index a740699..a9481b0 100644 --- a/abonapp/templates/abonapp/charts.html +++ b/abonapp/templates/abonapp/charts.html @@ -3,10 +3,10 @@ {% block content %}
-
+
-

График использования

+

{% trans 'Graph of use' %}

{% if charts_data %} @@ -16,7 +16,6 @@ new Chartist.Line('#chrt', { series: [ { - name: 'График траффика', data: [ {{ charts_data }} ] @@ -34,16 +33,31 @@ labelInterpolationFnc: function (value) { return moment(value).format('HH:mm:ss'); } - } + }, + lineSmooth: Chartist.Interpolation.cardinal({ + tension: 0 + }) }); }); {% else %} -

Траффик не найден

+

{% trans 'Static info was Not found' %}

{% endif %}
+
+
+
{% trans 'Graphs by dates' %}
+
+ {% for dat in dates %} + {{ dat|date:'j E' }} + {% empty %} + {% trans 'Static info was Not found' %} + {% endfor %} +
+
+
{% endblock %} diff --git a/abonapp/templates/abonapp/dial_log.html b/abonapp/templates/abonapp/dial_log.html new file mode 100644 index 0000000..695ab3a --- /dev/null +++ b/abonapp/templates/abonapp/dial_log.html @@ -0,0 +1,47 @@ +{% extends request.is_ajax|yesno:'nullcont.htm,abonapp/ext.htm' %} +{% load i18n %} +{% block content %} + + + + + + + + + + + + + + + + + {% for log in logs %} + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + + +
{% trans 'Play' %}{% trans 'calldate' %}{% trans 'src' %}{% trans 'dst' %}{% trans 'duration' %}{% trans 'start' %}{% trans 'answer' %}{% trans 'end' %}{% trans 'disposition' %}
+ + {{ log.calldate|date:'d E Y, H:i:s' }}{{ log.src }}{{ log.dst }}{{ log.duration }}{{ log.start|date:'d E Y, H:i:s' }}{{ log.answer|date:'d E Y, H:i:s' }}{{ log.end|date:'d E Y, H:i:s' }}{{ log.locate_disposition }}
{% trans 'Calls was not found' %}
+ + {% include 'toolbar_page.html' with pag=logs %} + +{% endblock %} diff --git a/abonapp/templates/abonapp/editAbon.html b/abonapp/templates/abonapp/editAbon.html index 3a2cda1..e5883ae 100644 --- a/abonapp/templates/abonapp/editAbon.html +++ b/abonapp/templates/abonapp/editAbon.html @@ -28,14 +28,21 @@
- {{ form.telephone }}{{ form.telephone.errors }} +
+ {{ form.telephone }}{{ form.telephone.errors }} + + + + + +
- +
@@ -106,23 +113,6 @@
- - {% if ip %}
@@ -140,36 +130,66 @@
-

{% trans 'DHCP information' %}

+

{% trans 'Select the device' %}

+
- {% csrf_token %} + {% csrf_token %}
- -
- {{ tech_form.mac }}{{ tech_form.mac.errors }} + +
+ {% if device %}
- +
- {{ tech_form.port }}{{ tech_form.port.errors }} + +
+
+
+
+
-
- - {% trans 'Delete' %} - +
- + {% endif %}
diff --git a/abonapp/templates/abonapp/ext.htm b/abonapp/templates/abonapp/ext.htm index 2aaff6f..4d0ca3f 100644 --- a/abonapp/templates/abonapp/ext.htm +++ b/abonapp/templates/abonapp/ext.htm @@ -53,6 +53,11 @@ {% trans 'Charts' %} + {% url 'abonapp:dials' abon_group.pk abon.pk as abdials %} + + {% trans 'Dialing' %} + +
diff --git a/abonapp/templates/abonapp/modal_addstreet.html b/abonapp/templates/abonapp/modal_addstreet.html new file mode 100644 index 0000000..c723157 --- /dev/null +++ b/abonapp/templates/abonapp/modal_addstreet.html @@ -0,0 +1,38 @@ +{% load i18n %} +
{% csrf_token %} + + + {% include 'message_block.html' %} + + + +
diff --git a/abonapp/templates/abonapp/modal_dev.html b/abonapp/templates/abonapp/modal_dev.html index 4e67bbd..38902f2 100644 --- a/abonapp/templates/abonapp/modal_dev.html +++ b/abonapp/templates/abonapp/modal_dev.html @@ -10,12 +10,14 @@ + + diff --git a/abonapp/templates/abonapp/peoples.html b/abonapp/templates/abonapp/peoples.html index 5f79838..905ea2e 100644 --- a/abonapp/templates/abonapp/peoples.html +++ b/abonapp/templates/abonapp/peoples.html @@ -18,6 +18,7 @@ + - + - + + {% with can_ch_trf=perms.tariff_app.change_tariff can_del_trf=perms.abonapp.delete_abon %} {% for human in peoples %} {% if human.is_active %} {% else %} {% endif %} + - + - + {% empty %} - {% endfor %} + {% endwith %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
# {% trans 'Sub' %} @@ -45,57 +46,64 @@ - {% trans 'House' %}/{% trans 'Apartment' %} + {% trans 'Apartment' %} {% if order_by == 'house' %}{% endif %} {% trans 'Telephone' %}{% trans 'Service' %} {% trans 'Ballance' %} {% if order_by == 'ballance' %}{% endif %} ##
{% if human.stat_cache and human.stat_cache.is_online %} + + {% else %} + + {% endif %} {{ human.username }} - {% if human.is_online %} - - {% endif %} - {% if human.traf %} - {{ human.traf.cur_time|date:"H:i" }} + {% if human.stat_cache %} + {% if human.stat_cache.is_today %} + {{ human.stat_cache.last_time|date:"H:i" }} + {% else %} + {{ human.stat_cache.last_time|date:"D H:i" }} + {% endif %} {% endif %} {{ human.ip_address|default:_('Not assigned') }} {{ human.fio }} {{ human.street|default:_('Not assigned') }}{{ human.house|default:_('Not assigned') }}{{ human.house|default:'-' }} {{ human.telephone }} + {% if human.active_tariff %} + {% if perms.tariff_app.change_tariff %} + {{ human.active_tariff.tariff.title }} + {% else %} + {{ human.active_tariff.tariff.title }} + {% endif %} + {% else %}——— + {% endif %} + {{ human.ballance }} - {% if perms.abonapp.delete_abon %} + {% if can_del_trf %} @@ -104,7 +112,7 @@
+ {% trans 'Subscribers not found' %}. {% if perms.abonapp.add_abon %} {% trans 'Add abon' %} @@ -112,10 +120,11 @@
+ {% if perms.abonapp.add_abon %} {% trans 'Add abon' %} @@ -134,16 +143,27 @@ {% include 'toolbar_page.html' with pag=peoples %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/abonapp/templates/abonapp/service.html b/abonapp/templates/abonapp/service.html new file mode 100644 index 0000000..976b982 --- /dev/null +++ b/abonapp/templates/abonapp/service.html @@ -0,0 +1,119 @@ +{% extends request.is_ajax|yesno:'nullcont.htm,abonapp/ext.htm' %} +{% load i18n %} +{% block content %} + +
+
+ +
+
+

{% trans "Subscriber's service" %}

+
+
+ {% if abon_tariff %} + +
+
{% trans 'Service' %}
+
+ {% if abon_tariff.tariff %} + {% if perms.tariff_app.change_tariff %} + + {{ abon_tariff.tariff.title }} + + {% else %} + {{ abon_tariff.tariff.title }} + {% endif %} + {% else %} + {% trans 'We have a problem in DB: AbonTariff instance has no related to service' %} + {% endif %} +
+
{% trans 'Sum' %}
+
{{ abon_tariff.tariff.amount }} {% trans 'currency' %}.
+ +
{% trans 'Input speed' %}
+
{{ abon_tariff.tariff.speedIn }}
+ +
{% trans 'Output speed' %}
+
{{ abon_tariff.tariff.speedOut }}
+ +
{% trans 'Date of start' %}
+
{{ abon_tariff.time_start|date:"d E Y, l H:i" }}
+ +
{% trans 'Works until' %}
+
{{ abon_tariff.deadline|date:"d E Y, l H:i" }}
+
+ +
+

{{ abon_tariff.tariff.descr }}

+
+ + {% else %} + {% trans 'Subscriber has no service' %}. + + {% trans 'Buy service' %} + + {% endif %} + + {% if abon_tariff %} + + {% trans 'Finish service' %} + + {% endif %} +
+
+
+
+
+
+

Услуги для заказа

+
+
+ + + + + + + + + + + + {% with can_ch_trf=perms.tariff_app.change_tariff %} + {% for service in services %} + + + + + + + + {% empty %} + + {% endfor %} + {% endwith %} + +
{% trans 'Pick a service' %}{% trans 'Service' %}{% trans 'Price' %}{% trans 'Speed In' %}{% trans 'Speed Out' %}
+ + + {% if can_ch_trf %} + {{ service.title }} + {% else %} + {{ service.title }} + {% endif %} + {{ service.amount }} {% trans 'currency' %}{{ service.speedIn }}{{ service.speedOut }}
+ {% trans 'This group has no services' %} + + {% trans 'Tariffs in groups' %} + +
+ + {% trans 'Attach services to group' %} + +
+
+
+
+ +{% endblock %} diff --git a/abonapp/templates/abonapp/services.html b/abonapp/templates/abonapp/services.html deleted file mode 100644 index 47b67a9..0000000 --- a/abonapp/templates/abonapp/services.html +++ /dev/null @@ -1,101 +0,0 @@ -{% extends request.is_ajax|yesno:'nullcont.htm,abonapp/ext.htm' %} -{% load i18n %} -{% block content %} - - {% trans 'Services of subscriber' %} - - - - - - - - - - - - - - - {% for trf in abon_tarifs %} - - - - - - - - {% if trf.id != active_abontariff_id %} - - {% else %} - - {% endif %} - - {% empty %} - - - - {% endfor %} - - {% if perms.abonapp.can_buy_tariff %} - - - - - - {% endif %} -
{% trans 'Priority' %}{% trans 'Service' %}{% trans 'Sum' %}{% trans 'Input speed' %}{% trans 'Output speed' %}{% trans 'Works until' %}{% trans 'Do' %}
{{ trf.tariff_priority }} - - {% if perms.tariff_app.change_tariff %} - - {{ trf.tariff.title }} - - {% else %} - {{ trf.tariff.title }} - {% endif %} - - {{ trf.tariff.amount }}{{ trf.tariff.speedIn }}{{ trf.tariff.speedOut }}{{ trf.deadline|date:"d E Y, l" }} - - {% if perms.abonapp.can_activate_service %} - {% if not active_abontariff_id %} - - - - {% endif %} - {% endif %} - - - - - - - - - - - - {% if perms.abonapp.delete_abontariff %} - - - - {% endif %} - - - {% trans 'Finish service' %} - -
{% trans 'Services of subscribers not found' %}. - {% if perms.abonapp.can_buy_tariff %} - {% trans 'Buy' %} - {% endif %} -
- - {% trans 'Buy service' %} - -
- - -{% endblock %} \ No newline at end of file diff --git a/abonapp/tests.py b/abonapp/tests.py deleted file mode 100644 index b66baf8..0000000 --- a/abonapp/tests.py +++ /dev/null @@ -1,225 +0,0 @@ -from django.shortcuts import resolve_url -from django.test import TestCase -from django.test.client import Client -from agent import NasNetworkError -from .models import AbonTariff, Abon, AbonGroup, AbonRawPassword -from tariff_app.models import Tariff -from mydefs import LogicError - - -class AbonTestCase(TestCase): - def setUp(self): - try: - Tariff.objects.create( - title='test_tariff', - descr='taroff descr', - speedIn=1.2, - speedOut=3.0, - amount=3 - ) - abon = Abon() - abon.username = '1234567' - abon.fio = 'mainuser' - abon.telephone = '+79788328884' - abon.set_password('ps') - abon.is_superuser = True - abon.save() - abon_group = AbonGroup.objects.create(title='abon_group') - abon_group.profiles.add(abon) - except NasNetworkError: - pass - - # проверка на пополнение счёта - def test_add_ballance(self): - try: - abon = Abon.objects.get(username='1234567') - ballance = abon.ballance - abon.add_ballance(abon, 13, 'test pay') - abon.save(update_fields=['ballance']) - self.assertEqual(abon.ballance, ballance+13) - ballance = abon.ballance - abon.add_ballance(abon, 5.34, 'test float pay') - abon.save(update_fields=['ballance']) - self.assertEqual(abon.ballance, ballance+5.34) - except NasNetworkError: - pass - - # пробуем выбрать услугу - def test_pick_tariff(self): - try: - tariff = Tariff.objects.get(title='test_tariff') - abon = Abon.objects.get(username='1234567') - try: - abon.pick_tariff(tariff, abon) - # нет денег, должно всплыть исключение и сюда дойти мы не должны - self.assertFalse(True) - except LogicError: - pass - act_tar = abon.active_tariff() - # если недостаточно денег на счету - assert abon.ballance <= tariff.amount - # У абонента на счету 0, не должна быть куплена услуга - self.assertEqual(act_tar, None) - # Раз услуги нет то и доступа быть не должно - self.assertTrue(not abon.is_access()) - - # с деньгами - abon.add_ballance(abon, 7.34, 'add pay for test pick tariff') - abon.pick_tariff(tariff, abon) - act_tar = abon.active_tariff() - # должны получить указанную услугу - self.assertEqual(act_tar, tariff) - # и получить доступ - self.assertTrue(abon.is_access()) - except NasNetworkError: - pass - - # тестим очередь услуг - def test_services_queue(self): - abon = Abon.objects.get(username='1234567') - tariff = Tariff.objects.get(title='test_tariff') - abon.add_ballance(abon, 9, 'add pay for test services queue') - abon.save() - abon.pick_tariff(tariff, abon) - abon.pick_tariff(tariff, abon) - abon.pick_tariff(tariff, abon) - # снять деньги должно было только за первый выбор, остальные стают в очередь услуг - self.assertEqual(abon.ballance, 6) - - c = Client() - # login - c.post(resolve_url('acc_app:login'), {'login': '1234567', 'password': 'ps'}) - url = resolve_url('abonapp:compl_srv', gid=1, uid=1, srvid=1) - resp = c.get(url) - print('RESP:', resp) - self.assertEqual(resp.status_code, 200) - resp = c.post(url, data={ - 'finish_confirm': 'yes' - }) - print('RESP:', resp) - # при успешной остановке услуги идёт редирект на др страницу - self.assertEqual(resp.status_code, 302) - # текущей услуги быть не должно - act_tar = abon.active_tariff() - self.assertIsNone(act_tar) - # не активных услуг останется 2 - noact_count = AbonTariff.objects.filter(abon=abon).filter(time_start=None).count() - self.assertEqual(noact_count, 2) - - # проверяем платёжку alltime - def test_allpay(self): - from hashlib import md5 - from djing.settings import pay_SECRET, pay_SERV_ID - import xmltodict - - def sig(act, pay_account, pay_id): - md = md5() - s = '_'.join((str(act), str(pay_account), pay_SERV_ID, str(pay_id), pay_SECRET)) - md.update(bytes(s, 'utf-8')) - return md.hexdigest() - - c = Client() - url = resolve_url('abonapp:terminal_pay') - r = c.get(url, { - 'ACT': 1, 'PAY_ACCOUNT': '1234567', - 'SERVICE_ID': pay_SERV_ID, - 'PAY_ID': 3561234, - 'TRADE_POINT': 377, - 'SIGN': sig(1, 1234567, 3561234) - }) - xobj = xmltodict.parse(r.content) - self.assertEqual(int(xobj['pay-response']['status_code']), 21) - r = c.get(url, { - 'ACT': 4, 'PAY_ACCOUNT': '1234567', - 'SERVICE_ID': pay_SERV_ID, - 'PAY_ID': 3561234, - 'PAY_AMOUNT': 1.0, - 'TRADE_POINT': 377, - 'SIGN': sig(4, 1234567, 3561234) - }) - xobj = xmltodict.parse(r.content) - self.assertEqual(int(xobj['pay-response']['status_code']), 22) - r = c.get(url, { - 'ACT': 4, 'PAY_ACCOUNT': '1234567', - 'SERVICE_ID': pay_SERV_ID, - 'PAY_ID': 3561234, - 'PAY_AMOUNT': 1.0, - 'TRADE_POINT': 377, - 'SIGN': sig(4, 1234567, 3561234) - }) - xobj = xmltodict.parse(r.content) - self.assertEqual(int(xobj['pay-response']['status_code']), -100) - r = c.get(url, { - 'ACT': 7, 'PAY_ACCOUNT': '1234567', - 'SERVICE_ID': pay_SERV_ID, - 'PAY_ID': 3561234, - 'PAY_AMOUNT': 1.0, - 'TRADE_POINT': 377, - 'SIGN': sig(7, 1234567, 3561234) - }) - xobj = xmltodict.parse(r.content) - self.assertEqual(int(xobj['pay-response']['status_code']), 11) - abon = Abon.objects.get(username='1234567') - self.assertEqual(abon.ballance, 1) - - # пробуем добавить группу абонентов - def test_add_abongroup(self): - abon = Abon.objects.get(username='1234567') - ag = AbonGroup.objects.create(title='%&34%$&*(') - ag.profiles.add(abon) - - # пробуем добавить абонента - def test_add_abon(self): - c = Client() - c.login(username='1234567', password='ps') - url = resolve_url('abonapp:add_abon', gid=1) - r = c.get(url) - # поглядим на страницу добавления абонента - self.assertEqual(r.status_code, 200) - r = c.post(url, { - 'username': '123', - 'password': 'ps', - 'fio': 'Abon Fio', - 'telephone': '+79783753914', - 'is_active': True - }) - self.assertEqual(r.status_code, 302) - r = c.get(resolve_url('abonapp:add_abon', gid=324)) - self.assertEqual(r.status_code, 404) - try: - abn = Abon.objects.get(username='123') - self.assertIsNotNone(abn) - psw = AbonRawPassword.objects.get(account=abn, passw_text='ps') - self.assertIsNotNone(psw) - except Abon.DoesNotExist: - # абонент должен был создаться - self.assertTrue(False) - except AbonRawPassword.DoesNotExist: - # должен быть пароль абонента простым текстом - self.assertTrue(False) - - # пробуем удалить абонента - def test_view_delentity(self): - c = Client() - c.login(username='1234567', password='ps') - url = resolve_url('abonapp:del_abon') + '?t=a&id=1' - r = c.get('/abons/1/addabon') - - -class AbonTariffTestCase(TestCase): - def setUp(self): - abon = Abon.objects.create( - username='1234567', - telephone='+79788328884' - ) - tariff = Tariff.objects.create( - title='test_tariff', - descr='taroff descr', - speedIn=1.2, - speedOut=3.0, - amount=3 - ) - AbonTariff.objects.create( - abon=abon, - tariff=tariff - ) diff --git a/abonapp/urls_abon.py b/abonapp/urls_abon.py index b93c5d3..2922a6b 100644 --- a/abonapp/urls_abon.py +++ b/abonapp/urls_abon.py @@ -6,6 +6,9 @@ urlpatterns = [ url(r'^$', views.peoples, name='people_list'), url(r'^addabon$', views.addabon, name='add_abon'), url(r'^services$', views.chgroup_tariff, name='ch_group_tariff'), + url(r'^street/add$', views.street_add, name='street_add'), + url(r'^street/edit', views.street_edit, name='street_edit'), + url(r'^street/(?P\d+)/delete$', views.street_del, name='street_del'), url(r'^(?P\d+)$', views.abonhome, name='abon_home'), url(r'^(?P\d+)/services$', views.abon_services, name='abon_services'), @@ -15,21 +18,20 @@ urlpatterns = [ url(r'^(?P\d+)/addinvoice$', views.add_invoice, name='add_invoice'), url(r'^(?P\d+)/pick$', views.pick_tariff, name='pick_tariff'), - url(r'^(?P\d+)/chpriority$', views.chpriority, name='chpriority_tariff'), url(r'^(?P\d+)/passport_view$', views.passport_view, name='passport_view'), url(r'^(?P\d+)/complete_service(?P\d+)$', views.complete_service, name='compl_srv'), - url(r'^(?P\d+)/activate_service(?P\d+)$', views.activate_service, name='activate_service'), - url(r'^(?P\d+)/opt82$', views.opt82, name='opt82'), url(r'^(?P\d+)/chart$', views.charts, name='charts'), + url(r'^(?P\d+)/dials$', views.dials, name='dials'), url(r'^(?P\d+)/extra_field$', views.make_extra_field, name='extra_field'), url(r'^(?P\d+)/extra_field/(?P\d+)/delete$', views.extra_field_delete, name='extra_field_delete'), url(r'^(?P\d+)/extra_field/edit$', views.extra_field_change, name='extra_field_edit'), - url(r'^(?P\d+)/unsubscribe_service(?P\d+)$', views.unsubscribe_service, + url(r'^(?P\d+)/unsubscribe_service(?P\d+)$', views.unsubscribe_service, name='unsubscribe_service'), url(r'^(?P\d+)/dev/$', views.dev, name='dev'), url(r'^(?P\d+)/clear_dev/$', views.clear_dev, name='clear_dev'), - url(r'^(?P\d+)/task_log$', views.task_log, name='task_log') + url(r'^(?P\d+)/task_log$', views.task_log, name='task_log'), + url(r'^(?P\d+)/user_dev$', views.save_user_dev_port, name='save_user_dev_port') ] diff --git a/abonapp/views.py b/abonapp/views.py index 7759ae7..a08d9ea 100644 --- a/abonapp/views.py +++ b/abonapp/views.py @@ -3,7 +3,8 @@ from json import dumps from django.contrib.gis.shortcuts import render_to_text from django.core.exceptions import PermissionDenied from django.db import IntegrityError, ProgrammingError -from django.db.models import Count +from django.db.models import Count, Q +from django.db.transaction import atomic from django.shortcuts import render, redirect, get_object_or_404, resolve_url from django.contrib.auth.decorators import login_required, permission_required from django.utils import timezone @@ -11,14 +12,17 @@ from django.http import HttpResponse from django.contrib import messages from django.utils.translation import ugettext_lazy as _ -from statistics.models import getModel +from statistics.models import StatCache from tariff_app.models import Tariff from agent import NasFailedResult, Transmitter, NasNetworkError from . import forms from . import models import mydefs -from devapp.models import Device -from datetime import datetime +from devapp.models import Device, Port as DevPort +from datetime import datetime, date +from taskapp.models import Task +from dialing_app.models import AsteriskCDR +from statistics.models import getModel, get_dates @login_required @@ -31,8 +35,6 @@ def peoples(request, gid): else: peoples_list = peoples_list.filter(group=gid) - StatModel = getModel() - # фильтр dr, field = mydefs.order_helper(request) if field: @@ -42,10 +44,10 @@ def peoples(request, gid): peoples_list = mydefs.pag_mn(request, peoples_list) for abon in peoples_list: if abon.ip_address is not None: - traf = StatModel.objects.traffic_by_ip(abon.ip_address) - if traf[1] is not None: - abon.traf = traf[1] - abon.is_online =traf[0] + try: + abon.stat_cache = StatCache.objects.get(ip=abon.ip_address) + except StatCache.DoesNotExist: + pass except mydefs.LogicError as e: messages.warning(request, e) @@ -189,6 +191,7 @@ def delentity(request): @login_required @permission_required('abonapp.can_add_ballance') +@atomic def abonamount(request, gid, uid): abon = get_object_or_404(models.Abon, pk=uid) try: @@ -242,16 +245,14 @@ def pay_history(request, gid, uid): @login_required @mydefs.only_admins def abon_services(request, gid, uid): + grp = get_object_or_404(models.AbonGroup, pk=gid) abon = get_object_or_404(models.Abon, pk=uid) - abon_tarifs = models.AbonTariff.objects.filter(abon=uid) - active_abontariff = abon_tarifs.exclude(time_start=None) - - return render(request, 'abonapp/services.html', { + return render(request, 'abonapp/service.html', { 'abon': abon, - 'abon_tarifs': abon_tarifs, - 'active_abontariff_id': active_abontariff[0].id if active_abontariff.count() > 0 else None, - 'abon_group': abon.group + 'abon_tariff': abon.current_tariff, + 'abon_group': abon.group, + 'services': grp.tariffs.all() }) @@ -262,20 +263,13 @@ def abonhome(request, gid, uid): abon_group = get_object_or_404(models.AbonGroup, pk=gid) frm = None passw = None - abon_device = None try: if request.method == 'POST': if not request.user.has_perm('abonapp.change_abon'): raise PermissionDenied frm = forms.AbonForm(request.POST, instance=abon) if frm.is_valid(): - # если нет option82, т.е. динамический ip то не сохраняем изменения ip - if abon.opt82 is None: - ip_str = request.POST.get('ip') - if ip_str: - abon.ip_address = ip_str - else: - abon.ip_address = None + abon.ip_address = request.POST.get('ip') frm.save() messages.success(request, _('edit abon success msg')) else: @@ -283,7 +277,8 @@ def abonhome(request, gid, uid): else: passw = models.AbonRawPassword.objects.get(account=abon).passw_text frm = forms.AbonForm(instance=abon, initial={'password': passw}) - abon_device = models.AbonDevice.objects.get(abon=abon) + if abon.device is None: + messages.warning(request, _('User device was not found')) except mydefs.LogicError as e: messages.error(request, e) passw = models.AbonRawPassword.objects.get(account=abon).passw_text @@ -293,8 +288,6 @@ def abonhome(request, gid, uid): messages.error(request, e) except models.AbonRawPassword.DoesNotExist: messages.warning(request, _('User has not have password, and cannot login')) - except models.AbonDevice.DoesNotExist: - messages.warning(request, _('User device was not found')) except mydefs.MultipleException as errs: for err in errs.err_list: messages.add_message(request, messages.constants.ERROR, err) @@ -306,8 +299,8 @@ def abonhome(request, gid, uid): 'abon_group': abon_group, 'ip': abon.ip_address, 'is_bad_ip': getattr(abon, 'is_bad_ip', False), - 'tech_form': forms.Opt82Form(instance=abon.opt82), - 'device': abon_device.device if abon_device is not None else None + 'device': abon.device, + 'dev_ports': DevPort.objects.filter(device=abon.device) if abon.device else None }) else: return render(request, 'abonapp/viewAbon.html', { @@ -318,40 +311,7 @@ def abonhome(request, gid, uid): }) -@login_required -@mydefs.only_admins -def opt82(request, gid, uid): - try: - abon = models.Abon.objects.get(pk=uid) - if request.method == 'POST': - try: - opt82_instance = models.Opt82.objects.get( - mac=request.POST.get('mac'), - port=request.POST.get('port') - ) - except models.Opt82.DoesNotExist: - frm = forms.Opt82Form(request.POST) - if frm.is_valid(): - opt82_instance = frm.save() - else: - messages.error(request, _('fix form errors')) - return redirect('abonapp:abon_home', gid=gid, uid=uid) - - abon.opt82 = opt82_instance - else: - act = request.GET.get('act') - if act is not None and act == 'release': - if abon.opt82 is not None: - abon.opt82.delete() - abon.opt82 = None - - abon.save(update_fields=['opt82']) - except models.Abon.DoesNotExist: - messages.error(request, _('User does not exist')) - return redirect('abonapp:abon_home', gid=gid, uid=uid) - - -@mydefs.require_ssl +@atomic def terminal_pay(request): from .pay_systems import allpay ret_text = allpay(request) @@ -397,6 +357,7 @@ def add_invoice(request, gid, uid): @login_required @permission_required('abonapp.can_buy_tariff') +@atomic def pick_tariff(request, gid, uid): grp = get_object_or_404(models.AbonGroup, pk=gid) abon = get_object_or_404(models.Abon, pk=uid) @@ -428,34 +389,14 @@ def pick_tariff(request, gid, uid): return render(request, 'abonapp/buy_tariff.html', { 'tariffs': tariffs, 'abon': abon, - 'abon_group': grp + 'abon_group': grp, + 'selected_tariff': mydefs.safe_int(request.GET.get('selected_tariff')) }) -@login_required -@mydefs.only_admins -def chpriority(request, gid, uid): - t = request.GET.get('t') - act = request.GET.get('a') - - current_abon_tariff = get_object_or_404(models.AbonTariff, pk=t) - - try: - if act == 'up': - current_abon_tariff.priority_up() - elif act == 'down': - current_abon_tariff.priority_down() - except (NasFailedResult, NasNetworkError) as e: - messages.error(request, e) - except mydefs.MultipleException as errs: - for err in errs.err_list: - messages.add_message(request, messages.constants.ERROR, err) - - return redirect('abonapp:abon_home', gid=gid, uid=uid) - - @login_required @permission_required('abonapp.can_complete_service') +@atomic def complete_service(request, gid, uid, srvid): abtar = get_object_or_404(models.AbonTariff, pk=srvid) abon = abtar.abon @@ -513,44 +454,11 @@ def complete_service(request, gid, uid, srvid): }) -@login_required -@permission_required('abonapp.can_activate_service') -def activate_service(request, gid, uid, srvid): - abtar = get_object_or_404(models.AbonTariff, pk=srvid) - amount = abtar.calc_amount_service() - - try: - if request.method == 'POST': - if request.POST.get('finish_confirm') != 'yes': - return HttpResponse(_('Not confirmed')) - - abtar.activate(request.user) - messages.success(request, _('Service has been activated successfully')) - return redirect('abonapp:abon_services', gid, uid) - - except (NasFailedResult, mydefs.LogicError) as e: - messages.error(request, e) - except NasNetworkError as e: - messages.warning(request, e) - except mydefs.MultipleException as errs: - for err in errs.err_list: - messages.add_message(request, messages.constants.ERROR, err) - calc_obj = abtar.tariff.get_calc_type()(abtar) - return render(request, 'abonapp/activate_service.html', { - 'abon': abtar.abon, - 'abon_group': abtar.abon.group, - 'abtar': abtar, - 'amount': amount, - 'diff': abtar.abon.ballance - amount, - 'deadline': calc_obj.calc_deadline() - }) - - @login_required @permission_required('abonapp.delete_abontariff') -def unsubscribe_service(request, gid, uid, srvid): +def unsubscribe_service(request, gid, uid, abon_tariff_id): try: - get_object_or_404(models.AbonTariff, pk=int(srvid)).delete() + get_object_or_404(models.AbonTariff, pk=int(abon_tariff_id)).delete() messages.success(request, _('User has been detached from service')) except NasFailedResult as e: messages.error(request, e) @@ -559,7 +467,7 @@ def unsubscribe_service(request, gid, uid, srvid): except mydefs.MultipleException as errs: for err in errs.err_list: messages.add_message(request, messages.constants.ERROR, err) - return redirect('abonapp:abon_home', gid=gid, uid=uid) + return redirect('abonapp:abon_services', gid=gid, uid=uid) @login_required @@ -610,7 +518,6 @@ def update_nas(request, group_id): @login_required @mydefs.only_admins def task_log(request, gid, uid): - from taskapp.models import Task abon = get_object_or_404(models.Abon, pk=uid) tasks = Task.objects.filter(abon=abon) return render(request, 'abonapp/task_log.html', { @@ -626,11 +533,15 @@ def passport_view(request, gid, uid): try: abon = models.Abon.objects.get(pk=uid) if request.method == 'POST': - frm = forms.PassportForm(request.POST) + try: + passport_instance = models.PassportInfo.objects.get(abon=abon) + except models.PassportInfo.DoesNotExist: + passport_instance = None + frm = forms.PassportForm(request.POST, instance=passport_instance) if frm.is_valid(): - passp_instance = frm.save(commit=False) - passp_instance.abon = abon - passp_instance.save() + pi = frm.save(commit=False) + pi.abon = abon + pi.save() messages.success(request, _('Passport information has been saved')) return redirect('abonapp:passport_view', gid=gid, uid=uid) else: @@ -672,24 +583,20 @@ def chgroup_tariff(request, gid): def dev(request, gid, uid): abon_dev = None try: + abon = models.Abon.objects.get(pk=uid) if request.method == 'POST': dev = Device.objects.get(pk=request.POST.get('dev')) - abon = models.Abon.objects.get(pk=uid) - try: - models.AbonDevice.objects.get(device=dev, abon=abon) - except models.AbonDevice.DoesNotExist: - models.AbonDevice.objects.create(abon=abon, device=dev) - messages.success(request, _('Device has successfully attached')) + abon.device = dev + abon.save(update_fields=['device']) + messages.success(request, _('Device has successfully attached')) return redirect('abonapp:abon_home', gid=gid, uid=uid) else: - abon_dev = models.AbonDevice.objects.get(abon=uid).device + abon_dev = abon.device except Device.DoesNotExist: messages.warning(request, _('Device your selected already does not exist')) except models.Abon.DoesNotExist: messages.error(request, _('Abon does not exist')) return redirect('abonapp:people_list', gid=gid) - except models.AbonDevice.DoesNotExist: - messages.warning(request, _('User device was not found')) return render(request, 'abonapp/modal_dev.html', { 'devices': Device.objects.filter(user_group=gid), 'dev': abon_dev, @@ -702,8 +609,8 @@ def dev(request, gid, uid): def clear_dev(request, gid, uid): try: abon = models.Abon.objects.get(pk=uid) - abdev = models.AbonDevice.objects.get(abon=abon) - abdev.delete() + abon.device = None + abon.save(update_fields=['device']) messages.success(request, _('Device has successfully unattached')) except models.Abon.DoesNotExist: messages.error(request, _('Abon does not exist')) @@ -714,14 +621,16 @@ def clear_dev(request, gid, uid): @login_required @mydefs.only_admins def charts(request, gid, uid): - from statistics.models import getModel - from datetime import datetime, date, time, timedelta high = 100 - def byte_to_mbit(x): - return ((x/60)*8)/2**20 + wandate = request.GET.get('wantdate') + if wandate: + wandate = datetime.strptime(wandate, '%d%m%Y').date() + else: + wandate = date.today() + try: - StatElem = getModel() + StatElem = getModel(wandate) abon = models.Abon.objects.get(pk=uid) if abon.group is None: abon.group = models.AbonGroup.objects.get(pk=gid) @@ -731,20 +640,18 @@ def charts(request, gid, uid): if abon.ip_address is None: charts_data = None else: - charts_data = StatElem.objects.filter(ip=abon.ip_address) - #oct_limit = StatElem.percentile([cd.octets for cd in charts_data], 0.05) - # ниже возвращаем пары значений трафика который переведён в mByte, и unix timestamp - midnight = datetime.combine(date.today(), time.min) - charts_data = [(cd.cur_time.timestamp()*1000, byte_to_mbit(cd.octets)) for cd in charts_data] - if len(charts_data) > 0: - charts_data.append( (charts_data[-1:][0][0], 0.0) ) - charts_data = ["{x: new Date(%d), y: %.2f}" % (cd[0], cd[1]) for cd in charts_data] - charts_data.append("{x:new Date(%d),y:0}" % (int((midnight + timedelta(days=1)).timestamp()) * 1000)) + charts_data = StatElem.objects.chart( + abon.ip_address, + count_of_parts=30, + want_date=wandate + ) abontariff = abon.active_tariff() - high = abontariff.speedIn + abontariff.speedOut - if high > 100: - high = 100 + if abontariff is not None: + trf = abontariff.tariff + high = trf.speedIn + trf.speedOut + if high > 100: + high = 100 except models.Abon.DoesNotExist: messages.error(request, _('Abon does not exist')) @@ -760,7 +667,8 @@ def charts(request, gid, uid): 'abon_group': abongroup, 'abon': abon, 'charts_data': ',\n'.join(charts_data) if charts_data is not None else None, - 'high': high + 'high': high, + 'dates': get_dates() }) @@ -799,7 +707,6 @@ def make_extra_field(request, gid, uid): @permission_required('abonapp.change_extra_fields_model') def extra_field_change(request, gid, uid): extras = [(int(x), y) for x, y in zip(request.POST.getlist('ed'), request.POST.getlist('ex'))] - print(extras) try: for ex in extras: extra_field = models.ExtraFieldsModel.objects.get(pk=ex[0]) @@ -860,12 +767,115 @@ def abon_ping(request): })) +@login_required +@mydefs.only_admins +def dials(request, gid, uid): + abon = get_object_or_404(models.Abon, pk=uid) + if hasattr(abon.group, 'pk') and abon.group.pk != int(gid): + return redirect('abonapp:dials', abon.group.pk, abon.pk) + if abon.telephone is not None and abon.telephone != '': + tel = abon.telephone.replace('+', '') + logs = AsteriskCDR.objects.filter( + Q(src__contains=tel) | Q(dst__contains=tel) + ) + logs = mydefs.pag_mn(request, logs) + else: + logs = None + return render(request, 'abonapp/dial_log.html', { + 'logs': logs, + 'abon_group': get_object_or_404(models.AbonGroup, pk=gid), + 'abon': abon + }) + + +@login_required +@mydefs.only_admins +def save_user_dev_port(request, gid, uid): + if request.method != 'POST': + messages.error(request, _('Method is not POST')) + return redirect('abonapp:abon_home', gid, uid) + user_port = mydefs.safe_int(request.POST.get('user_port')) + is_dynamic_ip = request.POST.get('is_dynamic_ip') + try: + if user_port == 0: + port = None + else: + port = DevPort.objects.get(pk=user_port) + abon = models.Abon.objects.get(pk=uid) + abon.dev_port = port + if abon.is_dynamic_ip != is_dynamic_ip: + abon.is_dynamic_ip = is_dynamic_ip + abon.save(update_fields=['dev_port', 'is_dynamic_ip']) + else: + abon.save(update_fields=['dev_port']) + messages.success(request, _('User port has been saved')) + except DevPort.DoesNotExist: + messages.error(request, _('Selected port does not exist')) + except models.Abon.DoesNotExist: + messages.error(request, _('User does not exist')) + return redirect('abonapp:abon_home', gid, uid) + + +@login_required +@permission_required('abonapp.add_abonstreet') +def street_add(request, gid): + if request.method == 'POST': + frm = forms.AbonStreetForm(request.POST) + if frm.is_valid(): + frm.save() + messages.success(request, _('Street successfully saved')) + return redirect('abonapp:people_list', gid) + else: + messages.error(request, _('fix form errors')) + else: + frm = forms.AbonStreetForm(initial={'group': gid}) + return render_to_text('abonapp/modal_addstreet.html', { + 'form': frm, + 'gid': gid + }, request=request) + + +@login_required +@permission_required('abonapp.change_abonstreet') +def street_edit(request, gid): + try: + if request.method == 'POST': + streets_pairs = [(int(sid), sname) for sid, sname in zip(request.POST.getlist('sid'), request.POST.getlist('sname'))] + for sid, sname in streets_pairs: + street = models.AbonStreet.objects.get(pk=sid) + street.name = sname + street.save() + messages.success(request, _('Streets has been saved')) + else: + return render_to_text('abonapp/modal_editstreet.html', { + 'gid': gid, + 'streets': models.AbonStreet.objects.filter(group=gid) + }, request=request) + + except models.AbonStreet.DoesNotExist: + messages.error(request, _('One of these streets has not been found')) + + return redirect('abonapp:people_list', gid) + + +@login_required +@permission_required('abonapp.delete_abonstreet') +def street_del(request, gid, sid): + try: + models.AbonStreet.objects.get(pk=sid, group=gid).delete() + messages.success(request, _('The street successfully deleted')) + except models.AbonStreet.DoesNotExist: + messages.error(request, _('The street has not been found')) + return redirect('abonapp:people_list', gid) + + + # API's def abons(request): ablist = [{ 'id': abn.pk, - 'tarif_id': abn.active_tariff().pk if abn.active_tariff() else 0, + 'tarif_id': abn.active_tariff().tariff.pk if abn.active_tariff() is not None else 0, 'ip': abn.ip_address.int_ip(), 'is_active': abn.is_active } for abn in models.Abon.objects.all()] @@ -887,5 +897,5 @@ def abons(request): def search_abon(request): word = request.GET.get('s') results = models.Abon.objects.filter(fio__icontains=word)[:8] - results = [{'id': usr.pk, 'name': usr.username, 'fio': usr.fio} for usr in results] + results = [{'id': usr.pk, 'text': "%s: %s" % (usr.username, usr.fio)} for usr in results] return HttpResponse(dumps(results, ensure_ascii=False)) diff --git a/accounts_app/locale/ru/LC_MESSAGES/django.po b/accounts_app/locale/ru/LC_MESSAGES/django.po index ba3578d..d3c7a5c 100644 --- a/accounts_app/locale/ru/LC_MESSAGES/django.po +++ b/accounts_app/locale/ru/LC_MESSAGES/django.po @@ -23,10 +23,6 @@ msgstr "" msgid "Users must have an telephone number" msgstr "У пользователей должен быть номер телефона" -#: accounts_app/models.py:52 -msgid "Telephone number" -msgstr "Номер телефона" - #: accounts_app/templates/accounts/group.html:7 #: accounts_app/templates/accounts/group_list.html:7 msgid "Administrators" @@ -81,7 +77,7 @@ msgstr "Добавить группу" #: accounts_app/templates/accounts/index.html:8 #: accounts_app/templates/accounts/settings/ch_info.html:37 msgid "Telephone" -msgstr "Номер телефона" +msgstr "Телефон" #: accounts_app/templates/accounts/index.html:12 #: accounts_app/templates/accounts/login.html:35 @@ -140,27 +136,35 @@ msgstr "Старый пароль" msgid "New password" msgstr "Новый пароль" -#: accounts_app/views.py:43 +#: accounts_app/views.py:42 msgid "Wrong login or password, please try again" msgstr "Неправильный логин или пароль, попробуйте ещё раз" -#: accounts_app/views.py:137 +#: accounts_app/views.py:141 +msgid "New password is empty, fill it" +msgstr "Новый пароль пустой, придумайте себе пароль" + +#: accounts_app/views.py:143 msgid "Wrong password" msgstr "Неправильный пароль" -#: accounts_app/views.py:162 +#: accounts_app/views.py:145 +msgid "Empty password, fill it" +msgstr "Пустой пароль, впишите что-то в пароль" + +#: accounts_app/views.py:168 msgid "You forget specify a password for the new account" msgstr "Забыли указать пароль для нового аккаунта" -#: accounts_app/views.py:165 +#: accounts_app/views.py:171 msgid "You forget to repeat a password for the new account" msgstr "Забыли повторить пароль для нового аккаунта" -#: accounts_app/views.py:174 +#: accounts_app/views.py:180 msgid "Subscriber with this name already exist" msgstr "Пользователь с таким именем уже есть" -#: accounts_app/views.py:176 +#: accounts_app/views.py:182 msgid "Passwords does not match, try again" msgstr "Пароли не совпадают, попробуйте ещё раз" @@ -184,3 +188,9 @@ msgstr "Редактировать" msgid "Set a task" msgstr "Дать задачу" + +msgid "Please select an image" +msgstr "Пожалуйста выберите изображение" + +msgid "Avatar successfully changed" +msgstr "Аватар успешно изменён" diff --git a/accounts_app/migrations/0007_auto_20170816_1109.py b/accounts_app/migrations/0007_auto_20170816_1109.py new file mode 100644 index 0000000..d9a26e3 --- /dev/null +++ b/accounts_app/migrations/0007_auto_20170816_1109.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-08-16 11:09 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts_app', '0006_auto_20170416_1029'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='telephone', + field=models.CharField(max_length=16, validators=[django.core.validators.RegexValidator('^\\+[7,8,9,3]\\d{10,11}$')], verbose_name='Телефон'), + ), + ] diff --git a/accounts_app/models.py b/accounts_app/models.py index f630c8a..2508257 100644 --- a/accounts_app/models.py +++ b/accounts_app/models.py @@ -49,7 +49,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin): is_admin = models.BooleanField(default=False) telephone = models.CharField( max_length=16, - verbose_name=_('Telephone number'), + verbose_name=_('Telephone'), #unique=True, validators=[RegexValidator('^\+[7,8,9,3]\d{10,11}$')] ) diff --git a/accounts_app/templates/accounts/index.html b/accounts_app/templates/accounts/index.html index 1ab3684..b90ee3b 100644 --- a/accounts_app/templates/accounts/index.html +++ b/accounts_app/templates/accounts/index.html @@ -2,6 +2,7 @@ {% load i18n %} {% block content %} +
@@ -32,5 +33,6 @@ {% endif %}
+
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/accounts_app/views.py b/accounts_app/views.py index b8c8961..0227436 100644 --- a/accounts_app/views.py +++ b/accounts_app/views.py @@ -4,7 +4,6 @@ from django.contrib.auth import authenticate, login, logout from django.core.exceptions import PermissionDenied from django.core.urlresolvers import NoReverseMatch from django.shortcuts import render, redirect, get_object_or_404, resolve_url -from django.http import Http404 from django.contrib.auth.models import Group, Permission from django.contrib import messages from django.utils.translation import ugettext as _ @@ -103,15 +102,20 @@ def chgroup(request, uid): @mydefs.only_admins def ch_ava(request): if request.method == 'POST': - user = request.user - if user.avatar: - user.avatar.delete() - photo = Photo() - photo.image = request.FILES.get('avatar') - photo.save() - user.avatar = photo - user.save(update_fields=['avatar']) - request.user = user + phname = request.FILES.get('avatar') + if phname is None: + messages.error(request, _('Please select an image')) + else: + user = request.user + if user.avatar: + user.avatar.delete() + photo = Photo() + photo.image = phname + photo.save() + user.avatar = photo + user.save(update_fields=['avatar']) + request.user = user + messages.success(request, _('Avatar successfully changed')) return render(request, 'accounts/settings/ch_info.html', { 'user': request.user @@ -129,14 +133,21 @@ def ch_info(request): user.telephone = request.POST.get('telephone') psw = request.POST.get('oldpasswd') - if psw != '': + if psw != '' and psw is not None: if user.check_password(psw): newpasswd = request.POST.get('newpasswd') - user.set_password(newpasswd) + if newpasswd != '' and newpasswd is not None: + user.set_password(newpasswd) + user.save() + request.user = user + logout(request) + return redirect('acc_app:other_profile', uid=user.pk) + else: + messages.error(request, _('New password is empty, fill it')) else: messages.error(request, _('Wrong password')) - user.save() - request.user = user + else: + messages.warning(request, _('Empty password, fill it')) return render(request, 'accounts/settings/ch_info.html', { 'user': request.user diff --git a/agent/commands/dhcp.py b/agent/commands/dhcp.py new file mode 100644 index 0000000..fd39c4e --- /dev/null +++ b/agent/commands/dhcp.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from django.core.exceptions import MultipleObjectsReturned +from django.utils.translation import ugettext as _ +from abonapp.models import Abon +from devapp.models import Device, Port + + +def dhcp_commit(client_ip, client_mac, switch_mac, switch_port): + try: + dev = Device.objects.get(mac_addr=switch_mac) + mngr_class = dev.get_manager_klass() + + port = _('') + if mngr_class.is_use_device_port(): + port = Port.objects.get(device=dev, num=switch_port) + abon = Abon.objects.get(dev_port=port, device=dev) + else: + abon = Abon.objects.get(device=dev) + if not abon.is_dynamic_ip: + print('D:', _('User settings is not dynamic')) + return + if not abon.is_access(): + print('D:', _('User is not access to service')) + return + abon.ip_address = client_ip + abon.is_dhcp = True + abon.save(update_fields=['ip_address']) + print('S:', _("Ip address:'%s' update for '%s' successfull, on port: %s") % (client_ip, abon.get_short_name(), port)) + except Abon.DoesNotExist: + print('N:', _("User with device '%s' does not exist") % dev) + except Device.DoesNotExist: + print('N:', _('Device with mac %s not found') % switch_mac) + except Port.DoesNotExist: + print('N:', _('Port %d on device with mac %s does not exist') % (int(switch_port), switch_mac)) + except MultipleObjectsReturned as e: + print('E:', 'MultipleObjectsReturned:', type(e), e) + + +def dhcp_expiry(client_ip): + try: + abon = Abon.objects.get(ip_address=client_ip) + abon.ip_address = None + abon.is_dhcp = True + abon.save(update_fields=['ip_address']) + except Abon.DoesNotExist: + pass + + +def dhcp_release(client_ip): + dhcp_expiry(client_ip) diff --git a/agent/core.py b/agent/core.py index 8561b56..756d68e 100644 --- a/agent/core.py +++ b/agent/core.py @@ -99,3 +99,34 @@ class BaseTransmitter(metaclass=ABCMeta): :param count: количество пингов :return: None если не пингуется, иначе кортеж, в котором (сколько вернулось, сколько было отправлено) """ + + @abstractmethod + def read_users(self): + """ + Читаем пользователей с NAS + :return: список AbonStruct + """ + + def _diff_users(self, users_from_db): + """ + :param users_from_db: QuerySet всех абонентов у которых может быть обслуживание + :return: на выходе получаем абонентов которых надо добавить в nas и которых надо удалить + """ + users_from_db = [ab.build_agent_struct() for ab in users_from_db if ab.is_access()] + users_from_db = set([ab for ab in users_from_db if ab is not None and ab.tariff is not None]) + users_from_nas = set(self.read_users()) + list_for_del = (users_from_db ^ users_from_nas) - users_from_db + list_for_add = users_from_db - users_from_nas + return list_for_add, list_for_del + + def sync_nas(self, users_from_db): + list_for_add, list_for_del = self._diff_users(users_from_db) + print('FOR DELETE') + for ld in list_for_del: + print(ld) + print('FOR ADD') + for la in list_for_add: + print(la) + self.remove_user_range( list_for_del ) + self.add_user_range( list_for_add ) + diff --git a/agent/mod_mikrotik.py b/agent/mod_mikrotik.py index 1b09957..e1345b3 100644 --- a/agent/mod_mikrotik.py +++ b/agent/mod_mikrotik.py @@ -5,13 +5,13 @@ from abc import ABCMeta from hashlib import md5 from .core import BaseTransmitter, NasFailedResult, NasNetworkError from mydefs import ping -from .structs import TariffStruct, AbonStruct, IpStruct, ShapeItem +from .structs import TariffStruct, AbonStruct, IpStruct from . import settings from djing.settings import DEBUG import re -#DEBUG=False +#DEBUG=True LIST_USERS_ALLOWED = 'DjingUsersAllowed' LIST_USERS_BLOCKED = 'DjingUsersBlocked' @@ -33,7 +33,7 @@ class ApiRos: md.update(bytes(pwd, 'utf-8')) md.update(chal) for r in self.talk_iter(["/login", "=name=" + username, - "=response=00" + binascii.hexlify(md.digest()).decode('utf-8')]): pass + "=response=00" + binascii.hexlify(md.digest()).decode('utf-8')]): pass def talk_iter(self, words): if self.writeSentence(words) == 0: return @@ -190,28 +190,29 @@ class TransmitterManager(BaseTransmitter, metaclass=ABCMeta): res = text_speed_digit elif text_append == 'k': res = text_speed_digit / 1000 - #elif text_append == 'G': + # elif text_append == 'G': # res = text_speed_digit * 0x400 else: - res = float(re.sub(r'[a-zA-Z]', '', text_speed)) / 1000**2 + res = float(re.sub(r'[a-zA-Z]', '', text_speed)) / 1000 ** 2 return res + speeds = info['=max-limit'].split('/') + t = TariffStruct( + speedIn=parse_speed(speeds[1]), + speedOut=parse_speed(speeds[0]) + ) try: - speeds = info['=max-limit'].split('/') - t = TariffStruct( - speedIn=parse_speed(speeds[1]), - speedOut=parse_speed(speeds[0]) - ) a = AbonStruct( uid=int(info['=name'][3:]), - #FIXME: тут в разных микротиках или =target-addresses или =target + # FIXME: тут в разных микротиках или =target-addresses или =target ip=info['=target'][:-3], tariff=t, is_active=False if info['=disabled'] == 'false' else True ) - return ShapeItem(abon=a, sid=info['=.id'].replace('*', '')) - except KeyError: - return + a.queue_id = info['=.id'] + return a + except ValueError: + pass class QueueManager(TransmitterManager, metaclass=ABCMeta): @@ -223,50 +224,55 @@ class QueueManager(TransmitterManager, metaclass=ABCMeta): def add(self, user): assert isinstance(user, AbonStruct) - assert isinstance(user.tariff, TariffStruct) + if user.tariff is None or not isinstance(user.tariff, TariffStruct): + return return self._exec_cmd(['/queue/simple/add', - '=name=uid%d' % user.uid, - #FIXME: тут в разных микротиках или =target-addresses или =target - '=target=%s' % user.ip.get_str(), - '=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn), - '=queue=MikroBILL_SFQ/MikroBILL_SFQ', - '=burst-time=1/1' - ]) + '=name=uid%d' % user.uid, + # FIXME: тут в разных микротиках или =target-addresses или =target + '=target=%s' % str(user.ip), + '=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn), + '=queue=MikroBILL_SFQ/MikroBILL_SFQ', + '=burst-time=1/1' + ]) def remove(self, user): assert isinstance(user, AbonStruct) q = self.find('uid%d' % user.uid) if q is not None: - return self._exec_cmd(['/queue/simple/remove', '=.id=*' + str(q.sid)]) + return self._exec_cmd(['/queue/simple/remove', '=.id=' + getattr(q, 'queue_id', '')]) def remove_range(self, q_ids): - names = ['%d' % usr for usr in q_ids] - return self._exec_cmd(['/queue/simple/remove'] + names) + if q_ids is not None and len(q_ids) > 0: + return self._exec_cmd(['/queue/simple/remove', '=numbers=' + ','.join(q_ids)]) def update(self, user): assert isinstance(user, AbonStruct) + if user.tariff is None or not isinstance(user.tariff, TariffStruct): + return queue = self.find('uid%d' % user.uid) if queue is None: # не нашли запись в шейпере об абоненте, добавим return self.add(user) else: - mk_id = queue.sid + mk_id = getattr(queue, 'queue_id', '') # обновляем шейпер абонента - return self._exec_cmd(['/queue/simple/set', '=.id=*' + mk_id, - '=name=uid%d' % user.uid, - '=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn), - #FIXME: тут в разных микротиках или =target-addresses или =target - '=target=%s' % user.ip.get_str(), - '=queue=MikroBILL_SFQ/MikroBILL_SFQ', - '=burst-time=1/1' - ]) + return self._exec_cmd(['/queue/simple/set', '=.id=' + mk_id, + '=name=uid%d' % user.uid, + '=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn), + # FIXME: тут в разных микротиках или =target-addresses или =target + '=target=%s' % str(user.ip), + '=queue=MikroBILL_SFQ/MikroBILL_SFQ', + '=burst-time=1/1' + ]) # читаем шейпер, возващаем записи о шейпере def read_queue_iter(self): queues = self._exec_cmd_iter(['/queue/simple/print', '=detail']) for queue in queues: if queue[0] == '!done': return - yield self._build_shape_obj(queue[1]) + sobj = self._build_shape_obj(queue[1]) + if sobj is not None: + yield sobj # то же что и выше, только получаем только номера в микротике def read_mikroids_iter(self): @@ -282,7 +288,7 @@ class QueueManager(TransmitterManager, metaclass=ABCMeta): self.add(user) return self.disable(user) else: - return self._exec_cmd(['/queue/simple/disable', '=.id=*' + q.sid]) + return self._exec_cmd(['/queue/simple/disable', '=.id=*' + getattr(q, 'queue_id', '')]) def enable(self, user): assert isinstance(user, AbonStruct) @@ -291,7 +297,13 @@ class QueueManager(TransmitterManager, metaclass=ABCMeta): self.add(user) self.enable(user) else: - return self._exec_cmd(['/queue/simple/enable', '=.id=*' + q.sid]) + return self._exec_cmd(['/queue/simple/enable', '=.id=*' + getattr(q, 'queue_id', '')]) + + +class IpAddressListObj(IpStruct): + def __init__(self, ip, mk_id): + super(IpAddressListObj, self).__init__(ip) + self.mk_id = str(mk_id).replace('*', '') class IpAddressListManager(TransmitterManager, metaclass=ABCMeta): @@ -301,7 +313,7 @@ class IpAddressListManager(TransmitterManager, metaclass=ABCMeta): commands = [ '/ip/firewall/address-list/add', '=list=%s' % list_name, - '=address=%s' % ip.get_str() + '=address=%s' % str(ip) ] if type(timeout) is int: commands.append('=timeout=%d' % timeout) @@ -311,22 +323,36 @@ class IpAddressListManager(TransmitterManager, metaclass=ABCMeta): if timeout is not None: commands = [ '/ip/firewall/address-list/set', '=.id=' + str(mk_id), - '=timeout=%d' % timeout + '=timeout=%d' % timeout ] return self._exec_cmd(commands) def remove(self, mk_id): return self._exec_cmd([ '/ip/firewall/address-list/remove', - '=.id=*' + str(mk_id) + '=.id=*' + str(mk_id).replace('*', '') ]) + def remove_range(self, items): + ids = [ip.mk_id for ip in items if isinstance(ip, IpAddressListObj)] + if len(ids) > 0: + return self._exec_cmd([ + '/ip/firewall/address-list/remove', + 'numbers=' + ','.join(ids) + ]) + def find(self, ip, list_name): assert isinstance(ip, IpStruct) return self._exec_cmd([ '/ip/firewall/address-list/print', 'where', '?list=%s' % list_name, - '?address=%s' % ip.get_str() + '?address=%s' % str(ip) + ]) + + def read_ips_iter(self, list_name): + return self._exec_cmd([ + '/ip/firewall/address-list/print', 'where', + '?list=%s' % list_name ]) def disable(self, user): @@ -355,18 +381,18 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager): self.add_user(usr) def remove_user_range(self, users): - queues = [QueueManager.find(self, 'uid%d' % user.uid) for user in users if isinstance(user, AbonStruct)] - queue_names = ["uid%d" % queue.sid for queue in queues] - QueueManager.remove_range(self, queue_names) + queue_ids = [usr.queue_id for usr in users if usr is not None] + QueueManager.remove_range(self, queue_ids) ips = [user.ip for user in users if isinstance(user, AbonStruct)] for ip in ips: ip_list_entity = IpAddressListManager.find(self, ip, LIST_USERS_ALLOWED) - if len(ip_list_entity) > 1: + if ip_list_entity is not None and len(ip_list_entity) > 1: IpAddressListManager.remove(self, ip_list_entity[0]['=.id']) def add_user(self, user, ip_timeout=None): - assert isinstance(user.tariff, TariffStruct) assert isinstance(user.ip, IpStruct) + if user.tariff is None or not isinstance(user.tariff, TariffStruct): + return QueueManager.add(self, user) IpAddressListManager.add(self, LIST_USERS_ALLOWED, user.ip, ip_timeout) # удаляем из списка заблокированных абонентов @@ -377,12 +403,11 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager): def remove_user(self, user): QueueManager.remove(self, user) firewall_ip_list_obj = IpAddressListManager.find(self, user.ip, LIST_USERS_ALLOWED) - if len(firewall_ip_list_obj) > 1: + if firewall_ip_list_obj is not None and len(firewall_ip_list_obj) > 1: IpAddressListManager.remove(self, firewall_ip_list_obj[0]['=.id']) # обновляем основную инфу абонента def update_user(self, user, ip_timeout=None): - assert isinstance(user.tariff, TariffStruct) assert isinstance(user.ip, IpStruct) # ищем ip абонента в списке ip @@ -396,6 +421,13 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager): IpAddressListManager.remove(self, find_res[0]['=.id']) return + # если нет услуги то её не должно быть и в nas + if user.tariff is None or not isinstance(user.tariff, TariffStruct): + queue = QueueManager.find(self, 'uid%d' % user.uid) + if queue is not None: + QueueManager.remove(self, user) + return + # если не найден (mikrotik возвращает пустой словарь в списке если ничего нет) if len(find_res) < 2: # добавим запись об абоненте @@ -410,7 +442,7 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager): if queue is None: QueueManager.add(self, user) return - if queue.abon != user: + if queue != user: QueueManager.update(self, user) def ping(self, host, count=10): @@ -423,14 +455,14 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager): interface = r[0]['=interface'] r = self._exec_cmd([ '/ping', '=address=%s' % host, '=arp-ping=yes', '=interval=100ms', '=count=%d' % count, - '=interface=%s' % interface + '=interface=%s' % interface ]) received, sent = int(r[-2:][0]['=received']), int(r[-2:][0]['=sent']) return received, sent # приостановливаем обслуживание абонента def pause_user(self, user): - pass + self.remove_user(user) # продолжаем обслуживание абонента def start_user(self, user): @@ -454,3 +486,9 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager): def remove_tariff(self, tid): pass + + def read_users(self): + # shapes is ShapeItem + # allowed_ips = IpAddressListManager.read_ips_iter(self, LIST_USERS_ALLOWED) + queues = QueueManager.read_queue_iter(self) + return queues diff --git a/agent/netflow/djing_flow.conf b/agent/netflow/djing_flow.conf deleted file mode 100644 index eccdbfa..0000000 --- a/agent/netflow/djing_flow.conf +++ /dev/null @@ -1,6 +0,0 @@ -# Config for djing flow - -version = "0.1"; - -# количество строк на запрос -mysql_rows_per_request = 65735; diff --git a/agent/structs.py b/agent/structs.py index eda9d10..849a296 100644 --- a/agent/structs.py +++ b/agent/structs.py @@ -34,9 +34,6 @@ class IpStruct(BaseStruct): self.__ip = int(dt[0]) return self - def get_str(self): - return int2ip(self.__ip) - def get_int(self): return self.__ip @@ -44,22 +41,32 @@ class IpStruct(BaseStruct): assert isinstance(other, IpStruct) return self.__ip == other.__ip + def __int__(self): + return self.__ip + def __str__(self): return int2ip(self.__ip) + def __hash__(self): + return hash(self.__ip) + # Как обслуживается абонент class TariffStruct(BaseStruct): def __init__(self, tariff_id=0, speedIn=None, speedOut=None): - self.tid = tariff_id - self.speedIn = speedIn if speedIn is not None else 0.001 - self.speedOut = speedOut if speedOut is not None else 0.001 + self.tid = int(tariff_id) + self.speedIn = float(speedIn if speedIn is not None else 0.001) + self.speedOut = float(speedOut if speedOut is not None else 0.001) def serialize(self): dt = pack("!Iff", int(self.tid), float(self.speedIn), float(self.speedOut)) return dt + # Да, если все значения нулевые + def is_empty(self): + return self.tid == 0 and self.speedIn == 0.001 and self.speedOut == 0.001 + def deserialize(self, data, *args): dt = unpack("!Iff", data) self.tid = int(dt[0]) @@ -68,7 +75,6 @@ class TariffStruct(BaseStruct): return self def __eq__(self, other): - assert isinstance(other, TariffStruct) # не сравниваем id, т.к. тарифы с одинаковыми скоростями для NAS одинаковы # Да и иногда не удобно доставать из nas id тарифы из базы return self.speedIn == other.speedIn and self.speedOut == other.speedOut @@ -76,6 +82,11 @@ class TariffStruct(BaseStruct): def __str__(self): return "Id=%d, speedIn=%.2f, speedOut=%.2f" % (self.tid, self.speedIn, self.speedOut) + # нужно чтоб хеши тарифов In10,Out20 и In20,Out10 были разными + # поэтому сначала float->str и потом хеш + def __hash__(self): + return hash(str(self.speedIn) + str(self.speedOut)) + # Абонент из базы class AbonStruct(BaseStruct): @@ -83,14 +94,15 @@ class AbonStruct(BaseStruct): def __init__(self, uid=None, ip=None, tariff=None, is_active=True): self.uid = int(uid) self.ip = IpStruct(ip) - assert isinstance(tariff, TariffStruct) self.tariff = tariff self.is_active = is_active def serialize(self): + if self.tariff is None: + return assert isinstance(self.tariff, TariffStruct) assert isinstance(self.ip, IpStruct) - dt = pack("!LII?", self.uid, self.ip.get_int(), self.tariff.tid, self.is_active) + dt = pack("!LII?", self.uid, int(self.ip), self.tariff.tid, self.is_active) return dt def deserialize(self, data, tariff=None): @@ -110,7 +122,10 @@ class AbonStruct(BaseStruct): return r def __str__(self): - return "uid=%d, ip=%s, tariff=%s" % (self.uid, self.ip, self.tariff) + return "uid=%d, ip=%s, tariff=%s" % (self.uid, self.ip, self.tariff or '') + + def __hash__(self): + return hash(int(self.ip) + hash(self.tariff)) if self.tariff is not None else 0 # Правило шейпинга в фаере, или ещё можно сказать услуга абонента на NAS diff --git a/bugs.txt b/bugs.txt index fa0bb2e..e2ba2d8 100644 --- a/bugs.txt +++ b/bugs.txt @@ -8,3 +8,7 @@ - Не надо коннектиться к микротику когда не собираемся ничего изменять. А то при сохранении залогинились и вышли без действий - Не удаляет просроченные услуги если не пингуется NAS - Надо отменить учёт временной зоны +!!! Обязательно проверить как отрабатывает на NAS удаление и изменение AbonTariff +!!! Удалить всё что связано с активацией услуги +!!! Убрать досрочное завершение услуги +! Проверить дату завершения услуги diff --git a/chatbot/locale/ru/LC_MESSAGES/django.po b/chatbot/locale/ru/LC_MESSAGES/django.po index b5a24c0..e6277c8 100644 --- a/chatbot/locale/ru/LC_MESSAGES/django.po +++ b/chatbot/locale/ru/LC_MESSAGES/django.po @@ -20,31 +20,32 @@ 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" -#: chatbot/telebot.py:60 +#: chatbot/telebot.py:61 msgid "Let's get acquainted, what is your name? Write your login from billing." msgstr "Давай знакомиться, как тебя зовут? Напиши свой логин из биллинга." -#: chatbot/telebot.py:80 +#: chatbot/telebot.py:81 msgid "I do not know the answer to this yet." msgstr "Я пока не знаю ответа на это" -#: chatbot/telebot.py:99 +#: chatbot/telebot.py:100 msgid "" "You are not found in the database, check that it correctly pointed out its " "LOGIN. Try again" msgstr "" -"Ты не найден в базе, проверь что правильно указал именно свой ЛОГИН. Попробуй ещё" +"Ты не найден в базе, проверь что правильно указал именно свой ЛОГИН. " +"Попробуй ещё" -#: chatbot/telebot.py:122 +#: chatbot/telebot.py:123 msgid "It's not like ip address, try again" msgstr "Это не похоже на ip адрес, попробуй ещё" -#: chatbot/telebot.py:125 +#: chatbot/telebot.py:126 #, python-format msgid "You're '%s', right?" msgstr "Ты ведь %s ?" -#: chatbot/telebot.py:135 +#: chatbot/telebot.py:136 #, python-format msgid "Recipient '%s' does not subscribed on notifications" msgstr "%s не подписан на оповещения" diff --git a/chatbot/telebot.py b/chatbot/telebot.py index 9aecb83..e9c9a56 100755 --- a/chatbot/telebot.py +++ b/chatbot/telebot.py @@ -113,7 +113,7 @@ class DjingTelebot(helper.ChatHandler): # пингуем адрес def ping(self, ip=None): if ip is None: - self._question("Let's ping, write ip. It will be necessary to wait 10 seconds", self.ping) + self._question(_("Let's ping, write ip. It will be necessary to wait 10 seconds"), self.ping) return try: socket.inet_aton(ip) diff --git a/clientsideapp/locale/ru/LC_MESSAGES/django.po b/clientsideapp/locale/ru/LC_MESSAGES/django.po index 6b08e8a..9fcf3b2 100644 --- a/clientsideapp/locale/ru/LC_MESSAGES/django.po +++ b/clientsideapp/locale/ru/LC_MESSAGES/django.po @@ -68,81 +68,15 @@ msgstr "Заказать услугу" msgid "Are you sure you want to order the service?" msgstr "Вы уверены что хотите заказать услугу?" -#: clientsideapp/templates/clientsideapp/modal_service_buy.html:10 -#, python-format -msgid "" -"Your current service %(current_service.title)s\n" -"for the %(amount)s rub." -msgstr "" -"Ваша текущая услуга %(current_service.title)s\n" -"за %(amount)s руб." -#: clientsideapp/templates/clientsideapp/modal_service_buy.html:13 -msgid "" -"You do not have an active service for the use of resources required service " -"purchase from the selection below.
\n" -"And if you have already booked then just activate the service from the list " -"ordered." -msgstr "" -"У вас нет активной услуги, для использования ресурсов приобретите нужную " -"услугу из представленных ниже.
\n" -"А если уже заказали то просто активируйте услугу из списка заказанных." - -#: clientsideapp/templates/clientsideapp/modal_service_buy.html:21 -#, python-format -msgid "" -"Inbound speed: %(speedIn)s MBit/s
\n" -"Outgoing speed: %(speedOut)s MBit/s
\n" -"Cost: %(amount)s rubles." -msgstr "" -"Входящая скорость: %(speedIn)s MBit/s
\n" -"Исходящая скорость: %(speedOut)s MBit/s
\n" -"Стоимость: %(amount)s руб." - -#: clientsideapp/templates/clientsideapp/modal_service_buy.html:27 +#: clientsideapp/templates/clientsideapp/modal_service_buy.html:28 msgid "Pick" msgstr "Заказать" -#: clientsideapp/templates/clientsideapp/modal_service_buy.html:29 -#: clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html:24 +#: clientsideapp/templates/clientsideapp/modal_service_buy.html:30 msgid "Close" msgstr "Закрыть" -#: clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html:5 -msgid "Unsubscribe from service" -msgstr "Отписаться от услуги" - -#: clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html:8 -msgid "Are you sure you want to unsubscribe from the service?" -msgstr "Вы уверены что хотите отписаться от услуги?" - -#: clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html:14 -#, fuzzy, python-format -#| msgid "" -#| "Inbound speed: %(service.speedIn)s MBit/s
\n" -#| "Outgoing speed: %(service.speedOut)s MBit/s
\n" -#| "Cost: %(service.amount)s rubles." -msgid "" -"Inbound speed: %(service.speedIn)s MBit/s
Outgoing speed: " -"%(service.speedOut)s MBit/s" -msgstr "" -"Входящая скорость: %(service.speedIn)s MBit/s
\n" -"Исходящая скорость: %(service.speedOut)s MBit/s" - -#: clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html:16 -msgid "" -"When you unsubscribe from the service, it just will remove it from the queue " -"inclusions your services.
\n" -"Your funds will not be affected, the money will not go away." -msgstr "" -"Когда вы отпишитесь от услуги, это просто уберёт её из очереди включений " -"ваших услуг.
\n" -"Ваши средства не буду затронуты, деньги не уйдут." - -#: clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html:22 -msgid "Unsubscribe" -msgstr "Отписаться" - #: clientsideapp/templates/clientsideapp/pays.html:6 msgid "conducted payments" msgstr "Проведённые платежи" @@ -162,3 +96,44 @@ msgstr "Комментарий" #: clientsideapp/templates/clientsideapp/pays.html:25 msgid "You have not spent payments" msgstr "У вас нет проведённых платежей" + +#: clientsideapp/views.py:53 +#, python-format +msgid "Buy the service via user side, service '%s'" +msgstr "Покупка тарифного плана через личный кабинет, тариф '%s'" + +#: clientsideapp/views.py:82 +#, python-format +msgid "Service '%s' has been finished" +msgstr "Услуга '%s' успешно завершена" + +#: clientsideapp/views.py:87 +#, python-format +msgid "Early terminated service '%s' via client side" +msgstr "Досрочное завершение услуги '%s' из личного кабинета" + +#: clientsideapp/views.py:90 clientsideapp/views.py:119 +#: clientsideapp/views.py:147 +msgid "Act is not confirmed" +msgstr "Действие не подтверждено" + +#: clientsideapp/views.py:103 clientsideapp/views.py:130 +msgid "Temporary network bug" +msgstr "Временные неполадки в сети" + +#: clientsideapp/views.py:126 +msgid "The service was not found" +msgstr "Указанная подписка на услугу не найдена" + +#: clientsideapp/views.py:145 +#, python-format +msgid "The service '%s' wan successfully activated" +msgstr "Услуга '%s' успешно подключена" + +#: clientsideapp/views.py:181 +msgid "Are you not sure that you want buy the service?" +msgstr "Вы не уверены что хотите оплатить долг?" + +#: clientsideapp/views.py:183 +msgid "Your account have not enough money" +msgstr "Недостаточно средств на счету" diff --git a/clientsideapp/templates/clientsideapp/ext.html b/clientsideapp/templates/clientsideapp/ext.html index 5e2b2bd..b19ef15 100644 --- a/clientsideapp/templates/clientsideapp/ext.html +++ b/clientsideapp/templates/clientsideapp/ext.html @@ -11,7 +11,7 @@ -/head> + - + -
{% if request.user.is_staff %} diff --git a/clientsideapp/templates/clientsideapp/index.html b/clientsideapp/templates/clientsideapp/index.html index 372425b..a0ff0b2 100644 --- a/clientsideapp/templates/clientsideapp/index.html +++ b/clientsideapp/templates/clientsideapp/index.html @@ -9,8 +9,7 @@
  • Телефоны: +79788328885, +79788318999
  • Адрес: пос. Нижнегорский, ул. Победы 38. 2й этаж старого дома быта, возле рынка
  • - +
    diff --git a/clientsideapp/templates/clientsideapp/modal_activate_service.html b/clientsideapp/templates/clientsideapp/modal_activate_service.html deleted file mode 100644 index 545fb76..0000000 --- a/clientsideapp/templates/clientsideapp/modal_activate_service.html +++ /dev/null @@ -1,28 +0,0 @@ -
    {% csrf_token %} - - - -
    diff --git a/clientsideapp/templates/clientsideapp/modal_complete_service.html b/clientsideapp/templates/clientsideapp/modal_complete_service.html deleted file mode 100644 index cb4ca76..0000000 --- a/clientsideapp/templates/clientsideapp/modal_complete_service.html +++ /dev/null @@ -1,32 +0,0 @@ -
    {% csrf_token %} - - - -
    diff --git a/clientsideapp/templates/clientsideapp/modal_service_buy.html b/clientsideapp/templates/clientsideapp/modal_service_buy.html index 433a1a6..93d6d7d 100644 --- a/clientsideapp/templates/clientsideapp/modal_service_buy.html +++ b/clientsideapp/templates/clientsideapp/modal_service_buy.html @@ -6,13 +6,7 @@ diff --git a/clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html b/clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html deleted file mode 100644 index e8250c0..0000000 --- a/clientsideapp/templates/clientsideapp/modal_unsubscribe_service.html +++ /dev/null @@ -1,26 +0,0 @@ -{% load i18n %} -
    {% csrf_token %} - - - -
    diff --git a/clientsideapp/templates/clientsideapp/services.html b/clientsideapp/templates/clientsideapp/services.html index b900a28..9646d95 100644 --- a/clientsideapp/templates/clientsideapp/services.html +++ b/clientsideapp/templates/clientsideapp/services.html @@ -1,81 +1,72 @@ {% extends 'clientsideapp/ext.html' %} +{% load i18n %} {% block client_main %} diff --git a/clientsideapp/urls.py b/clientsideapp/urls.py index 434bb73..8e95779 100644 --- a/clientsideapp/urls.py +++ b/clientsideapp/urls.py @@ -8,9 +8,6 @@ urlpatterns = [ url(r'^pays$', views.pays, name='pays'), url(r'^services$', views.services, name='services'), url(r'^services/(?P\d+)/buy$', views.buy_service, name='buy_service'), - url(r'^services/(?P\d+)/finish$', views.complete_service, name='complete_service'), - url(r'^services/(?P\d+)/unsubscribe$', views.unsubscribe_service, name='unsubscribe_service'), - url(r'^services/(?P\d+)/activate$', views.activate_service, name='activate_service'), url(r'^debts$', views.debts_list, name='debts'), url(r'^debts/(?P\d+)$', views.debt_buy, name='debt_buy') ] diff --git a/clientsideapp/views.py b/clientsideapp/views.py index fb3d785..9666e8a 100644 --- a/clientsideapp/views.py +++ b/clientsideapp/views.py @@ -3,11 +3,12 @@ from django.contrib.auth.decorators import login_required from django.contrib.gis.shortcuts import render_to_text from django.shortcuts import render, get_object_or_404, redirect from django.contrib import messages -from django.utils import timezone +from django.db.transaction import atomic +from django.utils.translation import ugettext_lazy as _ from abonapp.models import AbonLog, AbonTariff, InvoiceForPayment, Abon from tariff_app.models import Tariff -from mydefs import pag_mn, RuTimedelta, LogicError +from mydefs import pag_mn, LogicError from agent import NasFailedResult, NasNetworkError @@ -29,32 +30,28 @@ def pays(request): def services(request): abon = Abon.objects.get(pk=request.user.pk) all_tarifs = abon.group.tariffs.filter(is_admin=False) - own_abon_tariffs = AbonTariff.objects.filter(abon=abon) - current_service = own_abon_tariffs.exclude(time_start=None) - current_service = current_service[0] if current_service.count() > 0 else None return render(request, 'clientsideapp/services.html', { 'tarifs': all_tarifs, - 'own_abon_tariffs': own_abon_tariffs, - 'current_service': current_service + 'current_service': abon.active_tariff() }) @login_required +@atomic def buy_service(request, srv_id): abon = get_object_or_404(Abon, pk=request.user.pk) service = get_object_or_404(Tariff, pk=srv_id) try: current_service = abon.active_tariff() if request.method == 'POST': - abon.pick_tariff(service, request.user, 'Покупка тарифного плана через личный кабинет, тариф "%s"' - % service) - messages.success(request, 'Вы подписались на новую услугу. Она встала на очередь подключений. ' - 'Когда закончится ваша текущая услуга, то включится эта') + abon.pick_tariff(service, request.user, _("Buy the service via user side, service '%s'") + % service) + messages.success(request, _("The service '%s' wan successfully activated") % service.title) else: return render_to_text('clientsideapp/modal_service_buy.html', { 'service': service, - 'current_service': current_service + 'current_service': current_service.tariff if current_service is not None else None }, request=request) except LogicError as e: messages.error(request, e) @@ -63,98 +60,6 @@ def buy_service(request, srv_id): return redirect('client_side:services') -@login_required -def complete_service(request, srv_id): - abtar = get_object_or_404(AbonTariff, id=srv_id) - service = abtar.tariff - - try: - if request.method == 'POST': - # досрочно завершаем услугу - finish_confirm = request.POST.get('finish_confirm') - if finish_confirm == 'yes': - # удаляем запись о текущей услуге. - abtar.delete() - messages.success(request, 'Услуга "%s" успешно завершена' % service.title) - AbonLog.objects.create( - abon=abtar.abon, - amount=0.0, - author=abtar.abon, - comment='Досрочное завершение услуги "%s" из личного кабинета' % service.title - ) - else: - messages.error(request, 'Действие не подтверждено') - else: - time_use = RuTimedelta(timezone.now() - abtar.time_start) - return render_to_text('clientsideapp/modal_complete_service.html', { - 'service': service, - 'abtar': abtar, - 'time_use': time_use - }, request=request) - except LogicError as e: - messages.error(request, e) - except NasFailedResult as e: - messages.error(request, e) - except NasNetworkError: - messages.error(request, 'Временные неполадки') - return redirect('client_side:services') - - -@login_required -def unsubscribe_service(request, srv_id): - abtar = get_object_or_404(AbonTariff, id=srv_id) - service = abtar.tariff - try: - if request.method == 'POST': - # досрочно завершаем услугу - if request.POST.get('finish_confirm') == 'yes': - AbonTariff.objects.get(pk=srv_id).delete() - messages.success(request, 'Вы успешно отписались от услуги, "%s"' % service.title) - else: - messages.error(request, 'Действие не подтверждено') - else: - return render_to_text('clientsideapp/modal_unsubscribe_service.html', { - 'abtar': abtar, - 'service': service - }, request=request) - except AbonTariff.DoesNotExist: - messages.error(request, 'Указанная подписка на услугу не найдена') - except NasFailedResult as e: - messages.error(request, e) - except NasNetworkError: - messages.error(request, 'Временные неполадки') - return redirect('client_side:services') - - -@login_required -def activate_service(request, srv_id): - abtar = get_object_or_404(AbonTariff, id=srv_id) - service = abtar.tariff - amount = abtar.calc_amount_service() - try: - if request.method == 'POST': - # активируем услугу - if request.POST.get('finish_confirm') == 'yes': - abtar.activate(request.user) - messages.success(request, 'Услуга "%s" успешно активирована' % service.title) - else: - messages.error(request, 'Запрос не подтверждён') - return redirect('client_side:services') - except NasFailedResult as e: - messages.error(request, e) - except NasNetworkError as e: - messages.warning(request, e) - except LogicError as e: - messages.error(request, e) - return render_to_text('clientsideapp/modal_activate_service.html', { - 'abtar': abtar, - 'service': service, - 'amount': amount, - 'abon': abtar.abon, - 'diff': abtar.abon.ballance - amount - }, request=request) - - @login_required def debts_list(request): debts = InvoiceForPayment.objects.filter(abon=request.user) @@ -164,6 +69,7 @@ def debts_list(request): @login_required +@atomic def debt_buy(request, d_id): debt = get_object_or_404(InvoiceForPayment, id=d_id) abon = get_object_or_404(Abon, id=request.user.id) @@ -171,9 +77,9 @@ def debt_buy(request, d_id): try: sure = request.POST.get('sure') if sure != 'on': - raise LogicError('Вы не уверены что хотите оплатить долг?') + raise LogicError(_("Are you not sure that you want buy the service?")) if abon.ballance < debt.amount: - raise LogicError('Не достаточно средств на счету') + raise LogicError(_('Your account have not enough money')) abon.make_pay(request.user, debt.amount) debt.set_ok() diff --git a/cron.py b/cron.py index e42d2d8..1369392 100755 --- a/cron.py +++ b/cron.py @@ -16,7 +16,7 @@ def main(): for user in users: try: # бдим за услугами абонента: просроченные отключить, заказанные подключить - user.activate_next_tariff(user) + user.bill_service(user) # если нет ip то и нет смысла лезть в NAS if user.ip_address is None: diff --git a/devapp/admin.py b/devapp/admin.py index e27201c..6541f13 100644 --- a/devapp/admin.py +++ b/devapp/admin.py @@ -5,4 +5,3 @@ from . import models admin.site.register(models.Device) admin.site.register(models.Port) -admin.site.register(models.PortStates) diff --git a/devapp/base_intr.py b/devapp/base_intr.py index 37b1218..970ae4b 100644 --- a/devapp/base_intr.py +++ b/devapp/base_intr.py @@ -28,6 +28,16 @@ class DevBase(object, metaclass=ABCMeta): def get_template_name(self): """Получаем путь к html шаблону отображения устройства""" + @staticmethod + @abstractmethod + def has_attachable_to_subscriber(): + """Можно-ли подключать устройство к абоненту""" + + @staticmethod + @abstractmethod + def is_use_device_port(): + """True если при авторизации по opt82 используется порт""" + class BasePort(object, metaclass=ABCMeta): def __init__(self, num, name, status, mac, speed): diff --git a/devapp/dev_types.py b/devapp/dev_types.py index b0e64be..d9798cc 100644 --- a/devapp/dev_types.py +++ b/devapp/dev_types.py @@ -5,22 +5,6 @@ from datetime import timedelta from .base_intr import DevBase, SNMPBaseWorker, BasePort - -oids = { - 'reboot': '.1.3.6.1.4.1.2021.8.1.101.1', - 'get_ports': { - 'names': '.1.3.6.1.4.1.171.10.134.2.1.1.100.2.1.3', - 'stats': '.1.3.6.1.2.1.2.2.1.7', - 'macs': '.1.3.6.1.2.1.2.2.1.6', - 'speeds': '.1.3.6.1.2.1.31.1.1.1.15' - }, - 'name': '.1.3.6.1.2.1.1.1.0', - 'position': '.1.3.6.1.2.1.1.5.0', - 'toggle_port': '.1.3.6.1.2.1.2.2.1.7', - 'uptime': '.1.3.6.1.2.1.1.8.0' -} - - class DLinkPort(BasePort): def __init__(self, num, name, status, mac, speed, snmpWorker): @@ -31,15 +15,13 @@ class DLinkPort(BasePort): # выключаем этот порт def disable(self): self.snmp_worker.set_int_value( - "%s.%d" % (oids['toggle_port'], self.num), - 2 + "%s.%d" % ('.1.3.6.1.2.1.2.2.1.7', self.num), 2 ) # включаем этот порт def enable(self): self.snmp_worker.set_int_value( - "%s.%d" % (oids['toggle_port'], self.num), - 1 + "%s.%d" % ('.1.3.6.1.2.1.2.2.1.7', self.num), 1 ) @@ -54,15 +36,15 @@ class DLinkDevice(DevBase, SNMPBaseWorker): return _('DLink switch') def reboot(self): - pass + return self.get_item('.1.3.6.1.4.1.2021.8.1.101.1') def get_ports(self): - nams = self.get_list(oids['get_ports']['names']) - stats = self.get_list(oids['get_ports']['stats']) - macs = self.get_list(oids['get_ports']['macs']) - speeds = self.get_list(oids['get_ports']['speeds']) + nams = self.get_list('.1.3.6.1.4.1.171.10.134.2.1.1.100.2.1.3') + stats = self.get_list('.1.3.6.1.2.1.2.2.1.7') + macs = self.get_list('.1.3.6.1.2.1.2.2.1.6') + speeds = self.get_list('.1.3.6.1.2.1.31.1.1.1.15') res = [] - ln = len(macs) + ln = len(speeds) for n in range(ln): status = True if int(stats[n]) == 1 else False res.append(DLinkPort( @@ -75,21 +57,29 @@ class DLinkDevice(DevBase, SNMPBaseWorker): return res def get_device_name(self): - return self.get_item(oids['name']) + return self.get_item('.1.3.6.1.2.1.1.1.0') def uptime(self): - uptimestamp = safe_int(self.get_item(oids['uptime'])) + uptimestamp = safe_int(self.get_item('.1.3.6.1.2.1.1.8.0')) tm = RuTimedelta(timedelta(seconds=uptimestamp/100)) or RuTimedelta(timedelta()) return tm def get_template_name(self): - return 'devapp/ports.html' + return 'ports.html' + + @staticmethod + def has_attachable_to_subscriber(): + return True + + @staticmethod + def is_use_device_port(): + return True class ONUdev(BasePort): def __init__(self, num, name, status, mac, speed, signal, snmpWorker): - BasePort.__init__(self, num, name, status, mac, speed) - assert issubclass(snmpWorker.__class__ , SNMPBaseWorker) + super(ONUdev, self).__init__(num, name, status, mac, speed) + assert issubclass(snmpWorker.__class__, SNMPBaseWorker) self.snmp_worker = snmpWorker self.signal = signal @@ -113,7 +103,7 @@ class OLTDevice(DevBase, SNMPBaseWorker): @staticmethod def description(): - return _('PON ONU') + return _('PON OLT') def reboot(self): pass @@ -146,10 +136,104 @@ class OLTDevice(DevBase, SNMPBaseWorker): return tm def get_template_name(self): - return 'devapp/olt.html' + return 'olt.html' + + @staticmethod + def has_attachable_to_subscriber(): + return False + + @staticmethod + def is_use_device_port(): + return False + + +class OnuDevice(DevBase, SNMPBaseWorker): + + @staticmethod + def description(): + return _('PON ONU') + + def reboot(self): + pass + + def get_ports(self): + pass + + def get_device_name(self): + pass + + def uptime(self): + pass + + def get_template_name(self): + return "onu.html" + + @staticmethod + def has_attachable_to_subscriber(): + return True + + @staticmethod + def is_use_device_port(): + return False + + +class EltexPort(BasePort): + + def __init__(self, num, name, status, mac, speed, snmpWorker): + BasePort.__init__(self, num, name, status, mac, speed) + assert issubclass(snmpWorker.__class__ , SNMPBaseWorker) + self.snmp_worker = snmpWorker + + # выключаем этот порт + def disable(self): + self.snmp_worker.set_int_value( + "%s.%d" % ('.1.3.6.1.2.1.2.2.1.7', self.num), + 2 + ) + + # включаем этот порт + def enable(self): + self.snmp_worker.set_int_value( + "%s.%d" % ('.1.3.6.1.2.1.2.2.1.7', self.num), + 1 + ) -DEVICE_TYPES = ( - ('Dl', DLinkDevice), - ('Pn', OLTDevice) -) +class EltexSwitch(DLinkDevice): + + @staticmethod + def description(): + return _('Eltex switch') + + def get_ports(self): + #nams = self.get_list('.1.3.6.1.4.1.171.10.134.2.1.1.100.2.1.3') + stats = self.get_list('.1.3.6.1.2.1.2.2.1.7') + oper_stats = self.get_list('.1.3.6.1.2.1.2.2.1.8') + #macs = self.get_list('.1.3.6.1.2.1.2.2.1.6') + speeds = self.get_list('.1.3.6.1.2.1.31.1.1.1.15') + res = [] + for n in range(28): + res.append(EltexPort( + n+1, + '',#nams[n] if len(nams) > 0 else _('does not fetch the name'), + True if int(stats[n]) == 1 else False, + '',#macs[n] if len(macs) > 0 else _('does not fetch the mac'), + int(speeds[n]) if len(speeds) > 0 and int(oper_stats[n]) == 1 else 0, + self)) + return res + + def get_device_name(self): + return self.get_item('.1.3.6.1.2.1.1.5.0') + + def uptime(self): + uptimestamp = safe_int(self.get_item('.1.3.6.1.2.1.1.3.0')) + tm = RuTimedelta(timedelta(seconds=uptimestamp/100)) or RuTimedelta(timedelta()) + return tm + + @staticmethod + def has_attachable_to_subscriber(): + return False + + @staticmethod + def is_use_device_port(): + return False diff --git a/devapp/forms.py b/devapp/forms.py index fa1c9e8..826c714 100644 --- a/devapp/forms.py +++ b/devapp/forms.py @@ -1,11 +1,23 @@ # -*- coding: utf-8 -*- from django import forms +from django.utils.translation import ugettext as _ +from django.db import IntegrityError from . import models from mydefs import ip_addr_regex +from djing import MAC_ADDR_REGEX class DeviceForm(forms.ModelForm): + mac_addr = forms.CharField(widget=forms.TextInput(attrs={ + 'pattern': MAC_ADDR_REGEX, + 'required': True, + 'class': 'form-control' + }), error_messages={ + 'required': _('Mac address is required for fill'), + 'unique': _('Device with that mac is already exist') + }) + class Meta: model = models.Device fields = '__all__' @@ -13,10 +25,9 @@ class DeviceForm(forms.ModelForm): 'ip_address': forms.TextInput(attrs={ 'pattern': ip_addr_regex, 'placeholder': '192.168.0.100', - 'required': True, 'class': 'form-control' }), - 'comment': forms.Textarea(attrs={ + 'comment': forms.TextInput(attrs={ 'required': True, 'class': 'form-control' }), @@ -31,5 +42,32 @@ class DeviceForm(forms.ModelForm): }), 'user_group': forms.Select(attrs={ 'class': 'form-control' + }), + 'parent_dev': forms.Select(attrs={ + 'class': 'form-control' }) } + + +class PortForm(forms.ModelForm): + class Meta: + model = models.Port + exclude = ['device'] + widgets = { + 'num': forms.NumberInput(attrs={ + 'class': 'form-control', + 'min': '0' + }), + 'descr': forms.TextInput(attrs={ + 'class': 'form-control' + }) + } + + def save(self, commit=True): + try: + super(PortForm, self).save(commit) + except IntegrityError as e: + if "Duplicate entry" in str(e): + raise models.DeviceDBException(_('Port number on device must be unique')) + else: + raise models.DeviceDBException(e) diff --git a/devapp/locale/ru/LC_MESSAGES/django.po b/devapp/locale/ru/LC_MESSAGES/django.po index 5c0d5eb..7a6f8e3 100644 --- a/devapp/locale/ru/LC_MESSAGES/django.po +++ b/devapp/locale/ru/LC_MESSAGES/django.po @@ -1,4 +1,3 @@ -# 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, 2017. @@ -8,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-02-27 12:27+0300\n" +"POT-Creation-Date: 2017-05-22 11:59+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Dmitry Novikov nerosketch@gmail.com\n" "Language: ru\n" @@ -19,20 +18,36 @@ 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" +#: devapp/dev_types.py:54 +msgid "DLink switch" +msgstr "Свич D'Link" + +#: devapp/dev_types.py:70 +msgid "does not fetch the name" +msgstr "не получил имя" + +#: devapp/dev_types.py:72 +msgid "does not fetch the mac" +msgstr "не нашёл мак" + +#: devapp/dev_types.py:116 +msgid "PON OLT" +msgstr "PON OLT голова" + #: devapp/templates/devapp/add_dev.html:7 #: devapp/templates/devapp/devices.html:7 #: devapp/templates/devapp/devices_null_group.html:7 #: devapp/templates/devapp/group_list.html:7 #: devapp/templates/devapp/group_list.html:10 msgid "Groups" -msgstr "" +msgstr "Группы" #: devapp/templates/devapp/add_dev.html:8 msgid "Add new device" -msgstr "" +msgstr "Добавить устройство" #: devapp/templates/devapp/add_dev.html:14 -#: devapp/templates/devapp/ports.html:143 +#: devapp/templates/devapp/ports.html:93 msgid "Not assigned" msgstr "<Не назначено>" @@ -68,7 +83,7 @@ msgstr "Точка топологии" #: devapp/templates/devapp/add_dev.html:71 devapp/templates/devapp/dev.html:59 msgid "User group" -msgstr "" +msgstr "Группа" #: devapp/templates/devapp/add_dev.html:81 devapp/templates/devapp/dev.html:69 msgid "Save" @@ -102,7 +117,7 @@ msgstr "Устройства без группы" #: devapp/templates/devapp/group_list.html:18 msgid "Group title" -msgstr "" +msgstr "Название" #: devapp/templates/devapp/group_list.html:28 #, fuzzy @@ -112,7 +127,30 @@ msgstr "Эта точка не пингуется" #: devapp/templates/devapp/group_list.html:37 msgid "Add group" -msgstr "" +msgstr "Добавить группу" + +#: devapp/templates/devapp/olt.html:13 +msgid "Mac" +msgstr "Мак" + +msgid "Mac address" +msgstr "Мак адрес" + +#: devapp/templates/devapp/olt.html:14 +msgid "Name" +msgstr "Имя" + +#: devapp/templates/devapp/olt.html:15 +msgid "Distance(m)" +msgstr "Расстояние (м)" + +#: devapp/templates/devapp/olt.html:16 +msgid "Signal" +msgstr "Ур. сигнала" + +#: devapp/templates/devapp/olt.html:34 +msgid "Ports not found" +msgstr "Порты не найдены" #: devapp/templates/devapp/ports.html:9 msgid "Title of the type of switch" @@ -122,39 +160,50 @@ msgstr "Название типа свича" msgid "Uptime" msgstr "Без перезагрузки" -#: devapp/templates/devapp/ports.html:42 +#: devapp/templates/devapp/ports.html:33 +msgid "Disable port" +msgstr "Выключить порт" + +#: devapp/templates/devapp/ports.html:37 +msgid "Enable port" +msgstr "Включить порт" + +#: devapp/templates/devapp/ports.html:43 msgid "We have not received info, please check options :(" msgstr "Инфа не получена, проверьте настройки :(" -#: devapp/templates/devapp/ports.html:54 +#: devapp/templates/devapp/ports.html:55 msgid "Device log" msgstr "Лог устройства" -#: devapp/templates/devapp/ports.html:61 +#: devapp/templates/devapp/ports.html:62 msgid "Level" msgstr "Уровень" -#: devapp/templates/devapp/ports.html:62 +#: devapp/templates/devapp/ports.html:63 msgid "Description" msgstr "Описание" -#: devapp/templates/devapp/ports.html:63 +#: devapp/templates/devapp/ports.html:64 msgid "Date" msgstr "Дата" -#: devapp/templates/devapp/ports.html:129 +#: devapp/templates/devapp/ports.html:79 msgid "Ports comment" msgstr "Комментарии портов" -#: devapp/templates/devapp/ports.html:135 +#: devapp/templates/devapp/ports.html:85 msgid "Port" msgstr "Порт" -#: devapp/templates/devapp/ports.html:136 +msgid "Ports" +msgstr "Порты" + +#: devapp/templates/devapp/ports.html:86 msgid "Title" msgstr "Название" -#: devapp/templates/devapp/ports.html:147 +#: devapp/templates/devapp/ports.html:97 msgid "We have not received info for ports" msgstr "Инфа о портах не получена" @@ -170,47 +219,84 @@ msgstr "Инфа о точке сохранена" msgid "Form is invalid, check fields and try again" msgstr "Ошибка в данных, проверте их ещё раз" -#: devapp/views.py:112 devapp/views.py:140 +#: devapp/views.py:114 devapp/views.py:144 msgid "Not Set snmp device password" msgstr "Не указан snmp пароль для устройства" -#: devapp/views.py:114 devapp/views.py:142 +#: devapp/views.py:116 devapp/views.py:146 msgid "Dot was not pinged" msgstr "Эта точка не пингуется" -#: devapp/views.py:116 +#: devapp/views.py:118 msgid "wait for a reply from the SNMP Timeout" msgstr "Время ожидания ответа от SNMP истекло" -msgid "Ports" -msgstr "Порты" +#: devapp/views.py:120 +msgid "SNMP error on device" +msgstr "Ошибка SNMP на устройстве" msgid "Edit" msgstr "Редактировать" +msgid "Device does not exist" +msgstr "Устойство не найдено" + +msgid "Number" +msgstr "Номер" + +msgid "Mode" +msgstr "Режим" + +msgid "Add" +msgstr "Добавить" + +msgid "Add ports" +msgstr "Добавить порты" + +msgid "Device is not have a group, please fix that" +msgstr "У устройства нет группы, пожалуйста, исправьте это" + +msgid "Delete" +msgstr "Удалить" + +msgid "Port does not exist" +msgstr "Порт не найден" + +msgid "Port successfully removed" +msgstr "Порт успешно удалён" + msgid "PON ONU" -msgstr "ONU Голова" +msgstr "Онушка" -msgid "does not fetch the name" -msgstr "не получил имя" +msgid "Are you sure that you want to delete switch port from db?" +msgstr "Вы уверены что хотите удалить порт свича из бд?" -msgid "does not fetch the mac" -msgstr "не нашёл мак" +msgid "Port successfully saved" +msgstr "Порт успешно сохранён" -msgid "Ports not found" -msgstr "Онушки не получил" +msgid "Port number on device must be unique" +msgstr "Номер порта на устройстве должен быть уникальным" -msgid "DLink switch" -msgstr "Свич D'Link" +msgid "Mac address is required for fill" +msgstr "MAC-адрес необходим для заполнения" -msgid "Mac" -msgstr "Мак" +msgid "Device with that mac is already exist" +msgstr "Устройство с этим мак-адресом уже есть" -msgid "Name" -msgstr "Имя" +msgid "Parent device" +msgstr "Родительское устройство" -msgid "Distance(m)" -msgstr "Расстояние (м)" +msgid "Attached user" +msgstr "Прикрепленный абонент" -msgid "Signal" -msgstr "Ур. сигнала" +msgid "Find the device" +msgstr "Найти устройство" + +msgid "Find the subscriber" +msgstr "Найти абонента" + +msgid "View the device" +msgstr "Посмотреть устройство" + +msgid "Eltex switch" +msgstr "Элтекс свич" diff --git a/devapp/migrations/0005_auto_20170502_2232.py b/devapp/migrations/0005_auto_20170502_2232.py index 104ce71..91cbe61 100644 --- a/devapp/migrations/0005_auto_20170502_2232.py +++ b/devapp/migrations/0005_auto_20170502_2232.py @@ -15,6 +15,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='devtype', - field=models.CharField(choices=[('Dl', 'DLink switch'), ('Pn', 'PON ONU')], default='Dl', max_length=2), + field=models.CharField(choices=[('Dl', 'DLink switch'), ('Pn', 'PON OLT')], default='Dl', max_length=2), ), ] diff --git a/devapp/migrations/0006_auto_20170705_1403.py b/devapp/migrations/0006_auto_20170705_1403.py new file mode 100644 index 0000000..33d3b75 --- /dev/null +++ b/devapp/migrations/0006_auto_20170705_1403.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-07-05 14:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import djing.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('devapp', '0005_auto_20170502_2232'), + ] + + operations = [ + migrations.RemoveField( + model_name='portstates', + name='port', + ), + migrations.RemoveField( + model_name='port', + name='speed', + ), + migrations.AddField( + model_name='device', + name='mac_addr', + field=djing.fields.MACAddressField(blank=True, integer=True, null=True, unique=True), + ), + migrations.AddField( + model_name='device', + name='parent_dev', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='devapp.Device'), + ), + migrations.AddField( + model_name='port', + name='descr', + field=models.CharField(blank=True, max_length=60, null=True), + ), + migrations.AlterField( + model_name='device', + name='devtype', + field=models.CharField(choices=[('Dl', 'DLink switch'), ('Pn', 'PON OLT'), ('On', 'PON ONU')], default='Dl', max_length=2), + ), + migrations.DeleteModel( + name='PortStates', + ), + ] diff --git a/devapp/migrations/0007_auto_20170816_1109.py b/devapp/migrations/0007_auto_20170816_1109.py new file mode 100644 index 0000000..3cc6c28 --- /dev/null +++ b/devapp/migrations/0007_auto_20170816_1109.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-08-16 11:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('devapp', '0006_auto_20170705_1403'), + ] + + operations = [ + migrations.AlterField( + model_name='device', + name='devtype', + field=models.CharField(choices=[('Dl', 'DLink switch'), ('Pn', 'PON OLT'), ('On', 'PON ONU'), ('Ex', 'Eltex switch')], default='Dl', max_length=2), + ), + ] diff --git a/devapp/models.py b/devapp/models.py index 75812df..aaf0842 100644 --- a/devapp/models.py +++ b/devapp/models.py @@ -1,24 +1,36 @@ # -*- coding: utf-8 -*- from django.db import models +from djing.fields import MACAddressField from .base_intr import DevBase from mydefs import MyGenericIPAddressField, MyChoicesAdapter -from .dev_types import DEVICE_TYPES +from . import dev_types from mapapp.models import Dot +from subprocess import call +from django.conf import settings -class _DeviceChoicesAdapter(MyChoicesAdapter): - def __init__(self): - super().__init__(DEVICE_TYPES) +DEVICE_TYPES = ( + ('Dl', dev_types.DLinkDevice), + ('Pn', dev_types.OLTDevice), + ('On', dev_types.OnuDevice), + ('Ex', dev_types.EltexSwitch) +) + + +class DeviceDBException(Exception): + pass class Device(models.Model): ip_address = MyGenericIPAddressField() + mac_addr = MACAddressField(null=True, blank=True, unique=True) comment = models.CharField(max_length=256) - devtype = models.CharField(max_length=2, default=DEVICE_TYPES[0][0], choices=_DeviceChoicesAdapter()) + devtype = models.CharField(max_length=2, default=DEVICE_TYPES[0][0], choices=MyChoicesAdapter(DEVICE_TYPES)) man_passw = models.CharField(max_length=16, null=True, blank=True) map_dot = models.ForeignKey(Dot, on_delete=models.SET_NULL, null=True, blank=True) user_group = models.ForeignKey('abonapp.AbonGroup', on_delete=models.SET_NULL, null=True, blank=True) + parent_dev = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL) class Meta: db_table = 'dev' @@ -37,25 +49,57 @@ class Device(models.Model): return res return + # Можно-ли подключать устройство к абоненту + def has_attachable_to_subscriber(self): + mngr_class = self.get_manager_klass() + return mngr_class.has_attachable_to_subscriber() + def __str__(self): - return "%s: (%s) %s" % (self.comment, self.get_devtype_display(), self.ip_address) + return "%s: (%s) %s %s" % (self.comment, self.get_devtype_display(), self.ip_address, self.mac_addr or '') class Port(models.Model): - PORT_SPEEDS = ( - ('h', '100Mbps'), - ('k', '1Gbps'), - ('d', '10Gbps') - ) device = models.ForeignKey(Device) num = models.PositiveSmallIntegerField(default=0) - speed = models.CharField(max_length=1, default=PORT_SPEEDS[0][0], choices=PORT_SPEEDS) + descr = models.CharField(max_length=60, null=True, blank=True) + + def __str__(self): + return "%d: %s" % (int(self.num), self.descr) class Meta: db_table = 'dev_port' unique_together = (('device', 'num')) -class PortStates(models.Model): - port = models.OneToOneField(Port) - state_json_info = models.TextField() +def dev_post_save_signal(sender, instance, **kwargs): + if instance.devtype != 'On': + return + grp = instance.user_group.pk + code = '' + if grp == 87: + code = 'chk' + elif grp == 85: + code = 'drf' + elif grp == 86: + code = 'eme' + elif grp == 84: + code = 'kunc' + elif grp == 47: + code = 'mtr' + elif grp == 60: + code = 'nvg' + elif grp == 65: + code = 'ohot' + elif grp == 89: + code = 'psh' + elif grp == 92: + code = 'str' + elif grp == 80: + code = 'uy' + elif grp == 79 or grp == 91: + code = 'zrk' + newmac = str(instance.mac_addr) + call(["%s/devapp/onu_register.sh" % settings.BASE_DIR, newmac, code]) + + +models.signals.post_save.connect(dev_post_save_signal, sender=Device) diff --git a/devapp/onu_register.sh b/devapp/onu_register.sh new file mode 100755 index 0000000..c835550 --- /dev/null +++ b/devapp/onu_register.sh @@ -0,0 +1,34 @@ +#!/bin/bash + + +# old mac address +if [[ $1 =~ ^([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]{1,2})$ ]]; then + MAC=$1 +else + echo "Bad mac $MAC addr" + exit +fi + + +# part code +if [[ $2 =~ ^[a-zA-Z]+$ ]]; then + PART_CODE=$2 +else + echo 'code must contains only letters' + exit +fi + + +DHCP_PATH='/etc/dhcp/macs' +PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/bin + + +if grep "${MAC}" "${DHCP_PATH}/${PART_CODE}.conf" > /dev/null; then + # mac is already exists + exit +else + # add new mac + echo "subclass \"${PART_CODE}\" \"${MAC}\";" >> "${DHCP_PATH}/${PART_CODE}.conf" + /usr/bin/sudo /usr/bin/systemctl restart dhcpd.service +fi + diff --git a/devapp/templates/devapp/add_dev.html b/devapp/templates/devapp/add_dev.html index ba3fc23..0646437 100644 --- a/devapp/templates/devapp/add_dev.html +++ b/devapp/templates/devapp/add_dev.html @@ -5,13 +5,14 @@ {% include 'message_block.html' %}
    @@ -20,7 +21,7 @@
    -
    {% csrf_token %} + {% csrf_token %}
    @@ -31,6 +32,23 @@
    +
    + + +
    + + {{ form.mac_addr }} + {% if already_dev %} + + + {{ already_dev.comment }} + + + {% endif %} +
    + {{ form.mac_addr.errors }} +
    +
    @@ -89,4 +107,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/devapp/templates/devapp/olt.html b/devapp/templates/devapp/custom_dev_page/olt.html similarity index 65% rename from devapp/templates/devapp/olt.html rename to devapp/templates/devapp/custom_dev_page/olt.html index f47a296..84d0a3a 100644 --- a/devapp/templates/devapp/olt.html +++ b/devapp/templates/devapp/custom_dev_page/olt.html @@ -2,38 +2,48 @@ {% load i18n %} {% block content %} -
    + {% if uptime %} + {% trans 'Uptime' %} {{ uptime }} + {% endif %} - + + + {% with dip=dev.ip_address grp=dev.user_group.pk %} {% for port in ports %} + {% empty %} - + {% endfor %} + {% endwith %}
    ## {% trans 'Mac' %} {% trans 'Name' %} {% trans 'Distance(m)' %} {% trans 'Signal' %}#
    {% if port.st %} - {% else %} + {% else %} {% endif %} {{ port.mac }} {{ port.nm }} {{ port.sp }} {{ port.signal }} + + + +
    {% trans 'Ports not found' %}{% trans 'Ports not found' %}
    diff --git a/devapp/templates/devapp/custom_dev_page/onu.html b/devapp/templates/devapp/custom_dev_page/onu.html new file mode 100644 index 0000000..0a70d20 --- /dev/null +++ b/devapp/templates/devapp/custom_dev_page/onu.html @@ -0,0 +1,42 @@ +{% extends request.is_ajax|yesno:'nullcont.htm,devapp/ext.htm' %} +{% load i18n %} +{% block content %} + +
    +
    +
    +
    +
    {{ dev.get_devtype_display|default:_('Title of the type of switch') }}. + {% if uptime %} + {% trans 'Uptime' %} {{ uptime }} + {% endif %} +
    +
    +
    +
      +
    • {% trans 'Ip address' %}: {{ dev.ip_address }}
    • +
    • {% trans 'Mac' %}: {{ dev.mac_addr }}
    • +
    • {% trans 'Description' %} {{ dev.comment }}
    • + {% for da in dev_accs %} +
    • {% trans 'Attached user' %}: + {% if da.group %} + {{ da.get_full_name }} + {% else %} + {{ da.get_full_name }} + {% endif %} +
    • + {% endfor %} + {% if dev.parent_dev %} +
    • + {% with pdev=dev.parent_dev pdgrp=dev.parent_dev.user_group %} + {% trans 'Parent device' %}: {{ pdev.ip_address }} {{ pdev.comment }} + {% endwith %} +
    • + {% endif %} +
    +
    +
    +
    +
    + +{% endblock %} diff --git a/devapp/templates/devapp/ports.html b/devapp/templates/devapp/custom_dev_page/ports.html similarity index 57% rename from devapp/templates/devapp/ports.html rename to devapp/templates/devapp/custom_dev_page/ports.html index 21ea774..e6f8884 100644 --- a/devapp/templates/devapp/ports.html +++ b/devapp/templates/devapp/custom_dev_page/ports.html @@ -7,36 +7,46 @@
    {{ dev.get_devtype_display|default:_('Title of the type of switch') }}. - {% trans 'Uptime' %} {{ uptime }}
    + {% if uptime %} + {% trans 'Uptime' %} {{ uptime }} + {% endif %} +
    {% for port in ports %} {% if port.st %} {% if port.sp == 10 %} -
    +
    + 10 mbps {% elif port.sp == 100 %} -
    +
    + 100 mbps {% elif port.sp == 1000 %} -
    +
    + 1 gbps + {% elif port.sp == 10000 %} +
    + 10 gbps {% else %} -
    +
    {% endif %} {% else %} -
    +
    {% endif %} - + {{ port.num }} - + {% else %} + + + + {% endif %}
    {% empty %}

    {% trans 'We have not received info, please check options :(' %}

    @@ -65,58 +75,7 @@
    1Err:Disableport Eth1 state Down21 Dec 12:14:55
    2Err:bugport Eth1 state Up21 Dec 12:15:23
    3Err:Otherport Eth1 state Up21 Dec 12:15:45
    1Err:Disableport Eth1 state Down21 Dec 12:14:55
    1Err:Disableport Eth1 state Down21 Dec 12:14:55
    1Err:Disableport Eth1 state Down21 Dec 12:14:55
    1Err:Disableport Eth1 state Down21 Dec 12:14:55
    1Err:Disableport Eth1 state Down21 Dec 12:14:55
    1Err:Disableport Eth1 state Down21 Dec 12:14:55Coming soon..
    diff --git a/devapp/templates/devapp/dev.html b/devapp/templates/devapp/dev.html index 1e74645..43e627d 100644 --- a/devapp/templates/devapp/dev.html +++ b/devapp/templates/devapp/dev.html @@ -8,7 +8,7 @@
    - {% csrf_token %} + {% csrf_token %}
    @@ -19,6 +19,23 @@
    +
    + + +
    + + {{ form.mac_addr }} + {% if already_dev %} + + + {{ already_dev.comment }} + + + {% endif %} +
    + {{ form.mac_addr.errors }} +
    +
    @@ -64,6 +81,23 @@
    +
    + + +
    + + + {% if selected_parent_dev %} + + + {% else %} + + + {% endif %} + {{ form.parent_dev.errors }} +
    +
    +
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/devapp/templates/devapp/devices.html b/devapp/templates/devapp/devices.html index 3e1c133..74e86b8 100644 --- a/devapp/templates/devapp/devices.html +++ b/devapp/templates/devapp/devices.html @@ -27,6 +27,7 @@ {% if order_by == 'comment' %}{% endif %} + {% trans 'Mac address' %} {% trans 'Device type' %} @@ -38,19 +39,21 @@ + {% with can_del_dev=perms.devapp.delete_device can_ch_dev=perms.devapp.change_device %} {% for dev in devices %} - {{ dev.ip_address }} + {{ dev.ip_address }} {{ dev.comment }} + {{ dev.mac_addr }} {{ dev.get_devtype_display }} - {% if perms.devapp.delete_device %} - + {% if can_del_dev %} + {% endif %} - {% if perms.devapp.change_device %} - + {% if can_ch_dev %} + {% endif %} @@ -58,15 +61,16 @@ {% empty %} - {% trans 'Devices does not found' %}. {% trans 'Create' %} + {% trans 'Devices does not found' %}. {% trans 'Create' %} {% endfor %} + {% endwith %} - - + + {% trans 'Create' %} diff --git a/devapp/templates/devapp/devices_null_group.html b/devapp/templates/devapp/devices_null_group.html index bca5f07..569825c 100644 --- a/devapp/templates/devapp/devices_null_group.html +++ b/devapp/templates/devapp/devices_null_group.html @@ -58,7 +58,7 @@ {% empty %} - {% trans 'Devices does not found' %}. {% trans 'Create' %} + {% trans 'Devices does not found' %}. {% trans 'Create' %} {% endfor %} @@ -66,7 +66,7 @@ - + {% trans 'Create' %} diff --git a/devapp/templates/devapp/ext.htm b/devapp/templates/devapp/ext.htm index 05bdbcd..7273f5a 100644 --- a/devapp/templates/devapp/ext.htm +++ b/devapp/templates/devapp/ext.htm @@ -21,7 +21,7 @@ @@ -42,4 +47,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/devapp/templates/devapp/manage_ports/add_ports.html b/devapp/templates/devapp/manage_ports/add_ports.html new file mode 100644 index 0000000..33bb77c --- /dev/null +++ b/devapp/templates/devapp/manage_ports/add_ports.html @@ -0,0 +1,93 @@ +{% extends request.is_ajax|yesno:'bajax.html,base.html' %} +{% load i18n %} +{% block main %} + + + + {% include 'message_block.html' %} + + + +
    +
    +

    {{ dev.comment }}

    +
    +
    + + {% csrf_token %} + + + + + + + + + + + + {% with gid=dev.user_group.pk did=dev.pk can_del_port=perms.devapp.delete_port %} + {% for port in ports %} + + + + + + + {% endfor %} + {% endwith %} + + + + + + +
    #{% trans 'Mode' %}{% trans 'Description' %}
    {% if port.status %} + + {% else %} + + {% endif %}{{ port.pid }}{{ port.mode }} + + + + {% if port.from_db %} + {% if can_del_port %} + + {% else %} + + {% endif %} + + + {% else %} + + + + {% endif %} + +
    + {% if perms.devapp.add_port and ports %} + + {% else %} + + {% endif %} +
    + + + + +
    +
    + +{% endblock %} diff --git a/devapp/templates/devapp/manage_ports/list.html b/devapp/templates/devapp/manage_ports/list.html new file mode 100644 index 0000000..c2421ba --- /dev/null +++ b/devapp/templates/devapp/manage_ports/list.html @@ -0,0 +1,60 @@ +{% extends request.is_ajax|yesno:'nullcont.htm,devapp/ext.htm' %} +{% load i18n %} +{% block content %} + +
    +
    +
    + + + + + + + + + + + {% with gid=dev.user_group.pk did=dev.pk can_del_port=perms.devapp.delete_port can_edit_port=perms.devapp.change_port %} + {% for port in ports %} + + + + + + {% empty %} + + + + {% endfor %} + {% endwith %} + + + + + + + +
    {% trans 'Number' %}{% trans 'Description' %}#
    {{ port.num }}{{ port.descr }} + {% if can_del_port %} + + + + {% endif %} + {% if can_edit_port %} + + + + {% endif %} +
    {% trans 'Ports not found' %}
    + {% if perms.devapp.add_port %} + + {% trans 'Add ports' %} + + {% endif %} +
    +
    +
    +
    + +{% endblock %} diff --git a/devapp/templates/devapp/manage_ports/modal_add_edit_port.html b/devapp/templates/devapp/manage_ports/modal_add_edit_port.html new file mode 100644 index 0000000..7442975 --- /dev/null +++ b/devapp/templates/devapp/manage_ports/modal_add_edit_port.html @@ -0,0 +1,37 @@ +{% load i18n %} + +{% if port_id %} +
    {% else %} +{% endif %}{% csrf_token %} + + + + + +
    diff --git a/devapp/templates/devapp/manage_ports/modal_del_port.html b/devapp/templates/devapp/manage_ports/modal_del_port.html new file mode 100644 index 0000000..33e15d2 --- /dev/null +++ b/devapp/templates/devapp/manage_ports/modal_del_port.html @@ -0,0 +1,18 @@ +{% load i18n %} + +
    {% csrf_token %} + + + + + +
    diff --git a/devapp/urls.py b/devapp/urls.py index c79a932..b555031 100644 --- a/devapp/urls.py +++ b/devapp/urls.py @@ -5,11 +5,17 @@ from . import views urlpatterns = [ url(r'^$', views.group_list, name='group_list'), - url(r'^add$', views.dev, name='add'), url(r'^devices_without_groups$', views.devices_null_group, name='devices_null_group'), url(r'^(?P\d+)$', views.devices, name='devs'), + url(r'^(?P\d+)/add$', views.dev, name='add'), url(r'^(\d+)/(?P\d+)$', views.devview, name='view'), url(r'^(\d+)/(?P\d+)/del$', views.devdel, name='del'), - url(r'^(\d+)/(?P\d+)/edit$', views.dev, name='edit'), - url(r'^(\d+)/(?P\d+)/(?P\d+)_(?P[0-1]{1})$', views.toggle_port, name='port_toggle') + url(r'^(?P\d+)/(?P\d+)/add$', views.add_single_port, name='add_port'), + url(r'^(?P\d+)/(?P\d+)/edit$', views.dev, name='edit'), + url(r'^(\d+)/(?P\d+)/ports$', views.manage_ports, name='manage_ports'), + url(r'^(\d+)/(?P\d+)/ports_add', views.add_ports, name='add_ports'), + url(r'^(\d+)/(?P\d+)/(?P\d+)_(?P[0-1]{1})$', views.toggle_port, name='port_toggle'), + url(r'^(?P\d+)/(?P\d+)/(?P\d+)/del$', views.delete_single_port, name='del_port'), + url(r'^(?P\d+)/(?P\d+)/(?P\d+)/edit$', views.edit_single_port, name='edit_port'), + url(r'^search_dev$', views.search_dev) ] diff --git a/devapp/views.py b/devapp/views.py index 65126a4..46bab38 100644 --- a/devapp/views.py +++ b/devapp/views.py @@ -1,15 +1,20 @@ # -*- coding: utf-8 -*- from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.gis.shortcuts import render_to_text from django.core.exceptions import PermissionDenied +from django.db.models import Q +from django.http import HttpResponse from django.shortcuts import render, redirect, get_object_or_404, resolve_url from django.contrib import messages from django.utils.translation import ugettext_lazy as _ from easysnmp import EasySNMPTimeoutError, EasySNMPError +from json import dumps -from .models import Device +from .models import Device, Port, DeviceDBException from mydefs import pag_mn, res_success, res_error, only_admins, ping, order_helper -from .forms import DeviceForm -from abonapp.models import AbonGroup +from .forms import DeviceForm, PortForm +from abonapp.models import AbonGroup, Abon +from djing.settings import DEFAULT_SNMP_PASSWORD @login_required @@ -61,12 +66,16 @@ def devdel(request, did): return res_success(request, back_url) except Device.DoesNotExist: return res_error(request, _('Delete failed')) + except DeviceDBException as e: + return res_error(request, e) @login_required @only_admins -def dev(request, devid=0): +def dev(request, grp, devid=0): devinst = get_object_or_404(Device, id=devid) if devid != 0 else None + user_group = get_object_or_404(AbonGroup, pk=grp) + already_dev = None if request.method == 'POST': if devid == 0: @@ -77,32 +86,220 @@ def dev(request, devid=0): raise PermissionDenied frm = DeviceForm(request.POST, instance=devinst) if frm.is_valid(): - frm.save() + ndev = frm.save() messages.success(request, _('Device info has been saved')) + return redirect('devapp:edit', grp, ndev.pk) else: + try: + already_dev = Device.objects.get(mac_addr=request.POST.get('mac_addr')) + except Device.DoesNotExist: + pass messages.error(request, _('Form is invalid, check fields and try again')) else: - frm = DeviceForm(instance=devinst) + if devinst is None: + frm = DeviceForm(initial={ + 'user_group': user_group, + 'devtype': request.GET.get('t'), + 'mac_addr': request.GET.get('mac'), + 'comment': request.GET.get('c'), + 'ip_address': request.GET.get('ip'), + 'man_passw': DEFAULT_SNMP_PASSWORD + }) + else: + frm = DeviceForm(instance=devinst) if devinst is None: return render(request, 'devapp/add_dev.html', { - 'form': frm + 'form': frm, + 'group': user_group, + 'already_dev': already_dev }) else: return render(request, 'devapp/dev.html', { 'form': frm, - 'dev': devinst + 'dev': devinst, + 'selected_parent_dev': devinst.parent_dev or None, + 'group': user_group, + 'already_dev': already_dev }) +@login_required +@permission_required('devapp.change_device') +def manage_ports(request, devid): + try: + dev = Device.objects.get(pk=devid) + if dev.user_group is None: + messages.error(request, _('Device is not have a group, please fix that')) + return redirect('devapp:group_list') + ports = Port.objects.filter(device=dev) + + except Device.DoesNotExist: + messages.error(request, _('Device does not exist')) + return redirect('devapp:view', dev.user_group.pk if dev.user_group else 0, did=devid) + except DeviceDBException as e: + messages.error(request, e) + return render(request, 'devapp/manage_ports/list.html', { + 'ports': ports, + 'dev': dev + }) + + +@login_required +@permission_required('devapp.add_port') +def add_ports(request, devid): + class TempPort: + def __init__(self, pid, text, status, from_db, pk=None): + self.pid = pid + self.text = text + self.status = status + self.from_db = from_db + self.pk = pk + + def __eq__(self, other): + return self.pid == other.pid + + def __hash__(self): + return self.pid + + def __str__(self): + return "p:%d\tM:%s\tT:%s" % (self.pid, self.text) + + try: + res_ports = list() + dev = Device.objects.get(pk=devid) + if dev.user_group is None: + messages.error(request, _('Device is not have a group, please fix that')) + return redirect('devapp:group_list') + if request.method == 'POST': + ports = zip( + request.POST.getlist('p_text'), + request.POST.getlist('pids') + ) + for port_text, port_num in ports: + if port_text == '' or port_text is None: + continue + try: + port = Port.objects.get(num=port_num, device=dev) + port.descr = port_text + port.save(update_fields=['descr']) + except Port.DoesNotExist: + Port.objects.create( + num=port_num, + device=dev, + descr=port_text + ) + + db_ports = Port.objects.filter(device=dev) + db_ports = [TempPort(p.num, p.descr, None, True, p.pk) for p in db_ports] + + manager = dev.get_manager_klass()(dev.ip_address, dev.man_passw) + ports = manager.get_ports() + if ports is not None: + ports = [TempPort(p.num, p.nm, p.st, False) for p in ports] + res_ports = set(db_ports + ports) + else: + res_ports = db_ports + + except Device.DoesNotExist: + messages.error(request, _('Device does not exist')) + return redirect('devapp:group_list') + except DeviceDBException as e: + messages.error(request, e) + except EasySNMPTimeoutError: + messages.error(request, _('wait for a reply from the SNMP Timeout')) + return render(request, 'devapp/manage_ports/add_ports.html', { + 'ports': res_ports, + 'dev': dev + }) + + +@login_required +@permission_required('devapp.delete_port') +def delete_single_port(request, grp, did, portid): + try: + if request.method == 'POST': + if request.POST.get('confirm') == 'yes': + Port.objects.get(pk=portid).delete() + messages.success(request, _('Port successfully removed')) + else: + return render_to_text('devapp/manage_ports/modal_del_port.html', { + 'grp': grp, + 'did': did, + 'port_id': portid + }, request=request) + except Port.DoesNotExist: + messages.error(request, _('Port does not exist')) + except DeviceDBException as e: + messages.error(request, e) + return redirect('devapp:manage_ports', grp, did) + + +@login_required +@permission_required('devapp.add_port') +def edit_single_port(request, grp, did, pid): + try: + port = Port.objects.get(pk=pid) + if request.method == 'POST': + frm = PortForm(request.POST, instance=port) + if frm.is_valid(): + frm.save() + messages.success(request, _('Port successfully saved')) + else: + messages.error(request, _('Form is invalid, check fields and try again')) + return redirect('devapp:manage_ports', grp, did) + + frm = PortForm(instance=port) + return render_to_text('devapp/manage_ports/modal_add_edit_port.html', { + 'port_id': pid, + 'did': did, + 'gid': grp, + 'form': frm + }, request=request) + except Port.DoesNotExist: + messages.error(request, _('Port does not exist')) + except DeviceDBException as e: + messages.error(request, e) + return redirect('devapp:manage_ports', grp, did) + + +@login_required +@permission_required('devapp.add_port') +def add_single_port(request, grp, did): + try: + device = Device.objects.get(pk=did) + if request.method == 'POST': + frm = PortForm(request.POST, instance=Port(device=device)) + if frm.is_valid(): + frm.save() + messages.success(request, _('Port successfully saved')) + return redirect('devapp:manage_ports', grp, did) + else: + messages.error(request, _('Form is invalid, check fields and try again')) + else: + frm = PortForm(initial={ + 'num': request.GET.get('n'), + 'descr': request.GET.get('t') + }) + return render_to_text('devapp/manage_ports/modal_add_edit_port.html', { + 'did': did, + 'gid': grp, + 'form': frm + }, request=request) + except Device.DoesNotExist: + messages.error(request, _('Device does not exist')) + except DeviceDBException as e: + messages.error(request, e) + return redirect('devapp:manage_ports', grp, did) + + @login_required @only_admins def devview(request, did): - ports = None uptime = 0 dev = get_object_or_404(Device, id=did) - template_name = 'devapp/ports.html' + template_name = 'ports.html' try: if ping(dev.ip_address): if dev.man_passw: @@ -118,11 +315,14 @@ def devview(request, did): messages.error(request, _('wait for a reply from the SNMP Timeout')) except EasySNMPError: messages.error(request, _('SNMP error on device')) + except DeviceDBException as e: + messages.error(request, e) - return render(request, template_name, { + return render(request, 'devapp/custom_dev_page/'+template_name, { 'dev': dev, 'ports': ports, - 'uptime': uptime + 'uptime': uptime, + 'dev_accs': Abon.objects.filter(device=dev) }) @@ -132,18 +332,21 @@ def toggle_port(request, did, portid, status=0): portid = int(portid) status = int(status) dev = get_object_or_404(Device, id=int(did)) - if ping(dev.ip_address): - if dev.man_passw: - manager = dev.get_manager_klass()(dev.ip_address, dev.man_passw) - ports = manager.get_ports() - if status: - ports[portid-1].enable() + try: + if ping(dev.ip_address): + if dev.man_passw: + manager = dev.get_manager_klass()(dev.ip_address, dev.man_passw) + ports = manager.get_ports() + if status: + ports[portid-1].enable() + else: + ports[portid-1].disable() else: - ports[portid-1].disable() + messages.warning(request, _('Not Set snmp device password')) else: - messages.warning(request, _('Not Set snmp device password')) - else: - messages.error(request, _('Dot was not pinged')) + messages.error(request, _('Dot was not pinged')) + except EasySNMPTimeoutError: + messages.error(request, _('wait for a reply from the SNMP Timeout')) return redirect('devapp:view', dev.user_group.pk if dev.user_group is not None else 0, did) @@ -154,3 +357,14 @@ def group_list(request): return render(request, 'devapp/group_list.html', { 'groups': groups }) + + +@login_required +def search_dev(request): + word = request.GET.get('s') + if word is None: + results = [{'id': 0, 'text': ''}] + else: + results = Device.objects.filter(Q(comment__icontains=word) | Q(ip_address=word))[:16] + results = [{'id': dev.pk, 'text': "%s: %s" % (dev.ip_address, dev.comment)} for dev in results] + return HttpResponse(dumps(results, ensure_ascii=False)) diff --git a/dhcp_lever.py b/dhcp_lever.py index f499cc5..f6ac08c 100755 --- a/dhcp_lever.py +++ b/dhcp_lever.py @@ -1,13 +1,7 @@ #!/usr/bin/env python3 -import os import sys -import django -from django.core.exceptions import MultipleObjectsReturned, ValidationError -from django.utils.translation import ugettext as _ -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") -django.setup() -from agent import NasFailedResult, NasNetworkError -from abonapp.models import Abon, Opt82 +from redis import Redis +from rq import Queue def die(text): @@ -15,64 +9,17 @@ def die(text): exit(1) -def get_82_opts(switch_mac, switch_port): - try: - opt82 = Opt82.objects.get(mac=switch_mac, port=switch_port) - except MultipleObjectsReturned: - Opt82.objects.filter(mac=switch_mac, port=switch_port)[1:].delete() - return get_82_opts(switch_mac, switch_port) - except Opt82.DoesNotExist: - opt82 = Opt82.objects.create(mac=switch_mac, port=switch_port) - return opt82 - - -def dhcp_commit(client_ip, client_mac, switch_mac, switch_port): - opt82 = get_82_opts(switch_mac, switch_port) - if opt82 is None: - print(_("ERROR: opt82 with mac:%s and port:%d does not exist in db") % (switch_mac, switch_port)) - return - try: - abon = Abon.objects.get(opt82=opt82) - abon.ip_address = client_ip - abon.is_dhcp = True - abon.save(update_fields=['ip_address']) - print(_('Ip address update for %s successfull') % abon.get_short_name()) - except Abon.DoesNotExist: - print('ERROR: abon with option82(%s-%d) does not exist' % (opt82.mac, opt82.port)) - - -def dhcp_expiry(client_ip): - try: - abon = Abon.objects.get(ip_address=client_ip) - abon.ip_address = None - abon.is_dhcp = True - abon.save(update_fields=['ip_address']) - except Abon.DoesNotExist: - pass - - -def dhcp_release(client_ip): - dhcp_expiry(client_ip) - - -def main(argv): +if __name__ == "__main__": + argv = sys.argv if len(argv) < 3: - die(_('Too few arguments, exiting...')) + die('Too few arguments, exiting...') action = argv[1] + q = Queue(connection=Redis()) if action == 'commit': if len(argv) < 6: - die(_('Too few arguments, exiting...')) - dhcp_commit(argv[2], argv[3], argv[4], int(argv[5])) + die('Too few arguments, exiting...') + q.enqueue('agent.commands.dhcp.dhcp_commit', argv[2], argv[3], argv[4], int(argv[5])) elif action == 'expiry': - dhcp_expiry(argv[2]) + q.enqueue('agent.commands.dhcp.dhcp_expiry', argv[2]) elif action == 'release': - dhcp_release(argv[2]) - - -if __name__ == "__main__": - try: - main(sys.argv) - except (NasNetworkError, NasFailedResult) as e: - print('NAS:', e) - except (ValidationError, ValueError) as e: - print('ERROR:', e) + q.enqueue('agent.commands.dhcp.dhcp_release', argv[2]) diff --git a/dialing_app/__init__.py b/dialing_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dialing_app/admin.py b/dialing_app/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/dialing_app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/dialing_app/apps.py b/dialing_app/apps.py new file mode 100644 index 0000000..42a981b --- /dev/null +++ b/dialing_app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class DialingAppConfig(AppConfig): + name = 'dialing_app' diff --git a/dialing_app/locale/ru/LC_MESSAGES/django.po b/dialing_app/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000..4889f2b --- /dev/null +++ b/dialing_app/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,108 @@ +# 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, 2017. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-05-22 11:59+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Dmitry Novikov nerosketch@gmail.com\n" +"Language: \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" + +#: dialing_app/models.py:8 dialing_app/models.py:43 +msgid "No answer" +msgstr "Не отвечен" + +#: dialing_app/models.py:9 dialing_app/models.py:45 +msgid "Failed" +msgstr "С ошибкой" + +#: dialing_app/models.py:10 dialing_app/models.py:47 +msgid "Busy" +msgstr "Занято" + +#: dialing_app/models.py:11 dialing_app/models.py:49 +msgid "Answered" +msgstr "Отвечен" + +#: dialing_app/models.py:12 dialing_app/models.py:51 +msgid "Unknown" +msgstr "Не определён" + +#: dialing_app/templates/index.html:9 +msgid "Dialing" +msgstr "Звонки" + +#: dialing_app/templates/index.html:12 +msgid "Last calls" +msgstr "Последние звонки" + +#: dialing_app/templates/index.html:20 +msgid "Play" +msgstr "Слушать" + +#: dialing_app/templates/index.html:21 +msgid "calldate" +msgstr "дата звонка" + +#: dialing_app/templates/index.html:22 +msgid "src" +msgstr "кто" + +#: dialing_app/templates/index.html:23 +msgid "dst" +msgstr "куда" + +#: dialing_app/templates/index.html:24 +msgid "duration" +msgstr "продолжительность" + +#: dialing_app/templates/index.html:25 +msgid "start" +msgstr "начало" + +#: dialing_app/templates/index.html:26 +msgid "answer" +msgstr "ответ" + +#: dialing_app/templates/index.html:27 +msgid "end" +msgstr "конец" + +#: dialing_app/templates/index.html:28 +msgid "disposition" +msgstr "состояние" + +#: dialing_app/templates/index.html:50 +msgid "Calls was not found" +msgstr "Звонки не найдены" + +#: dialing_app/views.py:27 +msgid "Multiple users with the telephone number" +msgstr "Несколько абонентов с указанным номером телефона" + +#: dialing_app/views.py:29 +msgid "User with the telephone number not found" +msgstr "Абонент с таким номером телефона не найден" + +msgid "Voice mail" +msgstr "Оставленные сообщения" + +msgid "Type" +msgstr "Тип" + +msgid "Request" +msgstr "Заявка" + +msgid "Report" +msgstr "Поломка" \ No newline at end of file diff --git a/dialing_app/migrations/0001_initial.py b/dialing_app/migrations/0001_initial.py new file mode 100644 index 0000000..9459b1a --- /dev/null +++ b/dialing_app/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-05-30 13:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AsteriskCDR', + fields=[ + ('calldate', models.DateTimeField(default='0000-00-00 00:00:00', primary_key=True, serialize=False)), + ('clid', models.CharField(default='', max_length=80)), + ('src', models.CharField(default='', max_length=80)), + ('dst', models.CharField(default='', max_length=80)), + ('dcontext', models.CharField(default='', max_length=80)), + ('channel', models.CharField(default='', max_length=80)), + ('dstchannel', models.CharField(default='', max_length=80)), + ('lastapp', models.CharField(default='', max_length=80)), + ('lastdata', models.CharField(default='', max_length=80)), + ('duration', models.IntegerField(default=0)), + ('billsec', models.IntegerField(default=0)), + ('start', models.DateTimeField(blank=True, default=None, null=True)), + ('answer', models.DateTimeField(blank=True, default=None, null=True)), + ('end', models.DateTimeField(blank=True, default=None, null=True)), + ('disposition', models.CharField(choices=[('NO ANSWER', 'No answer'), ('FAILED', 'Failed'), ('BUSY', 'Busy'), ('ANSWERED', 'Answered'), ('UNKNOWN', 'Unknown')], default='', max_length=45)), + ('amaflags', models.IntegerField(default=0)), + ('accountcode', models.CharField(default='', max_length=20)), + ('userfield', models.CharField(default='', max_length=255)), + ('uniqueid', models.CharField(default='', max_length=32)), + ], + options={ + 'db_table': 'cdr', + }, + ), + ] diff --git a/dialing_app/migrations/__init__.py b/dialing_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dialing_app/models.py b/dialing_app/models.py new file mode 100644 index 0000000..ff6c720 --- /dev/null +++ b/dialing_app/models.py @@ -0,0 +1,63 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from djing import settings + + +class AsteriskCDR(models.Model): + DISPOSITION_CHOICES = ( + ('NO ANSWER', _('No answer')), + ('FAILED', _('Failed')), + ('BUSY', _('Busy')), + ('ANSWERED', _('Answered')), + ('UNKNOWN', _('Unknown')) + ) + calldate = models.DateTimeField(default='0000-00-00 00:00:00', primary_key=True) + clid = models.CharField(max_length=80, default='') + src = models.CharField(max_length=80, default='') + dst = models.CharField(max_length=80, default='') + dcontext = models.CharField(max_length=80, default='') + channel = models.CharField(max_length=80, default='') + dstchannel = models.CharField(max_length=80, default='') + lastapp = models.CharField(max_length=80, default='') + lastdata = models.CharField(max_length=80, default='') + duration = models.IntegerField(default=0) + billsec = models.IntegerField(default=0) + start = models.DateTimeField(null=True, blank=True, default=None) + answer = models.DateTimeField(null=True, blank=True, default=None) + end = models.DateTimeField(null=True, blank=True, default=None) + disposition = models.CharField(max_length=45, choices=DISPOSITION_CHOICES, default='') + amaflags = models.IntegerField(default=0) + accountcode = models.CharField(max_length=20, default='') + userfield = models.CharField(max_length=255, default='') + uniqueid = models.CharField(max_length=32, default='') + + def save(self, *args, **kwargs): + return + + def delete(self, *args, **kwargs): + return + + def locate_disposition(self): + dsp = self.disposition + if dsp == 'NO ANSWER': + return _('No answer') + elif dsp == 'FAILED': + return _('Failed') + elif dsp == 'BUSY': + return _('Busy') + elif dsp == 'ANSWERED': + return _('Answered') + elif dsp == 'UNKNOWN': + return _('Unknown') + return '' + + def path_to_media(self): + path = getattr(settings, 'DIALING_MEDIA', '/media') + if self.userfield == 'request': + return "%s/recording/request" % path + elif self.userfield == 'report': + return "%s/recording/bug" % path + return "%s/monitor" % path + + class Meta: + db_table = 'cdr' diff --git a/dialing_app/templates/ext.html b/dialing_app/templates/ext.html new file mode 100644 index 0000000..5d01acc --- /dev/null +++ b/dialing_app/templates/ext.html @@ -0,0 +1,40 @@ +{% extends request.is_ajax|yesno:'bajax.html,base.html' %} +{% load i18n %} +{% block main %} + + + + {% include 'message_block.html' %} + + + + + +
    +
    + {% block content %}{% endblock %} +
    +
    + +{% endblock %} diff --git a/dialing_app/templates/index.html b/dialing_app/templates/index.html new file mode 100644 index 0000000..adc5061 --- /dev/null +++ b/dialing_app/templates/index.html @@ -0,0 +1,53 @@ +{% extends request.is_ajax|yesno:'nullcont.htm,ext.html' %} +{% load i18n %} +{% load telephone_filters %} +{% block content %} + +
    + + + + + + + + + + + + + + + + {% for log in logs %} + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans 'Play' %}{% trans 'calldate' %}{% trans 'src' %}{% trans 'dst' %}{% trans 'duration' %}{% trans 'start' %}{% trans 'answer' %}{% trans 'end' %}{% trans 'disposition' %}
    + + + + + {{ log.calldate|date:'d E Y, H:i:s' }}{{ log.src|abon_if_telephone|safe }}{{ log.dst|abon_if_telephone|safe }}{{ log.duration }}{{ log.start|date:'d E Y, H:i:s' }}{{ log.answer|date:'d E Y, H:i:s' }}{{ log.end|date:'d E Y, H:i:s' }}{{ log.locate_disposition }}
    {% trans 'Calls was not found' %}
    +
    + + {% include 'toolbar_page.html' with pag=logs %} + +{% endblock %} diff --git a/dialing_app/templates/vmail.html b/dialing_app/templates/vmail.html new file mode 100644 index 0000000..fb4169a --- /dev/null +++ b/dialing_app/templates/vmail.html @@ -0,0 +1,57 @@ +{% extends request.is_ajax|yesno:'nullcont.htm,ext.html' %} +{% load i18n %} +{% load telephone_filters %} +{% block content %} + +
    + + + + + + + + + + + + + + + + {% for vmail in vmessages %} + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans 'Play' %}{% trans 'calldate' %}{% trans 'src' %}{% trans 'Type' %}{% trans 'duration' %}{% trans 'start' %}{% trans 'answer' %}{% trans 'end' %}{% trans 'disposition' %}
    + + + + + {{ vmail.calldate|date:'d E Y, H:i:s' }}{{ vmail.src|abon_if_telephone|safe }} + {% if vmail.userfield == 'request' %}{% trans 'Request' %} + {% elif vmail.userfield == 'report' %}{% trans 'Report' %} + {% else %}{{ vmail.userfield }}{% endif %} + {{ vmail.duration }}{{ vmail.start|date:'d E Y, H:i:s' }}{{ vmail.answer|date:'d E Y, H:i:s' }}{{ vmail.end|date:'d E Y, H:i:s' }}{{ vmail.locate_disposition }}
    {% trans 'Calls was not found' %}
    +
    + + {% include 'toolbar_page.html' with pag=vmessages %} + +{% endblock %} diff --git a/dialing_app/templatetags/__init__.py b/dialing_app/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dialing_app/templatetags/telephone_filters.py b/dialing_app/templatetags/telephone_filters.py new file mode 100644 index 0000000..83d9589 --- /dev/null +++ b/dialing_app/templatetags/telephone_filters.py @@ -0,0 +1,20 @@ +import re +from django import template +from django.shortcuts import resolve_url +from django.template.defaultfilters import stringfilter + +register = template.Library() + + +@register.filter +@stringfilter +def abon_if_telephone(value): + """Возвращаем ссыль на абонента если передали номер телефона""" + if re.match(r'^\+?\d+$', value): + if value[0] != '+': + value = '+'+value + url = resolve_url('dialapp:to_abon', tel=value) + a = '%s' % (url, value) + return a + else: + return value diff --git a/dialing_app/tests.py b/dialing_app/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/dialing_app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dialing_app/urls.py b/dialing_app/urls.py new file mode 100644 index 0000000..a35f0e2 --- /dev/null +++ b/dialing_app/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import url +from . import views + + +urlpatterns = [ + url(r'^$', views.home, name='home'), + url(r'^to_abon(?P\+?\d+)$', views.to_abon, name='to_abon'), + url(r'^voicemail$', views.vmail, name='vmail') +] diff --git a/dialing_app/views.py b/dialing_app/views.py new file mode 100644 index 0000000..98661eb --- /dev/null +++ b/dialing_app/views.py @@ -0,0 +1,49 @@ +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.shortcuts import render, redirect +from django.utils.translation import ugettext_lazy as _ + +from abonapp.models import Abon +from mydefs import only_admins, pag_mn +from .models import AsteriskCDR + + +@login_required +@only_admins +def home(request): + logs = AsteriskCDR.objects.exclude(userfield='request').order_by('-calldate') + logs = pag_mn(request, logs) + title = _('Last calls') + return render(request, 'index.html', { + 'logs': logs, + 'title': title + }) + + +@login_required +@only_admins +def to_abon(request, tel): + abon = Abon.objects.filter(telephone=tel) + abon_count = abon.count() + if abon_count > 1: + messages.warning(request, _('Multiple users with the telephone number')) + elif abon_count == 0: + messages.error(request, _('User with the telephone number not found')) + return redirect('dialapp:home') + abon = abon[0] + if abon.group: + return redirect('abonapp:abon_home', gid=abon.group.pk, uid=abon.pk) + else: + return redirect('abonapp:group_list') + + +@login_required +@only_admins +def vmail(request): + title = _('Voice mail') + cdr = AsteriskCDR.objects.filter(userfield='request').order_by('-calldate') + cdr = pag_mn(request, cdr) + return render(request, 'vmail.html', { + 'title': title, + 'vmessages': cdr + }) diff --git a/djing/__init__.py b/djing/__init__.py index e69de29..f71a4cf 100644 --- a/djing/__init__.py +++ b/djing/__init__.py @@ -0,0 +1,38 @@ +import importlib +from netaddr import mac_unix, mac_eui48 + + +MAC_ADDR_REGEX = r'^([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]{1,2})$' + +class mac_linux(mac_unix): + """MAC format with zero-padded all upper-case hex and colon separated""" + word_fmt = '%x' + + +def default_dialect(eui_obj=None): + return mac_linux + + +def format_mac(eui_obj, dialect): + # Format a EUI instance as a string using the supplied dialect class, allowing custom string classes by + # passing directly or as a string, a la 'module.dialect_cls', where 'module' is the module and 'dialect_cls' + # is the class name of the custom dialect. The dialect must either be defined or imported by the module's __init__.py if + # the module is a package. + if not isinstance(dialect, mac_eui48): + if isinstance(dialect, str): + module, dialect_cls = dialect.split('.') + dialect = getattr(importlib.import_module(module), dialect_cls) + eui_obj.dialect = dialect + return str(eui_obj) + + +from pkg_resources import get_distribution, DistributionNotFound + +try: + _dist = get_distribution('django-macaddress') +except DistributionNotFound: + __version__ = 'Please install this project with setup.py' +else: + __version__ = _dist.version +VERSION = __version__ # synonym +default_app_config = 'abonapp.apps.AbonappConfig' diff --git a/abonapp/fields.py b/djing/fields.py similarity index 100% rename from abonapp/fields.py rename to djing/fields.py diff --git a/abonapp/formfields.py b/djing/formfields.py similarity index 94% rename from abonapp/formfields.py rename to djing/formfields.py index 4e61e3c..bcd961f 100644 --- a/abonapp/formfields.py +++ b/djing/formfields.py @@ -5,10 +5,11 @@ from django.forms.fields import EMPTY_VALUES from django.forms.utils import ValidationError from django.utils.translation import ugettext_lazy as _ from netaddr import EUI, AddrFormatError +from . import MAC_ADDR_REGEX mac_address_validator = RegexValidator( - _lazy_re_compile(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'), + _lazy_re_compile(MAC_ADDR_REGEX), message=_('Enter a valid integer.'), code='invalid', ) diff --git a/djing/settings_example.py b/djing/settings_example.py index 4e8660d..dc09bd3 100644 --- a/djing/settings_example.py +++ b/djing/settings_example.py @@ -37,7 +37,8 @@ INSTALLED_APPS = [ 'taskapp', 'clientsideapp', 'chatbot', - 'django_messages' + 'django_messages', + 'dialing_app' ] MIDDLEWARE_CLASSES = [ @@ -123,7 +124,7 @@ USE_I18N = True USE_L10N = False -USE_TZ = True +USE_TZ = False DEFAULT_FROM_EMAIL = 'nerosketch@gmail.com' @@ -159,3 +160,9 @@ PAGINATION_ITEMS_PER_PAGE=10 pay_SERV_ID = '' pay_SECRET = '' + +DIALING_MEDIA = 'path/to/asterisk_records' + +DHCP_TIMEOUT = 14400 + +DEFAULT_SNMP_PASSWORD = 'public' diff --git a/djing/urls.py b/djing/urls.py index 43bf88b..e057649 100644 --- a/djing/urls.py +++ b/djing/urls.py @@ -17,6 +17,7 @@ urlpatterns = [ url(r'^tasks/', include('taskapp.urls', namespace='taskapp')), url(r'^client/', include('clientsideapp.urls', namespace='client_side')), url(r'^msg/', include('django_messages.urls', namespace='django_messages')), + url(r'^dialing/', include('dialing_app.urls', namespace='dialapp')), url(r'^admin/', admin.site.urls) ] diff --git a/djing/utils/load_dot_from_nodeny.py b/djing/utils/load_dot_from_nodeny.py deleted file mode 100644 index e8310d7..0000000 --- a/djing/utils/load_dot_from_nodeny.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/env python3 -# coding=utf-8 - -import os -import MySQLdb -from json import dumps - - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") - - db = MySQLdb.connect(host="localhost", user="root", passwd="ps", db="nodeny", charset='utf8') - cursor = db.cursor() - - result = dict() - - # выбираем абонентов - sql = r"SELECT location, descr FROM places WHERE location LIKE 'Сад_%'" - cursor.execute(sql) - places = list() - res = cursor.fetchone() - while res: - places.append({ - 'loc': res[0], - 'descr': res[1] - }) - res = cursor.fetchone() - - db.close() - f = open('../../places.json', 'w') - f.write(dumps(places, ensure_ascii=False).encode('utf8')) - f.close() diff --git a/djing/utils/load_from_nodeny.py b/djing/utils/load_from_nodeny.py deleted file mode 100755 index c8aa523..0000000 --- a/djing/utils/load_from_nodeny.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/env python3 -# coding=utf-8 - -import MySQLdb -from json import dumps, loads - - -def param_to_python(st): - st = st.replace('$VAR1 = ', '') - st = st.replace("'", '"') - st = st.replace(" =>", ':') - st = st.replace(";", '') - return loads(st) - - -def load_service(cursor, uid): - sql = "SELECT services.title, services.service_id, services.price, services.description, services.param " \ - "FROM services LEFT JOIN users_services ON " \ - "(users_services.service_id=services.service_id) WHERE users_services.uid=%d" % uid - cursor.execute(sql) - service_line = cursor.fetchone() - if service_line is not None: - service = { - 'title': service_line[0], - 'service_id': service_line[1], - 'price': service_line[2], - 'description': service_line[3], - 'param': param_to_python(service_line[4]) - } - else: - service = None - return service - - -def load_users(cursor, grp_id): - # выбираем абонентов - sql = r"SELECT users.name, users.fio, data0._adr_telefon, dictionary.v AS street, data0._adr_house, data0._birthday, " \ - "users.grp, INET_NTOA(ip_pool.ip) AS ip, users.balance, AES_DECRYPT(users.passwd, 'Vu6saiZa') as decr_passwd, users.id, " \ - "mac_uid.device_mac, mac_uid.device_port, mac_uid.oneconnect " \ - "FROM users " \ - "LEFT JOIN data0 ON (data0.uid = users.id) LEFT JOIN dictionary ON (dictionary.k = data0._adr_street AND dictionary.type = 'street') " \ - "LEFT JOIN mac_uid ON (mac_uid.uid=users.id) " \ - "LEFT JOIN ip_pool ON (ip_pool.uid = users.id) WHERE users.grp = %d" % grp_id - cursor.execute(sql) - users = [{ - 'name': res[0], - 'fio': res[1], - 'tel': res[2], - 'street': str(res[3] or ''), - 'house': str(res[4]), - 'birth': res[5], - 'grp': int(res[6]), - 'ip': str(res[7] or ''), - 'balance': float(res[8]), - 'passw': res[9].decode("utf-8") if res[9] is not None else '', - 'service': load_service(cursor, int(res[10])), - 'opt82': { - 'dev_mac': res[11], - 'dev_port': res[12], - 'oneconnect': res[13] - } - } for res in cursor.fetchall()] - return users - - -def load_groups(cursor): - # выбираем группы - sql = r'SELECT grp_id, grp_name FROM user_grp' - cursor.execute(sql) - groups = list() - for res in cursor.fetchall(): - users = load_users(cursor=cursor, grp_id=int(res[0])) - groups.append({ - 'gid': int(res[0]), - 'gname': res[1], - 'users': users - }) - return groups - - - -if __name__ == "__main__": - db = MySQLdb.connect(host="127.0.0.1", user="user", passwd="password", db="db", charset='utf8') - cursor = db.cursor() - - result = dict() - - result = load_groups(cursor=cursor) - db.close() - f = open('dump.json', 'w') - f.write(dumps(result, ensure_ascii=False)) - f.close() diff --git a/djing/utils/push_snmp_passw.py b/djing/utils/push_snmp_passw.py deleted file mode 100644 index 7d9d181..0000000 --- a/djing/utils/push_snmp_passw.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -import telnetlib -from mydefs import ping -from socket import error -from multiprocessing import Process - - -# Пробуем настроить свичи через telnet на snmp - - -def cmd(ip): - tn = telnetlib.Telnet(ip) - tn.read_until("login: ") - tn.write("\n") - tn.read_until("Password: ") - tn.write("\n") - - tn.write("create snmp community ertNjuWr ReadWrite\n") - tn.write("save\n") - tn.write("save config\n") - tn.write("save config config_id 1\n") - - tn.write("log\n") - print((tn.read_all())) - tn.close() - - -def prc(ip): - try: - if ping(ip): - cmd(ip) - except error: - print(('Error connect to', ip)) - - -if __name__ == '__main__': - proc_list = list() - with open('swips.txt', 'r') as f: - for ln in f: - ip = ln.strip() - p = Process(target=prc, args=(ip,)) - p.start() - proc_list.append(p) - for proc in proc_list: - proc.join() diff --git a/djing/utils/save_dot_from_nodeny.py b/djing/utils/save_dot_from_nodeny.py deleted file mode 100644 index e58f844..0000000 --- a/djing/utils/save_dot_from_nodeny.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/env python3 -# coding=utf-8 - -import os -from json import load -import django - - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") - django.setup() - from devapp.models import Device - - with open('../../places.json', 'r') as f: - dat = load(f) - - for dt in dat: - if dt['descr']: - dt['descr']=dt['descr'].replace('10.15.', '10.115.') - dt['loc']=dt['loc'].encode('utf8') - try: - dev = Device.objects.get(ip_address=dt['descr']) - except Device.DoesNotExist: - dev = Device( - ip_address=dt['descr'] - ) - dev.comment=dt['loc'] - dev.save() - print((dt['descr'], dt['loc'], dev)) diff --git a/djing/utils/save_from_nodeny.py b/djing/utils/save_from_nodeny.py index ab5cc4b..e69de29 100755 --- a/djing/utils/save_from_nodeny.py +++ b/djing/utils/save_from_nodeny.py @@ -1,280 +0,0 @@ -#!/bin/env python3 -# coding=utf-8 - -import os -from json import load -import django -from django.utils import timezone -from django.core.exceptions import ValidationError -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") -django.setup() -from abonapp.models import Abon, AbonGroup, AbonRawPassword, AbonStreet, AbonTariff, Opt82 -from ip_pool.models import IpPoolItem -from tariff_app.models import Tariff - - -class DumpService(object): - price = 0.0 - speedIn = 0.0 - speedOut = 0.0 - - def __init__(self, obj=None): - if obj is None: return - self.title = obj['title'] - self.price = obj['price'] - self.description = obj['description'] - self.speedIn = int(obj['param']['speed_in1']) / 1000000 - self.speedOut = int(obj['param']['speed_out1']) / 1000000 - - @staticmethod - def build_from_db(obj): - self = DumpService() - self.title = obj.title - self.price = obj.amount - self.description = obj.descr - self.speedIn = obj.speedIn - self.speedOut = obj.speedOut - return self - - def __eq__(self, other): - assert isinstance(other, DumpService) - print('DBG:', type(other.price), other.price, type(self.price), self.price) - r = self.price == other.price - r = r and self.speedIn == other.speedIn - r = r and self.speedOut == other.speedOut - return r - - def __ne__(self, other): - return not self.__eq__(other) - - def __str__(self): - return "%s; '%.2f', %f %f" % (self.title, self.price, self.speedIn, self.speedOut) - - -class DumpAbon(object): - - def __init__(self, obj=None): - if obj is None: return - self.name = obj['name'] - self.fio = obj['fio'] - self.tel = obj['tel'] - self.street = obj['street'] - self.house = obj['house'] - self.birth = obj['birth'] - self.grp = obj['grp'] - self.ip = obj['ip'] if obj['ip'] != '' else None - self.balance = obj['balance'] - self.passw = obj['passw'] - if obj['opt82']['dev_mac'] is not None and obj['opt82']['dev_port'] is not None: - self.opt82 = { - 'dev_mac': obj['opt82']['dev_mac'], - 'dev_port': obj['opt82']['dev_port'] - } - - if obj['service'] is not None: - self.service = DumpService(obj['service']) - else: - self.service = None - - @staticmethod - def build_from_django(obj): - assert isinstance(obj, Abon) - self = DumpAbon() - self.name = obj.username - self.fio = obj.fio - self.tel = obj.telephone - self.street = obj.street - self.house = obj.house - self.birth = obj.birth_day - if obj.group is None: - self.grp = None - else: - self.grp = obj.group.pk - if obj.ip_address is None: - self.ip = None - else: - self.ip = obj.ip_address - self.balance = obj.ballance - try: - raw_passw = AbonRawPassword.objects.get(account=obj) - except AbonRawPassword.DoesNotExist: - raw_passw = '' - self.passw = raw_passw - srv = obj.active_tariff() - if srv is not None: - self.service = DumpService.build_from_db(srv) - else: - self.service = None - if obj.opt82 is not None and obj.opt82.mac is not None and obj.opt82.port is not None: - self.opt82 = { - 'dev_mac': obj.opt82.mac, - 'dev_port': obj.opt82.port - } - return self - - def __eq__(self, other): - assert isinstance(other, DumpAbon) - r = self.name == other.name - r = r and self.name == other.name - r = r and self.fio == other.fio - r = r and self.tel == other.tel - r = r and self.street == other.street - r = r and self.house == other.house - r = r and self.birth == other.birth - r = r and self.grp == other.grp - r = r and self.ip == other.ip - r = r and self.balance == other.ballance - return r - - def __ne__(self, other): - return not self.__eq__(other) - - -def add_service_if_not_exist(service): - assert isinstance(service, DumpService) - try: - obj = Tariff.objects.get(speedIn=service.speedIn, speedOut=service.speedOut, amount=service.price) - except Tariff.DoesNotExist: - obj = Tariff.objects.create( - title=service.title, - descr=service.description, - speedIn=service.speedIn, - speedOut=service.speedOut, - amount=service.price, - calc_type='Dp' - ) - return obj - - -def add_raw_password_if_not_exist(acc, raw_passw): - try: - psw = AbonRawPassword.objects.get(account=acc) - #if psw != raw_passw: - # psw.passw_text = raw_passw - # psw.save(update_fields=['passw_text']) - except AbonRawPassword.DoesNotExist: - psw = AbonRawPassword.objects.create(account=acc, passw_text=raw_passw) - return psw - - -def add_opt82_if_not_exist(mac, port): - print(mac, port) - try: - opt82 = Opt82.objects.get(mac=mac, port=port) - except Opt82.DoesNotExist: - opt82 = Opt82.objects.create(mac=mac, port=port) - return opt82 - - -def load_users(obj, group): - if len(obj) < 1: - return - for usr in obj: - # абонент из дампа - dump_abon = DumpAbon(usr) - # абонент из биллинга - print('\t', dump_abon.name, dump_abon.fio, dump_abon.ip) - try: - abon = Abon.objects.get(username=dump_abon.name) - bl_abon = DumpAbon.build_from_django(abon) - if bl_abon != dump_abon: - update_user(abon, dump_abon, group) - except Abon.DoesNotExist: - # добавляем абонента - abon = add_user(dump_abon, group) - if abon is None: - raise Exception("Чё за херня!? Не создался абонент") - - abon_service_from_dump = dump_abon.service - if abon_service_from_dump is None: - continue - abon_service = add_service_if_not_exist(abon_service_from_dump) - try: - AbonTariff.objects.get(abon=abon, tariff=abon_service) - except AbonTariff.DoesNotExist: - calc_obj = abon_service.get_calc_type()(abon_service) - AbonTariff.objects.create( - abon=abon, - tariff=abon_service, - time_start=timezone.now(), - deadline=calc_obj.calc_deadline() - ) - try: - if hasattr(dump_abon, 'opt82'): - abon.opt82 = add_opt82_if_not_exist(dump_abon.opt82['dev_mac'], dump_abon.opt82['dev_port']) - abon.save(update_fields=['opt82']) - except ValidationError as e: - print('\t', e) - - -def add_user(obj, user_group): - assert isinstance(obj, DumpAbon) - street = None - ip = None - try: - if obj.ip is not None: - ip = IpPoolItem.objects.get(ip=obj.ip) - street = AbonStreet.objects.get(name=obj.street, group=user_group) - except IpPoolItem.DoesNotExist: - if obj.ip is not None: - ip = IpPoolItem.objects.create(ip=obj.ip) - except AbonStreet.DoesNotExist: - street = AbonStreet.objects.create(name=obj.street, group=user_group) - - abon = Abon() - abon.username = obj.name - abon.fio = obj.fio - abon.telephone = obj.tel - abon.street = street - abon.house = obj.house - abon.birth_day = obj.birth - abon.group = user_group - abon.ip_address = ip - abon.ballance = obj.balance - abon.set_password(obj.passw) - abon.save() - add_raw_password_if_not_exist(abon, obj.passw) - return abon - - -def update_user(db_abon, obj, user_group): - assert isinstance(obj, DumpAbon) - assert isinstance(db_abon, Abon) - street = None - ip = None - try: - if obj.ip is not None: - ip = IpPoolItem.objects.get(ip=obj.ip) - street = AbonStreet.objects.get(name=obj.street, group=user_group) - except IpPoolItem.DoesNotExist: - if obj.ip is not None: - ip = IpPoolItem.objects.create(ip=obj.ip) - except AbonStreet.DoesNotExist: - street = AbonStreet.objects.create(name=obj.street, group=user_group) - db_abon.fio = obj.fio - db_abon.telephone = obj.tel - db_abon.street = street - db_abon.house = obj.house - #db_abon.birth_day = datetime(obj.birth) - db_abon.group = user_group - db_abon.ip_address = ip - db_abon.ballance = obj.balance - db_abon.set_password(obj.passw) - db_abon.save() - add_raw_password_if_not_exist(db_abon, obj.passw) - - -if __name__ == "__main__": - - with open('dump.json', 'r') as f: - dat = load(f) - - for grp in dat: - try: - abgrp=AbonGroup.objects.get(title=grp['gname']) - except AbonGroup.DoesNotExist: - abgrp = AbonGroup.objects.create( - title=grp['gname'] - ) - print(grp['gname']) - load_users(grp['users'], abgrp) diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..2cd836b --- /dev/null +++ b/docs/install.md @@ -0,0 +1,80 @@ +## Установка(не завершил описание): +Работа предполагается на python3. +Я предпочитаю запускать wsgi сервер на связке uWSGI + Nginx, так что ставить будем соответствующие пакеты. + +На ArchLinux нужые пакеты можно установить так: +``` +# pacman -Sy mariadb-clients python3 python-pip nginx uwsgi +``` +Дальше ставим всё для python через pip: +``` +# pip install git+https://github.com/nerosketch/djing.git +``` + +### Настройка WEB Сервера +Условимся что путь к папке с проектом находится по адресу: + +Конфиг Nginx на моём рабочем сервере выглядит так: + + user http; + worker_processes auto; + pid /run/nginx.pid; + events { + worker_connections 1024; + } + http { + sendfile on; + upstream djing { server unix:///run/uwsgi/djing.sock; } + + server { + listen 80; + server_name <ваш-домен>.com; + root /var/www/djing; + charset utf-8; + + # укажите где лежит ваш раздел с медиа для сайта + location /media { + alias /var/www/djing/media; + } + + # местоположение статики + location /static { + alias /var/www/djing/static; + } + + # тут надо указать путь куда у вас установился Django + путь к статике админки + # путь к Django тут: /usr/lib/python3.5/site-packages/django + # путь к статике соответственно: contrib/admin/static/admin + location /static/admin { + alias /usr/lib/python3.5/site-packages/django/contrib/admin/static/admin; + } + + # на корневом url / реагируем с помощью сокета проекта + # у нас он называется "djing": upstream djing { server ... + location / { + uwsgi_pass djing; + include uwsgi_params; + } + } + } + +Это минимальный конфиг Nginx для работы. Проверте файл /run/uwsgi/djing.sock на доступность пользователю http для чтения. + +Далее настраиваем uWSGI. Мой конфиг для uWSGI в режиме emperor: + + [uwsgi] + uid = http + gid = http + pidfile = /run/uwsgi/uwsgi.pid + emperor = /etc/uwsgi.d + stats = /run/uwsgi/stats.sock + chmod-socket = 660 + emperor-tyrant = true + cap = setgid,setuid + +У меня конфиг лежит по адресу /etc/uwsgi.ini + + +### Настраиваем системные утилиты +Если ваша система работает с поддержкой *systemd* то в каталоге *systemd_units* проекта вы найдёте юниты для systemd. +Скопируйте их в каталог юнитов systemd diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 0000000..4611068 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,7 @@ +Текущие возможности: +1. Может наблюдать за устройствами по snmp +2. Отправлять изменения мгновенно на mikrotik +3. Привязывать на карте к точкам топологии устройства +4. Есть привязка монтажника к группам абонентов +5. Есть менеджер задач на абонентов. Это оператор может выбрать абонента и описать проблему. Система отправит оповещение через telegram ответственному за групу указанного абонента монтажнику с текстом проблемы, адресом и телефоном абонента. +6. Долгие или сложные задачи можно отправлять на очередь исполнения diff --git a/mapapp/templates/maps/map_tooltip.html b/mapapp/templates/maps/map_tooltip.html index b7488b1..a628124 100644 --- a/mapapp/templates/maps/map_tooltip.html +++ b/mapapp/templates/maps/map_tooltip.html @@ -1,7 +1,7 @@

    {{ dot.title }}

    {% for dev in devs %} - + {{ dev.comment }} {% empty %} diff --git a/photo_app/models.py b/photo_app/models.py index edc9748..5f33500 100644 --- a/photo_app/models.py +++ b/photo_app/models.py @@ -27,9 +27,7 @@ class Photo(models.Model): def save(self, *args, **kwargs): if not self.image: return - - super().save() - + super(Photo, self).save() im = Image.open(self.image.path) im.thumbnail((759, 759), Image.ANTIALIAS) @@ -44,8 +42,7 @@ class Photo(models.Model): im.save(fname) os.remove(self.image.path) self.image = "%s.%s" % (hs, ext) - super().save() - + super(Photo, self).save() # class Meta: # unique_together = (('image',),) diff --git a/queue_mngr.py b/queue_mngr.py new file mode 100755 index 0000000..a5b7835 --- /dev/null +++ b/queue_mngr.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import os +import sys +from rq import Connection, Worker +from pid.decorator import pidfile +import django +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") +from agent import NasFailedResult, NasNetworkError +from django.core.exceptions import ValidationError + + +""" + Заустить этот скрипт как демон, он соединяет redis и django +""" +@pidfile() +def main(): + try: + django.setup() + with Connection(): + qs = sys.argv[1:] or ['default'] + w = Worker(qs) + w.work() + except (NasNetworkError, NasFailedResult) as e: + print('NAS:', e) + except (ValidationError, ValueError) as e: + print('ERROR:', e) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index a57bd5f..327d758 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,5 @@ netaddr xmltodict mysqlclient easysnmp +rq +pid diff --git a/searchapp/views.py b/searchapp/views.py index 30bc4c7..4e5a8d4 100644 --- a/searchapp/views.py +++ b/searchapp/views.py @@ -4,14 +4,17 @@ from django.shortcuts import render from django.utils.html import escape from abonapp.models import Abon from mydefs import ip_addr_regex +from django.contrib.auth.decorators import login_required def replace_without_case(orig, old, new): return re.sub(old, new, orig, flags=re.IGNORECASE) +@login_required def home(request): s = request.GET.get('s') + s = s.replace('+', '') if s: if bool(re.match(ip_addr_regex, s)): diff --git a/static/css/custom.css b/static/css/custom.css index 68b70e7..d698291 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -138,6 +138,7 @@ td.btn-group { border-radius: 3px; padding: 6px; width: 79px; + font-size: x-small; } .port .port-img{ background-image: url(/static/img/icon-port-64x64-grey.png); @@ -155,6 +156,9 @@ a.port-img{ text-decoration: none; } +/* 10GBit/s */ +.port.ten{background-color: #a4fff7;} + /* 1GBit/s */ .port.giga{background-color: #caccfb;} @@ -238,4 +242,11 @@ button[data-toggle=offcanvas]{ */ .ct-series-a .ct-area { fill: black; + opacity: 0.3; + fill-opacity: 0.3; +} + +a.navbar-brand { + padding-left: 88px; } + diff --git a/static/js/my.js b/static/js/my.js index d7be95e..26f2a23 100644 --- a/static/js/my.js +++ b/static/js/my.js @@ -27,7 +27,7 @@ $(document).ajaxError(function (ev, jqXHR, ajaxSettings, thrownError) { $.fn.selectajax = function (opt) { var settings = $.extend( { - url : '/api' + url : this.attr('data-dst') }, opt); var selectbtn = this.children('button.selectajax-btn'); @@ -40,7 +40,6 @@ $(document).ajaxError(function (ev, jqXHR, ajaxSettings, thrownError) { var hr = a.attr('href'); var tx = a.text(); selecthid.val(hr.substr(1)); - console.debug(tx); selectbtn.text(tx).removeClass('hidden'); selectinp.addClass('hidden').val(tx); }; @@ -49,7 +48,7 @@ $(document).ajaxError(function (ev, jqXHR, ajaxSettings, thrownError) { $.getJSON(settings.url, {'s': this.value}, function (r) { selectul.empty(); r.forEach(function (o) { - var li = $('
  • ' + o.name + ": " + o.fio + '
  • '); + var li = $('
  • ' + o.text + '
  • '); selectul.append(li); li.on('click', selectajax_click) }); @@ -70,16 +69,55 @@ $(document).ajaxError(function (ev, jqXHR, ajaxSettings, thrownError) { })(jQuery); -$(document).ready(function () { +// AudioPlayer +(function ($) { + $.fn.aplayer = function(){ - // ajax tabs - $('.nav-tabs a').on('show.bs.tab', function (e) { - var ct = $(e.target).attr('href'); - var remoteUrl = $(this).attr('data-tab-remote'); - if (remoteUrl !== '') { - $(ct).load(remoteUrl); - } - }); + var def_play = function(e){ + var audiotag = e.data['audiotag'][0]; + + if(audiotag.readyState == 0){ + $(this).prop('disabled', true); + return; + }else + $(this).prop('disabled', false); + + if(audiotag.paused) + audiotag.play(); + else + audiotag.pause(); + }; + + var def_canplay = function(){ + var els = $(this).parent(); + els.prop('disabled', false).removeClass('disabled'); + els.siblings().prop('disabled', false).removeClass('disabled'); + }; + + var def_on_play = function(){ + $(this).siblings('span.glyphicon').attr('class', 'glyphicon glyphicon-pause'); + }; + + var def_on_pause = function(){ + $(this).siblings('span.glyphicon').attr('class', 'glyphicon glyphicon-play'); + }; + + this.each(function(){ + var i = $(this); + var audiotag = i.children('audio'); + var icon = i.children('span.glyphicon'); + i.on('click', {'audiotag': audiotag}, def_play); + audiotag.on('canplay', def_canplay); + audiotag.on('play', def_on_play); + audiotag.on('pause', def_on_pause); + }); + + }; +})(jQuery); + + + +$(document).ready(function () { // Live html5 image preview if (window.File && window.FileReader && window.FileList && window.Blob) { @@ -114,9 +152,7 @@ $(document).ready(function () { }); - $('div.selectajax').selectajax({ - url: '/abons/api/abon_filter' - }); + $('div.selectajax').selectajax(); $('[data-toggle=offcanvas]').click(function () { $('.row-offcanvas').toggleClass('active'); @@ -147,6 +183,10 @@ $(document).ready(function () { self.html(r.dat); }); return false; - }) + }); + + $('button.player-btn').aplayer(); + + $('[data-toggle="tooltip"]').tooltip({container:'body'}); }); diff --git a/statistics/migrations/0002_delete_statelem.py b/statistics/migrations/0002_delete_statelem.py deleted file mode 100644 index 8fd4fbe..0000000 --- a/statistics/migrations/0002_delete_statelem.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9 on 2017-04-25 13:27 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('statistics', '0001_initial'), - ] - - operations = [ - migrations.DeleteModel( - name='StatElem', - ), - ] diff --git a/statistics/migrations/0002_statcache.py b/statistics/migrations/0002_statcache.py new file mode 100644 index 0000000..391ea4e --- /dev/null +++ b/statistics/migrations/0002_statcache.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-05-31 17:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import mydefs +import statistics.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('statistics', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='StatCache', + fields=[ + ('last_time', statistics.fields.UnixDateTimeField()), + ('ip', mydefs.MyGenericIPAddressField(max_length=8, primary_key=True, protocol='ipv4', serialize=False)), + ('octets', models.PositiveIntegerField(default=0)), + ('packets', models.PositiveIntegerField(default=0)), + ], + options={ + 'db_table': 'flowcache', + }, + ), + migrations.DeleteModel( + name='StatElem', + ), + ] diff --git a/statistics/models.py b/statistics/models.py index 8940d52..c222edc 100644 --- a/statistics/models.py +++ b/statistics/models.py @@ -1,26 +1,51 @@ import math -from datetime import datetime, timedelta -from django.db import models, ProgrammingError -from django.utils import timezone +from datetime import datetime, timedelta, date, time +from django.db import models, connection +from django.utils.timezone import now + from mydefs import MyGenericIPAddressField from .fields import UnixDateTimeField -from mydefs import LogicError + + +def get_dates(): + tables = connection.introspection.table_names() + tables = [t.replace('flowstat_', '') for t in tables if t.startswith('flowstat_')] + return [datetime.strptime(t, '%d%m%Y').date() for t in tables] class StatManager(models.Manager): - def traffic_by_ip(self, ip): - try: - traf = self.order_by('-cur_time').filter(ip=ip, octets__gt=524288)[0] - now = datetime.now() - if traf.cur_time < now - timedelta(minutes=55): - return False, traf - else: - return True, traf - except IndexError: - return False, None - except ProgrammingError as e: - raise LogicError(e) + def chart(self, ip_addr, 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 [lst[i:i+chunk_size] for i in range(0, len(lst), chunk_size)] + + def avarage(elements): + return sum(elements) / len(elements) + + charts_data = self.filter(ip=ip_addr) + charts_times = [cd.cur_time.timestamp()*1000 for cd in charts_data] + charts_octets = [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 = [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 class StatElem(models.Model): @@ -31,6 +56,20 @@ class StatElem(models.Model): objects = StatManager() + # ReadOnly + def save(self, *args, **kwargs): + pass + + # ReadOnly + def delete(self, *args, **kwargs): + pass + + 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): """ @@ -57,10 +96,27 @@ class StatElem(models.Model): abstract = True -def getModel(): +def getModel(want_date=now()): class DynamicStatElem(StatElem): class Meta: - db_table = 'flowstat_%s' % timezone.now().strftime("%d%m%Y") + db_table = 'flowstat_%s' % want_date.strftime("%d%m%Y") abstract = False return DynamicStatElem + + +class StatCache(models.Model): + last_time = UnixDateTimeField() + ip = MyGenericIPAddressField(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' diff --git a/systemd_units/djing_queue.service b/systemd_units/djing_queue.service new file mode 100644 index 0000000..830a585 --- /dev/null +++ b/systemd_units/djing_queue.service @@ -0,0 +1,15 @@ +[Unit] +Description=Djing queue manager + +[Service] +Type=simple +ExecStart=/usr/bin/python3 ./queue_mngr.py > /dev/null +PIDFile=/run/queue_mngr.py.pid +WorkingDirectory=/var/www/djing +TimeoutSec=15 +Restart=always +User=http +Group=http + +[Install] +WantedBy=multi-user.target diff --git a/systemd_units/djing_telebot.service b/systemd_units/djing_telebot.service new file mode 100644 index 0000000..e7676af --- /dev/null +++ b/systemd_units/djing_telebot.service @@ -0,0 +1,15 @@ +[Unit] +Description=Djing telegram bot + +[Service] +Type=simple +ExecStart=/usr/bin/python3 ./telebot.py > /dev/null +PIDFile=/run/telebot.py.pid +WorkingDirectory=/var/www/djing +TimeoutSec=9 +Restart=always +User=http +Group=http + +[Install] +WantedBy=multi-user.target diff --git a/tariff_app/custom_tariffs.py b/tariff_app/custom_tariffs.py index a9888d2..21666b7 100644 --- a/tariff_app/custom_tariffs.py +++ b/tariff_app/custom_tariffs.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from datetime import timedelta, datetime from django.utils import timezone +from django.utils.translation import ugettext as _ from .base_intr import TariffBase from calendar import monthrange @@ -40,11 +41,11 @@ class TariffDefault(TariffBase): last_day = monthrange(nw.year, nw.month)[1] last_month_date = datetime(year=nw.year, month=nw.month, day=last_day, hour=23, minute=59, second=59) - return timezone.make_aware(last_month_date) + return last_month_date @staticmethod def description(): - return 'Базовый расчётный функционал' + return _('Base calculate functionality') class TariffDp(TariffDefault): @@ -67,11 +68,11 @@ class TariffCp(TariffDp): nw = timezone.now() long_long_time = datetime(year=nw.year+10, month=nw.month, day=nw.day, hour=23, minute=59, second=59) - return timezone.make_aware(long_long_time) + return long_long_time @staticmethod def description(): - return 'Для внутреннего пользования' + return _('Private service') # Первый - всегда по умолчанию diff --git a/tariff_app/locale/ru/LC_MESSAGES/django.po b/tariff_app/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000..3dd5f81 --- /dev/null +++ b/tariff_app/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,103 @@ +# 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, 2017. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 13:44+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Dmitry Novikov nerosketch@gmail.com\n" +"Language: \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" + +#: tariff_app/templates/tariff_app/modal_del_warning.html:23 +msgid "Delete" +msgstr "Удалить" + +#: tariff_app/templates/tariff_app/modal_del_warning.html:26 +msgid "Reset" +msgstr "Сбросить" + +msgid "Tarifs" +msgstr "Тарифы" + +msgid "Add" +msgstr "Добавить" + +msgid "Edit" +msgstr "Редактировать" + +msgid "Create" +msgstr "Создать" + +msgid "tariff" +msgstr "тариф" + +msgid "Service title" +msgstr "Название тарифа" + +msgid "Service description" +msgstr "Описание тарифа" + +msgid "Speed In" +msgstr "Входящая скорость" + +msgid "Speed Out" +msgstr "Исходящая скорость" + +msgid "Price" +msgstr "Стоимость" + +msgid "Script" +msgstr "Скрипт" + +msgid "Tech service" +msgstr "Административный тариф" + +msgid "Save" +msgstr "Сохранить" + +msgid "Service list" +msgstr "Список тарифов" + +msgid "Services does not exist yet" +msgstr "Ещё нет созданных тарифов" + +msgid "Base calculate functionality" +msgstr "Базовый расчётный функционал" + +msgid "Private service" +msgstr "Для внутреннего пользования" + +msgid "Service has been saved" +msgstr "Тариф успешно сохранён" + +msgid "Some fields were filled incorrect, please try again" +msgstr "Не все поля заполнены правильно, проверте и попробуйте ещё раз" + +msgid "Service has been deleted" +msgstr "Тарифный план успешно удалён" + +msgid "Not have a confirmations of delete" +msgstr "Нет подтверждения удаления" + +msgid "Delete service" +msgstr "Удалить тарифный план" + +msgid "Attention" +msgstr "Внимание" + +msgid "" +"after delete the tariff, subscribers who use that tariff will be disconnected from it." +msgstr "" +"После того как вы удалите тарифный план то абоненты, подписанные на него, сразу потеряют услугу по этому тарифу. " +"Так что сначала убедитесь что уже никто не пользуется тарифом, и только после этого удалите его." diff --git a/tariff_app/models.py b/tariff_app/models.py index 04aa970..0981537 100644 --- a/tariff_app/models.py +++ b/tariff_app/models.py @@ -5,21 +5,13 @@ from .custom_tariffs import TariffBase, TARIFF_CHOICES from mydefs import MyChoicesAdapter -# Класс похож на адаптер. Предназначен для Django CHOICES чтоб можно было передавать классывместо просто описания поля, -# классы передавать для того чтоб по значению из базы понять какой класс нужно взять для расчёта стоимости тарифа. -class _TariffChoicesAdapter(MyChoicesAdapter): - # На вход принимает кортеж кортежей, вложенный из 2х элементов: кода и класса, как: TARIFF_CHOICES - def __init__(self): - super().__init__(TARIFF_CHOICES) - - class Tariff(models.Model): title = models.CharField(max_length=32) descr = models.CharField(max_length=256) speedIn = models.FloatField(default=0.0) speedOut = models.FloatField(default=0.0) amount = models.FloatField(default=0.0) - calc_type = models.CharField(max_length=2, default=TARIFF_CHOICES[0][0], choices=_TariffChoicesAdapter()) + calc_type = models.CharField(max_length=2, default=TARIFF_CHOICES[0][0], choices=MyChoicesAdapter(TARIFF_CHOICES)) is_admin = models.BooleanField(default=False) # Возвращает потомок класса TariffBase, методы которого дают нужную логику оплаты по тарифу diff --git a/tariff_app/templates/tariff_app/editTarif.html b/tariff_app/templates/tariff_app/editTarif.html index dc86afe..30ebb81 100644 --- a/tariff_app/templates/tariff_app/editTarif.html +++ b/tariff_app/templates/tariff_app/editTarif.html @@ -1,15 +1,15 @@ {% extends request.is_ajax|yesno:'bajax.html,base.html' %} +{% load i18n %} {% block main %} - @@ -18,75 +18,75 @@
    -

    {% if tarif_id == 0 %}Создать{% else %}Редактировать{% endif %} тариф

    +

    {% if tarif_id == 0 %}{% trans 'Create' %}{% else %}{% trans 'Edit' %}{% endif %} {% trans 'tariff' %}

    {% csrf_token %}
    - +
    - + {{ form.title }}{{ form.title.errors }}
    - +
    - + {{ form.descr }}{{ form.descr.errors }}
    - +
    - + {{ form.speedIn }}{{ form.speedIn.errors }}
    - +
    - + {{ form.speedOut }}{{ form.speedOut.errors }}
    - +
    - + {{ form.amount }}{{ form.amount.errors }}
    - +
    - + {{ form.calc_type }}{{ form.calc_type.errors }}
    - +
    @@ -94,4 +94,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tariff_app/templates/tariff_app/modal_del_warning.html b/tariff_app/templates/tariff_app/modal_del_warning.html new file mode 100644 index 0000000..fbe951d --- /dev/null +++ b/tariff_app/templates/tariff_app/modal_del_warning.html @@ -0,0 +1,30 @@ +{% load i18n %} + + {% csrf_token %} + + + {% include 'message_block.html' %} + + + + diff --git a/tariff_app/templates/tariff_app/tarifs.html b/tariff_app/templates/tariff_app/tarifs.html index 668c0d1..0f76a49 100644 --- a/tariff_app/templates/tariff_app/tarifs.html +++ b/tariff_app/templates/tariff_app/tarifs.html @@ -1,51 +1,53 @@ {% extends 'base.html' %} +{% load i18n %} {% block main %} {% include 'message_block.html' %} -

    Список тарифов

    +

    {% trans 'Service list' %}

    - + + {% with can_ch_trf=perms.tariff_app.change_tariff can_del_trf=perms.tariff_app.delete_tariff %} {% for tar in tariflist %} - + {% empty %} - {% endfor %} + {% endwith %} {% if perms.tariff_app.add_tariff %} @@ -79,7 +82,7 @@ @@ -91,4 +94,4 @@ {% include 'toolbar_page.html' with pag=tariflist %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tariff_app/urls.py b/tariff_app/urls.py index 7df7bf5..48e93b9 100644 --- a/tariff_app/urls.py +++ b/tariff_app/urls.py @@ -7,5 +7,5 @@ urlpatterns = [ url(r'^$', views.tarifs, name='home'), url(r'^(?P\d+)$', views.edit_tarif, name='edit'), url(r'^add$', views.edit_tarif, name='add'), - url(r'^del(?P\d+)$', views.del_tarif, name='del') + url(r'^del(?P\d+)$', views.del_tarif, name='del') ] diff --git a/tariff_app/views.py b/tariff_app/views.py index 91150e4..9b8cd0c 100644 --- a/tariff_app/views.py +++ b/tariff_app/views.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.gis.shortcuts import render_to_text +from django.utils.translation import ugettext as _ from django.shortcuts import render, get_object_or_404, redirect from django.contrib import messages from django.core.exceptions import PermissionDenied @@ -45,10 +47,10 @@ def edit_tarif(request, tarif_id=0): frm = forms.TariffForm(request.POST, instance=tarif) if frm.is_valid(): frm.save() - messages.success(request, 'Тариф успешно сохранён') + messages.success(request, _('Service has been saved')) return redirect('tarifs:edit', tarif_id=tarif_id) else: - messages.warning(request, 'Не все поля заполнены правильно, проверте и попробуйте ещё раз') + messages.warning(request, _('Some fields were filled incorrect, please try again')) else: frm = forms.TariffForm(instance=tarif) @@ -60,7 +62,12 @@ def edit_tarif(request, tarif_id=0): @login_required @permission_required('tariff_app.delete_tariff') -def del_tarif(request, id): - tar_id = mydefs.safe_int(id) - get_object_or_404(Tariff, id=tar_id).delete() - return mydefs.res_success(request, 'tarifs:home') +def del_tarif(request, tid): + if request.method == 'POST': + if request.POST.get('confirm') == 'yes': + get_object_or_404(Tariff, id=tid).delete() + messages.success(request, _('Service has been deleted')) + else: + messages.error(request, _('Not have a confirmations of delete')) + return mydefs.res_success(request, 'tarifs:home') + return render_to_text('tariff_app/modal_del_warning.html', {'tid': tid}, request=request) diff --git a/taskapp/handle.py b/taskapp/handle.py index be59da7..e31303f 100644 --- a/taskapp/handle.py +++ b/taskapp/handle.py @@ -2,41 +2,50 @@ from django.utils.translation import ugettext as _ from chatbot.telebot import send_notify from chatbot.models import ChatException +from mydefs import MultipleException class TaskException(Exception): pass -def handle(task, author, recipient, abon_group): - try: - dst_account = recipient - text = _('Task') - # Если сигнал самому себе то молчим - if author == recipient: - return - # Если задача 'На выполнении' то молчим - if task.state == 'C': - return - # Если задача завершена - elif task.state == 'F': - text = _('Task completed') - # Меняем цель назначения на автора, т.к. при завершении - # идёт оповещение автору о выполнении - dst_account = author - if task.abon is not None: - fulltext="%s:\n%s\n" % (text, task.abon.get_full_name()) - else: - fulltext="%s\n" % text - fulltext += _('locality %s.\n') % abon_group.title - if task.abon: - fulltext += _('address %s %s.\ntelephone %s\n') % ( - task.abon.street.name if task.abon.street is not None else '<'+_('not chosen')+'>', - task.abon.house, - task.abon.telephone - ) - fulltext += _('Task type - %s.') % task.get_mode_display() + '\n' - fulltext += task.descr if task.descr else '' - send_notify(fulltext, dst_account) - except ChatException as e: - raise TaskException(e) +def handle(task, author, recipients, abon_group): + errors = [] + for recipient in recipients: + try: + dst_account = recipient + text = _('Task') + # Если сигнал самому себе то молчим + if author == recipient: + return + # Если задача завершена или провалена + elif task.state == 'F' or task.state == 'C': + text = _('Task completed') + if task.abon is not None: + fulltext = "%s:\n%s\n" % (text, task.abon.get_full_name()) + else: + fulltext = "%s\n" % text + fulltext += _('locality %s.\n') % abon_group.title + if task.abon: + fulltext += _('address %s %s.\ntelephone %s\n') % ( + task.abon.street.name if task.abon.street is not None else '<'+_('not chosen')+'>', + task.abon.house, + task.abon.telephone + ) + fulltext += _('Task type - %s.') % task.get_mode_display() + '\n' + fulltext += task.descr if task.descr else '' + + print('task.state:', task.state) + + if task.state == 'F' or task.state == 'C': + # Если задача завершена или провалена то отправляем одно оповещение автору + try: + send_notify(fulltext, author) + except ChatException as e: + raise TaskException(e) + else: + send_notify(fulltext, dst_account) + except ChatException as e: + errors.append(e) + if len(errors) > 0: + raise MultipleException(errors) diff --git a/taskapp/handle.sh b/taskapp/handle.sh index e08b533..8ac380f 100755 --- a/taskapp/handle.sh +++ b/taskapp/handle.sh @@ -44,6 +44,6 @@ fi FULLTEXT="$text: $ABON_FIO. $ABON_ADDR $ABON_TEL. $ABON_GRP. $FAIL_MODE. $DESCR" -echo "TO $RECIPIENT_TEL: $FULLTEXT" >> /tmp/task_sms.log +echo "TO $RECIPIENT_TEL: $FULLTEXT" /usr/bin/gammu-smsd-inject EMS $RECIPIENT_TEL -text "$FULLTEXT" -unicode diff --git a/taskapp/locale/ru/LC_MESSAGES/django.po b/taskapp/locale/ru/LC_MESSAGES/django.po index a80eae4..019c4d3 100644 --- a/taskapp/locale/ru/LC_MESSAGES/django.po +++ b/taskapp/locale/ru/LC_MESSAGES/django.po @@ -31,7 +31,10 @@ msgid "locality %s.\n" msgstr "с. %s\n" #: taskapp/handle.py:33 -msgid "address %s %s.\ntelephone %s\n" +#, python-format +msgid "" +"address %s %s.\n" +"telephone %s\n" msgstr "по адресу %s %s тел. %s.\n" #: taskapp/handle.py:34 taskapp/models.py:26 @@ -39,6 +42,7 @@ msgid "not chosen" msgstr "не выбрано" #: taskapp/handle.py:38 +#, python-format msgid "Task type - %s." msgstr "Тип задачи - %s." @@ -59,8 +63,8 @@ msgid "New" msgstr "Новая" #: taskapp/models.py:21 -msgid "In fulfilling" -msgstr "На выполнении" +msgid "Confused" +msgstr "Провалена" #: taskapp/models.py:22 msgid "Completed" @@ -123,8 +127,8 @@ msgid "Completing tasks" msgstr "Завершение задачи" #: taskapp/models.py:47 -msgid "The task started" -msgstr "Задача начата" +msgid "The task failed" +msgstr "Задача провалена" #: taskapp/models.py:78 msgid "Access to all tasks" @@ -141,11 +145,11 @@ msgid "Tasks" msgstr "Задачи" #: taskapp/templates/taskapp/add_edit_task.html:8 -#: taskapp/templates/taskapp/tasklist.html:48 -#: taskapp/templates/taskapp/tasklist_active.html:46 -#: taskapp/templates/taskapp/tasklist_all.html:54 -#: taskapp/templates/taskapp/tasklist_finish.html:43 -#: taskapp/templates/taskapp/tasklist_own.html:40 +#: taskapp/templates/taskapp/tasklist.html:60 +#: taskapp/templates/taskapp/tasklist_active.html:57 +#: taskapp/templates/taskapp/tasklist_all.html:72 +#: taskapp/templates/taskapp/tasklist_finish.html:54 +#: taskapp/templates/taskapp/tasklist_own.html:53 msgid "Edit" msgstr "Редактировать" @@ -156,17 +160,17 @@ msgstr "Создать" #: taskapp/templates/taskapp/add_edit_task.html:15 #: taskapp/templates/taskapp/footer_btns.html:3 #: taskapp/templates/taskapp/footer_btns.html:4 -#: taskapp/templates/taskapp/tasklist_all.html:75 -#: taskapp/templates/taskapp/tasklist_all.html:76 +#: taskapp/templates/taskapp/tasklist_all.html:93 +#: taskapp/templates/taskapp/tasklist_all.html:94 msgid "Add new task" msgstr "Добавьте новую задачу" #: taskapp/templates/taskapp/add_edit_task.html:27 -#: taskapp/templates/taskapp/tasklist.html:10 -#: taskapp/templates/taskapp/tasklist_active.html:10 -#: taskapp/templates/taskapp/tasklist_all.html:22 -#: taskapp/templates/taskapp/tasklist_finish.html:10 -#: taskapp/templates/taskapp/tasklist_own.html:10 +#: taskapp/templates/taskapp/tasklist.html:13 +#: taskapp/templates/taskapp/tasklist_active.html:13 +#: taskapp/templates/taskapp/tasklist_all.html:25 +#: taskapp/templates/taskapp/tasklist_finish.html:13 +#: taskapp/templates/taskapp/tasklist_own.html:13 #: taskapp/templates/taskapp/view.html:19 msgid "Description" msgstr "Описание" @@ -176,30 +180,24 @@ msgstr "Описание" #: taskapp/templates/taskapp/tasklist_active.html:12 #: taskapp/templates/taskapp/tasklist_all.html:24 #: taskapp/templates/taskapp/tasklist_finish.html:12 -#: taskapp/templates/taskapp/tasklist_own.html:11 +#: taskapp/templates/taskapp/tasklist_own.html:12 msgid "The nature of the damage" msgstr "Характер поломки" #: taskapp/templates/taskapp/add_edit_task.html:43 -#: taskapp/templates/taskapp/tasklist.html:14 -#: taskapp/templates/taskapp/tasklist_active.html:14 -#: taskapp/templates/taskapp/tasklist_all.html:26 -#: taskapp/templates/taskapp/tasklist_finish.html:14 -#: taskapp/templates/taskapp/tasklist_own.html:13 +#: taskapp/templates/taskapp/view.html:29 msgid "A priority" msgstr "Приоритет" #: taskapp/templates/taskapp/add_edit_task.html:51 -#: taskapp/templates/taskapp/tasklist.html:13 -#: taskapp/templates/taskapp/tasklist_active.html:13 -#: taskapp/templates/taskapp/tasklist_all.html:25 -#: taskapp/templates/taskapp/tasklist_finish.html:13 -#: taskapp/templates/taskapp/tasklist_own.html:12 +#: taskapp/templates/taskapp/tasklist_all.html:27 +#: taskapp/templates/taskapp/tasklist_own.html:14 +#: taskapp/templates/taskapp/view.html:34 msgid "Condition" msgstr "Состояние" #: taskapp/templates/taskapp/add_edit_task.html:59 -#: taskapp/templates/taskapp/view.html:30 +#: taskapp/templates/taskapp/view.html:35 msgid "Subscriber" msgstr "Абонент" @@ -207,15 +205,15 @@ msgstr "Абонент" msgid "Reality (the date by which you must complete the task)" msgstr "Актуальность (дата, до которой нужно завершить задачу)" -#: taskapp/templates/taskapp/add_edit_task.html:91 +#: taskapp/templates/taskapp/add_edit_task.html:90 msgid "Attached image" msgstr "Прикреплённое изображение" -#: taskapp/templates/taskapp/add_edit_task.html:99 +#: taskapp/templates/taskapp/add_edit_task.html:98 msgid "Save" msgstr "Сохранить" -#: taskapp/templates/taskapp/add_edit_task.html:102 +#: taskapp/templates/taskapp/add_edit_task.html:101 msgid "Reset" msgstr "Сбросить" @@ -223,52 +221,78 @@ msgstr "Сбросить" msgid "View all tasks" msgstr "Просмотреть все задачи" +#: taskapp/templates/taskapp/tasklist.html:10 +#: taskapp/templates/taskapp/tasklist_active.html:10 +#: taskapp/templates/taskapp/tasklist_all.html:22 +#: taskapp/templates/taskapp/tasklist_finish.html:10 +#: taskapp/templates/taskapp/tasklist_own.html:10 +msgid "Name" +msgstr "ФИО" + #: taskapp/templates/taskapp/tasklist.html:11 #: taskapp/templates/taskapp/tasklist_active.html:11 #: taskapp/templates/taskapp/tasklist_all.html:23 #: taskapp/templates/taskapp/tasklist_finish.html:11 +#: taskapp/templates/taskapp/tasklist_own.html:11 +msgid "Address" +msgstr "Адрес" + +#: taskapp/templates/taskapp/tasklist.html:14 +#: taskapp/templates/taskapp/tasklist_active.html:14 +#: taskapp/templates/taskapp/tasklist_all.html:26 +#: taskapp/templates/taskapp/tasklist_finish.html:14 +#: taskapp/templates/taskapp/view.html:21 msgid "Task author" msgstr "Кто назначил" #: taskapp/templates/taskapp/tasklist.html:15 #: taskapp/templates/taskapp/tasklist_active.html:15 -#: taskapp/templates/taskapp/tasklist_all.html:27 +#: taskapp/templates/taskapp/tasklist_all.html:28 #: taskapp/templates/taskapp/tasklist_finish.html:15 -#: taskapp/templates/taskapp/tasklist_own.html:14 -#: taskapp/templates/taskapp/view.html:27 +#: taskapp/templates/taskapp/tasklist_own.html:15 +#: taskapp/templates/taskapp/view.html:31 msgid "Date of create" msgstr "Дата создания" #: taskapp/templates/taskapp/tasklist.html:16 #: taskapp/templates/taskapp/tasklist_active.html:16 -#: taskapp/templates/taskapp/tasklist_all.html:28 -#: taskapp/templates/taskapp/tasklist_finish.html:16 -#: taskapp/templates/taskapp/tasklist_own.html:15 -#: taskapp/templates/taskapp/view.html:37 -msgid "Attachment" -msgstr "Приложение" - -#: taskapp/templates/taskapp/tasklist.html:17 -#: taskapp/templates/taskapp/tasklist_active.html:17 #: taskapp/templates/taskapp/tasklist_all.html:29 -#: taskapp/templates/taskapp/tasklist_finish.html:17 +#: taskapp/templates/taskapp/tasklist_finish.html:16 #: taskapp/templates/taskapp/tasklist_own.html:16 msgid "Actions" msgstr "Действия" -#: taskapp/templates/taskapp/tasklist.html:41 -msgid "Begin" -msgstr "Начать" +#: taskapp/templates/taskapp/tasklist.html:40 +#: taskapp/templates/taskapp/tasklist_active.html:40 +#: taskapp/templates/taskapp/tasklist_all.html:53 +#: taskapp/templates/taskapp/tasklist_all.html:64 +#: taskapp/templates/taskapp/tasklist_finish.html:40 +#: taskapp/templates/taskapp/tasklist_own.html:40 +#: taskapp/templates/taskapp/view.html:39 +msgid "Not assigned" +msgstr "<Не назначено>" -#: taskapp/templates/taskapp/tasklist.html:44 +#: taskapp/templates/taskapp/tasklist.html:42 #: taskapp/templates/taskapp/tasklist_active.html:42 +#: taskapp/templates/taskapp/tasklist_all.html:55 +#: taskapp/templates/taskapp/tasklist_finish.html:42 +#: taskapp/templates/taskapp/tasklist_own.html:42 taskapp/views.py:151 +msgid "User does not exist" +msgstr "Абонент не найден" + +#: taskapp/templates/taskapp/tasklist.html:53 +msgid "Mark as unsuccessful" +msgstr "Отметить задачу как проваленную" + +#: taskapp/templates/taskapp/tasklist.html:56 +#: taskapp/templates/taskapp/tasklist_active.html:53 msgid "Complete" msgstr "Завершить" -#: taskapp/templates/taskapp/tasklist.html:56 -#: taskapp/templates/taskapp/tasklist_active.html:54 -#: taskapp/templates/taskapp/tasklist_all.html:67 -#: taskapp/templates/taskapp/tasklist_finish.html:56 +#: taskapp/templates/taskapp/tasklist.html:68 +#: taskapp/templates/taskapp/tasklist_active.html:65 +#: taskapp/templates/taskapp/tasklist_all.html:85 +#: taskapp/templates/taskapp/tasklist_finish.html:67 msgid "The list is empty" msgstr "Список пуст" @@ -280,17 +304,17 @@ msgstr "Все задачи" msgid "Records of all the tasks in the system" msgstr "Лог всех задач в системе" -#: taskapp/templates/taskapp/tasklist_all.html:59 -#: taskapp/templates/taskapp/tasklist_own.html:45 +#: taskapp/templates/taskapp/tasklist_all.html:77 +#: taskapp/templates/taskapp/tasklist_own.html:58 msgid "Remind" msgstr "Напомнить" -#: taskapp/templates/taskapp/tasklist_finish.html:48 -#: taskapp/templates/taskapp/tasklist_own.html:50 +#: taskapp/templates/taskapp/tasklist_finish.html:59 +#: taskapp/templates/taskapp/tasklist_own.html:63 msgid "Delete" msgstr "Удалить" -#: taskapp/templates/taskapp/tasklist_own.html:58 +#: taskapp/templates/taskapp/tasklist_own.html:71 msgid "All your tasks has been performed" msgstr "Все ваши задачи выполнены" @@ -298,25 +322,25 @@ msgstr "Все ваши задачи выполнены" msgid "Task description" msgstr "Описание задачи" -#: taskapp/templates/taskapp/view.html:20 +#: taskapp/templates/taskapp/view.html:23 msgid "Implementers" msgstr "Исполнители" -#: taskapp/templates/taskapp/view.html:26 +#: taskapp/templates/taskapp/view.html:30 msgid "The task is valid until" msgstr "Задача действительна до" -#: taskapp/templates/taskapp/view.html:28 +#: taskapp/templates/taskapp/view.html:32 msgid "time left" msgstr "Времени осталось" -#: taskapp/templates/taskapp/view.html:29 +#: taskapp/templates/taskapp/view.html:33 msgid "Task type" msgstr "Тип задачи" -#: taskapp/templates/taskapp/view.html:34 -msgid "Not assigned" -msgstr "<Не назначено>" +#: taskapp/templates/taskapp/view.html:42 +msgid "Attachment" +msgstr "Приложение" #: taskapp/views.py:82 msgid "You cannot delete task that assigned to you" @@ -334,12 +358,8 @@ msgstr "Нужно выбрать абонента" msgid "Error in the form fields" msgstr "Ошибка в полях формы" -#: taskapp/views.py:151 -msgid "User does not exist" -msgstr "Абонент не найден" - -msgid "Active tasks" -msgstr "Активные задачи" +msgid "Failed tasks" +msgstr "Проваленные задачи" msgid "All my tasks" msgstr "Все мои задачи" @@ -358,9 +378,3 @@ msgstr "Задачи, которые необходимо выполнить" msgid "locality %s. Task type - %s. " msgstr "с. %s. Тип задачи - %s. " - -msgid "Name" -msgstr "ФИО" - -msgid "Address" -msgstr "Адрес" diff --git a/taskapp/migrations/0015_auto_20170816_1109.py b/taskapp/migrations/0015_auto_20170816_1109.py new file mode 100644 index 0000000..9755533 --- /dev/null +++ b/taskapp/migrations/0015_auto_20170816_1109.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-08-16 11:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('taskapp', '0014_auto_20170416_1029'), + ] + + operations = [ + migrations.AlterField( + model_name='changelog', + name='act_type', + field=models.CharField(choices=[('e', 'Изменение задачи'), ('c', 'Создание задачи'), ('d', 'Удаление задачи'), ('f', 'Завершение задачи'), ('b', 'Задача провалена')], max_length=1), + ), + migrations.AlterField( + model_name='task', + name='state', + field=models.CharField(choices=[('S', 'Новая'), ('C', 'Провалена'), ('F', 'Выполнена')], default='S', max_length=1), + ), + ] diff --git a/taskapp/models.py b/taskapp/models.py index e484e04..37e30f6 100644 --- a/taskapp/models.py +++ b/taskapp/models.py @@ -6,8 +6,7 @@ from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext as _ from abonapp.models import Abon -from .handle import handle as task_handle, TaskException -from mydefs import MultipleException +from .handle import handle as task_handle TASK_PRIORITIES = ( @@ -18,7 +17,7 @@ TASK_PRIORITIES = ( TASK_STATES = ( ('S', _('New')), - ('C', _('In fulfilling')), + ('C', _('Confused')), ('F', _('Completed')) ) @@ -44,7 +43,7 @@ class ChangeLog(models.Model): ('c', _('Create task')), ('d', _('Delete task')), ('f', _('Completing tasks')), - ('b', _('The task started')) + ('b', _('The task failed')) ) act_type = models.CharField(max_length=1, choices=ACT_CHOICES) when = models.DateTimeField(auto_now_add=True) @@ -89,8 +88,8 @@ class Task(models.Model): ) self.save(update_fields=['state', 'out_date']) - def begin(self, current_user): - self.state = 'C' # Начата + def do_fail(self, current_user): + self.state = 'C' # Провалена ChangeLog.objects.create( task=self, act_type='b', @@ -121,17 +120,10 @@ def task_handler(sender, instance, **kwargs): act_type='e', who=instance.author ) - errors = [] - for recipient in instance.recipients.all(): - try: - task_handle( - instance, instance.author, - recipient, group - ) - except TaskException as e: - errors.append(e) - if len(errors) > 0: - raise MultipleException(errors) + task_handle( + instance, instance.author, + instance.recipients.all(), group + ) #def task_delete(sender, instance, **kwargs): diff --git a/taskapp/templates/taskapp/add_edit_task.html b/taskapp/templates/taskapp/add_edit_task.html index d587491..de14dfa 100644 --- a/taskapp/templates/taskapp/add_edit_task.html +++ b/taskapp/templates/taskapp/add_edit_task.html @@ -58,15 +58,15 @@
    -
    +
    - + {% if selected_abon %} - + {% else %} - + {% endif %} {{ form.abon.errors }}
    diff --git a/taskapp/templates/taskapp/ext.htm b/taskapp/templates/taskapp/ext.htm index c955a40..04412ba 100644 --- a/taskapp/templates/taskapp/ext.htm +++ b/taskapp/templates/taskapp/ext.htm @@ -23,13 +23,6 @@ - {% url 'taskapp:active_tasks' as taskactive %} - - - {% trans 'Active tasks' %} - - - {% url 'taskapp:finished_tasks' as taskfin %} @@ -44,6 +37,13 @@ + {% url 'taskapp:failed_tasks' as taskfailed %} + + + {% trans 'Failed tasks' %} + + + {% url 'taskapp:my_tasks' as taskmy %} diff --git a/taskapp/templates/taskapp/footer_btns.html b/taskapp/templates/taskapp/footer_btns.html index ae9c76e..add4901 100644 --- a/taskapp/templates/taskapp/footer_btns.html +++ b/taskapp/templates/taskapp/footer_btns.html @@ -1,6 +1,6 @@ {% load i18n %} {% if perms.taskapp.add_task %} - + {% trans 'Add new task' %} {% endif %} diff --git a/taskapp/templates/taskapp/tasklist.html b/taskapp/templates/taskapp/tasklist.html index 7788eaa..860b025 100644 --- a/taskapp/templates/taskapp/tasklist.html +++ b/taskapp/templates/taskapp/tasklist.html @@ -33,10 +33,10 @@ {% endif %} {% endif %} -
    + {% if task.abon and task.abon.group %} - + {% else %} @@ -45,19 +45,19 @@ - - + - + {% if task.abon and task.abon.group %} @@ -59,22 +59,24 @@ - + - + {% if task.abon and task.abon.group %} @@ -45,16 +45,16 @@ - - + - - + - + @@ -33,7 +33,7 @@ {% endif %} {% endif %} - + {% if task.abon and task.abon.group %} @@ -45,21 +45,16 @@ - - + - {% empty %} diff --git a/taskapp/templates/taskapp/tasklist_own.html b/taskapp/templates/taskapp/tasklist_own.html index 030d4db..19f4348 100644 --- a/taskapp/templates/taskapp/tasklist_own.html +++ b/taskapp/templates/taskapp/tasklist_own.html @@ -46,7 +46,7 @@ - +
    - Тариф + {% trans 'tariff' %} {% if order_by == 'title' %}{% endif %} - Входящая скорость + {% trans 'Speed In' %} {% if order_by == 'speedIn' %}{% endif %} - Исходящая скорость + {% trans 'Speed Out' %} {% if order_by == 'speedOut' %}{% endif %} - Стоимость + {% trans 'Price' %} {% if order_by == 'amount' %}{% endif %} Название скрипта{% trans 'Script' %} Do
    - {% if perms.tariff_app.change_tariff %} + {% if can_ch_trf %} {{ tar.title }} {% else %} {{ tar.title }} @@ -53,11 +55,11 @@ {{ tar.speedIn }} {{ tar.speedOut }}{{ tar.amount }} руб{{ tar.amount }} {% trans 'currency' %} {{ tar.get_calc_type_display }} - {% if perms.tariff_app.delete_tariff %} - + {% if can_del_trf %} + {% endif %} @@ -65,13 +67,14 @@
    Ещё нет созданных тарифов. + {% trans 'Услуги пока не существуют' %}. {% if perms.tariff_app.add_tariff %} - Создать + {% trans 'Create' %} {% endif %}
    - Добавить + {% trans 'Add' %}
    {{ task.id }}{{ task.pk }}{{ task.abon.get_full_name }}{{ task.abon.get_full_name }} {{ task.abon.group.title }}, {{ task.abon.street|default:_('Not assigned') }} {{ task.abon.house|default:_('Not assigned') }}{% trans 'User does not exist' %}{{ task.get_mode_display }} {{ task.descr }}{{ task.author.username }}{{ task.time_of_create|date:'d N Y H:i:s' }}{{ task.time_of_create|date:'d E H:i' }} - - - - + + + + + {% if perms.taskapp.change_task %} - + {% endif %} diff --git a/taskapp/templates/taskapp/tasklist_all.html b/taskapp/templates/taskapp/tasklist_all.html index 6878c2a..710efc1 100644 --- a/taskapp/templates/taskapp/tasklist_all.html +++ b/taskapp/templates/taskapp/tasklist_all.html @@ -46,7 +46,7 @@ {% endif %} {% endif %} - {{ task.id }}{{ task.pk }}{{ task.abon.get_full_name }}{{ task.get_mode_display }} {{ task.descr }} {% if task.author %} - {{ task.author.username }} + + {{ task.author.username }} + {% else %} {% trans 'Not assigned' %} {% endif %} {{ task.get_state_display }}{{ task.time_of_create|date:'d N Y H:i:s' }}{{ task.time_of_create|date:'d E H:i' }} + {% if perms.taskapp.change_task %} - + {% endif %} {% if perms.taskapp.can_remind %} - + {% endif %} @@ -90,7 +92,7 @@
    {% if perms.taskapp.add_task %} - + {% trans 'Add new task' %} {% endif %} diff --git a/taskapp/templates/taskapp/tasklist_active.html b/taskapp/templates/taskapp/tasklist_failed.html similarity index 84% rename from taskapp/templates/taskapp/tasklist_active.html rename to taskapp/templates/taskapp/tasklist_failed.html index c325927..d93402d 100644 --- a/taskapp/templates/taskapp/tasklist_active.html +++ b/taskapp/templates/taskapp/tasklist_failed.html @@ -33,7 +33,7 @@ {% endif %} {% endif %} - {{ task.id }}{{ task.pk }}{{ task.abon.get_full_name }}{{ task.get_mode_display }} {{ task.descr }}{{ task.author.username }}{{ task.time_of_create|date:'d N Y H:i:s' }}{{ task.time_of_create|date:'d E H:i' }} - + + {% if perms.taskapp.change_task %} - + {% endif %} diff --git a/taskapp/templates/taskapp/tasklist_finish.html b/taskapp/templates/taskapp/tasklist_finish.html index 3dab8a6..70caf9e 100644 --- a/taskapp/templates/taskapp/tasklist_finish.html +++ b/taskapp/templates/taskapp/tasklist_finish.html @@ -8,12 +8,12 @@
    # {% trans 'Name' %}{% trans 'Address' %}{% trans 'Address' %} {% trans 'The nature of the damage' %} {% trans 'Description' %} {% trans 'Task author' %} {% trans 'Date of create' %}{% trans 'Actions' %}#
    {{ task.id }}{{ task.pk }}{{ task.abon.get_full_name }}{{ task.get_mode_display }} {{ task.descr }}{{ task.author.username }}{{ task.time_of_create|date:'d N Y H:i:s' }}{{ task.time_of_create|date:'d E H:i' }} + {% if perms.taskapp.change_task %} - + {% endif %} - {% if perms.taskapp.delete_task %} - - - - {% endif %}
    {{ task.get_mode_display }} {{ task.descr }} {{ task.get_state_display }}{{ task.time_of_create|date:'d N Y H:i:s' }}{{ task.time_of_create|date:'d E H:i' }} {% if perms.taskapp.change_task %} diff --git a/taskapp/templates/taskapp/view.html b/taskapp/templates/taskapp/view.html index 5dfe159..0c0a927 100644 --- a/taskapp/templates/taskapp/view.html +++ b/taskapp/templates/taskapp/view.html @@ -5,7 +5,7 @@ {% include 'message_block.html' %} @@ -18,23 +18,23 @@

    {% trans 'Description' %}: {{ task.descr }}

    - {% trans 'Task author' %}: {{ task.author.username }}
    + {% trans 'Task author' %}: {{ task.author.username }}
    {% trans 'Implementers' %}: {% trans 'A priority' %}: {{ task.get_priority_display }}
    {% trans 'The task is valid until' %} {{ task.out_date|date:'d E Y' }}
    - {% trans 'Date of create' %} {{ task.time_of_create|date:'d E Y' }}
    + {% trans 'Date of create' %} {{ task.time_of_create|date:'d E Y H:i:s' }}
    {% trans 'time left' %} {{ time_diff }}
    {% trans 'Task type' %}: {{ task.get_mode_display }}
    {% 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 %}
    diff --git a/taskapp/urls.py b/taskapp/urls.py index e297e3c..e921f3b 100644 --- a/taskapp/urls.py +++ b/taskapp/urls.py @@ -9,10 +9,10 @@ urlpatterns = [ url(r'^(?P\d+)/edit$', views.task_add_edit, name='edit'), url(r'^(?P\d+)/delete$', views.task_delete, name='delete'), url(r'^(?P\d+)/fin$', views.task_finish, name='finish'), - url(r'^(?P\d+)/begin$', views.task_begin, name='begin'), + url(r'^(?P\d+)/fail$', views.task_failed, name='fail'), url(r'^(?P\d+)/remind', views.remind, name='remind'), url(r'^add$', views.task_add_edit, name='add'), - url(r'^active$', views.active_tasks, name='active_tasks'), + url(r'^failed$', views.failed_tasks, name='failed_tasks'), url(r'^finished$', views.finished_tasks, name='finished_tasks'), url(r'^own$', views.own_tasks, name='own_tasks'), url(r'^my$', views.my_tasks, name='my_tasks'), diff --git a/taskapp/views.py b/taskapp/views.py index ceb764e..d8ec471 100644 --- a/taskapp/views.py +++ b/taskapp/views.py @@ -6,6 +6,8 @@ from django.contrib import messages from abonapp.models import Abon from django.utils.translation import ugettext as _ from datetime import date + +from .handle import TaskException from .models import Task from mydefs import pag_mn, only_admins, safe_int, MultipleException, RuTimedelta from .forms import TaskFrm @@ -23,10 +25,10 @@ def home(request): @login_required @only_admins -def active_tasks(request): - tasks = Task.objects.filter(recipients=request.user, state='C') # На выполнении +def failed_tasks(request): + tasks = Task.objects.filter(recipients=request.user, state='C') # Проваленные tasks = pag_mn(request, tasks) - return render(request, 'taskapp/tasklist_active.html', { + return render(request, 'taskapp/tasklist_failed.html', { 'tasks': tasks }) @@ -152,6 +154,8 @@ def task_add_edit(request, task_id=0): except MultipleException as errs: for err in errs.err_list: messages.add_message(request, messages.constants.ERROR, err) + except TaskException as e: + messages.error(request, e) return render(request, 'taskapp/add_edit_task.html', { 'form': frm, @@ -169,14 +173,16 @@ def task_finish(request, task_id): except MultipleException as errs: for err in errs.err_list: messages.add_message(request, messages.constants.ERROR, err) + except TaskException as e: + messages.error(request, e) return redirect('taskapp:home') @login_required @only_admins -def task_begin(request, task_id): +def task_failed(request, task_id): task = get_object_or_404(Task, id=task_id) - task.begin(request.user) + task.do_fail(request.user) return redirect('taskapp:home') @@ -189,4 +195,6 @@ def remind(request, task_id): except MultipleException as errs: for err in errs.err_list: messages.add_message(request, messages.constants.ERROR, err) + except TaskException as e: + messages.error(request, e) return redirect('taskapp:home') diff --git a/telebot.py b/telebot.py index 8669551..b34d78d 100755 --- a/telebot.py +++ b/telebot.py @@ -1,12 +1,14 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- import os +from pid.decorator import pidfile import django from telepot import DelegatorBot from telepot.delegate import per_chat_id, create_open, pave_event_space -if __name__ == '__main__': +@pidfile() +def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") django.setup() from chatbot.telebot import token, DjingTelebot @@ -16,3 +18,7 @@ if __name__ == '__main__': ), ]) bot.message_loop(run_forever='Listening ...') + + +if __name__ == '__main__': + main() diff --git a/templates/all_base.html b/templates/all_base.html index 7c66202..956692e 100644 --- a/templates/all_base.html +++ b/templates/all_base.html @@ -44,9 +44,6 @@ - {% url 'client_side:home' as client_url %} - Кабинет клиента -