Browse Source

Merge branch 'devel' of https://djing.git.beanstalkapp.com/djing into devel

devel
Dmitry Novikov 7 years ago
parent
commit
2ede9bf4ac
  1. 1
      abonapp/admin.py
  2. 23
      abonapp/locale/ru/LC_MESSAGES/django.po
  3. 34
      abonapp/migrations/0008_auto_20181115_1206.py
  4. 227
      abonapp/models.py
  5. 8
      abonapp/templates/abonapp/buy_tariff.html
  6. 66
      abonapp/templates/abonapp/charts.html
  7. 12
      abonapp/templates/abonapp/editAbon.html
  8. 5
      abonapp/templates/abonapp/ext.htm
  9. 34
      abonapp/templates/abonapp/peoples.html
  10. 55
      abonapp/templates/abonapp/service.html
  11. 3
      abonapp/urls.py
  12. 87
      abonapp/views.py
  13. 27
      chatbot/email_bot.py
  14. 5
      chatbot/send_func.py
  15. 4
      devapp/base_intr.py
  16. 46
      devapp/dev_types.py
  17. 34
      devapp/locale/ru/LC_MESSAGES/django.po
  18. 7
      devapp/models.py
  19. 9
      devapp/tasks.py
  20. 13
      devapp/templates/devapp/custom_dev_page/onu_for_zte.html
  21. 59
      devapp/views.py
  22. 6
      djing/__init__.py
  23. 9
      djing/celery.py
  24. 10
      djing/settings.py
  25. 56
      djing/tasks.py
  26. 2
      djing/urls.py
  27. 15
      docs/tarifs.md
  28. 83
      forward_pay.php
  29. 6
      gw_app/locale/ru/LC_MESSAGES/django.po
  30. 18
      gw_app/migrations/0003_nasmodel_enabled.py
  31. 4
      gw_app/models.py
  32. 25
      gw_app/nas_managers/mod_mikrotik.py
  33. 23
      gw_app/templates/gw_app/nasmodel_list.html
  34. 69
      ip_pool/models.py
  35. 3
      locale/ru/LC_MESSAGES/django.po
  36. 115
      msg_app/models.py
  37. 39
      periodic.py
  38. 4
      requirements.txt
  39. 8
      static/css/custom.css
  40. BIN
      static/img/loading.gif
  41. 3
      static/js/my.js
  42. 0
      statistics/__init__.py
  43. 0
      statistics/admin.py
  44. 5
      statistics/apps.py
  45. 53
      statistics/fields.py
  46. 29
      statistics/migrations/0001_initial.py
  47. 19
      statistics/migrations/0002_auto_20180808_1236.py
  48. 79
      statistics/migrations/0003_auto_20180814_1921.py
  49. 0
      statistics/migrations/__init__.py
  50. 132
      statistics/models.py
  51. 37
      statistics/templates/statistics/index.html
  52. 9
      statistics/urls.py
  53. 9
      statistics/views.py
  54. 6
      systemd_units/djing.service
  55. 14
      systemd_units/djing_celery.service
  56. 2
      systemd_units/djing_dial.service
  57. 4
      systemd_units/djing_telebot.service
  58. 17
      tariff_app/migrations/0003_auto_20181115_1206.py
  59. 9
      taskapp/handle.py
  60. 6
      templates/all_base.html

1
abonapp/admin.py

@ -9,6 +9,5 @@ admin.site.register(models.AbonTariff)
admin.site.register(models.AbonStreet) admin.site.register(models.AbonStreet)
admin.site.register(models.AllTimePayLog) admin.site.register(models.AllTimePayLog)
admin.site.register(models.AbonRawPassword) admin.site.register(models.AbonRawPassword)
admin.site.register(models.AllPayLog)
admin.site.register(models.PassportInfo) admin.site.register(models.PassportInfo)
admin.site.register(models.AdditionalTelephone) admin.site.register(models.AdditionalTelephone)

23
abonapp/locale/ru/LC_MESSAGES/django.po

@ -195,8 +195,8 @@ msgid "Service already activated"
msgstr "Услуга уже подключена" msgstr "Услуга уже подключена"
#: models.py:174 #: models.py:174
msgid "not enough money"
msgstr "Не хватает денег на счету"
msgid "%s not enough money for service %s"
msgstr "%s не имеет достаточно средств для %s"
#: models.py:190 #: models.py:190
msgid "Buy service default log" msgid "Buy service default log"
@ -939,8 +939,8 @@ msgstr "Квитанция на оплату была создана"
#: views.py:419 #: views.py:419
#, python-format #, python-format
msgid "Service '%(service_name)s' has connected via admin"
msgstr "Услуга '%(service_name)s' подключена администратором"
msgid "Service '%(service_name)s' has connected via admin until %(deadline)s"
msgstr "Услуга '%(service_name)s' подключена администратором до %(deadline)s"
#: views.py:429 #: views.py:429
msgid "Tariff has been picked" msgid "Tariff has been picked"
@ -1156,3 +1156,18 @@ msgstr "У пользователя нет ip"
msgid "Ip successfully updated" msgid "Ip successfully updated"
msgstr "IP успешно обновлён" msgstr "IP успешно обновлён"
msgid "IP address conflict"
msgstr "IP адрес уже есть"
msgid "Last connected service"
msgstr "Последняя подключённая услуга"
msgid "Ballance"
msgstr "Балланс"
msgid "Date joined"
msgstr "Дата создания"
msgid "Update ip address"
msgstr "Обновить ip адрес"

34
abonapp/migrations/0008_auto_20181115_1206.py

@ -0,0 +1,34 @@
# Generated by Django 2.1 on 2018-11-15 12:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def fill_last_tariff(apps, _):
Abon = apps.get_model('abonapp', 'Abon')
for abon in Abon.objects.exclude(current_tariff=None):
abon.last_connected_tariff = abon.current_tariff.tariff
abon.save(update_fields=('last_connected_tariff',))
class Migration(migrations.Migration):
dependencies = [
('tariff_app', '0003_auto_20181115_1206'),
('abonapp', '0007_auto_20181101_1545'),
]
operations = [
migrations.AddField(
model_name='abon',
name='last_connected_tariff',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tariff_app.Tariff', verbose_name='Last connected service'),
),
migrations.AlterField(
model_name='abonlog',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
),
migrations.RunPython(fill_last_tariff)
]

227
abonapp/models.py

