67 changed files with 1115 additions and 885 deletions
-
8abonapp/forms.py
-
4abonapp/migrations/0001_initial.py
-
2abonapp/models.py
-
2abonapp/pay_systems.py
-
22abonapp/templates/abonapp/abon_confirm_delete.html
-
106abonapp/views.py
-
18accounts_app/views.py
-
11agent/mod_mikrotik.py
-
34agent/structs.py
-
2clientsideapp/views.py
-
15devapp/base_intr.py
-
92devapp/dev_types.py
-
18devapp/forms.py
-
424devapp/locale/ru/LC_MESSAGES/django.po
-
4devapp/migrations/0001_initial.py
-
4devapp/migrations/0002_auto_20180409_1318.py
-
60devapp/migrations/0003_auto_20180529_1311.py
-
17devapp/models.py
-
2devapp/templates/devapp/add_dev.html
-
2devapp/templates/devapp/custom_dev_page/olt_ztec320_ports.html
-
19devapp/templates/devapp/custom_dev_page/onu.html
-
91devapp/templates/devapp/custom_dev_page/onu_for_zte.html
-
4devapp/templates/devapp/dev.html
-
14devapp/templates/devapp/device_confirm_delete.html
-
4devapp/templates/devapp/devices.html
-
4devapp/templates/devapp/devices_null_group.html
-
2devapp/urls.py
-
49devapp/views.py
-
2dialing.py
-
3dialing_app/views.py
-
70djing/lib/__init__.py
-
0djing/lib/auth_backends.py
-
35djing/lib/decorators.py
-
0djing/lib/messaging/__init__.py
-
7djing/lib/messaging/sms/__init__.py
-
0djing/lib/messaging/sms/base.py
-
0djing/lib/messaging/sms/consts.py
-
10djing/lib/messaging/sms/deliver.py
-
0djing/lib/messaging/sms/gsm0338.py
-
0djing/lib/messaging/sms/pdu.py
-
18djing/lib/messaging/sms/submit.py
-
0djing/lib/messaging/sms/udh.py
-
2djing/lib/messaging/sms/wap.py
-
0djing/lib/messaging/utils.py
-
4djing/lib/tln/__init__.py
-
266djing/lib/tln/tln.py
-
2djing/settings.py
-
2docs/dev.md
-
25group_app/templates/group_app/group_confirm_delete.html
-
2mapapp/views.py
-
7messaging/sms/__init__.py
-
359migrate_to_0.2.py
-
2periodic.py
-
18static/css/custom.css
-
5statistics/migrations/0001_initial.py
-
2statistics/models.py
-
4tariff_app/base_intr.py
-
3tariff_app/locale/ru/LC_MESSAGES/django.po
-
2tariff_app/models.py
-
30tariff_app/templates/tariff_app/modal_del_warning.html
-
15tariff_app/templates/tariff_app/tariff_confirm_delete.html
-
2tariff_app/urls.py
-
38tariff_app/views.py
-
2taskapp/handle.py
-
2taskapp/migrations/0001_initial.py
-
8taskapp/views.py
-
19templates/base_delete_modal.html
@ -1,18 +1,10 @@ |
|||||
|
{% extends 'base_delete_modal.html' %} |
||||
{% load i18n %} |
{% load i18n %} |
||||
<form role="form" action="{% url 'abonapp:del_abon' abon.group.pk abon.username %}" method="post">{% csrf_token %} |
|
||||
<div class="modal-header warning"> |
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
|
||||
<h4 class="modal-title"><span class="glyphicon glyphicon-earphone"></span>{% trans 'Remove subscriber' %}</h4> |
|
||||
</div> |
|
||||
|
|
||||
<div class="modal-body"> |
|
||||
|
{% block modal_form_url %} |
||||
|
{% url 'abonapp:del_abon' abon.group.pk abon.username %} |
||||
|
{% endblock %} |
||||
|
|
||||
<h4>{% trans 'Are you sure about them?' %}</h4> |
|
||||
|
|
||||
<button type="submit" class="btn btn-danger" value="DELETE"> |
|
||||
<span class="glyphicon glyphicon-remove"></span> {% trans 'Delete' %} |
|
||||
</button> |
|
||||
|
|
||||
</div> |
|
||||
|
|
||||
</form> |
|
||||
|
{% block modal_form_title %} |
||||
|
{% trans 'Remove subscriber' %} |
||||
|
{% endblock %} |
||||
@ -0,0 +1,60 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Generated by Django 1.11 on 2018-05-29 13:11 |
||||
|
from __future__ import unicode_literals |
||||
|
import os |
||||
|
from json import load |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
from django.core import serializers |
||||
|
|
||||
|
TMP_FILE = '/tmp/djing_snmp_info_backup.json' |
||||
|
|
||||
|
|
||||
|
def snmp_backup_info(apps, _): |
||||
|
Device = apps.get_model('devapp', 'Device') |
||||
|
obs = Device.objects.only('snmp_item_num') |
||||
|
with open(TMP_FILE, 'w') as f: |
||||
|
serializers.serialize('json', obs, stream=f) |
||||
|
|
||||
|
|
||||
|
def snmp_restore_info_to_new_scheme(apps, _): |
||||
|
Device = apps.get_model('devapp', 'Device') |
||||
|
with open(TMP_FILE, 'r') as f: |
||||
|
for device in load(f): |
||||
|
Device.objects.filter(pk=device['pk']).update(snmp_extra=device['fields']['snmp_item_num']) |
||||
|
if os.path.isfile(TMP_FILE): |
||||
|
os.remove(TMP_FILE) |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('devapp', '0002_auto_20180409_1318'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.RunPython(snmp_backup_info), |
||||
|
migrations.AlterModelOptions( |
||||
|
name='device', |
||||
|
options={'ordering': ('id',), 'permissions': (('can_view_device', 'Can view device'),), 'verbose_name': 'Device', 'verbose_name_plural': 'Devices'}, |
||||
|
), |
||||
|
migrations.AlterModelOptions( |
||||
|
name='port', |
||||
|
options={'ordering': ('num',), 'permissions': (('can_toggle_ports', 'Can toggle ports'),), 'verbose_name': 'Port', 'verbose_name_plural': 'Ports'}, |
||||
|
), |
||||
|
migrations.RemoveField( |
||||
|
model_name='device', |
||||
|
name='snmp_item_num', |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='device', |
||||
|
name='snmp_extra', |
||||
|
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='SNMP extra info'), |
||||
|
), |
||||
|
migrations.AlterField( |
||||
|
model_name='device', |
||||
|
name='devtype', |
||||
|
field=models.CharField(choices=[('Dl', 'DLink switch'), ('Pn', 'PON OLT'), ('On', 'PON ONU'), ('Ex', 'Eltex switch'), ('Zt', 'OLT ZTE C320'), ('Zo', 'ZTE PON ONU')], default='Dl', max_length=2, verbose_name='Device type'), |
||||
|
), |
||||
|
migrations.RunPython(snmp_restore_info_to_new_scheme) |
||||
|
] |
||||
@ -0,0 +1,91 @@ |
|||||
|
{% extends request.is_ajax|yesno:'nullcont.htm,devapp/ext.htm' %} |
||||
|
{% load i18n %} |
||||
|
{% block content %} |
||||
|
|
||||
|
{% with uptime=dev_manager.uptime onu_details=dev_manager.get_details %} |
||||
|
<div class="row"> |
||||
|
<div class="col-xs-12 col-sm-6"> |
||||
|
<div class="panel panel-default"> |
||||
|
<div class="panel-heading"> |
||||
|
<div class="panel-title">{{ dev.get_devtype_display|default:_('Title of the type of switch') }}. |
||||
|
{% if uptime %} |
||||
|
{% trans 'Uptime' %} {{ uptime }} |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="panel-body"> |
||||
|
<ul class="list-group"> |
||||
|
<li class="list-group-item">{% trans 'Ip address' %}: {{ dev.ip_address|default:'-' }}</li> |
||||
|
<li class="list-group-item">{% trans 'Mac' %}: {{ dev.mac_addr }}</li> |
||||
|
<li class="list-group-item">{% trans 'Description' %}: {{ dev.comment }}</li> |
||||
|
{% for da in dev_accs %} |
||||
|
<li class="list-group-item">{% trans 'Attached user' %}: |
||||
|
{% if da.group %} |
||||
|
<a href="{% url 'abonapp:abon_home' da.group.pk da.username %}" |
||||
|
target="_blank">{{ da.get_full_name }}</a> |
||||
|
{% else %} |
||||
|
{{ da.get_full_name }} |
||||
|
{% endif %} |
||||
|
</li> |
||||
|
{% endfor %} |
||||
|
{% if dev.parent_dev %} |
||||
|
<li class="list-group-item"> |
||||
|
{% with pdev=dev.parent_dev pdgrp=dev.parent_dev.group %} |
||||
|
{% trans 'Parent device' %}: |
||||
|
<a href="{% url 'devapp:view' pdgrp.pk pdev.pk %}" |
||||
|
title="{{ pdev.mac_addr|default:'' }}" |
||||
|
target="_blank"> |
||||
|
{{ pdev.ip_address|default:'-' }} {{ pdev.comment }} |
||||
|
</a> |
||||
|
{% endwith %} |
||||
|
</li> |
||||
|
{% endif %} |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-xs-12 col-sm-6"> |
||||
|
<div class="panel panel-default"> |
||||
|
<div class="panel-heading"> |
||||
|
<h3 class="panel-title">{% trans 'ONU Status' %}</h3> |
||||
|
</div> |
||||
|
|
||||
|
<div class="panel-body"> |
||||
|
{% if onu_details %} |
||||
|
{% if onu_details.err %} |
||||
|
<div class="media"> |
||||
|
<div class="media-left"><span class="media-object glyphicon glyphicon-remove-sign text-danger font-extra-large"></span></div> |
||||
|
</div> |
||||
|
<div class="media-body"> |
||||
|
<b>{% trans 'ONU error' %}</b>: {{ onu_details.err }}<br> |
||||
|
</div> |
||||
|
{% else %} |
||||
|
<div class="media"> |
||||
|
<div class="media-left font-extra-large"> |
||||
|
{% if onu_details.status == '1' %} |
||||
|
<span class="media-object glyphicon glyphicon-ok-sign text-success"></span> |
||||
|
{% elif onu_details.status == '2' %} |
||||
|
<span class="media-object glyphicon glyphicon-remove-sign text-danger"></span> |
||||
|
{% else %} |
||||
|
<span class="media-object glyphicon glyphicon-question-sign"></span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="media-body"> |
||||
|
|
||||
|
<b>{% trans 'Name on OLT' %}</b>: {{ onu_details.name }}<br> |
||||
|
<b>{% trans 'Distance(m)' %}</b>: {{ onu_details.distance }}<br> |
||||
|
<b>{% trans 'Signal' %}</b>: {{ onu_details.signal }}<br> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
{% else %} |
||||
|
<h3>{% trans 'Info does not fetch' %}</h3> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{% endwith %} |
||||
|
{% endblock %} |
||||
@ -0,0 +1,14 @@ |
|||||
|
{% extends 'base_delete_modal.html' %} |
||||
|
{% load i18n %} |
||||
|
|
||||
|
{% block modal_form_url %} |
||||
|
{% url 'devapp:del' object.group.pk object.pk %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block modal_form_title %} |
||||
|
{% trans 'Remove device' %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block modal_form_text %} |
||||
|
<h4>{% trans 'Are you sure you want to delete device?' %}</h4> |
||||
|
{% endblock %} |
||||
@ -0,0 +1,35 @@ |
|||||
|
from functools import wraps |
||||
|
from django.conf import settings |
||||
|
from django.http import HttpResponseRedirect |
||||
|
from django.shortcuts import redirect |
||||
|
|
||||
|
|
||||
|
DEBUG = getattr(settings, 'DEBUG', False) |
||||
|
|
||||
|
|
||||
|
def require_ssl(view): |
||||
|
""" |
||||
|
Decorator that requires an SSL connection. If the current connection is not SSL, we redirect to the SSL version of |
||||
|
the page. |
||||
|
from: https://gist.github.com/ckinsey/9709984 |
||||
|
""" |
||||
|
|
||||
|
@wraps(view) |
||||
|
def wrapper(request, *args, **kwargs): |
||||
|
if not DEBUG and not request.is_secure(): |
||||
|
target_url = "https://" + request.META['HTTP_HOST'] + request.path_info |
||||
|
return HttpResponseRedirect(target_url) |
||||
|
return view(request, *args, **kwargs) |
||||
|
|
||||
|
return wrapper |
||||
|
|
||||
|
|
||||
|
# Allow to view only admins |
||||
|
def only_admins(fn): |
||||
|
@wraps(fn) |
||||
|
def wrapped(request, *args, **kwargs): |
||||
|
if request.user.is_admin: |
||||
|
return fn(request, *args, **kwargs) |
||||
|
else: |
||||
|
return redirect('client_side:home') |
||||
|
return wrapped |
||||
@ -0,0 +1,7 @@ |
|||||
|
# See LICENSE |
||||
|
|
||||
|
from djing.lib.messaging.sms.deliver import SmsDeliver |
||||
|
from djing.lib.messaging.sms.submit import SmsSubmit |
||||
|
from djing.lib.messaging.sms.gsm0338 import is_gsm_text |
||||
|
|
||||
|
__all__ = ("SmsSubmit", "SmsDeliver", "is_gsm_text") |
||||
@ -0,0 +1,4 @@ |
|||||
|
from .tln import * |
||||
|
|
||||
|
__all__ = ('TelnetApi', 'ValidationError', 'ZTEFiberNumberNotFound', 'ZTEFiberIsFull', |
||||
|
'OnuZteRegisterError', 'ZteOltConsoleError', 'register_onu_ZTE_F660') |
||||
@ -0,0 +1,266 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
import re |
||||
|
import struct |
||||
|
from telnetlib import Telnet |
||||
|
from time import sleep |
||||
|
from typing import Generator, Dict, Optional, Tuple |
||||
|
|
||||
|
|
||||
|
class ZteOltConsoleError(Exception): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
class OnuZteRegisterError(ZteOltConsoleError): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
class ZTEFiberIsFull(ZteOltConsoleError): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
class ZTEFiberNumberNotFound(ZteOltConsoleError): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
class ValidationError(ValueError): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
MAC_ADDR_REGEX = b'^([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]{1,2})$' |
||||
|
IP_ADDR_REGEX = '^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' \ |
||||
|
'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' \ |
||||
|
'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' \ |
||||
|
'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' |
||||
|
ONU_SN_REGEX = b'^ZTEG[A-F\d]{8}$' |
||||
|
|
||||
|
|
||||
|
class TelnetApi(Telnet): |
||||
|
config_level = [] |
||||
|
|
||||
|
def __init__(self, *args, **kwargs): |
||||
|
timeout = kwargs.get('timeout') |
||||
|
if timeout: |
||||
|
self._timeout = timeout |
||||
|
self._prompt_string = b'ZTE-C320-PKP#' |
||||
|
super().__init__(*args, **kwargs) |
||||
|
|
||||
|
def write(self, buffer: bytes) -> None: |
||||
|
buffer = buffer + b'\n' |
||||
|
print('>>', buffer) |
||||
|
super().write(buffer) |
||||
|
|
||||
|
def resize_screen(self, width: int, height: int): |
||||
|
naws_cmd = struct.pack('>BBBHHBB', |
||||
|
255, 250, 31, # IAC SB NAWS |
||||
|
width, height, |
||||
|
255, 240 # IAC SE |
||||
|
) |
||||
|
sock = self.get_socket() |
||||
|
sock.send(naws_cmd) |
||||
|
|
||||
|
def enter(self, username: bytes, passw: bytes) -> None: |
||||
|
self.read_until(b'Username:') |
||||
|
self.write(username) |
||||
|
self.read_until(b'Password:') |
||||
|
self.write(passw) |
||||
|
|
||||
|
def read_lines(self) -> Generator: |
||||
|
while True: |
||||
|
line = self.read_until(b'\r\n', timeout=self._timeout) |
||||
|
line = line.replace(b'\r\n', b'') |
||||
|
if self._prompt_string == line: |
||||
|
break |
||||
|
if line == b'': |
||||
|
continue |
||||
|
yield line |
||||
|
|
||||
|
def command_to(self, cmd: bytes) -> Generator: |
||||
|
self.write(cmd) |
||||
|
return self.read_lines() |
||||
|
|
||||
|
def set_prompt_string(self, prompt_string: bytes) -> None: |
||||
|
self.config_level.append(prompt_string) |
||||
|
self._prompt_string = prompt_string |
||||
|
|
||||
|
def level_exit(self) -> Optional[Tuple]: |
||||
|
if len(self.config_level) < 2: |
||||
|
print('We are in root') |
||||
|
return |
||||
|
self.config_level.pop() |
||||
|
self.set_prompt_string(self.config_level[-1]) |
||||
|
return tuple(self.command_to(b'exit')) |
||||
|
|
||||
|
def __del__(self): |
||||
|
if self.sock: |
||||
|
self.write(b'exit') |
||||
|
super().__del__() |
||||
|
|
||||
|
|
||||
|
def parse_onu_name(onu_name: bytes, name_regexp=re.compile(b'[/:_]')) -> Dict[str, bytes]: |
||||
|
gpon_onu, stack_num, rack_num, fiber_num, onu_num = name_regexp.split(onu_name) |
||||
|
return { |
||||
|
'stack_num': stack_num, |
||||
|
'rack_num': rack_num, |
||||
|
'fiber_num': fiber_num, |
||||
|
'onu_num': onu_num |
||||
|
} |
||||
|
|
||||
|
|
||||
|
class OltZTERegister(TelnetApi): |
||||
|
|
||||
|
def __init__(self, screen_size: Tuple[int, int], *args, **kwargs): |
||||
|
super().__init__(*args, **kwargs) |
||||
|
self.resize_screen(*screen_size) |
||||
|
|
||||
|
def get_unregistered_onu(self, sn: bytes) -> Optional[Dict]: |
||||
|
lines = tuple(self.command_to(b'show gpon onu uncfg')) |
||||
|
if len(lines) > 3: |
||||
|
# devices available |
||||
|
# find onu by sn |
||||
|
line = tuple(ln for ln in lines if sn.lower() in ln.lower()) |
||||
|
if len(line) > 0: |
||||
|
line = line[0] |
||||
|
onu_name, onu_sn, onu_state = line.split() |
||||
|
onu_numbers = parse_onu_name(onu_name) |
||||
|
onu_numbers.update({ |
||||
|
'onu_name': onu_name, |
||||
|
'onu_sn': onu_sn, |
||||
|
'onu_state': onu_state |
||||
|
}) |
||||
|
return onu_numbers |
||||
|
|
||||
|
def get_last_registered_onu_number(self, stack_num: int, rack_num: int, fiber_num: int) -> int: |
||||
|
registered_lines = self.command_to(b'show run int gpon-olt_%d/%d/%d' % ( |
||||
|
stack_num, |
||||
|
rack_num, |
||||
|
fiber_num |
||||
|
)) |
||||
|
onu_type_regexp = re.compile(b'^\s{2}onu \d{1,3} type [-\w\d]{4,64} sn \w{4,64}$') |
||||
|
last_onu = 0 |
||||
|
for rl in registered_lines: |
||||
|
if rl == b' --More--': |
||||
|
self.write(b' ') |
||||
|
if onu_type_regexp.match(rl): |
||||
|
_onu, num, _type, onu_type, _sn, onu_sn = rl.split() |
||||
|
last_onu = int(num) |
||||
|
return last_onu |
||||
|
|
||||
|
def enter_to_config_mode(self) -> bool: |
||||
|
prompt = b'ZTE-C320-PKP(config)#' |
||||
|
self.set_prompt_string(prompt) |
||||
|
res = tuple(self.command_to(b'config terminal')) |
||||
|
if res[1].startswith(b'Enter configuration commands'): |
||||
|
# ok, we in the config mode |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
def go_to_olt_interface(self, stack_num: int, rack_num: int, fiber_num: int) -> Tuple: |
||||
|
self.set_prompt_string(b'ZTE-C320-PKP(config-if)#') |
||||
|
return tuple(self.command_to(b'interface gpon-olt_%d/%d/%d' % ( |
||||
|
stack_num, |
||||
|
rack_num, |
||||
|
fiber_num |
||||
|
))) |
||||
|
|
||||
|
def go_to_onu_interface(self, stack_num: int, rack_num: int, fiber_num: int, onu_port_num: int) -> Tuple: |
||||
|
self.set_prompt_string(b'ZTE-C320-PKP(config-if)#') |
||||
|
return tuple(self.command_to(b'interface gpon-onu_%d/%d/%d:%d' % ( |
||||
|
stack_num, |
||||
|
rack_num, |
||||
|
fiber_num, |
||||
|
onu_port_num |
||||
|
))) |
||||
|
|
||||
|
def apply_conf_to_onu(self, mac_addr: bytes, vlan_id: int) -> None: |
||||
|
tmpl = ( |
||||
|
b'switchport vlan %d tag vport 1' % vlan_id, |
||||
|
b'port-location format flexible-syntax vport 1', |
||||
|
b'port-location sub-option remote-id enable vport 1', |
||||
|
b'port-location sub-option remote-id name %s vport 1' % mac_addr, |
||||
|
b'dhcp-option82 enable vport 1', |
||||
|
b'dhcp-option82 trust true replace vport 1', |
||||
|
b'ip dhcp snooping enable vport 1' |
||||
|
) |
||||
|
for conf_line in tmpl: |
||||
|
self.write(conf_line) |
||||
|
|
||||
|
def register_onu_on_olt_fiber(self, onu_type: bytes, new_onu_num: int, onu_sn: bytes, line_profile: bytes, |
||||
|
remote_profile: bytes) -> Tuple: |
||||
|
# ok, we in interface |
||||
|
tpl = b'onu %d type %s sn %s' % (new_onu_num, onu_type, onu_sn) |
||||
|
r = tuple(self.command_to(tpl)) |
||||
|
return tuple(self.command_to(b'onu %d profile line %s remote %s' % ( |
||||
|
new_onu_num, |
||||
|
line_profile, |
||||
|
remote_profile |
||||
|
))) + r |
||||
|
|
||||
|
|
||||
|
def register_onu_ZTE_F660(olt_ip: str, onu_sn: bytes, login_passwd: Tuple[bytes, bytes], onu_mac: bytes): |
||||
|
onu_type = b'ZTE-F660' |
||||
|
line_profile = b'ZTE-F660-LINE' |
||||
|
remote_profile = b'ZTE-F660-ROUTER' |
||||
|
if not re.match(MAC_ADDR_REGEX, onu_mac): |
||||
|
raise ValidationError |
||||
|
if not re.match(IP_ADDR_REGEX, olt_ip): |
||||
|
raise ValidationError |
||||
|
if not re.match(ONU_SN_REGEX, onu_sn): |
||||
|
raise ValidationError |
||||
|
|
||||
|
tn = OltZTERegister(host=olt_ip, timeout=2, screen_size=(120, 128)) |
||||
|
tn.enter(*login_passwd) |
||||
|
|
||||
|
unregistered_onu = tn.get_unregistered_onu(onu_sn) |
||||
|
if unregistered_onu is None: |
||||
|
raise OnuZteRegisterError('unregistered onu not found, sn=%s' % onu_sn.decode('utf-8')) |
||||
|
|
||||
|
stack_num = int(unregistered_onu['stack_num']) |
||||
|
rack_num = int(unregistered_onu['rack_num']) |
||||
|
fiber_num = int(unregistered_onu['fiber_num']) |
||||
|
|
||||
|
last_onu_number = tn.get_last_registered_onu_number( |
||||
|
stack_num, rack_num, fiber_num |
||||
|
) |
||||
|
|
||||
|
if last_onu_number < 1: |
||||
|
raise ZTEFiberNumberNotFound |
||||
|
elif last_onu_number > 126: |
||||
|
raise ZTEFiberIsFull('olt fiber %d is full' % fiber_num) |
||||
|
|
||||
|
# enter to config |
||||
|
if not tn.enter_to_config_mode(): |
||||
|
raise ZteOltConsoleError('Failed to enter to config mode') |
||||
|
|
||||
|
# go to olt interface |
||||
|
if not tn.go_to_olt_interface(stack_num, rack_num, fiber_num): |
||||
|
raise ZteOltConsoleError('Failed to enter in olt fiber port') |
||||
|
|
||||
|
# new onu port number |
||||
|
new_onu_port_num = last_onu_number + 1 |
||||
|
|
||||
|
# register onu on olt interface |
||||
|
r = tn.register_onu_on_olt_fiber(onu_type, new_onu_port_num, onu_sn, line_profile, remote_profile) |
||||
|
print(r) |
||||
|
|
||||
|
# exit from olt interface |
||||
|
tn.level_exit() |
||||
|
|
||||
|
r = tn.go_to_onu_interface(stack_num, rack_num, fiber_num, new_onu_port_num) |
||||
|
print(r) |
||||
|
|
||||
|
tn.apply_conf_to_onu(onu_mac, 145) |
||||
|
sleep(1) |
||||
|
return |
||||
|
|
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
ip = '10.40.1.10' |
||||
|
try: |
||||
|
register_onu_ZTE_F660( |
||||
|
olt_ip=ip, onu_sn=b'ZTEGC0458DCE', login_passwd=(b'admin', b'2ekc3'), |
||||
|
onu_mac=b'cc:7b:35:8b:7:0' |
||||
|
) |
||||
|
except ZteOltConsoleError as e: |
||||
|
print(e) |
||||
|
except ConnectionRefusedError: |
||||
|
print('ERROR: connection refused', ip) |
||||
@ -1,13 +1,14 @@ |
|||||
|
{% extends 'base_delete_modal.html' %} |
||||
{% load i18n %} |
{% load i18n %} |
||||
<form role="form" action="{% url 'group_app:del' object.pk %}" method="post">{% csrf_token %} |
|
||||
<div class="modal-header warning"> |
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
|
||||
<h4 class="modal-title"><span class="glyphicon glyphicon-list-alt"></span>{% trans 'Remove group' %}</h4> |
|
||||
</div> |
|
||||
<div class="modal-body"> |
|
||||
<h4>{% blocktrans %}Are you sure you want to delete group {{ object }}?{% endblocktrans %}</h4> |
|
||||
<button type="submit" class="btn btn-danger" value="DELETE"> |
|
||||
<span class="glyphicon glyphicon-remove"></span> {% trans 'Delete' %} |
|
||||
</button> |
|
||||
</div> |
|
||||
</form> |
|
||||
|
|
||||
|
{% block modal_form_url %} |
||||
|
{% url 'group_app:del' object.pk %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block modal_form_title %} |
||||
|
{% trans 'Remove group' %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block modal_form_text %} |
||||
|
<h4>{% blocktrans %}Are you sure you want to delete group {{ object }}?{% endblocktrans %}</h4> |
||||
|
{% endblock %} |
||||
@ -1,7 +0,0 @@ |
|||||
# See LICENSE |
|
||||
|
|
||||
from messaging.sms.submit import SmsSubmit |
|
||||
from messaging.sms.deliver import SmsDeliver |
|
||||
from messaging.sms.gsm0338 import is_gsm_text |
|
||||
|
|
||||
__all__ = ["SmsSubmit", "SmsDeliver", "is_gsm_text"] |
|
||||
@ -1,359 +0,0 @@ |
|||||
#!/usr/bin/env python3 |
|
||||
# -*- coding: utf-8 -*- |
|
||||
import os |
|
||||
import sys |
|
||||
import shutil |
|
||||
from json import dump |
|
||||
import django |
|
||||
|
|
||||
''' |
|
||||
Some permissions is not migrates, all admins is superuser |
|
||||
after migrate. |
|
||||
''' |
|
||||
|
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings") |
|
||||
from django.db.models import fields as django_fields |
|
||||
|
|
||||
|
|
||||
def get_fixture_from_unchanget_model(model_name: str, model_class): |
|
||||
""" |
|
||||
Создаёт фикстуру если модели между версиями не изменились |
|
||||
:param model_name: str 'app_label.model_name' |
|
||||
:param model_class: Model модель для которой надо сделать фикстуру |
|
||||
:return: список словарей |
|
||||
""" |
|
||||
print(model_name) |
|
||||
|
|
||||
def get_fields(obj): |
|
||||
fields = dict() |
|
||||
for field in obj._meta.get_fields(): |
|
||||
if isinstance(field, |
|
||||
(django_fields.reverse_related.ManyToOneRel, django_fields.reverse_related.ManyToManyRel)): |
|
||||
continue |
|
||||
field_val = getattr(obj, field.name) |
|
||||
if field_val is None: |
|
||||
continue |
|
||||
if isinstance(field, django_fields.related.ManyToManyField): |
|
||||
fields[field.name] = [f.pk for f in field_val.all()] |
|
||||
elif isinstance(field, |
|
||||
(django_fields.related.ForeignKey, django.contrib.contenttypes.fields.GenericForeignKey)): |
|
||||
fields[field.name] = field_val.pk |
|
||||
elif isinstance(field, django_fields.FloatField): |
|
||||
fields[field.name] = float(field_val) |
|
||||
elif isinstance(field, django_fields.DateTimeField): |
|
||||
fields[field.name] = str(field_val) |
|
||||
elif isinstance(field, django_fields.AutoField): |
|
||||
continue |
|
||||
else: |
|
||||
fields[field.name] = field_val |
|
||||
return fields |
|
||||
|
|
||||
res = [{ |
|
||||
'model': model_name, |
|
||||
'pk': obj.pk, |
|
||||
'fields': get_fields(obj) |
|
||||
} for obj in model_class.objects.all()] |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_groups(): |
|
||||
from abonapp import models |
|
||||
print('group_app.group') |
|
||||
res = [{ |
|
||||
'model': 'group_app.group', |
|
||||
'pk': abon_group.pk, |
|
||||
'fields': { |
|
||||
'title': abon_group.title |
|
||||
} |
|
||||
} for abon_group in models.AbonGroup.objects.all()] |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_abonapp(): |
|
||||
from abonapp import models |
|
||||
|
|
||||
res = get_fixture_from_unchanget_model('abonapp.abontariff', models.AbonTariff) |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('abonapp.abonstreet', models.AbonStreet) |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('abonapp.extrafieldsmodel', models.ExtraFieldsModel) |
|
||||
|
|
||||
# res += get_fixture_from_unchanget_model('abonapp.abonlog', models.AbonLog) |
|
||||
|
|
||||
print('abonapp.abon') |
|
||||
res += [{ |
|
||||
'model': 'abonapp.abon', |
|
||||
'pk': abon.pk, |
|
||||
'fields': { |
|
||||
'current_tariff': abon.current_tariff.pk if abon.current_tariff else None, |
|
||||
'group': abon.group.pk if abon.group else None, |
|
||||
'ballance': abon.ballance, |
|
||||
'ip_address': abon.ip_address, |
|
||||
'description': abon.description, |
|
||||
'street': abon.street.pk if abon.street else None, |
|
||||
'house': abon.house, |
|
||||
'extra_fields': [a.pk for a in abon.extra_fields.all()], |
|
||||
'device': abon.device.pk if abon.device else None, |
|
||||
'dev_port': abon.dev_port if abon.dev_port else None, |
|
||||
'is_dynamic_ip': abon.is_dynamic_ip, |
|
||||
'markers': abon.markers |
|
||||
} |
|
||||
} for abon in models.Abon.objects.filter(is_admin=False)] |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('abonapp.passportinfo', models.PassportInfo) |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('abonapp.invoiceforpayment', models.InvoiceForPayment) |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('abonapp.alltimepaylog', models.AllTimePayLog) |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('abonapp.abonrawpassword', models.AbonRawPassword) |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('abonapp.additionaltelephone', models.AdditionalTelephone) |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('abonapp.periodicpayforid', models.PeriodicPayForId) |
|
||||
|
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_tariffs(): |
|
||||
from tariff_app import models |
|
||||
from abonapp.models import AbonGroup |
|
||||
print('tariff_app.tariff') |
|
||||
res = [{ |
|
||||
'model': 'tariff_app.tariff', |
|
||||
'pk': trf.pk, |
|
||||
'fields': { |
|
||||
'title': trf.title, |
|
||||
'descr': trf.descr, |
|
||||
'speedIn': trf.speedIn, |
|
||||
'speedOut': trf.speedOut, |
|
||||
'amount': trf.amount, |
|
||||
'calc_type': trf.calc_type, |
|
||||
'is_admin': trf.is_admin, |
|
||||
'groups': [ag.pk for ag in AbonGroup.objects.filter(tariffs__in=[trf])] |
|
||||
} |
|
||||
} for trf in models.Tariff.objects.all()] |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('tariff_app.periodicpay', models.PeriodicPay) |
|
||||
|
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_devs(): |
|
||||
from devapp import models |
|
||||
print('devapp.device') |
|
||||
res = [{ |
|
||||
'model': 'devapp.device', |
|
||||
'pk': dv.pk, |
|
||||
'fields': { |
|
||||
'ip_address': dv.ip_address, |
|
||||
'mac_addr': str(dv.mac_addr) if dv.mac_addr else None, |
|
||||
'comment': dv.comment, |
|
||||
'devtype': dv.devtype, |
|
||||
'man_passw': dv.man_passw, |
|
||||
'group': dv.user_group.pk if dv.user_group else None, |
|
||||
'parent_dev': dv.parent_dev.pk if dv.parent_dev else None, |
|
||||
'snmp_item_num': dv.snmp_item_num, |
|
||||
'status': dv.status, |
|
||||
'is_noticeable': dv.is_noticeable |
|
||||
} |
|
||||
} for dv in models.Device.objects.all()] |
|
||||
|
|
||||
res += get_fixture_from_unchanget_model('devapp.port', models.Port) |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_accounts(): |
|
||||
from accounts_app import models |
|
||||
from abonapp.models import AbonGroup |
|
||||
|
|
||||
def get_responsibility_groups(account): |
|
||||
responsibility_groups = AbonGroup.objects.filter(profiles__in=[account]) |
|
||||
ids = [ag.pk for ag in responsibility_groups] |
|
||||
return ids |
|
||||
|
|
||||
print('accounts_app.baseaccount') |
|
||||
res = [{ |
|
||||
'model': 'accounts_app.baseaccount', |
|
||||
'pk': up.pk, |
|
||||
'fields': { |
|
||||
'username': up.username, |
|
||||
'fio': up.fio, |
|
||||
'birth_day': up.birth_day, |
|
||||
'is_active': up.is_active, |
|
||||
'is_admin': up.is_admin, |
|
||||
'telephone': up.telephone, |
|
||||
'password': up.password, |
|
||||
'last_login': up.last_login, |
|
||||
'is_superuser': up.is_admin |
|
||||
} |
|
||||
} for up in models.UserProfile.objects.all()] |
|
||||
|
|
||||
print('accounts_app.userprofile') |
|
||||
res += [{ |
|
||||
'model': 'accounts_app.userprofile', |
|
||||
'pk': up.pk, |
|
||||
'fields': { |
|
||||
'avatar': up.avatar.pk if up.avatar else None, |
|
||||
'email': up.email, |
|
||||
'responsibility_groups': get_responsibility_groups(up) |
|
||||
} |
|
||||
} for up in models.UserProfile.objects.filter(is_admin=True)] |
|
||||
|
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_photos(): |
|
||||
from photo_app.models import Photo |
|
||||
print('photo_app.photo') |
|
||||
res = [{ |
|
||||
'model': 'photo_app.photo', |
|
||||
'pk': p.pk, |
|
||||
'fields': { |
|
||||
'image': "%s" % p.image, |
|
||||
'wdth': p.wdth, |
|
||||
'heigt': p.heigt |
|
||||
} |
|
||||
} for p in Photo.objects.all()] |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_chatbot(): |
|
||||
from chatbot import models |
|
||||
res = get_fixture_from_unchanget_model('chatbot.telegrambot', models.TelegramBot) |
|
||||
res += get_fixture_from_unchanget_model('chatbot.messagehistory', models.MessageHistory) |
|
||||
res += get_fixture_from_unchanget_model('chatbot.messagequeue', models.MessageQueue) |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_map(): |
|
||||
from mapapp import models |
|
||||
res = get_fixture_from_unchanget_model('mapapp.dot', models.Dot) |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_task_app(): |
|
||||
from taskapp import models |
|
||||
res = get_fixture_from_unchanget_model('taskapp.changelog', models.ChangeLog) |
|
||||
res += get_fixture_from_unchanget_model('taskapp.task', models.Task) |
|
||||
res += get_fixture_from_unchanget_model('taskapp.ExtraComment', models.ExtraComment) |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_auth(): |
|
||||
from django.contrib.auth import models |
|
||||
from django.contrib.contenttypes.models import ContentType |
|
||||
res = get_fixture_from_unchanget_model('contenttypes.contenttype', ContentType) |
|
||||
res += get_fixture_from_unchanget_model('auth.group', models.Group) |
|
||||
res += get_fixture_from_unchanget_model('auth.permission', models.Permission) |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def dump_guardian(): |
|
||||
from guardian import models |
|
||||
print('guardian.groupobjectpermission') |
|
||||
res = [{ |
|
||||
'model': 'guardian.groupobjectpermission', |
|
||||
'pk': gp.pk, |
|
||||
'fields': { |
|
||||
'group': gp.group.pk, |
|
||||
'permission': gp.permission.pk, |
|
||||
'content_type': gp.content_type.pk, |
|
||||
'object_pk': str(gp.object_pk), |
|
||||
'content_object': gp.content_object.pk |
|
||||
} |
|
||||
} for gp in models.GroupObjectPermission.objects.all()] |
|
||||
print('guardian.userobjectpermission') |
|
||||
res += [{ |
|
||||
'model': 'guardian.userobjectpermission', |
|
||||
'pk': up.pk, |
|
||||
'fields': { |
|
||||
'permission': up.permission.pk, |
|
||||
'content_type': up.content_type.pk, |
|
||||
'object_pk': str(up.object_pk), |
|
||||
'user': up.user.pk |
|
||||
} |
|
||||
} for up in models.UserObjectPermission.objects.all()] |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
def make_migration(): |
|
||||
from datetime import datetime, date |
|
||||
|
|
||||
def my_date_converter(o): |
|
||||
if isinstance(o, datetime) or isinstance(o, date): |
|
||||
return "%s" % o |
|
||||
|
|
||||
def appdump(path, func): |
|
||||
fname = os.path.join(*path) |
|
||||
path_dir = os.path.join(*path[:-1]) |
|
||||
if not os.path.isdir(path_dir): |
|
||||
os.mkdir(path_dir) |
|
||||
with open(fname, 'w') as f: |
|
||||
dump(func(), f, default=my_date_converter, ensure_ascii=False) |
|
||||
|
|
||||
if not os.path.isdir('fixtures'): |
|
||||
os.mkdir('fixtures') |
|
||||
appdump(['fixtures', 'group_app', 'groups_fixture.json'], dump_groups) |
|
||||
appdump(['fixtures', 'tariff_app', 'tariffs_fixture.json'], dump_tariffs) |
|
||||
appdump(['fixtures', 'photo_app', 'photos_fixture.json'], dump_photos) |
|
||||
appdump(['fixtures', 'devapp', 'devs_fixture.json'], dump_devs) |
|
||||
appdump(['fixtures', 'accounts_app', 'accounts_fixture.json'], dump_accounts) |
|
||||
appdump(['fixtures', 'abonapp', 'abon_fixture.json'], dump_abonapp) |
|
||||
appdump(['fixtures', 'chatbot', 'chatbot_fixture.json'], dump_chatbot) |
|
||||
appdump(['fixtures', 'mapapp', 'map_fixture.json'], dump_map) |
|
||||
appdump(['fixtures', 'taskapp', 'task_fixture.json'], dump_task_app) |
|
||||
# appdump(['fixtures', 'accounts_app', 'auth_fixture.json'], dump_auth) |
|
||||
# appdump(['fixtures', 'accounts_app', 'guardian_fixture.json'], dump_guardian) |
|
||||
|
|
||||
|
|
||||
def move_to_fixtures_dirs(): |
|
||||
fixdir = 'fixtures' |
|
||||
for dr in os.listdir(fixdir): |
|
||||
fixture_dir = os.path.join(fixdir, dr) |
|
||||
for fixture_file in os.listdir(fixture_dir): |
|
||||
from_file = os.path.join(fixture_dir, fixture_file) |
|
||||
dst_dir = os.path.join(dr, fixdir) |
|
||||
to_file = os.path.join(dst_dir, fixture_file) |
|
||||
if not os.path.isdir(dst_dir): |
|
||||
os.mkdir(dst_dir) |
|
||||
shutil.copyfile(from_file, to_file) |
|
||||
print('cp %s -> %s' % (from_file, to_file)) |
|
||||
|
|
||||
|
|
||||
def apply_fixtures(): |
|
||||
from django.core.management import execute_from_command_line |
|
||||
from accounts_app.models import UserProfile |
|
||||
# from django.contrib.auth import models |
|
||||
|
|
||||
UserProfile.objects.filter(username='AnonymousUser').delete() |
|
||||
# print('clearing auth.group') |
|
||||
# models.Group.objects.all().delete() |
|
||||
# print('clearing auth.permission') |
|
||||
# models.Permission.objects.all().delete() |
|
||||
|
|
||||
fixtures_names = [ |
|
||||
'groups_fixture.json', 'tariffs_fixture.json', 'photos_fixture.json', |
|
||||
'devs_fixture.json', 'accounts_fixture.json', 'abon_fixture.json', |
|
||||
'chatbot_fixture.json', 'map_fixture.json', 'task_fixture.json' |
|
||||
] |
|
||||
# 'auth_fixture.json', 'guardian_fixture.json' |
|
||||
print('./manage.py loaddata ' + ', '.join(fixtures_names)) |
|
||||
execute_from_command_line([sys.argv[0], 'loaddata'] + fixtures_names) |
|
||||
|
|
||||
|
|
||||
if __name__ == '__main__': |
|
||||
if len(sys.argv) < 2: |
|
||||
print('Usage: ./migrate_to_0.2.py [makedump OR applydump]') |
|
||||
exit(1) |
|
||||
choice = sys.argv[1] |
|
||||
if choice == 'applydump': |
|
||||
django.setup() |
|
||||
move_to_fixtures_dirs() |
|
||||
apply_fixtures() |
|
||||
shutil.rmtree('fixtures') |
|
||||
elif choice == 'makedump': |
|
||||
django.setup() |
|
||||
make_migration() |
|
||||
else: |
|
||||
print('Unexpected choice') |
|
||||
@ -1,30 +0,0 @@ |
|||||
{% load i18n %} |
|
||||
|
|
||||
<form role="form" action="{% url 'tarifs:del' tid %}" method="post"> {% csrf_token %} |
|
||||
<div class="modal-header warning"> |
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
|
||||
<h4 class="modal-title"><span class="glyphicon glyphicon-exclamation-sign"></span>{% trans 'Delete service' %}</h4> |
|
||||
</div> |
|
||||
|
|
||||
{% include 'message_block.html' %} |
|
||||
|
|
||||
<div class="modal-body"> |
|
||||
<div class="form-group-sm"> |
|
||||
<label for="id_dev">{% trans 'Attention' %}</label> |
|
||||
<p>{% blocktrans %}after delete the tariff, subscribers who use that tariff will be disconnected from it.{% endblocktrans %}</p> |
|
||||
|
|
||||
<input type="hidden" name="confirm" value="yes"> |
|
||||
|
|
||||
</div> |
|
||||
|
|
||||
<div class="btn-group"> |
|
||||
<button type="submit" class="btn btn-sm btn-danger"> |
|
||||
<span class="glyphicon glyphicon-remove"></span> {% trans 'Delete' %} |
|
||||
</button> |
|
||||
<button type="reset" class="btn btn-sm btn-default"> |
|
||||
<span class="glyphicon glyphicon-retweet"></span> {% trans 'Reset' %} |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
</form> |
|
||||
@ -0,0 +1,15 @@ |
|||||
|
{% extends 'base_delete_modal.html' %} |
||||
|
{% load i18n %} |
||||
|
|
||||
|
{% block modal_form_url %} |
||||
|
{% url 'tarifs:del' tid %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block modal_form_title %} |
||||
|
{% trans 'Delete service' %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block modal_form_text %} |
||||
|
<label>{% trans 'Are you sure you want to delete tariff?' %}</label> |
||||
|
<p>{% blocktrans %}after delete the tariff, subscribers who use that tariff will be disconnected from it.{% endblocktrans %}</p> |
||||
|
{% endblock %} |
||||
@ -0,0 +1,19 @@ |
|||||
|
{% load i18n %} |
||||
|
<form role="form" action="{% block modal_form_url %}#modal_url{% endblock %}" method="post">{% csrf_token %} |
||||
|
<div class="modal-header warning"> |
||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
||||
|
<h4 class="modal-title"> |
||||
|
<span class="glyphicon glyphicon-earphone"></span> |
||||
|
{% block modal_form_title %}Form title{% endblock %} |
||||
|
</h4> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
{% block modal_form_text %} |
||||
|
<h4>{% trans 'Are you sure about them?' %}</h4> |
||||
|
{% endblock %} |
||||
|
<button type="submit" class="btn btn-danger" value="DELETE"> |
||||
|
<span class="glyphicon glyphicon-remove"></span> |
||||
|
{% block modal_btn_delete_text %}{% trans 'Delete' %}{% endblock %} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue