Browse Source

Merge branch 'devel' into fin

Conflicts:
	abonapp/forms.py
	abonapp/migrations/0002_auto_20180808_1448.py
	abonapp/models.py
	abonapp/templates/abonapp/group_list.html
	abonapp/views.py
	clientsideapp/views.py
	djing/local_settings.py.example
	djing/settings.py
	djing/urls.py
	locale/ru/LC_MESSAGES/django.po
	periodic.py

Changes to be committed:
	modified:   abonapp/forms.py
	modified:   abonapp/locale/ru/LC_MESSAGES/django.po
	new file:   abonapp/migrations/0001_squashed_0008_auto_20181115_1206.py
	modified:   abonapp/migrations/0002_auto_20180808_1448.py
	modified:   abonapp/models.py
	new file:   abonapp/tasks.py
	modified:   abonapp/templates/abonapp/debtors.html
	modified:   abonapp/templates/abonapp/editAbon.html
	modified:   abonapp/templates/abonapp/group_list.html
	modified:   abonapp/templates/abonapp/invoiceForPayment.html
	modified:   abonapp/templates/abonapp/modal_phonebook.html
	modified:   abonapp/templates/abonapp/payHistory.html
	modified:   abonapp/templates/abonapp/peoples.html
	modified:   abonapp/templates/abonapp/service.html
	modified:   abonapp/views.py
	modified:   accounts_app/forms.py
	modified:   accounts_app/locale/ru/LC_MESSAGES/django.po
	new file:   accounts_app/migrations/0004_userprofile_flags.py
	modified:   accounts_app/models.py
	modified:   accounts_app/templates/accounts/acc_list.html
	modified:   accounts_app/templates/accounts/ext.htm
	modified:   accounts_app/templates/accounts/index.html
	modified:   accounts_app/templates/accounts/settings/ext.htm
	renamed:    accounts_app/templates/accounts/userprofile_form.html -> accounts_app/templates/accounts/settings/userprofile_form.html
	modified:   accounts_app/urls.py
	modified:   accounts_app/views.py
	new file:   agent/netflow/djing_flow.tar.gz
	modified:   agent/netflow/netflow_handler.py
	modified:   agent/netflow/start_netflow.sh
	deleted:    chatbot/admin.py
	deleted:    chatbot/apps.py
	deleted:    chatbot/locale/ru/LC_MESSAGES/django.po
	deleted:    chatbot/migrations/0001_initial.py
	deleted:    chatbot/migrations/0002_auto_20180808_1236.py
	deleted:    chatbot/models.py
	deleted:    chatbot/telebot.py
	deleted:    chatbot/views.py
	modified:   clientsideapp/views.py
	modified:   devapp/base_intr.py
	modified:   devapp/dev_types.py
	modified:   devapp/forms.py
	modified:   devapp/locale/ru/LC_MESSAGES/django.po
	new file:   devapp/migrations/0001_squashed_0005_device_ip_address_change.py
	modified:   devapp/models.py
	new file:   devapp/onu_config/__init__.py
	new file:   devapp/onu_config/base.py
	new file:   devapp/onu_config/f601.py
	new file:   devapp/onu_config/f660.py
	modified:   devapp/templates/devapp/custom_dev_page/generic_switch.html
	modified:   devapp/templates/devapp/custom_dev_page/onu.html
	modified:   devapp/templates/devapp/custom_dev_page/onu_for_zte.html
	modified:   devapp/templates/devapp/group_list.html
	modified:   devapp/urls.py
	modified:   devapp/views.py
	modified:   dialing_app/templates/index.html
	modified:   dialing_app/templatetags/telephone_filters.py
	modified:   djing/fields.py
	modified:   djing/lib/decorators.py
	modified:   djing/lib/mixins.py
	deleted:    djing/lib/tln/__init__.py
	deleted:    djing/lib/tln/tln.py
	modified:   djing/local_settings.py.example
	modified:   djing/settings.py
	modified:   djing/tasks.py
	modified:   djing/urls.py
	modified:   djing/views.py
	modified:   docs/extra_func.md
	modified:   group_app/views.py
	modified:   gw_app/nas_managers/core.py
	modified:   gw_app/nas_managers/mod_mikrotik.py
	modified:   gw_app/nas_managers/structs.py
	new file:   ip_pool/migrations/0001_squashed_0004_auto_20190305_1243.py
	new file:   ip_pool/migrations/0004_auto_20190305_1243.py
	modified:   ip_pool/models.py
	modified:   locale/ru/LC_MESSAGES/django.po
	new file:   messenger/__init__.py
	new file:   messenger/admin.py
	new file:   messenger/apps.py
	new file:   messenger/forms.py
	new file:   messenger/locale/ru/LC_MESSAGES/django.po
	new file:   messenger/migrations/0001_initial.py
	renamed:    chatbot/__init__.py -> messenger/migrations/__init__.py
	new file:   messenger/models.py
	new file:   messenger/tasks.py
	new file:   messenger/templates/messenger/add_messenger.html
	new file:   messenger/templates/messenger/messenger_list.html
	new file:   messenger/templates/messenger/vibermessenger_form.html
	new file:   messenger/tests.py
	new file:   messenger/urls.py
	new file:   messenger/views.py
	modified:   msg_app/forms.py
	modified:   msg_app/models.py
	modified:   msg_app/templates/msg_app/chat.html
	modified:   msg_app/urls.py
	modified:   msg_app/views.py
	modified:   periodic.py
	modified:   requirements.txt
	modified:   searchapp/templates/searchapp/index.html
	modified:   static/bad_ie.html
	modified:   static/css/all.min.css
	deleted:    static/css/bootstrap-datetimepicker.min.css
	new file:   static/css/chartist.min.css
	deleted:    static/img/gmarkers/dev.png
	deleted:    static/img/gmarkers/dev_bug.png
	deleted:    static/img/gmarkers/dev_ok.png
	deleted:    static/img/gmarkers/flag_black.png
	deleted:    static/img/gmarkers/relay_rack.png
	modified:   static/js/all.min.js
	deleted:    static/js/bootstrap-datetimepicker.min.js
	new file:   static/js/chartist.min.js
	deleted:    static/js/cidr.js
	modified:   static/js/my.js
	deleted:    systemd_units/djing_telebot.service
	modified:   systemd_units/do_backup.sh
	modified:   taskapp/forms.py
	modified:   taskapp/handle.py
	modified:   taskapp/locale/ru/LC_MESSAGES/django.po
	modified:   taskapp/models.py
	modified:   taskapp/templates/taskapp/details.html
	modified:   taskapp/templates/taskapp/footer_btns.html
	modified:   taskapp/urls.py
	modified:   taskapp/views.py
	deleted:    telebot.py
	modified:   templates/all_base.html
	modified:   templates/base.html
	renamed:    chatbot/migrations/__init__.py -> traf_stat/__init__.py
	new file:   traf_stat/admin.py
	new file:   traf_stat/apps.py
	new file:   traf_stat/fields.py
	new file:   traf_stat/migrations/0001_initial.py
	new file:   traf_stat/migrations/__init__.py
	new file:   traf_stat/models.py
	new file:   traf_stat/templates/statistics/index.html
	new file:   traf_stat/tests.py
	new file:   traf_stat/urls.py
	new file:   traf_stat/views.py
devel
Dmitry Novikov 7 years ago
parent
commit
2b0864dae2
  1. 13
      abonapp/forms.py
  2. 3
      abonapp/locale/ru/LC_MESSAGES/django.po
  3. 356
      abonapp/migrations/0001_squashed_0008_auto_20181115_1206.py
  4. 7
      abonapp/migrations/0002_auto_20180808_1448.py
  5. 46
      abonapp/models.py
  6. 47
      abonapp/tasks.py
  7. 14
      abonapp/templates/abonapp/debtors.html
  8. 2
      abonapp/templates/abonapp/editAbon.html
  9. 7
      abonapp/templates/abonapp/group_list.html
  10. 2
      abonapp/templates/abonapp/invoiceForPayment.html
  11. 2
      abonapp/templates/abonapp/modal_phonebook.html
  12. 4
      abonapp/templates/abonapp/payHistory.html
  13. 43
      abonapp/templates/abonapp/peoples.html
  14. 2
      abonapp/templates/abonapp/service.html
  15. 50
      abonapp/views.py
  16. 5
      accounts_app/forms.py
  17. 180
      accounts_app/locale/ru/LC_MESSAGES/django.po
  18. 19
      accounts_app/migrations/0004_userprofile_flags.py
  19. 9
      accounts_app/models.py
  20. 11
      accounts_app/templates/accounts/acc_list.html
  21. 9
      accounts_app/templates/accounts/ext.htm
  22. 24
      accounts_app/templates/accounts/index.html
  23. 18
      accounts_app/templates/accounts/settings/ext.htm
  24. 2
      accounts_app/templates/accounts/settings/userprofile_form.html
  25. 21
      accounts_app/urls.py
  26. 114
      accounts_app/views.py
  27. BIN
      agent/netflow/djing_flow.tar.gz
  28. 7
      agent/netflow/netflow_handler.py
  29. 2
      agent/netflow/start_netflow.sh
  30. 6
      chatbot/admin.py
  31. 5
      chatbot/apps.py
  32. 112
      chatbot/locale/ru/LC_MESSAGES/django.po
  33. 64
      chatbot/migrations/0001_initial.py
  34. 27
      chatbot/migrations/0002_auto_20180808_1236.py
  35. 73
      chatbot/models.py
  36. 149
      chatbot/telebot.py
  37. 3
      chatbot/views.py
  38. 3
      clientsideapp/views.py
  39. 14
      devapp/base_intr.py
  40. 174
      devapp/dev_types.py
  41. 4
      devapp/forms.py
  42. 49
      devapp/locale/ru/LC_MESSAGES/django.po
  43. 120
      devapp/migrations/0001_squashed_0005_device_ip_address_change.py
  44. 6
      devapp/models.py
  45. 6
      devapp/onu_config/__init__.py
  46. 86
      devapp/onu_config/base.py
  47. 165
      devapp/onu_config/f601.py
  48. 141
      devapp/onu_config/f660.py
  49. 18
      devapp/templates/devapp/custom_dev_page/generic_switch.html
  50. 5
      devapp/templates/devapp/custom_dev_page/onu.html
  51. 15
      devapp/templates/devapp/custom_dev_page/onu_for_zte.html
  52. 2
      devapp/templates/devapp/group_list.html
  53. 52
      devapp/urls.py
  54. 58
      devapp/views.py
  55. 2
      dialing_app/templates/index.html
  56. 2
      dialing_app/templatetags/telephone_filters.py
  57. 1
      djing/fields.py
  58. 22
      djing/lib/decorators.py
  59. 8
      djing/lib/mixins.py
  60. 4
      djing/lib/tln/__init__.py
  61. 274
      djing/lib/tln/tln.py
  62. 3
      djing/local_settings.py.example
  63. 10
      djing/settings.py
  64. 2
      djing/tasks.py
  65. 3
      djing/urls.py
  66. 2
      djing/views.py
  67. 3
      docs/extra_func.md
  68. 7
      group_app/views.py
  69. 5
      gw_app/nas_managers/core.py
  70. 16
      gw_app/nas_managers/mod_mikrotik.py
  71. 4
      gw_app/nas_managers/structs.py
  72. 36
      ip_pool/migrations/0001_squashed_0004_auto_20190305_1243.py
  73. 15
      ip_pool/migrations/0004_auto_20190305_1243.py
  74. 2
      ip_pool/models.py
  75. 3
      locale/ru/LC_MESSAGES/django.po
  76. 1
      messenger/__init__.py
  77. 7
      messenger/admin.py
  78. 5
      messenger/apps.py
  79. 28
      messenger/forms.py
  80. 190
      messenger/locale/ru/LC_MESSAGES/django.po
  81. 88
      messenger/migrations/0001_initial.py
  82. 0
      messenger/migrations/__init__.py
  83. 128
      messenger/models.py
  84. 56
      messenger/tasks.py
  85. 15
      messenger/templates/messenger/add_messenger.html
  86. 57
      messenger/templates/messenger/messenger_list.html
  87. 49
      messenger/templates/messenger/vibermessenger_form.html
  88. 3
      messenger/tests.py
  89. 17
      messenger/urls.py
  90. 161
      messenger/views.py
  91. 2
      msg_app/forms.py
  92. 25
      msg_app/models.py
  93. 4
      msg_app/templates/msg_app/chat.html
  94. 3
      msg_app/urls.py
  95. 22
      msg_app/views.py
  96. 11
      periodic.py
  97. 21
      requirements.txt
  98. 4
      searchapp/templates/searchapp/index.html
  99. 15
      static/bad_ie.html
  100. 4
      static/css/all.min.css

13
abonapp/forms.py

@ -226,13 +226,12 @@ class AddIpForm(forms.ModelForm):
groups=instance.group
).first()
if net is not None:
ips = (
ip.ip_address for ip in models.Abon.objects.filter(
group__in=net.groups.all()
).order_by('ip_address').only(
'ip_address'
).iterator()
)
ips = (ip.ip_address for ip in
models.Abon.objects.filter(
group__in=net.groups.all(),
nas=instance.nas
).order_by('ip_address').only(
'ip_address').iterator())
free_ip = net.get_free_ip(ips)
self.initial['ip_address'] = free_ip
else:

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

@ -1168,3 +1168,6 @@ msgstr "Балланс"
msgid "Date joined"
msgstr "Дата создания"
msgid "Update ip address"
msgstr "Обновить ip адрес"

356
abonapp/migrations/0001_squashed_0008_auto_20181115_1206.py

@ -0,0 +1,356 @@
# Generated by Django 2.1.1 on 2019-03-05 20:00
import bitfield.models
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('abonapp', '0001_initial'), ('abonapp', '0002_auto_20180808_1448'), ('abonapp', '0003_abon_nas'),
('abonapp', '0004_auto_20180918_1734'), ('abonapp', '0005_current_tariff'),
('abonapp', '0006_change_ip'), ('abonapp', '0007_auto_20181101_1545'),
('abonapp', '0008_auto_20181115_1206')]
initial = True
dependencies = [
('tariff_app', '0003_auto_20181115_1206'),
('gw_app', '0001_initial'),
('tariff_app', '0001_initial'),
('ip_pool', '0001_initial'),
('gw_app', '0002_auto_20181101_1545'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('group_app', '0001_initial'),
('accounts_app', '0001_initial'),
('devapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AbonTariff',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID'
)),
('tariff', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='linkto_tariff',
to='tariff_app.Tariff'
)),
('time_start', models.DateTimeField(
blank=True, default=None, null=True
)),
('deadline', models.DateTimeField(
blank=True, default=None, null=True
)),
],
options={
'ordering': ('time_start',),
'permissions': (('can_complete_service', 'finish service perm'),),
'verbose_name': 'Abon service',
'verbose_name_plural': 'Abon services',
'db_table': 'abonent_tariff'
},
),
migrations.CreateModel(
name='AbonStreet',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64)),
('group', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='group_app.Group'
)),
],
options={
'verbose_name': 'Street',
'verbose_name_plural': 'Streets',
'db_table': 'abon_street',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='Abon',
fields=[
('baseaccount_ptr', models.OneToOneField(
auto_created=True, on_delete=django.db.models.deletion.CASCADE,
parent_link=True, primary_key=True, serialize=False,
to='accounts_app.BaseAccount'
)),
('current_tariff', models.OneToOneField(
blank=True, default=None, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='abonapp.AbonTariff'
)),
('group', models.ForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='group_app.Group', verbose_name='User group'
)),
('ballance', models.FloatField(default=0.0)),
('ip_address', models.GenericIPAddressField(
blank=True, null=True,
verbose_name='Ip address'
)),
('description', models.TextField(
blank=True, null=True, verbose_name='Comment'
)),
('street', models.ForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='abonapp.AbonStreet', verbose_name='Street'
)),
('house', models.CharField(
blank=True, max_length=12,
null=True, verbose_name='House'
)),
('device', models.ForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='devapp.Device'
)),
('dev_port', models.ForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='devapp.Port'
)),
('is_dynamic_ip', models.BooleanField(
default=False, verbose_name='Is dynamic ip'
)),
('nas', models.ForeignKey(
blank=True, default=None, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='gw_app.NASModel', verbose_name='Network access server'
)),
('autoconnect_service', models.BooleanField(
default=False, verbose_name='Automatically connect next service'
)),
('last_connected_tariff', models.ForeignKey(
blank=True, default=None, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='tariff_app.Tariff',
verbose_name='Last connected service'
)),
('markers', bitfield.models.BitField(
(('icon_donkey', 'Donkey'), ('icon_fire', 'Fire'),
('icon_ok', 'Ok'), ('icon_king', 'King'),
('icon_tv', 'TV'), ('icon_smile', 'Smile'),
('icon_dollar', 'Dollar'), ('icon_service', 'Service'),
('icon_mrk', 'Marker')), default=0
)),
],
options={
'ordering': ('fio',),
'permissions': (
('can_buy_tariff', 'Buy service perm'),
('can_add_ballance', 'fill account'),
('can_ping', 'Can ping')),
'verbose_name': 'Abon',
'verbose_name_plural': 'Abons',
'db_table': 'abonent',
'unique_together': {('ip_address', 'nas')}
},
bases=('accounts_app.baseaccount',),
),
migrations.CreateModel(
name='AbonLog',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID'
)),
('abon', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='abonapp.Abon')),
('amount', models.FloatField(default=0.0)),
('author', models.ForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+', to=settings.AUTH_USER_MODEL
)),
('comment', models.CharField(max_length=128)),
('date', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'abonent_log',
'ordering': ('-date',)
},
),
migrations.CreateModel(
name='AdditionalTelephone',
fields=[
('abon', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='additional_telephones',
to='abonapp.Abon'
)),
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID'
)),
('telephone', models.CharField(
max_length=16, validators=[
django.core.validators.RegexValidator('^(\\+[7,8,9,3]\\d{10,11})?$')
],
verbose_name='Telephone')
),
('owner_name', models.CharField(max_length=127)),
],
options={
'verbose_name': 'Additional telephone',
'verbose_name_plural': 'Additional telephones',
'db_table': 'additional_telephones',
'ordering': ('owner_name',),
},
),
migrations.CreateModel(
name='AllPayLog',
fields=[
('pay_id', models.CharField(max_length=64, primary_key=True, serialize=False)),
('date_action', models.DateTimeField(auto_now_add=True)),
('summ', models.FloatField(default=0.0)),
('pay_system_name', models.CharField(max_length=16)),
],
options={
'db_table': 'all_pay_log',
'ordering': ('-date_action',),
},
),
migrations.CreateModel(
name='AllTimePayLog',
fields=[
('abon',
models.ForeignKey(
blank=True, default=None, null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to='abonapp.Abon'
)),
('pay_id', models.CharField(
max_length=36, primary_key=True,
serialize=False, unique=True
)),
('date_add', models.DateTimeField(auto_now_add=True)),
('summ', models.FloatField(default=0.0)),
('trade_point', models.CharField(
blank=True, default=None, max_length=20,
null=True, verbose_name='Trade point'
)),
('receipt_num', models.BigIntegerField(default=0, verbose_name='Receipt number')),
],
options={
'db_table': 'all_time_pay_log',
'ordering': ('-date_add',),
},
),
migrations.CreateModel(
name='InvoiceForPayment',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID'
)),
('abon', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='abonapp.Abon'
)),
('status', models.BooleanField(default=False)),
('amount', models.FloatField(default=0.0)),
('comment', models.CharField(max_length=128)),
('date_create', models.DateTimeField(auto_now_add=True)),
('date_pay', models.DateTimeField(blank=True, null=True)),
('author', models.ForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+', to=settings.AUTH_USER_MODEL
))
],
options={
'verbose_name': 'Debt',
'verbose_name_plural': 'Debts',
'db_table': 'abonent_inv_pay',
'ordering': ('date_create',)
},
),
migrations.CreateModel(
name='PassportInfo',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID'
)),
('series', models.CharField(
max_length=4, validators=[django.core.validators.integer_validator],
verbose_name='Pasport serial'
)),
('number', models.CharField(
max_length=6, validators=[django.core.validators.integer_validator],
verbose_name='Pasport number'
)),
('distributor', models.CharField(
max_length=64, verbose_name='Distributor'
)),
('date_of_acceptance', models.DateField(
verbose_name='Date of acceptance'
)),
('abon', models.OneToOneField(
blank=True, null=True,
on_delete=django.db.models.deletion.CASCADE,
to='abonapp.Abon'
))
],
options={
'ordering': ('series',),
'verbose_name': 'Passport Info',
'verbose_name_plural': 'Passport Info',
'db_table': 'passport_info'
},
),
migrations.CreateModel(
name='PeriodicPayForId',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID'
)),
('periodic_pay', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='tariff_app.PeriodicPay',
verbose_name='Periodic pay'
)),
('last_pay', models.DateTimeField(
blank=True, null=True, verbose_name='Last pay time'
)),
('next_pay', models.DateTimeField(
verbose_name='Next time to pay'
)),
('account', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='abonapp.Abon',
verbose_name='Account'
))
],
options={
'db_table': 'periodic_pay_for_id',
'ordering': ('last_pay',)
},
),
migrations.CreateModel(
name='AbonRawPassword',
fields=[
('account', models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True, serialize=False,
to='abonapp.Abon'
)),
('passw_text', models.CharField(max_length=64)),
],
options={
'db_table': 'abon_raw_password',
},
),
]

7
abonapp/migrations/0002_auto_20180808_1448.py

@ -19,12 +19,15 @@ TMP_FILE = '/tmp/djing_ip_field_abonapp_migrate.json'
def backup_info(apps, _):
Abon = apps.get_model('abonapp', 'Abon')
obs = Abon.objects.exclude(ip_address=None).only('ip_address', 'is_dynamic_ip')
with open(TMP_FILE, 'w') as f:
serializers.serialize('json', obs, stream=f, fields=('ip_address', 'is_dynamic_ip'))
if obs.exists():
with open(TMP_FILE, 'w') as f:
serializers.serialize('json', obs, stream=f, fields=('ip_address', 'is_dynamic_ip'))
def restore_info_to_new_scheme(apps, _):
Abon = apps.get_model('abonapp', 'Abon')
if not os.path.isfile(TMP_FILE):
return
with open(TMP_FILE, 'r') as f:
for abon in load(f):
ip_addr = abon['fields'].get('ip_address')

46
abonapp/models.py

@ -7,8 +7,7 @@ from django.conf import settings
from django.core import validators
from django.core.validators import RegexValidator
from django.db import models, transaction
from django.db.models.signals import post_delete, pre_delete, post_init, \
pre_save
from django.db.models.signals import post_init, pre_save
from django.dispatch import receiver
from django.shortcuts import resolve_url
from django.utils import timezone
@ -349,24 +348,6 @@ class Abon(BaseAccount):
except LogicError:
pass
def nas_remove_self(self):
"""
Will remove this user to network access server
:return:
"""
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
def get_absolute_url(self):
return resolve_url('abonapp:abon_home', self.group.id, self.username)
@ -539,17 +520,6 @@ class PeriodicPayForId(models.Model):
ordering = ('last_pay',)
@receiver(post_delete, sender=Abon)
def abon_del_signal(sender, **kwargs):
abon = kwargs.get("instance")
if abon is None:
raise ValueError('Instance does not passed to a signal')
try:
abon.nas_remove_self()
except (NasFailedResult, NasNetworkError, LogicError):
return True
@receiver(post_init, sender=AbonTariff)
def abon_tariff_post_init(sender, **kwargs):
abon_tariff = kwargs["instance"]
@ -566,17 +536,3 @@ def abon_tariff_pre_save(sender, **kwargs):
if getattr(abon_tariff, 'deadline') is None:
calc_obj = abon_tariff.tariff.get_calc_type()(abon_tariff)
abon_tariff.deadline = calc_obj.calc_deadline()
@receiver(pre_delete, sender=AbonTariff)
def abontariff_pre_delete(sender, **kwargs):
abon_tariff = kwargs.get("instance")
if abon_tariff is None:
raise ValueError('Instance does not passed to a signal')
try:
abon = Abon.objects.get(current_tariff=abon_tariff)
abon.nas_remove_self()
except (NasFailedResult, NasNetworkError, LogicError):
return True
except Abon.DoesNotExist:
print('Error: abontariff_pre_delete - user not found')

47
abonapp/tasks.py

@ -0,0 +1,47 @@
from celery import shared_task
from abonapp.models import Abon
from djing.lib import LogicError
from gw_app.models import NASModel
from gw_app.nas_managers import NasFailedResult, NasNetworkError, SubnetQueue
@shared_task
def customer_nas_command(customer_uid: int, command: str):
if command not in ('add', 'sync'):
return 'Command required'
try:
cust = Abon.objects.get(pk=customer_uid)
print(cust, command)
if command == 'sync':
r = cust.nas_sync_self()
if isinstance(r, Exception):
return 'ABONAPP SYNC ERROR: %s' % r
elif command == 'add':
cust.nas_add_self()
else:
return 'ABONAPP SYNC ERROR: Unknown command "%s"' % command
except Abon.DoesNotExist:
pass
except (LogicError, NasFailedResult, NasNetworkError, ConnectionResetError) as e:
return 'ABONAPP ERROR: %s' % e
@shared_task
def customer_nas_remove(customer_uid: int, ip_addr: str, speed: tuple, is_access: bool, nas_pk: int):
try:
if not isinstance(ip_addr, (str, int)):
ip_addr = str(ip_addr)
sq = SubnetQueue(
name="uid%d" % customer_uid,
network=ip_addr,
max_limit=speed,
is_access=is_access
)
nas = NASModel.objects.get(pk=nas_pk)
mngr = nas.get_nas_manager()
mngr.remove_user(sq)
except (ValueError, NasFailedResult, NasNetworkError, LogicError) as e:
return 'ABONAPP ERROR: %s' % e
except NASModel.DoesNotExist:
return 'NASModel.DoesNotExist id=%d' % nas_pk

14
abonapp/templates/abonapp/debtors.html

@ -31,13 +31,19 @@
{% for invoice in invoices %}
<tr>
<td>{{ invoice.id }}</td>
<td><a href="{% url 'abonapp:abon_home' invoice.abon.group.id invoice.abon.username %}"
target="_blank">{{ invoice.abon.username }}</a></td>
<td>
<a href="{% url 'abonapp:abon_home' invoice.abon.group.id invoice.abon.username %}">
{{ invoice.abon.username }}
</a>
</td>
<td>{{ invoice.amount }}</td>
<td>{{ invoice.comment }}</td>
<td>{{ invoice.date_create|date:'d b H:i' }}</td>
<td><a href="{% url 'acc_app:other_profile' invoice.author.id %}"
target="_blank">{{ invoice.author.username }}</a></td>
<td>
<a href="{% url 'acc_app:other_profile' invoice.author.id %}">
{{ invoice.author.username }}
</a>
</td>
</tr>
{% empty %}
<tr>

2
abonapp/templates/abonapp/editAbon.html

@ -136,7 +136,7 @@
<label for="id_method" class="col-sm-4 control-label">{% trans 'Device' %}</label>
<div class="col-sm-8 btn-group btn-group-sm">
{% if device %}
<a href="{% url 'devapp:view' group.pk device.pk %}" target="_blank" class="btn btn-sm btn-default" title="{% trans 'Mac Address' %}: {{ device.mac_addr|default:_('Not assigned') }}">
<a href="{% url 'devapp:view' group.pk device.pk %}" class="btn btn-sm btn-default" title="{% trans 'Mac Address' %}: {{ device.mac_addr|default:_('Not assigned') }}">
<span class="glyphicon glyphicon-hdd"></span>
<span class="hidden-md">{{ device.comment|truncatechars:11 }} {{ device.ip_address|default:'' }}</span>
</a>

7
abonapp/templates/abonapp/group_list.html

@ -65,7 +65,12 @@
<span class="glyphicon glyphicon-exclamation-sign"></span> <span class="hidden-xs">{% trans 'List of debtors' %}</span>
</a>
{% endif %}
<a href="{% url 'abonapp:vcards' %}" target="_blank" class="btn btn-default">
{% if request.user.is_superuser %}
<a href="{% url 'abonapp:fin_report' %}" class="btn btn-default">
<span class="glyphicon glyphicon-usd"></span> <span class="hidden-xs">{% trans 'Fin report' %}</span>
</a>
{% endif %}
<a href="{% url 'abonapp:vcards' %}" class="btn btn-default">
<span class="glyphicon glyphicon-phone"></span>
<span class="hidden-xs">{% trans 'Export vCards' %}</span>
</a>

2
abonapp/templates/abonapp/invoiceForPayment.html

@ -48,7 +48,7 @@
{% endif %}
</td>
<td class="col-xs-2">
<a href="{% url 'acc_app:other_profile' inv.author.id %}" target="_blank">{{ inv.author.username }}</a>
<a href="{% url 'acc_app:other_profile' inv.author.id %}">{{ inv.author.username }}</a>
</td>
</tr>
{% empty %}

2
abonapp/templates/abonapp/modal_phonebook.html

@ -26,7 +26,7 @@
</table>
<div class="modal-footer">
<a href="{% url 'abonapp:phonebook' gid %}?f=csv" class="btn btn-default" target="_blank">
<a href="{% url 'abonapp:phonebook' gid %}?f=csv" class="btn btn-default">
<span class="glyphicon glyphicon-export"></span> {% trans 'Export to csv' %}
</a>
</div>

4
abonapp/templates/abonapp/payHistory.html

@ -14,11 +14,11 @@
<tbody>
{% for ph in pay_history %}
<tr>
<td>{{ ph.amount }}</td>
<td>{{ ph.amount|floatformat:2 }}</td>
<td>{{ ph.date|date:'d F Y, H:i:s' }}</td>
<td>
{% if ph.author %}
<a target="_blank" href="{% url 'acc_app:other_profile' ph.author.pk %}">{{ ph.author.username }}</a>
<a href="{% url 'acc_app:other_profile' ph.author.pk %}">{{ ph.author.username }}</a>
{% else %}
{% trans 'System' %}
{% endif %}

43
abonapp/templates/abonapp/peoples.html

@ -24,7 +24,7 @@
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>#</th>
<th>#</th>
<th class="col-xs-1">
<a href="{% url 'abonapp:people_list' group.pk %}?{% url_order_by request order_by='username' %}">
{% trans 'Sub' %}
@ -64,11 +64,11 @@
{% if order_by == 'ballance' %}<span class="glyphicon glyphicon-filter"></span>{% endif %}
</th>
<th class="hidden-xs">{% trans 'Markers' %}</th>
<th class="col-xs-1">#</th>
<th class="col-xs-1">Ping</th>
</tr>
</thead>
<tbody>
{% with can_ch_trf=perms.tariff_app.change_tariff can_del_abon=perms.abonapp.delete_abon %}
{% with can_ch_trf=perms.tariff_app.change_tariff %}
{% for human in object_list %}
{% if human.is_active %}
<tr>
@ -76,27 +76,26 @@
<tr class="danger">
{% endif %}
<td>
<span class="glyphicon glyphicon-question-sign text-muted"></span>
{# {% if human.statcache.is_online %}#}
{# <span class="glyphicon glyphicon-ok text-success"></span>#}
{# {% else %}#}
{# <span class="glyphicon glyphicon-remove-sign text-muted"></span>#}
{# {% endif %}#}
{% if human.statcache.is_online %}
<span class="glyphicon glyphicon-ok text-success"></span>
{% else %}
<span class="glyphicon glyphicon-remove-sign text-muted"></span>
{% endif %}
</td>
<td class="col-xs-1">
<a href="{% url 'abonapp:abon_home' human.group_id human.username %}" title="{% trans 'Date joined' %}: {{ human.birth_day|date:'d E y' }}" data-toggle="tooltip">{{ human.username }}</a>
</td>
<td class="hidden-xs">
{# {% if human.statcache %}#}
{# {% if human.statcache.is_today %}#}
{# {{ human.statcache.last_time|date:"H:i" }}#}
{# {% else %}#}
{# {{ human.statcache.last_time|date:"D H:i" }}#}
{# {% endif %}#}
{# {% endif %}#}
{% if human.statcache %}
{% if human.statcache.is_today %}
{{ human.statcache.last_time|date:"H:i" }}
{% else %}
{{ human.statcache.last_time|date:"D H:i" }}
{% endif %}
{% endif %}
</td>
<td class="col-xs-1">{{ human.ip_address|default_if_none:'&mdash;' }}</td>
<td class="col-xs-2">{{ human.fio|default:'&mdash;' }}</td>
<td class="col-xs-1">{{ human.fio|default:'&mdash;' }}</td>
<td class="col-xs-1">{{ human.street|default:_('Not assigned') }}</td>
<td class="col-xs-1">{{ human.house|default:'&mdash;' }}</td>
<td class="col-xs-1"><a href="tel:{{ human.telephone }}">{{ human.telephone }}</a></td>
@ -115,17 +114,17 @@
{% for user_icon in human.get_flag_icons %}<span class="m-icon {{ user_icon }}"></span>
{% endfor %}
</td>
<td class="col-xs-1">
{% if can_del_abon %}
<a href="{% url 'abonapp:del_abon' group.pk human.username %}" class="btn btn-danger btn-sm btn-modal">
<span class="glyphicon glyphicon-remove"></span>
<td class="col-xs-2">
{% if perms.abonapp.can_ping %}
<a href="{% url 'abonapp:ping' group.pk human.username %}" class="btn btn-default btn-sm btn-cmd" title="Ping" data-param="{{ human.ip_address }}" data-toggle="tooltip">
<span class="glyphicon glyphicon-flash"></span>
</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="12">
<td colspan="10">
{% trans 'Subscribers not found' %}.
{% if perms.abonapp.add_abon %}
<a href="{% url 'abonapp:add_abon' group.pk %}">{% trans 'Add abon' %}</a>

2
abonapp/templates/abonapp/service.html

@ -117,7 +117,7 @@
<dt>{% trans 'Auto continue service.' %}</dt>
<dd>
<input type="checkbox" data-url="{% url 'abonapp:set_auto_continue_service' group.pk abon.username %}" class="autosave" {{ abon.autoconnect_service|yesno:'checked,' }}>
<a href="https://github.com/nerosketch/djing/blob/devel/docs/tarifs.md" title="{% trans 'Help' %}" target="_blank" data-toggle="tooltip">?</a>
<a href="https://github.com/nerosketch/djing/blob/devel/docs/tarifs.md" title="{% trans 'Help' %}" data-toggle="tooltip">?</a>
</dd>
</dl>

50
abonapp/views.py

@ -1,6 +1,7 @@
from datetime import datetime
from typing import Dict, Optional
from abonapp.tasks import customer_nas_command, customer_nas_remove
from agent.commands.dhcp import dhcp_commit, dhcp_expiry, dhcp_release
from devapp.models import Device, Port as DevPort
from dialing_app.models import AsteriskCDR
@ -51,11 +52,11 @@ class PeoplesListView(LoginRequiredMixin, OnlyAdminsMixin,
if street_id > 0:
peoples_list = peoples_list.filter(street=street_id)
peoples_list = peoples_list.select_related(
'group', 'street', 'current_tariff'
'group', 'street', 'current_tariff__tariff', 'statcache'
).only(
'group', 'street', 'fio',
'group', 'street', 'fio', 'birth_day',
'street', 'house', 'telephone', 'ballance', 'markers',
'username', 'is_active', 'current_tariff'
'username', 'is_active', 'current_tariff', 'ip_address'
)
ordering = self.get_ordering()
if ordering and isinstance(ordering, str):
@ -84,16 +85,15 @@ class PeoplesListView(LoginRequiredMixin, OnlyAdminsMixin,
class GroupListView(LoginRequiredMixin, OnlyAdminsMixin, OrderedFilteredList):
context_object_name = 'groups'
template_name = 'abonapp/group_list.html'
queryset = Group.objects.annotate(usercount=Count('abon'))
def get_queryset(self):
queryset = super(GroupListView, self).get_queryset()
queryset = get_objects_for_user(
self.request.user,
'group_app.view_group', klass=queryset,
'group_app.view_group', klass=Group,
use_groups=False,
accept_global_perms=False
)
return queryset
return queryset.annotate(usercount=Count('abon'))
class AbonCreateView(LoginRequiredMixin, OnlyAdminsMixin,
@ -175,6 +175,13 @@ class DelAbonDeleteView(LoginAdminMixin, PermissionRequiredMixin, DeleteView):
try:
abon = self.get_object()
gid = abon.group.id
if abon.current_tariff:
abon_tariff = abon.current_tariff.tariff
customer_nas_remove.delay(
customer_uid=abon.pk, ip_addr=abon.ip_address,
speed=(abon_tariff.speedIn, abon_tariff.speedOut),
is_access=abon.is_access(), nas_pk=abon.nas_id
)
abon.delete()
request.user.log(request.META, 'dusr', (
'%(uname)s, "%(fio)s", %(group)s %(street)s %(house)s' % {
@ -320,9 +327,7 @@ class AbonHomeUpdateView(LoginAdminMixin, PermissionRequiredMixin, UpdateView):
def form_valid(self, form):
r = super(AbonHomeUpdateView, self).form_valid(form)
abon = self.object
res = abon.nas_sync_self()
if isinstance(res, Exception):
messages.warning(self.request, res)
customer_nas_command.delay(abon.pk, 'sync')
messages.success(self.request, _('edit abon success msg'))
return r
@ -428,11 +433,8 @@ def pick_tariff(request, gid: int, uname):
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'))
else:
messages.error(request, r)
customer_nas_command.delay(abon.pk, 'sync')
messages.success(request, _('Tariff has been picked'))
return redirect('abonapp:abon_services', gid=gid,
uname=abon.username)
except (lib.LogicError, NasFailedResult) as e:
@ -467,6 +469,13 @@ def unsubscribe_service(request, gid: int, uname, abon_tariff_id: int):
try:
abon_tariff = get_object_or_404(models.AbonTariff,
pk=int(abon_tariff_id))
abon = abon_tariff.abon
trf = abon_tariff.tariff
customer_nas_remove.delay(
customer_uid=abon.pk, ip_addr=abon.ip_address,
speed=(trf.speedIn, trf.speedOut),
is_access=abon.is_access(), nas_pk=abon.nas_id
)
abon_tariff.delete()
messages.success(request, _('User has been detached from service'))
except NasFailedResult as e:
@ -594,9 +603,7 @@ class IpUpdateView(LoginAdminPermissionMixin, UpdateView):
def form_valid(self, form):
r = super(IpUpdateView, self).form_valid(form)
abon = self.object
res = abon.nas_sync_self()
if isinstance(res, Exception):
messages.warning(self.request, res)
customer_nas_command.delay(abon.pk, 'sync')
messages.success(self.request, _('Ip successfully updated'))
return r
@ -1214,6 +1221,7 @@ def user_session_free(request, gid: int, uname):
return redirect('abonapp:abon_home', gid, uname)
if abon.ip_address:
abon.free_ip_addr()
customer_nas_command.delay(abon.pk, 'remove')
messages.success(request, _('Ip lease has been freed'))
else:
messages.error(request, _('User not have ip'))
@ -1228,9 +1236,9 @@ def attach_nas(request, gid):
gateway_id = lib.safe_int(request.POST.get('gateway'))
if gateway_id:
nas = get_object_or_404(NASModel, pk=gateway_id)
abons = models.Abon.objects.filter(group__id=gid)
if abons.exists():
abons.update(nas=nas)
customers = models.Abon.objects.filter(group__id=gid)
if customers.exists():
customers.update(nas=nas)
messages.success(
request,
_('Network access server for users in this '

5
accounts_app/forms.py

@ -46,10 +46,11 @@ class UserPermissionsForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ('avatar', 'password', 'groups', 'user_permissions', 'responsibility_groups', 'is_superuser')
fields = ('user_permissions', 'is_superuser')
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
exclude = ('avatar', 'password', 'groups', 'user_permissions', 'responsibility_groups', 'is_superuser')
exclude = ('avatar', 'password', 'groups', 'user_permissions',
'responsibility_groups', 'is_admin', 'is_superuser', 'last_login')

180
accounts_app/locale/ru/LC_MESSAGES/django.po

@ -7,7 +7,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-31 16:28+0300\n"
"POT-Creation-Date: 2018-12-24 15:52+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Dmitry Novikov nerosketch@gmail.com\n"
"Language: ru\n"
@ -18,114 +18,129 @@ msgstr ""
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
#: models.py:22
#: models.py:23
msgid "Users must have an telephone number"
msgstr "У пользователей должен быть номер телефона"
#: models.py:50 templates/accounts/acc_list.html:21
#: models.py:51 templates/accounts/acc_list.html:21 views.py:31
msgid "profile username"
msgstr "Логин"
#: models.py:55
#: models.py:56
msgid "fio"
msgstr "ФИО"
#: models.py:56
#: models.py:57
msgid "birth day"
msgstr "дата рождения"
#: models.py:57
#: models.py:58
msgid "Is active"
msgstr "Активен"
#: models.py:61 templates/accounts/acc_list.html:23
#: templates/accounts/create_acc.html:62 templates/accounts/index.html:9
#: templates/accounts/settings/ch_info.html:38
#: models.py:62 templates/accounts/acc_list.html:23
#: templates/accounts/create_acc.html:61 templates/accounts/index.html:9
msgid "Telephone"
msgstr "Телефон"
#: models.py:95
#: models.py:96
msgid "Author"
msgstr "Автор"
#: models.py:96 templates/accounts/action_log.html:12
#: models.py:97 templates/accounts/action_log.html:12
msgid "Meta information"
msgstr "Мета информация"
#: models.py:98
#: models.py:99
msgid "Create user"
msgstr "Создание абонента"
#: models.py:99
#: models.py:100
msgid "Delete user"
msgstr "Удаление абонента"
#: models.py:100
#: models.py:101
msgid "Create device"
msgstr "Создание устройства"
#: models.py:101
#: models.py:102
msgid "Delete device"
msgstr "Удаление устройства"
#: models.py:102
#: models.py:103
msgid "Create NAS"
msgstr "Создание NAS"
#: models.py:103
#: models.py:104
msgid "Delete NAS"
msgstr "Удаление NAS"
#: models.py:104
#: models.py:105
msgid "Create service"
msgstr "Создание тарифа"
#: models.py:105
#: models.py:106
msgid "Delete service"
msgstr "Удаление тарифа"
#: models.py:107
#: models.py:108
msgid "Action type"
msgstr "Тип действия"
#: models.py:108
#: models.py:109
msgid "Additional info"
msgstr "Дополнительная информация"
#: models.py:109
#: models.py:110
msgid "Action date"
msgstr "Дата действия"
#: models.py:116
#: models.py:117
msgid "User profile log"
msgstr "Лог действий учётной записи"
#: models.py:117
#: models.py:118
msgid "User profile logs"
msgstr "Логи действий учётной записи"
#: models.py:126
#: models.py:127
msgid "Avatar"
msgstr "Аватар"
#: models.py:128
#: models.py:129
msgid "Responsibility groups"
msgstr "Группы администратора"
#: models.py:142
#: models.py:131
msgid "Notification about tasks"
msgstr "Оповещения о задачах"
#: models.py:132
msgid "Notification about messages"
msgstr "Оповещения о сообщениях"
#: models.py:133
msgid "Notification from monitoring"
msgstr "Оповещения из мониторинга"
#: models.py:135
msgid "Settings flags"
msgstr "Флаги настройки"
#: models.py:149
msgid "Staff account profile"
msgstr "Учётная запись работника"
#: models.py:143
#: models.py:150
msgid "Staff account profiles"
msgstr "Учётные записи работников"
#: templates/accounts/acc_list.html:7 templates/accounts/create_acc.html:8
#: templates/accounts/perms/change_global_perms.html:8
#: templates/accounts/acc_list.html:7 templates/accounts/create_acc.html:7
#: templates/accounts/perms/change_global_perms.html:7
#: templates/accounts/perms/ext.html:7
#: templates/accounts/perms/object/objects_of_type.html:7
#: templates/accounts/perms/object/objects_types.html:8
#: templates/accounts/perms/object/perms_edit.html:8
#: templates/accounts/perms/object/objects_types.html:7
#: templates/accounts/perms/object/perms_edit.html:7
msgid "Administrators"
msgstr "Сотрудники"
@ -142,7 +157,6 @@ msgid "Fullname, or login if name is empty"
msgstr "ФИО (или ник если нет)"
#: templates/accounts/acc_list.html:24
#: templates/accounts/settings/ch_info.html:28
msgid "Email"
msgstr "Адрес электронной почты"
@ -178,61 +192,63 @@ msgstr "Описание"
msgid "That admin has no logs"
msgstr "Эта учётная запись не имеет логов"
#: templates/accounts/create_acc.html:9
#: templates/accounts/create_acc.html:8
msgid "Add"
msgstr "Добавить"
#: templates/accounts/create_acc.html:16
#: templates/accounts/create_acc.html:15
msgid "Create new account"
msgstr "Создать новую учётную запись"
#: templates/accounts/create_acc.html:33 templates/accounts/create_acc.html:37
#: templates/accounts/create_acc.html:32 templates/accounts/create_acc.html:36
msgid "Username"
msgstr "Логин"
#: templates/accounts/create_acc.html:42 templates/accounts/create_acc.html:47
#: templates/accounts/create_acc.html:41 templates/accounts/create_acc.html:46
msgid "Fullname"
msgstr "Полное имя"
#: templates/accounts/create_acc.html:52
#: templates/accounts/create_acc.html:51
msgid "EMail"
msgstr "Адрес электронной почты"
#: templates/accounts/create_acc.html:67
#: templates/accounts/settings/ch_info.html:43
#: templates/accounts/create_acc.html:66
msgid "+[7,8,9,3] and 10,11 digits"
msgstr "+[7,8,9,3] и 10,11 цифр"
#: templates/accounts/create_acc.html:72
#: templates/accounts/create_acc.html:71
msgid "Type password"
msgstr "Введите пароль"
#: templates/accounts/create_acc.html:80
#: templates/accounts/create_acc.html:79
msgid "Repeat password"
msgstr "Повторите пароль"
#: templates/accounts/create_acc.html:89
#: templates/accounts/create_acc.html:88
#: templates/accounts/manage_responsibility_groups.html:20
#: templates/accounts/perms/change_global_perms.html:22
#: templates/accounts/perms/object/perms_edit.html:43
#: templates/accounts/perms/change_global_perms.html:21
#: templates/accounts/perms/object/perms_edit.html:42
#: templates/accounts/set_abon_groups_permission.html:20
#: templates/accounts/settings/ch_info.html:67
#: templates/accounts/settings/userprofile_form.html:11
msgid "Save"
msgstr "Сохранить"
#: templates/accounts/create_acc.html:92
#: templates/accounts/create_acc.html:91
#: templates/accounts/manage_responsibility_groups.html:21
#: templates/accounts/perms/object/perms_edit.html:46
#: templates/accounts/perms/object/perms_edit.html:45
#: templates/accounts/set_abon_groups_permission.html:21
#: templates/accounts/settings/ch_info.html:70
#: templates/accounts/settings/userprofile_form.html:14
msgid "Reset"
msgstr "Сбросить"
#: templates/accounts/index.html:13 templates/accounts/settings/ch_info.html:9
#: templates/accounts/settings/ch_info.html:13
#: templates/accounts/index.html:13
msgid "User name"
msgstr "Логин"
#: templates/accounts/index.html:17
msgid "Name and surname"
msgstr "Имя и отчество"
#: templates/accounts/index.html:21
msgid "Is enable"
msgstr "Включён-ли"
@ -242,8 +258,8 @@ msgid "Last login"
msgstr "Последняя авторизация"
#: templates/accounts/index.html:30
msgid "All permissions"
msgstr "Административный доступ (все права)"
msgid "Is superuser"
msgstr "Является суперпользователем"
#: templates/accounts/login.html:5
msgid "Auth"
@ -261,47 +277,47 @@ msgstr "Войти по местоположению"
msgid "The responsibility of the administrator of the group of subscribers"
msgstr "Ответственность администратора за группы абонентов"
#: templates/accounts/perms/change_global_perms.html:10
#: templates/accounts/perms/change_global_perms.html:9
#: templates/accounts/perms/ext.html:9 templates/accounts/perms/ext.html:14
msgid "Permission options"
msgstr "Права"
#: templates/accounts/perms/change_global_perms.html:11
#: templates/accounts/perms/change_global_perms.html:10
#: templates/accounts/perms/ext.html:22
msgid "Global permission options"
msgstr "Глобальные права"
#: templates/accounts/perms/change_global_perms.html:16
#: templates/accounts/perms/change_global_perms.html:15
msgid "Select permissions for picked account"
msgstr "Отметьте права для выбранной учётной записи"
#: templates/accounts/perms/ext.html:27
#: templates/accounts/perms/object/objects_of_type.html:9
#: templates/accounts/perms/object/objects_types.html:9
#: templates/accounts/perms/object/objects_types.html:10
#: templates/accounts/perms/object/objects_types.html:11
#: templates/accounts/perms/object/perms_edit.html:10
#: templates/accounts/perms/object/perms_edit.html:9
msgid "Object permission options"
msgstr "Права для каждого объекта"
#: templates/accounts/perms/object/objects_of_type.html:16
#: templates/accounts/perms/object/perms_edit.html:18
#: templates/accounts/perms/object/perms_edit.html:17
msgid "Pick object for edit permissions"
msgstr "Выберите объект для редактирования прав доступа"
#: templates/accounts/perms/object/objects_types.html:16
#: templates/accounts/perms/object/objects_types.html:15
msgid "Pick the type of object"
msgstr "Выберите тип объекта"
#: templates/accounts/perms/object/objects_types.html:24
#: templates/accounts/perms/object/objects_types.html:23
msgid "Group"
msgstr "Группа"
#: templates/accounts/perms/object/perms_edit.html:27
#: templates/accounts/perms/object/perms_edit.html:26
msgid "Profile is superuser, permissions to change it makes no sense"
msgstr ""
"Учётная запись является суперпользователем. Разрешения менять нет смысла.,"
"Учётная запись является суперпользователем. Разрешения менять нет смысла."
#: templates/accounts/perms/object/perms_edit.html:33
#: templates/accounts/perms/object/perms_edit.html:32
msgid "Change permission for that object"
msgstr "Изменение прав доступа для выбранного объекта"
@ -309,47 +325,35 @@ msgstr "Изменение прав доступа для выбранного
msgid "The list of user groups to which the account has access"
msgstr "Список групп абонентов, к которым учётка имеет доступ"
#: views.py:33
msgid "Wrong login or password, please try again"
msgstr "Неправильный логин или пароль, попробуйте ещё раз"
#: views.py:121
msgid "New password is empty, fill it"
msgstr "Новый пароль пустой, придумайте себе пароль"
#: views.py:123
msgid "Wrong password"
msgstr "Неправильный пароль"
#: views.py:125
msgid "Empty password, fill it"
msgstr "Пустой пароль, впишите что-то в пароль"
#: views.py:104 views.py:126
msgid "Saved successfully"
msgstr "Успешно сохранено"
#: views.py:149
#: views.py:154
msgid "You forget specify a password for the new account"
msgstr "Забыли указать пароль для нового аккаунта"
#: views.py:152
#: views.py:157
msgid "You forget to repeat a password for the new account"
msgstr "Забыли повторить пароль для нового аккаунта"
#: views.py:161
#: views.py:166
msgid "Subscriber with this name already exist"
msgstr "Пользователь с таким именем уже есть"
#: views.py:163
#: views.py:168
msgid "Passwords does not match, try again"
msgstr "Пароли не совпадают, попробуйте ещё раз"
#: views.py:178
#: views.py:183
msgid "Profile has been deleted"
msgstr "Учётная запись удалена"
#: views.py:240
#: views.py:244 views.py:288
msgid "Permissions has successfully updated"
msgstr "Права успешно обновлены"
#: views.py:352
#: views.py:354
msgid "Responsibilities has been updated"
msgstr "Ответственность за группы обновлена"
@ -374,5 +378,5 @@ msgstr "Лог действий"
msgid "Administrator"
msgstr "Сотрудник"
msgid "Saved successfully"
msgstr "Успешно сохранено"
msgid "Options"
msgstr "Настройки"

19
accounts_app/migrations/0004_userprofile_flags.py

@ -0,0 +1,19 @@
# Generated by Django 2.1.3 on 2018-12-24 15:52
import bitfield.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts_app', '0003_new_user_profile_log'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='flags',
field=bitfield.models.BitField((('notify_task', 'Notification about tasks'), ('notify_msg', 'Notification about messages'), ('notify_mon', 'Notification from monitoring')), default=0, verbose_name='Flags'),
),
]

9
accounts_app/models.py

@ -1,6 +1,7 @@
# -*- coding:utf-8 -*-
import os
from PIL import Image
from bitfield.models import BitField
from jsonfield import JSONField
from django.db import models
@ -124,8 +125,14 @@ class UserProfileManager(MyUserManager):
class UserProfile(BaseAccount):
avatar = models.ImageField(_('Avatar'), upload_to=os.path.join('user', 'avatar'), null=True, default=None, blank=True)
email = models.EmailField(default='')
email = models.EmailField(default='', blank=True)
responsibility_groups = models.ManyToManyField(Group, blank=True, verbose_name=_('Responsibility groups'))
USER_PROFILE_FLAGS = (
('notify_task', _('Notification about tasks')),
('notify_msg', _('Notification about messages')),
('notify_mon', _('Notification from monitoring'))
)
flags = BitField(flags=USER_PROFILE_FLAGS, default=0, verbose_name=_('Settings flags'))
objects = UserProfileManager()

11
accounts_app/templates/accounts/acc_list.html

@ -27,11 +27,12 @@
</thead>
<tbody>
{% for usr in users %}
<tr>
<td><a href="{% url 'acc_app:other_profile' usr.id %}">
<img width="50" src="{{ usr.get_min_ava }}"
alt="{{ usr.username }}"/>
</a></td>
<tr {{ usr.is_active|yesno:',class="danger"'|safe }}>
<td>
<a href="{% url 'acc_app:other_profile' usr.id %}">
<img width="50" src="{{ usr.get_min_ava }}" alt="{{ usr.username }}"/>
</a>
</td>
<td><a href="{% url 'acc_app:other_profile' usr.id %}">{{ usr.username }}</a></td>
<td>{{ usr.get_full_name }}</td>
<td class="hidden-xs">

9
accounts_app/templates/accounts/ext.htm

@ -17,7 +17,7 @@
<div class="row">
<div class="col-sm-3">
<div class="thumbnail">
<a href="{{ userprofile.get_big_ava }}" class="thumbnail" target="_blank">
<a href="{{ userprofile.get_big_ava }}" class="thumbnail">
<img alt="ava" src="{{ userprofile.get_min_ava }}"/>
</a>
<div class="caption btn-group btn-group-sm">
@ -26,6 +26,13 @@
<span class="glyphicon glyphicon-edit"></span>
<span class="hidden-sm hidden-md">{% trans 'Edit' %}</span>
</a>
{% else %}
{% if request.user.is_superuser %}
<a href="{% url 'acc_app:edit_profile' userprofile.id %}" class="btn btn-primary">
<span class="glyphicon glyphicon-edit"></span>
<span class="hidden-sm hidden-md">{% trans 'Edit' %}</span>
</a>
{% endif %}
{% endif %}
{% if request.user.is_superuser %}
<a href="{% url 'acc_app:setup_perms' userprofile.pk %}" class="btn btn-default"

24
accounts_app/templates/accounts/index.html

@ -6,29 +6,29 @@
<table class="table-striped table-bordered">
<tbody>
<tr>
<td class="col-sm-4">{% trans 'Telephone' %}</td>
<td><a href="tel:{{ userprofile.telephone }}">{{ userprofile.telephone }}</a></td>
<td class="col-sm-2">{% trans 'Telephone' %}</td>
<td class="col-sm-10"><a href="tel:{{ userprofile.telephone }}">{{ userprofile.telephone }}</a></td>
</tr>
<tr>
<td>{% trans 'User name' %}</td>
<td>{{ userprofile.username }}</td>
<td class="col-sm-2">{% trans 'User name' %}</td>
<td class="col-sm-10">{{ userprofile.username }}</td>
</tr>
<tr>
<td>{% trans 'Name and surname' %}</td>
<td>{{ userprofile.fio }}</td>
<td class="col-sm-2">{% trans 'Name and surname' %}</td>
<td class="col-sm-10">{{ userprofile.fio }}</td>
</tr>
<tr>
<td>{% trans 'Is enable' %}</td>
<td><input type="checkbox" {{ userprofile.is_active|yesno:' checked,' }}></td>
<td class="col-sm-2">{% trans 'Is enable' %}</td>
<td class="col-sm-10"><input type="checkbox" {{ userprofile.is_active|yesno:' checked,' }}></td>
</tr>
<tr>
<td>{% trans 'Last login' %}</td>
<td>{{ userprofile.last_login|date:"l d E Y H:i" }}</td>
<td class="col-sm-2">{% trans 'Last login' %}</td>
<td class="col-sm-10">{{ userprofile.last_login|date:"l d E Y H:i" }}</td>
</tr>
{% if request.user.is_superuser %}
<tr>
<td>{% trans 'All permissions' %}</td>
<td><input type="checkbox"{{ userprofile.is_staff|yesno:' checked,' }}></td>
<td class="col-sm-2">{% trans 'Is superuser' %}</td>
<td class="col-sm-10"><input type="checkbox"{{ userprofile.is_superuser|yesno:' checked,' }}></td>
</tr>
{% endif %}
</tbody>

18
accounts_app/templates/accounts/settings/ext.htm

@ -5,7 +5,13 @@
<ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li>
<li><a href="{% url 'acc_app:accounts_list' %}">{% trans 'Administrators' %}</a></li>
<li><a href="{% url 'acc_app:profile' %}">{{ user.username }}</a></li>
<li>
{% if object == request.user %}
<a href="{% url 'acc_app:setup_info' %}">{{ object.username }}</a>
{% else %}
<a href="{% url 'acc_app:other_profile' object.pk %}">{{ object.username }}</a>
{% endif %}
</li>
<li class="active">{% trans 'Options' %}</li>
</ol>
{% endblock %}
@ -17,10 +23,9 @@
{% block main %}
<div class="row">
<div class="col-sm-3">
<form action="{% url 'acc_app:setup_avatar' %}" method="post" class="thumbnail"
enctype="multipart/form-data">{% csrf_token %}
<form action="{% url 'acc_app:setup_avatar' %}" method="post" class="thumbnail" enctype="multipart/form-data">{% csrf_token %}
<img alt="ava" src="{{ user.get_min_ava }}"/>
<img alt="ava" src="{{ object.get_min_ava }}"/>
<div class="caption">
<button type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
@ -30,11 +35,10 @@
</div>
<div class="col-sm-9">
<h3>{{ user.username|default:_('Not assigned') }}</h3>
<h3>{{ object.username|default:_('Not assigned') }}</h3>
<ul class="nav nav-tabs">
<li class="active">
<a href="#livetab_content" data-tab-remote="{% url 'acc_app:setup_info' %}" role="tab"
data-toggle="tab">
<a href="#livetab_content" data-tab-remote="{% url 'acc_app:setup_info' %}" role="tab" data-toggle="tab">
{% trans 'Change self onfo' %}
</a>
</li>

2
accounts_app/templates/accounts/userprofile_form.html → accounts_app/templates/accounts/settings/userprofile_form.html

@ -2,7 +2,7 @@
{% load globaltags i18n bootstrap3 %}
{% block content %}
<form role="form" action="{% url 'acc_app:setup_info' %}" method="post">{% csrf_token %}
<form role="form" action="{{ form_url }}" method="post">{% csrf_token %}
{% bootstrap_form form %}

21
accounts_app/urls.py

@ -11,34 +11,25 @@ urlpatterns = [
path('logout/', LogoutView.as_view(next_page='acc_app:login'), name='logout'),
path('login_by_location/', views.location_login, name='llogin'),
path('me/', views.profile_show, name='profile'),
path('add/', views.create_profile, name='create_profile'),
path('settings/', views.UpdateSelfAccount.as_view(), name='setup_info'),
path('settings/change_ava/', views.AvatarUpdateView.as_view(), name='setup_avatar'),
path('<int:uid>/', views.profile_show, name='other_profile'),
path('<int:uid>/', views.ProfileShowDetailView.as_view(), name='other_profile'),
path('<int:uid>/edit/', views.UpdateAccount.as_view(), name='edit_profile'),
path('<int:uid>/perms/', views.PermsUpdateView.as_view(), name='setup_perms'),
path('<int:uid>/perms/object/', views.perms_object, name='setup_perms_object'),
re_path('^(?P<uid>\d+)/perms/object/(?P<klass_name>[a-z_]+\.[a-zA-Z_]+)/$',
views.PermissionClassListView.as_view(),
name='perms_klasses'),
re_path('^(?P<uid>\d+)/perms/object/(?P<klass_name>[a-z_]+\.[a-zA-Z_]+)/$', views.PermissionClassListView.as_view(), name='perms_klasses'),
re_path('^(?P<uid>\d+)/perms/object/(?P<klass_name>[a-z_]+\.[a-zA-Z_]+)/(?P<obj_id>\d+)/$',
views.perms_edit,
name='perms_edit'),
re_path('^(?P<uid>\d+)/perms/object/(?P<klass_name>[a-z_]+\.[a-zA-Z_]+)/(?P<obj_id>\d+)/$', views.perms_edit, name='perms_edit'),
path('<int:uid>/del/', views.delete_profile, name='delete_profile'),
path('<int:uid>/user_group_access/',
views.set_abon_groups_permission,
name='set_abon_groups_permission'),
path('<int:uid>/user_group_access/', views.set_abon_groups_permission, name='set_abon_groups_permission'),
path('<int:uid>/manage_responsibility_groups/',
views.ManageResponsibilityGroups.as_view(),
name='manage_responsibility_groups'),
path('<int:uid>/manage_responsibility_groups/', views.ManageResponsibilityGroups.as_view(), name='manage_responsibility_groups'),
path('<int:uid>/actions/', views.ActionListView.as_view(), name='action_log')
]

114
accounts_app/views.py

@ -1,6 +1,6 @@
from django.apps import apps
from django.contrib.auth.decorators import login_required
from django.contrib.auth import logout, login, authenticate
from django.contrib.auth import login, authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView
@ -10,28 +10,26 @@ from django.shortcuts import render, redirect, get_object_or_404, resolve_url
from django.contrib import messages
from django.urls import NoReverseMatch
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.generic import ListView, UpdateView
from django.utils.translation import ugettext as _, gettext
from django.views.generic import ListView, UpdateView, DetailView
from django.conf import settings
from group_app.models import Group
from .models import UserProfile, UserProfileLog
from .forms import AvatarChangeForm, UserPermissionsForm, MyUserObjectPermissionsForm, UserProfileForm
from djing import lib
from accounts_app import forms
from djing.lib.decorators import only_admins
from djing.lib.mixins import OnlyAdminsMixin, LoginAdminPermissionMixin, OnlySuperUserMixin
from guardian.decorators import permission_required_or_403 as permission_required
from guardian.shortcuts import get_objects_for_user, assign_perm, remove_perm
login_decs = login_required, only_admins
class CustomLoginView(LoginView):
template_name = 'accounts/login.html'
def form_invalid(self, form):
messages.error(self.request, _('Wrong login or password, please try again'))
for msg in form.non_field_errors():
messages.error(self.request, msg)
return super().form_invalid(form)
def get_success_url(self):
@ -39,7 +37,7 @@ class CustomLoginView(LoginView):
if next_url:
return next_url
if self.request.user.is_staff:
return resolve_url('acc_app:profile')
return resolve_url('acc_app:setup_info')
return resolve_url('client_side:home')
@ -52,7 +50,7 @@ def location_login(request):
login(request, auser)
if nextl == 'None' or nextl is None or nextl == '':
if request.user.is_staff:
return redirect('acc_app:profile')
return redirect('acc_app:setup_info')
return redirect('client_side:home')
return redirect(nextl)
return render(request, 'accounts/login.html', {
@ -63,35 +61,28 @@ def location_login(request):
return redirect('client_side:home')
@login_required
@only_admins
def profile_show(request, uid=0):
uid = lib.safe_int(uid)
class ProfileShowDetailView(LoginRequiredMixin, OnlyAdminsMixin, DetailView):
model = UserProfile
pk_url_kwarg = 'uid'
template_name = 'accounts/index.html'
context_object_name = 'userprofile'
if uid == 0:
return redirect('acc_app:other_profile', uid=request.user.id)
def get_context_data(self, **kwargs):
context = {
'uid': self.kwargs.get('uid')
}
context.update(kwargs)
return super(ProfileShowDetailView, self).get_context_data(**context)
usr = get_object_or_404(UserProfile, id=uid)
if request.user != usr and not request.user.has_perm('accounts_app.view_userprofile', usr):
raise PermissionDenied
if request.method == 'POST':
usr.username = request.POST.get('username')
usr.fio = request.POST.get('fio')
usr.telephone = request.POST.get('telephone')
usr.is_active = request.POST.get('stat')
usr.is_admin = request.POST.get('is_admin')
usr.save()
return redirect('acc_app:other_profile', uid=uid)
return render(request, 'accounts/index.html', {
'uid': uid,
'userprofile': usr
})
def dispatch(self, request, *args, **kwargs):
uid = self.kwargs.get('uid')
if uid == 0:
return redirect('acc_app:other_profile', uid=request.user.id)
return super(ProfileShowDetailView, self).dispatch(request, *args, **kwargs)
@method_decorator(login_decs, name='dispatch')
class AvatarUpdateView(UpdateView):
form_class = AvatarChangeForm
class AvatarUpdateView(LoginRequiredMixin, OnlyAdminsMixin, UpdateView):
form_class = forms.AvatarChangeForm
template_name = 'accounts/settings/ch_info.html'
def get_object(self, queryset=None):
@ -101,19 +92,46 @@ class AvatarUpdateView(UpdateView):
return resolve_url('acc_app:other_profile', uid=self.request.user.id)
class UpdateAccount(LoginRequiredMixin, OnlySuperUserMixin, UpdateView):
form_class = forms.UserProfileForm
pk_url_kwarg = 'uid'
model = UserProfile
template_name = 'accounts/settings/userprofile_form.html'
def form_valid(self, form):
r = super().form_valid(form)
messages.success(self.request, _('Saved successfully'))
return r
def get_context_data(self, **kwargs):
context = {
'form_url': resolve_url('acc_app:edit_profile', self.object.pk)
}
context.update(kwargs)
return super().get_context_data(**context)
class UpdateSelfAccount(LoginRequiredMixin, UpdateView):
form_class = UserProfileForm
form_class = forms.UserProfileForm
pk_url_kwarg = 'uid'
model = UserProfile
template_name = 'accounts/userprofile_form.html'
template_name = 'accounts/settings/userprofile_form.html'
def get_object(self, queryset=None):
return self.request.user
def form_valid(self, form):
r = super(UpdateSelfAccount, self).form_valid(form)
r = super().form_valid(form)
messages.success(self.request, _('Saved successfully'))
return r
def get_context_data(self, **kwargs):
context = {
'form_url': resolve_url('acc_app:setup_info')
}
context.update(kwargs)
return super().get_context_data(**context)
@login_required
@only_admins
@ -165,8 +183,7 @@ def delete_profile(request, uid: int):
return redirect('acc_app:accounts_list')
@method_decorator(login_decs, name='dispatch')
class AccountsListView(ListView):
class AccountsListView(LoginRequiredMixin, OnlyAdminsMixin, ListView):
http_method_names = 'get',
paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
template_name = 'accounts/acc_list.html'
@ -200,7 +217,7 @@ class PermsUpdateView(UpdateView):
http_method_names = 'get', 'post'
template_name = 'accounts/perms/change_global_perms.html'
model = UserProfile
form_class = UserPermissionsForm
form_class = forms.UserPermissionsForm
pk_url_kwarg = 'uid'
def get_success_url(self):
@ -227,8 +244,7 @@ class PermsUpdateView(UpdateView):
return super(PermsUpdateView, self).form_valid(form)
@method_decorator(login_decs, name='dispatch')
class PermissionClassListView(ListView):
class PermissionClassListView(LoginRequiredMixin, OnlyAdminsMixin, ListView):
http_method_names = 'get',
paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
template_name = 'accounts/perms/object/objects_of_type.html'
@ -265,7 +281,7 @@ def perms_edit(request, uid: int, klass_name, obj_id):
klass = apps.get_model(app_label, model_name)
obj = get_object_or_404(klass, pk=obj_id)
frm = MyUserObjectPermissionsForm(userprofile, obj, request.POST or None)
frm = forms.MyUserObjectPermissionsForm(userprofile, obj, request.POST or None)
if request.method == 'POST' and frm.is_valid():
frm.save_obj_perms()
messages.success(request, _('Permissions has successfully updated'))
@ -308,8 +324,7 @@ def set_abon_groups_permission(request, uid: int):
})
@method_decorator(login_decs, name='dispatch')
class ManageResponsibilityGroups(ListView):
class ManageResponsibilityGroups(LoginRequiredMixin, OnlyAdminsMixin, ListView):
http_method_names = ('get', 'post')
template_name = 'accounts/manage_responsibility_groups.html'
context_object_name = 'groups'
@ -339,11 +354,10 @@ class ManageResponsibilityGroups(ListView):
return HttpResponseRedirect(self.get_success_url())
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('accounts_app.view_userprofilelog'), name='dispatch')
class ActionListView(ListView):
class ActionListView(LoginAdminPermissionMixin, ListView):
paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
template_name = 'accounts/action_log.html'
permission_required = 'accounts_app.view_userprofilelog'
model = UserProfileLog
def get_queryset(self):

BIN
agent/netflow/djing_flow.tar.gz

7
agent/netflow/netflow_handler.py

@ -39,11 +39,8 @@ if __name__ == '__main__':
cursor = db.cursor()
sql = (
'SELECT INET_ATON(emps.ip) as uip, acc.id FROM abonent '
'LEFT JOIN base_accounts AS acc ON (acc.id = abonent.baseaccount_ptr_id) '
'LEFT JOIN abonent_ip_addresses AS ips ON (acc.id = ips.abon_id) '
'LEFT JOIN ip_pool_employed_ip AS emps ON (ips.ipleasemodel_id = emps.id) '
'WHERE INET_ATON(emps.ip) != 0;'
"SELECT INET_ATON(ip_address) as uip, baseaccount_ptr_id FROM abonent "
"WHERE INET_ATON(ip_address) != 'NULL';"
)
ln = cursor.execute(sql)
with open(tmp_ipuser_file, 'w') as f:

2
agent/netflow/start_netflow.sh

@ -1,6 +1,6 @@
#!/usr/bin/env bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/bin
PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
mkdir -p /tmp/djing_flow

6
chatbot/admin.py

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

5
chatbot/apps.py

@ -1,5 +0,0 @@
from django.apps import AppConfig
class ChatbotConfig(AppConfig):
name = 'chatbot'

112
chatbot/locale/ru/LC_MESSAGES/django.po

@ -1,112 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Dmitry Novikov <nerosketch@gmail.com>, 2017.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-09 14:57+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Dmitry Novikov <nerosketch@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
#: models.py:13
msgid "Employee"
msgstr "Сотрудник"
#: models.py:14
msgid "Telegram chat id"
msgstr "Номер чата из telegram"
#: models.py:21
msgid "Telegram bot"
msgstr "Telegram бот"
#: models.py:22
msgid "Telegram bots"
msgstr "Telegram боты"
#: models.py:36
msgid "Message history"
msgstr "История собщений"
#: models.py:37
msgid "Message histories"
msgstr "Истории собщений"
#: models.py:54
msgid "Target employee"
msgstr "Сотрудник"
#: models.py:55
msgid "Message"
msgstr "Сообщение"
#: models.py:60
msgid "Status of message"
msgstr "Статус сообщения"
#: models.py:62
msgid "App tag"
msgstr "Тэг приложения"
#: models.py:71 models.py:72
msgid "Message queue"
msgstr "Очередь оповещений"
#: telebot.py:64
msgid "Let's get acquainted, what is your name? Write your login from billing."
msgstr "Давай знакомиться, как тебя зовут? Напиши свой логин из биллинга."
#: telebot.py:85
msgid "I do not know the answer to this yet."
msgstr "Я пока не знаю ответа на это"
#: telebot.py:106
msgid ""
"You are not found in the database, check that it correctly pointed out its "
"LOGIN. Try again"
msgstr ""
"Ты не найден в базе, проверь что правильно указал именно свой ЛОГИН. "
"Попробуй ещё"
#: telebot.py:110
#, python-format
msgid ""
"Yes, it's nice to meet %(username)s, I will notify you about events in "
"billing. Successful work ;)"
msgstr ""
"Да, приятно познакомиться %(username)s, я буду оповещать тебя о событиях в "
"биллинге. Удачной работы ;)"
#: telebot.py:122
msgid "Let's ping, write ip. It will be necessary to wait 10 seconds"
msgstr "Давай пинганём, напиши ip. Нужно будет подождать 10 сек"
#: telebot.py:129
msgid "It's not like ip address, try again"
msgstr "Это не похоже на ip адрес, попробуй ещё"
#: telebot.py:132
#, python-format
msgid "You're '%s', right?"
msgstr "Ты ведь %s ?"
#: telebot.py:140
msgid "Telegram bot token not found"
msgstr "Токен для бота Telegram не найден"
#: telebot.py:145
#, python-format
msgid "Recipient '%s' does not subscribed on notifications"
msgstr "%s не подписан на оповещения"

64
chatbot/migrations/0001_initial.py

@ -1,64 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-26 00:20
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='MessageHistory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.CharField(max_length=255)),
('date_sent', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Message history',
'verbose_name_plural': 'Message histories',
'db_table': 'chat_message_history',
},
),
migrations.CreateModel(
name='MessageQueue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.CharField(max_length=255, verbose_name='Message')),
('status', models.CharField(choices=[('n', 'New'), ('r', 'Read')], default='n', max_length=1,
verbose_name='Status of message')),
('tag', models.CharField(default='none', max_length=6, verbose_name='App tag')),
('target_employee',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL,
verbose_name='Target employee')),
],
options={
'verbose_name': 'Message queue',
'verbose_name_plural': 'Message queue',
'db_table': 'chat_message_queue',
},
),
migrations.CreateModel(
name='TelegramBot',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('chat_id', models.PositiveIntegerField(default=0, verbose_name='Telegram chat id')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL,
verbose_name='Employee')),
],
options={
'verbose_name': 'Telegram bot',
'verbose_name_plural': 'Telegram bots',
'db_table': 'chat_telegram_bot',
},
),
]

27
chatbot/migrations/0002_auto_20180808_1236.py

@ -1,27 +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 = [
('chatbot', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='messagehistory',
options={'ordering': ('-date_sent',), 'verbose_name': 'Message history', 'verbose_name_plural': 'Message histories'},
),
migrations.AlterModelOptions(
name='messagequeue',
options={'ordering': ('target_employee__username',), 'verbose_name': 'Message queue', 'verbose_name_plural': 'Message queue'},
),
migrations.AlterModelOptions(
name='telegrambot',
options={'ordering': ('chat_id',), 'verbose_name': 'Telegram bot', 'verbose_name_plural': 'Telegram bots'},
),
]

73
chatbot/models.py

@ -1,73 +0,0 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.conf import settings
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL')
class ChatException(Exception):
pass
class TelegramBot(models.Model):
user = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_('Employee'))
chat_id = models.PositiveIntegerField(_('Telegram chat id'), default=0)
def __str__(self):
return "%s - %d" % (self.user.get_full_name(), self.chat_id)
class Meta:
db_table = 'chat_telegram_bot'
verbose_name = _('Telegram bot')
verbose_name_plural = _('Telegram bots')
ordering = ('chat_id',)
class MessageHistory(models.Model):
user = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE)
message = models.CharField(max_length=255)
date_sent = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.message
class Meta:
db_table = 'chat_message_history'
verbose_name = _('Message history')
verbose_name_plural = _('Message histories')
ordering = ('-date_sent',)
class MessageQueueManager(models.Manager):
def pop(self, user, tag='none'):
msgs = self.filter(target_employee=user, status='n', tag=tag)[:1].only('message').values('id', 'message')
if len(msgs) > 0:
self.filter(id=msgs[0]['id']).delete()
return msgs[0]['message']
def push(self, msg, user, tag='none'):
msg = self.create(target_employee=user, message=msg, tag=tag)
return msg
class MessageQueue(models.Model):
target_employee = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_('Target employee'))
message = models.CharField(_('Message'), max_length=255)
STATUSES = (
('n', 'New'),
('r', 'Read')
)
status = models.CharField(_('Status of message'), max_length=1, choices=STATUSES, default='n')
# tag: each application puts its own to separate messages between these applications
tag = models.CharField(_('App tag'), max_length=6, default='none')
objects = MessageQueueManager()
def __str__(self):
return self.message
class Meta:
db_table = 'chat_message_queue'
verbose_name = _('Message queue')
verbose_name_plural = _('Message queue')
ordering = ('target_employee__username',)

149
chatbot/telebot.py

@ -1,149 +0,0 @@
# -*- coding: utf-8 -*-
from telepot import helper, glance, Bot
from telepot.exception import TelegramError
import os
import socket
import collections
from django.utils.translation import ugettext as _
from urllib3.exceptions import ProtocolError
from .models import TelegramBot, ChatException, MessageQueue
from chatbot.models import MessageHistory
from accounts_app.models import UserProfile
from django.conf import settings
token = getattr(settings, 'TELEGRAM_BOT_TOKEN')
class DjingTelebot(helper.ChatHandler):
_current_user = None
_dialog_fn = None
_chat_id = 0
def __init__(self, seed_tuple, **kwargs):
super(DjingTelebot, self).__init__(seed_tuple, **kwargs)
self.cmds = {
'ping': self.ping,
'iam': self.say_me
}
# отвечаем пользователю
def _sent_reply(self, text):
self.sender.sendMessage(text)
# задаём вопрос пользователю, и ожидаем ответ в fn
def _question(self, text, fn):
if not isinstance(fn, collections.Callable):
raise TypeError
self._dialog_fn = fn
if text is not None:
self._sent_reply(text)
# сохраняем сообщение в базе
def _message_log(self, msg):
if self._current_user is None:
self._question(None, self.question_name)
return False
MessageHistory.objects.create(
user=self._current_user,
message=msg
)
return True
# Начинаем диалог
# @seed - chat_id
def open(self, initial_msg, seed):
content_type, chat_type, chat_id = glance(initial_msg)
if content_type != 'text':
return True
self._chat_id = chat_id
try:
tbot = TelegramBot.objects.get(chat_id=seed)
self._current_user = tbot.user
self._message_log(initial_msg['text'])
except TelegramBot.DoesNotExist:
self._question(_("Let's get acquainted, what is your name? Write your login from billing."),
self.question_name)
return True # prevent on_message() from being called on the initial message
# получаем сообщение
def on_chat_message(self, msg):
content_type, chat_type, chat_id = glance(msg)
if content_type != 'text':
return
self._chat_id = chat_id
text = msg['text'].lower()
# выполняем комманды если они есть
if text in self.cmds.keys():
self.cmds[text]()
elif self._dialog_fn is not None:
if not callable(self._dialog_fn):
raise TypeError
self._dialog_fn(text)
self._dialog_fn = None
else:
self._sent_reply(_('I do not know the answer to this yet.'))
if not self._message_log(text):
return
# спрашиваем имя пользователя
def question_name(self, username):
try:
profile = UserProfile.objects.get(username=username)
self._current_user = profile
try:
TelegramBot.objects.get(user=profile)
except TelegramBot.DoesNotExist:
if self._chat_id == 0:
raise ChatException('telebot.py. def question_name: Chat id is empty')
TelegramBot.objects.create(
user=profile,
chat_id=self._chat_id
)
except UserProfile.DoesNotExist:
self._question(
_("You are not found in the database, check that it correctly pointed out its LOGIN. Try again"),
self.question_name)
return
self._sent_reply(
_("Yes, it's nice to meet %(username)s, I will notify you about events in billing. Successful work ;)")
% {'username': profile.get_full_name()})
# заканчивается время диалога
# ex - время ожидания (timeout=ex в pave_event_space)
def on_close(self, ex):
self._current_user = None
self._dialog_fn = None
self._chat_id = 0
def ping(self, ip=None):
if ip is None:
self._question(_("Let's ping, write ip. It will be necessary to wait 10 seconds"), self.ping)
return
try:
socket.inet_aton(ip)
ret = os.popen('`which ping` -c 10 %s' % ip).read()
self._sent_reply(ret)
except socket.error:
self._question(_("It's not like ip address, try again"), self.ping)
def say_me(self):
self._sent_reply(_("You're '%s', right?") % self._current_user.get_full_name())
# Just sending text to specified account
def send_notify(msg_text, account, tag='none'):
try:
MessageQueue.objects.push(msg=msg_text, user=account, tag=tag)
if token is None:
raise ChatException(_('Telegram bot token not found'))
tb = TelegramBot.objects.get(user=account)
tbot = Bot(token)
tbot.sendMessage(tb.chat_id, msg_text)
except TelegramBot.DoesNotExist:
raise ChatException(_("Recipient '%s' does not subscribed on notifications") % account.get_full_name())
except ProtocolError as e:
raise ChatException('ProtocolError: %s' % e)
except TelegramError as e:
raise ChatException('Telegram error: %s' % e)

3
chatbot/views.py

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

3
clientsideapp/views.py

@ -5,6 +5,7 @@ from django.db import transaction
from django.utils.translation import gettext_lazy as _, gettext
from abonapp.models import AbonLog, InvoiceForPayment, Abon
from abonapp.tasks import customer_nas_command
from djing.lib.decorators import json_view
from tariff_app.models import Tariff
from taskapp.models import Task
@ -58,7 +59,7 @@ def buy_service(request, srv_id):
service, None,
_("Buy the service via user side, service '%s'") % service
)
abon.nas_sync_self()
customer_nas_command.delay(abon.pk, 'sync')
messages.success(
request,
_("The service '%s' wan successfully activated") % service.title

14
devapp/base_intr.py

@ -70,7 +70,7 @@ class DevBase(object, metaclass=ABCMeta):
def validate_extra_snmp_info(v: str) -> None:
"""
Validate extra snmp field for each device.
If validation failed then raise en exception from djing.lib.tln.ValidationError
If validation failed then raise en exception from devapp.onu_config.ExpectValidationError
with description of error.
:param v: String value for validate
"""
@ -89,12 +89,13 @@ class DevBase(object, metaclass=ABCMeta):
class BasePort(object, metaclass=ABCMeta):
def __init__(self, num, name, status, mac, speed):
def __init__(self, num, name, status, mac, speed, writable=False):
self.num = int(num)
self.nm = name
self.st = status
self._mac = mac
self.sp = speed
self.writable = writable
@abstractmethod
def disable(self):
@ -120,7 +121,10 @@ class SNMPBaseWorker(object, metaclass=ABCMeta):
def start_ses(self):
if self.ses is None:
self.ses = Session(hostname=self._ip, community=self._community, version=self._ver)
self.ses = Session(
hostname=self._ip, community=self._community,
version=self._ver
)
def set_int_value(self, oid: str, value):
self.start_ses()
@ -139,4 +143,6 @@ class SNMPBaseWorker(object, metaclass=ABCMeta):
def get_item(self, oid):
self.start_ses()
return self.ses.get(oid).value
v = self.ses.get(oid).value
if v != 'NOSUCHINSTANCE':
return v

174
devapp/dev_types.py

@ -2,11 +2,12 @@ import re
from typing import AnyStr, Iterable, Optional, Dict
from datetime import timedelta
from easysnmp import EasySNMPTimeoutError
from pexpect import TIMEOUT
from transliterate import translit
from django.utils.translation import gettext_lazy as _, gettext
from djing.lib import RuTimedelta, safe_int
from djing.lib.tln.tln import ValidationError as TlnValidationError, register_onu_ZTE_F660
from devapp.onu_config import register_f601_onu, register_f660_onu, ExpectValidationError, OnuZteRegisterError
from .base_intr import (
DevBase, SNMPBaseWorker, BasePort, DeviceImplementationError,
ListOrError, DeviceConfigurationError
@ -43,7 +44,7 @@ def plain_ip_device_mon_template(device) -> Optional[AnyStr]:
class DLinkPort(BasePort):
def __init__(self, num, name, status, mac, speed, snmp_worker):
BasePort.__init__(self, num, name, status, mac, speed)
BasePort.__init__(self, num, name, status, mac, speed, writable=True)
if not issubclass(snmp_worker.__class__, SNMPBaseWorker):
raise TypeError
self.snmp_worker = snmp_worker
@ -78,20 +79,18 @@ class DLinkDevice(DevBase, SNMPBaseWorker):
stats = tuple(self.get_list('.1.3.6.1.2.1.2.2.1.7'))
macs = tuple(self.get_list('.1.3.6.1.2.1.2.2.1.6'))
speeds = tuple(self.get_list('.1.3.6.1.2.1.2.2.1.5'))
res = []
try:
for n in range(interfaces_count):
status = True if int(stats[n]) == 1 else False
res.append(DLinkPort(
yield DLinkPort(
n + 1,
nams[n] if len(nams) > 0 else '',
status,
macs[n] if len(macs) > 0 else _('does not fetch the mac'),
int(speeds[n]) if len(speeds) > 0 else 0,
self))
return res
self)
except IndexError:
return DeviceImplementationError('Dlink port index error'), res
return DeviceImplementationError('Dlink port index error')
def get_device_name(self):
return self.get_item('.1.3.6.1.2.1.1.1.0')
@ -137,7 +136,7 @@ class ONUdev(BasePort):
class OLTDevice(DevBase, SNMPBaseWorker):
has_attachable_to_subscriber = False
description = _('PON OLT')
description = 'PON OLT'
is_use_device_port = False
def __init__(self, dev_instance):
@ -161,7 +160,7 @@ class OLTDevice(DevBase, SNMPBaseWorker):
status=True if status == '3' else False,
mac=self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.3.%d' % n),
speed=0,
signal=int(signal) / 10 if signal != 'NOSUCHINSTANCE' else 0,
signal=int(signal or 0),
snmp_worker=self)
res.append(onu)
except EasySNMPTimeoutError as e:
@ -196,7 +195,7 @@ class OLTDevice(DevBase, SNMPBaseWorker):
class OnuDevice(DevBase, SNMPBaseWorker):
has_attachable_to_subscriber = True
description = _('PON ONU')
description = 'PON ONU BDCOM'
tech_code = 'bdcom_onu'
is_use_device_port = False
@ -240,10 +239,12 @@ class OnuDevice(DevBase, SNMPBaseWorker):
status = self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.26.%d' % num)
signal = self.get_item('.1.3.6.1.4.1.3320.101.10.5.1.5.%d' % num)
distance = self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.27.%d' % num)
mac = ':'.join('%x' % ord(i) for i in self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.3.%d' % num))
mac = self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.3.%d' % num)
if mac is not None:
mac = ':'.join('%x' % ord(i) for i in mac)
# uptime = self.get_item('.1.3.6.1.2.1.2.2.1.9.%d' % num)
signal = safe_int(signal)
if status.isdigit():
if status is not None and status.isdigit():
return {
'status': status,
'signal': signal / 10 if signal != 0 else 0,
@ -260,7 +261,7 @@ class OnuDevice(DevBase, SNMPBaseWorker):
try:
int(v)
except ValueError:
raise TlnValidationError(_('Onu snmp field must be en integer'))
raise ExpectValidationError(_('Onu snmp field must be en integer'))
def monitoring_template(self, *args, **kwargs) -> Optional[str]:
device = self.db_instance
@ -317,17 +318,15 @@ class EltexSwitch(DLinkDevice):
tech_code = 'eltex_sw'
def get_ports(self) -> ListOrError:
res = []
for i, n in enumerate(range(49, 77), 1):
speed = self.get_item('.1.3.6.1.2.1.2.2.1.5.%d' % n)
res.append(EltexPort(self,
i,
self.get_item('.1.3.6.1.2.1.31.1.1.1.18.%d' % n),
self.get_item('.1.3.6.1.2.1.2.2.1.8.%d' % n),
self.get_item('.1.3.6.1.2.1.2.2.1.6.%d' % n),
int(speed) if speed != 'NOSUCHINSTANCE' else 0,
))
return res
yield EltexPort(self,
num=i,
name=self.get_item('.1.3.6.1.2.1.31.1.1.1.18.%d' % n),
status=self.get_item('.1.3.6.1.2.1.2.2.1.8.%d' % n),
mac=self.get_item('.1.3.6.1.2.1.2.2.1.6.%d' % n),
speed=int(speed or 0)
)
def get_device_name(self):
return self.get_item('.1.3.6.1.2.1.1.5.0')
@ -353,7 +352,7 @@ def conv_signal(lvl: int) -> float:
class Olt_ZTE_C320(OLTDevice):
description = _('OLT ZTE C320')
description = 'OLT ZTE C320'
def get_fibers(self):
fibers = ({
@ -415,8 +414,45 @@ class Olt_ZTE_C320(OLTDevice):
return 'olt_ztec320.html'
def _reg_dev_zte(device, extra_data: Dict, reg_func):
if not extra_data:
raise DeviceConfigurationError(_('You have not info in extra_data '
'field, please fill it in JSON'))
ip = None
if device.ip_address:
ip = device.ip_address
elif device.parent_dev:
ip = device.parent_dev.ip_address
if ip:
mac = str(device.mac_addr) if device.mac_addr else None
# Format serial number from mac address
# because saved mac address was make from serial number
sn = "ZTEG%s" % ''.join('%.2X' % int(x, base=16) for x in mac.split(':')[-4:])
telnet = extra_data.get('telnet')
try:
onu_snmp = reg_func(
onu_mac=mac,
serial=sn,
zte_ip_addr=str(ip),
telnet_login=telnet.get('login'),
telnet_passw=telnet.get('password'),
telnet_prompt=telnet.get('prompt'),
onu_vlan=extra_data.get('default_vid')
)
if onu_snmp is not None:
device.snmp_extra = onu_snmp
device.save(update_fields=('snmp_extra',))
else:
raise DeviceConfigurationError('unregistered onu not found, sn=%s' % sn)
except TIMEOUT as e:
raise OnuZteRegisterError(e)
else:
raise DeviceConfigurationError('not have ip')
class ZteOnuDevice(OnuDevice):
description = _('ZTE PON ONU')
description = 'Zte ONU F660'
tech_code = 'zte_onu'
def get_details(self) -> Optional[Dict]:
@ -432,18 +468,26 @@ class ZteOnuDevice(OnuDevice):
status = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.1.%s.1' % fiber_addr)
signal = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.10.%s.1' % fiber_addr)
distance = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.12.1.1.18.%s.1' % fiber_addr)
name = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.11.2.1.1.%s' % fiber_addr)
ip_addr = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.16.1.1.10.%s' % fiber_addr)
vlans = self.get_item('.1.3.6.1.4.1.3902.1012.3.50.15.100.1.1.7.%s.1.1' % fiber_addr)
int_name = self.get_item('.1.3.6.1.4.1.3902.1012.3.28.1.1.3.%s' % fiber_addr)
onu_type = self.get_item('.1.3.6.1.4.1.3902.1012.3.28.1.1.1.%s' % fiber_addr)
sn = self.get_item('.1.3.6.1.4.1.3902.1012.3.28.1.1.5.%s' % fiber_addr)
if sn is not None:
sn = 'ZTEG%s' % ''.join('%.2X' % ord(x) for x in sn[-4:])
return {
'status': status,
'signal': conv_signal(safe_int(signal)),
'name': name,
'distance': int(distance) / 10 if distance != 'NOSUCHINSTANCE' else 0,
'ip_addr': ip_addr if ip_addr != 'NOSUCHINSTANCE' and ip_addr else None,
'vlans': vlans if vlans != 'NOSUCHINSTANCE' else None
'distance': safe_int(distance) / 10,
'ip_addr': ip_addr,
'vlans': vlans,
'serial': sn,
'int_name': int_name,
'onu_type': onu_type
}
except ValueError:
except IndexError:
pass
def get_template_name(self):
@ -456,7 +500,7 @@ class ZteOnuDevice(OnuDevice):
fiber_num, onu_port = v.split('.')
int(fiber_num), int(onu_port)
except ValueError:
raise TlnValidationError(_('Zte onu snmp field must be two dot separated integers'))
raise ExpectValidationError(_('Zte onu snmp field must be two dot separated integers'))
def monitoring_template(self, *args, **kwargs) -> Optional[str]:
device = self.db_instance
@ -483,42 +527,7 @@ class ZteOnuDevice(OnuDevice):
return '\n'.join(i for i in r if i)
def register_device(self, extra_data: Dict):
if not extra_data:
raise DeviceConfigurationError(_('You have not info in extra_data field, please fill it in JSON'))
device = self.db_instance
ip = None
if device.ip_address:
ip = device.ip_address
elif device.parent_dev:
ip = device.parent_dev.ip_address
if ip:
mac = str(device.mac_addr).encode()
# Format serial number from mac address
# because saved mac address was make from serial number
sn = (b'%.2X' % int(x, base=16) for x in mac.split(b':')[-4:])
sn = b"ZTEG%s" % b''.join(sn)
telnet = extra_data.get('telnet')
if telnet is None:
raise DeviceConfigurationError('For ZTE configuration needed "telnet" section in extra_data')
login = telnet.get('login')
password = telnet.get('password')
prompt = telnet.get('prompt')
default_vid = extra_data.get('default_vid')
if login is None or password is None or prompt is None:
raise DeviceConfigurationError('For ZTE configuration needed login, password and'
' prompt for telnet access in extra_data')
if default_vid is None:
raise DeviceConfigurationError('Please specify default vlan id "default_vid" for configuration onu')
stack_num, rack_num, fiber_num, new_onu_port_num = register_onu_ZTE_F660(
olt_ip=ip, onu_sn=sn, login_passwd=(login.encode(), password.encode()),
onu_mac=mac, prompt_title=prompt.encode(), vlan_id=int(default_vid)
)
bin_snmp_fiber_number = "10000{0:08b}{1:08b}00000000".format(rack_num, fiber_num)
snmp_fiber_num = int(bin_snmp_fiber_number, base=2)
device.snmp_extra = "%d.%d" % (snmp_fiber_num, new_onu_port_num)
device.save(update_fields=('snmp_extra',))
_reg_dev_zte(self.db_instance, extra_data, register_f660_onu)
def get_fiber_str(self):
dev = self.db_instance
@ -534,3 +543,34 @@ class ZteOnuDevice(OnuDevice):
return 'gpon-onu_1/%d/%d:%s' % (
rack_num, fiber_num, onu_port_num
)
class ZteF601(ZteOnuDevice):
description = 'Zte ONU F601'
def register_device(self, extra_data: Dict):
_reg_dev_zte(self.db_instance, extra_data, register_f601_onu)
class HuaweiSwitch(EltexSwitch):
description = _('Huawei switch')
is_use_device_port = True
has_attachable_to_subscriber = True
tech_code = 'huawei_s2300'
def get_ports(self):
interfaces_ids = self.get_list('.1.3.6.1.2.1.17.1.4.1.2')
if interfaces_ids is None:
raise DeviceImplementationError('Switch returned null')
for i, n in enumerate(interfaces_ids):
n = int(n)
speed = self.get_item('.1.3.6.1.2.1.2.2.1.5.%d' % n)
status = self.get_item('.1.3.6.1.2.1.2.2.1.8.%d' % n)
yield EltexPort(
self,
num=i+1,
name=self.get_item('.1.3.6.1.2.1.2.2.1.2.%d' % n), # name
status=int(status or 0), # status
mac=self.get_item('.1.3.6.1.2.1.2.2.1.6.%d' % n), # mac
speed=int(speed or 0) # speed
)

4
devapp/forms.py

@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from django.db import IntegrityError
from djing.lib import DuplicateEntry
from djing.lib.tln.tln import ValidationError as TlnValidationError
from devapp.onu_config import ExpectValidationError
from . import models
from djing import MAC_ADDR_REGEX, IP_ADDR_REGEX
@ -43,7 +43,7 @@ class DeviceForm(forms.ModelForm):
manager_class = device.get_manager_klass()
try:
manager_class.validate_extra_snmp_info(snmp_extra)
except TlnValidationError as e:
except ExpectValidationError as e:
raise ValidationError(
e, code='invalid'
)

49
devapp/locale/ru/LC_MESSAGES/django.po

@ -30,18 +30,10 @@ msgstr "Свич D'Link"
msgid "does not fetch the mac"
msgstr "не нашёл мак"
#: dev_types.py:140
msgid "PON OLT"
msgstr ""
#: dev_types.py:169 views.py:345 views.py:503
msgid "wait for a reply from the SNMP Timeout"
msgstr "Время ожидания ответа от SNMP истекло"
#: dev_types.py:199
msgid "PON ONU"
msgstr ""
#: dev_types.py:214
msgid "Ip address or parent device with ip address required for ONU device"
msgstr ""
@ -60,14 +52,6 @@ msgstr "Поле для snmp информации об ONU должно быть
msgid "Eltex switch"
msgstr "Элтекс свич"
#: dev_types.py:356
msgid "OLT ZTE C320"
msgstr ""
#: dev_types.py:419
msgid "ZTE PON ONU"
msgstr ""
#: dev_types.py:454
msgid "Zte onu snmp field must be two dot separated integers"
msgstr ""
@ -324,7 +308,7 @@ msgstr "Название типа свича"
#: templates/devapp/custom_dev_page/onu.html:22
#: templates/devapp/custom_dev_page/onu_for_zte.html:22
msgid "Attached user"
msgstr "Прикрепленный абонент"
msgstr "Прикреплённый абонент"
#: templates/devapp/custom_dev_page/onu.html:48
#: templates/devapp/custom_dev_page/onu_for_zte.html:50
@ -337,7 +321,6 @@ msgid "ONU error"
msgstr "ONU ошибка"
#: templates/devapp/custom_dev_page/onu.html:72
#: templates/devapp/custom_dev_page/onu_for_zte.html:75
msgid "Name on OLT"
msgstr "Имя на OLT"
@ -642,20 +625,26 @@ msgstr "Не заполнено поле 'Техническая информа
msgid "Fiber"
msgstr "Интерфейс"
#~ msgid "Device %(device_name)s is up"
#~ msgstr "%(device_name)s в сети"
msgid "Onu type"
msgstr "Тип onu"
msgid "Serial"
msgstr "Серийник"
msgid "Device %(device_name)s is up"
msgstr "%(device_name)s в сети"
#~ msgid "Device %(device_name)s is down"
#~ msgstr "%(device_name)s не в сети"
msgid "Device %(device_name)s is down"
msgstr "%(device_name)s не в сети"
#~ msgid "Device %(device_name)s is unreachable"
#~ msgstr "%(device_name)s недостижим"
msgid "Device %(device_name)s is unreachable"
msgstr "%(device_name)s недостижим"
#~ msgid "Device %(device_name)s getting undefined status code"
#~ msgstr "Устройство %(device_name)s получило не определённый код состояния"
msgid "Device %(device_name)s getting undefined status code"
msgstr "Устройство %(device_name)s получило не определённый код состояния"
#~ msgid "View"
#~ msgstr "Посмотреть"
msgid "View"
msgstr "Посмотреть"
#~ msgid "Enter valid JSON"
#~ msgstr "Введите данные в формате JSON"
msgid "Enter valid JSON"
msgstr "Введите данные в формате JSON"

120
devapp/migrations/0001_squashed_0005_device_ip_address_change.py

@ -0,0 +1,120 @@
# Generated by Django 2.1.1 on 2019-03-05 19:32
from django.db import migrations, models
import django.db.models.deletion
import djing.fields
import jsonfield.fields
class Migration(migrations.Migration):
replaces = [
('devapp', '0001_initial'),
('devapp', '0002_auto_20180409_1318'),
('devapp', '0003_auto_20180529_1311'),
('devapp', '0004_device_extra_data'),
('devapp', '0005_device_ip_address_change')
]
initial = True
dependencies = [
('group_app', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Device',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID'
)),
('ip_address', models.GenericIPAddressField(
blank=True, null=True, verbose_name='Ip address'
)),
('mac_addr', djing.fields.MACAddressField(
blank=True, integer=True,
null=True, unique=True,
verbose_name='Mac address'
)),
('comment', models.CharField(
max_length=256, verbose_name='Comment'
)),
('devtype', models.CharField(
choices=[('Dl', 'DLink switch'), ('Pn', 'PON OLT'),
('On', 'PON ONU BDCOM'), ('Ex', 'Eltex switch'),
('Zt', 'OLT ZTE C320'), ('Zo', 'Zte ONU F660'),
('Z6', 'Zte ONU F601'), ('Hw', 'Huawei switch')],
default='Dl', max_length=2,
verbose_name='Device type'
)),
('man_passw', models.CharField(
blank=True, max_length=16,
null=True, verbose_name='SNMP password'
)),
('status', models.CharField(
choices=[('und', 'Undefined'), ('up', 'Up'),
('unr', 'Unreachable'), ('dwn', 'Down')],
default='und', max_length=3,
verbose_name='Status'
)),
('is_noticeable', models.BooleanField(
default=False,
verbose_name='Send notify when monitoring state changed'
)),
('group', models.ForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='group_app.Group', verbose_name='Device group'
)),
('parent_dev', models.ForeignKey(
blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='devapp.Device', verbose_name='Parent device'
)),
('extra_data', jsonfield.fields.JSONField(
blank=True,
help_text='Extra data in JSON format. You may use it for your custom data',
null=True, verbose_name='Extra data'
)),
('snmp_extra', models.CharField(
blank=True, max_length=256,
null=True, verbose_name='SNMP extra info'
))
],
options={
'verbose_name': 'Device',
'verbose_name_plural': 'Devices',
'db_table': 'dev',
'ordering': ('id',),
},
),
migrations.CreateModel(
name='Port',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True,
serialize=False, verbose_name='ID'
)),
('num', models.PositiveSmallIntegerField(
default=0, verbose_name='Number'
)),
('descr', models.CharField(
blank=True, max_length=60, null=True,
verbose_name='Description'
)),
('device', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='devapp.Device', verbose_name='Device'
)),
],
options={
'verbose_name': 'Port',
'verbose_name_plural': 'Ports',
'db_table': 'dev_port',
'permissions': (('can_toggle_ports', 'Can toggle ports'),),
'ordering': ('num',),
'unique_together': {('device', 'num')},
},
),
]

6
devapp/models.py

@ -32,7 +32,9 @@ class Device(models.Model):
('On', dev_types.OnuDevice),
('Ex', dev_types.EltexSwitch),
('Zt', dev_types.Olt_ZTE_C320),
('Zo', dev_types.ZteOnuDevice)
('Zo', dev_types.ZteOnuDevice),
('Z6', dev_types.ZteF601),
('Hw', dev_types.HuaweiSwitch)
)
devtype = models.CharField(_('Device type'), max_length=2, default=DEVICE_TYPES[0][0],
choices=MyChoicesAdapter(DEVICE_TYPES))
@ -108,7 +110,7 @@ class Port(models.Model):
class Meta:
db_table = 'dev_port'
unique_together = (('device', 'num'),)
unique_together = ('device', 'num')
permissions = (
('can_toggle_ports', _('Can toggle ports')),
)

6
devapp/onu_config/__init__.py

@ -0,0 +1,6 @@
from .f601 import register_onu as register_f601_onu
from .f660 import register_onu as register_f660_onu
from .base import (
ZteOltConsoleError, OnuZteRegisterError,
ZTEFiberIsFull, ZteOltLoginFailed, ExpectValidationError
)

86
devapp/onu_config/base.py

@ -0,0 +1,86 @@
import re
import sys
from pexpect import spawn
class ZteOltConsoleError(Exception):
pass
class OnuZteRegisterError(ZteOltConsoleError):
pass
class ZTEFiberIsFull(ZteOltConsoleError):
pass
class ZteOltLoginFailed(ZteOltConsoleError):
pass
class ExpectValidationError(ValueError):
pass
class MySpawn(spawn):
def __init__(self, *args, **kwargs):
super(MySpawn, self).__init__(encoding='utf-8', *args, **kwargs)
self.logfile = sys.stdout
def do_cmd(self, c, prompt):
self.sendline(c)
return self.expect_exact(prompt)
def get_lines(self):
return self.buffer.split('\r\n')
def get_lines_before(self):
return self.before.split('\r\n')
def parse_onu_name(onu_name: str, name_regexp=re.compile('[/:_]')):
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
}
def get_unregistered_onu(lines, serial):
for line in lines:
if line.startswith('gpon-onu_'):
spls = re.split(r'\s+', line)
if len(spls) > 2:
if serial == spls[1]:
onu_index, sn, state = spls[:3]
return parse_onu_name(onu_index)
def get_free_registered_onu_number(lines):
onu_type_regexp = re.compile(r'^\s{1,5}onu \d{1,3} type [-\w\d]{4,64} sn \w{4,64}$')
onu_olt_num = None
i = 0
for l in lines:
if onu_type_regexp.match(l):
# match line
i += 1
onu, num, onu_type, onu_type, sn, onu_sn = l.split()
onu_olt_num = int(num)
if onu_olt_num > i:
return i
return onu_olt_num + 1
def sn_to_mac(sn: str):
t = sn[4:].lower()
r = tuple(t[i:i + 2] for i in range(0, len(t), 2))
return '45:47:%s' % ':'.join(r)
def onu_conv(rack_num: int, fiber_num: int, port_num: int):
r = "10000{0:08b}{1:08b}00000000".format(rack_num, fiber_num)
snmp_fiber_num = int(r, base=2)
return "%d.%d" % (snmp_fiber_num, port_num)

165
devapp/onu_config/f601.py

@ -0,0 +1,165 @@
import re
from typing import Optional
from djing.lib import process_lock
from . import base
def get_onu_template(vlan_id: int, mac_addr: str):
template = (
'sn-bind enable sn',
'tcont 1 profile HSI_100',
'gemport 1 unicast tcont 1 dir both',
'switchport mode hybrid vport 1',
'switchport vlan %d tag vport 1' % vlan_id,
'port-location format flexible-syntax vport 1',
'port-location sub-option remote-id enable vport 1',
'port-location sub-option remote-id name %s vport 1' % mac_addr,
'dhcp-option82 enable vport 1',
'dhcp-option82 trust true replace vport 1',
'ip dhcp snooping enable vport 1'
)
return template
def get_pon_mng_template(vlan_id: int):
template = (
'service HSI type internet gemport 1 vlan %d' % vlan_id,
'loop-detect ethuni eth_0/1 enable',
'vlan port eth_0/1 mode tag vlan %d' % vlan_id,
'dhcp-ip ethuni eth_0/1 from-internet'
)
return template
def appy_config(onu_mac: str, sn: str, hostname: str, login: str, password: str, prompt: str, vlan: int):
onu_type = 'ZTE-F601'
# Входим
ch = base.MySpawn('telnet %s' % hostname)
ch.timeout = 15
ch.expect_exact('Username:')
ch.do_cmd(login, 'Password:')
choice = ch.do_cmd(password, ['bad password.', '%s#' % prompt])
if choice == 0:
raise base.ZteOltLoginFailed
ch.do_cmd('terminal length 0', '%s#' % prompt)
choice = ch.do_cmd('show gpon onu uncfg', ['No related information to show', '%s#' % prompt])
if choice == 0:
ch.close()
raise base.OnuZteRegisterError('unregistered onu not found, sn=%s' % sn)
elif choice == 1:
# Получим незареганные onu
unregistered_onu = base.get_unregistered_onu(
lines=ch.get_lines_before(),
serial=sn
)
if unregistered_onu is None:
ch.close()
raise base.OnuZteRegisterError('unregistered onu not found, sn=%s' % sn)
stack_num = int(unregistered_onu.get('stack_num'))
rack_num = int(unregistered_onu.get('rack_num'))
fiber_num = int(unregistered_onu.get('fiber_num'))
# Получим последнюю зарегистрированную onu
ch.do_cmd('show run int gpon-olt_%(stack)s/%(rack)s/%(fiber)s' % {
'stack': stack_num,
'rack': rack_num,
'fiber': fiber_num
}, '%s#' % prompt)
free_onu_number = base.get_free_registered_onu_number(
ch.get_lines_before()
)
if free_onu_number > 126:
ch.close()
raise base.ZTEFiberIsFull('olt fiber %d is full' % fiber_num)
# enter to config
ch.do_cmd('conf t', '%s(config)#' % prompt)
int_addr = '%d/%d/%d' % (
stack_num,
rack_num,
fiber_num
)
# go to olt interface
ch.do_cmd('interface gpon-olt_%s' % int_addr, '%s(config-if)#' % prompt)
# register onu on olt interface
ch.do_cmd('onu %d type %s sn %s' % (
free_onu_number,
onu_type,
sn
), '%s(config-if)#' % prompt)
# Exit from int olt
ch.do_cmd('exit', '%s(config)#' % prompt)
# Enter to int onu
ch.do_cmd('int gpon-onu_%(int_addr)s:%(onu_num)d' % {
'int_addr': int_addr,
'onu_num': free_onu_number
}, '%s(config-if)#' % prompt)
# Apply int onu config
template = get_onu_template(vlan, onu_mac)
for line in template:
ch.do_cmd(line, '%s(config-if)#' % prompt)
# Exit
ch.do_cmd('exit', '%s(config)#' % prompt)
# Enter to pon-onu-mng
ch.do_cmd('pon-onu-mng gpon-onu_%(int_addr)s:%(onu_num)d' % {
'int_addr': int_addr,
'onu_num': free_onu_number
}, '%s(gpon-onu-mng)#' % prompt)
# Apply config to pon-onu-mng
for line in get_pon_mng_template(vlan):
ch.do_cmd(line, '%s(gpon-onu-mng)#' % prompt)
# Exit
ch.do_cmd('exit', '%s(config)#' % prompt)
ch.close()
return base.onu_conv(
rack_num=rack_num,
fiber_num=fiber_num,
port_num=free_onu_number
)
else:
ch.close()
raise base.ZteOltConsoleError("I don't know what choice:", choice)
# Main Entry point
@process_lock
def register_onu(onu_mac: Optional[str], serial: str, zte_ip_addr: str, telnet_login: str,
telnet_passw: str, telnet_prompt: str, onu_vlan: int):
if not re.match(r'^ZTEG[0-9A-F]{8}$', serial):
raise base.ExpectValidationError('Serial not valid, match: ^ZTEG[0-9A-F]{8}$')
if not isinstance(onu_vlan, int):
onu_vlan = int(onu_vlan)
if onu_mac is None:
onu_mac = base.sn_to_mac(serial)
IP4_ADDR_REGEX = (
r'^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
)
if not re.match(IP4_ADDR_REGEX, zte_ip_addr):
raise base.ExpectValidationError('ip address for zte not valid')
return appy_config(onu_mac, serial, zte_ip_addr, telnet_login,
telnet_passw, telnet_prompt, onu_vlan)

141
devapp/onu_config/f660.py

@ -0,0 +1,141 @@
import re
from typing import Optional
from djing.lib import process_lock
from . import base
def get_onu_template(vlan_id: int, mac_addr: str):
template = (
'switchport mode hybrid vport 1',
'switchport vlan %d tag vport 1' % vlan_id,
'port-location format flexible-syntax vport 1',
'port-location sub-option remote-id enable vport 1',
'port-location sub-option remote-id name %s vport 1' % mac_addr,
'dhcp-option82 enable vport 1',
'dhcp-option82 trust true replace vport 1',
'ip dhcp snooping enable vport 1'
)
return template
def appy_config(onu_mac: str, sn: str, hostname: str, login: str, password: str, prompt: str, vlan: int):
onu_type = 'ZTE-F660'
# Входим
ch = base.MySpawn('telnet %s' % hostname)
ch.timeout = 15
ch.expect_exact('Username:')
ch.do_cmd(login, 'Password:')
choice = ch.do_cmd(password, ['bad password.', '%s#' % prompt])
if choice == 0:
raise base.ZteOltLoginFailed
ch.do_cmd('terminal length 0', '%s#' % prompt)
choice = ch.do_cmd('show gpon onu uncfg', ['No related information to show', '%s#' % prompt])
if choice == 0:
ch.close()
raise base.OnuZteRegisterError('unregistered onu not found, sn=%s' % sn)
elif choice == 1:
# Получим незареганные onu
unregistered_onu = base.get_unregistered_onu(
lines=ch.get_lines_before(),
serial=sn
)
if unregistered_onu is None:
ch.close()
raise base.OnuZteRegisterError('unregistered onu not found, sn=%s' % sn)
stack_num = int(unregistered_onu.get('stack_num'))
rack_num = int(unregistered_onu.get('rack_num'))
fiber_num = int(unregistered_onu.get('fiber_num'))
# Получим последнюю зарегистрированную onu
ch.do_cmd('show run int gpon-olt_%(stack)s/%(rack)s/%(fiber)s' % {
'stack': stack_num,
'rack': rack_num,
'fiber': fiber_num
}, '%s#' % prompt)
free_onu_number = base.get_free_registered_onu_number(
ch.get_lines_before()
)
if free_onu_number > 126:
ch.close()
raise base.ZTEFiberIsFull('olt fiber %d is full' % fiber_num)
# enter to config
ch.do_cmd('conf t', '%s(config)#' % prompt)
int_addr = '%d/%d/%d' % (
stack_num,
rack_num,
fiber_num
)
# go to olt interface
ch.do_cmd('interface gpon-olt_%s' % int_addr, '%s(config-if)#' % prompt)
# register onu on olt interface
ch.do_cmd('onu %d type %s sn %s' % (
free_onu_number,
onu_type,
sn
), '%s(config-if)#' % prompt)
# register onu profile on olt interface
ch.do_cmd(
'onu %d profile line ZTE-F660-LINE remote ZTE-F660-ROUTER' % free_onu_number,
'%s(config-if)#' % prompt
)
# Exit from int olt
ch.do_cmd('exit', '%s(config)#' % prompt)
# Enter to int onu
ch.do_cmd('int gpon-onu_%(int_addr)s:%(onu_num)d' % {
'int_addr': int_addr,
'onu_num': free_onu_number
}, '%s(config-if)#' % prompt)
# Apply int onu config
template = get_onu_template(vlan, onu_mac)
for line in template:
ch.do_cmd(line, '%s(config-if)#' % prompt)
# Exit
ch.do_cmd('exit', '%s(config)#' % prompt)
ch.do_cmd('exit', '%s#' % prompt)
ch.close()
return base.onu_conv(
rack_num=rack_num,
fiber_num=fiber_num,
port_num=free_onu_number
)
else:
ch.close()
raise base.ZteOltConsoleError("I don't know what choice:", choice)
# Main Entry point
@process_lock
def register_onu(onu_mac: Optional[str], serial: str, zte_ip_addr: str, telnet_login: str,
telnet_passw: str, telnet_prompt: str, onu_vlan: int):
if not re.match(r'^ZTEG[0-9A-F]{8}$', serial):
raise base.ExpectValidationError('Serial not valid, match: ^ZTEG[0-9A-F]{8}$')
if not isinstance(onu_vlan, int):
onu_vlan = int(onu_vlan)
if onu_mac is None:
onu_mac = base.sn_to_mac(serial)
IP4_ADDR_REGEX = (
r'^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
)
if not re.match(IP4_ADDR_REGEX, zte_ip_addr):
raise base.ExpectValidationError('ip address for zte not valid')
return appy_config(onu_mac, serial, zte_ip_addr, telnet_login,
telnet_passw, telnet_prompt, onu_vlan)

18
devapp/templates/devapp/custom_dev_page/generic_switch.html

@ -40,13 +40,19 @@
<a href="javascript:void(0);" class="port-img" title="{{ port.nm }}">
<b>{{ port.num }}</b>
</a>
{% if port.st %}
<a href="{% url 'devapp:port_toggle' dev.group.pk|default:0 dev.id port.num 0 %}" class="btn btn-xs btn-danger" title="{% trans 'Disable port' %}">
<span class="glyphicon glyphicon-off"></span>
</a>
{% if port.writable %}
{% if port.st %}
<a href="{% url 'devapp:port_toggle' dev.group.pk|default:0 dev.id port.num 0 %}" class="btn btn-xs btn-danger" title="{% trans 'Disable port' %}">
<span class="glyphicon glyphicon-off"></span>
</a>
{% else %}
<a href="{% url 'devapp:port_toggle' dev.group.pk|default:0 dev.id port.num 1 %}" class="btn btn-xs btn-success" title="{% trans 'Enable port' %}">
<span class="glyphicon glyphicon-ok"></span>
</a>
{% endif %}
{% else %}
<a href="{% url 'devapp:port_toggle' dev.group.pk|default:0 dev.id port.num 1 %}" class="btn btn-xs btn-success" title="{% trans 'Enable port' %}">
<span class="glyphicon glyphicon-ok"></span>
<a href="#" class="btn btn-xs btn-danger disabled">
<span class="glyphicon glyphicon-off"></span>
</a>
{% endif %}
</div>

5
devapp/templates/devapp/custom_dev_page/onu.html

@ -21,8 +21,7 @@
{% 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>
<a href="{% url 'abonapp:abon_home' da.group.pk da.username %}">{{ da.get_full_name }}</a>
{% else %}
{{ da.get_full_name }}
{% endif %}
@ -32,7 +31,7 @@
<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">
<a href="{% url 'devapp:view' pdgrp.pk pdev.pk %}" title="{{ pdev.mac_addr|default:'' }}">
{{ pdev.ip_address|default:'-' }} {{ pdev.comment }}
</a>
{% endwith %}

15
devapp/templates/devapp/custom_dev_page/onu_for_zte.html

@ -22,8 +22,7 @@
{% 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>
<a href="{% url 'abonapp:abon_home' da.group.pk da.username %}">{{ da.get_full_name }}</a>
{% else %}
{{ da.get_full_name }}
{% endif %}
@ -33,9 +32,7 @@
<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">
<a href="{% url 'devapp:view' pdgrp.pk pdev.pk %}" title="{{ pdev.mac_addr|default:'' }}">
{{ pdev.ip_address|default:'-' }} {{ pdev.comment }}
</a>
{% endwith %}
@ -73,15 +70,17 @@
</div>
<div class="media-body">
<b>{% trans 'Name on OLT' %}</b>: {{ onu_details.name }}<br>
<b>{% trans 'Distance(m)' %}</b>: {{ onu_details.distance }}<br>
<b>{% trans 'Distance(m)' %}</b>: {{ onu_details.distance|default:'-' }}<br>
<b>{% trans 'Signal' %}</b>: {{ onu_details.signal }}<br>
{% if onu_details.ip_addr %}
<b>{% trans 'Ip addr' %}</b>: {{ onu_details.ip_addr }}<br>
{% endif %}
{% if onu_details.vlans %}
<b>{% trans 'VLan list' %}</b>: {{ onu_details.vlans }}
<b>{% trans 'VLan list' %}</b>: {{ onu_details.vlans }}<br>
{% endif %}
<b>{% trans 'Serial' %}</b>: {{ onu_details.serial|default:'-' }}<br>
<b>{% trans 'Onu type' %}</b>: {{ onu_details.onu_type|default:'-' }}<br>
<b>{% trans 'Name' %}</b>: {{ onu_details.int_name|default:'-' }}
</div>
</div>

2
devapp/templates/devapp/group_list.html

@ -35,7 +35,7 @@
<a href="{% url 'devapp:devices_null_group' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-list-alt"></span> {% trans 'Devices without group' %}
</a>
<a href="{% url 'devapp:nagios_objects_conf' %}" class="btn btn-default" target="_blank">
<a href="{% url 'devapp:nagios_objects_conf' %}" class="btn btn-default">
<span class="glyphicon glyphicon-export"></span> {% trans 'Export to nagios objects' %}
</a>
</td>

52
devapp/urls.py

@ -5,52 +5,32 @@ app_name = 'devapp'
urlpatterns = [
path('', views.GroupsListView.as_view(), name='group_list'),
path('devices_without_groups/',
views.DevicesWithoutGroupsListView.as_view(),
name='devices_null_group'),
path('devices_without_groups/', views.DevicesWithoutGroupsListView.as_view(), name='devices_null_group'),
path('fix_onu/', views.fix_onu, name='fix_onu'),
path('<int:group_id>/', views.DevicesListView.as_view(), name='devs'),
path('<int:group_id>/add/', views.DeviceCreateView.as_view(), name='add'),
path('<int:group_id>/<int:device_id>/', views.devview, name='view'),
path('<int:group_id>/<int:device_id>/del/',
views.DeviceDeleteView.as_view(), name='del'),
path('<int:group_id>/<int:device_id>/add/', views.add_single_port,
name='add_port'),
path('<int:group_id>/<int:device_id>/edit/', views.DeviceUpdate.as_view(),
name='edit'),
path('<int:group_id>/<int:device_id>/edit_extra/',
views.DeviceUpdateExtra.as_view(), name='extra_data_edit'),
path(
'<int:group_id>/<int:device_id>/ports/<int:port_id>/fix_port_conflict/',
views.fix_port_conflict,
name='fix_port_conflict'),
path(
'<int:group_id>/<int:device_id>/ports/<int:port_id>/show_subscriber_on_port/',
views.ShowSubscriberOnPort.as_view(), name='show_subscriber_on_port'),
path('<int:group_id>/<int:device_id>/ports_add/', views.add_ports,
name='add_ports'),
path('<int:group_id>/<int:device_id>/register_device/',
views.register_device, name='dev_register'),
re_path('^(\d+)/(?P<device_id>\d+)/(?P<port_id>\d+)_(?P<status>[0-1]{1})$',
views.toggle_port, name='port_toggle'),
path('<int:group_id>/<int:device_id>/<int:port_id>/del/',
views.delete_single_port, name='del_port'),
path('<int:group_id>/<int:device_id>/<int:port_id>/edit/',
views.EditSinglePort.as_view(), name='edit_port'),
path('fix_device_group/<int:device_id>/', views.fix_device_group,
name='fix_device_group'),
path('<int:group_id>/<int:device_id>/del/', views.DeviceDeleteView.as_view(), name='del'),
path('<int:group_id>/<int:device_id>/add/', views.add_single_port, name='add_port'),
path('<int:group_id>/<int:device_id>/edit/', views.DeviceUpdate.as_view(), name='edit'),
path('<int:group_id>/<int:device_id>/edit_extra/', views.DeviceUpdateExtra.as_view(), name='extra_data_edit'),
path('<int:group_id>/<int:device_id>/ports/<int:port_id>/fix_port_conflict/', views.fix_port_conflict, name='fix_port_conflict'),
path('<int:group_id>/<int:device_id>/ports/<int:port_id>/show_subscriber_on_port/', views.ShowSubscriberOnPort.as_view(), name='show_subscriber_on_port'),
path('<int:group_id>/<int:device_id>/ports_add/', views.add_ports, name='add_ports'),
path('<int:group_id>/<int:device_id>/register_device/', views.register_device, name='dev_register'),
re_path('^(\d+)/(?P<device_id>\d+)/(?P<port_id>\d+)_(?P<status>[0-1]{1})$', views.toggle_port, name='port_toggle'),
path('<int:group_id>/<int:device_id>/<int:port_id>/del/', views.delete_single_port, name='del_port'),
path('<int:group_id>/<int:device_id>/<int:port_id>/edit/', views.EditSinglePort.as_view(), name='edit_port'),
path('fix_device_group/<int:device_id>/', views.fix_device_group, name='fix_device_group'),
path('search_dev/', views.search_dev),
# ZTE ports under fibers
path('<int:group_id>/<int:device_id>/<int:fiber_id>/',
views.zte_port_view_uncfg, name='zte_port_view_uncfg'),
path('<int:group_id>/<int:device_id>/<int:fiber_id>/', views.zte_port_view_uncfg, name='zte_port_view_uncfg'),
# Monitoring api
path('on_device_event/', views.OnDeviceMonitoringEvent.as_view()),
# Nagios mon generate
path('nagios/hosts/', views.nagios_objects_conf,
name='nagios_objects_conf'),
path('api/getall/', views.DevicesGetListView.as_view(),
name='nagios_get_all_hosts')
path('nagios/hosts/', views.nagios_objects_conf, name='nagios_objects_conf'),
path('api/getall/', views.DevicesGetListView.as_view(), name='nagios_get_all_hosts')
]

58
devapp/views.py

@ -3,7 +3,6 @@ from ipaddress import ip_address
from abonapp.models import Abon
from accounts_app.models import UserProfile
from chatbot.models import ChatException
from devapp.base_intr import DeviceImplementationError
from django.conf import settings
from django.contrib import messages
@ -22,17 +21,16 @@ from djing.lib import safe_int, ProcessLocked, DuplicateEntry
from djing.lib.decorators import json_view
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 \
permission_required_or_403 as permission_required
from messenger.tasks import multicast_viber_notify
from guardian.decorators import permission_required_or_403 as permission_required
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
from devapp.forms import DeviceForm, PortForm, DeviceExtraDataForm
from devapp.models import Device, Port, DeviceDBException, DeviceMonitoringException
from devapp.tasks import onu_register
from devapp import onu_config
class DevicesListView(LoginAdminPermissionMixin,
@ -485,9 +483,8 @@ def devview(request, group_id: int, device_id: int):
template_name = 'generic_switch.html'
try:
if device.ip_address:
if not ping(str(device.ip_address)):
messages.error(request, _('Dot was not pinged'))
if device.ip_address and not ping(str(device.ip_address)):
messages.error(request, _('Dot was not pinged'))
if device.man_passw:
manager = device.get_manager_object()
ports = tuple(manager.get_ports())
@ -498,13 +495,15 @@ def devview(request, group_id: int, device_id: int):
template_name = manager.get_template_name()
else:
messages.warning(request, _('Not Set snmp device password'))
return render(request, 'devapp/custom_dev_page/' + template_name, {
'dev': device,
'ports': ports,
'dev_accs': Abon.objects.filter(device=device),
'dev_manager': manager,
'ports_db': Port.objects.filter(device=device).annotate(
num_abons=Count('abon')),
num_abons=Count('abon')
),
})
except EasySNMPError as e:
messages.error(request,
@ -726,21 +725,22 @@ class OnDeviceMonitoringEvent(global_base_views.SecureApiView):
}
recipients = UserProfile.objects.get_profiles_by_group(
device_down.group.pk)
device_down.group.pk).filter(flags=UserProfile.flags.notify_mon)
multicast_email_notify.delay(msg_text=gettext(notify_text) % {
user_ids = tuple(recipient.pk for recipient in recipients.only('pk').iterator())
text = gettext(notify_text) % {
'device_name': "%s(%s) %s" % (
device_down.ip_address,
device_down.mac_addr,
device_down.comment
)
}, account_ids=(
recipient.pk for recipient in recipients.only('pk').iterator()
))
}
multicast_email_notify.delay(msg_text=text, account_ids=user_ids)
multicast_viber_notify.delay(None, account_id_list=user_ids, message_text=text)
return {
'text': 'notification successfully sent'
}
except ChatException as e:
except ValueError as e:
return {
'text': str(e)
}
@ -802,17 +802,25 @@ def register_device(request, group_id: int, device_id: int):
try:
device.register_device()
status = 0
except OnuZteRegisterError:
except onu_config.OnuZteRegisterError:
text = format_msg(gettext('Unregistered onu not found'), 'eye-close')
except ZteOltLoginFailed:
text = format_msg(gettext('Wrong login or password for telnet access'),
'lock')
except (ConnectionRefusedError, ZteOltConsoleError) as e:
except onu_config.ZteOltLoginFailed:
text = format_msg(
gettext('Wrong login or password for telnet access'),
'lock'
)
except (
ConnectionRefusedError, onu_config.ZteOltConsoleError,
onu_config.ExpectValidationError, onu_config.ZTEFiberIsFull
) as e:
text = format_msg(e, 'exclamation-sign')
except DeviceImplementationError as e:
text = format_msg(e, 'wrench')
text = format_msg(str(e), 'wrench')
except ProcessLocked:
text = format_msg(gettext('Process locked by another process'), 'time')
text = format_msg(
gettext('Process locked by another process'),
'time'
)
else:
text = format_msg(msg='ok', icon='ok')
return {

2
dialing_app/templates/index.html

@ -60,7 +60,7 @@
<span class="glyphicon glyphicon-play"></span>
<audio preload="metadata" src="{{ lurl|default:'#' }}"></audio>
</button>
<a href="{{ lurl|default:'#' }}" class="btn btn-default disabled" target="_blank" title="{% trans 'Download' %}">
<a href="{{ lurl|default:'#' }}" class="btn btn-default disabled" title="{% trans 'Download' %}">
<span class="glyphicon glyphicon-download-alt"></span>
</a>
</td>

2
dialing_app/templatetags/telephone_filters.py

@ -14,7 +14,7 @@ def abon_if_telephone(value):
if value[0] != '+':
value = '+' + value
url = resolve_url('dialapp:to_abon', tel=value)
a = '<a href="%s" target="_blank">%s</a>' % (url, value)
a = '<a href="%s">%s</a>' % (url, value)
return a
else:
return value

1
djing/fields.py

@ -111,6 +111,7 @@ except ImportError:
pass
# DEPRECATED: remove after clean old migrations
class MyGenericIPAddressField(models.GenericIPAddressField):
description = "Int32 notation ip address"

22
djing/lib/decorators.py

@ -1,29 +1,11 @@
from functools import wraps
from django.conf import settings
from django.http import HttpResponseRedirect, HttpResponseForbidden, JsonResponse
from django.http import HttpResponseForbidden, JsonResponse
from django.shortcuts import redirect
from djing.lib import check_sign
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):
debug = getattr(settings, 'DEBUG', False)
if not debug and not request.is_secure():
target_url = "https://%s%s" % (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)
@ -99,6 +81,8 @@ def json_view(fn):
@wraps(fn)
def wrapped(request, *args, **kwargs):
r = fn(request, *args, **kwargs)
if isinstance(r, dict) and not isinstance(r.get('text'), str):
r['text'] = str(r.get('text'))
return JsonResponse(r, safe=False, json_dumps_params={
'ensure_ascii': False
})

8
djing/lib/mixins.py

@ -2,6 +2,14 @@ from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin
from guardian.mixins import PermissionRequiredMixin
class OnlySuperUserMixin(AccessMixin):
"""Verify that the current user is superuser."""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
class OnlyAdminsMixin(AccessMixin):
"""Verify that the current user is admin."""
def dispatch(self, request, *args, **kwargs):

4
djing/lib/tln/__init__.py

@ -1,4 +0,0 @@
from .tln import *
__all__ = ('TelnetApi', 'ValidationError', 'ZTEFiberIsFull', 'ZteOltLoginFailed',
'OnuZteRegisterError', 'ZteOltConsoleError', 'register_onu_ZTE_F660')

274
djing/lib/tln/tln.py

@ -1,274 +0,0 @@
#!/usr/bin/env python3
import re
import struct
from telnetlib import Telnet
from time import sleep
from typing import Generator, Dict, Optional, Tuple
from djing.lib import process_lock
class ZteOltConsoleError(Exception):
pass
class OnuZteRegisterError(ZteOltConsoleError):
pass
class ZTEFiberIsFull(ZteOltConsoleError):
pass
class ZteOltLoginFailed(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):
def __init__(self, prompt_string: bytes, *args, **kwargs):
timeout = kwargs.get('timeout')
if timeout:
self._timeout = timeout
self._prompt_string = prompt_string or b'ZTE#'
self.config_level = []
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 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], prompt_title: bytes, *args, **kwargs):
super().__init__(prompt_string=prompt_title, *args, **kwargs)
self.prompt_title = prompt_title
self.set_prompt_string(b'%s#' % prompt_title)
self.resize_screen(*screen_size)
def enter(self, username: bytes, passw: bytes) -> None:
self.read_until(b'Username:')
self.write(username)
self.read_until(b'Password:')
self.write(passw)
for l in self.read_lines():
if b'bad password' in l:
raise ZteOltLoginFailed
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'%s(config)#' % self.prompt_title
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'%s(config-if)#' % self.prompt_title)
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'%s(config-if)#' % self.prompt_title)
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
@process_lock
def register_onu_ZTE_F660(olt_ip: str, onu_sn: bytes, login_passwd: Tuple[bytes, bytes], onu_mac: bytes, prompt_title: bytes, vlan_id: int) -> Tuple:
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), prompt_title=prompt_title)
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 > 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, vlan_id)
sleep(1)
return stack_num, rack_num, fiber_num, new_onu_port_num
if __name__ == '__main__':
ip = '192.168.0.100'
try:
register_onu_ZTE_F660(
olt_ip=ip, onu_sn=b'ZTEG^#*$&@&', login_passwd=(b'login', b'password'),
onu_mac=b'MAC'
)
except ZteOltConsoleError as e:
print(e)
except ConnectionRefusedError:
print('ERROR: connection refused', ip)

