simo 2.4.1__py3-none-any.whl → 2.5.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 (82) hide show
  1. simo/backups/__pycache__/admin.cpython-38.pyc +0 -0
  2. simo/backups/__pycache__/models.cpython-38.pyc +0 -0
  3. simo/backups/__pycache__/tasks.cpython-38.pyc +0 -0
  4. simo/backups/admin.py +10 -13
  5. simo/backups/migrations/0003_alter_backuplog_options_alter_backup_size.py +22 -0
  6. simo/backups/migrations/0004_alter_backup_options_alter_backuplog_options_and_more.py +29 -0
  7. simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-38.pyc +0 -0
  8. simo/backups/migrations/__pycache__/0004_alter_backup_options_alter_backuplog_options_and_more.cpython-38.pyc +0 -0
  9. simo/backups/models.py +1 -7
  10. simo/backups/tasks.py +221 -145
  11. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/events.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  21. simo/core/admin.py +4 -4
  22. simo/core/api.py +20 -4
  23. simo/core/app_widgets.py +5 -0
  24. simo/core/controllers.py +2 -2
  25. simo/core/events.py +2 -0
  26. simo/core/forms.py +2 -0
  27. simo/core/management/commands/gateways_manager.py +0 -3
  28. simo/core/middleware.py +7 -1
  29. simo/core/migrations/0042_alter_instance_timezone.py +18 -0
  30. simo/core/migrations/__pycache__/0042_alter_instance_timezone.cpython-38.pyc +0 -0
  31. simo/core/models.py +26 -6
  32. simo/core/serializers.py +17 -17
  33. simo/core/tasks.py +10 -7
  34. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  35. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  36. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  37. simo/fleet/controllers.py +86 -22
  38. simo/fleet/forms.py +224 -185
  39. simo/fleet/migrations/0038_alter_colonel_type.py +18 -0
  40. simo/fleet/migrations/0039_auto_20241016_1047.py +28 -0
  41. simo/fleet/migrations/0040_alter_colonel_pwm_frequency.py +18 -0
  42. simo/fleet/migrations/__pycache__/0038_alter_colonel_type.cpython-38.pyc +0 -0
  43. simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-38.pyc +0 -0
  44. simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-38.pyc +0 -0
  45. simo/fleet/models.py +2 -2
  46. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  47. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  48. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  49. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  50. simo/generic/controllers.py +41 -2
  51. simo/generic/forms.py +71 -7
  52. simo/generic/models.py +0 -1
  53. simo/generic/scripting/__init__.py +16 -0
  54. simo/generic/scripting/__pycache__/__init__.cpython-38.pyc +0 -0
  55. simo/generic/scripting/__pycache__/serializers.cpython-38.pyc +0 -0
  56. simo/generic/scripting/helpers.py +35 -0
  57. simo/generic/scripting/serializers.py +77 -0
  58. simo/generic/templates/admin/controller_widgets/weather_forecast.html +2 -2
  59. simo/notifications/__pycache__/models.cpython-38.pyc +0 -0
  60. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  61. simo/notifications/utils.py +30 -12
  62. simo/scripting.py +2 -2
  63. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  64. simo/users/__pycache__/managers.cpython-38.pyc +0 -0
  65. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  66. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  67. simo/users/__pycache__/utils.cpython-38.pyc +0 -0
  68. simo/users/api.py +36 -7
  69. simo/users/managers.py +5 -1
  70. simo/users/migrations/0033_alter_user_ssh_key.py +18 -0
  71. simo/users/migrations/0034_instanceuser_last_seen_location_and_more.py +24 -0
  72. simo/users/migrations/__pycache__/0033_alter_user_ssh_key.cpython-38.pyc +0 -0
  73. simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-38.pyc +0 -0
  74. simo/users/models.py +37 -32
  75. simo/users/serializers.py +11 -8
  76. simo/users/utils.py +14 -3
  77. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/METADATA +1 -1
  78. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/RECORD +82 -59
  79. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/WHEEL +1 -1
  80. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/LICENSE.md +0 -0
  81. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/entry_points.txt +0 -0
  82. {simo-2.4.1.dist-info → simo-2.5.1.dist-info}/top_level.txt +0 -0
