simo 2.0.24__py3-none-any.whl → 2.0.26__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 (37) hide show
  1. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  2. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  4. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  9. simo/core/admin.py +1 -0
  10. simo/core/api.py +35 -20
  11. simo/core/controllers.py +5 -1
  12. simo/core/forms.py +29 -1
  13. simo/core/middleware.py +1 -0
  14. simo/core/migrations/0032_auto_20240506_0834.py +24 -0
  15. simo/core/migrations/__pycache__/0032_auto_20240506_0834.cpython-38.pyc +0 -0
  16. simo/core/models.py +4 -2
  17. simo/core/permissions.py +9 -3
  18. simo/core/serializers.py +20 -3
  19. simo/core/utils/__pycache__/json.cpython-38.pyc +0 -0
  20. simo/core/utils/json.py +20 -0
  21. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  22. simo/fleet/forms.py +123 -174
  23. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  24. simo/generic/forms.py +24 -25
  25. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  26. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  27. simo/users/admin.py +0 -5
  28. simo/users/migrations/0027_permissionsrole_can_manage_components.py +18 -0
  29. simo/users/migrations/0028_auto_20240506_1146.py +22 -0
  30. simo/users/migrations/__pycache__/0027_permissionsrole_can_manage_components.cpython-38.pyc +0 -0
  31. simo/users/migrations/__pycache__/0028_auto_20240506_1146.cpython-38.pyc +0 -0
  32. simo/users/models.py +7 -6
  33. {simo-2.0.24.dist-info → simo-2.0.26.dist-info}/METADATA +1 -1
  34. {simo-2.0.24.dist-info → simo-2.0.26.dist-info}/RECORD +37 -29
  35. {simo-2.0.24.dist-info → simo-2.0.26.dist-info}/LICENSE.md +0 -0
  36. {simo-2.0.24.dist-info → simo-2.0.26.dist-info}/WHEEL +0 -0
  37. {simo-2.0.24.dist-info → simo-2.0.26.dist-info}/top_level.txt +0 -0
Binary file
Binary file
Binary file
Binary file
simo/core/admin.py CHANGED
@@ -60,6 +60,7 @@ class IconAdmin(admin.ModelAdmin):
60
60
  @admin.register(Instance)
61
61
  class InstanceAdmin(admin.ModelAdmin):
62
62
  list_display = 'name', 'timezone', 'uid'
63
+ exclude = 'learn_fingerprints_start', 'learn_fingerprints'
63
64
 
64
65
 
65
66
  def has_module_permission(self, request):
simo/core/api.py CHANGED
@@ -2,11 +2,13 @@ import datetime
2
2
  from calendar import monthrange
3
3
  import pytz
4
4
  import time
5
- from django.db.models import Q, Prefetch
5
+ import json
6
+ from django.db.models import Q
6
7
  from django.utils.translation import gettext_lazy as _
7
8
  from django.utils import timezone
8
9
  from django.http import HttpResponse, Http404
9
10
  from simo.core.utils.helpers import get_self_ip, search_queryset
11
+ from rest_framework import status
10
12
  from rest_framework.pagination import PageNumberPagination
11
13
  from rest_framework import viewsets
12
14
  from django_filters.rest_framework import DjangoFilterBackend
@@ -14,6 +16,7 @@ from rest_framework.decorators import action
14
16
  from rest_framework.response import Response as RESTResponse
15
17
  from rest_framework.exceptions import ValidationError as APIValidationError
16
18
  from simo.core.utils.config_values import ConfigException
19
+ from simo.core.utils.json import restore_json
17
20
  from .models import (
18
21
  Instance, Category, Zone, Component, Icon, ComponentHistory,
19
22
  HistoryAggregate, Gateway
@@ -35,11 +38,7 @@ class InstanceMixin:
35
38
  slug=self.request.resolver_match.kwargs.get('instance_slug')
36
39
  )
37
40
  except Instance.DoesNotExist:
38
- return HttpResponse(
39
- f"Instance {self.request.resolver_match.kwargs.get('instance_slug')} "
40
- "is not found on this SIMO.io hub!",
41
- status=400
42
- )
41
+ raise Http404()
43
42
  return super().dispatch(request, *args, **kwargs)
