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.AllTimePayLog)
admin.site.register(models.AbonRawPassword)
admin.site.register(models.AllPayLog)
admin.site.register(models.PassportInfo)
admin.site.register(models.AdditionalTelephone)

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

@ -195,8 +195,8 @@ msgid "Service already activated"
msgstr "Услуга уже подключена"
#: models.py:174
msgid "not enough money"
msgstr "Не хватает денег на счету"
msgid "%s not enough money for service %s"
msgstr "%s не имеет достаточно средств для %s"
#: models.py:190
msgid "Buy service default log"
@ -939,8 +939,8 @@ msgstr "Квитанция на оплату была создана"
#: views.py:419
#, 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
msgid "Tariff has been picked"
@ -1156,3 +1156,18 @@ msgstr "У пользователя нет ip"
msgid "Ip successfully updated"
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 typing import Optional
from accounts_app.models import UserProfile, MyUserManager, BaseAccount
from bitfield import BitField
from django.conf import settings
from django.core import validators
from django.core.validators import RegexValidator
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.shortcuts import resolve_url
from django.utils import timezone
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 group_app.models import Group
from gw_app.nas_managers import SubnetQueue, NasFailedResult, NasNetworkError
from ip_pool.models import NetworkModel
from tariff_app.models import Tariff, PeriodicPay
from bitfield import BitField
class AbonLog(models.Model):
abon = models.ForeignKey('Abon', on_delete=models.CASCADE)
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)
date = models.DateTimeField(auto_now_add=True)
@ -36,7 +37,11 @@ class AbonLog(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)
@ -46,10 +51,6 @@ class AbonTariff(models.Model):
amount = self.tariff.amount
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):
return "%s: %s" % (
self.deadline,
@ -86,19 +87,75 @@ class AbonManager(MyUserManager):
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)
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 = (
('icon_donkey', _('Donkey')),
@ -144,7 +201,8 @@ class Abon(BaseAccount):
AbonLog.objects.create(
abon=self,
amount=amount,
author=current_user if isinstance(current_user, UserProfile) else None,
author=current_user if isinstance(current_user,
UserProfile) else None,
comment=comment
)
self.ballance += amount
@ -153,10 +211,11 @@ class Abon(BaseAccount):
"""
Trying to buy a service if enough money.
: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 deadline: Instance of datetime.datetime. Date when service is expired.
:param deadline: Instance of datetime.datetime. Date when service is
expired.
:return: Nothing
"""
if not isinstance(tariff, Tariff):
@ -166,7 +225,9 @@ class Abon(BaseAccount):
if tariff.is_admin and author is not None:
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.tariff == tariff:
@ -178,18 +239,26 @@ class Abon(BaseAccount):
# if not enough money
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():
new_abtar = AbonTariff.objects.create(
deadline=deadline, tariff=tariff
)
self.current_tariff = new_abtar
if self.last_connected_tariff != tariff:
self.last_connected_tariff = tariff
# charge for the service
self.ballance -= amount
self.save(update_fields=('ballance', 'current_tariff'))
self.save(update_fields=(
'ballance',
'current_tariff',
'last_connected_tariff'
))
# make log about it
AbonLog.objects.create(
@ -217,7 +286,8 @@ class Abon(BaseAccount):
return True
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:
if not self.is_active:
return False
@ -301,7 +371,7 @@ class Abon(BaseAccount):
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 deadline: Time when service is expired
:param time_start: Time when service has started
@ -316,15 +386,32 @@ class Abon(BaseAccount):
time_start=time_start
)
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):
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'))
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:
db_table = 'passport_info'
@ -343,7 +430,13 @@ class InvoiceForPayment(models.Model):
comment = models.CharField(max_length=128)
date_create = models.DateTimeField(auto_now_add=True)
date_pay = models.DateTimeField(blank=True, null=True)
author = models.ForeignKey(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):
return "%s -> %.2f" % (self.abon.username, self.amount)
@ -367,7 +460,9 @@ class AllTimePayLogManager(models.Manager):
def by_days():
cur = connection.cursor()
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")'
)
while True:
@ -375,16 +470,35 @@ class AllTimePayLogManager(models.Manager):
if r is None:
break
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"
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)
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)
objects = AllTimePayLogManager()
@ -424,7 +538,11 @@ class AbonRawPassword(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(
max_length=16,
verbose_name=_('Telephone'),
@ -446,10 +564,18 @@ class AdditionalTelephone(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)
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):
"""
@ -465,9 +591,10 @@ class PeriodicPayForId(models.Model):
next_pay_date = pp.get_next_time_to_pay(self.last_pay)
abon = self.account
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',))
self.last_pay = now
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>
<select class="form-control" name="tariff" id="id_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>
{% else %}
<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 %}
<div class="input-group">
<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">
$(function () {
$('#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="panel panel-default">
<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 class="panel-body">
@ -99,6 +102,13 @@
<span class="glyphicon glyphicon-paperclip"></span> {% trans 'Passport information' %}
</a>
{% 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>

5
abonapp/templates/abonapp/ext.htm

@ -39,11 +39,6 @@
<a href="{{ abtasklog }}">{% trans 'History of tasks' %}</a>
</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 %}
<li{% if abdials == request.path %} class="active"{% endif %}>
<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 %}
</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">
<a href="{% url 'abonapp:people_list' group.pk %}?{% url_order_by request order_by='ballance' %}">
{% trans 'Balance' %}
@ -71,23 +75,25 @@
{% else %}
<tr class="danger">
{% 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 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 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 class="col-xs-1">{{ human.ip_address|default_if_none:'&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 %}
{% for service in services %}
<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>
{% if can_ch_trf %}
<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>
</div>
<div class="panel-body">
{% if abon_tariff %}
<dl class="dl-horizontal">
<dl class="dl-horizontal">
{% if abon_tariff %}
<dt>{% trans 'Service' %}</dt>
<dd>
{% if abon_tariff.tariff %}
@ -93,21 +100,29 @@
<dt>{% trans 'Works until' %}</dt>
<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 %}
{% if abon_tariff %}

3
abonapp/urls.py

@ -1,6 +1,6 @@
from django.urls import path, include, re_path
from . import views
from abonapp import views
app_name = 'abonapp'
@ -13,7 +13,6 @@ subscriber_patterns = [
path('addinvoice/', views.add_invoice, name='add_invoice'),
path('pick/', views.pick_tariff, name='pick_tariff'),
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('reset_ip/', views.reset_ip, name='reset_ip'),
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 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
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError, ProgrammingError, transaction, \
OperationalError, DatabaseError
DatabaseError
from django.db.models import Count, Q
from django.http import HttpResponse, HttpResponseBadRequest, \
HttpResponseRedirect
@ -33,7 +33,6 @@ from guardian.shortcuts import get_objects_for_user, assign_perm
from gw_app.models import NASModel
from gw_app.nas_managers import NasFailedResult, NasNetworkError
from ip_pool.models import NetworkModel
from statistics.models import getModel
from tariff_app.models import Tariff
from taskapp.models import Task
from xmlview.decorators import xml_view
@ -52,9 +51,9 @@ class PeoplesListView(LoginRequiredMixin, OnlyAdminsMixin,
if street_id > 0:
peoples_list = peoples_list.filter(street=street_id)
peoples_list = peoples_list.select_related(
'group', 'street', 'statcache', 'current_tariff'
'group', 'street', 'current_tariff'
).only(
'group', 'street', 'statcache', 'fio',
'group', 'street', 'fio',
'street', 'house', 'telephone', 'ballance', 'markers',
'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'))
deadline = request.POST.get('deadline')
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')
abon.pick_tariff(trf, request.user, deadline=deadline,
comment=log_comment)
else:
abon.pick_tariff(trf, request.user, comment=log_comment)
r = abon.nas_sync_self()
if r is None:
messages.success(request, _('Tariff has been picked'))
@ -469,11 +470,15 @@ def pick_tariff(request, gid: int, uname):
except ValueError as 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', {
'tariffs': tariffs,
'abon': abon,
'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)
except lib.LogicError as 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))
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)
@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
@only_admins
@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):
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,
mac=self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.3.%d' % n),
speed=0,
signal=int(signal) / 10 if signal != 'NOSUCHINSTANCE' else 0,
signal=int(signal or 0),
snmp_worker=self)
res.append(onu)
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.2.2.1.8.%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
@ -428,17 +428,30 @@ class ZteOnuDevice(OnuDevice):
try:
fiber_num, onu_num = snmp_extra.split('.')
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 {
'status': status,
'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
def get_template_name(self):
@ -514,3 +527,18 @@ class ZteOnuDevice(OnuDevice):
snmp_fiber_num = int(bin_snmp_fiber_number, base=2)
device.snmp_extra = "%d.%d" % (snmp_fiber_num, new_onu_port_num)
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 ошибка"
#: templates/devapp/custom_dev_page/onu.html:72
#: templates/devapp/custom_dev_page/onu_for_zte.html:75
msgid "Name on OLT"
msgstr "Имя на OLT"
@ -639,20 +638,29 @@ msgstr "Процесс занят другой задачей, подождит
msgid "You have not info in extra_data field, please fill it in JSON"
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):
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]:
mng = self.get_manager_object()
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 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:
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:
continue
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 'Mac' %}: {{ dev.mac_addr }}</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 %}
<li class="list-group-item">{% trans 'Attached user' %}:
{% if da.group %}
@ -72,9 +73,17 @@
</div>
<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>
{% 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>

59
devapp/views.py

@ -4,7 +4,6 @@ from ipaddress import ip_address
from abonapp.models import Abon
from accounts_app.models import UserProfile
from chatbot.models import ChatException
from chatbot.send_func import send_notify
from devapp.base_intr import DeviceImplementationError
from django.conf import settings
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.tln import ZteOltConsoleError, OnuZteRegisterError, \
ZteOltLoginFailed
from djing.tasks import multicast_email_notify
from easysnmp import EasySNMPTimeoutError, EasySNMPError
from group_app.models import Group
from guardian.decorators import \
@ -32,6 +32,7 @@ from guardian.decorators import \
from guardian.shortcuts import get_objects_for_user
from .forms import DeviceForm, PortForm, DeviceExtraDataForm
from .models import Device, Port, DeviceDBException, DeviceMonitoringException
from .tasks import onu_register
class DevicesListView(LoginAdminPermissionMixin,
@ -91,7 +92,9 @@ class DeviceDeleteView(LoginAdminPermissionMixin, DeleteView):
self.object.mac_addr 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:
messages.error(request, e)
messages.success(request, _('Device successfully deleted'))
@ -141,7 +144,9 @@ class DeviceUpdate(LoginAdminPermissionMixin, UpdateView):
r = super().form_valid(form)
# change device info in dhcpd.conf
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'))
except PermissionError as e:
messages.error(self.request, e)
@ -197,13 +202,16 @@ class DeviceCreateView(LoginAdminMixin, PermissionRequiredMixin, CreateView):
r = super().form_valid(form)
# change device info in dhcpd.conf
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'))
except PermissionError as e:
messages.error(self.request, e)
@ -382,6 +390,8 @@ class EditSinglePort(LoginAdminPermissionMixin, UpdateView):
pk_url_kwarg = 'port_id'
permission_required = 'devapp.change_port'
template_name = 'devapp/manage_ports/modal_add_edit_port.html'
model = Port
form_class = PortForm
def dispatch(self, request, *args, **kwargs):
try:
@ -403,7 +413,8 @@ class EditSinglePort(LoginAdminPermissionMixin, UpdateView):
def get_success_url(self):
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):
group_id = self.kwargs.get('group_id')
@ -697,24 +708,18 @@ class OnDeviceMonitoringEvent(global_base_views.SecureApiView):
recipients = UserProfile.objects.get_profiles_by_group(
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 {
'text': 'notification successfully sent',
'recipients': names
'text': 'notification successfully sent'
}
except ChatException as e:
return {

6
djing/__init__.py

@ -1,14 +1,14 @@
import importlib
import os
import re
import importlib
import typing as t
from urllib.parse import unquote
from django.http import HttpResponseRedirect, HttpResponse
from netaddr import mac_unix, mac_eui48
from django.shortcuts import _get_queryset
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})$'

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)
# 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('dev/', include('devapp.urls', namespace='devapp')),
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('client/', include('clientsideapp.urls', namespace='client_side')),
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."
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)
nas_type = models.CharField(_('Type'), max_length=4, choices=MyChoicesAdapter(NAS_TYPES), default=NAS_TYPES[0][0])
default = models.BooleanField(_('Is default'), default=False)
enabled = models.BooleanField(_('Enabled'), default=True)
def get_nas_manager_klass(self):
try:
@ -32,7 +33,8 @@ class NASModel(models.Model):
login=self.auth_login,
password=self.auth_passw,
ip=self.ip_address,
port=int(self.ip_port)
port=int(self.ip_port),
enabled=bool(self.enabled)
)
setattr(self, '_nas_mngr', o)
return o

25
gw_app/nas_managers/mod_mikrotik.py

@ -170,14 +170,17 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
(ABCMeta, LazyInitMetaclass), {})):
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:
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)
self.login(username=login, pwd=password)
except ConnectionRefusedError:
@ -268,9 +271,9 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
# FIXME: тут в разных микротиках или =target-addresses или =target
'=target=%s' % queue.network,
'=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:
@ -304,7 +307,7 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
# FIXME: тут в разных версиях прошивки микротика
# или =target-addresses или =target
'=target=%s' % queue.network,
'=queue=Djing_pcq/Djing_pcq',
'=queue=Djing_pcq_up/Djing_pcq_down',
'=burst-time=1/1'
]
if queue.queue_id:

23
gw_app/templates/gw_app/nasmodel_list.html

@ -18,11 +18,10 @@
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">dasdasd</h3>
<h3 class="panel-title">{{ nas.title }}</h3>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>{% trans 'Title' %}</dt> <dd>{{ nas.title }}</dd>
<dt>{% trans 'Ip address' %}</dt> <dd>{{ nas.ip_address }}</dd>
<dt>{% trans 'Port' %}</dt> <dd>{{ nas.ip_port }}</dd>
<dt>{% trans 'Auth login' %}</dt> <dd>{{ nas.auth_login }}</dd>
@ -32,16 +31,22 @@
<dd>
<input type="checkbox" {{ nas.default|yesno:'checked,' }}>
</dd>
<dt>{% trans 'Enabled' %}</dt>
<dd>
<input type="checkbox" {{ nas.enabled|yesno:'checked,' }}>
</dd>
</dl>
</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>
{% endif %}
</div>
</div>
{% empty %}

69
ip_pool/models.py

@ -1,13 +1,10 @@
from datetime import timedelta
from ipaddress import ip_network, ip_address
from typing import Optional, Generator
from django.conf import settings
from django.db.utils import IntegrityError
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.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djing.fields import MACAddressField
@ -21,7 +18,8 @@ class NetworkModel(models.Model):
network = GenericIpAddressWithPrefix(
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
)
NETWORK_KINDS = (
@ -31,7 +29,10 @@ class NetworkModel(models.Model):
('device', _('Devices')),
('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)
groups = models.ManyToManyField(Group, verbose_name=_('Groups'))
@ -56,33 +57,53 @@ class NetworkModel(models.Model):
def clean(self):
errs = {}
if self.network is None:
errs['network'] = ValidationError(_('Network is invalid'), code='invalid')
errs['network'] = ValidationError(
_('Network is invalid'),
code='invalid'
)
raise ValidationError(errs)
net = self.get_network()
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)
start_ip = ip_address(self.ip_start)
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:
errs['ip_end'] = ValidationError(_('Ip end is invalid'), code='invalid')
errs['ip_end'] = ValidationError(
_('Ip end is invalid'),
code='invalid'
)
raise ValidationError(errs)
end_ip = ip_address(self.ip_end)
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:
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():
return
for onet in other_nets.iterator():
onet_netw = onet.get_network()
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)
def get_scope(self) -> str:
@ -108,7 +129,8 @@ class NetworkModel(models.Model):
def get_free_ip(self, employed_ips: Optional[Generator]):
"""
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
"""
network = self.get_network()
@ -144,7 +166,10 @@ class IpLeaseManager(models.Manager):
netw = network.get_network()
work_range_start_ip = ip_address(network.ip_start)
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():
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:
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)
try:
return self.create(
@ -176,13 +202,6 @@ class IpLeaseManager(models.Manager):
except IntegrityError as 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
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."
msgstr "На сервере произошла ошибка. Пожалуйста свяжитесь с системным администратором."
msgid "Are you sure about them?"
msgstr "Вы уверены в этом?"

115
msg_app/models.py

@ -1,7 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
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
@ -10,17 +10,29 @@ class MessageError(Exception):
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 = (
('new', _('New')),
('old', _('Seen')),
('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):
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:
db_table = 'message_status'
@ -34,10 +46,22 @@ class MessageStatus(models.Model):
class Message(models.Model):
text = models.TextField(_("Body"))
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):
return self.text[:9]
@ -70,7 +94,10 @@ class Message(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)
PARTICIPANT_STATUS = (
('adm', _('Admin')),
@ -78,9 +105,14 @@ class ConversationMembership(models.Model):
('ban', _('Banned user')),
('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):
return "%s < %s" % (self.conversation, self.account)
@ -102,7 +134,9 @@ def id_to_userprofile(acc):
class ConversationManager(models.Manager):
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:
usernames = tuple(acc.username for acc in other_participants)
if not usernames:
@ -112,7 +146,8 @@ class ConversationManager(models.Manager):
conversation = self.create(title=title, author=author)
for acc in other_participants:
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(
@ -123,12 +158,16 @@ class ConversationManager(models.Manager):
@staticmethod
def get_new_messages_count(account):
if isinstance(account, UserProfile):
return MessageStatus.objects.filter(user=account, state='new').count()
return MessageStatus.objects.filter(
user=account, state='new'
).count()
else:
return 0
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)
)
return conversations
@ -136,9 +175,11 @@ class ConversationManager(models.Manager):
class Conversation(models.Model):
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)
date_create = models.DateTimeField(auto_now_add=True)
@ -152,7 +193,9 @@ class Conversation(models.Model):
def get_messages_new_count(self, account):
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):
messages = Message.objects.filter(conversation=self)
@ -162,14 +205,18 @@ class Conversation(models.Model):
def new_message(self, text, attachment, author, with_status=True):
try:
msg = Message.objects.create(
text=text, conversation=self, attachment=attachment, author=author
text=text, conversation=self,
attachment=attachment, author=author
)
if with_status:
for participant in self.participants.all():
if participant == author:
continue
MessageStatus.objects.create(msg=msg, user=participant)
send_notify(msg_text=text, account=participant, tag='msgapp')
send_email_notify.delay(
msg_text=text,
account_id=participant.pk
)
return msg
except ChatException as e:
raise MessageError(e)
@ -188,10 +235,13 @@ class Conversation(models.Model):
def _make_participant_status(self, user, status, cm=None):
if cm is None:
cm = ConversationMembership.objects.get(account=user, conversation=self)
cm = ConversationMembership.objects.get(
account=user, conversation=self
)
else:
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.save(update_fields=('status',))
return cm
@ -210,21 +260,28 @@ class Conversation(models.Model):
def remove_participant(self, user):
try:
cm = ConversationMembership.objects.get(account=user, conversation=self)
cm = ConversationMembership.objects.get(
account=user, conversation=self
)
cm.delete()
except ConversationMembership.DoesNotExist:
pass
def add_participant(self, author, user):
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):
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):
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':
qs = qs.exclude(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)
AbonTariff.objects.filter(abon=None).delete()
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
with transaction.atomic():
@ -49,16 +51,19 @@ def main():
amount=0,
author=None,
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)
expired_services.delete()
# 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
trf = ex.tariff
amount = round(trf.amount, 2)
@ -91,7 +96,23 @@ def main():
)
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
ppays = PeriodicPayForId.objects.filter(next_pay__lt=now) \
@ -102,7 +123,7 @@ def main():
# sync subscribers on GW
threads = tuple(NasSyncThread(nas) for nas in NASModel.objects.
annotate(usercount=Count('abon')).
filter(usercount__gt=0))
filter(usercount__gt=0, enabled=True))
for t in threads:
t.start()
for t in threads:

4
requirements.txt

@ -28,3 +28,7 @@ asterisk
# django-xmlview for pay system allpay
-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;
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-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button></div>';
show_ModalMyContent(cnt);
$('#loading').hide();
}
function showErr(errContent) {show_Modal('Ошибка', errContent, 'warning');}
@ -125,11 +124,9 @@ $(document).ajaxError(function (ev, jqXHR, ajaxSettings, thrownError) {
}, opt);
var fill_block_fn = function(){
$('#loading').show();
var url = $(this).attr('data-href');
$.get(url, function(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]
Type=simple
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]
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
ExecStart=/usr/bin/python3 dialing.py
PIDFile=/run/dialing.py.pid
WorkingDirectory=/srv/http/djing
WorkingDirectory=/var/www/djing
User=http
Group=http

4
systemd_units/djing_telebot.service

@ -8,8 +8,8 @@ PIDFile=/run/djing_telebot.pid
WorkingDirectory=/var/www/djing
TimeoutSec=9
Restart=always
User=http
Group=http
User=www-data
Group=www-data
[Install]
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 -*-
from django.template.loader import render_to_string
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 djing.lib import MultipleException
@ -30,12 +30,9 @@ def handle(task, author, recipients):
if task.state == 'F' or task.state == 'C':
# 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:
send_notify(fulltext, recipient, tag='taskap')
send_email_notify.delay(fulltext, recipient.pk)
except ChatException as e:
errors.append(e)
if len(errors) > 0:

6
templates/all_base.html

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

Loading…
Cancel
Save