3
djing/local_settings.py.example

@ -66,6 +66,9 @@ EMAIL_PORT = 587
EMAIL_HOST_PASSWORD = 'password'
EMAIL_USE_TLS = True
# public url for Viber Bot
VIBER_BOT_PUBLIC_URL = 'https://your_domain.name'
# Encrypted fields
# https://pypi.org/project/django-encrypted-model-fields/
# You must change this value

10
djing/settings.py

@ -42,6 +42,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'encrypted_model_fields',
'django_cleanup.apps.CleanupConfig',
'ip_pool',
'accounts_app',
'gw_app',
@ -50,10 +51,11 @@ INSTALLED_APPS = [
'searchapp',
'devapp',
'mapapp',
'traf_stat',
'finapp',
'taskapp',
'clientsideapp',
'chatbot',
'messenger',
'msg_app',
'dialing_app',
'group_app',
@ -182,7 +184,7 @@ DEFAULT_PICTURE = '/static/img/user_ava.gif'
AUTH_USER_MODEL = 'accounts_app.UserProfile'
LOGIN_URL = reverse_lazy('acc_app:login')
LOGIN_REDIRECT_URL = reverse_lazy('acc_app:profile')
LOGIN_REDIRECT_URL = reverse_lazy('acc_app:setup_info')
LOGOUT_URL = reverse_lazy('acc_app:logout')
PAGINATION_ITEMS_PER_PAGE = local_settings.PAGINATION_ITEMS_PER_PAGE
@ -234,6 +236,10 @@ BROKER_TRANSPORT_OPTIONS = {'visibility_timeout': 3600}
CELERY_RESULT_BACKEND = 'redis://' + REDIS_HOST + ':' + REDIS_PORT + '/0'
# public url for Viber Bot
VIBER_BOT_PUBLIC_URL = local_settings.VIBER_BOT_PUBLIC_URL
# Encrypted fields
# https://pypi.org/project/django-encrypted-model-fields/
FIELD_ENCRYPTION_KEY = getattr(

2
djing/tasks.py

@ -37,6 +37,8 @@ def send_email_notify(msg_text: str, account_id: int):
def multicast_email_notify(msg_text: str, account_ids: Iterable):
text_content = strip_tags(msg_text)
for acc_id in account_ids:
if not acc_id:
continue
try:
account = UserProfile.objects.get(pk=acc_id)
target_email = account.email

3
djing/urls.py

@ -11,13 +11,14 @@ 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('traf_stat.urls', namespace='traf_stat')),
path('tasks/', include('taskapp.urls', namespace='taskapp')),
path('client/', include('clientsideapp.urls', namespace='client_side')),
path('msg/', include('msg_app.urls', namespace='msg_app')),
path('dialing/', include('dialing_app.urls', namespace='dialapp')),
path('groups/', include('group_app.urls', namespace='group_app')),
path('ip_pool/', include('ip_pool.urls', namespace='ip_pool')),
path('messenger/', include('messenger.urls', namespace='messenger')),
path('gw/', include('gw_app.urls', namespace='gw_app')),
path('fin/', include('finapp.urls', namespace='finapp'))

2
djing/views.py

@ -5,6 +5,6 @@ from django.shortcuts import redirect
@login_required
def home(request):
if request.user.is_staff:
return redirect('acc_app:profile')
return redirect('acc_app:setup_info')
else:
return redirect('client_side:home')

3
docs/extra_func.md

@ -3,13 +3,10 @@
Его совсем не много, но без внимания оставить нельзя.
Все вспомогательные модули можно найти в пакете **djing.lib**.
### tln
Это модуль работы по *telnet*
### messaging
Этот модуль помогает работать с форматами СМС сообщений.
### init
Содержит всякие мелкие примочки, код прост и с комментариями, зайдите посмотрите.

7
group_app/views.py

@ -9,6 +9,7 @@ from django.contrib import messages
from django.conf import settings
from djing.lib.decorators import only_admins
from guardian.decorators import permission_required_or_403 as permission_required
from guardian.shortcuts import get_objects_for_user
from djing.global_base_views import OrderedFilteredList
from . import models
@ -27,6 +28,12 @@ class GroupListView(OrderedFilteredList):
model = models.Group
context_object_name = 'groups'
def get_queryset(self):
queryset = get_objects_for_user(self.request.user,
'group_app.view_group', klass=self.model,
accept_global_perms=False)
return queryset
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('group_app.change_group'), name='dispatch')

