simo 2.3.6__py3-none-any.whl → 2.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of simo might be problematic. Click here for more details.

Files changed (34) hide show
  1. simo/__pycache__/settings.cpython-38.pyc +0 -0
  2. simo/backups/__pycache__/admin.cpython-38.pyc +0 -0
  3. simo/backups/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
  4. simo/backups/__pycache__/models.cpython-38.pyc +0 -0
  5. simo/backups/admin.py +71 -7
  6. simo/backups/dynamic_settings.py +0 -7
  7. simo/backups/migrations/0002_backuplog_backup_level_backup_size.py +32 -0
  8. simo/backups/migrations/__pycache__/0002_backuplog_backup_level_backup_size.cpython-38.pyc +0 -0
  9. simo/backups/models.py +14 -1
  10. simo/backups/tasks.py +102 -26
  11. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/context.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  18. simo/core/api.py +2 -7
  19. simo/core/controllers.py +8 -1
  20. simo/core/management/_hub_template/hub/supervisor.conf +4 -0
  21. simo/core/middleware.py +1 -1
  22. simo/core/models.py +6 -2
  23. simo/core/tasks.py +14 -1
  24. simo/core/views.py +4 -12
  25. simo/settings.py +1 -0
  26. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  27. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  28. simo/users/models.py +38 -16
  29. {simo-2.3.6.dist-info → simo-2.4.1.dist-info}/METADATA +2 -1
  30. {simo-2.3.6.dist-info → simo-2.4.1.dist-info}/RECORD +34 -32
  31. {simo-2.3.6.dist-info → simo-2.4.1.dist-info}/LICENSE.md +0 -0
  32. {simo-2.3.6.dist-info → simo-2.4.1.dist-info}/WHEEL +0 -0
  33. {simo-2.3.6.dist-info → simo-2.4.1.dist-info}/entry_points.txt +0 -0
  34. {simo-2.3.6.dist-info → simo-2.4.1.dist-info}/top_level.txt +0 -0
Binary file
simo/backups/admin.py CHANGED
@@ -1,13 +1,18 @@
1
1
  from django.contrib import admin
2
- from .models import Backup
2
+ from django.contrib import messages
3
+ from django.utils.timezone import localtime
4
+ from django_object_actions import DjangoObjectActions, action
5
+ from .models import Backup, BackupLog
3
6
 
4
7
 
5
8
  @admin.register(Backup)
6
- class BackupAdmin(admin.ModelAdmin):
7
- list_display = 'datetime', 'device', 'filepath'
8
- fields = 'datetime', 'device', 'filepath'
9
- readonly_fields = 'datetime', 'device', 'filepath'
10
- list_filter = 'datetime', 'mac',
9
+ class BackupAdmin(DjangoObjectActions, admin.ModelAdmin):
10
+ list_display = 'datetime', 'device', 'level', 'size_display'
11
+ fields = 'datetime', 'device', 'level', 'size_display', 'filepath'
12
+ readonly_fields = 'datetime', 'device', 'level', 'size_display', 'filepath'
13
+ list_filter = 'datetime', 'mac', 'level'
14
+ actions = 'restore',
15
+ changelist_actions = ('backup',)
11
16
 
12
17
  def has_change_permission(self, request, obj=None):
13
18
  return False
@@ -16,4 +21,63 @@ class BackupAdmin(admin.ModelAdmin):
16
21
  return False
17
22
 
18
23
  def has_delete_permission(self, request, obj=None):
19
- return False
24
+ return False
25
+
26
+ def size_display(self, obj):
27
+ if obj.size > 1024 * 1024 * 1024 * 1024:
28
+ return f'{round(obj.size / (1024 * 1024 * 1024), 2)} Gb'
29
+ elif obj.size > 1024 * 1024 * 1024:
30
+ return f'{round(obj.size / (1024 * 1024), 2)} Mb'
31
+ elif obj.size > 1024 * 1024:
32
+ return f'{round(obj.size / (1024 * 1024), 2)} Kb'
33
+ return f'{obj.size} bytes'
34
+ size_display.short_description = 'size'
35
+
36
+ def restore(self, request, queryset):
37
+ if queryset.count() > 1:
38
+ messages.add_message(
39
+ request, messages.ERROR,
40
+ "Please select one snapshot."
41
+ )
42
+ return
43
+ from simo.backups.tasks import restore_backup
44
+ backup = queryset.first()
45
+ restore_backup.delay(backup.id)
46
+ messages.add_message(
47
+ request, messages.WARNING,
48
+ f"Restore command initiated. "
49
+ f"If things go well, your hub will reboot in ~15 minutes to the "
50
+ f"state it was on {localtime(backup.datetime)}. "
51
+ )
52
+
53
+ @action(
54
+ label="Backup now!", # optional
55
+ description="Start backup now!" # optional
56
+ )
57
+ def backup(modeladmin, request, queryset=None):
58
+ from simo.backups.tasks import perform_backup
59
+ perform_backup.delay()
60
+ messages.add_message(
61
+ request, messages.INFO,
62
+ f"Backup command initiated. "
63
+ f"If things go well, you will see "
64
+ f"a new backup in here in less than 10 mins. "
65
+ f"Check backup logs for errors if not."
66
+ )
67
+
68
+
69
+ @admin.register(BackupLog)
70
+ class BackupLogAdmin(admin.ModelAdmin):
71
+ fields = 'datetime', 'level', 'msg'
72
+ readonly_fields = fields
73
+ list_fields = fields
74
+ list_filter = 'datetime', 'level'
75
+ search_fields = 'msg',
76
+
77
+ def has_delete_permission(self, request, obj=None):
78
+ return False
79
+
80
+ def has_add_permission(self, request):
81
+ return False
82
+
83
+
@@ -7,13 +7,6 @@ from dynamic_preferences.registries import global_preferences_registry
7
7
  backups = Section('backups')
8
8
 
9
9
 
10
- @global_preferences_registry.register
11
- class LastBackupError(StringPreference):
12
- section = backups
13
- name = 'last_error'
14
- default = ''
15
-
16
-
17
10
  @global_preferences_registry.register
18
11
  class LastBackupCheck(IntegerPreference):
19
12
  section = backups
@@ -0,0 +1,32 @@
1
+ # Generated by Django 4.2.10 on 2024-10-09 07:49
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('backups', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name='BackupLog',
15
+ fields=[
16
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17
+ ('datetime', models.DateTimeField(auto_now_add=True, db_index=True)),
18
+ ('level', models.CharField(choices=[('info', 'INFO'), ('warning', 'WARNING'), ('error', 'ERROR')], default='info')),
19
+ ('msg', models.TextField()),
20
+ ],
21
+ ),
22
+ migrations.AddField(
23
+ model_name='backup',
24
+ name='level',
25
+ field=models.IntegerField(db_index=True, default=0),
26
+ ),
27
+ migrations.AddField(
28
+ model_name='backup',
29
+ name='size',
30
+ field=models.IntegerField(default=0),
31
+ ),
32
+ ]
simo/backups/models.py CHANGED
@@ -6,6 +6,8 @@ class Backup(models.Model):
6
6
  datetime = models.DateTimeField(db_index=True)