@ -1,29 +1,30 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from accounts_app.models import UserProfile, MyUserManager, BaseAccount
from bitfield import BitField
from django.conf import settings from django.conf import settings
from django.core import validators from django.core import validators
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models, connection, transaction from django.db import models, connection, transaction
from django.db.models.signals import post_delete, pre_delete, post_init, pre_save
from django.db.models.signals import post_delete, pre_delete, post_init, \
pre_save
from django.dispatch import receiver from django.dispatch import receiver
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, gettext from django.utils.translation import ugettext_lazy as _, gettext
from accounts_app.models import UserProfile, MyUserManager, BaseAccount
from gw_app.nas_managers import SubnetQueue, NasFailedResult, NasNetworkError
from group_app.models import Group
from djing.lib import LogicError from djing.lib import LogicError
from group_app.models import Group
from gw_app.nas_managers import SubnetQueue, NasFailedResult, NasNetworkError
from ip_pool.models import NetworkModel from ip_pool.models import NetworkModel
from tariff_app.models import Tariff, PeriodicPay from tariff_app.models import Tariff, PeriodicPay
from bitfield import BitField
class AbonLog(models.Model): class AbonLog(models.Model):
abon = models.ForeignKey('Abon', on_delete=models.CASCADE) abon = models.ForeignKey('Abon', on_delete=models.CASCADE)
amount = models.FloatField(default=0.0) amount = models.FloatField(default=0.0)
author = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='+', blank=True, null=True)
author = models.ForeignKey(UserProfile, on_delete=models.SET_NULL,
related_name='+', blank=True, null=True)
comment = models.CharField(max_length=128) comment = models.CharField(max_length=128)
date = models.DateTimeField(auto_now_add=True) date = models.DateTimeField(auto_now_add=True)
@ -36,7 +37,11 @@ class AbonLog(models.Model):
class AbonTariff(models.Model): class AbonTariff(models.Model):
tariff = models.ForeignKey(Tariff, on_delete=models.CASCADE, related_name='linkto_tariff')
tariff = models.ForeignKey(
Tariff,
on_delete=models.CASCADE,
related_name='linkto_tariff'
)
time_start = models.DateTimeField(null=True, blank=True, default=None) time_start = models.DateTimeField(null=True, blank=True, default=None)
@ -46,10 +51,6 @@ class AbonTariff(models.Model):
amount = self.tariff.amount amount = self.tariff.amount
return round(amount, 2) return round(amount, 2)
# is used service now, if time start is present than it activated
def is_started(self):
return False if self.time_start is None else True
def __str__(self): def __str__(self):
return "%s: %s" % ( return "%s: %s" % (
self.deadline, self.deadline,
@ -86,19 +87,75 @@ class AbonManager(MyUserManager):
class Abon(BaseAccount): class Abon(BaseAccount):
current_tariff = models.OneToOneField(AbonTariff, null=True, blank=True, on_delete=models.SET_NULL, default=None)
group = models.ForeignKey(Group, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('User group'))
current_tariff = models.OneToOneField(
AbonTariff,
null=True,
blank=True,
on_delete=models.SET_NULL,
default=None
)
group = models.ForeignKey(
Group,
on_delete=models.SET_NULL,
blank=True, null=True,
verbose_name=_('User group')
)
ballance = models.FloatField(default=0.0) ballance = models.FloatField(default=0.0)
ip_address = models.GenericIPAddressField(verbose_name=_('Ip address'), null=True, blank=True)
description = models.TextField(_('Comment'), null=True, blank=True)
street = models.ForeignKey(AbonStreet, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Street'))
house = models.CharField(_('House'), max_length=12, null=True, blank=True)
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(_('Is dynamic ip'), default=False)
nas = models.ForeignKey('gw_app.NASModel', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name=_('Network access server'), default=None)
autoconnect_service = models.BooleanField(_('Automatically connect next service'), default=False)
ip_address = models.GenericIPAddressField(
verbose_name=_('Ip address'),
null=True,
blank=True
)
description = models.TextField(
_('Comment'),
null=True,
blank=True
)
street = models.ForeignKey(
AbonStreet,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('Street')
)
house = models.CharField(
_('House'),
max_length=12,
null=True,
blank=True
)
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(
_('Is dynamic ip'),
default=False
)
nas = models.ForeignKey(
'gw_app.NASModel',
null=True,
blank=True,
on_delete=models.SET_NULL,
verbose_name=_('Network access server'),
default=None
)
autoconnect_service = models.BooleanField(
_('Automatically connect next service'),
default=False
)
last_connected_tariff = models.ForeignKey(
Tariff, verbose_name=_('Last connected service'),
on_delete=models.SET_NULL, null=True, blank=True, default=None
)
MARKER_FLAGS = ( MARKER_FLAGS = (
('icon_donkey', _('Donkey')), ('icon_donkey', _('Donkey')),
@ -144,7 +201,8 @@ class Abon(BaseAccount):
AbonLog.objects.create( AbonLog.objects.create(
abon=self, abon=self,
amount=amount, amount=amount,
author=current_user if isinstance(current_user, UserProfile) else None,
author=current_user if isinstance(current_user,
UserProfile) else None,
comment=comment comment=comment
) )
self.ballance += amount self.ballance += amount
@ -153,10 +211,11 @@ class Abon(BaseAccount):
""" """
Trying to buy a service if enough money. Trying to buy a service if enough money.
:param tariff: instance of tariff_app.models.Tariff. :param tariff: instance of tariff_app.models.Tariff.
:param author: Instance of accounts_app.models.UserProfile. Who connected this
service. May be None if author is a system.
:param author: Instance of accounts_app.models.UserProfile.
Who connected this service. May be None if author is a system.
:param comment: Optional text for logging this pay. :param comment: Optional text for logging this pay.
:param deadline: Instance of datetime.datetime. Date when service is expired.
:param deadline: Instance of datetime.datetime. Date when service is
expired.
:return: Nothing :return: Nothing
""" """
if not isinstance(tariff, Tariff): if not isinstance(tariff, Tariff):
@ -166,7 +225,9 @@ class Abon(BaseAccount):
if tariff.is_admin and author is not None: if tariff.is_admin and author is not None:
if not author.is_staff: if not author.is_staff:
raise LogicError(_('User that is no staff can not buy admin services'))
raise LogicError(
_('User that is no staff can not buy admin services')
)
if self.current_tariff is not None: if self.current_tariff is not None:
if self.current_tariff.tariff == tariff: if self.current_tariff.tariff == tariff:
@ -178,18 +239,26 @@ class Abon(BaseAccount):
# if not enough money # if not enough money
if self.ballance < amount: if self.ballance < amount:
raise LogicError(_('not enough money'))
raise LogicError(_('%s not enough money for service %s') % (
self.username, tariff.title
))
with transaction.atomic(): with transaction.atomic():
new_abtar = AbonTariff.objects.create( new_abtar = AbonTariff.objects.create(
deadline=deadline, tariff=tariff deadline=deadline, tariff=tariff
) )
self.current_tariff = new_abtar self.current_tariff = new_abtar
if self.last_connected_tariff != tariff:
self.last_connected_tariff = tariff
# charge for the service # charge for the service
self.ballance -= amount self.ballance -= amount
self.save(update_fields=('ballance', 'current_tariff'))
self.save(update_fields=(
'ballance',
'current_tariff',
'last_connected_tariff'
))
# make log about it # make log about it
AbonLog.objects.create( AbonLog.objects.create(
@ -217,7 +286,8 @@ class Abon(BaseAccount):
return True return True
return False return False
# is subscriber have access to service, view in tariff_app.custom_tariffs.<TariffBase>.manage_access()
# is subscriber have access to service,
# view in tariff_app.custom_tariffs.<TariffBase>.manage_access()
def is_access(self) -> bool: def is_access(self) -> bool:
if not self.is_active: if not self.is_active:
return False return False
@ -301,7 +371,7 @@ class Abon(BaseAccount):
def enable_service(self, tariff: Tariff, deadline=None, time_start=None): def enable_service(self, tariff: Tariff, deadline=None, time_start=None):
""" """
Makes a services for current user
Makes a services for current user, without money
:param tariff: Instance of service :param tariff: Instance of service
:param deadline: Time when service is expired :param deadline: Time when service is expired
:param time_start: Time when service has started :param time_start: Time when service has started
@ -316,15 +386,32 @@ class Abon(BaseAccount):
time_start=time_start time_start=time_start
) )
self.current_tariff = new_abtar self.current_tariff = new_abtar
self.save(update_fields=('current_tariff',))
self.last_connected_tariff = tariff
self.save(update_fields=('current_tariff', 'last_connected_tariff'))
class PassportInfo(models.Model): class PassportInfo(models.Model):
series = models.CharField(_('Pasport serial'), max_length=4, validators=(validators.integer_validator,))
number = models.CharField(_('Pasport number'), max_length=6, validators=(validators.integer_validator,))
distributor = models.CharField(_('Distributor'), max_length=64)
series = models.CharField(
_('Pasport serial'),
max_length=4,
validators=(validators.integer_validator,)
)
number = models.CharField(
_('Pasport number'),
max_length=6,
validators=(validators.integer_validator,)
)
distributor = models.CharField(
_('Distributor'),
max_length=64
)
date_of_acceptance = models.DateField(_('Date of acceptance')) date_of_acceptance = models.DateField(_('Date of acceptance'))
abon = models.OneToOneField(Abon, on_delete=models.CASCADE, blank=True, null=True)
abon = models.OneToOneField(
Abon,
on_delete=models.CASCADE,
blank=True,
null=True
)
class Meta: class Meta:
db_table = 'passport_info' db_table = 'passport_info'
@ -343,7 +430,13 @@ class InvoiceForPayment(models.Model):
comment = models.CharField(max_length=128) comment = models.CharField(max_length=128)
date_create = models.DateTimeField(auto_now_add=True) date_create = models.DateTimeField(auto_now_add=True)
date_pay = models.DateTimeField(blank=True, null=True) date_pay = models.DateTimeField(blank=True, null=True)
author = models.ForeignKey(UserProfile, related_name='+', on_delete=models.SET_NULL, blank=True, null=True)
author = models.ForeignKey(
UserProfile,
related_name='+',
on_delete=models.SET_NULL,
blank=True,
null=True
)
def __str__(self): def __str__(self):
return "%s -> %.2f" % (self.abon.username, self.amount) return "%s -> %.2f" % (self.abon.username, self.amount)
@ -367,7 +460,9 @@ class AllTimePayLogManager(models.Manager):
def by_days(): def by_days():
cur = connection.cursor() cur = connection.cursor()
cur.execute( cur.execute(
'SELECT SUM(summ) AS alsum, DATE_FORMAT(date_add, "%Y-%m-%d") AS pay_date FROM all_time_pay_log '
'SELECT SUM(summ) AS alsum, '
'DATE_FORMAT(date_add, "%Y-%m-%d") AS pay_date '
'FROM all_time_pay_log '
'GROUP BY DATE_FORMAT(date_add, "%Y-%m-%d")' 'GROUP BY DATE_FORMAT(date_add, "%Y-%m-%d")'
) )
while True: while True:
@ -375,16 +470,35 @@ class AllTimePayLogManager(models.Manager):
if r is None: if r is None:
break break
summ, dat = r summ, dat = r
yield {'summ': summ, 'pay_date': datetime.strptime(dat, '%Y-%m-%d')}
yield {
'summ': summ,
'pay_date': datetime.strptime(dat, '%Y-%m-%d')
}
# Log for pay system "AllTime" # Log for pay system "AllTime"
class AllTimePayLog(models.Model): class AllTimePayLog(models.Model):
abon = models.ForeignKey(Abon, on_delete=models.SET_DEFAULT, blank=True, null=True, default=None)
pay_id = models.CharField(max_length=36, unique=True, primary_key=True)
abon = models.ForeignKey(
Abon,
on_delete=models.SET_DEFAULT,
blank=True,
null=True,
default=None
)
pay_id = models.CharField(
max_length=36,
unique=True,
primary_key=True
)
date_add = models.DateTimeField(auto_now_add=True) date_add = models.DateTimeField(auto_now_add=True)
summ = models.FloatField(default=0.0) summ = models.FloatField(default=0.0)
trade_point = models.CharField(_('Trade point'), max_length=20, default=None, null=True, blank=True)
trade_point = models.CharField(
_('Trade point'),
max_length=20,
default=None,
null=True,
blank=True
)
receipt_num = models.BigIntegerField(_('Receipt number'), default=0) receipt_num = models.BigIntegerField(_('Receipt number'), default=0)
objects = AllTimePayLogManager() objects = AllTimePayLogManager()
@ -424,7 +538,11 @@ class AbonRawPassword(models.Model):
class AdditionalTelephone(models.Model): class AdditionalTelephone(models.Model):
abon = models.ForeignKey(Abon, on_delete=models.CASCADE, related_name='additional_telephones')
abon = models.ForeignKey(
Abon,
on_delete=models.CASCADE,
related_name='additional_telephones'
)
telephone = models.CharField( telephone = models.CharField(
max_length=16, max_length=16,
verbose_name=_('Telephone'), verbose_name=_('Telephone'),
@ -446,10 +564,18 @@ class AdditionalTelephone(models.Model):
class PeriodicPayForId(models.Model): class PeriodicPayForId(models.Model):
periodic_pay = models.ForeignKey(PeriodicPay, on_delete=models.CASCADE, verbose_name=_('Periodic pay'))
periodic_pay = models.ForeignKey(
PeriodicPay,
on_delete=models.CASCADE,
verbose_name=_('Periodic pay')
)
last_pay = models.DateTimeField(_('Last pay time'), blank=True, null=True) last_pay = models.DateTimeField(_('Last pay time'), blank=True, null=True)
next_pay = models.DateTimeField(_('Next time to pay')) next_pay = models.DateTimeField(_('Next time to pay'))
account = models.ForeignKey(Abon, on_delete=models.CASCADE, verbose_name=_('Account'))
account = models.ForeignKey(
Abon,
on_delete=models.CASCADE,
verbose_name=_('Account')
)
def payment_for_service(self, author: UserProfile = None, now=None): def payment_for_service(self, author: UserProfile = None, now=None):
""" """
@ -465,9 +591,10 @@ class PeriodicPayForId(models.Model):
next_pay_date = pp.get_next_time_to_pay(self.last_pay) next_pay_date = pp.get_next_time_to_pay(self.last_pay)
abon = self.account abon = self.account
with transaction.atomic(): with transaction.atomic():
abon.add_ballance(author, -amount, comment=gettext('Charge for "%(service)s"') % {
'service': self.periodic_pay
})
abon.add_ballance(author, -amount, comment=gettext(
'Charge for "%(service)s"') % {
'service': self.periodic_pay
})
abon.save(update_fields=('ballance',)) abon.save(update_fields=('ballance',))
self.last_pay = now self.last_pay = now
self.next_pay = next_pay_date self.next_pay = next_pay_date

8
abonapp/templates/abonapp/buy_tariff.html

@ -33,7 +33,7 @@
<span class="input-group-addon"><span class="glyphicon glyphicon-bullhorn"></span></span> <span class="input-group-addon"><span class="glyphicon glyphicon-bullhorn"></span></span>
<select class="form-control" name="tariff" id="id_tariffs"> <select class="form-control" name="tariff" id="id_tariffs">
{% for trf in tariffs %} {% for trf in tariffs %}
{% if trf.pk == selected_tariff %}
{% if trf == selected_tariff %}
<option value="{{ trf.pk }}" data-deadline='{{ trf.calc_deadline|date:"Y-m-d H:i:s" }}' selected> <option value="{{ trf.pk }}" data-deadline='{{ trf.calc_deadline|date:"Y-m-d H:i:s" }}' selected>
{% else %} {% else %}
<option value="{{ trf.pk }}" data-deadline='{{ trf.calc_deadline|date:"Y-m-d H:i:s" }}'> <option value="{{ trf.pk }}" data-deadline='{{ trf.calc_deadline|date:"Y-m-d H:i:s" }}'>
@ -46,7 +46,11 @@
{% if not abon.active_tariff %} {% if not abon.active_tariff %}
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-calendar"></span></span> <span class="input-group-addon"><span class="glyphicon glyphicon-calendar"></span></span>
<input type="text" class="form-control" name="deadline" id="id_deadline" value="{{ tariffs.0.calc_deadline|date:'Y-m-d H:i:s' }}">
{% if selected_tariff %}
<input type="text" class="form-control" name="deadline" id="id_deadline" value="{{ selected_tariff.calc_deadline|date:'Y-m-d H:i:s' }}">
{% else %}
<input type="text" class="form-control" name="deadline" id="id_deadline" value="{{ tariffs.0.calc_deadline|date:'Y-m-d H:i:s' }}">
{% endif %}
<script type="text/javascript"> <script type="text/javascript">
$(function () { $(function () {
$('#id_deadline').datetimepicker({ $('#id_deadline').datetimepicker({

66
abonapp/templates/abonapp/charts.html

@ -1,66 +0,0 @@
{% extends request.is_ajax|yesno:'nullcont.htm,abonapp/ext.htm' %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% blocktrans with wantdate_d=wantdate|date:'j E Y' %}Graph of use by {{ wantdate_d }}{% endblocktrans %}</h3>
</div>
<div class="panel-body">
{% if charts_data %}
<div id="chrt"></div>
<script type="text/javascript">
$(document).ready(function ($) {
new Chartist.Line('#chrt', {
series: [
{
data: [
{{ charts_data }}
]
}
]
}, {
height: '600px',
showArea: true,
showLine: false,
showPoint: false,
high: {{ high }},
axisX: {
type: Chartist.FixedScaleAxis,
divisor: 12,
labelInterpolationFnc: function (value) {
return moment(value).format('HH:mm:ss');
}
},
lineSmooth: Chartist.Interpolation.cardinal({
tension: 0
})
});
});
</script>
{% else %}
<h2>{% trans 'Static info was Not found' %}</h2>
{% endif %}
<form action="{% url 'abonapp:charts' group.pk abon.username %}" method="get" class="input-group">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-calendar"></span> {% trans 'Show graph by date' %}
</button>
</span>
<input type="text" class="form-control" placeholder="{% trans 'Choose a date' %}" id="date_choose" name="wantdate" value="{{ wantdate|date:'dmY' }}">
</form>
<script type="text/javascript">
$(document).ready(function ($) {
$('#date_choose').datetimepicker({
format: 'DDMMYYYY'
});
});
</script>
</div>
</div>
</div>
</div>
{% endblock %}

12
abonapp/templates/abonapp/editAbon.html

@ -6,7 +6,10 @@
<div class="col-sm-12 col-xs-12 col-md-6"> <div class="col-sm-12 col-xs-12 col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{% trans 'Change subscriber' %}</h3>
<h3 class="panel-title">
{% trans 'Change subscriber' %}
<small>{% trans 'Date joined' %}: {{ abon.birth_day|date:'d E Y' }}</small>
</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
@ -99,6 +102,13 @@
<span class="glyphicon glyphicon-paperclip"></span> {% trans 'Passport information' %} <span class="glyphicon glyphicon-paperclip"></span> {% trans 'Passport information' %}
</a> </a>
{% endif %} {% endif %}
{% if perms.abonapp.delete_abon %}
<a href="{% url 'abonapp:del_abon' group.pk abon.username %}" class="btn btn-danger btn-modal" title="{% trans 'Remove subscriber' %}" data-toggle="tooltip">
<span class="glyphicon glyphicon-remove"></span>
<span>{% trans 'Remove subscriber' %}</span>
</a>
{% endif %}
</div> </div>
</div> </div>

5
abonapp/templates/abonapp/ext.htm

@ -39,11 +39,6 @@
<a href="{{ abtasklog }}">{% trans 'History of tasks' %}</a> <a href="{{ abtasklog }}">{% trans 'History of tasks' %}</a>
</li> </li>
{% url 'abonapp:charts' group.pk abon.username as abtasklog %}
<li{% if abtasklog == request.path %} class="active"{% endif %}>
<a href="{{ abtasklog }}">{% trans 'Charts' %}</a>
</li>
{% url 'abonapp:dials' group.pk abon.username as abdials %} {% url 'abonapp:dials' group.pk abon.username as abdials %}
<li{% if abdials == request.path %} class="active"{% endif %}> <li{% if abdials == request.path %} class="active"{% endif %}>
<a href="{{ abdials }}">{% trans 'Dialing' %}</a> <a href="{{ abdials }}">{% trans 'Dialing' %}</a>

34
abonapp/templates/abonapp/peoples.html

@ -52,7 +52,11 @@
{% if order_by == 'house' %}<span class="glyphicon glyphicon-filter"></span>{% endif %} {% if order_by == 'house' %}<span class="glyphicon glyphicon-filter"></span>{% endif %}
</th> </th>
<th class="col-xs-1">{% trans 'Telephone' %}</th> <th class="col-xs-1">{% trans 'Telephone' %}</th>
<th class="col-xs-2">{% trans 'Service' %}</th>
<th class="col-xs-2">
<a href="{% url 'abonapp:people_list' group.pk %}?{% url_order_by request order_by='current_tariff__tariff' %}">
{% trans 'Service' %}
</a>
</th>
<th class="hidden-xs col-sm-1"> <th class="hidden-xs col-sm-1">
<a href="{% url 'abonapp:people_list' group.pk %}?{% url_order_by request order_by='ballance' %}"> <a href="{% url 'abonapp:people_list' group.pk %}?{% url_order_by request order_by='ballance' %}">
{% trans 'Balance' %} {% trans 'Balance' %}
@ -71,23 +75,25 @@
{% else %} {% else %}
<tr class="danger"> <tr class="danger">
{% endif %} {% endif %}
<td>{% if human.statcache.is_online %}
<span class="glyphicon glyphicon-ok text-success"></span>
{% else %}
<span class="glyphicon glyphicon-remove-sign text-muted"></span>
{% endif %}
<td>
<span class="glyphicon glyphicon-question-sign text-muted"></span>
{# {% if human.statcache.is_online %}#}
{# <span class="glyphicon glyphicon-ok text-success"></span>#}
{# {% else %}#}
{# <span class="glyphicon glyphicon-remove-sign text-muted"></span>#}
{# {% endif %}#}
</td> </td>
<td class="col-xs-1"> <td class="col-xs-1">
<a href="{% url 'abonapp:abon_home' human.group_id human.username %}">{{ human.username }}</a>
<a href="{% url 'abonapp:abon_home' human.group_id human.username %}" title="{% trans 'Date joined' %}: {{ human.birth_day|date:'d E y' }}" data-toggle="tooltip">{{ human.username }}</a>
</td> </td>
<td class="hidden-xs"> <td class="hidden-xs">
{% if human.statcache %}
{% if human.statcache.is_today %}
{{ human.statcache.last_time|date:"H:i" }}
{% else %}
{{ human.statcache.last_time|date:"D H:i" }}
{% endif %}
{% endif %}
{# {% if human.statcache %}#}
{# {% if human.statcache.is_today %}#}
{# {{ human.statcache.last_time|date:"H:i" }}#}
{# {% else %}#}
{# {{ human.statcache.last_time|date:"D H:i" }}#}
{# {% endif %}#}
{# {% endif %}#}
</td> </td>
<td class="col-xs-1">{{ human.ip_address|default_if_none:'&mdash;' }}</td> <td class="col-xs-1">{{ human.ip_address|default_if_none:'&mdash;' }}</td>
<td class="col-xs-2">{{ human.fio|default:'&mdash;' }}</td> <td class="col-xs-2">{{ human.fio|default:'&mdash;' }}</td>

55
abonapp/templates/abonapp/service.html

@ -23,10 +23,18 @@
{% with can_ch_trf=perms.tariff_app.change_tariff %} {% with can_ch_trf=perms.tariff_app.change_tariff %}
{% for service in services %} {% for service in services %}
<tr> <tr>
<td><a href="{% url 'abonapp:pick_tariff' group.pk abon.username %}?selected_tariff={{ service.pk }}"
class="btn btn-sm btn-default" title="{{ service.get_calc_type_display }}" data-toggle="tooltip"{% if abon_tariff %} disabled{% endif %}>
<span class="glyphicon glyphicon-shopping-cart"></span>
</a></td>
<td>
{% if abon_tariff %}
<a href="#" class="btn btn-sm btn-default disabled">
<span class="glyphicon glyphicon-shopping-cart"></span>
</a>
{% else %}
<a href="{% url 'abonapp:pick_tariff' group.pk abon.username %}?selected_tariff={{ service.pk }}"
class="btn btn-sm btn-default" title="{{ service.get_calc_type_display }}" data-toggle="tooltip">
<span class="glyphicon glyphicon-shopping-cart"></span>
</a>
{% endif %}
</td>
<td> <td>
{% if can_ch_trf %} {% if can_ch_trf %}
<a href="{% url 'tarifs:edit' service.pk %}" title="{{ service.descr }}" data-toggle="tooltip"><b>{{ service.title }}</b></a> <a href="{% url 'tarifs:edit' service.pk %}" title="{{ service.descr }}" data-toggle="tooltip"><b>{{ service.title }}</b></a>
@ -62,9 +70,8 @@
<h3 class="panel-title">{% trans "Subscriber's service" %}</h3> <h3 class="panel-title">{% trans "Subscriber's service" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% if abon_tariff %}
<dl class="dl-horizontal">
<dl class="dl-horizontal">
{% if abon_tariff %}
<dt>{% trans 'Service' %}</dt> <dt>{% trans 'Service' %}</dt>
<dd> <dd>
{% if abon_tariff.tariff %} {% if abon_tariff.tariff %}
@ -93,21 +100,29 @@
<dt>{% trans 'Works until' %}</dt> <dt>{% trans 'Works until' %}</dt>
<dd>{{ abon_tariff.deadline|date:"d E Y, l H:i" }}</dd> <dd>{{ abon_tariff.deadline|date:"d E Y, l H:i" }}</dd>
</dl>
{% else %}
<dt>{% trans 'Subscriber has no service' %}</dt>
<dd>
<a href="{% url 'abonapp:pick_tariff' group.pk abon.username %}">
{% trans 'Buy service' %}
</a>
</dd>
{% endif %}
<blockquote>
<p>
{% trans 'Auto continue service.' %}
<input type="checkbox" data-url="{% url 'abonapp:set_auto_continue_service' group.pk abon.username %}" class="autosave" {{ abon.autoconnect_service|yesno:'checked,' }}>
</p>
<p>{{ abon_tariff.tariff.descr }}</p>
</blockquote>
{% if abon.last_connected_tariff %}
<dt>{% trans 'Last connected service' %}</dt>
<dd><a href="{{ abon.last_connected_tariff.get_absolute_url }}">{{ abon.last_connected_tariff.title }}</a></dd>
{% endif %}
{% else %}
{% trans 'Subscriber has no service' %}.
<a href="{% url 'abonapp:pick_tariff' group.pk abon.username %}">
{% trans 'Buy service' %}
</a>
<dt>{% trans 'Auto continue service.' %}</dt>
<dd>
<input type="checkbox" data-url="{% url 'abonapp:set_auto_continue_service' group.pk abon.username %}" class="autosave" {{ abon.autoconnect_service|yesno:'checked,' }}>
<a href="https://github.com/nerosketch/djing/blob/devel/docs/tarifs.md" title="{% trans 'Help' %}" target="_blank" data-toggle="tooltip">?</a>
</dd>
</dl>
{% if abon_tariff.tariff.descr %}
<p>{{ abon_tariff.tariff.descr }}</p>
{% endif %} {% endif %}
{% if abon_tariff %} {% if abon_tariff %}

3
abonapp/urls.py

@ -1,6 +1,6 @@
from django.urls import path, include, re_path from django.urls import path, include, re_path
from . import views
from abonapp import views
app_name = 'abonapp' app_name = 'abonapp'
@ -13,7 +13,6 @@ subscriber_patterns = [
path('addinvoice/', views.add_invoice, name='add_invoice'), path('addinvoice/', views.add_invoice, name='add_invoice'),
path('pick/', views.pick_tariff, name='pick_tariff'), path('pick/', views.pick_tariff, name='pick_tariff'),
path('passport_view/', views.PassportUpdateView.as_view(), name='passport_view'), path('passport_view/', views.PassportUpdateView.as_view(), name='passport_view'),
path('chart/', views.charts, name='charts'),
path('dials/', views.DialsListView.as_view(), name='dials'), path('dials/', views.DialsListView.as_view(), name='dials'),
# path('reset_ip/', views.reset_ip, name='reset_ip'), # path('reset_ip/', views.reset_ip, name='reset_ip'),
path('unsubscribe_service/<int:abon_tariff_id>/', views.unsubscribe_service, name='unsubscribe_service'), path('unsubscribe_service/<int:abon_tariff_id>/', views.unsubscribe_service, name='unsubscribe_service'),

87
abonapp/views.py

@ -1,4 +1,4 @@
from datetime import datetime, date
from datetime import datetime
from typing import Dict, Optional from typing import Dict, Optional
from agent.commands.dhcp import dhcp_commit, dhcp_expiry, dhcp_release from agent.commands.dhcp import dhcp_commit, dhcp_expiry, dhcp_release
@ -11,7 +11,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, \
PermissionRequiredMixin as PermissionRequiredMixin_django, PermissionRequiredMixin PermissionRequiredMixin as PermissionRequiredMixin_django, PermissionRequiredMixin
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError, ProgrammingError, transaction, \ from django.db import IntegrityError, ProgrammingError, transaction, \
OperationalError, DatabaseError
DatabaseError
from django.db.models import Count, Q from django.db.models import Count, Q
from django.http import HttpResponse, HttpResponseBadRequest, \ from django.http import HttpResponse, HttpResponseBadRequest, \
HttpResponseRedirect HttpResponseRedirect
@ -33,7 +33,6 @@ from guardian.shortcuts import get_objects_for_user, assign_perm
from gw_app.models import NASModel from gw_app.models import NASModel
from gw_app.nas_managers import NasFailedResult, NasNetworkError from gw_app.nas_managers import NasFailedResult, NasNetworkError
from ip_pool.models import NetworkModel from ip_pool.models import NetworkModel
from statistics.models import getModel
from tariff_app.models import Tariff from tariff_app.models import Tariff
from taskapp.models import Task from taskapp.models import Task
from xmlview.decorators import xml_view from xmlview.decorators import xml_view
@ -52,9 +51,9 @@ class PeoplesListView(LoginRequiredMixin, OnlyAdminsMixin,
if street_id > 0: if street_id > 0:
peoples_list = peoples_list.filter(street=street_id) peoples_list = peoples_list.filter(street=street_id)
peoples_list = peoples_list.select_related( peoples_list = peoples_list.select_related(
'group', 'street', 'statcache', 'current_tariff'
'group', 'street', 'current_tariff'
).only( ).only(
'group', 'street', 'statcache', 'fio',
'group', 'street', 'fio',
'street', 'house', 'telephone', 'ballance', 'markers', 'street', 'house', 'telephone', 'ballance', 'markers',
'username', 'is_active', 'current_tariff' 'username', 'is_active', 'current_tariff'
) )
@ -440,15 +439,17 @@ def pick_tariff(request, gid: int, uname):
trf = Tariff.objects.get(pk=request.POST.get('tariff')) trf = Tariff.objects.get(pk=request.POST.get('tariff'))
deadline = request.POST.get('deadline') deadline = request.POST.get('deadline')
log_comment = _( log_comment = _(
"Service '%(service_name)s' has connected via admin") % {
'service_name': trf.title
}
if deadline == '' or deadline is None:
abon.pick_tariff(trf, request.user, comment=log_comment)
else:
"Service '%(service_name)s' "
"has connected via admin until %(deadline)s") % {
'service_name': trf.title,
'deadline': deadline
}
if deadline:
deadline = datetime.strptime(deadline, '%Y-%m-%d %H:%M:%S') deadline = datetime.strptime(deadline, '%Y-%m-%d %H:%M:%S')
abon.pick_tariff(trf, request.user, deadline=deadline, abon.pick_tariff(trf, request.user, deadline=deadline,
comment=log_comment) comment=log_comment)
else:
abon.pick_tariff(trf, request.user, comment=log_comment)
r = abon.nas_sync_self() r = abon.nas_sync_self()
if r is None: if r is None:
messages.success(request, _('Tariff has been picked')) messages.success(request, _('Tariff has been picked'))
@ -469,11 +470,15 @@ def pick_tariff(request, gid: int, uname):
except ValueError as e: except ValueError as e:
messages.error(request, "%s: %s" % (_('fix form errors'), e)) messages.error(request, "%s: %s" % (_('fix form errors'), e))
selected_tariff = request.GET.get('selected_tariff')
if selected_tariff:
selected_tariff = get_object_or_404(Tariff, pk=selected_tariff)
return render(request, 'abonapp/buy_tariff.html', { return render(request, 'abonapp/buy_tariff.html', {
'tariffs': tariffs, 'tariffs': tariffs,
'abon': abon, 'abon': abon,
'group': grp, 'group': grp,
'selected_tariff': lib.safe_int(request.GET.get('selected_tariff'))
'selected_tariff': selected_tariff
}) })
@ -594,6 +599,12 @@ class IpUpdateView(LoginAdminPermissionMixin, UpdateView):
return super(IpUpdateView, self).dispatch(request, *args, **kwargs) return super(IpUpdateView, self).dispatch(request, *args, **kwargs)
except lib.LogicError as e: except lib.LogicError as e:
messages.error(request, e) messages.error(request, e)
except IntegrityError as e:
str_text = str(e)
if 'abonent_ip_address_nas_id' in str_text and 'duplicate key value' in str_text:
messages.error(request, _('IP address conflict'))
else:
messages.error(request, e)
return self.render_to_response(self.get_context_data(**kwargs)) return self.render_to_response(self.get_context_data(**kwargs))
def form_valid(self, form): def form_valid(self, form):
@ -679,58 +690,6 @@ def clear_dev(request, gid: int, uname):
return redirect('abonapp:abon_home', gid=gid, uname=uname) return redirect('abonapp:abon_home', gid=gid, uname=uname)
@login_required
@only_admins
@permission_required('group_app.view_group', (Group, 'pk', 'gid'))
def charts(request, gid: int, uname):
high = 100
wandate = request.GET.get('wantdate')
if wandate:
wandate = datetime.strptime(wandate, '%d%m%Y').date()
else:
wandate = date.today()
try:
StatElem = getModel(wandate)
abon = models.Abon.objects.get(username=uname)
if abon.group is None:
abon.group = Group.objects.get(pk=gid)
abon.save(update_fields=('group',))
charts_data = StatElem.objects.chart(
abon,
count_of_parts=30,
want_date=wandate
)
abontariff = abon.active_tariff()
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'))
return redirect('abonapp:people_list', gid)
except Group.DoesNotExist:
messages.error(request, _("Group what you want doesn't exist"))
return redirect('abonapp:group_list')
except (ProgrammingError, OperationalError) as e:
messages.error(request, e)
return redirect('abonapp:charts', gid=gid, uname=uname)
return render(request, 'abonapp/charts.html', {
'group': abon.group,
'abon': abon,
'charts_data': ',\n'.join(
charts_data) if charts_data is not None else None,
'high': high,
'wantdate': wandate
})
@login_required @login_required
@only_admins @only_admins
@permission_required('abonapp.can_ping') @permission_required('abonapp.can_ping')

27
chatbot/email_bot.py

@ -1,27 +0,0 @@
from _socket import gaierror
from smtplib import SMTPException
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
from django.conf import settings
from chatbot.models import ChatException
def send_notify(msg_text, account, tag='none'):
try:
# MessageQueue.objects.push(msg=msg_text, user=account, tag=tag)
target_email = account.email
text_content = strip_tags(msg_text)
msg = EmailMultiAlternatives(
subject=getattr(settings, 'COMPANY_NAME', 'Djing notify'),
body=text_content,
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL'),
to=(target_email,)
)
msg.attach_alternative(msg_text, 'text/html')
msg.send()
except SMTPException as e:
raise ChatException('SMTPException: %s' % e)
except gaierror as e:
raise ChatException('Socket error: %s' % e)

5
chatbot/send_func.py

@ -1,5 +0,0 @@
# send via email
from .email_bot import send_notify
# for Telegram
# from chatbot.telebot import send_notify

4
devapp/base_intr.py

@ -139,4 +139,6 @@ class SNMPBaseWorker(object, metaclass=ABCMeta):
def get_item(self, oid): def get_item(self, oid):
self.start_ses() self.start_ses()
return self.ses.get(oid).value
v = self.ses.get(oid).value
if v != 'NOSUCHINSTANCE':
return v

46
devapp/dev_types.py

@ -161,7 +161,7 @@ class OLTDevice(DevBase, SNMPBaseWorker):
status=True if status == '3' else False, status=True if status == '3' else False,
mac=self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.3.%d' % n), mac=self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.3.%d' % n),
speed=0, speed=0,
signal=int(signal) / 10 if signal != 'NOSUCHINSTANCE' else 0,
signal=int(signal or 0),
snmp_worker=self) snmp_worker=self)
res.append(onu) res.append(onu)
except EasySNMPTimeoutError as e: except EasySNMPTimeoutError as e:
@ -325,7 +325,7 @@ class EltexSwitch(DLinkDevice):
self.get_item('.1.3.6.1.2.1.31.1.1.1.18.%d' % n), self.get_item('.1.3.6.1.2.1.31.1.1.1.18.%d' % n),
self.get_item('.1.3.6.1.2.1.2.2.1.8.%d' % n), self.get_item('.1.3.6.1.2.1.2.2.1.8.%d' % n),
self.get_item('.1.3.6.1.2.1.2.2.1.6.%d' % n), self.get_item('.1.3.6.1.2.1.2.2.1.6.%d' % n),
int(speed) if speed != 'NOSUCHINSTANCE' else 0,
int(speed or 0),
)) ))
return res return res
@ -428,17 +428,30 @@ class ZteOnuDevice(OnuDevice):
try: try:
fiber_num, onu_num = snmp_extra.split('.') fiber_num, onu_num = snmp_extra.split('.')
fiber_num, onu_num = int(fiber_num), int(onu_num) fiber_num, onu_num = int(fiber_num), int(onu_num)
status = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.1.%d.%d.1' % (fiber_num, onu_num))
signal = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.10.%d.%d.1' % (fiber_num, onu_num))
distance = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.18.%d.%d.1' % (fiber_num, onu_num))
name = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.11.2.1.1.%d.%d' % (fiber_num, onu_num))
fiber_addr = '%d.%d' % (fiber_num, onu_num)
status = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.1.%s.1' % fiber_addr)
signal = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.10.%s.1' % fiber_addr)
distance = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.18.%s.1' % fiber_addr)
ip_addr = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.16.1.1.10.%s' % fiber_addr)
vlans = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.15.100.1.1.7.%s.1.1' % fiber_addr)
int_name = self.get_item('.1.3.6.1.4.1.3902.1012.3.28.1.1.3.%s' % fiber_addr)
onu_type = self.get_item('.1.3.6.1.4.1.3902.1012.3.28.1.1.1.%s' % fiber_addr)
sn = self.get_item('.1.3.6.1.4.1.3902.1012.3.28.1.1.5.%s' % fiber_addr)
if sn is not None:
sn = 'ZTEG%s' % ''.join('%.2X' % ord(x) for x in sn[-4:])
return { return {
'status': status, 'status': status,
'signal': conv_signal(safe_int(signal)), 'signal': conv_signal(safe_int(signal)),
'name': name,
'distance': int(distance) / 10 if distance != 'NOSUCHINSTANCE' else 0
'distance': safe_int(distance) / 10,
'ip_addr': ip_addr,
'vlans': vlans,
'serial': sn,
'int_name': int_name,
'onu_type': onu_type
} }
except ValueError:
except IndexError:
pass pass
def get_template_name(self): def get_template_name(self):
@ -514,3 +527,18 @@ class ZteOnuDevice(OnuDevice):
snmp_fiber_num = int(bin_snmp_fiber_number, base=2) snmp_fiber_num = int(bin_snmp_fiber_number, base=2)
device.snmp_extra = "%d.%d" % (snmp_fiber_num, new_onu_port_num) device.snmp_extra = "%d.%d" % (snmp_fiber_num, new_onu_port_num)
device.save(update_fields=('snmp_extra',)) device.save(update_fields=('snmp_extra',))
def get_fiber_str(self):
dev = self.db_instance
if not dev:
return
dat = dev.snmp_extra
if dat and '.' in dat:
snmp_fiber_num, onu_port_num = dat.split('.')
snmp_fiber_num = int(snmp_fiber_num)
bin_snmp_fiber_num = bin(snmp_fiber_num)[2:]
rack_num = int(bin_snmp_fiber_num[5:13], 2)
fiber_num = int(bin_snmp_fiber_num[13:21], 2)
return 'gpon-onu_1/%d/%d:%s' % (
rack_num, fiber_num, onu_port_num
)

34
devapp/locale/ru/LC_MESSAGES/django.po

@ -337,7 +337,6 @@ msgid "ONU error"
msgstr "ONU ошибка" msgstr "ONU ошибка"
#: templates/devapp/custom_dev_page/onu.html:72 #: templates/devapp/custom_dev_page/onu.html:72
#: templates/devapp/custom_dev_page/onu_for_zte.html:75
msgid "Name on OLT" msgid "Name on OLT"
msgstr "Имя на OLT" msgstr "Имя на OLT"
@ -639,20 +638,29 @@ msgstr "Процесс занят другой задачей, подождит
msgid "You have not info in extra_data field, please fill it in JSON" msgid "You have not info in extra_data field, please fill it in JSON"
msgstr "Не заполнено поле 'Техническая информация', обратитесь к администратору" msgstr "Не заполнено поле 'Техническая информация', обратитесь к администратору"
#~ msgid "Device %(device_name)s is up"
#~ msgstr "%(device_name)s в сети"
msgid "Fiber"
msgstr "Интерфейс"
#~ msgid "Device %(device_name)s is down"
#~ msgstr "%(device_name)s не в сети"
msgid "Onu type"
msgstr "Тип onu"
#~ msgid "Device %(device_name)s is unreachable"
#~ msgstr "%(device_name)s недостижим"
msgid "Serial"
msgstr "Серийник"
#~ msgid "Device %(device_name)s getting undefined status code"
#~ msgstr "Устройство %(device_name)s получило не определённый код состояния"
msgid "Device %(device_name)s is up"
msgstr "%(device_name)s в сети"
#~ msgid "View"
#~ msgstr "Посмотреть"
msgid "Device %(device_name)s is down"
msgstr "%(device_name)s не в сети"
#~ msgid "Enter valid JSON"
#~ msgstr "Введите данные в формате JSON"
msgid "Device %(device_name)s is unreachable"
msgstr "%(device_name)s недостижим"
msgid "Device %(device_name)s getting undefined status code"
msgstr "Устройство %(device_name)s получило не определённый код состояния"
msgid "View"
msgstr "Посмотреть"
msgid "Enter valid JSON"
msgstr "Введите данные в формате JSON"

7
devapp/models.py

@ -83,13 +83,6 @@ class Device(models.Model):
def __str__(self): def __str__(self):
return "%s: (%s) %s %s" % (self.comment, self.get_devtype_display(), self.ip_address or '', self.mac_addr or '') return "%s: (%s) %s %s" % (self.comment, self.get_devtype_display(), self.ip_address or '', self.mac_addr or '')
@staticmethod
def update_dhcp():
from .onu_register import onu_register
onu_register(
Device.objects.exclude(group=None).select_related('group').only('mac_addr', 'group__code').iterator()
)
def generate_config_template(self) -> Optional[AnyStr]: def generate_config_template(self) -> Optional[AnyStr]:
mng = self.get_manager_object() mng = self.get_manager_object()
return mng.monitoring_template() return mng.monitoring_template()

9
devapp/onu_register.py → devapp/tasks.py

@ -1,11 +1,14 @@
#!/usr/bin/env python3
from typing import Iterable from typing import Iterable
from subprocess import run from subprocess import run
from celery import shared_task
from devapp.models import Device
def onu_register(devices: Iterable):
@shared_task
def onu_register(device_ids: Iterable[int]):
with open('/etc/dhcp/macs.conf', 'w') as f: with open('/etc/dhcp/macs.conf', 'w') as f:
for dev in devices:
for dev_id in device_ids:
dev = Device.objects.get(pk=dev_id)
if not dev.has_attachable_to_subscriber() or dev.mac_addr is None: if not dev.has_attachable_to_subscriber() or dev.mac_addr is None:
continue continue
group_code = dev.group.code group_code = dev.group.code

13
devapp/templates/devapp/custom_dev_page/onu_for_zte.html

@ -18,6 +18,7 @@
<li class="list-group-item">{% trans 'Ip address' %}: {{ dev.ip_address|default:'-' }}</li> <li class="list-group-item">{% trans 'Ip address' %}: {{ dev.ip_address|default:'-' }}</li>
<li class="list-group-item">{% trans 'Mac' %}: {{ dev.mac_addr }}</li> <li class="list-group-item">{% trans 'Mac' %}: {{ dev.mac_addr }}</li>
<li class="list-group-item">{% trans 'Description' %}: {{ dev.comment }}</li> <li class="list-group-item">{% trans 'Description' %}: {{ dev.comment }}</li>
<li class="list-group-item">{% trans 'Fiber' %}: {{ dev_manager.get_fiber_str }}</li>
{% for da in dev_accs %} {% for da in dev_accs %}
<li class="list-group-item">{% trans 'Attached user' %}: <li class="list-group-item">{% trans 'Attached user' %}:
{% if da.group %} {% if da.group %}
@ -72,9 +73,17 @@
</div> </div>
<div class="media-body"> <div class="media-body">
<b>{% trans 'Name on OLT' %}</b>: {{ onu_details.name }}<br>
<b>{% trans 'Distance(m)' %}</b>: {{ onu_details.distance }}<br>
<b>{% trans 'Distance(m)' %}</b>: {{ onu_details.distance|default:'-' }}<br>
<b>{% trans 'Signal' %}</b>: {{ onu_details.signal }}<br> <b>{% trans 'Signal' %}</b>: {{ onu_details.signal }}<br>
{% if onu_details.ip_addr %}
<b>{% trans 'Ip addr' %}</b>: {{ onu_details.ip_addr }}<br>
{% endif %}
{% if onu_details.vlans %}
<b>{% trans 'VLan list' %}</b>: {{ onu_details.vlans }}<br>
{% endif %}
<b>{% trans 'Serial' %}</b>: {{ onu_details.serial|default:'-' }}<br>
<b>{% trans 'Onu type' %}</b>: {{ onu_details.onu_type|default:'-' }}<br>
<b>{% trans 'Name' %}</b>: {{ onu_details.int_name|default:'-' }}
</div> </div>
</div> </div>

59
devapp/views.py

@ -4,7 +4,6 @@ from ipaddress import ip_address
from abonapp.models import Abon from abonapp.models import Abon
from accounts_app.models import UserProfile from accounts_app.models import UserProfile
from chatbot.models import ChatException from chatbot.models import ChatException
from chatbot.send_func import send_notify
from devapp.base_intr import DeviceImplementationError from devapp.base_intr import DeviceImplementationError
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@ -25,6 +24,7 @@ from djing.lib.decorators import only_admins, hash_auth_view
from djing.lib.mixins import LoginAdminPermissionMixin, LoginAdminMixin from djing.lib.mixins import LoginAdminPermissionMixin, LoginAdminMixin
from djing.lib.tln import ZteOltConsoleError, OnuZteRegisterError, \ from djing.lib.tln import ZteOltConsoleError, OnuZteRegisterError, \
ZteOltLoginFailed ZteOltLoginFailed
from djing.tasks import multicast_email_notify
from easysnmp import EasySNMPTimeoutError, EasySNMPError from easysnmp import EasySNMPTimeoutError, EasySNMPError
from group_app.models import Group from group_app.models import Group
from guardian.decorators import \ from guardian.decorators import \
@ -32,6 +32,7 @@ from guardian.decorators import \
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from .forms import DeviceForm, PortForm, DeviceExtraDataForm from .forms import DeviceForm, PortForm, DeviceExtraDataForm
from .models import Device, Port, DeviceDBException, DeviceMonitoringException from .models import Device, Port, DeviceDBException, DeviceMonitoringException
from .tasks import onu_register
class DevicesListView(LoginAdminPermissionMixin, class DevicesListView(LoginAdminPermissionMixin,
@ -91,7 +92,9 @@ class DeviceDeleteView(LoginAdminPermissionMixin, DeleteView):
self.object.mac_addr or '-', self.object.mac_addr or '-',
self.object.comment or '-' self.object.comment or '-'
)) ))
self.object.update_dhcp()
onu_register.delay(
tuple(dev.pk for dev in Device.objects.exclude(group=None).only('pk').iterator())
)
except (DeviceDBException, PermissionError) as e: except (DeviceDBException, PermissionError) as e:
messages.error(request, e) messages.error(request, e)
messages.success(request, _('Device successfully deleted')) messages.success(request, _('Device successfully deleted'))
@ -141,7 +144,9 @@ class DeviceUpdate(LoginAdminPermissionMixin, UpdateView):
r = super().form_valid(form) r = super().form_valid(form)
# change device info in dhcpd.conf # change device info in dhcpd.conf
try: try:
self.object.update_dhcp()
onu_register.delay(
tuple(dev.pk for dev in Device.objects.exclude(group=None).only('pk').iterator())
)
messages.success(self.request, _('Device info has been saved')) messages.success(self.request, _('Device info has been saved'))
except PermissionError as e: except PermissionError as e:
messages.error(self.request, e) messages.error(self.request, e)
@ -197,13 +202,16 @@ class DeviceCreateView(LoginAdminMixin, PermissionRequiredMixin, CreateView):
r = super().form_valid(form) r = super().form_valid(form)
# change device info in dhcpd.conf # change device info in dhcpd.conf
try: try:
self.request.user.log(self.request.META, 'cdev',
'ip %s, mac: %s, "%s"' % (
self.object.ip_address,
self.object.mac_addr,
self.object.comment
))
self.object.update_dhcp()
self.request.user.log(
self.request.META, 'cdev',
'ip %s, mac: %s, "%s"' % (
self.object.ip_address,
self.object.mac_addr,
self.object.comment
))
onu_register.delay(
tuple(dev.pk for dev in Device.objects.exclude(group=None).only('pk').iterator())
)
messages.success(self.request, _('Device info has been saved')) messages.success(self.request, _('Device info has been saved'))
except PermissionError as e: except PermissionError as e:
messages.error(self.request, e) messages.error(self.request, e)
@ -382,6 +390,8 @@ class EditSinglePort(LoginAdminPermissionMixin, UpdateView):
pk_url_kwarg = 'port_id' pk_url_kwarg = 'port_id'
permission_required = 'devapp.change_port' permission_required = 'devapp.change_port'
template_name = 'devapp/manage_ports/modal_add_edit_port.html' template_name = 'devapp/manage_ports/modal_add_edit_port.html'
model = Port
form_class = PortForm
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
@ -403,7 +413,8 @@ class EditSinglePort(LoginAdminPermissionMixin, UpdateView):
def get_success_url(self): def get_success_url(self):
group_id = self.kwargs.get('group_id') group_id = self.kwargs.get('group_id')
return resolve_url('devapp:view', group_id, self.pk)
device_id = self.kwargs.get('device_id')
return resolve_url('devapp:view', group_id, device_id)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
group_id = self.kwargs.get('group_id') group_id = self.kwargs.get('group_id')
@ -697,24 +708,18 @@ class OnDeviceMonitoringEvent(global_base_views.SecureApiView):
recipients = UserProfile.objects.get_profiles_by_group( recipients = UserProfile.objects.get_profiles_by_group(
device_down.group.pk) device_down.group.pk)
names = list()
for recipient in recipients.iterator():
send_notify(
msg_text=gettext(notify_text) % {
'device_name': "%s(%s) %s" % (
device_down.ip_address,
device_down.mac_addr,
device_down.comment
)
},
account=recipient,
tag='devmon'
multicast_email_notify.delay(msg_text=gettext(notify_text) % {
'device_name': "%s(%s) %s" % (
device_down.ip_address,
device_down.mac_addr,
device_down.comment
) )
names.append(recipient.username)
}, account_ids=(
recipient.pk for recipient in recipients.only('pk').iterator()
))
return { return {
'text': 'notification successfully sent',
'recipients': names
'text': 'notification successfully sent'
} }
except ChatException as e: except ChatException as e:
return { return {

6
djing/__init__.py

@ -1,14 +1,14 @@
import importlib
import os import os
import re import re
import importlib
import typing as t import typing as t
from urllib.parse import unquote from urllib.parse import unquote
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from netaddr import mac_unix, mac_eui48
from django.shortcuts import _get_queryset from django.shortcuts import _get_queryset
from django.utils.http import is_safe_url from django.utils.http import is_safe_url
from netaddr import mac_unix, mac_eui48
from djing.celery import app
MAC_ADDR_REGEX = '^([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]{1,2})$' MAC_ADDR_REGEX = '^([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]{1,2})$'

9
djing/celery.py

@ -0,0 +1,9 @@
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings")
app = Celery('djing', broker='redis://localhost:6379/0')
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

10
djing/settings.py

@ -228,7 +228,9 @@ EMAIL_USE_TLS = getattr(local_settings, 'EMAIL_USE_TLS', True)
SERVER_EMAIL = getattr(local_settings, 'SERVER_EMAIL', EMAIL_HOST_USER) SERVER_EMAIL = getattr(local_settings, 'SERVER_EMAIL', EMAIL_HOST_USER)
# Inactive ip lease time in seconds.
# If lease time more than time of create, and lease is inactive
# then delete it. Used in ip_pool app.
LEASE_LIVE_TIME = 86400
# REDIS related settings
REDIS_HOST = 'localhost'
REDIS_PORT = '6379'
BROKER_URL = 'redis://' + REDIS_HOST + ':' + REDIS_PORT + '/0'
BROKER_TRANSPORT_OPTIONS = {'visibility_timeout': 3600}
CELERY_RESULT_BACKEND = 'redis://' + REDIS_HOST + ':' + REDIS_PORT + '/0'

56
djing/tasks.py

@ -0,0 +1,56 @@
import logging
from _socket import gaierror
from smtplib import SMTPException
from typing import Iterable
from accounts_app.models import UserProfile
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
from celery import shared_task
@shared_task
def send_email_notify(msg_text: str, account_id: int):
try:
account = UserProfile.objects.get(pk=account_id)
target_email = account.email
text_content = strip_tags(msg_text)
msg = EmailMultiAlternatives(
subject=getattr(settings, 'COMPANY_NAME', 'Djing notify'),
body=text_content,
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL'),
to=(target_email,)
)
msg.attach_alternative(msg_text, 'text/html')
msg.send()
except SMTPException as e:
logging.error('SMTPException: %s' % e)
except gaierror as e:
logging.error('Socket error: %s' % e)
except UserProfile.DoesNotExist:
logging.error('UserProfile with pk=%d not found' % account_id)
@shared_task
def multicast_email_notify(msg_text: str, account_ids: Iterable):
text_content = strip_tags(msg_text)
for acc_id in account_ids:
try:
account = UserProfile.objects.get(pk=acc_id)
target_email = account.email
msg = EmailMultiAlternatives(
subject=getattr(settings, 'COMPANY_NAME', 'Djing notify'),
body=text_content,
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL'),
to=(target_email,)
)
msg.attach_alternative(msg_text, 'text/html')
msg.send()
except SMTPException as e:
logging.error('SMTPException: %s' % e)
except gaierror as e:
logging.error('Socket error: %s' % e)
except UserProfile.DoesNotExist:
logging.error('UserProfile with pk=%d not found' % acc_id)

2
djing/urls.py

@ -11,7 +11,7 @@ urlpatterns = [
path('search/', include('searchapp.urls', namespace='searchapp')), path('search/', include('searchapp.urls', namespace='searchapp')),
path('dev/', include('devapp.urls', namespace='devapp')), path('dev/', include('devapp.urls', namespace='devapp')),
path('map/', include('mapapp.urls', namespace='mapapp')), path('map/', include('mapapp.urls', namespace='mapapp')),
path('statistic/', include('statistics.urls', namespace='statistics')),
# path('statistic/', include('statistics.urls', namespace='statistics')),
path('tasks/', include('taskapp.urls', namespace='taskapp')), path('tasks/', include('taskapp.urls', namespace='taskapp')),
path('client/', include('clientsideapp.urls', namespace='client_side')), path('client/', include('clientsideapp.urls', namespace='client_side')),
path('msg/', include('msg_app.urls', namespace='msg_app')), path('msg/', include('msg_app.urls', namespace='msg_app')),

15
docs/tarifs.md

@ -0,0 +1,15 @@
## Услуги и тарифы
### Автопродление услуги
Кнопка **автопродление услуги** предназначена для того чтоб абоненту не
приходилось каждый месяц заходить в личный кабинет и подключать себе услугу.
Кнопка моделирует поведение когда абонент пополнил счёт, и сразу начал
пользоваться интернетом. Без этого абоненту нужно покупать услугу для доступа в
интернет каждый раз когда старая услуга закончит своё действие.
Нас транице абонента галочка находится в блоке *Текущая услуга абонента*. Так
же она доступна на странице абонента в личном кабинете.
Для того чтоб кнопка стала активной достаточно выставить галочку, и не нужно
сохраняться, состояние кнопки сразу же сохранится навсегда в системе.

83
forward_pay.php

@ -1,83 +0,0 @@
#!/usr/bin/env php
<?php
define("API_AUTH_SECRET", "secret");
define("SERVER_DOMAIN", 'http://localhost:8000');
define('N', '
');
function calc_hash($str)
{
$hash = bin2hex(mhash(MHASH_SHA256,$str));
return $hash;
}
function make_sign($data)
{
asort($data, SORT_STRING);
array_push($data, API_AUTH_SECRET);
$str_to_hash = join('_', $data);
return calc_hash($str_to_hash);
}
function send_to($data)
{
$sign = make_sign($data);
$data['sign'] = $sign;
$url_params = http_build_query($data);
$r = file_get_contents(SERVER_DOMAIN."/abons/api/duplicate_pay/?".$url_params);
return $r;
}
function forward_pay_request($act, $pay_account, $service_id, $trade_point, $receipt_num, $pay_id, $pay_amount)
{
require('./users_uname_pk_pairs.php');
// $user_id_pairs
if($act == 1)
{
$pay = [
"ACT" => 1,
"PAY_ACCOUNT" => $user_id_pairs[$pay_account]
];
return send_to($pay);
}else if($act == 4)
{
$pay = [
"ACT" => 4,
"PAY_ACCOUNT" => $user_id_pairs[$pay_account],
"TRADE_POINT" => $trade_point,
"RECEIPT_NUM" => $receipt_num,
"PAY_ID" => $pay_id,
"PAY_AMOUNT" => $pay_amount,
"SERVICE_ID" => $service_id
];
return send_to($pay);
}else if($act == 7)
{
$pay = [
"ACT" => 7,
"PAY_ID" => $pay_id,
"SERVICE_ID" => $service_id
];
return send_to($pay);
}
}
# Request
echo forward_pay_request(1, '1234', null, null, null, null, null);
# Add cash
echo forward_pay_request('4', '1234', 'mypaysrv', '3432', '289473', '897879-989-68669', '1');
# check cash
echo forward_pay_request(7, null, 'mypaysrv', null, null, '897879-989-68669', null);
?>

6
gw_app/locale/ru/LC_MESSAGES/django.po

@ -157,3 +157,9 @@ msgstr "Убедитесь что значение меньше или равн
msgid "%(model_name)s with this %(field_label)s already exists." msgid "%(model_name)s with this %(field_label)s already exists."
msgstr "%(model_name)s с таким %(field_label)s уже существует." msgstr "%(model_name)s с таким %(field_label)s уже существует."
msgid "Enabled"
msgstr "Включен"
msgid "Gateway disabled"
msgstr "Шлюз выключен"

18
gw_app/migrations/0003_nasmodel_enabled.py

@ -0,0 +1,18 @@
# Generated by Django 2.1 on 2018-11-15 13:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gw_app', '0002_auto_20181101_1545'),
]
operations = [
migrations.AddField(
model_name='nasmodel',
name='enabled',
field=models.BooleanField(default=True, verbose_name='Enabled'),
),
]

4
gw_app/models.py

@ -16,6 +16,7 @@ class NASModel(models.Model):
auth_passw = models.CharField(_('Auth password'), max_length=127) auth_passw = models.CharField(_('Auth password'), max_length=127)
nas_type = models.CharField(_('Type'), max_length=4, choices=MyChoicesAdapter(NAS_TYPES), default=NAS_TYPES[0][0]) nas_type = models.CharField(_('Type'), max_length=4, choices=MyChoicesAdapter(NAS_TYPES), default=NAS_TYPES[0][0])
default = models.BooleanField(_('Is default'), default=False) default = models.BooleanField(_('Is default'), default=False)
enabled = models.BooleanField(_('Enabled'), default=True)
def get_nas_manager_klass(self): def get_nas_manager_klass(self):
try: try:
@ -32,7 +33,8 @@ class NASModel(models.Model):
login=self.auth_login, login=self.auth_login,
password=self.auth_passw, password=self.auth_passw,
ip=self.ip_address, ip=self.ip_address,
port=int(self.ip_port)
port=int(self.ip_port),
enabled=bool(self.enabled)
) )
setattr(self, '_nas_mngr', o) setattr(self, '_nas_mngr', o)
return o return o

25
gw_app/nas_managers/mod_mikrotik.py

@ -170,14 +170,17 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
(ABCMeta, LazyInitMetaclass), {})): (ABCMeta, LazyInitMetaclass), {})):
description = _('Mikrotik NAS') description = _('Mikrotik NAS')
def __init__(self, login: str, password: str, ip: str, port: int, *args,
**kwargs):
def __init__(self, login: str, password: str, ip: str, port: int,
enabled: bool, *args, **kwargs):
if not enabled:
raise core.NasFailedResult(_('Gateway disabled'))
try: try:
core.BaseTransmitter.__init__(self,
login=login, password=password,
ip=ip,
port=port, *args, **kwargs
)
core.BaseTransmitter.__init__(
self, login=login,
password=password,
ip=ip, port=port,
*args, **kwargs
)
ApiRos.__init__(self, ip, port) ApiRos.__init__(self, ip, port)
self.login(username=login, pwd=password) self.login(username=login, pwd=password)
except ConnectionRefusedError: except ConnectionRefusedError:
@ -268,9 +271,9 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
# FIXME: тут в разных микротиках или =target-addresses или =target # FIXME: тут в разных микротиках или =target-addresses или =target
'=target=%s' % queue.network, '=target=%s' % queue.network,
'=max-limit=%.3fM/%.3fM' % queue.max_limit, '=max-limit=%.3fM/%.3fM' % queue.max_limit,
'=queue=Djing_pcq/Djing_pcq',
'=burst-time=1/1',
'=total-queue=Djing_pcq'
'=queue=Djing_pcq_up/Djing_pcq_down',
'=burst-time=1/5',
#'=total-queue=Djing_pcq_down'
)) ))
def remove_queue(self, queue: i_structs.SubnetQueue) -> None: def remove_queue(self, queue: i_structs.SubnetQueue) -> None:
@ -304,7 +307,7 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
# FIXME: тут в разных версиях прошивки микротика # FIXME: тут в разных версиях прошивки микротика
# или =target-addresses или =target # или =target-addresses или =target
'=target=%s' % queue.network, '=target=%s' % queue.network,
'=queue=Djing_pcq/Djing_pcq',
'=queue=Djing_pcq_up/Djing_pcq_down',
'=burst-time=1/1' '=burst-time=1/1'
] ]
if queue.queue_id: if queue.queue_id:

23
gw_app/templates/gw_app/nasmodel_list.html

@ -18,11 +18,10 @@
<div class="col-sm-6"> <div class="col-sm-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">dasdasd</h3>
<h3 class="panel-title">{{ nas.title }}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>{% trans 'Title' %}</dt> <dd>{{ nas.title }}</dd>
<dt>{% trans 'Ip address' %}</dt> <dd>{{ nas.ip_address }}</dd> <dt>{% trans 'Ip address' %}</dt> <dd>{{ nas.ip_address }}</dd>
<dt>{% trans 'Port' %}</dt> <dd>{{ nas.ip_port }}</dd> <dt>{% trans 'Port' %}</dt> <dd>{{ nas.ip_port }}</dd>
<dt>{% trans 'Auth login' %}</dt> <dd>{{ nas.auth_login }}</dd> <dt>{% trans 'Auth login' %}</dt> <dd>{{ nas.auth_login }}</dd>
@ -32,16 +31,22 @@
<dd> <dd>
<input type="checkbox" {{ nas.default|yesno:'checked,' }}> <input type="checkbox" {{ nas.default|yesno:'checked,' }}>
</dd> </dd>
<dt>{% trans 'Enabled' %}</dt>
<dd>
<input type="checkbox" {{ nas.enabled|yesno:'checked,' }}>
</dd>
</dl> </dl>
</div> </div>
<div class="panel-footer">
<div class="btn-group btn-group-sm">
<a href="{% url 'gw_app:edit' nas.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-eye-open"></span>
<span class="hidden-md">{% trans 'View' %}</span>
</a>
{% if perms.gw_app.change_nasmodel %}
<div class="panel-footer">
<div class="btn-group btn-group-sm">
<a href="{% url 'gw_app:edit' nas.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-edit"></span>
<span class="hidden-md">{% trans 'Edit' %}</span>
</a>
</div>
</div> </div>
</div>
{% endif %}
</div> </div>
</div> </div>
{% empty %} {% empty %}

69
ip_pool/models.py

@ -1,13 +1,10 @@
from datetime import timedelta
from ipaddress import ip_network, ip_address from ipaddress import ip_network, ip_address
from typing import Optional, Generator from typing import Optional, Generator
from django.conf import settings
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djing.fields import MACAddressField from djing.fields import MACAddressField
@ -21,7 +18,8 @@ class NetworkModel(models.Model):
network = GenericIpAddressWithPrefix( network = GenericIpAddressWithPrefix(
verbose_name=_('IP network'), verbose_name=_('IP network'),
help_text=_('Ip address of network. For example: 192.168.1.0 or fde8:6789:1234:1::'),
help_text=_('Ip address of network. For example: '
'192.168.1.0 or fde8:6789:1234:1::'),
unique=True unique=True
) )
NETWORK_KINDS = ( NETWORK_KINDS = (
@ -31,7 +29,10 @@ class NetworkModel(models.Model):
('device', _('Devices')), ('device', _('Devices')),
('admin', _('Admin')) ('admin', _('Admin'))
) )
kind = models.CharField(_('Kind of network'), max_length=6, choices=NETWORK_KINDS, default='guest')
kind = models.CharField(
_('Kind of network'), max_length=6,
choices=NETWORK_KINDS, default='guest'
)
description = models.CharField(_('Description'), max_length=64) description = models.CharField(_('Description'), max_length=64)
groups = models.ManyToManyField(Group, verbose_name=_('Groups')) groups = models.ManyToManyField(Group, verbose_name=_('Groups'))
@ -56,33 +57,53 @@ class NetworkModel(models.Model):
def clean(self): def clean(self):
errs = {} errs = {}
if self.network is None: if self.network is None:
errs['network'] = ValidationError(_('Network is invalid'), code='invalid')
errs['network'] = ValidationError(
_('Network is invalid'),
code='invalid'
)
raise ValidationError(errs) raise ValidationError(errs)
net = self.get_network() net = self.get_network()
if self.ip_start is None: if self.ip_start is None:
errs['ip_start'] = ValidationError(_('Ip start is invalid'), code='invalid')
errs['ip_start'] = ValidationError(
_('Ip start is invalid'),
code='invalid'
)
raise ValidationError(errs) raise ValidationError(errs)
start_ip = ip_address(self.ip_start) start_ip = ip_address(self.ip_start)
if start_ip not in net: if start_ip not in net:
errs['ip_start'] = ValidationError(_('Start ip must be in subnet of specified network'), code='invalid')
errs['ip_start'] = ValidationError(
_('Start ip must be in subnet of specified network'),
code='invalid'
)
if self.ip_end is None: if self.ip_end is None:
errs['ip_end'] = ValidationError(_('Ip end is invalid'), code='invalid')
errs['ip_end'] = ValidationError(
_('Ip end is invalid'),
code='invalid'
)
raise ValidationError(errs) raise ValidationError(errs)
end_ip = ip_address(self.ip_end) end_ip = ip_address(self.ip_end)
if end_ip not in net: if end_ip not in net:
errs['ip_end'] = ValidationError(_('End ip must be in subnet of specified network'), code='invalid')
errs['ip_end'] = ValidationError(
_('End ip must be in subnet of specified network'),
code='invalid'
)
if errs: if errs:
raise ValidationError(errs) raise ValidationError(errs)
other_nets = NetworkModel.objects.exclude(pk=self.pk).only('network').order_by('network')
other_nets = NetworkModel.objects.exclude(
pk=self.pk
).only('network').order_by('network')
if not other_nets.exists(): if not other_nets.exists():
return return
for onet in other_nets.iterator(): for onet in other_nets.iterator():
onet_netw = onet.get_network() onet_netw = onet.get_network()
if net.overlaps(onet_netw): if net.overlaps(onet_netw):
errs['network'] = ValidationError(_('Network is overlaps with %(other_network)s'), params={
'other_network': str(onet_netw)
})
errs['network'] = ValidationError(
_('Network is overlaps with %(other_network)s'),
params={
'other_network': str(onet_netw)
}
)
raise ValidationError(errs) raise ValidationError(errs)
def get_scope(self) -> str: def get_scope(self) -> str:
@ -108,7 +129,8 @@ class NetworkModel(models.Model):
def get_free_ip(self, employed_ips: Optional[Generator]): def get_free_ip(self, employed_ips: Optional[Generator]):
""" """
Find free ip in network. Find free ip in network.
:param employed_ips: Sorted from less to more ip addresses from current network.
:param employed_ips: Sorted from less to more
ip addresses from current network.
:return: single finded ip :return: single finded ip
""" """
network = self.get_network() network = self.get_network()
@ -144,7 +166,10 @@ class IpLeaseManager(models.Manager):
netw = network.get_network() netw = network.get_network()
work_range_start_ip = ip_address(network.ip_start) work_range_start_ip = ip_address(network.ip_start)
work_range_end_ip = ip_address(network.ip_end) work_range_end_ip = ip_address(network.ip_end)
employed_ip_queryset = self.filter(network=network, is_dynamic=False).order_by('ip').only('ip')
employed_ip_queryset = self.filter(
network=network,
is_dynamic=False
).order_by('ip').only('ip')
if employed_ip_queryset.exists(): if employed_ip_queryset.exists():
used_ip_gen = employed_ip_queryset.iterator() used_ip_gen = employed_ip_queryset.iterator()
@ -164,7 +189,8 @@ class IpLeaseManager(models.Manager):
if work_range_start_ip <= net <= work_range_end_ip: if work_range_start_ip <= net <= work_range_end_ip:
return net return net
def create_from_ip(self, ip: str, net: Optional[NetworkModel], mac=None, is_dynamic=True):
def create_from_ip(self, ip: str, net: Optional[NetworkModel],
mac=None, is_dynamic=True):
# ip = ip_address(ip) # ip = ip_address(ip)
try: try:
return self.create( return self.create(
@ -176,13 +202,6 @@ class IpLeaseManager(models.Manager):
except IntegrityError as e: except IntegrityError as e:
raise DuplicateEntry(e) raise DuplicateEntry(e)
def expired(self):
lease_live_time = getattr(settings, 'LEASE_LIVE_TIME')
if lease_live_time is None:
raise ImproperlyConfigured('You must specify LEASE_LIVE_TIME in settings')
senility = now() - timedelta(seconds=lease_live_time)
return self.filter(lease_time__lt=senility)
# Deprecated. Remove after migrations squashed # Deprecated. Remove after migrations squashed
class IpLeaseModel(models.Model): class IpLeaseModel(models.Model):

3
locale/ru/LC_MESSAGES/django.po

@ -122,3 +122,6 @@ msgstr "500 - Ошибка сервера"
msgid "A server has error occurred. Please contact the administrator." msgid "A server has error occurred. Please contact the administrator."
msgstr "На сервере произошла ошибка. Пожалуйста свяжитесь с системным администратором." msgstr "На сервере произошла ошибка. Пожалуйста свяжитесь с системным администратором."
msgid "Are you sure about them?"
msgstr "Вы уверены в этом?"

115
msg_app/models.py

@ -1,7 +1,7 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts_app.models import UserProfile from accounts_app.models import UserProfile
from chatbot.send_func import send_notify
from djing.tasks import send_email_notify
from chatbot.models import ChatException from chatbot.models import ChatException
@ -10,17 +10,29 @@ class MessageError(Exception):
class MessageStatus(models.Model): class MessageStatus(models.Model):
msg = models.ForeignKey('Message', on_delete=models.CASCADE, related_name='msg_statuses')
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='usr_msg_status')
msg = models.ForeignKey(
'Message', on_delete=models.CASCADE,
related_name='msg_statuses'
)
user = models.ForeignKey(
UserProfile, on_delete=models.CASCADE,
related_name='usr_msg_status'
)
MESSAGE_STATES = ( MESSAGE_STATES = (
('new', _('New')), ('new', _('New')),
('old', _('Seen')), ('old', _('Seen')),
('del', _('Deleted')) ('del', _('Deleted'))
) )
state = models.CharField(max_length=3, choices=MESSAGE_STATES, default='new')
state = models.CharField(
max_length=3, choices=MESSAGE_STATES,
default='new'
)
def __str__(self): def __str__(self):
return "%s for %s (%s)" % (self.get_state_display(), self.user, self.msg)
return "%s for %s (%s)" % (
self.get_state_display(),
self.user, self.msg
)
class Meta: class Meta:
db_table = 'message_status' db_table = 'message_status'
@ -34,10 +46,22 @@ class MessageStatus(models.Model):
class Message(models.Model): class Message(models.Model):
text = models.TextField(_("Body")) text = models.TextField(_("Body"))
sent_at = models.DateTimeField(_("sent at"), auto_now_add=True) sent_at = models.DateTimeField(_("sent at"), auto_now_add=True)
author = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='messages')
conversation = models.ForeignKey('Conversation', on_delete=models.CASCADE, verbose_name=_('Conversation'))
attachment = models.FileField(upload_to='messages_attachments/%Y_%m_%d', blank=True, null=True)
account_status = models.ManyToManyField(UserProfile, through=MessageStatus, through_fields=('msg', 'user'))
author = models.ForeignKey(
UserProfile, on_delete=models.CASCADE,
related_name='messages'
)
conversation = models.ForeignKey(
'Conversation', on_delete=models.CASCADE,
verbose_name=_('Conversation')
)
attachment = models.FileField(
upload_to='messages_attachments/%Y_%m_%d',
blank=True, null=True
)
account_status = models.ManyToManyField(
UserProfile, through=MessageStatus,
through_fields=('msg', 'user')
)
def __str__(self): def __str__(self):
return self.text[:9] return self.text[:9]
@ -70,7 +94,10 @@ class Message(models.Model):
class ConversationMembership(models.Model): class ConversationMembership(models.Model):
account = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='memberships')
account = models.ForeignKey(
UserProfile, on_delete=models.CASCADE,
related_name='memberships'
)
conversation = models.ForeignKey('Conversation', on_delete=models.CASCADE) conversation = models.ForeignKey('Conversation', on_delete=models.CASCADE)
PARTICIPANT_STATUS = ( PARTICIPANT_STATUS = (
('adm', _('Admin')), ('adm', _('Admin')),
@ -78,9 +105,14 @@ class ConversationMembership(models.Model):
('ban', _('Banned user')), ('ban', _('Banned user')),
('inv', _('Inviter')) ('inv', _('Inviter'))
) )
status = models.CharField(max_length=3, choices=PARTICIPANT_STATUS, default='gst')
who_invite_that_user = models.ForeignKey(UserProfile, on_delete=models.CASCADE, null=True, blank=True,
related_name='self_conversations')
status = models.CharField(
max_length=3, choices=PARTICIPANT_STATUS, default='gst'
)
who_invite_that_user = models.ForeignKey(
UserProfile, on_delete=models.CASCADE,
null=True, blank=True,
related_name='self_conversations'
)
def __str__(self): def __str__(self):
return "%s < %s" % (self.conversation, self.account) return "%s < %s" % (self.conversation, self.account)
@ -102,7 +134,9 @@ def id_to_userprofile(acc):
class ConversationManager(models.Manager): class ConversationManager(models.Manager):
def create_conversation(self, author, other_participants, title=None): def create_conversation(self, author, other_participants, title=None):
other_participants = tuple(id_to_userprofile(acc) for acc in other_participants)
other_participants = tuple(
id_to_userprofile(acc) for acc in other_participants
)
if not title: if not title:
usernames = tuple(acc.username for acc in other_participants) usernames = tuple(acc.username for acc in other_participants)
if not usernames: if not usernames:
@ -112,7 +146,8 @@ class ConversationManager(models.Manager):
conversation = self.create(title=title, author=author) conversation = self.create(title=title, author=author)
for acc in other_participants: for acc in other_participants:
ConversationMembership.objects.create( ConversationMembership.objects.create(
account=acc, conversation=conversation, status='adm', who_invite_that_user=author
account=acc, conversation=conversation,
status='adm', who_invite_that_user=author
) )
ConversationMembership.objects.create( ConversationMembership.objects.create(
@ -123,12 +158,16 @@ class ConversationManager(models.Manager):
@staticmethod @staticmethod
def get_new_messages_count(account): def get_new_messages_count(account):
if isinstance(account, UserProfile): if isinstance(account, UserProfile):
return MessageStatus.objects.filter(user=account, state='new').count()
return MessageStatus.objects.filter(
user=account, state='new'
).count()
else: else:
return 0 return 0
def fetch(self, account): def fetch(self, account):
conversations = self.filter(models.Q(author=account) | models.Q(participants__in=(account,))).annotate(
conversations = self.filter(
models.Q(author=account) | models.Q(participants__in=(account,))
).annotate(
msg_count=models.Count('message', distinct=True) msg_count=models.Count('message', distinct=True)
) )
return conversations return conversations
@ -136,9 +175,11 @@ class ConversationManager(models.Manager):
class Conversation(models.Model): class Conversation(models.Model):
title = models.CharField(max_length=32) title = models.CharField(max_length=32)
participants = models.ManyToManyField(UserProfile, related_name='conversations',
through='ConversationMembership',
through_fields=('conversation', 'account'))
participants = models.ManyToManyField(
UserProfile, related_name='conversations',
through='ConversationMembership',
through_fields=('conversation', 'account')
)
author = models.ForeignKey(UserProfile, on_delete=models.CASCADE) author = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
date_create = models.DateTimeField(auto_now_add=True) date_create = models.DateTimeField(auto_now_add=True)
@ -152,7 +193,9 @@ class Conversation(models.Model):
def get_messages_new_count(self, account): def get_messages_new_count(self, account):
msgs = Message.objects.filter(conversation=self) msgs = Message.objects.filter(conversation=self)
return MessageStatus.objects.filter(user=account, msg__in=msgs, state='new').count()
return MessageStatus.objects.filter(
user=account, msg__in=msgs, state='new'
).count()
def last_message(self): def last_message(self):
messages = Message.objects.filter(conversation=self) messages = Message.objects.filter(conversation=self)
@ -162,14 +205,18 @@ class Conversation(models.Model):
def new_message(self, text, attachment, author, with_status=True): def new_message(self, text, attachment, author, with_status=True):
try: try:
msg = Message.objects.create( msg = Message.objects.create(
text=text, conversation=self, attachment=attachment, author=author
text=text, conversation=self,
attachment=attachment, author=author
) )
if with_status: if with_status:
for participant in self.participants.all(): for participant in self.participants.all():
if participant == author: if participant == author:
continue continue
MessageStatus.objects.create(msg=msg, user=participant) MessageStatus.objects.create(msg=msg, user=participant)
send_notify(msg_text=text, account=participant, tag='msgapp')
send_email_notify.delay(
msg_text=text,
account_id=participant.pk
)
return msg return msg
except ChatException as e: except ChatException as e:
raise MessageError(e) raise MessageError(e)
@ -188,10 +235,13 @@ class Conversation(models.Model):
def _make_participant_status(self, user, status, cm=None): def _make_participant_status(self, user, status, cm=None):
if cm is None: if cm is None:
cm = ConversationMembership.objects.get(account=user, conversation=self)
cm = ConversationMembership.objects.get(
account=user, conversation=self
)
else: else:
if not isinstance(cm, ConversationMembership): if not isinstance(cm, ConversationMembership):
raise TypeError('cm must be instance of msg_app.ConversationMembership')
raise TypeError('cm must be instance of '
'msg_app.ConversationMembership')
cm.status = status cm.status = status
cm.save(update_fields=('status',)) cm.save(update_fields=('status',))
return cm return cm
@ -210,21 +260,28 @@ class Conversation(models.Model):
def remove_participant(self, user): def remove_participant(self, user):
try: try:
cm = ConversationMembership.objects.get(account=user, conversation=self)
cm = ConversationMembership.objects.get(
account=user, conversation=self
)
cm.delete() cm.delete()
except ConversationMembership.DoesNotExist: except ConversationMembership.DoesNotExist:
pass pass
def add_participant(self, author, user): def add_participant(self, author, user):
return ConversationMembership.objects.create( return ConversationMembership.objects.create(
account=user, conversation=self, status='gst', who_invite_that_user=author
account=user, conversation=self,
status='gst', who_invite_that_user=author
) )
def find_messages_by_text(self, text): def find_messages_by_text(self, text):
return Message.objects.filter(text__icontains=text, conversation=self)
return Message.objects.filter(
text__icontains=text, conversation=self
)
def _make_messages_status(self, account, status): def _make_messages_status(self, account, status):
qs = MessageStatus.objects.filter(msg__conversation=self, user=account).exclude(state='del')
qs = MessageStatus.objects.filter(
msg__conversation=self, user=account
).exclude(state='del')
if status != 'del': if status != 'del':
qs = qs.exclude(state=status) qs = qs.exclude(state=status)
return qs.update(state=status) return qs.update(state=status)

39
periodic.py

@ -37,9 +37,11 @@ def main():
signals.pre_delete.disconnect(abontariff_pre_delete, sender=AbonTariff) signals.pre_delete.disconnect(abontariff_pre_delete, sender=AbonTariff)
AbonTariff.objects.filter(abon=None).delete() AbonTariff.objects.filter(abon=None).delete()
now = timezone.now() now = timezone.now()
fields = ('id', 'tariff__title', 'abon__id')
expired_services = AbonTariff.objects.exclude(abon=None).filter(deadline__lt=now,
abon__autoconnect_service=False)
fields = ('id', 'tariff__title', 'abon__id', 'abon__username')
expired_services = AbonTariff.objects.exclude(abon=None).filter(
deadline__lt=now,
abon__autoconnect_service=False
)
# finishing expires services # finishing expires services
with transaction.atomic(): with transaction.atomic():
@ -49,16 +51,19 @@ def main():
amount=0, amount=0,
author=None, author=None,
date=now, date=now,
comment="Срок действия услуги '%(service_name)s' истёк" % {
'service_name': ex_srv['tariff__title']
comment="Срок действия услуги '%(service_name)s' для '%(username)s' истёк" % {
'service_name': ex_srv['tariff__title'],
'username': ex_srv['abon__username']
} }
) )
print(log) print(log)
expired_services.delete() expired_services.delete()
# Automatically connect new service # Automatically connect new service
for ex in AbonTariff.objects.filter(deadline__lt=now, abon__autoconnect_service=True).exclude(
abon=None).iterator():
for ex in AbonTariff.objects.filter(
deadline__lt=now,
abon__autoconnect_service=True
).exclude(abon=None).iterator():
abon = ex.abon abon = ex.abon
trf = ex.tariff trf = ex.tariff
amount = round(trf.amount, 2) amount = round(trf.amount, 2)
@ -91,7 +96,23 @@ def main():
) )
print(l.comment) print(l.comment)
# signals.pre_delete.connect(abontariff_pre_delete, sender=AbonTariff)
# Post connect service
# connect service when autoconnect is True, and user have enough money
for ab in Abon.objects.filter(
is_active=True,
current_tariff=None,
autoconnect_service=True
).exclude(last_connected_tariff=None).iterator():
try:
tariff = ab.last_connected_tariff
if tariff is None or tariff.is_admin:
continue
ab.pick_tariff(
tariff, None,
"Автоматическое продление услуги '%s'" % tariff.title
)
except LogicError as e:
print(e)
# manage periodic pays # manage periodic pays
ppays = PeriodicPayForId.objects.filter(next_pay__lt=now) \ ppays = PeriodicPayForId.objects.filter(next_pay__lt=now) \
@ -102,7 +123,7 @@ def main():
# sync subscribers on GW # sync subscribers on GW
threads = tuple(NasSyncThread(nas) for nas in NASModel.objects. threads = tuple(NasSyncThread(nas) for nas in NASModel.objects.
annotate(usercount=Count('abon')). annotate(usercount=Count('abon')).
filter(usercount__gt=0))
filter(usercount__gt=0, enabled=True))
for t in threads: for t in threads:
t.start() t.start()
for t in threads: for t in threads:

4
requirements.txt

@ -28,3 +28,7 @@ asterisk
# django-xmlview for pay system allpay # django-xmlview for pay system allpay
-e git://github.com/nerosketch/django-xmlview.git#egg=django-xmlview -e git://github.com/nerosketch/django-xmlview.git#egg=django-xmlview
Celery
redis==2.10.6
celery[redis]

8
static/css/custom.css

@ -256,14 +256,6 @@ div#loading {
z-index: 1; z-index: 1;
display: none; display: none;
} }
div#loading>div.gif {
position: absolute;
left: 49%;
top: 39%;
background-color: white;
border-radius: 13px;
border-style: ridge;
}

BIN
static/img/loading.gif

Before

Width: 100  |  Height: 100  |  Size: 6.6 KiB

3
static/js/my.js

@ -11,7 +11,6 @@ var cnt='<div class="modal-header '+type_class+'">' +
'<div class="modal-body">'+content+'</div>' + '<div class="modal-body">'+content+'</div>' +
'<div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button></div>'; '<div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button></div>';
show_ModalMyContent(cnt); show_ModalMyContent(cnt);
$('#loading').hide();
} }
function showErr(errContent) {show_Modal('Ошибка', errContent, 'warning');} function showErr(errContent) {show_Modal('Ошибка', errContent, 'warning');}
@ -125,11 +124,9 @@ $(document).ajaxError(function (ev, jqXHR, ajaxSettings, thrownError) {
}, opt); }, opt);
var fill_block_fn = function(){ var fill_block_fn = function(){
$('#loading').show();
var url = $(this).attr('data-href'); var url = $(this).attr('data-href');
$.get(url, function(r){ $.get(url, function(r){
$(settings.dst_block).html(r); $(settings.dst_block).html(r);
$('#loading').hide();
}); });
}; };

0
statistics/__init__.py

0
statistics/admin.py

5
statistics/apps.py

@ -1,5 +0,0 @@
from django.apps import AppConfig
class StatisticsConfig(AppConfig):
name = 'statistics'

53
statistics/fields.py

@ -1,53 +0,0 @@
#
# Get from https://github.com/Niklas9/django-unixdatetimefield
#
import datetime
import time
import django.db.models as models
class UnixDateTimeField(models.DateTimeField):
# TODO(niklas9):
# * should we take care of transforming between time zones in any way here ?
# * get default datetime format from settings ?
DEFAULT_DATETIME_FMT = '%Y-%m-%d %H:%M:%S'
TZ_CONST = '+'
# TODO(niklas9):
# * metaclass below just for Django < 1.9, fix a if stmt for it?
# __metaclass__ = models.SubfieldBase
description = "Unix timestamp integer to datetime object"
def get_internal_type(self):
return 'PositiveIntegerField'
def to_python(self, val):
if val is None or isinstance(val, datetime.datetime):
return val
if isinstance(val, datetime.date):
return datetime.datetime(val.year, val.month, val.day)
elif self._is_string(val):
# TODO(niklas9):
# * not addressing time zone support as todo above for now
if self.TZ_CONST in val:
val = val.split(self.TZ_CONST)[0]
return datetime.datetime.strptime(val, self.DEFAULT_DATETIME_FMT)
else:
return datetime.datetime.fromtimestamp(float(val))
@staticmethod
def _is_string(val):
return isinstance(val, str)
def get_db_prep_value(self, val, *args, **kwargs):
if val is None:
if self.default == models.fields.NOT_PROVIDED: return None
return self.default
return int(time.mktime(val.timetuple()))
def value_to_string(self, obj):
val = self._get_val_from_obj(obj)
return self.to_python(val).strftime(self.DEFAULT_DATETIME_FMT)
def from_db_value(self, val, expression, connection):
return self.to_python(val)

29
statistics/migrations/0001_initial.py

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-26 00:20
from __future__ import unicode_literals
from django.db import migrations, models
from djing.fields import MyGenericIPAddressField
import statistics.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='StatCache',
fields=[
('last_time', statistics.fields.UnixDateTimeField()),
('ip', 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',
},
),
]

19
statistics/migrations/0002_auto_20180808_1236.py

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-08-08 12:36
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('statistics', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='statcache',
options={'ordering': ('-last_time',)},
),
]

79
statistics/migrations/0003_auto_20180814_1921.py

@ -1,79 +0,0 @@
# Generated by Django 2.1 on 2018-09-22 14:30
from django.core.exceptions import ImproperlyConfigured
from django.db import migrations, connection, models
from statistics.fields import UnixDateTimeField
# def psql_migr(apps, _):
# pass
class Migration(migrations.Migration):
dependencies = [
('abonapp', '0005_current_tariff'),
('statistics', '0002_auto_20180808_1236'),
]
operations = [
migrations.AlterModelOptions(
name='statcache',
options={'ordering': ('-last_time',)},
),
]
db_e = connection.settings_dict.get('ENGINE')
if db_e is None:
raise ImproperlyConfigured('Database ENGINE is not set')
# if 'postgresql' in db_e:
# # Postgres
Migration.operations.insert(0, migrations.RunPython(psql_migr))
if 'mysql' in db_e:
Migration.operations.insert(0, migrations.RunSQL(
(
"DROP TABLE `flowcache`;",
"CREATE TABLE `flowcache` ( "
" `last_time` INT(10) UNSIGNED NOT NULL, "
" `abon_id` INT(11) DEFAULT NULL UNIQUE, "
" `octets` INT(10) UNSIGNED NOT NULL, "
" `packets` INT(10) UNSIGNED NOT NULL, "
" KEY `flowcache_abon_id_91e1085d` (`abon_id`) "
") ENGINE = MEMORY DEFAULT CHARSET = utf8;"
),
state_operations=[
migrations.DeleteModel(name='statcache'),
migrations.CreateModel(
name='statcache',
fields=[
('last_time', UnixDateTimeField()),
('abon', models.OneToOneField('abonapp.Abon', on_delete=models.CASCADE, primary_key=True)),
('octets', models.PositiveIntegerField(default=0)),
('packets', models.PositiveIntegerField(default=0))
],
options={
'db_table': 'flowcache',
},
)
]
))
else:
Migration.operations.extend(
(
migrations.DeleteModel(name='statcache'),
migrations.CreateModel(
name='statcache',
fields=[
('last_time', UnixDateTimeField()),
('abon', models.OneToOneField('abonapp.Abon', on_delete=models.CASCADE, primary_key=True)),
('octets', models.PositiveIntegerField(default=0)),
('packets', models.PositiveIntegerField(default=0))
],
options={
'db_table': 'flowcache',
'ordering': ('-last_time',),
#'db_tablespace': 'ram'
},
)
)
)