5
gw_app/nas_managers/core.py

@ -1,4 +1,4 @@
from abc import ABC, abstractmethod, abstractproperty
from abc import ABC, abstractmethod
from typing import Iterator, Tuple, Optional
from djing import ping
from gw_app.nas_managers.structs import SubnetQueue, VectorQueue
@ -16,7 +16,8 @@ class NasNetworkError(Exception):
# Communicate with gw
class BaseTransmitter(ABC):
@abstractproperty
@property
@abstractmethod
def description(self):
"""
:return: Returnd a description of nas implementation

16
gw_app/nas_managers/mod_mikrotik.py

@ -272,8 +272,9 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
'=target=%s' % queue.network,
'=max-limit=%.3fM/%.3fM' % queue.max_limit,
'=queue=Djing_pcq_up/Djing_pcq_down',
'=burst-time=1/5',
#'=total-queue=Djing_pcq_down'
'=burst-time=5/5',
'=burst-limit=%.3fM/%.3fM' % tuple(i * 2 for i in queue.max_limit),
'=burst-threshold=%.3fM/%.3fM' % tuple(i / 1.2 for i in queue.max_limit)
))
def remove_queue(self, queue: i_structs.SubnetQueue) -> None:
@ -300,7 +301,7 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
if queue_gw is None:
return self.add_queue(queue)
else:
cmd = [
cmd = (
'/queue/simple/set',
'=name=%s' % queue.name,
'=max-limit=%.3fM/%.3fM' % queue.max_limit,
@ -308,10 +309,11 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
# или =target-addresses или =target
'=target=%s' % queue.network,
'=queue=Djing_pcq_up/Djing_pcq_down',
'=burst-time=1/1'
]
if queue.queue_id:
cmd.insert(1, '=.id=%s' % queue.queue_id)
'=burst-time=5/5',
'=burst-limit=%.3fM/%.3fM' % tuple(i * 2 for i in queue.max_limit),
'=burst-threshold=%.3fM/%.3fM' % tuple(i / 1.2 for i in queue.max_limit),
'=numbers=%s' % queue_gw.queue_id
)
r = self._exec_cmd(cmd)
return r

4
gw_app/nas_managers/structs.py

@ -23,7 +23,7 @@ class SubnetQueue(BaseStruct):
return self._max_limit
def set_max_limit(self, v):
if isinstance(v, tuple):
if isinstance(v, (tuple, list)):
self._max_limit = v
elif isinstance(v, str):
s_in, s_out = v.split('/')
@ -32,7 +32,7 @@ class SubnetQueue(BaseStruct):
sp = float(v)
self._max_limit = sp, sp
else:
raise ValueError('Unexpected format for max_limit')
raise ValueError('Unexpected format for max_limit %s' % v)
max_limit = property(get_max_limit, set_max_limit)

36
ip_pool/migrations/0001_squashed_0004_auto_20190305_1243.py

@ -0,0 +1,36 @@
# Generated by Django 2.1.1 on 2019-03-05 12:47
from django.db import migrations, models
import ip_pool.fields
class Migration(migrations.Migration):
replaces = [('ip_pool', '0001_initial'), ('ip_pool', '0002_change_unique'), ('ip_pool', '0003_auto_20181019_1230'), ('ip_pool', '0004_auto_20190305_1243')]
initial = True
dependencies = [
('group_app', '0002_group_code'),
]
operations = [
migrations.CreateModel(
name='NetworkModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('network', ip_pool.fields.GenericIpAddressWithPrefix(help_text='Ip address of network. For example: 192.168.1.0 or fde8:6789:1234:1::', unique=True, verbose_name='IP network')),
('kind', models.CharField(choices=[('inet', 'Internet'), ('guest', 'Guest'), ('trust', 'Trusted'), ('device', 'Devices'), ('admin', 'Admin')], default='guest', max_length=6, verbose_name='Kind of network')),
('description', models.CharField(max_length=64, verbose_name='Description')),
('ip_start', models.GenericIPAddressField(verbose_name='Start work ip range')),
('ip_end', models.GenericIPAddressField(verbose_name='End work ip range')),
('groups', models.ManyToManyField(to='group_app.Group', verbose_name='Groups')),
],
options={
'verbose_name': 'Network',
'verbose_name_plural': 'Networks',
'db_table': 'ip_pool_network',
'ordering': ('network',),
},
)
]

15
ip_pool/migrations/0004_auto_20190305_1243.py

@ -0,0 +1,15 @@
# Generated by Django 2.1.1 on 2019-03-05 12:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ip_pool', '0003_auto_20181019_1230'),
]
operations = [
migrations.DeleteModel(name='IpLeaseModel'),
migrations.DeleteModel(name='LeasesHistory')
]

2
ip_pool/models.py

@ -160,6 +160,7 @@ class NetworkModel(models.Model):
ordering = ('network',)
# Deprecated. Remove after migrations squashed
class IpLeaseManager(models.Manager):
def get_free_ip(self, network: NetworkModel):
@ -234,6 +235,7 @@ class IpLeaseModel(models.Model):
unique_together = ('ip', 'network', 'mac_addr')
# Deprecated. Remove after migrations squashed
class LeasesHistory(models.Model):
ip = models.GenericIPAddressField(verbose_name=_('Ip address'))
lease_time = models.DateTimeField(_('Lease time'), auto_now_add=True)

3
locale/ru/LC_MESSAGES/django.po

@ -128,3 +128,6 @@ msgstr "Вы уверены в этом?"
msgid "Finance"
msgstr "Финансы"
msgid "Traffic"
msgstr "Траффик"

1
messenger/__init__.py

@ -0,0 +1 @@
default_app_config = 'messenger.apps.messengerConfig'

7
messenger/admin.py

@ -0,0 +1,7 @@
from django.contrib import admin
from messenger import models
admin.site.register(models.Messenger)
admin.site.register(models.ViberMessenger)
admin.site.register(models.ViberSubscriber)
admin.site.register(models.ViberMessage)

5
messenger/apps.py

@ -0,0 +1,5 @@
from django.apps import AppConfig
class messengerConfig(AppConfig):
name = 'messenger'

28
messenger/forms.py

@ -0,0 +1,28 @@
from django import forms
from messenger import models
class MessengerForm(forms.ModelForm):
class Meta:
model = models.Messenger
fields = ('bot_type',)
class MessengerViberForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
kwargs['initial']['bot_type'] = 1
super().__init__(*args, **kwargs)
inst = getattr(self, 'instance')
if inst:
self.fields['bot_type'].disabled = True
#self.fields['bot_type'].widget.attrs['disabled'] = True
class Meta:
model = models.ViberMessenger
fields = '__all__'
class MessengerViberMessageForm(forms.ModelForm):
class Meta:
model = models.ViberMessage
fields = '__all__'

190
messenger/locale/ru/LC_MESSAGES/django.po

@ -0,0 +1,190 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Dmitry Novikov nerosketch@gmail.com, 2019.
#
#, fuzzy
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-02-06 13:45+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Dmitry Novikov nerosketch@gmail.com\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
#: models.py:16 templates/messenger/messenger_list.html:20
msgid "Title"
msgstr "Название"
#: models.py:18
msgid "Viber"
msgstr "Вайбер"
#: models.py:20
msgid "Bot type"
msgstr "Тип бота"
#: models.py:21 templates/messenger/messenger_list.html:22
msgid "Slug"
msgstr "Ссыль"
#: models.py:28 models.py:92
msgid "messenger"
msgstr "Мэссенджер"
#: models.py:29 templates/messenger/messenger_list.html:7
#: templates/messenger/messenger_list.html:12
#: templates/messenger/vibermessenger_form.html:8
msgid "Messengers"
msgstr "Мэссенджэры"
#: models.py:48
msgid "Bot secret token"
msgstr "Секретный токен viber"
#: models.py:49 models.py:108
msgid "Avatar"
msgstr "Аватар"
#: models.py:83
msgid "Viber messenger"
msgstr "Viber мэссенджэр"
#: models.py:84
msgid "Viber messengers"
msgstr "Viber мэссенджэры"
#: models.py:89
msgid "Message"
msgstr "Сообщение"
#: models.py:90
msgid "Date"
msgstr "Дата"
#: models.py:91
msgid "Sender"
msgstr "Отправитель"
#: models.py:93
msgid "Subscriber"
msgstr "Подписчик"
#: models.py:100
msgid "Viber message"
msgstr "Сообщение viber"
#: models.py:101
msgid "Viber messages"
msgstr "Сообщения viber"
#: models.py:106
msgid "User unique id in viber"
msgstr "Уникальный id viber"
#: models.py:107
msgid "Name"
msgstr "Имя"
#: models.py:109
msgid "System account"
msgstr "Системная учётная запись"
#: models.py:116
msgid "Viber subscriber"
msgstr "Подписчик viber"
#: models.py:117
msgid "Viber subscribers"
msgstr "Подписчики viber"
#: templates/messenger/add_messenger.html:5
msgid "Select bot type"
msgstr "Выберите тип бота"
#: templates/messenger/add_messenger.html:11
msgid "Add"
msgstr "Добавить"
#: templates/messenger/messenger_list.html:21
msgid "Type"
msgstr "Тип"
#: templates/messenger/messenger_list.html:33
msgid "Edit"
msgstr "Изменить"
#: templates/messenger/messenger_list.html:40
msgid "messengers was not found"
msgstr "Мэссенджеры не найдены"
#: templates/messenger/messenger_list.html:49
msgid "New"
msgstr "Новый"
#: templates/messenger/vibermessenger_form.html:10
msgid "Update messenger"
msgstr "Обновить мэссенджэр"
#: templates/messenger/vibermessenger_form.html:11
msgid "Change viber"
msgstr "Изменить viber"
#: templates/messenger/vibermessenger_form.html:13
msgid "Add messenger"
msgstr "Добавить мэссенджэр"
#: templates/messenger/vibermessenger_form.html:14
msgid "Add viber"
msgstr "Добавить viber"
#: templates/messenger/vibermessenger_form.html:25
msgid "Change messenger"
msgstr "Изменить мэссенджэр"
#: templates/messenger/vibermessenger_form.html:28
msgid "Add new messenger"
msgstr "Добавить мэссенджэр"
#: templates/messenger/vibermessenger_form.html:39
msgid "Save"
msgstr "Сохранить"
#: templates/messenger/vibermessenger_form.html:43
msgid "Send webhook"
msgstr "Отправить webhook"
#: views.py:38
msgid "Unexpected bot type"
msgstr "Не известный тип бота"
#: views.py:51
msgid "New viber messenger successfully created"
msgstr "Новый viber мэссенджэр успешно создан"
#: views.py:62
msgid "Viber messenger successfully updated"
msgstr "viber мэссенджэр успешно обновлён"
#: views.py:73
msgid "Viber messenger successfully deleted"
msgstr "viber мэссенджэр успешно удалён"
#: views.py:132
msgid "My telephone number"
msgstr "Мой номер телефона"
#: views.py:150
msgid ""
"Telephone not found, please specify telephone number in account in billing"
msgstr ""
"Номер телефона не найден. Укажите свой номер телефона в учётке в биллинге"
msgid "Your account is attached. Now you will be receive notifications from billing"
msgstr "Ваша учётка из биллинга привязана. Теперь вы будете получать оповещения из биллинга."

88
messenger/migrations/0001_initial.py

@ -0,0 +1,88 @@
# Generated by Django 2.1.3 on 2019-02-07 12:31
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Messenger',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=64, verbose_name='Title')),
('bot_type', models.PositiveSmallIntegerField(blank=True, choices=[(1, 'Viber')], verbose_name='Bot type')),
('slug', models.SlugField(verbose_name='Slug')),
],
options={
'verbose_name': 'messenger',
'verbose_name_plural': 'Messengers',
'db_table': 'messengers',
'ordering': ('title',),
},
),
migrations.CreateModel(
name='ViberMessage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('msg', models.TextField(verbose_name='Message')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
('sender', models.CharField(max_length=32, verbose_name='Sender')),
],
options={
'verbose_name': 'Viber message',
'verbose_name_plural': 'Viber messages',
'db_table': 'viber_messages_notifications',
'ordering': ('-date',),
},
),
migrations.CreateModel(
name='ViberSubscriber',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uid', models.CharField(max_length=32, verbose_name='User unique id in viber')),
('name', models.CharField(blank=True, max_length=32, null=True, verbose_name='Name')),
('avatar', models.URLField(blank=True, max_length=250, null=True, verbose_name='Avatar')),
('account', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='System account')),
],
options={
'verbose_name': 'Viber subscriber',
'verbose_name_plural': 'Viber subscribers',
'db_table': 'viber_subscriber',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='ViberMessenger',
fields=[
('messenger_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='messenger.Messenger')),
('token', models.CharField(max_length=64, verbose_name='Bot secret token')),
('avatar', models.ImageField(null=True, upload_to='viber_avatar', verbose_name='Avatar')),
],
options={
'verbose_name': 'Viber messenger',
'verbose_name_plural': 'Viber messengers',
'db_table': 'viber_messenger_notifications',
'ordering': ('title',),
},
bases=('messenger.messenger',),
),
migrations.AddField(
model_name='vibermessage',
name='subscriber',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='messenger.ViberSubscriber', verbose_name='Subscriber'),
),
migrations.AddField(
model_name='vibermessage',
name='messenger',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='messenger.ViberMessenger', verbose_name='messenger'),
),
]

0
chatbot/__init__.py → messenger/migrations/__init__.py

128
messenger/models.py

@ -0,0 +1,128 @@
from urllib.parse import urljoin
from django.conf import settings
from django.shortcuts import resolve_url
from django.utils.translation import gettext_lazy as _
from django.db import models
from viberbot import Api, BotConfiguration
from viberbot.api.messages import TextMessage
from viberbot.api.messages.message import Message
from accounts_app.models import UserProfile
class Messenger(models.Model):
title = models.CharField(_('Title'), max_length=64)
CHAT_TYPES = (
(1, _('Viber')),
)
bot_type = models.PositiveSmallIntegerField(_('Bot type'), choices=CHAT_TYPES, blank=True)
slug = models.SlugField(_('Slug'))
def __str__(self):
return self.title
class Meta:
db_table = 'messengers'
verbose_name = _('messenger')
verbose_name_plural = _('Messengers')
ordering = ('title',)
def get_absolute_url(self):
if self.bot_type == 1:
return resolve_url('messenger:update_viber_messenger', self.slug)
def get_next_url(self):
if self.bot_type == 1: # Viber
return resolve_url('messenger:update_viber_messenger', self.slug)
else:
return resolve_url('messenger:messengers_list')
class ViberMessenger(Messenger):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._viber_cache = None
token = models.CharField(_('Bot secret token'), max_length=64)
avatar = models.ImageField(_('Avatar'), upload_to='viber_avatar', null=True)
def get_viber(self):
if self._viber_cache is None:
self._viber_cache = Api(BotConfiguration(
name=str(self.slug),
avatar=self.avatar.url,
auth_token=str(self.token)
))
return self._viber_cache
def send_message(self, to: UserProfile, msg):
try:
viber = self.get_viber()
vs = to.vibersubscriber
if issubclass(msg.__class__, Message):
viber.send_messages(str(vs.uid), msg)
else:
viber.send_messages(str(vs.uid), TextMessage(text=msg))
except ViberSubscriber.DoesNotExist:
pass
def send_messages(self, receivers, msg_text: str):
"""
:param receivers: QuerySet of accounts_app.UserProfile
:param msg_text: text message
:return: nothing
"""
viber = self.get_viber()
msg = TextMessage(text=msg_text)
for vs in ViberSubscriber.objects.filter(account__in=receivers).iterator():
viber.send_messages(str(vs.uid), msg)
def send_webhook(self):
pub_url = getattr(settings, 'VIBER_BOT_PUBLIC_URL')
listen_url = resolve_url('messenger:listen_viber_bot', self.slug)
public_url = urljoin(pub_url, listen_url)
viber = self.get_viber()
viber.set_webhook(public_url, ['failed', 'subscribed', 'unsubscribed', 'conversation_started'])
def __str__(self):
return self.title
class Meta:
db_table = 'viber_messenger_notifications'
verbose_name = _('Viber messenger')
verbose_name_plural = _('Viber messengers')
ordering = ('title',)
class ViberMessage(models.Model):
msg = models.TextField(_('Message'))
date = models.DateTimeField(_('Date'), auto_now_add=True)
sender = models.CharField(_('Sender'), max_length=32)
messenger = models.ForeignKey(ViberMessenger, verbose_name=_('messenger'), on_delete=models.CASCADE)
subscriber = models.ForeignKey('ViberSubscriber', on_delete=models.SET_NULL, verbose_name=_('Subscriber'), null=True)
def __str__(self):
return self.msg
class Meta:
db_table = 'viber_messages_notifications'
verbose_name = _('Viber message')
verbose_name_plural = _('Viber messages')
ordering = ('-date',)
class ViberSubscriber(models.Model):
uid = models.CharField(_('User unique id in viber'), max_length=32)
name = models.CharField(_('Name'), max_length=32, null=True, blank=True)
avatar = models.URLField(_('Avatar'), max_length=250, null=True, blank=True)
account = models.OneToOneField(UserProfile, on_delete=models.CASCADE, verbose_name=_('System account'), blank=True, null=True)
def __str__(self):
return self.name or 'no'
class Meta:
db_table = 'viber_subscriber'
verbose_name = _('Viber subscriber')
verbose_name_plural = _('Viber subscribers')
ordering = ('name',)

56
messenger/tasks.py

@ -0,0 +1,56 @@
from typing import Optional, Iterable
from celery import shared_task
from accounts_app.models import UserProfile
from messenger.models import ViberMessenger
@shared_task
def send_viber_message(messenger_id: Optional[int], account_id: int, message_text: str) -> Optional[str]:
"""
Send text message via viber
:param messenger_id: Primary key UID for messanger.ViberMessenger
:param account_id: User id from accounts_app.UserProfile
:param message_text:
:return: Optional text for log
"""
if not message_text:
return 'ERROR: empty message text'
try:
sp = UserProfile.objects.get(pk=account_id)
if messenger_id is None:
for vm in ViberMessenger.objects.all().iterator():
vm.send_message(sp, message_text)
else:
vm = ViberMessenger.objects.get(pk=messenger_id)
vm.send_message(sp, message_text)
except ViberMessenger.DoesNotExist:
return 'ERROR: Viber messanger with id=%d not found' % messenger_id
except UserProfile.DoesNotExist:
return 'ERROR: accounts_app.UserProfile with pk=%d does not exist' % account_id
@shared_task
def multicast_viber_notify(messenger_id: Optional[int], account_id_list: Iterable[int], message_text: str):
"""
Send multiple message via Viber to several addresses
:param messenger_id: Primary key UID for messanger.ViberMessenger
:param account_id_list: list of account ids from accounts_app.UserProfile
:param message_text:
:return: Optional text for log
"""
if not message_text:
return 'ERROR: empty message text'
account_id_list = tuple(account_id_list)
recipients = UserProfile.objects.filter(pk__in=account_id_list)
if not recipients.exists():
return 'No recipients found from ids: %s' % ','.join(str(i) for i in account_id_list)
if messenger_id is None:
for vm in ViberMessenger.objects.all().iterator():
vm.send_messages(recipients, message_text)
else:
vm = ViberMessenger.objects.filter(pk=messenger_id).first()
if vm is None:
return 'ERROR ViberMessenger with pk=%d does not exist' % messenger_id
vm.send_messages(recipients, message_text)

15
messenger/templates/messenger/add_messenger.html

@ -0,0 +1,15 @@
{% load i18n bootstrap3 %}
<form role="form" action="{% url 'messenger:add_messenger' %}" method="post">{% csrf_token %}
<div class="modal-header primary">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title"><span class="glyphicon glyphicon-blackboard"></span>{% trans 'Select bot type' %}</h4>
</div>
<div class="modal-body">
{% bootstrap_form form %}
<button type="submit" class="btn btn-success">
<span class="glyphicon glyphicon-plus"></span> {% trans 'Add' %}
</button>
</div>
</form>

57
messenger/templates/messenger/messenger_list.html

@ -0,0 +1,57 @@
{% extends 'base.html' %}
{% load dpagination i18n %}
{% block breadcrumb %}
<ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li>
<li class="active">{% trans 'Messengers' %}</li>
</ol>
{% endblock %}
{% block page-header %}
{% trans 'Messengers' %}
{% endblock %}
{% block main %}
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="col-sm-4">{% trans 'Title' %}</th>
<th class="col-sm-3">{% trans 'Type' %}</th>
<th class="col-sm-4">{% trans 'Slug' %}</th>
<th class="col-sm-1">#</th>
</tr>
</thead>
<tbody>
{% for messenger in object_list %}
<tr>
<td>{{ messenger.title }}</td>
<td>{{ messenger.get_bot_type_display }}</td>
<td>{{ messenger.slug }}</td>
<td>
<a href="{{ messenger.get_absolute_url }}" class="btn btn-sm btn-default" title="{% trans 'Edit' %}">
<span class="glyphicon glyphicon-edit"></span>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4"><a href="#">{% trans 'messengers was not found' %}</a></td>
</tr>
{% endfor %}
</tbody>
{% if perms.messenger.add_messenger %}
<tfoot>
<tr>
<td colspan="4" class="btn-group btn-group-sm">
<a href="{% url 'messenger:add_messenger' %}" class="btn btn-default btn-modal">
<span class="glyphicon glyphicon-plus"></span> <span class="hidden-xs">{% trans 'New' %}</span>
</a>
</td>
</tr>
</tfoot>
{% endif %}
</table>
</div>
{% endblock %}

49
messenger/templates/messenger/vibermessenger_form.html

@ -0,0 +1,49 @@
{% extends request.is_ajax|yesno:'bajax.html,base.html' %}
{% load i18n bootstrap3 %}
{% block breadcrumb %}
<ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li>
<li><a href="{% url 'messenger:messengers_list' %}">{% trans 'Messengers' %}</a></li>
{% if object %}
<li><a href="{{ object.get_absolute_url }}">{% trans 'Update messenger' %}</a></li>
<li class="active">{% trans 'Change viber' %}</li>
{% else %}
<li><a href="{% url 'messenger:add_messenger' %}">{% trans 'Add messenger' %}</a></li>
<li class="active">{% trans 'Add viber' %}</li>
{% endif %}
</ol>
{% endblock %}
{% block main %}
{% if object %}
{% url 'messenger:update_viber_messenger' object.slug as form_url %}
{% trans 'Change messenger' as panel_title %}
{% else %}
{% url 'messenger:add_viber_messenger' as form_url %}
{% trans 'Add new messenger' as panel_title %}
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{ panel_title }}</h3>
</div>
<div class="panel-body">
<form role="form" action="{{ form_url }}" method="post" enctype="multipart/form-data">{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-sm btn-default">
<span class="glyphicon glyphicon-save"></span> {% trans 'Save' %}
</button>
{% if object %}
<a href="{% url 'messenger:webhook_viber_bot' object.slug %}" class="btn btn-default btn-sm btn-modal">
<span class="glyphicon glyphicon-share"></span> {% trans 'Send webhook' %}
</a>
{% endif %}
</form>
</div>
</div>
{% endblock %}

3
messenger/tests.py

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

17
messenger/urls.py

@ -0,0 +1,17 @@
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from messenger import views
app_name = 'messenger'
urlpatterns = [
path('', views.messengerListView.as_view(), name='messengers_list'),
path('new/', views.AddmessengerCreateView.as_view(), name='add_messenger'),
path('viber/new/', views.AddmessengerViberCreateView.as_view(), name='add_viber_messenger'),
path('viber/<slug:slug>/update/', views.UpdateVibermessengerUpdateView.as_view(), name='update_viber_messenger'),
path('viber/<slug:slug>/delete/', views.RemoveVibermessengerDeleteView.as_view(), name='delete_viber_messenger'),
path('viber/<slug:slug>/listen/', csrf_exempt(views.ListenViberView.as_view()), name='listen_viber_bot'),
path('viber/<slug:slug>/set_webhook/', views.SetWebhook.as_view(), name='webhook_viber_bot'),
]

161
messenger/views.py

@ -0,0 +1,161 @@
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.http import HttpResponseForbidden, HttpResponse, HttpResponseNotFound
from django.shortcuts import resolve_url
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _, gettext
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, FormView, View
from django.views.generic.detail import SingleObjectMixin
from viberbot.api.messages import KeyboardMessage, ContactMessage
from viberbot.api.user_profile import UserProfile as ViberUserProfile
from viberbot.api.viber_requests import ViberMessageRequest, ViberSubscribedRequest, ViberFailedRequest, \
ViberUnsubscribedRequest
from accounts_app.models import UserProfile
from djing.lib.mixins import LoginAdminPermissionMixin, LoginAdminMixin
from messenger import forms, models
from messenger.models import ViberMessage, ViberSubscriber
class messengerListView(LoginAdminPermissionMixin, ListView):
model = models.Messenger
permission_required = 'messenger.view_messenger'
class AddmessengerCreateView(LoginAdminMixin, FormView):
template_name = 'messenger/add_messenger.html'
form_class = forms.MessengerForm
def form_valid(self, form):
bot_type = form.cleaned_data.get('bot_type')
if isinstance(bot_type, int) and bot_type > 0:
if bot_type == 1:
self.success_url = resolve_url('messenger:add_viber_messenger')
return super().form_valid(form)
messages.info(self.request, _('Unexpected bot type'))
self.success_url = resolve_url('messenger:messengers_list')
return super().form_valid(form)
class AddmessengerViberCreateView(LoginAdminMixin, PermissionRequiredMixin, CreateView):
model = models.ViberMessenger
form_class = forms.MessengerViberForm
permission_required = 'messenger.add_vibermessenger'
success_url = reverse_lazy('messenger:messengers_list')
def form_valid(self, form):
r = super().form_valid(form)
messages.success(self.request, _('New viber messenger successfully created'))
return r
class UpdateVibermessengerUpdateView(LoginAdminPermissionMixin, UpdateView):
model = models.ViberMessenger
form_class = forms.MessengerViberForm
permission_required = 'messenger.change_vibermessenger'
def form_valid(self, form):
r = super().form_valid(form)
messages.success(self.request, _('Viber messenger successfully updated'))
return r
class RemoveVibermessengerDeleteView(LoginAdminPermissionMixin, DeleteView):
model = models.ViberMessenger
permission_required = 'messenger.delete_vibermessenger'
success_url = reverse_lazy('messenger:messengers_list')
def delete(self, request, *args, **kwargs):
r = super().delete(request, *args, **kwargs)
messages.success(request, _('Viber messenger successfully deleted'))
return r
@method_decorator(csrf_exempt, name='post')
class ListenViberView(SingleObjectMixin, View):
http_method_names = 'post',
model = models.ViberMessenger
def post(self, request, *args, **kwargs):
obj = self.get_object()
if not obj:
return HttpResponseNotFound()
self.object = obj
viber = obj.get_viber()
if not viber.verify_signature(request.body, request.META.get('HTTP_X_VIBER_CONTENT_SIGNATURE')):
return HttpResponseForbidden()
# this library supplies a simple way to receive a request object
vr = viber.parse_request(request.body)
if isinstance(vr, ViberMessageRequest):
in_msg = vr.message
if isinstance(in_msg, ContactMessage):
self.inbox_contact(in_msg, vr.sender)
subscriber, created = self.make_subscriber(vr.sender)
if not created:
ViberMessage.objects.create(
msg=vr.message,
sender=vr.sender.id,
messenger=obj,
subscriber=subscriber
)
elif isinstance(vr, ViberSubscribedRequest):
self.make_subscriber(vr.user)
elif isinstance(vr, ViberFailedRequest):
print("client failed receiving message. failure: {0}".format(vr))
elif isinstance(vr, ViberUnsubscribedRequest):
ViberSubscriber.objects.filter(
uid=vr.user_id
).delete()
return HttpResponse(status=200)
def make_subscriber(self, viber_user_profile: ViberUserProfile):
subscriber, created = ViberSubscriber.objects.get_or_create(
uid=viber_user_profile.id,
defaults={
'name': viber_user_profile.name,
'avatar': viber_user_profile.avatar
}
)
if created and hasattr(self, 'object'):
msg = KeyboardMessage(keyboard={
'Type': 'keyboard',
'DefaultHeight': True,
'Buttons': ({
'ActionType': 'share-phone',
'ActionBody': 'reply to me',
"Text": gettext('My telephone number'),
"TextSize": "medium"
},)
}, min_api_version=3)
viber = self.object.get_viber()
viber.send_messages(viber_user_profile.id, msg)
return subscriber, created
def inbox_contact(self, msg, sender: ViberUserProfile):
tel = msg.contact.phone_number
accs = UserProfile.objects.filter(telephone__icontains=tel)
viber = self.object.get_viber()
if accs.exists():
subs = ViberSubscriber.objects.filter(uid=sender.id)
if subs.exists():
subs.update(account=accs.first())
viber.send_messages(sender.id, gettext(
'Your account is attached. Now you will be receive notifications from billing'
))
else:
viber.send_messages(sender.id, gettext('Telephone not found, please specify telephone number in account in billing'))
class SetWebhook(LoginAdminMixin, SingleObjectMixin, View):
http_method_names = 'get',
model = models.ViberMessenger
def get(self, request, *args, **kwargs):
obj = self.get_object()
if not obj:
return HttpResponseNotFound
obj.send_webhook()
return HttpResponse(b'ok', status=200)

2
msg_app/forms.py

@ -7,7 +7,7 @@ from accounts_app.models import UserProfile
class ConversationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ConversationForm, self).__init__(*args, **kwargs)
user_profile_queryset = UserProfile.objects.filter(is_admin=True)
user_profile_queryset = UserProfile.objects.filter(is_admin=True, is_active=True)
if user_profile_queryset is not None:
self.fields['participants'].choices = [(up.pk, up.get_full_name()) for up in user_profile_queryset]

25
msg_app/models.py

@ -2,7 +2,6 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from accounts_app.models import UserProfile
from djing.tasks import send_email_notify
from chatbot.models import ChatException
class MessageError(Exception):
@ -203,23 +202,21 @@ class Conversation(models.Model):
return messages[0]
def new_message(self, text, attachment, author, with_status=True):
try:
msg = Message.objects.create(
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)
msg = Message.objects.create(
text=text, conversation=self,
attachment=attachment, author=author
)
if with_status:
for participant in self.participants.filter(is_active=True):
if participant == author:
continue
MessageStatus.objects.create(msg=msg, user=participant)
if participant.flags.notify_msg:
send_email_notify.delay(
msg_text=text,
account_id=participant.pk
)
return msg
except ChatException as e:
raise MessageError(e)
return msg
@staticmethod
def remove_message(msg):

4
msg_app/templates/msg_app/chat.html

@ -37,7 +37,7 @@
<div class="row">
<div class="col-sm-1">
{% if can_view_profile %}
<a href="{% url 'acc_app:other_profile' author.pk %}" class="thumbnail" target="_blank" title="{{ author.get_full_name }}" data-toggle="tooltip">
<a href="{% url 'acc_app:other_profile' author.pk %}" class="thumbnail" title="{{ author.get_full_name }}" data-toggle="tooltip">
<img src="{{ author.get_min_ava }}" alt="ava">
</a>
{% else %}
@ -51,7 +51,7 @@
<pre>{{ msg.text }}</pre>
{% if msg.attachment %}
<a href="{{ msg.attachment }}" class="btn btn-default btn-sm" target="_blank">
<a href="{{ msg.attachment }}" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-gift"></span> {{ msg.attachment }}
</a>
{% endif %}

3
msg_app/urls.py

@ -7,6 +7,5 @@ urlpatterns = [
path('', views.ConversationsListView.as_view(), name='home'),
path('new/', views.new_conversation, name='new_conversation'),
path('<int:conv_id>/', views.to_conversation, name='to_conversation'),
path('<int:conv_id>/<int:msg_id>/del/', views.remove_msg, name='remove_msg'),
path('check_news/', views.check_news, name='check_news')
path('<int:conv_id>/<int:msg_id>/del/', views.remove_msg, name='remove_msg')
]

22
msg_app/views.py

@ -1,15 +1,12 @@
from json import dumps
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.contrib import messages
from django.shortcuts import render, redirect, get_object_or_404
from django.views.generic import ListView
from chatbot.models import MessageQueue
from djing.lib.decorators import only_admins
from guardian.decorators import permission_required_or_403 as permission_required
@ -87,22 +84,3 @@ def remove_msg(request, conv_id, msg_id):
conversation_id = msg.conversation.pk
msg.delete()
return redirect('msg_app:to_conversation', conversation_id)
@login_required
@only_admins
def check_news(request):
if request.user.is_authenticated:
msg = MessageQueue.objects.pop(user=request.user, tag='msgapp')
if msg is None:
r = {'auth': True, 'exist': False}
else:
r = {
'auth': True,
'exist': True,
'content': msg,
'title': "%s" % _('Message')
}
else:
r = {'auth': False}
return HttpResponse(dumps(r))

11
periodic.py

@ -7,9 +7,8 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings")
django.setup()
from django.utils import timezone
from django.db import transaction
from django.db.models import signals, Count
from abonapp.models import Abon, AbonTariff, abontariff_pre_delete, \
PeriodicPayForId, AbonLog
from django.db.models import Count
from abonapp.models import Abon, AbonTariff, PeriodicPayForId, AbonLog
from gw_app.nas_managers import NasNetworkError, NasFailedResult
from gw_app.models import NASModel
from djing.lib import LogicError
@ -35,7 +34,6 @@ class NasSyncThread(Thread):
def main():
signals.pre_delete.disconnect(abontariff_pre_delete, sender=AbonTariff)
AbonTariff.objects.filter(abon=None).delete()
now = timezone.now()
fields = ('id', 'tariff__title', 'abon__id', 'abon__username')
@ -79,7 +77,7 @@ def main():
# make log about it
l = AbonLog.objects.create(
abon=abon, amount=-amount,
comment="Автоматическое продление услуги '%s'" % trf.title
comment="Автоматическое продление услуги '%s' для %s" % (trf.title, abon)
)
print(l.comment)
else:
@ -101,7 +99,8 @@ def main():
# connect service when autoconnect is True, and user have enough money
for ab in Abon.objects.filter(
is_active=True,
current_tariff=None
current_tariff=None,
autoconnect_service=True
).exclude(last_connected_tariff=None).iterator():
try:
tariff = ab.last_connected_tariff

21
requirements.txt

@ -1,13 +1,12 @@
urllib3
Django>=2
Pillow
telepot
# for mac address field
netaddr
# for testing required xmltodict
xmltodict
#xmltodict
dicttoxml
# db client for Postgres
@ -18,11 +17,13 @@ pid
django-guardian
pinax-theme-bootstrap
django-bootstrap3
django-jsonfield
# django-jsonfield
-e git://github.com/dmkoch/django-jsonfield.git#egg=django-jsonfield
requests
webdavclient
pyst2
django-bitfield
transliterate
asterisk
django-encrypted-model-fields
@ -30,6 +31,18 @@ django-encrypted-model-fields
# django-xmlview for pay system allpay
-e git://github.com/nerosketch/django-xmlview.git#egg=django-xmlview
# django-bitfield
-e git://github.com/disqus/django-bitfield.git#egg=django-bitfield
# django_cleanup for clean unused media
-e git://github.com/un1t/django-cleanup.git#egg=django_cleanup
# viberbot
-e git://github.com/Viber/viber-bot-python.git#egg=viberbot
# pexpect
-e git://github.com/pexpect/pexpect.git#egg=pexpect
Celery
redis==2.10.6
celery[redis]

4
searchapp/templates/searchapp/index.html

@ -38,7 +38,7 @@
</form>
<div class="list-group">
{% for ab in abons %}
<a href="{% url 'abonapp:abon_home' ab.group.id ab.username %}" target="_blank" class="list-group-item">
<a href="{% url 'abonapp:abon_home' ab.group.id ab.username %}" class="list-group-item">
<h4 class="list-group-item-heading">
<span class="glyphicon glyphicon-user"></span>
{{ ab.username_display|safe }}
@ -70,7 +70,7 @@
{% else %}
{% url 'devapp:fix_device_group' dev.pk as devviewlink %}
{% endif %}
<a href="{{ devviewlink }}" target="_blank" class="list-group-item">
<a href="{{ devviewlink }}" class="list-group-item">
<h4 class="list-group-item-heading">
<span class="glyphicon glyphicon-hdd"></span>
{{ dev.comment|safe }}

15
static/bad_ie.html

@ -1,11 +1,18 @@
<!DOCTYPE html>
<html>
<html lang="ru">
<head>
<title>Старый браузер</title>
<meta charset="UTF-8">
<title>Старый браузер</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>У вас старый ослик, обновитесь хотяб до IE10</h1>
<h1>Ваш InternetExplorer устарел, обновите ваш браузер на более современный</h1>
<p>Можете воспользоваться ссылками ниже:</p>
<ul>
<li><a href="https://www.google.ru/chrome/">Google Chrome</a></li>
<li><a href="https://www.opera.com/ru/download">Opera</a></li>
<li><a href="https://browser.yandex.ru/">Yandex Browser</a></li>
<li><a href="https://www.mozilla.org/ru/firefox/new/">Mozilla Firefox</a></li>
</ul>
</body>
</html>

4
static/css/all.min.css
File diff suppressed because it is too large
View File

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save