44
43
 
45
44
  def get_serializer_context(self):
@@ -159,7 +158,9 @@ def get_components_queryset(instance, user):
159
158
  return qs
160
159
 
161
160
 
162
- class ComponentViewSet(InstanceMixin, viewsets.ModelViewSet):
161
+ class ComponentViewSet(
162
+ InstanceMixin, viewsets.ModelViewSet
163
+ ):
163
164
  url = 'core/components'
164
165
  basename = 'components'
165
166
  serializer_class = ComponentSerializer
@@ -185,11 +186,14 @@ class ComponentViewSet(InstanceMixin, viewsets.ModelViewSet):
185
186
 
186
187
  def perform_controller_method(self, json_data, component):
187
188
  for method_name, param in json_data.items():
189
+ if method_name in ('id', 'secret'):
190
+ continue
188
191
  if not hasattr(component, method_name):
189
192
  raise APIValidationError(
190
193
  _('"%s" method not found on controller') % method_name,
191
194
  code=400
192
195
  )
196
+ print("VARYSIM: ", method_name, param, type(param))
193
197
  call = getattr(component, method_name)
194
198
 
195
199
  if not isinstance(param, list) and not isinstance(param, dict):
@@ -210,10 +214,13 @@ class ComponentViewSet(InstanceMixin, viewsets.ModelViewSet):
210
214
  return RESTResponse(result)
211
215
 
212
216
  # TODO: remove post when app is updated for all users
213
- @action(detail=True, methods=['post', 'put'])
217
+ @action(detail=True, methods=['post'])
214
218
  def subcomponent(self, request, pk=None, *args, **kwargs):
215
219
  component = self.get_object()
216
- json_data = request.data
220
+ data = request.data
221
+ if not isinstance(request.data, dict):
222
+ data = data.dict()
223
+ json_data = restore_json(data)
217
224
  subcomponent_id = json_data.pop('id', -1)
218
225
  try:
219
226
  subcomponent = component.slaves.get(pk=subcomponent_id)
@@ -230,18 +237,28 @@ class ComponentViewSet(InstanceMixin, viewsets.ModelViewSet):
230
237
  self.check_object_permissions(self.request, subcomponent)
231
238
  return self.perform_controller_method(json_data, subcomponent)
232
239
 
233
- @action(detail=True, methods=['post', 'put'])
240
+ @action(detail=True, methods=['post'])
234
241
  def controller(self, request, pk=None, *args, **kwargs):
235
242
  component = self.get_object()
236
- return self.perform_controller_method(request.data, component)
243
+ data = request.data
244
+ if not isinstance(request.data, dict):
245
+ data = data.dict()
246
+ request_data = restore_json(data)
247
+ return self.perform_controller_method(
248
+ restore_json(request_data), component
249
+ )
237
250
 
238
- @action(detail=False, methods=['post', 'put'])
251
+ @action(detail=False, methods=['post'])
239
252
  def control(self, request, *args, **kwargs):
240
- component = self.get_queryset().filter(id=request.data.pop('id', 0))
253
+ data = request.data
254
+ if not isinstance(request.data, dict):
255
+ data = data.dict()
256
+ request_data = restore_json(data)
257
+ component = self.get_queryset().filter(id=request_data.pop('id', 0)).first()
241
258
  if not component:
242
259
  raise Http404()
243
260
  self.check_object_permissions(self.request, component)
244
- return self.perform_controller_method(request.data, component)
261
+ return self.perform_controller_method(request_data, component)
245
262
 
246
263
  @action(detail=True, methods=['get'])
247
264
  def value_history(self, request, pk=None, *args, **kwargs):
@@ -255,6 +272,8 @@ class ComponentViewSet(InstanceMixin, viewsets.ModelViewSet):
255
272
  return RESTResponse(resp_data)
256
273
 
257
274
 
275
+
276
+
258
277
  class HistoryResultsSetPagination(PageNumberPagination):
259
278
  page_size = 50
