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 groups=instance.group
).first() ).first()
if net is not None: 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) free_ip = net.get_free_ip(ips)
self.initial['ip_address'] = free_ip self.initial['ip_address'] = free_ip
else: else:

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

@ -1168,3 +1168,6 @@ msgstr "Балланс"
msgid "Date joined" msgid "Date joined"
msgstr "Дата создания" 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, _): def backup_info(apps, _):
Abon = apps.get_model('abonapp', 'Abon') Abon = apps.get_model('abonapp', 'Abon')
obs = Abon.objects.exclude(ip_address=None).only('ip_address', 'is_dynamic_ip') 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, _): def restore_info_to_new_scheme(apps, _):
Abon = apps.get_model('abonapp', 'Abon') Abon = apps.get_model('abonapp', 'Abon')
if not os.path.isfile(TMP_FILE):
return
with open(TMP_FILE, 'r') as f: with open(TMP_FILE, 'r') as f:
for abon in load(f): for abon in load(f):
ip_addr = abon['fields'].get('ip_address') 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 import validators
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models, transaction 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.dispatch import receiver
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.utils import timezone from django.utils import timezone
@ -349,24 +348,6 @@ class Abon(BaseAccount):
except LogicError: except LogicError:
pass 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): def get_absolute_url(self):
return resolve_url('abonapp:abon_home', self.group.id, self.username) return resolve_url('abonapp:abon_home', self.group.id, self.username)
@ -539,17 +520,6 @@ class PeriodicPayForId(models.Model):
ordering = ('last_pay',) 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) @receiver(post_init, sender=AbonTariff)
def abon_tariff_post_init(sender, **kwargs): def abon_tariff_post_init(sender, **kwargs):
abon_tariff = kwargs["instance"] abon_tariff = kwargs["instance"]
@ -566,17 +536,3 @@ def abon_tariff_pre_save(sender, **kwargs):
if getattr(abon_tariff, 'deadline') is None: if getattr(abon_tariff, 'deadline') is None:
calc_obj = abon_tariff.tariff.get_calc_type()(abon_tariff) calc_obj = abon_tariff.tariff.get_calc_type()(abon_tariff)
abon_tariff.deadline = calc_obj.calc_deadline() 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 %} {% for invoice in invoices %}
<tr> <tr>
<td>{{ invoice.id }}</td> <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.amount }}</td>
<td>{{ invoice.comment }}</td> <td>{{ invoice.comment }}</td>
<td>{{ invoice.date_create|date:'d b H:i' }}</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> </tr>
{% empty %} {% empty %}
<tr> <tr>

2
abonapp/templates/abonapp/editAbon.html

@ -136,7 +136,7 @@
<label for="id_method" class="col-sm-4 control-label">{% trans 'Device' %}</label> <label for="id_method" class="col-sm-4 control-label">{% trans 'Device' %}</label>
<div class="col-sm-8 btn-group btn-group-sm"> <div class="col-sm-8 btn-group btn-group-sm">
{% if device %} {% 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="glyphicon glyphicon-hdd"></span>
<span class="hidden-md">{{ device.comment|truncatechars:11 }} {{ device.ip_address|default:'' }}</span> <span class="hidden-md">{{ device.comment|truncatechars:11 }} {{ device.ip_address|default:'' }}</span>
</a> </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> <span class="glyphicon glyphicon-exclamation-sign"></span> <span class="hidden-xs">{% trans 'List of debtors' %}</span>
</a> </a>
{% endif %} {% 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="glyphicon glyphicon-phone"></span>
<span class="hidden-xs">{% trans 'Export vCards' %}</span> <span class="hidden-xs">{% trans 'Export vCards' %}</span>
</a> </a>

2
abonapp/templates/abonapp/invoiceForPayment.html

@ -48,7 +48,7 @@
{% endif %} {% endif %}
</td> </td>
<td class="col-xs-2"> <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> </td>
</tr> </tr>
{% empty %} {% empty %}

2
abonapp/templates/abonapp/modal_phonebook.html

@ -26,7 +26,7 @@
</table> </table>
<div class="modal-footer"> <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' %} <span class="glyphicon glyphicon-export"></span> {% trans 'Export to csv' %}
</a> </a>
</div> </div>

4
abonapp/templates/abonapp/payHistory.html

@ -14,11 +14,11 @@
<tbody> <tbody>
{% for ph in pay_history %} {% for ph in pay_history %}
<tr> <tr>
<td>{{ ph.amount }}</td>
<td>{{ ph.amount|floatformat:2 }}</td>
<td>{{ ph.date|date:'d F Y, H:i:s' }}</td> <td>{{ ph.date|date:'d F Y, H:i:s' }}</td>
<td> <td>
{% if ph.author %} {% 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 %} {% else %}
{% trans 'System' %} {% trans 'System' %}
{% endif %} {% endif %}

43
abonapp/templates/abonapp/peoples.html

@ -24,7 +24,7 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>#</th>
<th>#</th>
<th class="col-xs-1"> <th class="col-xs-1">
<a href="{% url 'abonapp:people_list' group.pk %}?{% url_order_by request order_by='username' %}"> <a href="{% url 'abonapp:people_list' group.pk %}?{% url_order_by request order_by='username' %}">
{% trans 'Sub' %} {% trans 'Sub' %}
@ -64,11 +64,11 @@
{% if order_by == 'ballance' %}<span class="glyphicon glyphicon-filter"></span>{% endif %} {% if order_by == 'ballance' %}<span class="glyphicon glyphicon-filter"></span>{% endif %}
</th> </th>
<th class="hidden-xs">{% trans 'Markers' %}</th> <th class="hidden-xs">{% trans 'Markers' %}</th>
<th class="col-xs-1">#</th>
<th class="col-xs-1">Ping</th>
</tr> </tr>
</thead> </thead>
<tbody> <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 %} {% for human in object_list %}
{% if human.is_active %} {% if human.is_active %}
<tr> <tr>
@ -76,27 +76,26 @@
<tr class="danger"> <tr class="danger">
{% endif %} {% endif %}
<td> <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>
<td class="col-xs-1"> <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> <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>
<td class="hidden-xs"> <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>
<td class="col-xs-1">{{ human.ip_address|default_if_none:'&mdash;' }}</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.street|default:_('Not assigned') }}</td>
<td class="col-xs-1">{{ human.house|default:'&mdash;' }}</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> <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> {% for user_icon in human.get_flag_icons %}<span class="m-icon {{ user_icon }}"></span>
{% endfor %} {% endfor %}
</td> </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> </a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="12">
<td colspan="10">
{% trans 'Subscribers not found' %}. {% trans 'Subscribers not found' %}.
{% if perms.abonapp.add_abon %} {% if perms.abonapp.add_abon %}
<a href="{% url 'abonapp:add_abon' group.pk %}">{% trans 'Add abon' %}</a> <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> <dt>{% trans 'Auto continue service.' %}</dt>
<dd> <dd>
<input type="checkbox" data-url="{% url 'abonapp:set_auto_continue_service' group.pk abon.username %}" class="autosave" {{ abon.autoconnect_service|yesno:'checked,' }}> <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> </dd>
</dl> </dl>

50
abonapp/views.py

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

5
accounts_app/forms.py

