diff --git a/abonapp/admin.py b/abonapp/admin.py index 9448164..cda6f48 100644 --- a/abonapp/admin.py +++ b/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) diff --git a/abonapp/locale/ru/LC_MESSAGES/django.po b/abonapp/locale/ru/LC_MESSAGES/django.po index 5425e07..758df1c 100644 --- a/abonapp/locale/ru/LC_MESSAGES/django.po +++ b/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 адрес" diff --git a/abonapp/migrations/0008_auto_20181115_1206.py b/abonapp/migrations/0008_auto_20181115_1206.py new file mode 100644 index 0000000..8fb29e2 --- /dev/null +++ b/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) + ] diff --git a/abonapp/models.py b/abonapp/models.py index 1af42ae..0d063b9 100644 --- a/abonapp/models.py +++ b/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..manage_access() + # is subscriber have access to service, + # view in tariff_app.custom_tariffs..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 diff --git a/abonapp/templates/abonapp/buy_tariff.html b/abonapp/templates/abonapp/buy_tariff.html index fb2d366..98c01d2 100644 --- a/abonapp/templates/abonapp/buy_tariff.html +++ b/abonapp/templates/abonapp/buy_tariff.html @@ -33,7 +33,7 @@ + {% if selected_tariff %} + + {% else %} + + {% endif %} - {% else %} -

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

- {% endif %} -
- - - - -
- - - - - - -{% endblock %} diff --git a/abonapp/templates/abonapp/editAbon.html b/abonapp/templates/abonapp/editAbon.html index 685c246..f7116ed 100644 --- a/abonapp/templates/abonapp/editAbon.html +++ b/abonapp/templates/abonapp/editAbon.html @@ -6,7 +6,10 @@
-

{% trans 'Change subscriber' %}

+

+ {% trans 'Change subscriber' %} + {% trans 'Date joined' %}: {{ abon.birth_day|date:'d E Y' }} +

