From b02ade77209aac64edcf378305a1ad78e9ba59e5 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Sat, 3 Nov 2018 10:43:57 +0300 Subject: [PATCH 01/32] fix --- abonapp/locale/ru/LC_MESSAGES/django.po | 3 +++ abonapp/views.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/abonapp/locale/ru/LC_MESSAGES/django.po b/abonapp/locale/ru/LC_MESSAGES/django.po index 5425e07..f30a5f6 100644 --- a/abonapp/locale/ru/LC_MESSAGES/django.po +++ b/abonapp/locale/ru/LC_MESSAGES/django.po @@ -1156,3 +1156,6 @@ msgstr "У пользователя нет ip" msgid "Ip successfully updated" msgstr "IP успешно обновлён" + +msgid "IP address conflict" +msgstr "IP адрес уже есть" diff --git a/abonapp/views.py b/abonapp/views.py index 8b4975d..a91a2b7 100644 --- a/abonapp/views.py +++ b/abonapp/views.py @@ -594,6 +594,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): From 47648226fa0e8f3cbab67908056da09276468f58 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Mon, 5 Nov 2018 12:44:53 +0300 Subject: [PATCH 02/32] Fix date picking --- abonapp/templates/abonapp/buy_tariff.html | 8 ++++++-- abonapp/views.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) 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/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/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 d0b72cf..e731908 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 @@ -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')) @@ -689,56 +690,56 @@ 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('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 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/views.py b/devapp/views.py index 688769c..7781c75 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 \ @@ -700,24 +700,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/ip_pool/models.py b/ip_pool/models.py index daf15bd..a8e267d 100644 --- a/ip_pool/models.py +++ b/ip_pool/models.py @@ -176,13 +176,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/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/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/statistics/__init__.py b/statistics/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/statistics/admin.py b/statistics/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/statistics/apps.py b/statistics/apps.py deleted file mode 100644 index 6770560..0000000 --- a/statistics/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class StatisticsConfig(AppConfig): - name = 'statistics' diff --git a/statistics/fields.py b/statistics/fields.py deleted file mode 100644 index 62f61bf..0000000 --- a/statistics/fields.py +++ /dev/null @@ -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) diff --git a/statistics/migrations/0001_initial.py b/statistics/migrations/0001_initial.py deleted file mode 100644 index 7131ef2..0000000 --- a/statistics/migrations/0001_initial.py +++ /dev/null @@ -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', - }, - ), - ] diff --git a/statistics/migrations/0002_auto_20180808_1236.py b/statistics/migrations/0002_auto_20180808_1236.py deleted file mode 100644 index be359df..0000000 --- a/statistics/migrations/0002_auto_20180808_1236.py +++ /dev/null @@ -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',)}, - ), - ] diff --git a/statistics/migrations/0003_auto_20180814_1921.py b/statistics/migrations/0003_auto_20180814_1921.py deleted file mode 100644 index 9e6c5be..0000000 --- a/statistics/migrations/0003_auto_20180814_1921.py +++ /dev/null @@ -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' - }, - ) - ) - ) diff --git a/statistics/migrations/__init__.py b/statistics/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/statistics/models.py b/statistics/models.py deleted file mode 100644 index 6812a0c..0000000 --- a/statistics/models.py +++ /dev/null @@ -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' diff --git a/statistics/templates/statistics/index.html b/statistics/templates/statistics/index.html deleted file mode 100644 index 0911ad6..0000000 --- a/statistics/templates/statistics/index.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'base.html' %} -{% block main %} - - - -{% endblock %} \ No newline at end of file diff --git a/statistics/urls.py b/statistics/urls.py deleted file mode 100644 index ab0f9d3..0000000 --- a/statistics/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path - -from . import views - -app_name = 'statistics' - -urlpatterns = [ - path('', views.home, name='home'), -] diff --git a/statistics/views.py b/statistics/views.py deleted file mode 100644 index d5eb1b8..0000000 --- a/statistics/views.py +++ /dev/null @@ -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') diff --git a/taskapp/handle.py b/taskapp/handle.py index c8bf45f..a492bc0 100644 --- a/taskapp/handle.py +++ b/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: From 0d434f5a2f14615c038deee11bb576778a2c4d06 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Fri, 16 Nov 2018 14:10:42 +0300 Subject: [PATCH 13/32] Make celery task queue --- abonapp/templates/abonapp/charts.html | 66 --------- abonapp/templates/abonapp/ext.htm | 5 - abonapp/urls.py | 3 +- abonapp/views.py | 71 ++-------- chatbot/email_bot.py | 27 ---- chatbot/send_func.py | 5 - devapp/views.py | 28 ++-- djing/__init__.py | 6 +- djing/celery.py | 9 ++ djing/settings.py | 10 +- djing/tasks.py | 56 ++++++++ djing/urls.py | 2 +- ip_pool/models.py | 7 - msg_app/models.py | 115 +++++++++++---- requirements.txt | 4 + statistics/__init__.py | 0 statistics/admin.py | 0 statistics/apps.py | 5 - statistics/fields.py | 53 ------- statistics/migrations/0001_initial.py | 29 ---- .../migrations/0002_auto_20180808_1236.py | 19 --- .../migrations/0003_auto_20180814_1921.py | 79 ----------- statistics/migrations/__init__.py | 0 statistics/models.py | 132 ------------------ statistics/templates/statistics/index.html | 37 ----- statistics/urls.py | 9 -- statistics/views.py | 9 -- taskapp/handle.py | 9 +- 28 files changed, 190 insertions(+), 605 deletions(-) delete mode 100644 abonapp/templates/abonapp/charts.html delete mode 100644 chatbot/email_bot.py delete mode 100644 chatbot/send_func.py create mode 100644 djing/celery.py create mode 100644 djing/tasks.py delete mode 100644 statistics/__init__.py delete mode 100644 statistics/admin.py delete mode 100644 statistics/apps.py delete mode 100644 statistics/fields.py delete mode 100644 statistics/migrations/0001_initial.py delete mode 100644 statistics/migrations/0002_auto_20180808_1236.py delete mode 100644 statistics/migrations/0003_auto_20180814_1921.py delete mode 100644 statistics/migrations/__init__.py delete mode 100644 statistics/models.py delete mode 100644 statistics/templates/statistics/index.html delete mode 100644 statistics/urls.py delete mode 100644 statistics/views.py diff --git a/abonapp/templates/abonapp/charts.html b/abonapp/templates/abonapp/charts.html deleted file mode 100644 index 2f55c5b..0000000 --- a/abonapp/templates/abonapp/charts.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends request.is_ajax|yesno:'nullcont.htm,abonapp/ext.htm' %} -{% load i18n %} -{% block content %} - -
-
-
-
-