@ -46,10 +46,11 @@ class UserPermissionsForm(forms.ModelForm):
class Meta: class Meta:
model = UserProfile model = UserProfile
fields = ('avatar', 'password', 'groups', 'user_permissions', 'responsibility_groups', 'is_superuser')
fields = ('user_permissions', 'is_superuser')
class UserProfileForm(forms.ModelForm): class UserProfileForm(forms.ModelForm):
class Meta: class Meta:
model = UserProfile 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 "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Dmitry Novikov nerosketch@gmail.com\n" "Last-Translator: Dmitry Novikov nerosketch@gmail.com\n"
"Language: ru\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" "%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" "%100>=11 && n%100<=14)? 2 : 3);\n"
#: models.py:22
#: models.py:23
msgid "Users must have an telephone number" msgid "Users must have an telephone number"
msgstr "У пользователей должен быть номер телефона" 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" msgid "profile username"
msgstr "Логин" msgstr "Логин"
#: models.py:55
#: models.py:56
msgid "fio" msgid "fio"
msgstr "ФИО" msgstr "ФИО"
#: models.py:56
#: models.py:57
msgid "birth day" msgid "birth day"
msgstr "дата рождения" msgstr "дата рождения"
#: models.py:57
#: models.py:58
msgid "Is active" msgid "Is active"
msgstr "Активен" 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" msgid "Telephone"
msgstr "Телефон" msgstr "Телефон"
#: models.py:95
#: models.py:96
msgid "Author" msgid "Author"
msgstr "Автор" msgstr "Автор"
#: models.py:96 templates/accounts/action_log.html:12
#: models.py:97 templates/accounts/action_log.html:12
msgid "Meta information" msgid "Meta information"
msgstr "Мета информация" msgstr "Мета информация"
#: models.py:98
#: models.py:99
msgid "Create user" msgid "Create user"
msgstr "Создание абонента" msgstr "Создание абонента"
#: models.py:99
#: models.py:100
msgid "Delete user" msgid "Delete user"
msgstr "Удаление абонента" msgstr "Удаление абонента"
#: models.py:100
#: models.py:101
msgid "Create device" msgid "Create device"
msgstr "Создание устройства" msgstr "Создание устройства"
#: models.py:101
#: models.py:102
msgid "Delete device" msgid "Delete device"
msgstr "Удаление устройства" msgstr "Удаление устройства"
#: models.py:102
#: models.py:103
msgid "Create NAS" msgid "Create NAS"
msgstr "Создание NAS" msgstr "Создание NAS"
#: models.py:103
#: models.py:104
msgid "Delete NAS" msgid "Delete NAS"
msgstr "Удаление NAS" msgstr "Удаление NAS"
#: models.py:104
#: models.py:105
msgid "Create service" msgid "Create service"
msgstr "Создание тарифа" msgstr "Создание тарифа"
#: models.py:105
#: models.py:106
msgid "Delete service" msgid "Delete service"
msgstr "Удаление тарифа" msgstr "Удаление тарифа"
#: models.py:107
#: models.py:108
msgid "Action type" msgid "Action type"
msgstr "Тип действия" msgstr "Тип действия"
#: models.py:108
#: models.py:109
msgid "Additional info" msgid "Additional info"
msgstr "Дополнительная информация" msgstr "Дополнительная информация"
#: models.py:109
#: models.py:110
msgid "Action date" msgid "Action date"
msgstr "Дата действия" msgstr "Дата действия"
#: models.py:116
#: models.py:117
msgid "User profile log" msgid "User profile log"
msgstr "Лог действий учётной записи" msgstr "Лог действий учётной записи"
#: models.py:117
#: models.py:118
msgid "User profile logs" msgid "User profile logs"
msgstr "Логи действий учётной записи" msgstr "Логи действий учётной записи"
#: models.py:126
#: models.py:127
msgid "Avatar" msgid "Avatar"
msgstr "Аватар" msgstr "Аватар"
#: models.py:128
#: models.py:129
msgid "Responsibility groups" msgid "Responsibility groups"
msgstr "Группы администратора" 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" msgid "Staff account profile"
msgstr "Учётная запись работника" msgstr "Учётная запись работника"
#: models.py:143
#: models.py:150
msgid "Staff account profiles" msgid "Staff account profiles"
msgstr "Учётные записи работников" 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/ext.html:7
#: templates/accounts/perms/object/objects_of_type.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" msgid "Administrators"
msgstr "Сотрудники" msgstr "Сотрудники"
@ -142,7 +157,6 @@ msgid "Fullname, or login if name is empty"
msgstr "ФИО (или ник если нет)" msgstr "ФИО (или ник если нет)"
#: templates/accounts/acc_list.html:24 #: templates/accounts/acc_list.html:24
#: templates/accounts/settings/ch_info.html:28
msgid "Email" msgid "Email"
msgstr "Адрес электронной почты" msgstr "Адрес электронной почты"
@ -178,61 +192,63 @@ msgstr "Описание"
msgid "That admin has no logs" msgid "That admin has no logs"
msgstr "Эта учётная запись не имеет логов" msgstr "Эта учётная запись не имеет логов"
#: templates/accounts/create_acc.html:9
#: templates/accounts/create_acc.html:8
msgid "Add" msgid "Add"
msgstr "Добавить" msgstr "Добавить"
#: templates/accounts/create_acc.html:16
#: templates/accounts/create_acc.html:15
msgid "Create new account" msgid "Create new account"
msgstr "Создать новую учётную запись" 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" msgid "Username"
msgstr "Логин" 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" msgid "Fullname"
msgstr "Полное имя" msgstr "Полное имя"
#: templates/accounts/create_acc.html:52
#: templates/accounts/create_acc.html:51
msgid "EMail" msgid "EMail"
msgstr "Адрес электронной почты" 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" msgid "+[7,8,9,3] and 10,11 digits"
msgstr "+[7,8,9,3] и 10,11 цифр" msgstr "+[7,8,9,3] и 10,11 цифр"
#: templates/accounts/create_acc.html:72
#: templates/accounts/create_acc.html:71
msgid "Type password" msgid "Type password"
msgstr "Введите пароль" msgstr "Введите пароль"
#: templates/accounts/create_acc.html:80
#: templates/accounts/create_acc.html:79
msgid "Repeat password" msgid "Repeat password"
msgstr "Повторите пароль" msgstr "Повторите пароль"
#: templates/accounts/create_acc.html:89
#: templates/accounts/create_acc.html:88
#: templates/accounts/manage_responsibility_groups.html:20 #: 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/set_abon_groups_permission.html:20
#: templates/accounts/settings/ch_info.html:67
#: templates/accounts/settings/userprofile_form.html:11
msgid "Save" msgid "Save"
msgstr "Сохранить" msgstr "Сохранить"
#: templates/accounts/create_acc.html:92
#: templates/accounts/create_acc.html:91
#: templates/accounts/manage_responsibility_groups.html:21 #: 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/set_abon_groups_permission.html:21
#: templates/accounts/settings/ch_info.html:70
#: templates/accounts/settings/userprofile_form.html:14
msgid "Reset" msgid "Reset"
msgstr "Сбросить" 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" msgid "User name"
msgstr "Логин" msgstr "Логин"
#: templates/accounts/index.html:17
msgid "Name and surname"
msgstr "Имя и отчество"
#: templates/accounts/index.html:21 #: templates/accounts/index.html:21
msgid "Is enable" msgid "Is enable"
msgstr "Включён-ли" msgstr "Включён-ли"
@ -242,8 +258,8 @@ msgid "Last login"
msgstr "Последняя авторизация" msgstr "Последняя авторизация"
#: templates/accounts/index.html:30 #: templates/accounts/index.html:30
msgid "All permissions"
msgstr "Административный доступ (все права)"
msgid "Is superuser"
msgstr "Является суперпользователем"
#: templates/accounts/login.html:5 #: templates/accounts/login.html:5
msgid "Auth" msgid "Auth"
@ -261,47 +277,47 @@ msgstr "Войти по местоположению"
msgid "The responsibility of the administrator of the group of subscribers" msgid "The responsibility of the administrator of the group of subscribers"
msgstr "Ответственность администратора за группы абонентов" 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 #: templates/accounts/perms/ext.html:9 templates/accounts/perms/ext.html:14
msgid "Permission options" msgid "Permission options"
msgstr "Права" msgstr "Права"
#: templates/accounts/perms/change_global_perms.html:11
#: templates/accounts/perms/change_global_perms.html:10
#: templates/accounts/perms/ext.html:22 #: templates/accounts/perms/ext.html:22
msgid "Global permission options" msgid "Global permission options"
msgstr "Глобальные права" msgstr "Глобальные права"
#: templates/accounts/perms/change_global_perms.html:16
#: templates/accounts/perms/change_global_perms.html:15
msgid "Select permissions for picked account" msgid "Select permissions for picked account"
msgstr "Отметьте права для выбранной учётной записи" msgstr "Отметьте права для выбранной учётной записи"
#: templates/accounts/perms/ext.html:27 #: templates/accounts/perms/ext.html:27
#: templates/accounts/perms/object/objects_of_type.html:9 #: 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: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" msgid "Object permission options"
msgstr "Права для каждого объекта" msgstr "Права для каждого объекта"
#: templates/accounts/perms/object/objects_of_type.html:16 #: 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" msgid "Pick object for edit permissions"
msgstr "Выберите объект для редактирования прав доступа" msgstr "Выберите объект для редактирования прав доступа"
#: templates/accounts/perms/object/objects_types.html:16
#: templates/accounts/perms/object/objects_types.html:15
msgid "Pick the type of object" msgid "Pick the type of object"
msgstr "Выберите тип объекта" msgstr "Выберите тип объекта"
#: templates/accounts/perms/object/objects_types.html:24
#: templates/accounts/perms/object/objects_types.html:23
msgid "Group" msgid "Group"
msgstr "Группа" 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" msgid "Profile is superuser, permissions to change it makes no sense"
msgstr "" msgstr ""
"Учётная запись является суперпользователем. Разрешения менять нет смысла.,"
"Учётная запись является суперпользователем. Разрешения менять нет смысла."
#: templates/accounts/perms/object/perms_edit.html:33
#: templates/accounts/perms/object/perms_edit.html:32
msgid "Change permission for that object" msgid "Change permission for that object"
msgstr "Изменение прав доступа для выбранного объекта" msgstr "Изменение прав доступа для выбранного объекта"
@ -309,47 +325,35 @@ msgstr "Изменение прав доступа для выбранного
msgid "The list of user groups to which the account has access" msgid "The list of user groups to which the account has access"
msgstr "Список групп абонентов, к которым учётка имеет доступ" 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" msgid "You forget specify a password for the new account"
msgstr "Забыли указать пароль для нового аккаунта" msgstr "Забыли указать пароль для нового аккаунта"
#: views.py:152
#: views.py:157
msgid "You forget to repeat a password for the new account" msgid "You forget to repeat a password for the new account"
msgstr "Забыли повторить пароль для нового аккаунта" msgstr "Забыли повторить пароль для нового аккаунта"
#: views.py:161
#: views.py:166
msgid "Subscriber with this name already exist" msgid "Subscriber with this name already exist"
msgstr "Пользователь с таким именем уже есть" msgstr "Пользователь с таким именем уже есть"
#: views.py:163
#: views.py:168
msgid "Passwords does not match, try again" msgid "Passwords does not match, try again"
msgstr "Пароли не совпадают, попробуйте ещё раз" msgstr "Пароли не совпадают, попробуйте ещё раз"
#: views.py:178
#: views.py:183
msgid "Profile has been deleted" msgid "Profile has been deleted"
msgstr "Учётная запись удалена" msgstr "Учётная запись удалена"
#: views.py:240
#: views.py:244 views.py:288
msgid "Permissions has successfully updated" msgid "Permissions has successfully updated"
msgstr "Права успешно обновлены" msgstr "Права успешно обновлены"
#: views.py:352
#: views.py:354
msgid "Responsibilities has been updated" msgid "Responsibilities has been updated"
msgstr "Ответственность за группы обновлена" msgstr "Ответственность за группы обновлена"
@ -374,5 +378,5 @@ msgstr "Лог действий"
msgid "Administrator" msgid "Administrator"
msgstr "Сотрудник" 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 -*- # -*- coding:utf-8 -*-
import os import os
from PIL import Image from PIL import Image
from bitfield.models import BitField
from jsonfield import JSONField from jsonfield import JSONField
from django.db import models from django.db import models
@ -124,8 +125,14 @@ class UserProfileManager(MyUserManager):
class UserProfile(BaseAccount): class UserProfile(BaseAccount):
avatar = models.ImageField(_('Avatar'), upload_to=os.path.join('user', 'avatar'), null=True, default=None, blank=True) 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')) 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() objects = UserProfileManager()

11
accounts_app/templates/accounts/acc_list.html

@ -27,11 +27,12 @@
</thead> </thead>
<tbody> <tbody>
{% for usr in users %} {% 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><a href="{% url 'acc_app:other_profile' usr.id %}">{{ usr.username }}</a></td>
<td>{{ usr.get_full_name }}</td> <td>{{ usr.get_full_name }}</td>
<td class="hidden-xs"> <td class="hidden-xs">

9
accounts_app/templates/accounts/ext.htm

@ -17,7 +17,7 @@
<div class="row"> <div class="row">
<div class="col-sm-3"> <div class="col-sm-3">
<div class="thumbnail"> <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 }}"/> <img alt="ava" src="{{ userprofile.get_min_ava }}"/>
</a> </a>
<div class="caption btn-group btn-group-sm"> <div class="caption btn-group btn-group-sm">
@ -26,6 +26,13 @@
<span class="glyphicon glyphicon-edit"></span> <span class="glyphicon glyphicon-edit"></span>
<span class="hidden-sm hidden-md">{% trans 'Edit' %}</span> <span class="hidden-sm hidden-md">{% trans 'Edit' %}</span>
</a> </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 %} {% endif %}
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<a href="{% url 'acc_app:setup_perms' userprofile.pk %}" class="btn btn-default" <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"> <table class="table-striped table-bordered">
<tbody> <tbody>
<tr> <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>
<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>
<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>
<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>
<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> </tr>
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<tr> <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> </tr>
{% endif %} {% endif %}
</tbody> </tbody>

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