0
statistics/migrations/__init__.py

132
statistics/models.py

@ -1,132 +0,0 @@
import math
from datetime import datetime, timedelta, date, time
from django.db import models, connection, ProgrammingError
from django.utils.timezone import now
from djing.fields import MyGenericIPAddressField
from .fields import UnixDateTimeField
def get_dates():
tables = connection.introspection.table_names()
tables = (t.replace('flowstat_', '') for t in tables if t.startswith('flowstat_'))
return tuple(datetime.strptime(t, '%d%m%Y').date() for t in tables)
class StatManager(models.Manager):
def chart(self, user, count_of_parts=12, want_date=date.today()):
def byte_to_mbit(x):
return ((x / 60) * 8) / 2 ** 20
def split_list(lst, chunk_count):
chunk_size = len(lst) // chunk_count
if chunk_size == 0:
chunk_size = 1
return tuple(lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size))
def avarage(elements):
return sum(elements) / len(elements)
try:
charts_data = self.filter(abon=user)
charts_times = tuple(cd.cur_time.timestamp() * 1000 for cd in charts_data)
charts_octets = tuple(cd.octets for cd in charts_data)
if len(charts_octets) > 0 and len(charts_octets) == len(charts_times):
charts_octets = split_list(charts_octets, count_of_parts)
charts_octets = (byte_to_mbit(avarage(c)) for c in charts_octets)
charts_times = split_list(charts_times, count_of_parts)
charts_times = tuple(avarage(t) for t in charts_times)
charts_data = zip(charts_times, charts_octets)
charts_data = ["{x: new Date(%d), y: %.2f}" % (cd[0], cd[1]) for cd in charts_data]
midnight = datetime.combine(want_date, time.min)
charts_data.append("{x:new Date(%d),y:0}" % (int(charts_times[-1:][0]) + 1))
charts_data.append("{x:new Date(%d),y:0}" % (int((midnight + timedelta(days=1)).timestamp()) * 1000))
return charts_data
else:
return
except ProgrammingError as e:
if "Table 'djing_db_n.flowstat" in str(e):
return
class StatElem(models.Model):
cur_time = UnixDateTimeField(primary_key=True)
abon = models.ForeignKey('abonapp.Abon', on_delete=models.CASCADE, null=True, default=None, blank=True)
ip = MyGenericIPAddressField()
octets = models.PositiveIntegerField(default=0)
packets = models.PositiveIntegerField(default=0)
objects = StatManager()
# ReadOnly
def save(self, *args, **kwargs):
pass
# ReadOnly
def delete(self, *args, **kwargs):
pass
@property
def table_name(self):
return self._meta.db_table
def delete_month(self):
cursor = connection.cursor()
table_name = self._meta.db_table
sql = "DROP TABLE %s;" % table_name
cursor.execute(sql)
@staticmethod
def percentile(N, percent, key=lambda x: x):
"""
Find the percentile of a list of values.
@parameter N - is a list of values. Note N MUST BE already sorted.
@parameter percent - a float value from 0.0 to 1.0.
@parameter key - optional key function to compute value from each element of N.
@return - the percentile of the values
"""
if not N:
return None
k = (len(N) - 1) * percent
f = math.floor(k)
c = math.ceil(k)
if f == c:
return key(N[int(k)])
d0 = key(N[int(f)]) * (c - k)
d1 = key(N[int(c)]) * (k - f)
return d0 + d1
class Meta:
abstract = True
def getModel(want_date=now()):
class DynamicStatElem(StatElem):
class Meta:
db_table = 'flowstat_%s' % want_date.strftime("%d%m%Y")
abstract = False
return DynamicStatElem
class StatCache(models.Model):
last_time = UnixDateTimeField()
# ip = MyGenericIPAddressField(primary_key=True)
abon = models.OneToOneField('abonapp.Abon', on_delete=models.CASCADE, primary_key=True)
octets = models.PositiveIntegerField(default=0)
packets = models.PositiveIntegerField(default=0)
def is_online(self):
return self.last_time > now() - timedelta(minutes=55)
def is_today(self):
return date.today() == self.last_time.date()
class Meta:
db_table = 'flowcache'
ordering = ('-last_time',)
# db_tablespace = 'ram'