simo/fleet/models.py CHANGED
@@ -91,7 +91,7 @@ class Colonel(DirtyFieldsMixin, models.Model):
91
91
  "and reset if a lot of data is being transmitted. "
92
92
  "Leave this off, unleess you know what you are doing!"
93
93
  )
94
- pwm_frequency = models.IntegerField(default=1, choices=(
94
+ pwm_frequency = models.IntegerField(default=0, choices=(
95
95
  (0, "3kHz"), (1, "22kHz")
96
96
  ), help_text="Affects Ample Wall dimmer PWM output (dimmer) frequency")
97
97
 
@@ -311,7 +311,7 @@ def post_component_delete(sender, instance, *args, **kwargs):
311
311
  comp, instance.config.get('da', 0), remove=True
312
312
  )
313
313
 
314
- elif instance.controller.family == 'dali':
314
+ elif instance.controller and instance.controller.family == 'dali':
315
315
  colonel = Colonel.objects.filter(id=instance.config['colonel']).first()
316
316
  if colonel:
317
317
  GatewayObjectCommand(
@@ -3,6 +3,10 @@ import threading
3
3
  import pytz
4
4
  import datetime
5
5
  import json
6
+ import requests
7
+ import traceback
8
+ import sys
9
+ from bs4 import BeautifulSoup
6
10
  from django.core.exceptions import ValidationError
7
11
  from django.utils import timezone
8
12
  from django.utils.functional import cached_property
@@ -16,6 +20,7 @@ from simo.core.events import GatewayObjectCommand
16
20
  from simo.core.models import RUN_STATUS_CHOICES_MAP, Component
17
21
  from simo.core.utils.helpers import get_random_string
18
22
  from simo.core.utils.operations import OPERATIONS
23
+ from simo.core.middleware import get_current_instance
19
24
  from simo.core.controllers import (
20
25
  BEFORE_SEND, BEFORE_SET, ControllerBase,
21
26
  BinarySensor, NumericSensor, MultiSensor, Switch, Dimmer, DimmerPlus,
@@ -42,12 +47,14 @@ from .forms import (
42
47
  BlindsConfigForm, WateringConfigForm, StateSelectForm,
43
48
  AlarmClockConfigForm
44
49
  )
50
+ from .scripting import get_current_state
51
+ from .scripting.serializers import UserSerializer
45
52
 
46
53
  # ----------- Generic controllers -----------------------------
47
54
 
55
+
48
56
  class Script(ControllerBase, TimerMixin):
49
- masters_only = True
50
- name = _("Script")
57
+ name = _("AI Script")
51
58
  base_type = 'script'
52
59
  gateway_class = GenericGatewayHandler
53
60
  app_widget = ScriptWidget
@@ -101,6 +108,38 @@ class Script(ControllerBase, TimerMixin):
101
108
  else:
102
109
  self.send('start')
103
110
 
111
+ def ai_assistant(self, wish):
112
+ try:
113
+ request_data = {
114
+ 'hub_uid': dynamic_settings['core__hub_uid'],
115
+ 'hub_secret': dynamic_settings['core__hub_secret'],
116
+ 'instance_uid': get_current_instance().uid,
117
+ 'system_data': json.dumps(get_current_state()),
118
+ 'wish': wish,
119
+ }
120
+ except Exception as e:
121
+ print(traceback.format_exc(), file=sys.stderr)
122
+ return {'status': 'error', 'result': f"Internal error: {e}"}
123
+ user = get_current_user()
124
+ if user:
125
+ request_data['current_user'] = UserSerializer(user, many=False).data
126
+ try:
127
+ response = requests.post(
128
+ 'https://simo.io/hubs/ai-assist/scripts/', json=request_data
129
+ )
130
+ except:
131
+ return {'status': 'error', 'result': "Connection error"}
132
+
133
+ if response.status_code != 200:
134
+ content = response.content.decode()
135
+ if '<html' in content:
136
+ # Parse the HTML content
137
+ soup = BeautifulSoup(response.content, 'html.parser')
138
+ content = F"Server error {response.status_code}: {soup.title.string}"
139
+ return {'status': 'error', 'result': content}
140
+
141
+ return {'status': 'success', 'result': response.content.decode()}
142
+
104
143
 
105
144
  class PresenceLighting(Script):
106
145
  masters_only = False
simo/generic/forms.py CHANGED
@@ -1,3 +1,4 @@
1
+ import time
1
2
  from django import forms
2
3
  from django.forms import formset_factory
3
4
  from django.db.models import Q
@@ -35,11 +36,30 @@ class ScriptConfigForm(BaseComponentForm):
35
36
  initial=True, required=False,
36
37
  help_text="Restart the script if it fails. "
37
38
  )
38
- code = forms.CharField(widget=PythonCode)
39
+ assistant_request = forms.CharField(
40
+ label="Request for AI assistant", required=False, max_length=1000,
41
+ widget=forms.Textarea(
42
+ attrs={'placeholder':
43
+ "Close the blind and turn on the main light "
44
+ "in my living room when it get's dark."
45
+ }
46
+ ),
47
+ help_text="Clearly describe in your own words what kind of automation "
48
+ "you want to happen with this scenario script. <br>"
49
+ "The more defined, exact and clear is your description the more "
50
+ "accurate automation script SIMO.io AI assistanw will generate.<br>"
51
+ "Use component, zone and category id's for best accuracy. <br>"
52
+ "SIMO.io AI will re-generate your automation code, "
53
+ "every time this field is changed. <br>"
54
+ "Actual script code can only be edited via SIMO.io Admin.",
55
+ )
56
+ code = forms.CharField(widget=PythonCode, required=False)
39
57
  log = forms.CharField(
40
58
  widget=forms.HiddenInput, required=False
41
59
  )
42
60
 
61
+ app_exclude_fields = ('alarm_category', 'code', 'log')
62
+
43
63
  def __init__(self, *args, **kwargs):
44
64
  super().__init__(*args, **kwargs)
45
65
  self.basic_fields.extend(['autostart', 'keep_alive'])
@@ -47,19 +67,20 @@ class ScriptConfigForm(BaseComponentForm):
47
67
  prefix = get_script_prefix()
48
68
  if prefix == '/':
49
69
  prefix = ''
50
- self.fields['log'].widget = LogOutputWidget(
51
- prefix + '/ws/log/%d/%d/' % (
52
- ContentType.objects.get_for_model(Component).id,
53
- self.instance.id
70
+ if 'log' in self.fields:
71
+ self.fields['log'].widget = LogOutputWidget(
72
+ prefix + '/ws/log/%d/%d/' % (
73
+ ContentType.objects.get_for_model(Component).id,
74
+ self.instance.id
75
+ )
54
76
  )
55
- )
56
77
 
57
78
  @classmethod
58
79
  def get_admin_fieldsets(cls, request, obj=None):
59
80
  base_fields = (
60
81
  'id', 'gateway', 'base_type', 'name', 'icon', 'zone', 'category',
61
82
  'show_in_app', 'autostart', 'keep_alive',
62
- 'code', 'control', 'log'
83
+ 'assistant_request', 'code', 'control', 'log'
63
84
  )
64
85
 
65
86
  fieldsets = [
@@ -71,6 +92,36 @@ class ScriptConfigForm(BaseComponentForm):
71
92
  ]
72
93
  return fieldsets
73
94
 
95
+ def clean(self):
96
+ if self.cleaned_data.get('assistant_request'):
97
+ if self.instance.pk:
98
+ org = Component.objects.get(pk=self.instance.pk)
99
+ call_assistant = org.config.get('assistant_request') \
100
+ != self.cleaned_data['assistant_request']
101
+ else:
102
+ call_assistant = True
103
+ if call_assistant:
104
+ resp = self.instance.ai_assistant(
105
+ self.cleaned_data['assistant_request'],
106
+ )
107
+ if resp['status'] == 'success':
108
+ self.cleaned_data['code'] = resp['result']
109
+ self.instance.config['code'] = resp['result']
110
+ elif resp['status'] == 'error':
111
+ self.add_error('assistant_request', resp['result'])
112
+
113
+ return self.cleaned_data
114
+
115
+ def save(self, commit=True):
116
+ obj = super().save(commit)
117
+ if commit:
118
+ obj.controller.stop()
119
+ if self.cleaned_data.get('keep_alive') \
120
+ or self.cleaned_data.get('autostart'):
121
+ time.sleep(2)
122
+ obj.controller.start()
123
+ return obj
124
+
74
125
 
75
126
  class ConditionForm(forms.Form):
76
127
  component = forms.ModelChoiceField(
@@ -717,6 +768,19 @@ class StateSelectForm(BaseComponentForm):
717
768
  states = FormsetField(
718
769
  formset_factory(StateForm, can_delete=True, can_order=True, extra=0)
719
770
  )
771
+ is_main = forms.BooleanField(
772
+ initial=False, required=False,
773
+ help_text="Will be displayed in the app "
774
+ "right top corner for quick access."
775
+ )
776
+
777
+ def save(self, commit=True):
778
+ if commit and self.cleaned_data['is_main']:
779
+ from .controllers import StateSelect
780
+ for c in Component.objects.filter(controller_uid=StateSelect.uid):
781
+ c.config['is_main'] = False
782
+ c.save()
783
+ return super().save(commit)
720
784
 
721
785
 
722
786
  class AlarmClockEventForm(forms.Form):
simo/generic/models.py CHANGED
@@ -56,7 +56,6 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
56
56
  )
57
57
  from simo.notifications.utils import notify_users
58
58
  notify_users(
59
- alarm_group_component.zone.instance,
60
59
  'alarm', str(alarm_group_component), body,
61
60
  component=alarm_group_component
62
61
  )
@@ -0,0 +1,16 @@
1
+ from .serializers import *
2
+ from simo.core.models import Zone, Category, Component
3
+ from simo.users.models import InstanceUser
4
+ from simo.core.middleware import get_current_instance
5
+
6
+
7
+ def get_current_state():
8
+ get_current_instance()
9
+ return {
10
+ 'zones': ZoneSerializer(Zone.objects.all(), many=True).data,
11
+ 'categories': CategorySerializer(Category.objects.all(), many=True).data,
12
+ 'component': ComponentSerializer(Component.objects.all(), many=True).data,
13
+ 'instanceusers': InstanceUserSerializer(
14
+ InstanceUser.objects.all(), many=True
15
+ ).data,
16
+ }
@@ -0,0 +1,35 @@
1
+ from django.utils import timezone
2
+ from suntime import Sun
3
+ from simo.core.models import Instance
4
+
5
+
6
+ class LocalSun(Sun):
7
+
8
+ def __init__(self, instance=None):
9
+ if not instance:
10
+ instance = Instance.objects.all().first()
11
+ coordinates = instance.location.split(',')
12
+ try:
13
+ lat = float(coordinates[0])
14
+ except:
15
+ lat = 0
16
+ try:
17
+ lon = float(coordinates[1])
18
+ except:
19
+ lon = 0
20
+ super().__init__(lat, lon)
21
+
22
+ def is_night(self):
23
+ if timezone.now() > self.get_sunset_time():
24
+ return True
25
+ if timezone.now() < self.get_sunrise_time():
26
+ return True
27
+ return False
28
+
29
+ def seconds_to_sunset(self):
30
+ return (self.get_sunset_time() - timezone.now()).total_seconds()
31
+
32
+ def seconds_to_sunrise(self):
33
+ return (self.get_sunrise_time() - timezone.now()).total_seconds()
34
+
35
+
@@ -0,0 +1,77 @@
1
+ from rest_framework import serializers
2
+ from simo.core.models import Zone, Category, Component
3
+ from simo.users.models import User, InstanceUser, PermissionsRole
4
+
5
+
6
+ class ZoneSerializer(serializers.ModelSerializer):
7
+ '''Zone serializer for AI scripts helper'''
8
+
9
+ class Meta:
10
+ model = Zone
11
+ fields = 'id', 'name'
12
+
13
+
14
+ class CategorySerializer(serializers.ModelSerializer):
15
+ '''Category serializer for AI scripts helper'''
16
+
17
+ class Meta:
18
+ model = Category
19
+ fields = 'id', 'name'
20
+
21
+
22
+ class ComponentSerializer(serializers.ModelSerializer):
23
+ '''Component serializer for AI scripts helper'''
24
+
25
+ MAX_LENGTH = 500
26
+
27
+ value = serializers.SerializerMethodField()
28
+ meta = serializers.SerializerMethodField()
29
+ config = serializers.SerializerMethodField()
30
+
31
+ class Meta:
32
+ model = Component
33
+ fields = (
34
+ 'id', 'name', 'icon', 'zone', 'category', 'base_type',
35
+ 'value', 'value_units', 'meta', 'config'
36
+ )
37
+
38
+ def get_value(self, obj):
39
+ if obj.base_type in ('ip-camera', ):
40
+ return 'SKIP'
41
+ if len(str(obj.value)) > self.MAX_LENGTH:
42
+ return 'SKIP'
43
+ return obj.value
44
+
45
+ def get_meta(self, obj):
46
+ if len(str(obj.value)) > self.MAX_LENGTH:
47
+ return 'SKIP'
48
+ return obj.value
49
+
50
+ def get_config(self, obj):
51
+ if len(str(obj.value)) > self.MAX_LENGTH:
52
+ return 'SKIP'
53
+ return obj.value
54
+
55
+
56
+ class UserSerializer(serializers.ModelSerializer):
57
+
58
+ class Meta:
59
+ model = User
60
+ fields = 'email', 'name'
61
+
62
+
63
+ class PermissionsRoleSerializer(serializers.ModelSerializer):
64
+
65
+ class Meta:
66
+ model = PermissionsRole
67
+ fields = 'id', 'name', 'is_owner', 'is_superuser'
68
+
69
+
70
+ class InstanceUserSerializer(serializers.ModelSerializer):
71
+ '''Role serializer for AI scripts helper'''
72
+ user = UserSerializer()
73
+ role = PermissionsRoleSerializer()
74
+
75
+ class Meta:
76
+ model = InstanceUser
77
+ fields = 'user', 'role',
@@ -3,11 +3,11 @@
3
3
  data-ws_url="{{ obj.get_socket_url|default_if_none:"" }}">
4
4
  <span style="
5
5
  width: 20px; height:20px;
6
- background: url({% static 'weather_icons' %}/{{ obj.value.current.weather.0.icon }}@2x.png) center no-repeat;
6
+ background: url({% static 'weather_icons' %}/{{ obj.value.weather.0.icon }}@2x.png) center no-repeat;
7
7
  background-size: 150%;
8
8
  display: inline-block;
9
9
  position: relative;
10
10
  bottom: -4px;
11
11
  "></span>
12
- {{ obj.value.current.temp }}ᴼ {% if obj.zone.instance.units_of_measure == 'metric' %}C{% else %}F{% endif %}
12
+ {{ obj.value.temp }}ᴼ {% if obj.zone.instance.units_of_measure == 'metric' %}C{% else %}F{% endif %}
13
13
  </div>
@@ -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,18 @@
1
+ # Generated by Django 4.2.10 on 2024-10-09 09:16
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('users', '0032_remove_userdevice_user_alter_userdevice_users'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='user',
15
+ name='ssh_key',
16
+ field=models.TextField(blank=True, help_text='Will be placed in /root/.ssh/authorized_keys if user is active and is master of a hub.', null=True),
17
+ ),
18
+ ]
@@ -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
+ ]