260
279
  page_size_query_param = 'page_size'
@@ -532,9 +551,7 @@ class StatesViewSet(InstanceMixin, viewsets.GenericViewSet):
532
551
  ).exclude(email__in=('system@simo.io', 'device@simo.io'))
533
552
  component_values = get_components_queryset(
534
553
  self.instance, request.user
535
- ).filter(zone__instance=self.instance).prefetch_related(
536
- Prefetch('history', queryset=ComponentHistory.objects.filter())
537
- ).values(
554
+ ).filter(zone__instance=self.instance).values(
538
555
  'id', 'value', 'last_change', 'arm_status', 'battery_level',
539
556
  'alive', 'meta'
540
557
  )
@@ -623,6 +640,4 @@ class RunningDiscoveries(InstanceMixin, viewsets.GenericViewSet):
623
640
  if 'controller_uid' in request.GET:
624
641
  for gateway in gateways:
625
642
  gateway.retry_discovery()
626
- return RESTResponse(self.get_data(gateways))
627
-
628
-
643
+ return RESTResponse(self.get_data(gateways))
simo/core/controllers.py CHANGED
@@ -187,6 +187,8 @@ class ControllerBase(ABC):
187
187
  bulk_send_map = {self.component: value}
188
188
  for slave in self.component.slaves.all():
189
189
  bulk_send_map[slave] = value
190
+
191
+ print("BULK SEND MAP: ", bulk_send_map)
190
192
  from .models import Component
191
193
  Component.objects.bulk_send(bulk_send_map)
192
194
  return
@@ -652,7 +654,9 @@ class MultiSwitchBase(ControllerBase):
652
654
  if not(0 < number_of_values < 16):
653
655
  raise ValidationError("Wrong number of values")
654
656
  if number_of_values == 1:
655
- if not isinstance(value, bool):
657
+ if isinstance(value, int):
658
+ value = bool(value)
659
+ elif not isinstance(value, bool):
656
660
  raise ValidationError("Must be a boolean value")
657
661
  else:
658
662
  if not isinstance(value, list):
simo/core/forms.py CHANGED
@@ -195,6 +195,9 @@ class ConfigFieldsMixin:
195
195
 
196
196
  def save(self, commit=True):
197
197
  for field_name in self.config_fields:
198
+ # support for partial forms
199
+ if field_name not in self.cleaned_data:
200
+ continue
198
201
  if isinstance(self.cleaned_data[field_name], models.Model):
199
202
  self.instance.config[field_name] = \
200
203
  self.cleaned_data[field_name].pk
@@ -282,6 +285,10 @@ class ComponentAdminForm(forms.ModelForm):
282
285
  has_icon = True
283
286
  has_alarm = True
284
287
 
288
+ # fields that can be edited via SIMO.io app by instance owners.
289
+ # Users who have is_owner enabled on their user role.
290
+ basic_fields = ['name', 'icon', 'zone', 'category']
291
+
285
292
  class Meta:
286
293
  model = Component
287
294
  fields = '__all__'
@@ -324,6 +331,27 @@ class ComponentAdminForm(forms.ModelForm):
324
331
  self.instance.config = self.controller.default_config
325
332
  self.instance.meta = self.controller.default_meta
326
333
 
334
+ self.cleanup_missing_keys(kwargs.get("data"))
335
+
336
+ def cleanup_missing_keys(self, data):
337
+ """
338
+ Removes missing keys from fields on form submission.
339
+ This avoids resetting fields that are not present in
340
+ the submitted data, which may be the sign of a buggy
341
+ or incomplete template.
342
+ Note that this cleanup relies on the HTML form being
343
+ patched to send all keys, even for checkboxes, via
344
+ input[type="hidden"] fields or some JS magic.
345
+ """
346
+ if data is None:
347
+ # not a form submission, don't modify self.fields
348
+ return
349
+
350
+ got_keys = data.keys()
351
+ field_names = self.fields.keys()
352
+ for missing in set(field_names) - set(got_keys):
353
+ del self.fields[missing]
354
+
327
355
  @classmethod
328
356
  def get_admin_fieldsets(cls, request, obj=None):
329
357
  main_fields = (
@@ -518,7 +546,7 @@ class SwitchForm(BaseComponentForm):
518
546
 
519
547
  def save(self, commit=True):
520
548
  obj = super().save(commit=commit)
521
- if commit:
549
+ if commit and 'slaves' in self.cleaned_data:
522
550
  obj.slaves.set(self.cleaned_data['slaves'])
523
551
  return obj
524
552
 
simo/core/middleware.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import pytz
2
2
  import threading
3
+ import time
3
4
  from django.urls import set_script_prefix
4
5
  from django.utils import timezone
5
6
  from simo.conf import dynamic_settings
@@ -0,0 +1,24 @@
1
+ # Generated by Django 3.2.9 on 2024-05-06 08:34
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('core', '0031_auto_20240429_1231'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='category',
16
+ name='order',
17
+ field=models.PositiveIntegerField(db_index=True),
18
+ ),
19
+ migrations.AlterField(
20
+ model_name='instance',
21
+ name='indoor_climate_sensor',
22
+ field=models.ForeignKey(blank=True, limit_choices_to={'base_type__in': ['numeric-sensor', 'multi-sensor']}, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.component'),
23
+ ),
24
+ ]
simo/core/models.py CHANGED
@@ -80,7 +80,7 @@ class Instance(DirtyFieldsMixin, models.Model, SimoAdminMixin):
80
80
  name = models.CharField(max_length=100, db_index=True, unique=True)
81
81
  slug = models.CharField(max_length=100, db_index=True, unique=True)
82
82
  cover_image = models.ImageField(
83
- upload_to='hub_covers', null=True, blank=True
83
+ name='cover_image', upload_to='hub_covers', null=True, blank=True
84
84
  )
85
85
  cover_image_synced = models.BooleanField(default=False)
86
86
  secret_key = models.CharField(max_length=100, blank=True)
@@ -100,7 +100,8 @@ class Instance(DirtyFieldsMixin, models.Model, SimoAdminMixin):
100
100
 
101
101
  )
102
102
  indoor_climate_sensor = models.ForeignKey(
103
- 'Component', null=True, blank=True, on_delete=models.SET_NULL
103
+ 'Component', null=True, blank=True, on_delete=models.SET_NULL,
104
+ limit_choices_to={'base_type__in': ['numeric-sensor', 'multi-sensor']}
104
105
  )
105
106
  history_days = models.PositiveIntegerField(
106
107
  default=90, help_text="How many days of component history do we keep?"
@@ -128,6 +129,7 @@ class Instance(DirtyFieldsMixin, models.Model, SimoAdminMixin):
128
129
  def components(self):
129
130
  return Component.objects.filter(zone__instance=self)
130
131
 
132
+
131
133
  class Zone(DirtyFieldsMixin, models.Model, SimoAdminMixin):
132
134
  instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
133
135
  name = models.CharField(_('name'), max_length=40)
simo/core/permissions.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from rest_framework.permissions import BasePermission, SAFE_METHODS, IsAuthenticated
2
+ from django.http import Http404
2
3
  from .models import Instance, Category, Zone
3
4
 
4
5
 
@@ -13,7 +14,7 @@ class InstancePermission(BasePermission):
13
14
  slug=request.resolver_match.kwargs.get('instance_slug')
14
15
  ).first()
15
16
  if not instance:
16
- return False
17
+ raise Http404()
17
18
 
18
19
  if instance not in request.user.instances:
19
20
  return False
@@ -37,12 +38,14 @@ class InstanceSuperuserCanEdit(BasePermission):
37
38
  def has_object_permission(self, request, view, obj):
38
39
  '''
39
40
  in this permission we only care about:
40
- POST - create new object
41
+ POST - new object can be created
42
+ PUT - create new object
41
43
  PATCH - modify an object
42
44
  DELETE - delete an oject
43
45
  '''
46
+ # TODO: allow object creation only with PUT method, this way proper permissions system will be guaranteed.
44
47
 