37
statistics/templates/statistics/index.html

@ -1,37 +0,0 @@
{% extends 'base.html' %}
{% block main %}
<link href="/static/css/chartist.min.css" rel="stylesheet" type="text/css"/>
<script src="/static/js/chartist.min.js"></script>
<script>
var chart = new Chartist.Line('#maincontent', {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
series: [
[1, 5, 2, 5, 4, 3],
[2, 3, 4, 8, 1, 2],
[5, 4, 3, 2, 1, 0.5]
]
}, {
low: 0,
showArea: true,
showPoint: false,
fullWidth: true,
height: 500
});
chart.on('draw', function (data) {
if (data.type === 'line' || data.type === 'area') {
data.element.animate({
d: {
begin: 2000 * data.index,
dur: 2000,
from: data.path.clone().scale(1, 0).translate(0, data.chartRect.height()).stringify(),
to: data.path.clone().stringify(),
easing: Chartist.Svg.Easing.easeOutQuint
}
});
}
});
</script>
{% endblock %}

9
statistics/urls.py

@ -1,9 +0,0 @@
from django.urls import path
from . import views
app_name = 'statistics'
urlpatterns = [
path('', views.home, name='home'),
]

9
statistics/views.py

@ -1,9 +0,0 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from djing.lib.decorators import only_admins
@login_required
@only_admins
def home(request):
return render(request, 'statistics/index.html')

6
systemd_units/djing.service

@ -4,9 +4,9 @@ Description=A job for djing
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/python3 periodic.py ExecStart=/usr/bin/python3 periodic.py
WorkingDirectory=/srv/http/djing
User=http
Group=http
WorkingDirectory=/var/www/djing
User=www-data
Group=www-data
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

14
systemd_units/djing_celery.service

@ -0,0 +1,14 @@
[Unit]
Description=Celery worker for djing
[Service]
Type=simple
ExecStart=/var/www/djing/venv/bin/celery worker -A djing --loglevel=info --concurrency=4
WorkingDirectory=/var/www/djing
TimeoutSec=7
Restart=always
User=www-data
Group=www-data
[Install]
WantedBy=multi-user.target

2
systemd_units/djing_dial.service

@ -5,7 +5,7 @@ Description=Dialing inbox sms unit
Type=simple Type=simple
ExecStart=/usr/bin/python3 dialing.py ExecStart=/usr/bin/python3 dialing.py
PIDFile=/run/dialing.py.pid PIDFile=/run/dialing.py.pid
WorkingDirectory=/srv/http/djing
WorkingDirectory=/var/www/djing
User=http User=http
Group=http Group=http

4
systemd_units/djing_telebot.service

@ -8,8 +8,8 @@ PIDFile=/run/djing_telebot.pid
WorkingDirectory=/var/www/djing WorkingDirectory=/var/www/djing
TimeoutSec=9 TimeoutSec=9
Restart=always Restart=always
User=http
Group=http
User=www-data
Group=www-data
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