@@ -99,6 +102,13 @@ {% trans 'Passport information' %} {% endif %} + + {% if perms.abonapp.delete_abon %} + + + {% trans 'Remove subscriber' %} + + {% endif %}
diff --git a/abonapp/templates/abonapp/ext.htm b/abonapp/templates/abonapp/ext.htm index 51ac412..6d45196 100644 --- a/abonapp/templates/abonapp/ext.htm +++ b/abonapp/templates/abonapp/ext.htm @@ -39,11 +39,6 @@ {% trans 'History of tasks' %} - {% url 'abonapp:charts' group.pk abon.username as abtasklog %} - - {% trans 'Charts' %} - - {% url 'abonapp:dials' group.pk abon.username as abdials %} {% trans 'Dialing' %} diff --git a/abonapp/templates/abonapp/peoples.html b/abonapp/templates/abonapp/peoples.html index 843fc3a..a74ca9e 100644 --- a/abonapp/templates/abonapp/peoples.html +++ b/abonapp/templates/abonapp/peoples.html @@ -52,7 +52,11 @@ {% if order_by == 'house' %}{% endif %} {% trans 'Telephone' %} - {% trans 'Service' %} + + + {% trans 'Service' %} + + {% trans 'Balance' %} @@ -71,23 +75,25 @@ {% else %} {% endif %} - {% if human.statcache.is_online %} - - {% else %} - - {% endif %} + + +{# {% if human.statcache.is_online %}#} +{# #} +{# {% else %}#} +{# #} +{# {% endif %}#} - {{ human.username }} + {{ human.username }} - {% 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 %}#} {{ human.ip_address|default_if_none:'—' }} {{ human.fio|default:'—' }} diff --git a/abonapp/templates/abonapp/service.html b/abonapp/templates/abonapp/service.html index a00c3bd..dd1326c 100644 --- a/abonapp/templates/abonapp/service.html +++ b/abonapp/templates/abonapp/service.html @@ -23,10 +23,18 @@ {% with can_ch_trf=perms.tariff_app.change_tariff %} {% for service in services %} - - - + + {% if abon_tariff %} + + + + {% else %} + + + + {% endif %} + {% if can_ch_trf %} {{ service.title }} @@ -62,9 +70,8 @@

{% trans "Subscriber's service" %}

- {% if abon_tariff %} - -
+
+ {% if abon_tariff %}
{% trans 'Service' %}
{% if abon_tariff.tariff %} @@ -93,21 +100,29 @@
{% trans 'Works until' %}
{{ abon_tariff.deadline|date:"d E Y, l H:i" }}
-
+ {% else %} +
{% trans 'Subscriber has no service' %}
+
+ + {% trans 'Buy service' %} + +
+ {% endif %} -
-

- {% trans 'Auto continue service.' %} - -

-

{{ abon_tariff.tariff.descr }}

-
+ {% if abon.last_connected_tariff %} +
{% trans 'Last connected service' %}
+
{{ abon.last_connected_tariff.title }}
+ {% endif %} - {% else %} - {% trans 'Subscriber has no service' %}. - - {% trans 'Buy service' %} - +
{% trans 'Auto continue service.' %}
+
+ + ? +
+
+ + {% if abon_tariff.tariff.descr %} +

{{ abon_tariff.tariff.descr }}

{% endif %} {% if abon_tariff %} diff --git a/abonapp/urls.py b/abonapp/urls.py index c329a84..16092a6 100644 --- a/abonapp/urls.py +++ b/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//', views.unsubscribe_service, name='unsubscribe_service'), diff --git a/abonapp/views.py b/abonapp/views.py index 61b08f3..de135df 100644 --- a/abonapp/views.py +++ b/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') diff --git a/chatbot/email_bot.py b/chatbot/email_bot.py deleted file mode 100644 index ccdde95..0000000 --- a/chatbot/email_bot.py +++ /dev/null @@ -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) diff --git a/chatbot/send_func.py b/chatbot/send_func.py deleted file mode 100644 index 5571f23..0000000 --- a/chatbot/send_func.py +++ /dev/null @@ -1,5 +0,0 @@ -# send via email -from .email_bot import send_notify - -# for Telegram -# from chatbot.telebot import send_notify diff --git a/devapp/base_intr.py b/devapp/base_intr.py index 5157232..bc080b9 100644 --- a/devapp/base_intr.py +++ b/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 diff --git a/devapp/dev_types.py b/devapp/dev_types.py index c57dfde..bed0c93 100644 --- a/devapp/dev_types.py +++ b/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 + ) diff --git a/devapp/locale/ru/LC_MESSAGES/django.po b/devapp/locale/ru/LC_MESSAGES/django.po index 6ab7c45..9cfa2d7 100644 --- a/devapp/locale/ru/LC_MESSAGES/django.po +++ b/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" diff --git a/devapp/models.py b/devapp/models.py index 4180b03..a8ad8d5 100644 --- a/devapp/models.py +++ b/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() diff --git a/devapp/onu_register.py b/devapp/tasks.py similarity index 79% rename from devapp/onu_register.py rename to devapp/tasks.py index c3118b7..b918033 100644 --- a/devapp/onu_register.py +++ b/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 diff --git a/devapp/templates/devapp/custom_dev_page/onu_for_zte.html b/devapp/templates/devapp/custom_dev_page/onu_for_zte.html index 49317c6..f7a0d0f 100644 --- a/devapp/templates/devapp/custom_dev_page/onu_for_zte.html +++ b/devapp/templates/devapp/custom_dev_page/onu_for_zte.html @@ -18,6 +18,7 @@
  • {% trans 'Ip address' %}: {{ dev.ip_address|default:'-' }}
  • {% trans 'Mac' %}: {{ dev.mac_addr }}
  • {% trans 'Description' %}: {{ dev.comment }}
  • +
  • {% trans 'Fiber' %}: {{ dev_manager.get_fiber_str }}
  • {% for da in dev_accs %}
  • {% trans 'Attached user' %}: {% if da.group %} @@ -72,9 +73,17 @@
  • - {% trans 'Name on OLT' %}: {{ onu_details.name }}
    - {% trans 'Distance(m)' %}: {{ onu_details.distance }}
    + {% trans 'Distance(m)' %}: {{ onu_details.distance|default:'-' }}
    {% trans 'Signal' %}: {{ onu_details.signal }}
    + {% if onu_details.ip_addr %} + {% trans 'Ip addr' %}: {{ onu_details.ip_addr }}
    + {% endif %} + {% if onu_details.vlans %} + {% trans 'VLan list' %}: {{ onu_details.vlans }}
    + {% endif %} + {% trans 'Serial' %}: {{ onu_details.serial|default:'-' }}
    + {% trans 'Onu type' %}: {{ onu_details.onu_type|default:'-' }}
    + {% trans 'Name' %}: {{ onu_details.int_name|default:'-' }}
    diff --git a/devapp/views.py b/devapp/views.py index be4e018..8b6a71c 100644 --- a/devapp/views.py +++ b/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 { diff --git a/djing/__init__.py b/djing/__init__.py index 90ca955..e2c9155 100644 --- a/djing/__init__.py +++ b/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})$' diff --git a/djing/celery.py b/djing/celery.py new file mode 100644 index 0000000..aae6b6c --- /dev/null +++ b/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() diff --git a/djing/settings.py b/djing/settings.py index bd79b72..12d6a85 100644 --- a/djing/settings.py +++ b/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' diff --git a/djing/tasks.py b/djing/tasks.py new file mode 100644 index 0000000..6f5a9e3 --- /dev/null +++ b/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) diff --git a/djing/urls.py b/djing/urls.py index d24bd1a..ad7e9da 100644 --- a/djing/urls.py +++ b/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')), diff --git a/docs/tarifs.md b/docs/tarifs.md new file mode 100644 index 0000000..bced512 --- /dev/null +++ b/docs/tarifs.md @@ -0,0 +1,15 @@ +## Услуги и тарифы + +### Автопродление услуги + +Кнопка **автопродление услуги** предназначена для того чтоб абоненту не +приходилось каждый месяц заходить в личный кабинет и подключать себе услугу. +Кнопка моделирует поведение когда абонент пополнил счёт, и сразу начал +пользоваться интернетом. Без этого абоненту нужно покупать услугу для доступа в +интернет каждый раз когда старая услуга закончит своё действие. + +Нас транице абонента галочка находится в блоке *Текущая услуга абонента*. Так +же она доступна на странице абонента в личном кабинете. + +Для того чтоб кнопка стала активной достаточно выставить галочку, и не нужно +сохраняться, состояние кнопки сразу же сохранится навсегда в системе. diff --git a/forward_pay.php b/forward_pay.php deleted file mode 100755 index 90bc3c6..0000000 --- a/forward_pay.php +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env php - 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); - -?> diff --git a/gw_app/locale/ru/LC_MESSAGES/django.po b/gw_app/locale/ru/LC_MESSAGES/django.po index f6f3689..d25f558 100644 --- a/gw_app/locale/ru/LC_MESSAGES/django.po +++ b/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 "Шлюз выключен" diff --git a/gw_app/migrations/0003_nasmodel_enabled.py b/gw_app/migrations/0003_nasmodel_enabled.py new file mode 100644 index 0000000..026d150 --- /dev/null +++ b/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'), + ), + ] diff --git a/gw_app/models.py b/gw_app/models.py index 78793d3..217bb68 100644 --- a/gw_app/models.py +++ b/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 diff --git a/gw_app/nas_managers/mod_mikrotik.py b/gw_app/nas_managers/mod_mikrotik.py index c7b985d..f702544 100644 --- a/gw_app/nas_managers/mod_mikrotik.py +++ b/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: diff --git a/gw_app/templates/gw_app/nasmodel_list.html b/gw_app/templates/gw_app/nasmodel_list.html index af377ed..c641673 100644 --- a/gw_app/templates/gw_app/nasmodel_list.html +++ b/gw_app/templates/gw_app/nasmodel_list.html @@ -18,11 +18,10 @@
    -

    dasdasd

    +

    {{ nas.title }}

    -
    {% trans 'Title' %}
    {{ nas.title }}
    {% trans 'Ip address' %}
    {{ nas.ip_address }}
    {% trans 'Port' %}
    {{ nas.ip_port }}
    {% trans 'Auth login' %}
    {{ nas.auth_login }}
    @@ -32,16 +31,22 @@
    +
    {% trans 'Enabled' %}
    +
    + +
    -
    {% empty %} diff --git a/ip_pool/models.py b/ip_pool/models.py index daf15bd..bc6f771 100644 --- a/ip_pool/models.py +++ b/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): diff --git a/locale/ru/LC_MESSAGES/django.po b/locale/ru/LC_MESSAGES/django.po index 5469e5a..43fb3cd 100644 --- a/locale/ru/LC_MESSAGES/django.po +++ b/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 "Вы уверены в этом?" diff --git a/msg_app/models.py b/msg_app/models.py index 7c06d46..c8f12e9 100644 --- a/msg_app/models.py +++ b/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) diff --git a/periodic.py b/periodic.py index b1963d0..c96d018 100755 --- a/periodic.py +++ b/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: diff --git a/requirements.txt b/requirements.txt index 8c24d4a..c0fa958 100644 --- a/requirements.txt +++ b/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] diff --git a/static/css/custom.css b/static/css/custom.css index 3903f00..08519f7 100644 --- a/static/css/custom.css +++ b/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; -} diff --git a/static/img/loading.gif b/static/img/loading.gif deleted file mode 100644 index 3ca78de..0000000 Binary files a/static/img/loading.gif and /dev/null differ diff --git a/static/js/my.js b/static/js/my.js index 91649f7..82a5def 100644 --- a/static/js/my.js +++ b/static/js/my.js @@ -11,7 +11,6 @@ var cnt='