45
- if request.method in SAFE_METHODS + ('PUT', 'POST', ): # TODO: remove POST
48
+ if request.method in SAFE_METHODS + ('POST',):
46
49
  return True
47
50
 
48
51
  # allow deleting only empty categories and zones
@@ -56,6 +59,9 @@ class InstanceSuperuserCanEdit(BasePermission):
56
59
  if user_role.is_superuser:
57
60
  return True
58
61
 
62
+ if user_role.is_owner and request.method == 'PUT':
63
+ return True
64
+
59
65
  return False
60
66
 
61
67
 
simo/core/serializers.py CHANGED
@@ -261,6 +261,12 @@ class ComponentSerializer(FormSerializer):
261
261
  self.set_form_cls()
262
262
 
263
263
  ret = OrderedDict()
264
+ if not self.context['request'].user.is_master:
265
+ user_role = self.context['request'].user.get_role(
266
+ self.context['instance']
267
+ )
268
+ if not any([user_role.is_superuser, user_role.is_owner]):
269
+ return ret
264
270
 
265
271
  field_mapping = reduce_attr_dict_from_instance(
266
272
  self,
@@ -269,9 +275,10 @@ class ComponentSerializer(FormSerializer):
269
275
  )
270
276
 
271
277
  if not self.instance or isinstance(self.instance, Iterable):
272
- form = self.Meta.form()
278
+ form = self.get_form()
273
279
  else:
274
- form = self.Meta.form(instance=self.instance)
280
+ form = self.get_form(instance=self.instance)
281
+
275
282
  for field_name in form.fields:
276
283
  # if field is specified as excluded field
277
284
  if field_name in getattr(self.Meta, 'exclude', []):
@@ -355,7 +362,7 @@ class ComponentSerializer(FormSerializer):
355
362
 
356
363
  def get_form(self, data=None, **kwargs):
357
364
  self.set_form_cls()
358
- if not self.instance:
365
+ if not self.instance or isinstance(self.instance, Iterable):
359
366
  #controller_uid = 'simo.generic.controllers.AlarmClock'
360
367
  controller_uid = self.context['request'].META.get('HTTP_CONTROLLER')
361
368
  else:
@@ -365,6 +372,14 @@ class ComponentSerializer(FormSerializer):
365
372
  controller_uid=controller_uid,
366
373
  **kwargs
367
374
  )
375
+ if not self.context['request'].user.is_master:
376
+ user_role = self.context['request'].user.get_role(
377
+ self.context['instance']
378
+ )
379
+ if not user_role.is_superuser and user_role.is_owner:
380
+ for field_name in list(form.fields.keys()):
381
+ if field_name not in form.basic_fields:
382
+ del form.fields[field_name]
368
383
  return form
369
384
 
370
385
  def accomodate_formsets(self, form, data):
@@ -395,6 +410,7 @@ class ComponentSerializer(FormSerializer):
395
410
  )
396
411
  form = self.get_form(instance=self.instance)
397
412
  a_data = self.accomodate_formsets(form, data)
413
+
398
414
  form = self.get_form(
399
415
  data=a_data, instance=self.instance
400
416
  )
@@ -410,6 +426,7 @@ class ComponentSerializer(FormSerializer):
410
426
  a_data = self.accomodate_formsets(form, validated_data)
411
427
  form = self.get_form(instance=instance, data=a_data)
412
428
  if form.is_valid():
429
+ print("FORM FIELDS", form.fields)
413
430
  instance = form.save(commit=True)
414
431
  return instance
415
432
  raise serializers.ValidationError(form.errors)
@@ -0,0 +1,20 @@
1
+
2
+ def restore_json(data):
3
+ for key, val in data.items():
4
+ if not isinstance(val, str):
5
+ continue
6
+ try:
7
+ data[key] = int(val)
8
+ continue
9
+ except:
10
+ pass
11
+ try:
12
+ data[key] = float(val)
13
+ continue
14
+ except:
15
+ pass
16
+ if val.lower() == 'true':
17
+ data[key] = True
18
+ elif val.lower() == 'false':
19
+ data[key] = False
20
+ return data
Binary file