@ -5,7 +5,13 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li> <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: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> <li class="active">{% trans 'Options' %}</li>
</ol> </ol>
{% endblock %} {% endblock %}
@ -17,10 +23,9 @@
{% block main %} {% block main %}
<div class="row"> <div class="row">
<div class="col-sm-3"> <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"> <div class="caption">
<button type="submit" class="btn btn-primary">{% trans 'Save' %}</button> <button type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
@ -30,11 +35,10 @@
</div> </div>
<div class="col-sm-9"> <div class="col-sm-9">
<h3>{{ user.username|default:_('Not assigned') }}</h3>
<h3>{{ object.username|default:_('Not assigned') }}</h3>
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active"> <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' %} {% trans 'Change self onfo' %}
</a> </a>
</li> </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 %} {% load globaltags i18n bootstrap3 %}
{% block content %} {% 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 %} {% 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('logout/', LogoutView.as_view(next_page='acc_app:login'), name='logout'),
path('login_by_location/', views.location_login, name='llogin'), 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('add/', views.create_profile, name='create_profile'),
path('settings/', views.UpdateSelfAccount.as_view(), name='setup_info'), path('settings/', views.UpdateSelfAccount.as_view(), name='setup_info'),
path('settings/change_ava/', views.AvatarUpdateView.as_view(), name='setup_avatar'), 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/', views.PermsUpdateView.as_view(), name='setup_perms'),
path('<int:uid>/perms/object/', views.perms_object, name='setup_perms_object'), 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>/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') 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.apps import apps
from django.contrib.auth.decorators import login_required 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.forms import AuthenticationForm
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView 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.contrib import messages
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
from django.utils.decorators import method_decorator 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 django.conf import settings
from group_app.models import Group from group_app.models import Group
from .models import UserProfile, UserProfileLog 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.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.decorators import permission_required_or_403 as permission_required
from guardian.shortcuts import get_objects_for_user, assign_perm, remove_perm from guardian.shortcuts import get_objects_for_user, assign_perm, remove_perm
login_decs = login_required, only_admins
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
template_name = 'accounts/login.html' template_name = 'accounts/login.html'
def form_invalid(self, form): 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) return super().form_invalid(form)
def get_success_url(self): def get_success_url(self):
@ -39,7 +37,7 @@ class CustomLoginView(LoginView):
if next_url: if next_url:
return next_url return next_url
if self.request.user.is_staff: 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') return resolve_url('client_side:home')
@ -52,7 +50,7 @@ def location_login(request):
login(request, auser) login(request, auser)
if nextl == 'None' or nextl is None or nextl == '': if nextl == 'None' or nextl is None or nextl == '':
if request.user.is_staff: if request.user.is_staff:
return redirect('acc_app:profile')
return redirect('acc_app:setup_info')
return redirect('client_side:home') return redirect('client_side:home')
return redirect(nextl) return redirect(nextl)
return render(request, 'accounts/login.html', { return render(request, 'accounts/login.html', {
@ -63,35 +61,28 @@ def location_login(request):
return redirect('client_side:home') 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' template_name = 'accounts/settings/ch_info.html'
def get_object(self, queryset=None): 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) 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): class UpdateSelfAccount(LoginRequiredMixin, UpdateView):
form_class = UserProfileForm
form_class = forms.UserProfileForm
pk_url_kwarg = 'uid'
model = UserProfile model = UserProfile
template_name = 'accounts/userprofile_form.html'
template_name = 'accounts/settings/userprofile_form.html'
def get_object(self, queryset=None): def get_object(self, queryset=None):
return self.request.user return self.request.user
def form_valid(self, form): def form_valid(self, form):
r = super(UpdateSelfAccount, self).form_valid(form)
r = super().form_valid(form)
messages.success(self.request, _('Saved successfully')) messages.success(self.request, _('Saved successfully'))
return r 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 @login_required
@only_admins @only_admins
@ -165,8 +183,7 @@ def delete_profile(request, uid: int):
return redirect('acc_app:accounts_list') return redirect('acc_app:accounts_list')
@method_decorator(login_decs, name='dispatch')
class AccountsListView(ListView):
class AccountsListView(LoginRequiredMixin, OnlyAdminsMixin, ListView):
http_method_names = 'get', http_method_names = 'get',
paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10) paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
template_name = 'accounts/acc_list.html' template_name = 'accounts/acc_list.html'
@ -200,7 +217,7 @@ class PermsUpdateView(UpdateView):
http_method_names = 'get', 'post' http_method_names = 'get', 'post'
template_name = 'accounts/perms/change_global_perms.html' template_name = 'accounts/perms/change_global_perms.html'
model = UserProfile model = UserProfile
form_class = UserPermissionsForm
form_class = forms.UserPermissionsForm
pk_url_kwarg = 'uid' pk_url_kwarg = 'uid'
def get_success_url(self): def get_success_url(self):
@ -227,8 +244,7 @@ class PermsUpdateView(UpdateView):
return super(PermsUpdateView, self).form_valid(form) 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', http_method_names = 'get',
paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10) paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
template_name = 'accounts/perms/object/objects_of_type.html' 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) klass = apps.get_model(app_label, model_name)
obj = get_object_or_404(klass, pk=obj_id) 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(): if request.method == 'POST' and frm.is_valid():
frm.save_obj_perms() frm.save_obj_perms()
messages.success(request, _('Permissions has successfully updated')) 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') http_method_names = ('get', 'post')
template_name = 'accounts/manage_responsibility_groups.html' template_name = 'accounts/manage_responsibility_groups.html'
context_object_name = 'groups' context_object_name = 'groups'
@ -339,11 +354,10 @@ class ManageResponsibilityGroups(ListView):
return HttpResponseRedirect(self.get_success_url()) 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) paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
template_name = 'accounts/action_log.html' template_name = 'accounts/action_log.html'
permission_required = 'accounts_app.view_userprofilelog'
model = UserProfileLog model = UserProfileLog
def get_queryset(self): 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() cursor = db.cursor()
sql = ( 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) ln = cursor.execute(sql)
with open(tmp_ipuser_file, 'w') as f: with open(tmp_ipuser_file, 'w') as f:

2
agent/netflow/start_netflow.sh

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/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 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 django.utils.translation import gettext_lazy as _, gettext
from abonapp.models import AbonLog, InvoiceForPayment, Abon from abonapp.models import AbonLog, InvoiceForPayment, Abon
from abonapp.tasks import customer_nas_command
from djing.lib.decorators import json_view from djing.lib.decorators import json_view
from tariff_app.models import Tariff from tariff_app.models import Tariff
from taskapp.models import Task from taskapp.models import Task
@ -58,7 +59,7 @@ def buy_service(request, srv_id):
service, None, service, None,
_("Buy the service via user side, service '%s'") % service _("Buy the service via user side, service '%s'") % service
) )
abon.nas_sync_self()
customer_nas_command.delay(abon.pk, 'sync')
messages.success( messages.success(
request, request,
_("The service '%s' wan successfully activated") % service.title _("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: def validate_extra_snmp_info(v: str) -> None:
""" """
Validate extra snmp field for each device. 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. with description of error.
:param v: String value for validate :param v: String value for validate
""" """
@ -89,12 +89,13 @@ class DevBase(object, metaclass=ABCMeta):
class BasePort(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.num = int(num)
self.nm = name self.nm = name
self.st = status self.st = status
self._mac = mac self._mac = mac
self.sp = speed self.sp = speed
self.writable = writable
@abstractmethod @abstractmethod
def disable(self): def disable(self):
@ -120,7 +121,10 @@ class SNMPBaseWorker(object, metaclass=ABCMeta):
def start_ses(self): def start_ses(self):
if self.ses is None: 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): def set_int_value(self, oid: str, value):
self.start_ses() self.start_ses()
@ -139,4 +143,6 @@ class SNMPBaseWorker(object, metaclass=ABCMeta):
def get_item(self, oid): def get_item(self, oid):
self.start_ses() 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 typing import AnyStr, Iterable, Optional, Dict
from datetime import timedelta from datetime import timedelta
from easysnmp import EasySNMPTimeoutError from easysnmp import EasySNMPTimeoutError
from pexpect import TIMEOUT
from transliterate import translit from transliterate import translit
from django.utils.translation import gettext_lazy as _, gettext from django.utils.translation import gettext_lazy as _, gettext
from djing.lib import RuTimedelta, safe_int 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 ( from .base_intr import (
DevBase, SNMPBaseWorker, BasePort, DeviceImplementationError, DevBase, SNMPBaseWorker, BasePort, DeviceImplementationError,
ListOrError, DeviceConfigurationError ListOrError, DeviceConfigurationError
@ -43,7 +44,7 @@ def plain_ip_device_mon_template(device) -> Optional[AnyStr]:
class DLinkPort(BasePort): class DLinkPort(BasePort):
def __init__(self, num, name, status, mac, speed, snmp_worker): 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): if not issubclass(snmp_worker.__class__, SNMPBaseWorker):
raise TypeError raise TypeError
self.snmp_worker = snmp_worker 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')) 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')) 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')) speeds = tuple(self.get_list('.1.3.6.1.2.1.2.2.1.5'))
res = []
try: try:
for n in range(interfaces_count): for n in range(interfaces_count):
status = True if int(stats[n]) == 1 else False status = True if int(stats[n]) == 1 else False
res.append(DLinkPort(
yield DLinkPort(
n + 1, n + 1,
nams[n] if len(nams) > 0 else '', nams[n] if len(nams) > 0 else '',
status, status,
macs[n] if len(macs) > 0 else _('does not fetch the mac'), macs[n] if len(macs) > 0 else _('does not fetch the mac'),
int(speeds[n]) if len(speeds) > 0 else 0, int(speeds[n]) if len(speeds) > 0 else 0,
self))
return res
self)
except IndexError: except IndexError:
return DeviceImplementationError('Dlink port index error'), res
return DeviceImplementationError('Dlink port index error')
def get_device_name(self): def get_device_name(self):
return self.get_item('.1.3.6.1.2.1.1.1.0') return self.get_item('.1.3.6.1.2.1.1.1.0')
@ -137,7 +136,7 @@ class ONUdev(BasePort):
class OLTDevice(DevBase, SNMPBaseWorker): class OLTDevice(DevBase, SNMPBaseWorker):
has_attachable_to_subscriber = False has_attachable_to_subscriber = False
description = _('PON OLT')
description = 'PON OLT'
is_use_device_port = False is_use_device_port = False
def __init__(self, dev_instance): def __init__(self, dev_instance):
@ -161,7 +160,7 @@ class OLTDevice(DevBase, SNMPBaseWorker):
status=True if status == '3' else False, 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), mac=self.get_item('.1.3.6.1.4.1.3320.101.10.1.1.3.%d' % n),
speed=0, speed=0,
signal=int(signal) / 10 if signal != 'NOSUCHINSTANCE' else 0,
signal=int(signal or 0),
snmp_worker=self) snmp_worker=self)
res.append(onu) res.append(onu)
except EasySNMPTimeoutError as e: except EasySNMPTimeoutError as e:
@ -196,7 +195,7 @@ class OLTDevice(DevBase, SNMPBaseWorker):
class OnuDevice(DevBase, SNMPBaseWorker): class OnuDevice(DevBase, SNMPBaseWorker):
has_attachable_to_subscriber = True has_attachable_to_subscriber = True
description = _('PON ONU')
description = 'PON ONU BDCOM'
tech_code = 'bdcom_onu' tech_code = 'bdcom_onu'
is_use_device_port = False 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) 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) 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) 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) # uptime = self.get_item('.1.3.6.1.2.1.2.2.1.9.%d' % num)
signal = safe_int(signal) signal = safe_int(signal)
if status.isdigit():
if status is not None and status.isdigit():
return { return {
'status': status, 'status': status,
'signal': signal / 10 if signal != 0 else 0, 'signal': signal / 10 if signal != 0 else 0,
@ -260,7 +261,7 @@ class OnuDevice(DevBase, SNMPBaseWorker):
try: try:
int(v) int(v)
except ValueError: 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]: def monitoring_template(self, *args, **kwargs) -> Optional[str]:
device = self.db_instance device = self.db_instance
@ -317,17 +318,15 @@ class EltexSwitch(DLinkDevice):
tech_code = 'eltex_sw' tech_code = 'eltex_sw'
def get_ports(self) -> ListOrError: def get_ports(self) -> ListOrError:
res = []
for i, n in enumerate(range(49, 77), 1): 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) 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): def get_device_name(self):
return self.get_item('.1.3.6.1.2.1.1.5.0') 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): class Olt_ZTE_C320(OLTDevice):
description = _('OLT ZTE C320')
description = 'OLT ZTE C320'
def get_fibers(self): def get_fibers(self):
fibers = ({ fibers = ({
@ -415,8 +414,45 @@ class Olt_ZTE_C320(OLTDevice):
return 'olt_ztec320.html' 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): class ZteOnuDevice(OnuDevice):
description = _('ZTE PON ONU')
description = 'Zte ONU F660'
tech_code = 'zte_onu' tech_code = 'zte_onu'
def get_details(self) -> Optional[Dict]: 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) 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) 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) 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) 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) 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 { return {
'status': status, 'status': status,
'signal': conv_signal(safe_int(signal)), '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 pass
def get_template_name(self): def get_template_name(self):
@ -456,7 +500,7 @@ class ZteOnuDevice(OnuDevice):
fiber_num, onu_port = v.split('.') fiber_num, onu_port = v.split('.')
int(fiber_num), int(onu_port) int(fiber_num), int(onu_port)
except ValueError: 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]: def monitoring_template(self, *args, **kwargs) -> Optional[str]:
device = self.db_instance device = self.db_instance
@ -483,42 +527,7 @@ class ZteOnuDevice(OnuDevice):
return '\n'.join(i for i in r if i) return '\n'.join(i for i in r if i)
def register_device(self, extra_data: Dict): 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): def get_fiber_str(self):
dev = self.db_instance dev = self.db_instance
@ -534,3 +543,34 @@ class ZteOnuDevice(OnuDevice):
return 'gpon-onu_1/%d/%d:%s' % ( return 'gpon-onu_1/%d/%d:%s' % (
rack_num, fiber_num, onu_port_num 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 django.db import IntegrityError
from djing.lib import DuplicateEntry from djing.lib import DuplicateEntry
from djing.lib.tln.tln import ValidationError as TlnValidationError
from devapp.onu_config import ExpectValidationError
from . import models from . import models
from djing import MAC_ADDR_REGEX, IP_ADDR_REGEX from djing import MAC_ADDR_REGEX, IP_ADDR_REGEX
@ -43,7 +43,7 @@ class DeviceForm(forms.ModelForm):
manager_class = device.get_manager_klass() manager_class = device.get_manager_klass()
try: try:
manager_class.validate_extra_snmp_info(snmp_extra) manager_class.validate_extra_snmp_info(snmp_extra)
except TlnValidationError as e:
except ExpectValidationError as e:
raise ValidationError( raise ValidationError(
e, code='invalid' e, code='invalid'
) )

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

@ -30,18 +30,10 @@ msgstr "Свич D'Link"
msgid "does not fetch the mac" msgid "does not fetch the mac"
msgstr "не нашёл мак" msgstr "не нашёл мак"
#: dev_types.py:140
msgid "PON OLT"
msgstr ""
#: dev_types.py:169 views.py:345 views.py:503 #: dev_types.py:169 views.py:345 views.py:503
msgid "wait for a reply from the SNMP Timeout" msgid "wait for a reply from the SNMP Timeout"
msgstr "Время ожидания ответа от SNMP истекло" msgstr "Время ожидания ответа от SNMP истекло"
#: dev_types.py:199
msgid "PON ONU"
msgstr ""
#: dev_types.py:214 #: dev_types.py:214
msgid "Ip address or parent device with ip address required for ONU device" msgid "Ip address or parent device with ip address required for ONU device"
msgstr "" msgstr ""
@ -60,14 +52,6 @@ msgstr "Поле для snmp информации об ONU должно быть
msgid "Eltex switch" msgid "Eltex switch"
msgstr "Элтекс свич" msgstr "Элтекс свич"
#: dev_types.py:356
msgid "OLT ZTE C320"
msgstr ""
#: dev_types.py:419
msgid "ZTE PON ONU"
msgstr ""
#: dev_types.py:454 #: dev_types.py:454
msgid "Zte onu snmp field must be two dot separated integers" msgid "Zte onu snmp field must be two dot separated integers"
msgstr "" msgstr ""
@ -324,7 +308,7 @@ msgstr "Название типа свича"
#: templates/devapp/custom_dev_page/onu.html:22 #: templates/devapp/custom_dev_page/onu.html:22
#: templates/devapp/custom_dev_page/onu_for_zte.html:22 #: templates/devapp/custom_dev_page/onu_for_zte.html:22
msgid "Attached user" msgid "Attached user"
msgstr "Прикрепленный абонент"
msgstr "Прикреплённый абонент"
#: templates/devapp/custom_dev_page/onu.html:48 #: templates/devapp/custom_dev_page/onu.html:48
#: templates/devapp/custom_dev_page/onu_for_zte.html:50 #: templates/devapp/custom_dev_page/onu_for_zte.html:50
@ -337,7 +321,6 @@ msgid "ONU error"
msgstr "ONU ошибка" msgstr "ONU ошибка"
#: templates/devapp/custom_dev_page/onu.html:72 #: templates/devapp/custom_dev_page/onu.html:72
#: templates/devapp/custom_dev_page/onu_for_zte.html:75
msgid "Name on OLT" msgid "Name on OLT"
msgstr "Имя на OLT" msgstr "Имя на OLT"
@ -642,20 +625,26 @@ msgstr "Не заполнено поле 'Техническая информа
msgid "Fiber" msgid "Fiber"
msgstr "Интерфейс" 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), ('On', dev_types.OnuDevice),
('Ex', dev_types.EltexSwitch), ('Ex', dev_types.EltexSwitch),
('Zt', dev_types.Olt_ZTE_C320), ('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], devtype = models.CharField(_('Device type'), max_length=2, default=DEVICE_TYPES[0][0],
choices=MyChoicesAdapter(DEVICE_TYPES)) choices=MyChoicesAdapter(DEVICE_TYPES))
@ -108,7 +110,7 @@ class Port(models.Model):
class Meta: class Meta:
db_table = 'dev_port' db_table = 'dev_port'
unique_together = (('device', 'num'),)
unique_together = ('device', 'num')
permissions = ( permissions = (
('can_toggle_ports', _('Can toggle ports')), ('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 }}"> <a href="javascript:void(0);" class="port-img" title="{{ port.nm }}">
<b>{{ port.num }}</b> <b>{{ port.num }}</b>
</a> </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 %} {% 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> </a>
{% endif %} {% endif %}
</div> </div>

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

@ -21,8 +21,7 @@
{% for da in dev_accs %} {% for da in dev_accs %}
<li class="list-group-item">{% trans 'Attached user' %}: <li class="list-group-item">{% trans 'Attached user' %}:
{% if da.group %} {% 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 %} {% else %}
{{ da.get_full_name }} {{ da.get_full_name }}
{% endif %} {% endif %}
@ -32,7 +31,7 @@
<li class="list-group-item"> <li class="list-group-item">
{% with pdev=dev.parent_dev pdgrp=dev.parent_dev.group %} {% with pdev=dev.parent_dev pdgrp=dev.parent_dev.group %}
{% trans 'Parent device' %}: {% 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 }} {{ pdev.ip_address|default:'-' }} {{ pdev.comment }}
</a> </a>
{% endwith %} {% endwith %}

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

@ -22,8 +22,7 @@
{% for da in dev_accs %} {% for da in dev_accs %}
<li class="list-group-item">{% trans 'Attached user' %}: <li class="list-group-item">{% trans 'Attached user' %}:
{% if da.group %} {% 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 %} {% else %}
{{ da.get_full_name }} {{ da.get_full_name }}
{% endif %} {% endif %}
@ -33,9 +32,7 @@
<li class="list-group-item"> <li class="list-group-item">
{% with pdev=dev.parent_dev pdgrp=dev.parent_dev.group %} {% with pdev=dev.parent_dev pdgrp=dev.parent_dev.group %}
{% trans 'Parent device' %}: {% 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 }} {{ pdev.ip_address|default:'-' }} {{ pdev.comment }}
</a> </a>
{% endwith %} {% endwith %}
@ -73,15 +70,17 @@
</div> </div>
<div class="media-body"> <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> <b>{% trans 'Signal' %}</b>: {{ onu_details.signal }}<br>
{% if onu_details.ip_addr %} {% if onu_details.ip_addr %}
<b>{% trans 'Ip addr' %}</b>: {{ onu_details.ip_addr }}<br> <b>{% trans 'Ip addr' %}</b>: {{ onu_details.ip_addr }}<br>
{% endif %} {% endif %}
{% if onu_details.vlans %} {% if onu_details.vlans %}
<b>{% trans 'VLan list' %}</b>: {{ onu_details.vlans }}
<b>{% trans 'VLan list' %}</b>: {{ onu_details.vlans }}<br>
{% endif %} {% 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>
</div> </div>

2
devapp/templates/devapp/group_list.html

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

52
devapp/urls.py

@ -5,52 +5,32 @@ app_name = 'devapp'
urlpatterns = [ urlpatterns = [
path('', views.GroupsListView.as_view(), name='group_list'), 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('fix_onu/', views.fix_onu, name='fix_onu'),
path('<int:group_id>/', views.DevicesListView.as_view(), name='devs'), 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>/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>/', 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), path('search_dev/', views.search_dev),
# ZTE ports under fibers # 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 # Monitoring api
path('on_device_event/', views.OnDeviceMonitoringEvent.as_view()), path('on_device_event/', views.OnDeviceMonitoringEvent.as_view()),
# Nagios mon generate # 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 abonapp.models import Abon
from accounts_app.models import UserProfile from accounts_app.models import UserProfile
from chatbot.models import ChatException
from devapp.base_intr import DeviceImplementationError from devapp.base_intr import DeviceImplementationError
from django.conf import settings from django.conf import settings
from django.contrib import messages 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 json_view
from djing.lib.decorators import only_admins, hash_auth_view from djing.lib.decorators import only_admins, hash_auth_view
from djing.lib.mixins import LoginAdminPermissionMixin, LoginAdminMixin from djing.lib.mixins import LoginAdminPermissionMixin, LoginAdminMixin
from djing.lib.tln import ZteOltConsoleError, OnuZteRegisterError, \
ZteOltLoginFailed
from djing.tasks import multicast_email_notify from djing.tasks import multicast_email_notify
from easysnmp import EasySNMPTimeoutError, EasySNMPError from easysnmp import EasySNMPTimeoutError, EasySNMPError
from group_app.models import Group 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 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, class DevicesListView(LoginAdminPermissionMixin,
@ -485,9 +483,8 @@ def devview(request, group_id: int, device_id: int):
template_name = 'generic_switch.html' template_name = 'generic_switch.html'
try: 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: if device.man_passw:
manager = device.get_manager_object() manager = device.get_manager_object()
ports = tuple(manager.get_ports()) ports = tuple(manager.get_ports())
@ -498,13 +495,15 @@ def devview(request, group_id: int, device_id: int):
template_name = manager.get_template_name() template_name = manager.get_template_name()
else: else:
messages.warning(request, _('Not Set snmp device password')) messages.warning(request, _('Not Set snmp device password'))
return render(request, 'devapp/custom_dev_page/' + template_name, { return render(request, 'devapp/custom_dev_page/' + template_name, {
'dev': device, 'dev': device,
'ports': ports, 'ports': ports,
'dev_accs': Abon.objects.filter(device=device), 'dev_accs': Abon.objects.filter(device=device),
'dev_manager': manager, 'dev_manager': manager,
'ports_db': Port.objects.filter(device=device).annotate( 'ports_db': Port.objects.filter(device=device).annotate(
num_abons=Count('abon')),
num_abons=Count('abon')
),
}) })
except EasySNMPError as e: except EasySNMPError as e:
messages.error(request, messages.error(request,
@ -726,21 +725,22 @@ class OnDeviceMonitoringEvent(global_base_views.SecureApiView):
} }
recipients = UserProfile.objects.get_profiles_by_group( 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_name': "%s(%s) %s" % (
device_down.ip_address, device_down.ip_address,
device_down.mac_addr, device_down.mac_addr,
device_down.comment 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 { return {
'text': 'notification successfully sent' 'text': 'notification successfully sent'
} }
except ChatException as e:
except ValueError as e:
return { return {
'text': str(e) 'text': str(e)
} }
@ -802,17 +802,25 @@ def register_device(request, group_id: int, device_id: int):
try: try:
device.register_device() device.register_device()
status = 0 status = 0
except OnuZteRegisterError:
except onu_config.OnuZteRegisterError:
text = format_msg(gettext('Unregistered onu not found'), 'eye-close') 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') text = format_msg(e, 'exclamation-sign')
except DeviceImplementationError as e: except DeviceImplementationError as e:
text = format_msg(e, 'wrench')
text = format_msg(str(e), 'wrench')
except ProcessLocked: except ProcessLocked:
text = format_msg(gettext('Process locked by another process'), 'time')
text = format_msg(
gettext('Process locked by another process'),
'time'
)
else: else:
text = format_msg(msg='ok', icon='ok') text = format_msg(msg='ok', icon='ok')
return { return {

2
dialing_app/templates/index.html

@ -60,7 +60,7 @@
<span class="glyphicon glyphicon-play"></span> <span class="glyphicon glyphicon-play"></span>
<audio preload="metadata" src="{{ lurl|default:'#' }}"></audio> <audio preload="metadata" src="{{ lurl|default:'#' }}"></audio>
</button> </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> <span class="glyphicon glyphicon-download-alt"></span>
</a> </a>
</td> </td>

2
dialing_app/templatetags/telephone_filters.py

@ -14,7 +14,7 @@ def abon_if_telephone(value):
if value[0] != '+': if value[0] != '+':
value = '+' + value value = '+' + value
url = resolve_url('dialapp:to_abon', tel=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 return a
else: else:
return value return value

1
djing/fields.py

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

22
djing/lib/decorators.py

@ -1,29 +1,11 @@
from functools import wraps from functools import wraps
from django.conf import settings from django.conf import settings
from django.http import HttpResponseRedirect, HttpResponseForbidden, JsonResponse
from django.http import HttpResponseForbidden, JsonResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from djing.lib import check_sign 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 # Allow to view only admins
def only_admins(fn): def only_admins(fn):
@wraps(fn) @wraps(fn)
@ -99,6 +81,8 @@ def json_view(fn):
@wraps(fn) @wraps(fn)
def wrapped(request, *args, **kwargs): def wrapped(request, *args, **kwargs):
r = fn(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={ return JsonResponse(r, safe=False, json_dumps_params={
'ensure_ascii': False '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 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): class OnlyAdminsMixin(AccessMixin):
"""Verify that the current user is admin.""" """Verify that the current user is admin."""
def dispatch(self, request, *args, **kwargs): 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_HOST_PASSWORD = 'password'
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
# public url for Viber Bot
VIBER_BOT_PUBLIC_URL = 'https://your_domain.name'
# Encrypted fields # Encrypted fields
# https://pypi.org/project/django-encrypted-model-fields/ # https://pypi.org/project/django-encrypted-model-fields/
# You must change this value # You must change this value

10
djing/settings.py

@ -42,6 +42,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'encrypted_model_fields', 'encrypted_model_fields',
'django_cleanup.apps.CleanupConfig',
'ip_pool', 'ip_pool',
'accounts_app', 'accounts_app',
'gw_app', 'gw_app',
@ -50,10 +51,11 @@ INSTALLED_APPS = [
'searchapp', 'searchapp',
'devapp', 'devapp',
'mapapp', 'mapapp',
'traf_stat',
'finapp', 'finapp',
'taskapp', 'taskapp',
'clientsideapp', 'clientsideapp',
'chatbot',
'messenger',
'msg_app', 'msg_app',
'dialing_app', 'dialing_app',
'group_app', 'group_app',
@ -182,7 +184,7 @@ DEFAULT_PICTURE = '/static/img/user_ava.gif'
AUTH_USER_MODEL = 'accounts_app.UserProfile' AUTH_USER_MODEL = 'accounts_app.UserProfile'
LOGIN_URL = reverse_lazy('acc_app:login') 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') LOGOUT_URL = reverse_lazy('acc_app:logout')
PAGINATION_ITEMS_PER_PAGE = local_settings.PAGINATION_ITEMS_PER_PAGE 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' 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 # Encrypted fields
# https://pypi.org/project/django-encrypted-model-fields/ # https://pypi.org/project/django-encrypted-model-fields/
FIELD_ENCRYPTION_KEY = getattr( 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): def multicast_email_notify(msg_text: str, account_ids: Iterable):
text_content = strip_tags(msg_text) text_content = strip_tags(msg_text)
for acc_id in account_ids: for acc_id in account_ids:
if not acc_id:
continue
try: try:
account = UserProfile.objects.get(pk=acc_id) account = UserProfile.objects.get(pk=acc_id)
target_email = account.email target_email = account.email

3
djing/urls.py

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

2
djing/views.py

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

3
docs/extra_func.md

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

7
group_app/views.py

@ -9,6 +9,7 @@ from django.contrib import messages
from django.conf import settings from django.conf import settings
from djing.lib.decorators import only_admins from djing.lib.decorators import only_admins
from guardian.decorators import permission_required_or_403 as permission_required 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 djing.global_base_views import OrderedFilteredList
from . import models from . import models
@ -27,6 +28,12 @@ class GroupListView(OrderedFilteredList):
model = models.Group model = models.Group
context_object_name = 'groups' 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(login_decs, name='dispatch')
@method_decorator(permission_required('group_app.change_group'), 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 typing import Iterator, Tuple, Optional
from djing import ping from djing import ping
from gw_app.nas_managers.structs import SubnetQueue, VectorQueue from gw_app.nas_managers.structs import SubnetQueue, VectorQueue
@ -16,7 +16,8 @@ class NasNetworkError(Exception):
# Communicate with gw # Communicate with gw
class BaseTransmitter(ABC): class BaseTransmitter(ABC):
@abstractproperty
@property
@abstractmethod
def description(self): def description(self):
""" """
:return: Returnd a description of nas implementation :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, '=target=%s' % queue.network,
'=max-limit=%.3fM/%.3fM' % queue.max_limit, '=max-limit=%.3fM/%.3fM' % queue.max_limit,
'=queue=Djing_pcq_up/Djing_pcq_down', '=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: def remove_queue(self, queue: i_structs.SubnetQueue) -> None:
@ -300,7 +301,7 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
if queue_gw is None: if queue_gw is None:
return self.add_queue(queue) return self.add_queue(queue)
else: else:
cmd = [
cmd = (
'/queue/simple/set', '/queue/simple/set',
'=name=%s' % queue.name, '=name=%s' % queue.name,
'=max-limit=%.3fM/%.3fM' % queue.max_limit, '=max-limit=%.3fM/%.3fM' % queue.max_limit,
@ -308,10 +309,11 @@ class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
# или =target-addresses или =target # или =target-addresses или =target
'=target=%s' % queue.network, '=target=%s' % queue.network,
'=queue=Djing_pcq_up/Djing_pcq_down', '=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) r = self._exec_cmd(cmd)
return r return r

4
gw_app/nas_managers/structs.py

@ -23,7 +23,7 @@ class SubnetQueue(BaseStruct):
return self._max_limit return self._max_limit
def set_max_limit(self, v): def set_max_limit(self, v):
if isinstance(v, tuple):
if isinstance(v, (tuple, list)):
self._max_limit = v self._max_limit = v
elif isinstance(v, str): elif isinstance(v, str):
s_in, s_out = v.split('/') s_in, s_out = v.split('/')
@ -32,7 +32,7 @@ class SubnetQueue(BaseStruct):
sp = float(v) sp = float(v)
self._max_limit = sp, sp self._max_limit = sp, sp
else: 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) 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',) ordering = ('network',)
# Deprecated. Remove after migrations squashed
class IpLeaseManager(models.Manager): class IpLeaseManager(models.Manager):
def get_free_ip(self, network: NetworkModel): def get_free_ip(self, network: NetworkModel):
@ -234,6 +235,7 @@ class IpLeaseModel(models.Model):
unique_together = ('ip', 'network', 'mac_addr') unique_together = ('ip', 'network', 'mac_addr')
# Deprecated. Remove after migrations squashed
class LeasesHistory(models.Model): class LeasesHistory(models.Model):
ip = models.GenericIPAddressField(verbose_name=_('Ip address')) ip = models.GenericIPAddressField(verbose_name=_('Ip address'))
lease_time = models.DateTimeField(_('Lease time'), auto_now_add=True) lease_time = models.DateTimeField(_('Lease time'), auto_now_add=True)

3
locale/ru/LC_MESSAGES/django.po

@ -128,3 +128,6 @@ msgstr "Вы уверены в этом?"
msgid "Finance" msgid "Finance"
msgstr "Финансы" 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): class ConversationForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ConversationForm, self).__init__(*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: if user_profile_queryset is not None:
self.fields['participants'].choices = [(up.pk, up.get_full_name()) for up in user_profile_queryset] 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 django.utils.translation import gettext_lazy as _
from accounts_app.models import UserProfile from accounts_app.models import UserProfile
from djing.tasks import send_email_notify from djing.tasks import send_email_notify
from chatbot.models import ChatException
class MessageError(Exception): class MessageError(Exception):
@ -203,23 +202,21 @@ class Conversation(models.Model):
return messages[0] return messages[0]
def new_message(self, text, attachment, author, with_status=True): 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( send_email_notify.delay(
msg_text=text, msg_text=text,
account_id=participant.pk account_id=participant.pk
) )
return msg
except ChatException as e:
raise MessageError(e)
return msg
@staticmethod @staticmethod
def remove_message(msg): def remove_message(msg):

4
msg_app/templates/msg_app/chat.html

@ -37,7 +37,7 @@
<div class="row"> <div class="row">
<div class="col-sm-1"> <div class="col-sm-1">
{% if can_view_profile %} {% 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"> <img src="{{ author.get_min_ava }}" alt="ava">
</a> </a>
{% else %} {% else %}
@ -51,7 +51,7 @@
<pre>{{ msg.text }}</pre> <pre>{{ msg.text }}</pre>
{% if msg.attachment %} {% 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 }} <span class="glyphicon glyphicon-gift"></span> {{ msg.attachment }}
</a> </a>
{% endif %} {% endif %}

3
msg_app/urls.py

@ -7,6 +7,5 @@ urlpatterns = [
path('', views.ConversationsListView.as_view(), name='home'), path('', views.ConversationsListView.as_view(), name='home'),
path('new/', views.new_conversation, name='new_conversation'), path('new/', views.new_conversation, name='new_conversation'),
path('<int:conv_id>/', views.to_conversation, name='to_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.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib import messages from django.contrib import messages
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.views.generic import ListView from django.views.generic import ListView
from chatbot.models import MessageQueue
from djing.lib.decorators import only_admins from djing.lib.decorators import only_admins
from guardian.decorators import permission_required_or_403 as permission_required 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 conversation_id = msg.conversation.pk
msg.delete() msg.delete()
return redirect('msg_app:to_conversation', conversation_id) 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() django.setup()
from django.utils import timezone from django.utils import timezone
from django.db import transaction 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.nas_managers import NasNetworkError, NasFailedResult
from gw_app.models import NASModel from gw_app.models import NASModel
from djing.lib import LogicError from djing.lib import LogicError
@ -35,7 +34,6 @@ class NasSyncThread(Thread):
def main(): def main():
signals.pre_delete.disconnect(abontariff_pre_delete, sender=AbonTariff)
AbonTariff.objects.filter(abon=None).delete() AbonTariff.objects.filter(abon=None).delete()
now = timezone.now() now = timezone.now()
fields = ('id', 'tariff__title', 'abon__id', 'abon__username') fields = ('id', 'tariff__title', 'abon__id', 'abon__username')
@ -79,7 +77,7 @@ def main():
# make log about it # make log about it
l = AbonLog.objects.create( l = AbonLog.objects.create(
abon=abon, amount=-amount, abon=abon, amount=-amount,
comment="Автоматическое продление услуги '%s'" % trf.title
comment="Автоматическое продление услуги '%s' для %s" % (trf.title, abon)
) )
print(l.comment) print(l.comment)
else: else:
@ -101,7 +99,8 @@ def main():
# connect service when autoconnect is True, and user have enough money # connect service when autoconnect is True, and user have enough money
for ab in Abon.objects.filter( for ab in Abon.objects.filter(
is_active=True, is_active=True,
current_tariff=None
current_tariff=None,
autoconnect_service=True
).exclude(last_connected_tariff=None).iterator(): ).exclude(last_connected_tariff=None).iterator():
try: try:
tariff = ab.last_connected_tariff tariff = ab.last_connected_tariff

21
requirements.txt

@ -1,13 +1,12 @@
urllib3 urllib3
Django>=2 Django>=2
Pillow Pillow
telepot
# for mac address field # for mac address field
netaddr netaddr
# for testing required xmltodict # for testing required xmltodict
xmltodict
#xmltodict
dicttoxml dicttoxml
# db client for Postgres # db client for Postgres
@ -18,11 +17,13 @@ pid
django-guardian django-guardian
pinax-theme-bootstrap pinax-theme-bootstrap
django-bootstrap3 django-bootstrap3
django-jsonfield
# django-jsonfield
-e git://github.com/dmkoch/django-jsonfield.git#egg=django-jsonfield
requests requests
webdavclient webdavclient
pyst2 pyst2
django-bitfield
transliterate transliterate
asterisk asterisk
django-encrypted-model-fields django-encrypted-model-fields
@ -30,6 +31,18 @@ django-encrypted-model-fields
# django-xmlview for pay system allpay # django-xmlview for pay system allpay
-e git://github.com/nerosketch/django-xmlview.git#egg=django-xmlview -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 Celery
redis==2.10.6 redis==2.10.6
celery[redis] celery[redis]

4
searchapp/templates/searchapp/index.html

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

15
static/bad_ie.html

@ -1,11 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html>
<html lang="ru">
<head> <head>
<title>Старый браузер</title>
<meta charset="UTF-8">
<title>Старый браузер</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body> <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> </body>
</html> </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