simo 2.4.2__py3-none-any.whl → 2.5.2__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 (73) hide show
  1. simo/backups/tasks.py +11 -1
  2. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  4. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/events.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  14. simo/core/admin.py +4 -4
  15. simo/core/api.py +20 -4
  16. simo/core/app_widgets.py +5 -0
  17. simo/core/controllers.py +4 -3
  18. simo/core/events.py +13 -4
  19. simo/core/forms.py +2 -0
  20. simo/core/management/commands/gateways_manager.py +0 -3
  21. simo/core/middleware.py +12 -6
  22. simo/core/models.py +26 -6
  23. simo/core/serializers.py +17 -17
  24. simo/core/socket_consumers.py +6 -2
  25. simo/core/static/admin/js/codemirror-init.js +1 -0
  26. simo/core/tasks.py +10 -7
  27. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  28. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  29. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  30. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  31. simo/fleet/controllers.py +86 -22
  32. simo/fleet/forms.py +84 -9
  33. simo/fleet/migrations/0039_auto_20241016_1047.py +28 -0
  34. simo/fleet/migrations/0040_alter_colonel_pwm_frequency.py +18 -0
  35. simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-38.pyc +0 -0
  36. simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-38.pyc +0 -0
  37. simo/fleet/models.py +6 -2
  38. simo/fleet/socket_consumers.py +13 -5
  39. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  40. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  41. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  42. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  43. simo/generic/controllers.py +45 -2
  44. simo/generic/forms.py +81 -7
  45. simo/generic/models.py +0 -1
  46. simo/generic/scripting/__init__.py +16 -0
  47. simo/generic/scripting/__pycache__/__init__.cpython-38.pyc +0 -0
  48. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  49. simo/generic/scripting/__pycache__/serializers.cpython-38.pyc +0 -0
  50. simo/generic/scripting/helpers.py +35 -0
  51. simo/generic/scripting/serializers.py +77 -0
  52. simo/generic/templates/admin/controller_widgets/weather_forecast.html +2 -2
  53. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  54. simo/notifications/utils.py +30 -12
  55. simo/scripting.py +2 -2
  56. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  57. simo/users/__pycache__/managers.cpython-38.pyc +0 -0
  58. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  59. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  60. simo/users/__pycache__/utils.cpython-38.pyc +0 -0
  61. simo/users/api.py +36 -7
  62. simo/users/managers.py +5 -1
  63. simo/users/migrations/0034_instanceuser_last_seen_location_and_more.py +24 -0
  64. simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-38.pyc +0 -0
  65. simo/users/models.py +37 -32
  66. simo/users/serializers.py +11 -8
  67. simo/users/utils.py +14 -3
  68. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/METADATA +1 -1
  69. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/RECORD +73 -60
  70. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/WHEEL +1 -1
  71. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/LICENSE.md +0 -0
  72. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/entry_points.txt +0 -0
  73. {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,27 @@
1
- from simo.users.models import User
1
+ from simo.core.middleware import get_current_instance
2
2
  from .models import Notification, UserNotification
3
3
 
4
4
 
5
- def notify_users(instance, severity, title, body=None, component=None, users=None):
5
+ def notify_users(severity, title, body=None, component=None, instance_users=None, instance=None):
6
+ '''
7
+ Sends a notification to specified users with a given severity level and message details.
8
+ :param severity: One of: 'info', 'warning', 'alarm'
9
+ :param title: A short, descriptive title of the event.
10
+ :param body: (Optional) A more detailed description of the event.
11
+ :param component: (Optional) simo.core.Component linked to this event.
12
+ :param instance_users: List of instance users to receive this notification. All active instance users will receive the message if not specified.
13
+ :return:
14
+ '''
15
+ if not instance:
16
+ if component:
17
+ instance = component.zone.instance
18
+ else:
19
+ instance = get_current_instance()
20
+ if not instance:
21
+ return
22
+ if component and component.zone.instance != instance:
23
+ # something is completely wrong!
24
+ return
6
25
  assert severity in ('info', 'warning', 'alarm')
7
26
  notification = Notification.objects.create(
8
27
  instance=instance,
@@ -10,20 +29,19 @@ def notify_users(instance, severity, title, body=None, component=None, users=Non
10
29
  severity=severity, body=body,
11
30
  component=component
12
31
  )
13
- if not users:
14
- users = User.objects.filter(
15
- instance_roles__instance=instance,
16
- instance_roles__is_active=True
17
- )
18
- for user in users:
32
+ if not instance_users:
33
+ instance_users = instance.instance_users.filter(
34
+ is_active=True
35
+ ).select_related('user')
36
+ for iuser in instance_users:
19
37
  # do not send emails to system users
20
- if user.email.endswith('simo.io'):
38
+ if iuser.user.email.endswith('simo.io'):
21
39
  continue
22
- if instance not in user.instances:
40
+ if iuser.instance.id != instance.id:
23
41
  continue
24
- if component and not component.can_write(user):
42
+ if component and not component.can_read(iuser.user):
25
43
  continue
26
44
  UserNotification.objects.create(
27
- user=user, notification=notification,
45
+ user=iuser.user, notification=notification,
28
46
  )
29
47
  notification.dispatch()
simo/scripting.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from django.utils import timezone
2
2
  from suntime import Sun
3
- from simo.core.models import Instance
3
+ from simo.core.middleware import get_current_instance
4
4
 
5
5
 
6
6
  class LocalSun(Sun):
7
7
 
8
8
  def __init__(self, instance=None):
9
9
  if not instance:
10
- instance = Instance.objects.all().first()
10
+ instance = get_current_instance()
11
11
  coordinates = instance.location.split(',')
12
12
  try:
13
13
  lat = float(coordinates[0])
Binary file
Binary file
simo/users/api.py CHANGED
@@ -4,17 +4,18 @@ from rest_framework import viewsets, mixins, status
4
4
  from rest_framework.serializers import Serializer
5
5
  from rest_framework.decorators import action
6
6
  from rest_framework.response import Response as RESTResponse
7
- from rest_framework.exceptions import ValidationError
7
+ from rest_framework.exceptions import ValidationError, PermissionDenied
8
8
  from django.contrib.gis.geos import Point
9
9
  from django.utils import timezone
10
+ from django_filters.rest_framework import DjangoFilterBackend
10
11
  from simo.core.api import InstanceMixin
11
12
  from .models import (
12
13
  User, UserDevice, UserDeviceReportLog, PermissionsRole, InstanceInvitation,
13
- Fingerprint
14
+ Fingerprint, ComponentPermission, InstanceUser
14
15
  )
15
16
  from .serializers import (
16
17
  UserSerializer, PermissionsRoleSerializer, InstanceInvitationSerializer,
17
- FingerprintSerializer
18
+ FingerprintSerializer, ComponentPermissionSerializer
18
19
  )
19
20
 
20
21
 
@@ -63,9 +64,6 @@ class UsersViewSet(mixins.RetrieveModelMixin,
63
64
  request.data.pop(key)
64
65
 
65
66
 
66
- target_user.set_instance(self.instance)
67
-
68
-
69
67
  serializer = self.get_serializer(
70
68
  target_user, data=request.data, partial=partial
71
69
  )
@@ -135,12 +133,38 @@ class RolesViewsets(InstanceMixin, viewsets.ReadOnlyModelViewSet):
135
133
  url = 'users/roles'
136
134
  basename = 'roles'
137
135
  serializer_class = PermissionsRoleSerializer
138
- queryset = PermissionsRole.objects.all()
139
136
 
140
137
  def get_queryset(self):
141
138
  return PermissionsRole.objects.filter(instance=self.instance)
142
139
 
143
140
 
141
+ class ComponentPermissionViewsets(
142
+ InstanceMixin,
143
+ mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
144
+ mixins.ListModelMixin, viewsets.GenericViewSet
145
+ ):
146
+ url = 'users/componentpermissions'
147
+ basename = 'componentpermissions'
148
+ serializer_class = ComponentPermissionSerializer
149
+ filter_backends = [DjangoFilterBackend]
150
+ filterset_fields = ['component', 'role']
151
+
152
+ def get_queryset(self):
153
+ return ComponentPermission.objects.filter(role__instance=self.instance)
154
+
155
+ def update(self, request, *args, **kwargs):
156
+ if request.user.is_master:
157
+ return super().update(request, *args, **kwargs)
158
+ iuser = InstanceUser.objects.get(
159
+ instance=self.instance, user=request.user
160
+ ).select_related('role')
161
+ if not iuser.is_active:
162
+ raise PermissionDenied()
163
+ if iuser.role.is_owner or iuser.role.is_superuser:
164
+ return super().update(request, *args, **kwargs)
165
+ raise PermissionDenied()
166
+
167
+
144
168
  class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
145
169
  url = 'users'
146
170
  basename = 'device_report'
@@ -188,6 +212,11 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
188
212
  ).exclude(id=user_device.id).update(is_primary=False)
189
213
  user_device.save()
190
214
 
215
+ for iu in request.user.instance_roles.filter(is_active=True):
216
+ iu.last_seen_location = user_device.last_seen_location
217
+ iu.last_seen_location_datetime = user_device.last_seen
218
+ iu.save()
219
+
191
220
  request.user.last_seen_location = user_device.last_seen_location
192
221
  request.user.last_seen_location_datetime = user_device.last_seen
193
222
  request.user.save()
simo/users/managers.py CHANGED
@@ -1,7 +1,11 @@
1
1
  from django.db import models
2
+ from simo.core.middleware import get_current_instance
2
3
 
3
4
 
4
5
  class ActiveInstanceManager(models.Manager):
5
6
 
6
7
  def get_queryset(self):
7
- return super().get_queryset().filter(instance__is_active=True)
8
+ instance = get_current_instance()
9
+ return super().get_queryset().filter(
10
+ instance__is_active=True, instance=instance
11
+ )
@@ -0,0 +1,24 @@
1
+ # Generated by Django 4.2.10 on 2024-10-18 08:00
2
+
3
+ from django.db import migrations, models
4
+ import location_field.models.plain
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('users', '0033_alter_user_ssh_key'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='instanceuser',
16
+ name='last_seen_location',
17
+ field=location_field.models.plain.PlainLocationField(blank=True, help_text='Sent by user mobile app', max_length=63, null=True),
18
+ ),
19
+ migrations.AddField(
20
+ model_name='instanceuser',
21
+ name='last_seen_location_datetime',
22
+ field=models.DateTimeField(blank=True, null=True),
23
+ ),
24
+ ]
simo/users/models.py CHANGED
@@ -22,6 +22,7 @@ from simo.conf import dynamic_settings
22
22
  from simo.core.utils.mixins import SimoAdminMixin
23
23
  from simo.core.utils.helpers import get_random_string
24
24
  from simo.core.events import OnChangeMixin
25
+ from simo.core.middleware import get_current_instance
25
26
  from .middleware import get_current_user
26
27
  from .utils import rebuild_authorized_keys
27
28
  from .managers import ActiveInstanceManager
@@ -98,6 +99,12 @@ class InstanceUser(DirtyFieldsMixin, models.Model, OnChangeMixin):
98
99
  role = models.ForeignKey(PermissionsRole, on_delete=models.CASCADE)
99
100
  at_home = models.BooleanField(default=False, db_index=True)
100
101
  is_active = models.BooleanField(default=True, db_index=True)
102
+ last_seen_location = PlainLocationField(
103
+ zoom=7, null=True, blank=True, help_text="Sent by user mobile app"
104
+ )
105
+ last_seen_location_datetime = models.DateTimeField(
106
+ null=True, blank=True,
107
+ )
101
108
 
102
109
  objects = ActiveInstanceManager()
103
110
 
@@ -123,17 +130,18 @@ def post_instance_user_save(sender, instance, created, **kwargs):
123
130
  return
124
131
  from simo.core.events import ObjectChangeEvent
125
132
  dirty_fields = instance.get_dirty_fields()
126
- if 'at_home' in dirty_fields:
133
+ if 'at_home' in dirty_fields or 'last_seen_location' in dirty_fields:
127
134
  def post_update():
128
- if instance.at_home:
129
- verb = 'came home'
130
- else:
131
- verb = 'left'
132
- action.send(
133
- instance.user, verb=verb,
134
- instance_id=instance.instance.id,
135
- action_type='user_presence', value=instance.at_home
136
- )
135
+ if 'at_home' in dirty_fields:
136
+ if instance.at_home:
137
+ verb = 'came home'
138
+ else:
139
+ verb = 'left'
140
+ action.send(
141
+ instance.user, verb=verb,
142
+ instance_id=instance.instance.id,
143
+ action_type='user_presence', value=instance.at_home
144
+ )
137
145
  ObjectChangeEvent(
138
146
  instance.instance, instance, dirty_fields=dirty_fields
139
147
  ).publish()
@@ -182,8 +190,6 @@ class User(AbstractBaseUser, SimoAdminMixin):
182
190
  USERNAME_FIELD = 'email'
183
191
  REQUIRED_FIELDS = ['name']
184
192
 
185
- _instance = None
186
-
187
193
  class Meta:
188
194
  verbose_name = _('user')
189
195
  verbose_name_plural = _('users')
@@ -206,12 +212,10 @@ class User(AbstractBaseUser, SimoAdminMixin):
206
212
  obj = super().save(*args, **kwargs)
207
213
 
208
214
  if org:
209
- if org.can_ssh() != self.can_ssh():
215
+ if org.can_ssh() != self.can_ssh() or org.ssh_key != self.ssh_key:
210
216
  rebuild_authorized_keys()
211
217
  elif self.can_ssh():
212
218
  rebuild_authorized_keys()
213
- elif org and org.ssh_key != self.ssh_key:
214
- rebuild_authorized_keys()
215
219
 
216
220
  if not org or (org.secret_key != self.secret_key):
217
221
  self.update_mqtt_secret()
@@ -230,7 +234,7 @@ class User(AbstractBaseUser, SimoAdminMixin):
230
234
  )