7
7
  mac = models.CharField(max_length=100, db_index=True)
8
8
  filepath = models.CharField(max_length=200)
9
+ level = models.IntegerField(default=0, db_index=True)
10
+ size = models.IntegerField(default=0)
9
11
 
10
12
  class Meta:
11
13
  unique_together = 'datetime', 'mac'
@@ -15,4 +17,15 @@ class Backup(models.Model):
15
17
  def device(self):
16
18
  if self.mac == str(hex(uuid.getnode())):
17
19
  return "This machine"
18
- return self.mac
20
+ return self.mac
21
+
22
+
23
+ class BackupLog(models.Model):
24
+ datetime = models.DateTimeField(db_index=True, auto_now_add=True)
25
+ level = models.CharField(default='info', choices=(
26
+ ('info', "INFO"), ('warning', "WARNING"), ('error', "ERROR")
27
+ ))
28
+ msg = models.TextField()
29
+
30
+ class Meta:
31
+ ordering = 'datetime',
simo/backups/tasks.py CHANGED
@@ -2,6 +2,7 @@ import os, subprocess, json, uuid, datetime, shutil
2
2
  from datetime import datetime, timezone
3
3
  from celeryc import celery_app
4
4
  from simo.conf import dynamic_settings
5
+ from simo.core.utils.helpers import get_random_string
5
6
 
6
7
 
7
8
  @celery_app.task
@@ -9,7 +10,7 @@ def check_backups():
9
10
  '''
10
11
  syncs up backups on external medium to the database
11
12
  '''
12
- from .models import Backup
13
+ from simo.backups.models import Backup, BackupLog
13
14
 
14
15
  try:
15
16
  lv_group, lv_name, mountpoint = get_partitions()
@@ -35,38 +36,39 @@ def check_backups():
35
36
  continue
36
37
  for filename in os.listdir(os.path.join(hub_dir, month_folder)):
37
38
  try:
38
- day, time, back = filename.split('.')
39
+ day, time, level, back = filename.split('.')
39
40
  hour, minute, second = time.split('-')
40
- day, hour, minute, second = \
41
- int(day), int(hour), int(minute), int(second)
41
+ day, hour, minute, second, level = \
42
+ int(day), int(hour), int(minute), int(second), int(level)
42
43
  except:
43
44
  continue
44
45
 
46
+ filepath = os.path.join(hub_dir, month_folder, filename)
47
+ file_stats = os.stat(filepath)
45
48
  obj, new = Backup.objects.update_or_create(
46
49
  datetime=datetime(
47
50
  year, month, day, hour, minute, second,
48
51
  tzinfo=timezone.utc
49
52
  ), mac=hub_mac, defaults={
50
- 'filepath': os.path.join(
51
- hub_dir, month_folder, filename
52
- )
53
+ 'filepath': filepath, 'level': level,
54
+ 'size': file_stats.st_size
53
55
  }
54
56
  )
55
57
  backups_mentioned.append(obj.id)
56
58
 
57
59
  Backup.objects.all().exclude(id__in=backups_mentioned).delete()
58
60
 
59
- dynamic_settings['backups__last_check'] = datetime.now()
61
+ dynamic_settings['backups__last_check'] = int(datetime.now().timestamp())
60
62
 
61
63
 
62
64
  def create_snap(lv_group, lv_name):
63
65
  try:
64
66
  return subprocess.check_output(
65
- f'lvcreate -s -n {lv_name}-snap {lv_group}/{lv_name} -L 3G',
67
+ f'lvcreate -s -n {lv_name}-snap {lv_group}/{lv_name} -L 5G',
66
68
  shell=True
67
69
  ).decode()
68
70
  except:
69
- return ''
71
+ return
70
72
 
71
73
 
72
74
  def get_lvm_partition(lsblk_data):
@@ -93,6 +95,7 @@ def get_backup_device(lsblk_data):
93
95
 
94
96
 
95
97
  def get_partitions():
98
+ from simo.backups.models import BackupLog
96
99
 