17
tariff_app/migrations/0003_auto_20181115_1206.py

@ -0,0 +1,17 @@
# Generated by Django 2.1 on 2018-11-15 12:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tariff_app', '0002_auto_20180807_1548'),
]
operations = [
migrations.AlterModelOptions(
name='periodicpay',
options={'ordering': ('-id',), 'verbose_name': 'Periodic pay', 'verbose_name_plural': 'Periodic pays'},
),
]

9
taskapp/handle.py

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from chatbot.send_func import send_notify
from djing.tasks import send_email_notify
from chatbot.models import ChatException from chatbot.models import ChatException
from djing.lib import MultipleException from djing.lib import MultipleException
@ -30,12 +30,9 @@ def handle(task, author, recipients):
if task.state == 'F' or task.state == 'C': if task.state == 'F' or task.state == 'C':
# If task completed or failed than send one message to author # If task completed or failed than send one message to author
try:
send_notify(fulltext, author, tag='taskap')
except ChatException as e:
raise TaskException(e)
send_email_notify.delay(fulltext, author.pk)
else: else:
send_notify(fulltext, recipient, tag='taskap')
send_email_notify.delay(fulltext, recipient.pk)
except ChatException as e: except ChatException as e:
errors.append(e) errors.append(e)
if len(errors) > 0: if len(errors) > 0:

6
templates/all_base.html

@ -16,12 +16,6 @@
</head> </head>
<body> <body>
<div id="loading">
<div class="gif">
<img src="/static/img/loading.gif"/>
</div>
</div>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="modFrm" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" id="modFrm" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">

Loading…
Cancel
Save