231
235
 
232
236
  def can_ssh(self):
233
- return self.is_active and self.ssh_key and self.is_master
237
+ return self.is_active and self.is_master
234
238
 
235
239
  def get_role(self, instance):
236
240
  cache_key = f'user-{self.id}_instance-{instance.id}_role'
@@ -244,19 +248,17 @@ class User(AbstractBaseUser, SimoAdminMixin):
244
248
  cache.set(cache_key, role, 20)
245
249
  return role
246
250
 
247
- def set_instance(self, instance):
248
- self._instance = instance
249
-
250
251
  @property
251
252
  def role_id(self):
252
253
  '''Used by API serializer to get users role on a given instance.'''
253
- if not self._instance:
254
+ instance = get_current_instance()
255
+ if not instance:
254
256
  return None
255
- cache_key = f'user-{self.id}_instance-{self._instance.id}-role-id'
257
+ cache_key = f'user-{self.id}_instance-{instance.id}-role-id'
256
258
  cached_val = cache.get(cache_key, 'expired')
257
259
  if cached_val == 'expired':
258
260
  for role in self.roles.all().select_related('instance'):
259
- if role.instance == self._instance:
261
+ if role.instance == instance:
260
262
  cached_val = role.id
261
263
  cache.set(cache_key, role.id, 20)