97
100
  lsblk_data = json.loads(subprocess.check_output(
98
101
  'lsblk --output NAME,HOTPLUG,MOUNTPOINT,FSTYPE,TYPE,LABEL,PARTLABEL --json',
@@ -104,7 +107,9 @@ def get_partitions():
104
107
  lvm_partition = get_lvm_partition(lsblk_data)
105
108
  if not lvm_partition:
106
109
  print("No LVM partition!")
107
- dynamic_settings['backups__last_error'] = 'No LVM partition!'
110
+ BackupLog.objects.create(
111
+ level='warning', msg="Can't backup. No LVM partition!"
112
+ )
108
113
  return
109
114
 
110
115
  try:
@@ -114,14 +119,17 @@ def get_partitions():
114
119
  lv_name = name[split_at + 1:].replace('--', '-')
115
120
  except:
116
121
  print("Failed to identify LVM partition")
117
- dynamic_settings['backups__last_error'] = \
118
- 'Failed to identify LVM partition'
122
+ BackupLog.objects.create(
123
+ level='warning', msg="Can't backup. Failed to identify LVM partition."
124
+ )
119
125
  return
120
126
 
121
127
  if not lv_name:
122
128
  print("LVM was not found on this system. Abort!")
123
- dynamic_settings['backups__last_error'] = \
124
- 'Failed to identify LVM partition name'
129
+ BackupLog.objects.create(
130
+ level='warning',
131
+ msg="Can't backup. Failed to identify LVM partition name."
132
+ )
125
133
  return
126
134
 
127
135
 
@@ -130,8 +138,10 @@ def get_partitions():
130
138
  backup_device = get_backup_device(lsblk_data)
131
139
 
132
140
  if not backup_device:
133
- dynamic_settings['backups__last_error'] = \
134
- 'No external exFAT backup device on this machine'
141
+ BackupLog.objects.create(
142
+ level='warning',
143
+ msg="Can't backup. No external exFAT backup device on this machine."
144
+ )
135
145
  return
136
146
 
137
147
  if lvm_partition.get('partlabel'):
@@ -159,18 +169,21 @@ def get_partitions():
159
169
 
160
170
  @celery_app.task
161
171
  def perform_backup():
162
-
172
+ from simo.backups.models import BackupLog
163
173
  try:
164
174
  lv_group, lv_name, mountpoint = get_partitions()
165
175
  except:
166
176
  return
167
177
 
168
178
  output = create_snap(lv_group, lv_name)
169
- if not output:
170
- subprocess.check_output(
171
- f'lvremove -f {lv_group}/{lv_name}-snap',
172
- shell=True
173
- )
179
+ if not output or f'Logical volume "{lv_name}-snap" created' not in output:
180
+ try:
181
+ subprocess.check_output(
182
+ f'lvremove -f {lv_group}/{lv_name}-snap',
183
+ shell=True
184
+ )
185
+ except:
186
+ pass
174
187
  output = create_snap(lv_group, lv_name)
175
188
 
176
189
  if f'Logical volume "{lv_name}-snap" created' not in output:
@@ -183,7 +196,7 @@ def perform_backup():
183
196
  if not os.path.exists(device_backups_path):
184
197
  os.makedirs(device_backups_path)
185
198
 
186
- now = datetime.datetime.now()
199
+ now = datetime.now()
187
200
  level = now.day
188
201
  month_folder = os.path.join(
189
202
  device_backups_path, f'{now.year}-{now.month}'
@@ -213,7 +226,7 @@ def perform_backup():
213
226
  level = 0
214
227
 
215
228
  time_mark = now.strftime("%H-%M-%S")
216
- backup_file = f"{month_folder}/{now.day}.{time_mark}.back"
229
+ backup_file = f"{month_folder}/{now.day}.{time_mark}.{level}.back"
217
230
  snap_mapper = f"/dev/mapper/{lv_group}-{lv_name.replace('-', '--')}--snap"
218
231
  label = f"simo {now.strftime('%Y-%m-%d')}"
219
232
  dumpdates_file = os.path.join(month_folder, 'dumpdates')
@@ -253,6 +266,9 @@ def perform_backup():
253
266
  except:
254
267
  try:
255
268
  os.remove(backup_file)
269
+ BackupLog.objects.create(
270
+ level='error', msg="Can't backup. Dump failed."
271
+ )
256
272
  print("Dump failed!")
257
273
  except:
258
274
  pass
@@ -263,7 +279,67 @@ def perform_backup():
263
279
  )
264
280
  if success:
265
281
  print("DONE!")
266
- dynamic_settings['backups__last_error'] = ''
282
+ BackupLog.objects.create(
283
+ level='info', msg="Backup success!"
284
+ )
285
+
286
+
287
+ @celery_app.task
288
+ def restore_backup(backup_id):
289
+ from simo.backups.models import Backup, BackupLog
290
+ backup = Backup.objects.get(id=backup_id)
291
+
292
+ try:
293
+ lv_group, lv_name, mpt = get_partitions()
294
+ except:
295
+ BackupLog.objects.create(
296
+ level='error',
297
+ msg="Can't restore. LVM group is not present on this machine."
298
+ )
299
+ return
300
+
301
+ snap_name = f'{lv_name}-{get_random_string(5)}'
302
+ output = create_snap(lv_group, snap_name)
303
+ if not output or f'Logical volume "{lv_name}-snap" created' not in output:
304
+ BackupLog.objects.create(
305
+ level='error',
306
+ msg="Can't restore. Can't create LVM snapshot\n\n" + output
307
+ )
308
+ return
309
+
310
+ if not os.path.exists('/var/backups/simo-main'):
311
+ os.makedirs('/var/backups/simo-main')
312
+
313
+ subprocess.call('umount /var/backup/simo-main', shell=True)
314
+
315
+ subprocess.call('rm -rf /var/backup/simo-main/*', shell=True)
316
+
317
+ subprocess.call(
318
+ f"mount /dev/mapper/{lv_group}-{snap_name.replace('-', '--')} /var/backup/simo-main",
319
+ shell=True, stdout=subprocess.PIPE
320
+ )
321
+ try:
322
+ subprocess.call(
323
+ f"restore -C -v -b 1024 -f {backup.filepath} -D /var/backup/simo-main",
324
+ shell=True
325
+ )
326
+ subprocess.call(
327
+ f"lvconvert --mergesnapshot {lv_group}/{snap_name}",
328
+ shell=True
329
+ )
330
+ except Exception as e:
331
+ BackupLog.objects.create(
332
+ level='error',
333
+ msg="Can't restore. \n\n" + str(e)
334
+ )
335
+ subprocess.call('umount /var/backup/simo-main', shell=True)
336
+ subprocess.call(
337
+ f"lvremove -f {lv_group}/{snap_name}", shell=True,
338
+ stdout=subprocess.PIPE
339
+ )
340
+ else:
341
+ print("All good! REBOOT!")
342
+ subprocess.call('reboot')
267
343
 
268
344
 
269
345
  @celery_app.on_after_finalize.connect
Binary file
Binary file
Binary file
Binary file
simo/core/api.py CHANGED
@@ -262,13 +262,11 @@ class ComponentViewSet(
262
262
  def controller(self, request, pk=None, *args, **kwargs):
263
263
  start = time.time()
264
264
  component = self.get_object()
265
- print(f"Component retrieved in : {time.time() - start}s")
266
265
  data = request.data
267
266
  if not isinstance(request.data, dict):
268
267
  data = data.dict()
269
268
  request_data = restore_json(data)
270
269
  resp = self.perform_controller_method(request_data, component)
271
- print(f"Command executed in : {time.time() - start}s")
272
270
  return resp
273
271
 
274
272
  @action(detail=False, methods=['post'])
@@ -359,8 +357,6 @@ class ComponentHistoryViewSet(InstanceMixin, viewsets.ReadOnlyModelViewSet):
359
357
  minutes=start_from.minute, seconds=start_from.second
360
358
  )
361
359
 
362
- print("START FROM: ", start_from)
363
-
364
360
  return RESTResponse(
365
361
  self.get_aggregated_data(
366
362
  component, request.GET['interval'], start_from
@@ -645,9 +641,9 @@ class StatesViewSet(InstanceMixin, viewsets.GenericViewSet):
645
641
  component_values = get_components_queryset(
646
642
  self.instance, request.user
647
643
  ).filter(zone__instance=self.instance).values(
648
- 'id', 'value', 'last_change', 'last_modified',
644
+ 'id', 'last_change', 'last_modified',
649
645
  'arm_status', 'battery_level', 'alive', 'error_msg',
650
- 'meta'
646
+ 'value', 'meta'
651
647
  )
652
648
  for vals in component_values:
653
649
  vals['last_change'] = datetime.datetime.timestamp(
@@ -665,7 +661,6 @@ class StatesViewSet(InstanceMixin, viewsets.GenericViewSet):
665
661
  cat['last_modified']
666
662
  )
667
663
 
668
-
669
664
  return RESTResponse({
670
665
  'zones': Zone.objects.filter(instance=self.instance).values(
671
666
  'id', 'name'
simo/core/controllers.py CHANGED
@@ -539,6 +539,14 @@ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
539
539
  default_value = 0
540
540
  default_value_units = '%'
541
541
 
542
+ def _receive_from_device(self, value, *args, **kwargs):
543
+ if isinstance(value, bool):
544
+ if value:
545
+ value = self.component.config.get('max', 100.0)
546
+ else:
547
+ value = self.component.config.get('min', 0)
548
+ return super()._receive_from_device(value, *args, **kwargs)
549
+
542
550
  def _prepare_for_send(self, value):
543
551
  if isinstance(value, bool):
544
552
  if value:
@@ -579,7 +587,6 @@ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
579
587
  self.turn_on()
580
588
 
581
589
 
582
-
583
590
  class DimmerPlus(ControllerBase, TimerMixin, OnOffPokerMixin):
584
591
  name = _("Dimmer Plus")
585
592
  base_type = 'dimmer-plus'
@@ -10,6 +10,10 @@ stdout_logfile_backups=3
10
10
  redirect_stderr=true
11
11
  autostart=true
12
12
  autorestart=true
13
+ stopasgroup=true
14
+ killasgroup=true
15
+ stopsignal=INT
16
+
13
17
 
14
18
  # using daphne for socket connections routed to /ws/ on nginx.conf
15
19
  [program:simo-daphne]
simo/core/middleware.py CHANGED
@@ -58,7 +58,7 @@ def instance_middleware(get_response):
58
58
  def middleware(request):
59
59
 
60
60
  if request.path.startswith('/admin'):
61
- if not (request.user.is_authenticated and request.user.is_master):
61
+ if request.user.is_authenticated and not request.user.is_master:
62
62
  return render(request, 'admin/msg_page.html', {
63
63
  'page_title': "You are not allowed in here",
64
64
  'msg': "Page you are trying to access is only for hub masters.",
simo/core/models.py CHANGED
@@ -502,7 +502,6 @@ def is_in_alarm(self):
502
502
  action_type='comp_value', value=self.value
503
503
  )
504
504
  action_performed = True
505
- self.last_change = timezone.now()
506
505
  if 'arm_status' in dirty_fields:
507
506
  ComponentHistory.objects.create(
508
507
  component=self, type='security',
@@ -514,11 +513,16 @@ def is_in_alarm(self):
514
513
  action_type='security', value=self.value
515
514
  )
516
515
  action_performed = True
517
- self.last_change = timezone.now()
518
516
  if action_performed:
519
517
  actor.last_action = timezone.now()
520
518
  actor.save()
521
519
 
520
+ if any(
521
+ f in dirty_fields for f in
522
+ ['value', 'arm_status', 'battery_level', 'alive', 'meta']
523
+ ):
524
+ self.last_change = timezone.now()
525
+
522
526
  modifying_fields = (
523
527
  'name', 'icon', 'zone', 'category', 'config', 'meta',
524
528
  'value_units', 'slaves', 'show_in_app', 'alarm_category'
simo/core/tasks.py CHANGED
@@ -14,19 +14,26 @@ from django.template.loader import render_to_string
14
14
  from celeryc import celery_app
15
15
  from django.utils import timezone
16
16
  from actstream.models import Action
17
- from easy_thumbnails.files import get_thumbnailer
18
17
  from simo.conf import dynamic_settings
19
18
  from simo.core.utils.helpers import get_self_ip
20
19
  from simo.users.models import PermissionsRole, InstanceUser
21
20
  from .models import Instance, Component, ComponentHistory, HistoryAggregate
22
21
 
23
22
 
23
+ @celery_app.task
24
24
  def supervisor_restart():
25
25
  time.sleep(2)
26
26
  subprocess.run(['redis-cli', 'flushall'])
27
27
  subprocess.run(['supervisorctl', 'restart', 'all'])
28
28
 
29
29
 
30
+ @celery_app.task
31
+ def hardware_reboot():
32
+ time.sleep(2)
33
+ print("Reboot system")
34
+ subprocess.run(['reboot'])
35
+
36
+
30
37
  def save_config(data):
31
38
 
32
39
  vpn_change = False
@@ -92,6 +99,12 @@ def save_config(data):
92
99
  )
93
100
  except:
94
101
  pass
102
+ try:
103
+ subprocess.run(
104
+ ['service', 'openvpn', 'reload']
105
+ )
106
+ except:
107
+ pass
95
108
 
96
109
  if vpn_change:
97
110
  threading.Thread(target=restart_openvpn).start()
simo/core/views.py CHANGED
@@ -1,6 +1,4 @@
1
1
  import time
2
- import threading
3
- import subprocess
4
2
  import re
5
3
  from django.contrib.auth.decorators import login_required
6
4
  from django.urls import reverse
@@ -9,7 +7,7 @@ from django.http import HttpResponse, Http404, JsonResponse
9
7
  from django.contrib import messages
10
8
  from simo.conf import dynamic_settings
11
9
  from .models import Instance
12
- from .tasks import update as update_task, supervisor_restart
10
+ from .tasks import update as update_task, supervisor_restart, hardware_reboot
13
11
  from .middleware import introduce_instance
14
12
 
15
13
 
@@ -21,7 +19,7 @@ def update(request):
21
19
  if not request.user.is_superuser:
22
20
  raise Http404()
23
21
  messages.warning(request, "Hub update initiated. ")
24
- threading.Thread(target=update_task).start()
22
+ update_task.delay()
25
23
  if request.META.get('HTTP_REFERER'):
26
24
  return redirect(request.META.get('HTTP_REFERER'))
27
25
  return redirect(reverse('admin:index'))
@@ -35,7 +33,7 @@ def restart(request):
35
33
  request, "Hub restart initiated. "
36
34
  "Your hub will be out of operation for next few seconds."
37
35
  )
38
- threading.Thread(target=supervisor_restart).start()
36
+ supervisor_restart.delay()
39
37
  if request.META.get('HTTP_REFERER'):
40
38
  return redirect(request.META.get('HTTP_REFERER'))
41
39
  return redirect(reverse('admin:index'))
@@ -50,13 +48,7 @@ def reboot(request):
50
48
  request,
51
49
  "Hub reboot initiated. Hub will be out of reach for a minute or two."
52
50
  )
53
-
54
- def hardware_reboot():
55
- time.sleep(2)
56
- print("Reboot system")
57
- subprocess.run(['reboot'])
58
-
59
- threading.Thread(target=hardware_reboot).start()
51
+ hardware_reboot.delay()
60
52
  if request.META.get('HTTP_REFERER'):
61
53
  return redirect(request.META.get('HTTP_REFERER'))
62
54
  return redirect(reverse('admin:index'))
simo/settings.py CHANGED
@@ -65,6 +65,7 @@ INSTALLED_APPS = [
65
65
  'bootstrap4',
66
66
  'taggit',
67
67
  'actstream',
68
+ 'django_object_actions',
68
69
 
69
70
  'simo.core',
70
71
  'simo.users',
simo/users/models.py CHANGED
@@ -252,10 +252,15 @@ class User(AbstractBaseUser, SimoAdminMixin):
252
252
  '''Used by API serializer to get users role on a given instance.'''
253
253
  if not self._instance:
254
254
  return None
255
- for role in self.roles.all():
256
- if role.instance == self._instance:
257
- return role.id
258
- return None
255
+ cache_key = f'user-{self.id}_instance-{self._instance.id}-role-id'
256
+ cached_val = cache.get(cache_key, 'expired')
257
+ if cached_val == 'expired':
258
+ for role in self.roles.all().select_related('instance'):
259
+ if role.instance == self._instance:
260
+ cached_val = role.id
261
+ cache.set(cache_key, role.id, 20)
262
+ return cached_val
263
+ return cached_val
259
264
 
260
265
  @role_id.setter
261
266
  def role_id(self, id):
@@ -290,7 +295,6 @@ class User(AbstractBaseUser, SimoAdminMixin):
290
295
  )
291
296
  ])
292
297
  cache.set(cache_key, instances, 10)
293
- print("INSTANCES: ", instances)
294
298
  return instances
295
299
 
296
300
  @property
@@ -301,26 +305,44 @@ class User(AbstractBaseUser, SimoAdminMixin):
301
305
 
302
306
  @property
303
307
  def is_active(self):
304
- if self.is_master and not self.instance_roles.all():
305
- # Master who have no roles on any instance are in GOD mode!
306
- # It can not be disabled by anybody, nor it is seen by anybody. :)
307
- return True
308
- if self._instance:
309
- return bool(
310
- self.instance_roles.filter(
311
- instance=self._instance, is_active=True
312
- ).first()
313
- )
314
- return any([ir.is_active for ir in self.instance_roles.all()])
308
+ if not self._instance:
309
+ cache_key = f'user-{self.id}_is_active'
310
+ else:
311
+ cache_key = f'user-{self.id}_is_active_instance-{self._instance.id}'
312
+ cached_value = cache.get(cache_key, 'expired')
313
+ if cached_value == 'expired':
314
+ if self.is_master and not self.instance_roles.all():
315
+ # Master who have no roles on any instance are in GOD mode!
316
+ # It can not be disabled by anybody, nor it is seen by anybody. :)
317
+ cached_value = True
318
+ elif self._instance:
319
+ cached_value = bool(
320
+ self.instance_roles.filter(
321
+ instance=self._instance, is_active=True
322
+ ).first()
323
+ )
324
+ else:
325
+ cached_value = any(
326
+ [ir.is_active for ir in self.instance_roles.all()]
327
+ )
328
+ cache.set(cache_key, cached_value, 20)
329
+ return cached_value
315
330
 
316
331
 
317
332
  @is_active.setter
318
333
  def is_active(self, val):
319
334
  if not self._instance:
320
335
  return
336
+
321
337
  self.instance_roles.filter(
322
338
  instance=self._instance
323
339
  ).update(is_active=bool(val))
340
+ cache_key = f'user-{self.id}_is_active_instance-{self._instance.id}'
341
+ try:
342
+ cache.delete(cache_key)
343
+ except:
344
+ pass
345
+
324
346
  rebuild_authorized_keys()
325
347
 
326
348
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.3.6
3
+ Version: 2.4.1
4
4
  Summary: Smart Home on Steroids!
5
5
  Author-email: Simanas Venčkauskas <simanas@simo.io>
6
6
  Project-URL: Homepage, https://simo.io
@@ -49,4 +49,5 @@ Requires-Dist: django-markdownify ==0.9.5
49
49
  Requires-Dist: django-activity-stream ==2.0.0
50
50
  Requires-Dist: gunicorn ==23.0.0
51
51
  Requires-Dist: python-crontab ==3.2.0
52
+ Requires-Dist: django-object-actions ==4.3.0
52
53
 
@@ -3,31 +3,33 @@ simo/asgi.py,sha256=L8CUVZLM32IMzWDZ4IShdDN-m69t7oxAUeHods4-xNM,822
3
3
  simo/celeryc.py,sha256=eab7_e9rw0c__DCeoUFUh_tjAGVlulxVrk75BaJf57Q,1512
4
4
  simo/conf.py,sha256=H2BhXAV8MEDVXF8AbkaLSfR4ULd-9_bS4bnhE5sE5fg,112
5
5
  simo/scripting.py,sha256=PVIkGsiMDWj4CNTbOM3rq7pJ6ruavuns-ZMU7VudLa4,923
6
- simo/settings.py,sha256=bwri9NuM9wjCacmUxzB1joy79Bu64tO0jEBZlJUeakc,6933
6
+ simo/settings.py,sha256=BcirAAKau1KiJ5qKTD0ni8hTk3jHAASBcUCBNZPGNCk,6962
7
7
  simo/urls.py,sha256=fRmAsNQ_pzFloimLmxNeDcR6hHRJ3rOoZ3kGy8zOQ_A,2402
8
8
  simo/__pycache__/__init__.cpython-38.pyc,sha256=j81de0BqHMr6bs0C7cuYrXl7HwtK_vv8hDEtAdSwDJc,153
9
9
  simo/__pycache__/asgi.cpython-38.pyc,sha256=5W_YSKOIrRd6NQQuJDuA3Yuj688GzirXVVOyLe8wJIQ,845
10
10
  simo/__pycache__/celeryc.cpython-38.pyc,sha256=eSRoaKwfYlxVaxAiwqpQ2ndEcx7W-VpZtbxRFSV8UYg,1653
11
11
  simo/__pycache__/conf.cpython-38.pyc,sha256=MYP2yk3ULxiYwZsZR6tCLjKnU-z03A3avzQzIn66y3k,273
12
- simo/__pycache__/settings.cpython-38.pyc,sha256=NdG2wtrRDI5YO1CfMzJuSGRARls5OnNJs7_XpLxGOQI,6083
12
+ simo/__pycache__/settings.cpython-38.pyc,sha256=Mi9iweMgAZ16ppKmiK0eFSDYsrY6XRHFpXXHK8is-Pc,6110
13
13
  simo/__pycache__/urls.cpython-38.pyc,sha256=u0x6EqT8S1YfDOSPgbI8Kf-RDlveY9OV-EDXMYKAQ7w,2125
14
14
  simo/__pycache__/wsgi.cpython-38.pyc,sha256=TpRxO7VM_ql31hbKphVdanydC5RI1nHB4l0QA2pdWxo,322
15
15
  simo/backups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- simo/backups/admin.py,sha256=doPMTahbMtWHBpYDW2xh3tz2bkb6KqJXfH2cAeHUalM,533
17
- simo/backups/dynamic_settings.py,sha256=MrXECAxnpQjBf8yWug6qusSAyxRCtzBuQxfhFX8uV-c,560
18
- simo/backups/models.py,sha256=wjXvb8uXWGk_iqmbMBnX8owp5AYpj1WS0KteW4a0Fbk,463
19
- simo/backups/tasks.py,sha256=GueIJLZazeUZTfupe24k1m2IiflceSE610bL6KTXuAo,8496
16
+ simo/backups/admin.py,sha256=2079VRAih91_F4k2yIi8068Po2QtTTfEqI2r0yQM7Zs,2797
17
+ simo/backups/dynamic_settings.py,sha256=Q52RLa3UQsmAhqkwR16cM6pbBnIbXqmVQ2oIUP2ZVD0,416
18
+ simo/backups/models.py,sha256=XNaGprjSS3XKHMu36PZj11kjT4AtO0R5LDWfRGY7H-k,875
19
+ simo/backups/tasks.py,sha256=7A5sJtY-ratqf9QSv4wuhe66uQijW6YVRw_1n5579eA,11041
20
20
  simo/backups/__pycache__/__init__.cpython-38.pyc,sha256=vzOf-JIMeZ6P85FyvTpYev3mscFosUy-SJTshcQbOHU,161
21
- simo/backups/__pycache__/admin.cpython-38.pyc,sha256=yT_AJOT6ypJl2iCQqphioMw2h0qm7N8IxDzEVF9RJWw,1027
22
- simo/backups/__pycache__/dynamic_settings.cpython-38.pyc,sha256=Ko0le9M96_zv4jv4zUsGUZTjEzNFQxr7NNjEwp2FRqU,900
23
- simo/backups/__pycache__/models.cpython-38.pyc,sha256=ifXK00chQNJZQL9QohBMTwH343Kt_CqVWUkuUQjnUX4,922
21
+ simo/backups/__pycache__/admin.cpython-38.pyc,sha256=eUSpoTgvrQ2Dfq87G6ljvij9KUKZ6jibSuPko0-7jW8,3172
22
+ simo/backups/__pycache__/dynamic_settings.cpython-38.pyc,sha256=51gJFjn_XqQBRoHeubo6ppb9pNuFQKI5hAR0ms9flE8,731
23
+ simo/backups/__pycache__/models.cpython-38.pyc,sha256=1c0gqn5zP9OfQfqDLXbXej7v1n5mBMFjWDewxrPPHEg,1476
24
24
  simo/backups/migrations/0001_initial.py,sha256=0LzCusTUyYf61ksiepdnqXIuYYNTNd_djh_Wa484HA0,770
25
+ simo/backups/migrations/0002_backuplog_backup_level_backup_size.py,sha256=w9T9MQWuecy91OZE1fMExriwPuXA8HMPKsPwXhmC8_k,1023
25
26
  simo/backups/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
27
  simo/backups/migrations/__pycache__/0001_initial.cpython-38.pyc,sha256=jumm0xjwRhiYPOAXi4o-AGpcRi-GVOj5bSJkOTYtT7w,834
28
+ simo/backups/migrations/__pycache__/0002_backuplog_backup_level_backup_size.cpython-38.pyc,sha256=moD04sv6Fc6crxZwTQHMtJ18iFG2O_zrqbMBzskPhgw,1024
27
29
  simo/backups/migrations/__pycache__/__init__.cpython-38.pyc,sha256=Lz1fs6V05h2AoxTOLNye0do9bEMnyuaXB_hHOjG5-HU,172
28
30
  simo/core/__init__.py,sha256=_s2TjJfQImsMrTIxqLAx9AZie1Ojmm6sCHASdl3WLGU,50
29
31
  simo/core/admin.py,sha256=hoJ0OhfWL9T0d6JCY6_Gm3GR-nrgtDR2UQM67rMsjiE,18141
30
- simo/core/api.py,sha256=laWtqQUL0ngjTtdDGqcUH42WYzWd9WYJwcKL3yCsDk4,27952
32
+ simo/core/api.py,sha256=BU-Lkt7m5sj5nzCqCi6uDU__xzBU97MwugL7FghI-es,27775
31
33
  simo/core/api_auth.py,sha256=vCxvczA8aWNcW0VyKs5WlC_ytlqeGP_H_hkKUNVkCwM,1247
32
34
  simo/core/api_meta.py,sha256=EaiY-dCADP__9MvLpoHvhjytFT92IrxPZDv95xgqasU,4955
33
35
  simo/core/app_widgets.py,sha256=4Lh9FDzdkfh_mccJMe09dyRTT3Uqf9VXwbkurJ9E9oQ,2115
@@ -36,7 +38,7 @@ simo/core/auto_urls.py,sha256=nNXEgLAAAQAhRWQDA9AbDtw-zcPKmu_pufJaSa8g818,1102
36
38
  simo/core/autocomplete_views.py,sha256=JT5LA2_Wtr60XYSAIqaXFKFYPjrmkEf6yunXD9y2zco,4022
37
39
  simo/core/base_types.py,sha256=qVh6MrXZEfN7bFOyFftC7u0yyz0PkvpsjllLBc6SCp4,616
38
40
  simo/core/context.py,sha256=snfPIGcZQTrx8iiZc5PI91A0dRQH6y5kH4uG_lfhU6Q,1486
39
- simo/core/controllers.py,sha256=rLgJqnEMzRC0GpZnQb0m_Cz43Gp2zaYbODU7UUxE5oA,29602
41
+ simo/core/controllers.py,sha256=KB-nzDR60x5EPwtfraIRE_CoXQXpnvjqcLFw6nqcOtQ,29930
40
42
  simo/core/dynamic_settings.py,sha256=bUs58XEZOCIEhg1TigR3LmYggli13KMryBZ9pC7ugAQ,1872
41
43
  simo/core/events.py,sha256=LvtonJGNyCb6HLozs4EG0WZItnDwNdtnGQ4vTcnKvUs,4438
42
44
  simo/core/filters.py,sha256=ghtOZcrwNAkIyF5_G9Sn73NkiI71mXv0NhwCk4IyMIM,411
@@ -45,22 +47,22 @@ simo/core/forms.py,sha256=sFVz8tENhi4QSudORu_e5tlGEC0aXumy6wzJqQNYBPk,21904
45
47
  simo/core/gateways.py,sha256=m0eS3XjVe34Dge6xtoCq16kFWCKJcdQrT0JW0REqoq8,3715
46
48
  simo/core/loggers.py,sha256=EBdq23gTQScVfQVH-xeP90-wII2DQFDjoROAW6ggUP4,1645
47
49
  simo/core/managers.py,sha256=n-b3I4uXzfHKTeB1VMjSaMsDUxp8FegFJwnbV1IsWQ4,3019
48
- simo/core/middleware.py,sha256=Kh5tOkPs3WCOd3XF3PZ3XdYjNi_OVRLpMq9T_xiabgc,2732
49
- simo/core/models.py,sha256=hQA8SeAPtGRSs4gk-bci2Ni3ZANtS62V-IDgCyJh2qE,21563
50
+ simo/core/middleware.py,sha256=1xKsaN0Ro5Fb2slB9FnlQdlDEdO4_kUGdnhA8rccBsA,2730
51
+ simo/core/models.py,sha256=5UwGbe2UYj15yM-P6QCIO1HErEdzpNF3nHJnInBGdpM,21666
50
52
  simo/core/permissions.py,sha256=v0iJM4LOeYoEfMiw3OLPYio272G1aUEAg_z9Wd1q5m0,2993
51
53
  simo/core/routing.py,sha256=X1_IHxyA-_Q7hw1udDoviVP4_FSBDl8GYETTC2zWTbY,499
52
54
  simo/core/serializers.py,sha256=1hcCnLnItw8T3jLWivKBjSAt7kwqjcOYE0vaepw55Z0,20966
53
55
  simo/core/signal_receivers.py,sha256=9-qFCCeSLcMFEMg6QUtKOVgUsoNoqhzGoI98nuNSTEo,6228
54
56
  simo/core/socket_consumers.py,sha256=n7VE2Fvqt4iEAYLTRbTPOcI-7tszMAADu7gimBxB-Fg,9635
55
57
  simo/core/storage.py,sha256=_5igjaoWZAiExGWFEJMElxUw55DzJG1jqFty33xe8BE,342
56
- simo/core/tasks.py,sha256=772H1Rvn1tRE0owdCMFNzd_ql_PFFST_TRFl3E3GMOs,14211
58
+ simo/core/tasks.py,sha256=-swS6mJIJhAbdgKgPkGwYNn03-E9MKM_SO18mI7sml0,14433
57
59
  simo/core/todos.py,sha256=eYVXfLGiapkxKK57XuviSNe3WsUYyIWZ0hgQJk7ThKo,665
58
60
  simo/core/types.py,sha256=WJEq48mIbFi_5Alt4wxWMGXxNxUTXqfQU5koH7wqHHI,1108
59
- simo/core/views.py,sha256=VVqfEPzK0EdbVMMarkG8rd7cODG5QHpXnr3e8UdrTQE,2600
61
+ simo/core/views.py,sha256=3SRZr00fyLQf8ja3U-9eekKt-ld5TvU1WQqUWprXfQ4,2390
60
62
  simo/core/widgets.py,sha256=J9e06C6I22F6xKic3VMgG7WeX07glAcl-4bF2Mg180A,2827
61
63
  simo/core/__pycache__/__init__.cpython-38.pyc,sha256=ZJFM_XN0RmJMULQulgA_wFiOnEtsMoedcOWnXjH-Y8o,208
62
64
  simo/core/__pycache__/admin.cpython-38.pyc,sha256=c5Q8YfIiLk25yVCxQvNsH-lwuOwfQa88X6xDYjOBogU,13428
63
- simo/core/__pycache__/api.cpython-38.pyc,sha256=Fs0SFPiz8rA0zUUDGEGnY6EAZuPuvwtvx9yByf6O3uQ,21838
65
+ simo/core/__pycache__/api.cpython-38.pyc,sha256=w4uvlv0sZtY-xlVJ4UNLC60EyP8fRFGqUwKNc9XcrrE,21694
64
66
  simo/core/__pycache__/api_auth.cpython-38.pyc,sha256=6M9Cl_ha4y_Vf8Rv4GMYL8dcBCmp0KzYi6jn3SQTgys,1712
65
67
  simo/core/__pycache__/api_meta.cpython-38.pyc,sha256=VYx5ZeDyNBI4B_CBEIhV5B3GnLsMOx9s3rNZTSMODco,3703
66
68
  simo/core/__pycache__/app_widgets.cpython-38.pyc,sha256=vUCEAYqppjgRZYMs6pTuSxWWuZxreLygPuPBGw044dQ,3643
@@ -68,8 +70,8 @@ simo/core/__pycache__/apps.cpython-38.pyc,sha256=JL0BEqgXcSQvMlcK48PBpPfyDEkPMdO
68
70
  simo/core/__pycache__/auto_urls.cpython-38.pyc,sha256=Tyf8PYHq5YqSwTp25Joy-eura_Fm86fpX9zKLSklhvo,872
69
71
  simo/core/__pycache__/autocomplete_views.cpython-38.pyc,sha256=hJ6JILI1LqrAtpQMvxnLvljGdW1v1gpvBsD79vFkZ58,3972
70
72
  simo/core/__pycache__/base_types.cpython-38.pyc,sha256=hmq22vvGyCmhbYyuV6bFAOOSIupspgW5yq_VzqWd-vY,759
71
- simo/core/__pycache__/context.cpython-38.pyc,sha256=EbjtX2vOdl7-NA02zEwp_iUMKI2SSs-NZ-sHryo0GGY,1278
72
- simo/core/__pycache__/controllers.cpython-38.pyc,sha256=LA5Bnzev4kS3S8Ta0nfaVHQMIPfUDPYDL1GvfgHtIWg,26584
73
+ simo/core/__pycache__/context.cpython-38.pyc,sha256=ck1FcBljLB4__5F6poS2tEEn8IDDgK7pU3FcXDPc_mI,1329
74
+ simo/core/__pycache__/controllers.cpython-38.pyc,sha256=EOs8vHYOjzV9-zrOLr6FS49fywErGkegPteuKt2FqGU,26860
73
75
  simo/core/__pycache__/dynamic_settings.cpython-38.pyc,sha256=wGpnscX1DxFpRl54MQURhjz2aD3NJohSzw9JCFnzh2Y,2384
74
76
  simo/core/__pycache__/events.cpython-38.pyc,sha256=A1Axx-qftd1r7st7wkO3DkvTdt9-RkcJe5KJhpzJVk8,5109
75
77
  simo/core/__pycache__/filters.cpython-38.pyc,sha256=VIMADCBiYhziIyRmxAyUDJluZvuZmiC4bNYWTRsGSao,721
@@ -78,17 +80,17 @@ simo/core/__pycache__/forms.cpython-38.pyc,sha256=JDCNYJQrZHUaMb4qZ6A0f1JptoCHQK
78
80
  simo/core/__pycache__/gateways.cpython-38.pyc,sha256=D1ooHL-iSpQrxnD8uAl4xWFJmm-QWZfbkLiLlFOMtdU,4553
79
81
  simo/core/__pycache__/loggers.cpython-38.pyc,sha256=Z-cdQnC6XlIonPV4Sl4E52tP4NMEdPAiHK0cFaIL7I8,1623
80
82
  simo/core/__pycache__/managers.cpython-38.pyc,sha256=6RTIxyjOgpQGtAqcUyE2vFPS09w1V5Wmd_vOV7rHRRI,3370
81
- simo/core/__pycache__/middleware.cpython-38.pyc,sha256=C9v8kFb4BvD8TCvbSGpRRYH0xtoOM3nId2VaId5jmiA,2408
82
- simo/core/__pycache__/models.cpython-38.pyc,sha256=h_neN4zzReOTuou4NX9RbgVJ8T47z0tLqSr37Fm1iHU,17874
83
+ simo/core/__pycache__/middleware.cpython-38.pyc,sha256=OH2wvHHavuS4YYCgyvJd4GTrTCEV577CQ-ZBrTFuy6Y,2408
84
+ simo/core/__pycache__/models.cpython-38.pyc,sha256=h8vUx9VV721RwNmMMjKc8OY3qva4h-Ydc7Gr60un_tM,18017
83
85
  simo/core/__pycache__/permissions.cpython-38.pyc,sha256=fH4iyqd9DdzRLEu2b621-FeM-napR0M7hzBUTHo9Q3g,2972
84
86
  simo/core/__pycache__/routing.cpython-38.pyc,sha256=3T3FPJ8Cn99xZCGvMyg2xjl7al-Shm9CelbSpkJtNP8,599
85
87
  simo/core/__pycache__/serializers.cpython-38.pyc,sha256=xsGMcx9ISHmvY_PzWqosUdkWRBNqYKOtVkFDYNEHc1A,19479
86
88
  simo/core/__pycache__/signal_receivers.cpython-38.pyc,sha256=3Bt9S47DR_ZFS3O-crElFgLLXPIYyDgPIc2ibwEkaic,4904
87
89
  simo/core/__pycache__/socket_consumers.cpython-38.pyc,sha256=NJUr7nRyHFvmAumxxWpsod5wzVVZM99rCEuJs1utHA4,8432
88
90
  simo/core/__pycache__/storage.cpython-38.pyc,sha256=9R1Xu0FJDflfRXUPsqEgt0SpwiP7FGk7HaR8s8XRyI8,721
89
- simo/core/__pycache__/tasks.cpython-38.pyc,sha256=0L_HDgv8LCTX3zQSniEF-uH1pdr1tMxa3yCeZEMFvhs,9773
91
+ simo/core/__pycache__/tasks.cpython-38.pyc,sha256=t2WCeMMfyXe9pdJVU-QP6g8pOP1iXYljjQl20AF2Kew,9968
90
92
  simo/core/__pycache__/todos.cpython-38.pyc,sha256=lOqGZ58siHM3isoJV4r7sg8igrfE9fFd-jSfeBa0AQI,253
91
- simo/core/__pycache__/views.cpython-38.pyc,sha256=D7X_fFxYySe-mKNB4bEnIt9ubYM7CbbRcIpX52Ou4RE,2809
93
+ simo/core/__pycache__/views.cpython-38.pyc,sha256=ySH8l-clhUdpkIVm9aMQOlt1VoZoJgNrqoE_sEiYMR8,2495
92
94
  simo/core/__pycache__/widgets.cpython-38.pyc,sha256=sR0ZeHCHrhnNDBJuRrxp3zUsfBp0xrtF0xrK2TkQv1o,3520
93
95
  simo/core/db_backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
96
  simo/core/db_backend/base.py,sha256=wY5jsZ8hQpwot3o-7JNtDe33xy-vlfMLXa011htDojI,601
@@ -147,7 +149,7 @@ simo/core/management/_hub_template/hub/celeryc.py,sha256=3ksDXftIZKJ4Cq9WNKJERdZ
147
149
  simo/core/management/_hub_template/hub/manage.py,sha256=PNNlw3EVeIJDgkG0l-klqoxsKWfTYWG9jzRG0upmAaI,620
148
150
  simo/core/management/_hub_template/hub/nginx.conf,sha256=40hvXL42MeiqqkLURNcDQsRudv1dNFLJnvb2-Y3RCkk,2394
149
151
  simo/core/management/_hub_template/hub/settings.py,sha256=4QhvhbtLRxHvAntwqG_qeAAtpDUqKvN4jzw9u3vqff8,361
150
- simo/core/management/_hub_template/hub/supervisor.conf,sha256=tl1nW95DrVwcGxgLXAOgIn5ow1p_-LRNNE1bCEoq00k,1888
152
+ simo/core/management/_hub_template/hub/supervisor.conf,sha256=MOsMo8QjBKUl5EUWG4GrA6BjLc4s7pU17xAKY2uGjZU,1938
151
153
  simo/core/management/_hub_template/hub/urls.py,sha256=Ydm-1BkYAzWeEF-MKSDIFf-7aE4qNLPm48-SA51XgJQ,25
152
154
  simo/core/management/_hub_template/hub/wsgi.py,sha256=Lo-huLHnMDTxSmMBOodVFMWBls9poddrV2KRzXU0xGo,280
153
155
  simo/core/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -10416,7 +10418,7 @@ simo/users/auto_urls.py,sha256=lcJvteBsbHQMJieZpDz-63tDYejLApqsW3CUnDakd7k,272
10416
10418
  simo/users/dynamic_settings.py,sha256=sEIsi4yJw3kH46Jq_aOkSuK7QTfQACGUE-lkyBogCaM,570
10417
10419
  simo/users/managers.py,sha256=M_51bk9z4jn8e2Ci3pJfIqbf6cRNqfQNSOAg0vPl6Vo,175
10418
10420
  simo/users/middleware.py,sha256=GMCrnWSc_2qCleyQIkfQGdL-pU-UTEcSg1wPvIKZ9uk,1210
10419
- simo/users/models.py,sha256=0KGZZjXdzNHPRuS6uYo8PE3DBZDw6N9TU3zxiLpCQxs,19370
10421
+ simo/users/models.py,sha256=mYZHclBNGLUWqFQpUyplncXQrsPYjmlxZ0Jl6Fn_Dhk,20278
10420
10422
  simo/users/permissions.py,sha256=IwtYS8yQdupWbYKR9VimSRDV3qCJ2jXP57Lyjpb2EQM,242
10421
10423
  simo/users/serializers.py,sha256=a4R408ZgWVbF7OFw4bBfN33Wnn8ljqS8iFcsqmllkWU,2552
10422
10424
  simo/users/sso_urls.py,sha256=gQOaPvGMYFD0NCVSwyoWO-mTEHe5j9sbzV_RK7kdvp0,251
@@ -10433,9 +10435,9 @@ simo/users/__pycache__/auto_urls.cpython-38.pyc,sha256=K-3sz2h-cEitoflSmZk1t0eUg
10433
10435
  simo/users/__pycache__/dynamic_settings.cpython-38.pyc,sha256=6F8JBjZkHykySnmZjNEzjS0ijbmPdcp9yUAZ5kqq_Fo,864
10434
10436
  simo/users/__pycache__/managers.cpython-38.pyc,sha256=C5-diljm874RAFMTkZdcfzPhkHzlUGPAhz2gTvqkDy8,604
10435
10437
  simo/users/__pycache__/middleware.cpython-38.pyc,sha256=Tj4nVEAvxEW3xA63fBRiJWRJpz_M848ZOqbHioc_IPE,1149
10436
- simo/users/__pycache__/models.cpython-38.pyc,sha256=MMCLklK0yVxhSsl2m8Z2xnAmnxSSCpFJcnXzx5TdQQM,18096
10438
+ simo/users/__pycache__/models.cpython-38.pyc,sha256=tOiUcbFxH2AKUT8X8lygDYxETjpAUR7C8vD7gqB1Nz8,18523
10437
10439
  simo/users/__pycache__/permissions.cpython-38.pyc,sha256=ez5NxoL_JUeeH6GsKhvFreuA3FCBgGf9floSypdXUtM,633
10438
- simo/users/__pycache__/serializers.cpython-38.pyc,sha256=PuMy6H0PhEhq89RFmdnFH4pMHB0N3w7opJEFS90JUCY,3477
10440
+ simo/users/__pycache__/serializers.cpython-38.pyc,sha256=AkMwibZBo52rsWOwhAIc9ig1Ck1-fQAEkboe6wnh7yE,3477
10439
10441
  simo/users/__pycache__/sso_urls.cpython-38.pyc,sha256=uAwDozpOmrhUald-8tOHANILXkH7-TI8fNYXOtPkSY8,402
10440
10442
  simo/users/__pycache__/sso_views.cpython-38.pyc,sha256=sHEoxLOac3U3Epmhm197huFnW_J3gGCDZSji57itijU,3969
10441
10443
  simo/users/__pycache__/tasks.cpython-38.pyc,sha256=XLMKt3suT7BlcXrJZoH9ZIhhtBuqyiW4lsOB9IbBkko,1225
@@ -10517,9 +10519,9 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
10517
10519
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10518
10520
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10519
10521
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10520
- simo-2.3.6.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10521
- simo-2.3.6.dist-info/METADATA,sha256=a9X6k-TUA5MHLUmXQiCure-2esr9ZNNvVOsAyBamzsM,1878
10522
- simo-2.3.6.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
10523
- simo-2.3.6.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
10524
- simo-2.3.6.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10525
- simo-2.3.6.dist-info/RECORD,,
10522
+ simo-2.4.1.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10523
+ simo-2.4.1.dist-info/METADATA,sha256=dkUhhxK0C6qOP_J2jRgkD2ltsJnLcWq2ZTsXtSRIeHg,1923
10524
+ simo-2.4.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
10525
+ simo-2.4.1.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
10526
+ simo-2.4.1.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10527
+ simo-2.4.1.dist-info/RECORD,,
File without changes