Browse Source

Merge branch 'djing_mon_feauture' into devel

Conflicts:
	abonapp/locale/ru/LC_MESSAGES/django.po
	abonapp/views.py
	agent/netflow/netflow_handler.sh
	agent/netflow/start_netflow.sh

 Похоже, что вы пытаетесь закоммитить слияние.
 Если это ошибка, пожалуйста удалите файл
	.git/MERGE_HEAD
 и попробуйте снова.

 Пожалуйста, введите сообщение коммита для ваших изменений. Строки,
 начинающиеся с «#» будут проигнорированы, а пустое сообщение
 отменяет процесс коммита.
 На ветке devel
 Все конфликты исправлены, но вы все еще в процессе слияния.

 Изменения, которые будут включены в коммит:
	изменено:      abonapp/locale/ru/LC_MESSAGES/django.po
	новый файл:    abonapp/templates/abonapp/charts.html
	изменено:      abonapp/templates/abonapp/ext.htm
	изменено:      abonapp/templates/abonapp/passport_view.html
	изменено:      abonapp/urls_abon.py
	изменено:      abonapp/views.py
	изменено:      agent/netflow/netflow_handler.sh
	изменено:      agent/netflow/start_netflow.sh
	удалено:       agent/netflow/to_mysql.c
	удалено:       agent/netflow/to_mysql.py
	новый файл:    static/js/Chart.min.js
	изменено:      statistics/admin.py
	новый файл:    statistics/fields.py
	новый файл:    statistics/migrations/0002_delete_statelem.py
	изменено:      statistics/models.py
	новый файл:    systemd_units/djing_rotate.service
	новый файл:    systemd_units/djing_rotate.timer
	изменено:      taskapp/templates/taskapp/add_edit_task.html
	изменено:      templates/all_base.html
devel
bashmak 9 years ago
parent
commit
f260ed0e6e
  1. 6
      abonapp/locale/ru/LC_MESSAGES/django.po
  2. 48
      abonapp/templates/abonapp/charts.html
  3. 18
      abonapp/templates/abonapp/ext.htm
  4. 1
      abonapp/templates/abonapp/passport_view.html
  5. 1
      abonapp/urls_abon.py
  6. 51
      abonapp/views.py
  7. 6
      agent/netflow/netflow_handler.sh
  8. 2
      agent/netflow/start_netflow.sh
  9. 125
      agent/netflow/to_mysql.c
  10. 66
      agent/netflow/to_mysql.py
  11. 14
      static/js/Chart.min.js
  12. 6
      statistics/admin.py
  13. 53
      statistics/fields.py
  14. 18
      statistics/migrations/0002_delete_statelem.py
  15. 45
      statistics/models.py
  16. 12
      systemd_units/djing_rotate.service
  17. 11
      systemd_units/djing_rotate.timer
  18. 15
      taskapp/templates/taskapp/add_edit_task.html
  19. 2
      templates/all_base.html

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

@ -721,6 +721,12 @@ msgstr "Для абонента не найдены паспортные дан
msgid "currency"
msgstr "руб"
msgid "Charts"
msgstr "Графики"
msgid "Group what you want doesn't exist"
msgstr "Указанная вами группа не найдена"
msgid "User device was not found"
msgstr "Пользовательское устройство не найдено"

48
abonapp/templates/abonapp/charts.html

@ -0,0 +1,48 @@
{% extends request.is_ajax|yesno:'nullcont.htm,abonapp/ext.htm' %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">График использования</h3>
</div>
<div class="panel-body">
<canvas id="netChart" width="300" height="100"></canvas>
<script type="text/javascript">
var myChart = new Chart('netChart', {
type: 'line',
data: {
datasets: [{
label: 'Траффик абонента',
data: [{{ charts_data }}],
borderWidth: 1,
pointRadius: 1,
lineTension: 0.1
}]
},
options: {
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'hour',
min: {{ time_min }},
max: {{ time_max }},
displayFormats: {
hour: 'HH:MM:SS'
}
},
position: 'bottom'
}]
}
}
});
</script>
</div>
</div>
</div>
</div>
{% endblock %}

18
abonapp/templates/abonapp/ext.htm