262
264
  return cached_val
@@ -264,16 +266,17 @@ class User(AbstractBaseUser, SimoAdminMixin):
264
266
 
265
267
  @role_id.setter
266
268
  def role_id(self, id):
267
- if not self._instance:
269
+ instance = get_current_instance()
270
+ if not instance:
268
271
  return
269
272
  role = PermissionsRole.objects.filter(
270
- id=id, instance=self._instance
273
+ id=id, instance=instance
271
274
  ).first()
272
275
  if not role:
273
276
  raise ValueError("There is no such a role on this instance")
274
277
 
275
278
  InstanceUser.objects.update_or_create(
276
- user=self, instance=self._instance, defaults={
279
+ user=self, instance=instance, defaults={
277
280
  'role': role
278
281
  }
279
282
  )
@@ -305,20 +308,21 @@ class User(AbstractBaseUser, SimoAdminMixin):
305
308
 
306
309
  @property
307
310
  def is_active(self):
308
- if not self._instance:
311
+ instance = get_current_instance()
312
+ if not instance:
309
313
  cache_key = f'user-{self.id}_is_active'
310
314
  else:
311
- cache_key = f'user-{self.id}_is_active_instance-{self._instance.id}'
315
+ cache_key = f'user-{self.id}_is_active_instance-{instance.id}'
312
316
  cached_value = cache.get(cache_key, 'expired')
313
317
  if cached_value == 'expired':
314
318
  if self.is_master and not self.instance_roles.all():
315
319
  # Master who have no roles on any instance are in GOD mode!
316
320
  # It can not be disabled by anybody, nor it is seen by anybody. :)
317
321
  cached_value = True
318
- elif self._instance:
322
+ elif instance:
319
323
  cached_value = bool(
320
324
  self.instance_roles.filter(
321
- instance=self._instance, is_active=True
325
+ instance=instance, is_active=True
322
326
  ).first()
323
327
  )
324
328
  else:
@@ -331,13 +335,14 @@ class User(AbstractBaseUser, SimoAdminMixin):
331
335
 
332
336
  @is_active.setter
333
337
  def is_active(self, val):
334
- if not self._instance:
338
+ instance = get_current_instance()
339
+ if not instance:
335
340
  return
336
341
 
337
342
  self.instance_roles.filter(
338
- instance=self._instance
343
+ instance=instance
339
344
  ).update(is_active=bool(val))
340
- cache_key = f'user-{self.id}_is_active_instance-{self._instance.id}'
345
+ cache_key = f'user-{self.id}_is_active_instance-{instance.id}'
341
346
  try:
342
347
  cache.delete(cache_key)
343
348
  except:
simo/users/serializers.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from rest_framework import serializers
2
2
  from collections.abc import Iterable
3
- from simo.core.middleware import get_current_request
3
+ from simo.core.middleware import get_current_request, get_current_instance
4
4
  from simo.core.utils.api import ReadWriteSerializerMethodField
5
5
  from .models import (
6
- User, PermissionsRole, InstanceInvitation, InstanceUser, Fingerprint
6
+ User, PermissionsRole, ComponentPermission,
7
+ InstanceInvitation, InstanceUser, Fingerprint
7
8
  )
8
9
 
9
10
 
@@ -15,11 +16,6 @@ class UserSerializer(serializers.ModelSerializer):
15
16
 
16
17
  def __init__(self, *args, **kwargs):
17
18
  super().__init__(*args, **kwargs)
18
- if isinstance(self.instance, Iterable):
19
- for inst in self.instance:
20
- inst.set_instance(self.context['instance'])
21
- elif self.instance:
22
- self.instance.set_instance(self.context['instance'])
23
19
 
24
20
  class Meta:
25
21
  model = User
@@ -53,7 +49,7 @@ class UserSerializer(serializers.ModelSerializer):
53
49
 
54
50
  def get_at_home(self, obj):
55
51
  iu = InstanceUser.objects.filter(
56
- user=obj, instance=obj._instance
52
+ user=obj, instance=get_current_instance()
57
53
  ).first()
58
54
  if iu:
59
55
  return iu.at_home
@@ -68,6 +64,13 @@ class PermissionsRoleSerializer(serializers.ModelSerializer):
68
64
  fields = '__all__'
69
65
 
70
66
 
67
+ class ComponentPermissionSerializer(serializers.ModelSerializer):
68
+
69
+ class Meta:
70
+ model = ComponentPermission
71
+ fields = '__all__'
72
+
73
+
71
74
  class InstanceInvitationSerializer(serializers.ModelSerializer):
72
75
 
73
76
  class Meta:
simo/users/utils.py CHANGED
@@ -29,10 +29,21 @@ def rebuild_authorized_keys():
29
29
  try:
30
30
  with open('/root/.ssh/authorized_keys', 'w') as keys_file:
31
31
  for user in User.objects.filter(
32
- ssh_key__isnull=False
32
+ ssh_key__isnull=False, is_master=True
33
33
  ):
34
- if user.is_active and user.is_master:
35
- keys_file.write(user.ssh_key + '\n')
34
+ has_roles = user.instance_roles.filter(
35
+ instance__is_active=True
36
+ ).first()
37
+ has_active_roles = user.instance_roles.filter(
38
+ instance__is_active=True, is_active=True
39
+ ).first()
40
+ # if master user has active roles on some instances
41
+ # but no longer has a single active role on an instance
42
+ # he is most probably has been disabled by the property owner
43
+ # therefore he should no longer be able to ssh in to this hub!
44
+ if has_roles and not has_active_roles:
45
+ continue
46
+ keys_file.write(user.ssh_key + '\n')
36
47
  except:
37
48
  print(traceback.format_exc(), file=sys.stderr)
38
49
  pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.4.2
3
+ Version: 2.5.2
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