{% blocktrans with wantdate_d=wantdate|date:'j E Y' %}Graph of use by {{ wantdate_d }}{% endblocktrans %}

-
-
- {% if charts_data %} -
- - {% else %} -

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

- {% endif %} -
- - - - -
- -
-
-
-
- -{% endblock %} 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/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 d0b72cf..ef628e4 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 @@ -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')) @@ -689,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/views.py b/devapp/views.py index 688769c..7781c75 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 \ @@ -700,24 +700,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/ip_pool/models.py b/ip_pool/models.py index daf15bd..a8e267d 100644 --- a/ip_pool/models.py +++ b/ip_pool/models.py @@ -176,13 +176,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/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/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/statistics/__init__.py b/statistics/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/statistics/admin.py b/statistics/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/statistics/apps.py b/statistics/apps.py deleted file mode 100644 index 6770560..0000000 --- a/statistics/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class StatisticsConfig(AppConfig): - name = 'statistics' diff --git a/statistics/fields.py b/statistics/fields.py deleted file mode 100644 index 62f61bf..0000000 --- a/statistics/fields.py +++ /dev/null @@ -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) diff --git a/statistics/migrations/0001_initial.py b/statistics/migrations/0001_initial.py deleted file mode 100644 index 7131ef2..0000000 --- a/statistics/migrations/0001_initial.py +++ /dev/null @@ -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', - }, - ), - ] diff --git a/statistics/migrations/0002_auto_20180808_1236.py b/statistics/migrations/0002_auto_20180808_1236.py deleted file mode 100644 index be359df..0000000 --- a/statistics/migrations/0002_auto_20180808_1236.py +++ /dev/null @@ -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',)}, - ), - ] diff --git a/statistics/migrations/0003_auto_20180814_1921.py b/statistics/migrations/0003_auto_20180814_1921.py deleted file mode 100644 index 9e6c5be..0000000 --- a/statistics/migrations/0003_auto_20180814_1921.py +++ /dev/null @@ -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' - }, - ) - ) - ) diff --git a/statistics/migrations/__init__.py b/statistics/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/statistics/models.py b/statistics/models.py deleted file mode 100644 index 6812a0c..0000000 --- a/statistics/models.py +++ /dev/null @@ -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' diff --git a/statistics/templates/statistics/index.html b/statistics/templates/statistics/index.html deleted file mode 100644 index 0911ad6..0000000 --- a/statistics/templates/statistics/index.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'base.html' %} -{% block main %} - - - -{% endblock %} \ No newline at end of file diff --git a/statistics/urls.py b/statistics/urls.py deleted file mode 100644 index ab0f9d3..0000000 --- a/statistics/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path - -from . import views - -app_name = 'statistics' - -urlpatterns = [ - path('', views.home, name='home'), -] diff --git a/statistics/views.py b/statistics/views.py deleted file mode 100644 index d5eb1b8..0000000 --- a/statistics/views.py +++ /dev/null @@ -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') diff --git a/taskapp/handle.py b/taskapp/handle.py index c8bf45f..a492bc0 100644 --- a/taskapp/handle.py +++ b/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: From e4246f073bd49b19a8018017c4d0de5543612d81 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Fri, 16 Nov 2018 14:26:49 +0300 Subject: [PATCH 14/32] Add celery services --- abonapp/templates/abonapp/peoples.html | 26 ++++++++++++++------------ abonapp/views.py | 4 ++-- systemd_units/djing.service | 6 +++--- systemd_units/djing_celery.service | 14 ++++++++++++++ systemd_units/djing_dial.service | 2 +- systemd_units/djing_telebot.service | 4 ++-- 6 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 systemd_units/djing_celery.service diff --git a/abonapp/templates/abonapp/peoples.html b/abonapp/templates/abonapp/peoples.html index 1432448..e5be88d 100644 --- a/abonapp/templates/abonapp/peoples.html +++ b/abonapp/templates/abonapp/peoples.html @@ -75,23 +75,25 @@ {% else %} {% endif %} - {% if human.statcache.is_online %} - - {% else %} - - {% endif %} + + +{# {% if human.statcache.is_online %}#} +{# #} +{# {% else %}#} +{# #} +{# {% endif %}#} {{ 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/views.py b/abonapp/views.py index ef628e4..de135df 100644 --- a/abonapp/views.py +++ b/abonapp/views.py @@ -51,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' ) diff --git a/systemd_units/djing.service b/systemd_units/djing.service index 974cef8..79be6fa 100644 --- a/systemd_units/djing.service +++ b/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 diff --git a/systemd_units/djing_celery.service b/systemd_units/djing_celery.service new file mode 100644 index 0000000..931aa70 --- /dev/null +++ b/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 diff --git a/systemd_units/djing_dial.service b/systemd_units/djing_dial.service index 8f2eab0..d3db17b 100644 --- a/systemd_units/djing_dial.service +++ b/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 diff --git a/systemd_units/djing_telebot.service b/systemd_units/djing_telebot.service index 2afa575..eb0de74 100644 --- a/systemd_units/djing_telebot.service +++ b/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 From 94cdf1fd64479a1f00a54f499fa0044772ce3efe Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Fri, 16 Nov 2018 15:25:52 +0300 Subject: [PATCH 15/32] Add tasks for abonapp --- abonapp/models.py | 34 ++++------------------------------ abonapp/tasks.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 abonapp/tasks.py diff --git a/abonapp/models.py b/abonapp/models.py index 7878895..ecf24f4 100644 --- a/abonapp/models.py +++ b/abonapp/models.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import Optional +from abonapp.tasks import user_remove_from_gw, user_add_to_gw, user_nas_sync from accounts_app.models import UserProfile, MyUserManager, BaseAccount from bitfield import BitField from django.conf import settings @@ -317,16 +318,7 @@ class Abon(BaseAccount): """ if self.nas is None: raise LogicError(_('gateway required')) - try: - agent_abon = self.build_agent_struct() - if agent_abon is not None: - mngr = self.nas.get_nas_manager() - mngr.update_user(agent_abon) - except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: - print('ERROR:', e) - return e - except LogicError: - pass + user_nas_sync.delay(self.pk) def nas_add_self(self): """ @@ -335,16 +327,7 @@ class Abon(BaseAccount): """ if self.nas is None: raise LogicError(_('gateway required')) - try: - agent_abon = self.build_agent_struct() - if agent_abon is not None: - mngr = self.nas.get_nas_manager() - mngr.add_user(agent_abon) - except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: - print('ERROR:', e) - return e - except LogicError: - pass + user_add_to_gw.delay(self.pk) def nas_remove_self(self): """ @@ -353,16 +336,7 @@ class Abon(BaseAccount): """ if self.nas is None: raise LogicError(_('gateway required')) - try: - agent_abon = self.build_agent_struct() - if agent_abon is not None: - mngr = self.nas.get_nas_manager() - mngr.remove_user(agent_abon) - except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: - print('ERROR:', e) - return e - except LogicError: - pass + user_remove_from_gw.delay(self.pk) def get_absolute_url(self): return resolve_url('abonapp:abon_home', self.group.id, self.username) diff --git a/abonapp/tasks.py b/abonapp/tasks.py new file mode 100644 index 0000000..08be9ab --- /dev/null +++ b/abonapp/tasks.py @@ -0,0 +1,46 @@ +from celery import shared_task +from abonapp.models import Abon +from djing.lib import LogicError +from gw_app.nas_managers import NasFailedResult, NasNetworkError + + +@shared_task +def user_remove_from_gw(user_id: int): + try: + user = Abon.objects.get(pk=user_id) + agent_abon = user.build_agent_struct() + if agent_abon is not None: + mngr = user.nas.get_nas_manager() + mngr.remove_user(agent_abon) + except ( + Abon.DoesNotExist, NasFailedResult, + NasNetworkError, ConnectionResetError, LogicError + ): + pass + + +@shared_task +def user_add_to_gw(user_id: int): + try: + user = Abon.objects.get(pk=user_id) + agent_abon = user.build_agent_struct() + if agent_abon is not None: + mngr = user.nas.get_nas_manager() + mngr.add_user(agent_abon) + except ( + Abon.DoesNotExist, NasFailedResult, + NasNetworkError, ConnectionResetError, LogicError + ): + pass + + +@shared_task +def user_nas_sync(user_id: int): + try: + user = Abon.objects.get(pk=user_id) + agent_abon = user.build_agent_struct() + if agent_abon is not None: + mngr = user.nas.get_nas_manager() + mngr.update_user(agent_abon) + except (NasFailedResult, NasNetworkError, ConnectionResetError, LogicError) as e: + return 'ERROR:%s' % e From 61620815e8c528873f841eea69b7dcfeff607221 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Fri, 16 Nov 2018 15:50:53 +0300 Subject: [PATCH 16/32] Revert "Add tasks for abonapp" This reverts commit 94cdf1fd64479a1f00a54f499fa0044772ce3efe. Bad commit. Cirkullar dependency --- abonapp/models.py | 34 ++++++++++++++++++++++++++++++---- abonapp/tasks.py | 46 ---------------------------------------------- 2 files changed, 30 insertions(+), 50 deletions(-) delete mode 100644 abonapp/tasks.py diff --git a/abonapp/models.py b/abonapp/models.py index ecf24f4..7878895 100644 --- a/abonapp/models.py +++ b/abonapp/models.py @@ -1,7 +1,6 @@ from datetime import datetime from typing import Optional -from abonapp.tasks import user_remove_from_gw, user_add_to_gw, user_nas_sync from accounts_app.models import UserProfile, MyUserManager, BaseAccount from bitfield import BitField from django.conf import settings @@ -318,7 +317,16 @@ class Abon(BaseAccount): """ if self.nas is None: raise LogicError(_('gateway required')) - user_nas_sync.delay(self.pk) + try: + agent_abon = self.build_agent_struct() + if agent_abon is not None: + mngr = self.nas.get_nas_manager() + mngr.update_user(agent_abon) + except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: + print('ERROR:', e) + return e + except LogicError: + pass def nas_add_self(self): """ @@ -327,7 +335,16 @@ class Abon(BaseAccount): """ if self.nas is None: raise LogicError(_('gateway required')) - user_add_to_gw.delay(self.pk) + try: + agent_abon = self.build_agent_struct() + if agent_abon is not None: + mngr = self.nas.get_nas_manager() + mngr.add_user(agent_abon) + except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: + print('ERROR:', e) + return e + except LogicError: + pass def nas_remove_self(self): """ @@ -336,7 +353,16 @@ class Abon(BaseAccount): """ if self.nas is None: raise LogicError(_('gateway required')) - user_remove_from_gw.delay(self.pk) + try: + agent_abon = self.build_agent_struct() + if agent_abon is not None: + mngr = self.nas.get_nas_manager() + mngr.remove_user(agent_abon) + except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: + print('ERROR:', e) + return e + except LogicError: + pass def get_absolute_url(self): return resolve_url('abonapp:abon_home', self.group.id, self.username) diff --git a/abonapp/tasks.py b/abonapp/tasks.py deleted file mode 100644 index 08be9ab..0000000 --- a/abonapp/tasks.py +++ /dev/null @@ -1,46 +0,0 @@ -from celery import shared_task -from abonapp.models import Abon -from djing.lib import LogicError -from gw_app.nas_managers import NasFailedResult, NasNetworkError - - -@shared_task -def user_remove_from_gw(user_id: int): - try: - user = Abon.objects.get(pk=user_id) - agent_abon = user.build_agent_struct() - if agent_abon is not None: - mngr = user.nas.get_nas_manager() - mngr.remove_user(agent_abon) - except ( - Abon.DoesNotExist, NasFailedResult, - NasNetworkError, ConnectionResetError, LogicError - ): - pass - - -@shared_task -def user_add_to_gw(user_id: int): - try: - user = Abon.objects.get(pk=user_id) - agent_abon = user.build_agent_struct() - if agent_abon is not None: - mngr = user.nas.get_nas_manager() - mngr.add_user(agent_abon) - except ( - Abon.DoesNotExist, NasFailedResult, - NasNetworkError, ConnectionResetError, LogicError - ): - pass - - -@shared_task -def user_nas_sync(user_id: int): - try: - user = Abon.objects.get(pk=user_id) - agent_abon = user.build_agent_struct() - if agent_abon is not None: - mngr = user.nas.get_nas_manager() - mngr.update_user(agent_abon) - except (NasFailedResult, NasNetworkError, ConnectionResetError, LogicError) as e: - return 'ERROR:%s' % e From 8a525bfd7369ee38ad1c5c4d37e7402486bdb972 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Fri, 16 Nov 2018 16:06:55 +0300 Subject: [PATCH 17/32] fix --- periodic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/periodic.py b/periodic.py index f239c2c..2fd3806 100755 --- a/periodic.py +++ b/periodic.py @@ -72,8 +72,7 @@ def main(): abon.ballance -= amount ex.time_start = now ex.deadline = None # Deadline sets automatically in signal pre_save - ex.is_active = True - ex.save(update_fields=('time_start', 'deadline', 'is_active')) + ex.save(update_fields=('time_start', 'deadline')) abon.save(update_fields=('ballance',)) # make log about it l = AbonLog.objects.create( From 0834ebc565e05e8560b9e1117a2ad2b7347a8073 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Fri, 16 Nov 2018 16:39:07 +0300 Subject: [PATCH 18/32] Add devapp task --- devapp/models.py | 7 ------- devapp/{onu_register.py => tasks.py} | 9 ++++++--- devapp/views.py | 26 +++++++++++++++++--------- 3 files changed, 23 insertions(+), 19 deletions(-) rename devapp/{onu_register.py => tasks.py} (79%) 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/views.py b/devapp/views.py index 7781c75..8b6a71c 100644 --- a/devapp/views.py +++ b/devapp/views.py @@ -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) From 466027712ebb8b3935d1fac51efd9851838a98b7 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Tue, 20 Nov 2018 15:36:01 +0300 Subject: [PATCH 19/32] Reformat code to PEP8 --- ip_pool/models.py | 62 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/ip_pool/models.py b/ip_pool/models.py index a8e267d..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( From deef97fca6812ffd05e2bc0172751920d5a6b416 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Wed, 21 Nov 2018 10:00:38 +0300 Subject: [PATCH 20/32] remove loader from frontend --- static/css/custom.css | 8 -------- static/img/loading.gif | Bin 6808 -> 0 bytes static/js/my.js | 3 --- templates/all_base.html | 6 ------ 4 files changed, 17 deletions(-) delete mode 100644 static/img/loading.gif 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 3ca78dee2132ea98c3957481855aab1a182bed91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6808 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 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='