@ -5,7 +5,7 @@
<ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li>
<li><a href="{% url 'abonapp:group_list' %}">{% trans 'User groups' %}</a></li>
<li><a href="{% url 'abonapp:people_list' abon_group.id %}">{{ abon_group.title }}</a></li>
<li><a href="{% url 'abonapp:people_list' abon_group.pk %}">{{ abon_group.title }}</a></li>
<li class="active">{{ abon.fio }}</li>
</ol>
@ -21,32 +21,38 @@
<ul class="nav nav-tabs">
{% url 'abonapp:abon_home' abon_group.id abon.id as abon_home %}
{% url 'abonapp:abon_home' abon_group.pk abon.pk as abon_home %}
<li{% if abon_home == request.path %} class="active"{% endif %}>
<a href="{{ abon_home }}">{% trans 'Sub information' %}</a>
</li>
{% url 'abonapp:abon_services' abon_group.id abon.id as abserv %}
{% url 'abonapp:abon_services' abon_group.pk abon.pk as abserv %}
<li{% if abserv == request.path %} class="active"{% endif %}>
<a href="{{ abserv }}">{% trans 'Services' %}</a>
</li>
{% if perms.abonapp.can_view_passport %}
{% url 'abonapp:passport_view' abon_group.id abon.id as passport_view_url %}
{% url 'abonapp:passport_view' abon_group.pk abon.pk as passport_view_url %}
<li{% if passport_view_url == request.path %} class="active"{% endif %}>
<a href="{{ passport_view_url }}">{% trans 'Passport information' %}</a>
</li>
{% endif %}
{% url 'abonapp:abon_phistory' abon_group.id abon.id as abphist %}
{% url 'abonapp:abon_phistory' abon_group.pk abon.pk as abphist %}
<li{% if abphist == request.path %} class="active"{% endif %}>
<a href="{{ abphist }}">{% trans 'Payments' %}</a>
</li>
{% url 'abonapp:task_log' abon_group.id abon.id as abtasklog %}
{% url 'abonapp:task_log' abon_group.pk abon.pk as abtasklog %}
<li{% if abtasklog == request.path %} class="active"{% endif %}>
<a href="{{ abtasklog }}">{% trans 'History of tasks' %}</a>
</li>
{% url 'abonapp:charts' abon_group.pk abon.pk as abtasklog %}
<li{% if abtasklog == request.path %} class="active"{% endif %}>
<a href="{{ abtasklog }}">{% trans 'Charts' %}</a>
</li>
</ul>
<div class="tab-content">

1
abonapp/templates/abonapp/passport_view.html

@ -34,7 +34,6 @@
<div class="col-sm-9">
{{ frm.date_of_acceptance }}{{ frm.date_of_acceptance.errors }}
</div>
<script type="text/javascript" src="/static/js/datetime_with_moment.min.js"></script>
<script type="text/javascript">
$(function () {
$('#id_date_of_acceptance').datetimepicker({

1
abonapp/urls_abon.py

@ -20,6 +20,7 @@ urlpatterns = [
url(r'^(?P<uid>\d+)/complete_service(?P<srvid>\d+)$', views.complete_service, name='compl_srv'),
url(r'^(?P<uid>\d+)/activate_service(?P<srvid>\d+)$', views.activate_service, name='activate_service'),
url(r'^(?P<uid>\d+)/opt82$', views.opt82, name='opt82'),
url(r'^(?P<uid>\d+)/chart$', views.charts, name='charts'),
url(r'^(?P<uid>\d+)/unsubscribe_service(?P<srvid>\d+)$', views.unsubscribe_service,
name='unsubscribe_service'),

51
abonapp/views.py

@ -2,7 +2,7 @@
from json import dumps
from django.contrib.gis.shortcuts import render_to_text
from django.core.exceptions import PermissionDenied, MultipleObjectsReturned
from django.db import IntegrityError
from django.db import IntegrityError, ProgrammingError
from django.db.models import Count
from django.shortcuts import render, redirect, get_object_or_404, resolve_url
from django.contrib.auth.decorators import login_required, permission_required
@ -570,14 +570,15 @@ def log_page(request):
@mydefs.only_admins
def debtors(request):
# peoples_list = models.Abon.objects.filter(invoiceforpayment__status=True)
#peoples_list = mydefs.pag_mn(request, peoples_list)
# peoples_list = mydefs.pag_mn(request, peoples_list)
invs = models.InvoiceForPayment.objects.filter(status=True)
invs = mydefs.pag_mn(request, invs)
return render(request, 'abonapp/debtors.html', {
#'peoples': peoples_list
# 'peoples': peoples_list
'invoices': invs
})
@login_required
@mydefs.only_admins
def update_nas(request, group_id):
@ -704,6 +705,48 @@ def clear_dev(request, gid, uid):
return redirect('abonapp:abon_home', gid=gid, uid=uid)
@login_required
@mydefs.only_admins
def charts(request, gid, uid):
from statistics.models import getModel
from datetime import datetime, date, time, timedelta
try:
StatElem = getModel()
abon = models.Abon.objects.get(pk=uid)
if abon.group is None:
abon.group = models.AbonGroup.objects.get(pk=gid)
abon.save(update_fields=['group'])
abongroup = abon.group
if abon.ip_address is None:
charts_data = None
else:
charts_data = StatElem.objects.filter(ip=abon.ip_address.ip)
oct_limit = StatElem.percentile([cd.octets for cd in charts_data], 0.05)
charts_data = ["{x:%d,y:%d}" % (cd.cur_time.timestamp(), cd.octets) for cd in charts_data if
cd.octets < oct_limit and cd.octets > 102400]
except models.Abon.DoesNotExist:
messages.error(request, _('Abon does not exist'))
return redirect('abonapp:people_list', gid)
except models.AbonGroup.DoesNotExist:
messages.error(request, _("Group what you want doesn't exist"))
return redirect('abonapp:group_list')
except ProgrammingError as e:
messages.error(request, e)
return redirect('abonapp:abon_home', gid=gid, uid=uid)
midnight = datetime.combine(date.today(), time.min)
return render(request, 'abonapp/charts.html', {
'abon_group': abongroup,
'abon': abon,
'charts_data': ','.join(charts_data),
'time_min': midnight.timestamp(),
'time_max': (midnight + timedelta(hours=23, minutes=59, seconds=59)).timestamp()
})
# API's
def abons(request):
@ -731,5 +774,5 @@ def abons(request):
def search_abon(request):
word = request.GET.get('s')
results = models.Abon.objects.filter(fio__icontains=word)[:8]
results = [{'id':usr.pk, 'name':usr.username, 'fio':usr.fio} for usr in results]
results = [{'id': usr.pk, 'name': usr.username, 'fio': usr.fio} for usr in results]
return HttpResponse(dumps(results, ensure_ascii=False))

6
agent/netflow/netflow_handler.sh

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
FNAME="$1"
@ -14,11 +14,9 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/bin
TMP_DUMP=/tmp/djing_flow/djing_flow_dump.tmp
cd $CUR_DIR
mkdir -p /tmp/djing_flow
mv $DUMP_FILE $TMP_DUMP
./djing_flow < $TMP_DUMP | /usr/bin/mysql -uUSER -h <IP Database> -p <DBUSER> --password=<DB_PASSWORD>
rm $TMP_DUMP

2
agent/netflow/start_netflow.sh

@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/bin

125
agent/netflow/to_mysql.c

@ -1,125 +0,0 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#define FLOW_COLS 8
#define uint unsigned int
uint32_t ip2int(const char* ip)
{
uint32_t res = 0;
inet_pton(AF_INET, ip, &res);
return htonl(res);
}
uint str_split(char* str, const char* delimiter, char** pInChunks)
{
char* dat = strtok(str, " ");
register uint n=0;
while(dat)
{
pInChunks[n++] = dat;
dat = strtok(NULL, " ");
}
return n;
}
void curtime(char* pInStrTime, const uint maxlen)
{
time_t rawtime;
time( &rawtime );
strftime(pInStrTime, maxlen, "flowstat_%d%m%Y", localtime( &rawtime ));
}
void convert(char* query, char* pInRes)
{
char* chunks[FLOW_COLS] = {NULL};
int chunk_count = str_split(query, " ", chunks);
if(chunk_count < 7)
{
printf("Too short input line\n");
exit(1);
}
uint32_t src_ip = ip2int(chunks[0]);
uint32_t dst_ip = ip2int(chunks[1]);
uint proto = atoi(chunks[2]);
uint16_t src_port = ip2int(chunks[3]);
uint16_t dst_port = ip2int(chunks[4]);
uint octets = atoi(chunks[5]);
uint packets = atoi(chunks[6]);
sprintf(pInRes, ",(%u,%u,%u,%u,%u,%u,%u)\0",
src_ip, dst_ip, proto, src_port, dst_port, octets, packets);
}
int main()
{
char buf_result_convert[0xff] = {0};
FILE* f = stdin;
char* input_line = malloc(0xff);
size_t input_line_len = 0;
ssize_t read_len = 0;
char table_name[19] = {0};
curtime(table_name, 19);
printf("CREATE TABLE IF NOT EXISTS %s (\n", table_name);
printf("`id` int(10) AUTO_INCREMENT NOT NULL,\n");
printf("`src_ip` INT(10) UNSIGNED NOT NULL,\n");
printf("`dst_ip` INT(10) UNSIGNED NOT NULL,\n");
printf("`proto` smallint(2) unsigned NOT NULL DEFAULT 0,\n");
printf("`src_port` smallint(5) unsigned NOT NULL DEFAULT 0,\n");
printf("`dst_port` smallint(5) unsigned NOT NULL DEFAULT 0,\n");
printf("`octets` INT unsigned NOT NULL DEFAULT 0,\n");
printf("`packets` INT unsigned NOT NULL DEFAULT 0,\n");
printf("PRIMARY KEY (`id`)\n");
printf(") ENGINE=MyISAM DEFAULT CHARSET=utf8;\n");
char ins_sql[0xff] = {0};
sprintf(ins_sql, "INSERT INTO %s(`src_ip`, `dst_ip`, `proto`, `src_port`, `dst_port`, `octets`, `packets`) VALUES", table_name);
// always none
read_len = getline(&input_line, &input_line_len, f);
while(true)
{
register uint n=0xfff;
read_len = getline(&input_line, &input_line_len, f);
if(read_len <= 0)
break;
convert(input_line, buf_result_convert);
printf("%s\n", ins_sql);
// without first comma
printf("%s\n", buf_result_convert+1);
while(n>0)
{
read_len = getline(&input_line, &input_line_len, f);
if(read_len <= 0)
break;
convert(input_line, buf_result_convert);
printf("%s\n", buf_result_convert);
n--;
}
putc(';', stdout);
}
free(input_line);
return 0;
}

66
agent/netflow/to_mysql.py

@ -1,66 +0,0 @@
#!/bin/env python3
import sys
import socket
import struct
from re import sub
from django.utils import timezone
def ip2int(strip):
return struct.unpack("!I", socket.inet_aton(strip))[0]
def convert(query):
dat = sub(r'\s+', ' ', query.strip('\n')).split(' ')
if len(dat) == 1:
return
src_ip = ip2int(dat[0])
dst_ip = ip2int(dat[1])
proto = int(dat[2])
src_port = int(dat[3])
dst_port = int(dat[4])
octets = int(dat[5])
packets = int(dat[6])
sql = ",(%d,%d,%d,%d,%d,%d,%d)" % (
src_ip, dst_ip, proto, src_port, dst_port, octets, packets
)
return sql
if __name__ == '__main__':
f = sys.stdin
table_name = "flowstat_%s" % timezone.now().strftime("%d%m%Y")
print(("CREATE TABLE IF NOT EXISTS %s (" % table_name))
print("`id` int(10) AUTO_INCREMENT NOT NULL,")
print("`src_ip` INT(10) UNSIGNED NOT NULL,")
print("`dst_ip` INT(10) UNSIGNED NOT NULL,")
print("`proto` smallint(2) unsigned NOT NULL DEFAULT 0,")
print("`src_port` smallint(5) unsigned NOT NULL DEFAULT 0,")
print("`dst_port` smallint(5) unsigned NOT NULL DEFAULT 0,")
print("`octets` INT unsigned NOT NULL DEFAULT 0,")
print("`packets` INT unsigned NOT NULL DEFAULT 0,")
print("PRIMARY KEY (`id`)")
print(") ENGINE=MyISAM DEFAULT CHARSET=utf8;")
ins_sql = r"INSERT INTO %s(`src_ip`, `dst_ip`, `proto`, `src_port`, `dst_port`, `octets`, `packets`) VALUES" % table_name
# always none
f.readline()
while True:
n = 0xfff
rs = convert(f.readline())
if not rs: exit()
# without first comma
print(ins_sql)
print((rs[1:]))
while n > 0:
rs = convert(f.readline())
if not rs: exit()
print(rs)
n -= 1
print(';')
f.close()

14
static/js/Chart.min.js
File diff suppressed because it is too large
View File

6
statistics/admin.py

@ -1,6 +0,0 @@
from django.contrib import admin
from . import models
admin.site.register(models.StatElem)

53
statistics/fields.py

@ -0,0 +1,53 @@
#
# 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))
def _is_string(value, 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, context):
return self.to_python(val)

18
statistics/migrations/0002_delete_statelem.py

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2017-04-25 13:27
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('statistics', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='StatElem',
),
]

45
statistics/models.py

@ -1,18 +1,47 @@
import math
from django.db import models
from django.utils import timezone
from mydefs import MyGenericIPAddressField
from .fields import UnixDateTimeField
class StatElem(models.Model):
src_ip = MyGenericIPAddressField()
dst_ip = MyGenericIPAddressField()
proto = models.PositiveSmallIntegerField(default=0)
src_port = models.PositiveIntegerField(default=0)
dst_port = models.PositiveIntegerField(default=0)
cur_time = UnixDateTimeField(primary_key=True)
ip = MyGenericIPAddressField()
octets = models.PositiveIntegerField(default=0)
packets = models.PositiveIntegerField(default=0)
@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:
db_table = 'flowstat'
abstract = True
def getModel():
class DynamicStatElem(StatElem):
class Meta:
db_table = 'flowstat_%s' % timezone.now().strftime("%d%m%Y")
abstract = False
return DynamicStatElem

12
systemd_units/djing_rotate.service

@ -0,0 +1,12 @@
[Unit]
Description=A job for rotate djing netflow data
[Service]
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/bin"
Type=oneshot
ExecStart=/bin/bash -c "kill -HUP `cat /run/flow.pid.6343`"
User=root
Group=root
[Install]
WantedBy=multi-user.target

11
systemd_units/djing_rotate.timer

@ -0,0 +1,11 @@
[Unit]
Description=Run every one minute rotate flows for djing
[Timer]
OnCalendar=*-*-* *:*:59
Persistent=true
RandomizedDelaySec=5
Unit=djing_rotate.service
[Install]
WantedBy=timers.target

15
taskapp/templates/taskapp/add_edit_task.html

@ -4,8 +4,8 @@
<ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li>
<li><a href="{% url 'taskapp:home' %}">{% trans 'Tasks' %}</a></li>
<li class="active">{% if task_id %}{% trans 'Edit' %}{% else %}{% trans 'Create' %}{% endif %}</li>
<li><a href="{% url 'taskapp:home' %}">{% trans 'Tasks' %}</a></li>
<li class="active">{% if task_id %}{% trans 'Edit' %}{% else %}{% trans 'Create' %}{% endif %}</li>
</ol>
{% include 'message_block.html' %}
@ -32,7 +32,7 @@
</div>
</div>
<div class="form-group">
<label for="id_mode">{% trans 'The nature of the damage' %}</label>
<label for="id_mode">{% trans 'The nature of the damage' %}</label>
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-pawn"></span></span>
@ -40,7 +40,7 @@
</div>
</div>
<div class="form-group">
<label for="id_priority">{% trans 'A priority' %}</label>
<label for="id_priority">{% trans 'A priority' %}</label>
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-sort-by-order"></span></span>
@ -48,7 +48,7 @@
</div>
</div>
<div class="form-group">
<label for="id_state">{% trans 'Condition' %}</label>
<label for="id_state">{% trans 'Condition' %}</label>
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-retweet"></span></span>
@ -56,7 +56,7 @@
</div>
</div>
<div class="form-group">
<label for="id_abon">{% trans 'Subscriber' %}</label>
<label for="id_abon">{% trans 'Subscriber' %}</label>
<div class="input-group selectajax">
<span class="input-group-addon"><span class="glyphicon glyphicon-user"></span></span>
@ -72,13 +72,12 @@
</div>
</div>
<div class="form-group">
<label for="id_out_date">{% trans 'Reality (the date by which you must complete the task)' %}</label>
<label for="id_out_date">{% trans 'Reality (the date by which you must complete the task)' %}</label>
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-calendar"></span></span>
{{ form.out_date }}{{ form.out_date.errors }}
</div>
<script type="text/javascript" src="/static/js/datetime_with_moment.min.js"></script>
<script type="text/javascript">
$(function () {
$('#id_out_date').datetimepicker({

2
templates/all_base.html

@ -8,6 +8,8 @@
<link rel="stylesheet" href="/static/css/all.min.css">
<link rel="stylesheet" href="/static/css/custom.css">
<script src="/static/js/all.min.js"></script>
<script type="text/javascript" src="/static/js/datetime_with_moment.min.js"></script>
<script src="/static/js/Chart.min.js"></script>
<script src="/static/js/my.js"></script>
<link rel="shortcut icon" href="/static/img/favicon_m.ico">
<meta name="author" content="Дмитрий Новиков">

Loading…
Cancel
Save