simo 2.3.3__py3-none-any.whl → 2.3.5__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.
- simo/__pycache__/asgi.cpython-38.pyc +0 -0
- simo/__pycache__/settings.cpython-38.pyc +0 -0
- simo/backups/__init__.py +0 -0
- simo/backups/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/backups/__pycache__/admin.cpython-38.pyc +0 -0
- simo/backups/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
- simo/backups/__pycache__/models.cpython-38.pyc +0 -0
- simo/backups/admin.py +19 -0
- simo/backups/dynamic_settings.py +21 -0
- simo/backups/migrations/0001_initial.py +27 -0
- simo/backups/migrations/__init__.py +0 -0
- simo/backups/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/backups/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/backups/models.py +18 -0
- simo/backups/tasks.py +272 -0
- simo/core/__pycache__/context.cpython-38.pyc +0 -0
- simo/core/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
- simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/context.py +2 -2
- simo/core/middleware.py +16 -0
- simo/core/serializers.py +3 -1
- simo/core/tasks.py +0 -2
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/controllers.py +1 -2
- simo/fleet/forms.py +6 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/generic/forms.py +4 -2
- simo/settings.py +1 -0
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/admin.py +14 -15
- simo/users/models.py +22 -20
- {simo-2.3.3.dist-info → simo-2.3.5.dist-info}/METADATA +1 -1
- {simo-2.3.3.dist-info → simo-2.3.5.dist-info}/RECORD +42 -32
- simo/core/templates/admin/formset_widget_old.html +0 -122
- simo/core/templates/setup_wizard/clearable_easy_thumbnails_widget.html +0 -15
- simo/core/templates/setup_wizard/form.html +0 -225
- {simo-2.3.3.dist-info → simo-2.3.5.dist-info}/LICENSE.md +0 -0
- {simo-2.3.3.dist-info → simo-2.3.5.dist-info}/WHEEL +0 -0
- {simo-2.3.3.dist-info → simo-2.3.5.dist-info}/entry_points.txt +0 -0
- {simo-2.3.3.dist-info → simo-2.3.5.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
simo/backups/__init__.py
ADDED
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/backups/admin.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import Backup
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@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',
|
|
11
|
+
|
|
12
|
+
def has_change_permission(self, request, obj=None):
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
def has_add_permission(self, request):
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
def has_delete_permission(self, request, obj=None):
|
|
19
|
+
return False
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dynamic_preferences.preferences import Section
|
|
2
|
+
from dynamic_preferences.types import (
|
|
3
|
+
BooleanPreference, StringPreference, ChoicePreference, IntegerPreference,
|
|
4
|
+
)
|
|
5
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
6
|
+
|
|
7
|
+
backups = Section('backups')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@global_preferences_registry.register
|
|
11
|
+
class LastBackupError(StringPreference):
|
|
12
|
+
section = backups
|
|
13
|
+
name = 'last_error'
|
|
14
|
+
default = ''
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@global_preferences_registry.register
|
|
18
|
+
class LastBackupCheck(IntegerPreference):
|
|
19
|
+
section = backups
|
|
20
|
+
name = 'last_check'
|
|
21
|
+
default = 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-10-01 13:01
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
initial = True
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name='Backup',
|
|
16
|
+
fields=[
|
|
17
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
18
|
+
('datetime', models.DateTimeField(db_index=True)),
|
|
19
|
+
('mac', models.CharField(db_index=True, max_length=100)),
|
|
20
|
+
('filepath', models.CharField(max_length=200)),
|
|
21
|
+
],
|
|
22
|
+
options={
|
|
23
|
+
'ordering': ('datetime',),
|
|
24
|
+
'unique_together': {('datetime', 'mac')},
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
]
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
simo/backups/models.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Backup(models.Model):
|
|
6
|
+
datetime = models.DateTimeField(db_index=True)
|
|
7
|
+
mac = models.CharField(max_length=100, db_index=True)
|
|
8
|
+
filepath = models.CharField(max_length=200)
|
|
9
|
+
|
|
10
|
+
class Meta:
|
|
11
|
+
unique_together = 'datetime', 'mac'
|
|
12
|
+
ordering = 'datetime',
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def device(self):
|
|
16
|
+
if self.mac == str(hex(uuid.getnode())):
|
|
17
|
+
return "This machine"
|
|
18
|
+
return self.mac
|
simo/backups/tasks.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import os, subprocess, json, uuid, datetime, shutil
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from celeryc import celery_app
|
|
4
|
+
from simo.conf import dynamic_settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@celery_app.task
|
|
8
|
+
def check_backups():
|
|
9
|
+
'''
|
|
10
|
+
syncs up backups on external medium to the database
|
|
11
|
+
'''
|
|
12
|
+
from .models import Backup
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
lv_group, lv_name, mountpoint = get_partitions()
|
|
16
|
+
except:
|
|
17
|
+
return Backup.objects.all().delete()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
backups_dir = os.path.join(mountpoint, 'simo_backups')
|
|
21
|
+
if not os.path.exists(backups_dir):
|
|
22
|
+
return Backup.objects.all().delete()
|
|
23
|
+
|
|
24
|
+
backups_mentioned = []
|
|
25
|
+
for item in os.listdir(backups_dir):
|
|
26
|
+
if not item.startswith('hub-'):
|
|
27
|
+
continue
|
|
28
|
+
hub_mac = item.split('-')[1]
|
|
29
|
+
hub_dir = os.path.join(backups_dir, item)
|
|
30
|
+
for month_folder in os.listdir(hub_dir):
|
|
31
|
+
try:
|
|
32
|
+
year, month = month_folder.split('-')
|
|
33
|
+
year, month = int(year), int(month)
|
|
34
|
+
except:
|
|
35
|
+
continue
|
|
36
|
+
for filename in os.listdir(os.path.join(hub_dir, month_folder)):
|
|
37
|
+
try:
|
|
38
|
+
day, time, back = filename.split('.')
|
|
39
|
+
hour, minute, second = time.split('-')
|
|
40
|
+
day, hour, minute, second = \
|
|
41
|
+
int(day), int(hour), int(minute), int(second)
|
|
42
|
+
except:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
obj, new = Backup.objects.update_or_create(
|
|
46
|
+
datetime=datetime(
|
|
47
|
+
year, month, day, hour, minute, second,
|
|
48
|
+
tzinfo=timezone.utc
|
|
49
|
+
), mac=hub_mac, defaults={
|
|
50
|
+
'filepath': os.path.join(
|
|
51
|
+
hub_dir, month_folder, filename
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
backups_mentioned.append(obj.id)
|
|
56
|
+
|
|
57
|
+
Backup.objects.all().exclude(id__in=backups_mentioned).delete()
|
|
58
|
+
|
|
59
|
+
dynamic_settings['backups__last_check'] = datetime.now()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_snap(lv_group, lv_name):
|
|
63
|
+
try:
|
|
64
|
+
return subprocess.check_output(
|
|
65
|
+
f'lvcreate -s -n {lv_name}-snap {lv_group}/{lv_name} -L 3G',
|
|
66
|
+
shell=True
|
|
67
|
+
).decode()
|
|
68
|
+
except:
|
|
69
|
+
return ''
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_lvm_partition(lsblk_data):
|
|
73
|
+
for device in lsblk_data:
|
|
74
|
+
if device['type'] == 'lvm' and device['mountpoint'] == '/':
|
|
75
|
+
return device
|
|
76
|
+
if 'children' in device:
|
|
77
|
+
return get_lvm_partition(device['children'])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_backup_device(lsblk_data):
|
|
81
|
+
for device in lsblk_data:
|
|
82
|
+
if not device['hotplug']:
|
|
83
|
+
continue
|
|
84
|
+
target_device = None
|
|
85
|
+
if device.get('fstype') == 'exfat':
|
|
86
|
+
target_device = device
|
|
87
|
+
elif device.get('children'):
|
|
88
|
+
for child in device.get('children'):
|
|
89
|
+
if child.get('fstype') == 'exfat':
|
|
90
|
+
target_device = child
|
|
91
|
+
if target_device:
|
|
92
|
+
return target_device
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_partitions():
|
|
96
|
+
|
|
97
|
+
lsblk_data = json.loads(subprocess.check_output(
|
|
98
|
+
'lsblk --output NAME,HOTPLUG,MOUNTPOINT,FSTYPE,TYPE,LABEL,PARTLABEL --json',
|
|
99
|
+
shell=True
|
|
100
|
+
).decode())['blockdevices']
|
|
101
|
+
|
|
102
|
+
# Figure out if we are running in an LVM group
|
|
103
|
+
|
|
104
|
+
lvm_partition = get_lvm_partition(lsblk_data)
|
|
105
|
+
if not lvm_partition:
|
|
106
|
+
print("No LVM partition!")
|
|
107
|
+
dynamic_settings['backups__last_error'] = 'No LVM partition!'
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
name = lvm_partition.get('name')
|
|
112
|
+
split_at = name.find('-')
|
|
113
|
+
lv_group = name[:split_at]
|
|
114
|
+
lv_name = name[split_at + 1:].replace('--', '-')
|
|
115
|
+
except:
|
|
116
|
+
print("Failed to identify LVM partition")
|
|
117
|
+
dynamic_settings['backups__last_error'] = \
|
|
118
|
+
'Failed to identify LVM partition'
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
if not lv_name:
|
|
122
|
+
print("LVM was not found on this system. Abort!")
|
|
123
|
+
dynamic_settings['backups__last_error'] = \
|
|
124
|
+
'Failed to identify LVM partition name'
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# check if we have any removable devices storage devices plugged in
|
|
129
|
+
|
|
130
|
+
backup_device = get_backup_device(lsblk_data)
|
|
131
|
+
|
|
132
|
+
if not backup_device:
|
|
133
|
+
dynamic_settings['backups__last_error'] = \
|
|
134
|
+
'No external exFAT backup device on this machine'
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if lvm_partition.get('partlabel'):
|
|
138
|
+
mountpoint = f"/media/{backup_device['partlabel']}"
|
|
139
|
+
elif lvm_partition.get('label'):
|
|
140
|
+
mountpoint = f"/media/{backup_device['label']}"
|
|
141
|
+
else:
|
|
142
|
+
mountpoint = f"/media/{backup_device['name']}"
|
|
143
|
+
|
|
144
|
+
if not os.path.exists(mountpoint):
|
|
145
|
+
os.makedirs(mountpoint)
|
|
146
|
+
|
|
147
|
+
if backup_device.get('mountpoint') != mountpoint:
|
|
148
|
+
|
|
149
|
+
if backup_device.get('mountpoint'):
|
|
150
|
+
subprocess.call(f"umount {backup_device['mountpoint']}", shell=True)
|
|
151
|
+
|
|
152
|
+
subprocess.call(
|
|
153
|
+
f'mount /dev/{backup_device["name"]} {mountpoint}', shell=True,
|
|
154
|
+
stdout=subprocess.PIPE
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return lv_group, lv_name, mountpoint
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@celery_app.task
|
|
161
|
+
def perform_backup():
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
lv_group, lv_name, mountpoint = get_partitions()
|
|
165
|
+
except:
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
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
|
+
)
|
|
174
|
+
output = create_snap(lv_group, lv_name)
|
|
175
|
+
|
|
176
|
+
if f'Logical volume "{lv_name}-snap" created' not in output:
|
|
177
|
+
print(output)
|
|
178
|
+
print(f"Unable to create {lv_name}-snap.")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
mac = str(hex(uuid.getnode()))
|
|
182
|
+
device_backups_path = f'{mountpoint}/simo_backups/hub-{mac}'
|
|
183
|
+
if not os.path.exists(device_backups_path):
|
|
184
|
+
os.makedirs(device_backups_path)
|
|
185
|
+
|
|
186
|
+
now = datetime.datetime.now()
|
|
187
|
+
level = now.day
|
|
188
|
+
month_folder = os.path.join(
|
|
189
|
+
device_backups_path, f'{now.year}-{now.month}'
|
|
190
|
+
)
|
|
191
|
+
if not os.path.exists(month_folder):
|
|
192
|
+
os.makedirs(month_folder)
|
|
193
|
+
level = 0
|
|
194
|
+
|
|
195
|
+
if level != 0:
|
|
196
|
+
# check if level 0 exists
|
|
197
|
+
level_0_exists = False
|
|
198
|
+
for filename in os.listdir(month_folder):
|
|
199
|
+
if '-' not in filename:
|
|
200
|
+
continue
|
|
201
|
+
try:
|
|
202
|
+
level, date = filename.split('-')
|
|
203
|
+
level = int(level)
|
|
204
|
+
if level == 0:
|
|
205
|
+
level_0_exists = True
|
|
206
|
+
break
|
|
207
|
+
except:
|
|
208
|
+
continue
|
|
209
|
+
if not level_0_exists:
|
|
210
|
+
print("Level 0 does not exist! Backups must be started from 0!")
|
|
211
|
+
shutil.rmtree(month_folder)
|
|
212
|
+
os.makedirs(month_folder)
|
|
213
|
+
level = 0
|
|
214
|
+
|
|
215
|
+
time_mark = now.strftime("%H-%M-%S")
|
|
216
|
+
backup_file = f"{month_folder}/{now.day}.{time_mark}.back"
|
|
217
|
+
snap_mapper = f"/dev/mapper/{lv_group}-{lv_name.replace('-', '--')}--snap"
|
|
218
|
+
label = f"simo {now.strftime('%Y-%m-%d')}"
|
|
219
|
+
dumpdates_file = os.path.join(month_folder, 'dumpdates')
|
|
220
|
+
|
|
221
|
+
estimated_size = int(subprocess.check_output(
|
|
222
|
+
f'dump -{level} -Squz9 -b 1024 {snap_mapper}',
|
|
223
|
+
shell=True
|
|
224
|
+
)) * 0.5
|
|
225
|
+
|
|
226
|
+
folders = []
|
|
227
|
+
for item in os.listdir(device_backups_path):
|
|
228
|
+
if not os.path.isdir(os.path.join(device_backups_path, item)):
|
|
229
|
+
continue
|
|
230
|
+
try:
|
|
231
|
+
year, month = item.split('-')
|
|
232
|
+
folders.append([
|
|
233
|
+
os.path.join(device_backups_path, item),
|
|
234
|
+
int(year) * 12 + int(month)
|
|
235
|
+
])
|
|
236
|
+
except:
|
|
237
|
+
continue
|
|
238
|
+
folders.sort(key=lambda v: v[1])
|
|
239
|
+
|
|
240
|
+
# delete old backups to free up space required for this backup to take place
|
|
241
|
+
while shutil.disk_usage('/media/backup').free < estimated_size:
|
|
242
|
+
remove_folder = folders.pop()[0]
|
|
243
|
+
print(f"REMOVE: {remove_folder}")
|
|
244
|
+
shutil.rmtree(remove_folder)
|
|
245
|
+
|
|
246
|
+
success = False
|
|
247
|
+
try:
|
|
248
|
+
subprocess.check_call(
|
|
249
|
+
f'dump -{level} -quz9 -b 1024 -L "{label}" -D {dumpdates_file} -f {backup_file} {snap_mapper}',
|
|
250
|
+
shell=True,
|
|
251
|
+
)
|
|
252
|
+
success = True
|
|
253
|
+
except:
|
|
254
|
+
try:
|
|
255
|
+
os.remove(backup_file)
|
|
256
|
+
print("Dump failed!")
|
|
257
|
+
except:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
subprocess.call(
|
|
261
|
+
f"lvremove -f {lv_group}/{lv_name}-snap", shell=True,
|
|
262
|
+
stdout=subprocess.PIPE
|
|
263
|
+
)
|
|
264
|
+
if success:
|
|
265
|
+
print("DONE!")
|
|
266
|
+
dynamic_settings['backups__last_error'] = ''
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@celery_app.on_after_finalize.connect
|
|
270
|
+
def setup_periodic_tasks(sender, **kwargs):
|
|
271
|
+
sender.add_periodic_task(60 * 60, check_backups.s())
|
|
272
|
+
sender.add_periodic_task(60 * 60 * 8, perform_backup.s()) # perform auto backup every 8 hours
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/core/context.py
CHANGED
|
@@ -15,11 +15,11 @@ def additional_templates_context(request):
|
|
|
15
15
|
'dynamic_settings': dynamic_settings,
|
|
16
16
|
'current_version': pkg_resources.get_distribution('simo').version,
|
|
17
17
|
'update_available': is_update_available(True),
|
|
18
|
-
'instances':
|
|
18
|
+
'instances': request.user.instances,
|
|
19
19
|
'current_instance': get_current_instance()
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
if request.path
|
|
22
|
+
if request.path == '/admin/':
|
|
23
23
|
ctx['todos'] = []
|
|
24
24
|
for app_name, app in apps.app_configs.items():
|
|
25
25
|
try:
|
simo/core/middleware.py
CHANGED
|
@@ -2,6 +2,7 @@ import pytz
|
|
|
2
2
|
import threading
|
|
3
3
|
import re
|
|
4
4
|
from django.utils import timezone
|
|
5
|
+
from django.shortcuts import render
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
_thread_locals = threading.local()
|
|
@@ -15,9 +16,13 @@ def get_current_request():
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def introduce_instance(instance, request=None):
|
|
19
|
+
if request and request.user.is_authenticated \
|
|
20
|
+
and instance not in request.user.instances:
|
|
21
|
+
return
|
|
18
22
|
_thread_locals.instance = instance
|
|
19
23
|
if request:
|
|
20
24
|
request.session['instance_id'] = instance.id
|
|
25
|
+
request.instance = instance
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
def get_current_instance(request=None):
|
|
@@ -51,6 +56,17 @@ def simo_router_middleware(get_response):
|
|
|
51
56
|
def instance_middleware(get_response):
|
|
52
57
|
|
|
53
58
|
def middleware(request):
|
|
59
|
+
|
|
60
|
+
if request.path.startswith('/admin'):
|
|
61
|
+
if not (request.user.is_authenticated and request.user.is_master):
|
|
62
|
+
return render(request, 'admin/msg_page.html', {
|
|
63
|
+
'page_title': "You are not allowed in here",
|
|
64
|
+
'msg': "Page you are trying to access is only for hub masters.",
|
|
65
|
+
'suggestion': "Try switching your user to the one who has proper "
|
|
66
|
+
"rights to come here or ask for somebody who already has "
|
|
67
|
+
"master rights enable these rights for you."
|
|
68
|
+
})
|
|
69
|
+
|
|
54
70
|
from simo.core.models import Instance
|
|
55
71
|
|
|
56
72
|
instance = None
|
simo/core/serializers.py
CHANGED
|
@@ -416,11 +416,13 @@ class ComponentSerializer(FormSerializer):
|
|
|
416
416
|
controller_uid=controller_uid, instance=instance,
|
|
417
417
|
**kwargs
|
|
418
418
|
)
|
|
419
|
+
# only masters and superusers can fully manage components via app
|
|
420
|
+
# others can only change basic fields
|
|
419
421
|
if not self.context['request'].user.is_master:
|
|
420
422
|
user_role = self.context['request'].user.get_role(
|
|
421
423
|
self.context['instance']
|
|
422
424
|
)
|
|
423
|
-
if not user_role.is_superuser
|
|
425
|
+
if not user_role.is_superuser:
|
|
424
426
|
for field_name in list(form.fields.keys()):
|
|
425
427
|
if field_name not in form.basic_fields:
|
|
426
428
|
del form.fields[field_name]
|
simo/core/tasks.py
CHANGED
|
Binary file
|
|
Binary file
|
simo/fleet/controllers.py
CHANGED
|
@@ -424,12 +424,11 @@ class PWMOutput(FadeMixin, FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
|
|
|
424
424
|
return value
|
|
425
425
|
|
|
426
426
|
|
|
427
|
-
|
|
428
427
|
class RGBLight(FleeDeviceMixin, BasicOutputMixin, BaseRGBWLight):
|
|
429
428
|
config_form = ColonelRGBLightConfigForm
|
|
430
429
|
|
|
431
430
|
|
|
432
|
-
class DualMotorValve(FleeDeviceMixin, BasicOutputMixin,
|
|
431
|
+
class DualMotorValve(FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
|
|
433
432
|
gateway_class = FleetGatewayHandler
|
|
434
433
|
config_form = DualMotorValveForm
|
|
435
434
|
name = "Dual Motor Valve"
|
simo/fleet/forms.py
CHANGED
|
@@ -1109,6 +1109,12 @@ class BlindsConfigForm(ColonelComponentForm):
|
|
|
1109
1109
|
help_text="Time in seconds it takes for your blinds to go "
|
|
1110
1110
|
"from fully closed to fully open."
|
|
1111
1111
|
)
|
|
1112
|
+
close_duration = forms.FloatField(
|
|
1113
|
+
label='Close duration', min_value=1, max_value=360000,
|
|
1114
|
+
initial=30,
|
|
1115
|
+
help_text="Time in seconds it takes for your blinds to go "
|
|
1116
|
+
"from fully open to fully closed."
|
|
1117
|
+
)
|
|
1112
1118
|
control_type = forms.ChoiceField(
|
|
1113
1119
|
initial=0, required=True, choices=(
|
|
1114
1120
|
('hold', "Hold"), ('click', 'Click')
|
|
Binary file
|
|
Binary file
|
simo/generic/forms.py
CHANGED
|
@@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
7
7
|
from simo.core.forms import HiddenField, BaseComponentForm
|
|
8
8
|
from simo.core.models import Icon, Component
|
|
9
9
|
from simo.core.controllers import (
|
|
10
|
-
BEFORE_SET, BinarySensor, NumericSensor, MultiSensor, Switch
|
|
10
|
+
BEFORE_SET, BinarySensor, NumericSensor, MultiSensor, Switch, Dimmer
|
|
11
11
|
)
|
|
12
12
|
from simo.core.widgets import PythonCode, LogOutputWidget
|
|
13
13
|
from dal import autocomplete, forward
|
|
@@ -615,7 +615,9 @@ class ContourForm(forms.Form):
|
|
|
615
615
|
|
|
616
616
|
name = forms.CharField()
|
|
617
617
|
switch = forms.ModelChoiceField(
|
|
618
|
-
Component.objects.filter(
|
|
618
|
+
Component.objects.filter(
|
|
619
|
+
base_type__in=(Switch.base_type, Dimmer.base_type)
|
|
620
|
+
),
|
|
619
621
|
widget=autocomplete.ModelSelect2(
|
|
620
622
|
url='autocomplete-component', attrs={'data-html': True},
|
|
621
623
|
forward=(
|
simo/settings.py
CHANGED
|
Binary file
|
|
Binary file
|
simo/users/admin.py
CHANGED
|
@@ -25,16 +25,20 @@ class ComponentPermissionInline(admin.TabularInline):
|
|
|
25
25
|
|
|
26
26
|
@admin.register(PermissionsRole)
|
|
27
27
|
class PermissionsRoleAdmin(admin.ModelAdmin):
|
|
28
|
-
list_display = 'name', '
|
|
28
|
+
list_display = 'name', 'is_superuser', 'is_default'
|
|
29
29
|
search_fields = 'name',
|
|
30
|
-
list_filter = 'instance',
|
|
31
30
|
inlines = ComponentPermissionInline,
|
|
32
31
|
|
|
33
32
|
def get_queryset(self, request):
|
|
34
33
|
qs = super().get_queryset(request)
|
|
35
34
|
if request.user.is_master:
|
|
36
35
|
return qs
|
|
37
|
-
return qs.filter(
|
|
36
|
+
return qs.filter(instance=request.instance)
|
|
37
|
+
|
|
38
|
+
def save_model(self, request, obj, form, change):
|
|
39
|
+
if not obj.id:
|
|
40
|
+
obj.instance = request.instance
|
|
41
|
+
obj.save()
|
|
38
42
|
|
|
39
43
|
def get_fields(self, request, obj=None):
|
|
40
44
|
if request.user.is_master:
|
|
@@ -49,7 +53,7 @@ class PermissionsRoleAdmin(admin.ModelAdmin):
|
|
|
49
53
|
class InstanceUserInline(admin.TabularInline):
|
|
50
54
|
model = InstanceUser
|
|
51
55
|
extra = 0
|
|
52
|
-
readonly_fields = '
|
|
56
|
+
readonly_fields = 'at_home',
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
@admin.register(User)
|
|
@@ -102,7 +106,7 @@ class UserAdmin(OrgUserAdmin):
|
|
|
102
106
|
qs = super().get_queryset(request)
|
|
103
107
|
if request.user.is_master:
|
|
104
108
|
return qs
|
|
105
|
-
return qs.filter(
|
|
109
|
+
return qs.filter(instance_roles__instance=request.instance)
|
|
106
110
|
|
|
107
111
|
|
|
108
112
|
from django.contrib.auth.models import Group
|
|
@@ -127,11 +131,9 @@ class UserDeviceLogInline(admin.ModelAdmin):
|
|
|
127
131
|
|
|
128
132
|
def get_queryset(self, request):
|
|
129
133
|
qs = super().get_queryset(request)
|
|
130
|
-
if request.user.is_master:
|
|
131
|
-
return qs
|
|
132
134
|
return qs.filter(
|
|
133
|
-
|
|
134
|
-
)
|
|
135
|
+
user_device__users__roles__instance=request.instance
|
|
136
|
+
).distinct()
|
|
135
137
|
|
|
136
138
|
|
|
137
139
|
@admin.register(UserDevice)
|
|
@@ -144,9 +146,7 @@ class UserDeviceAdmin(admin.ModelAdmin):
|
|
|
144
146
|
|
|
145
147
|
def get_queryset(self, request):
|
|
146
148
|
qs = super().get_queryset(request)
|
|
147
|
-
|
|
148
|
-
return qs
|
|
149
|
-
return qs.filter(user__role__instance__in=request.user.instances)
|
|
149
|
+
return qs.filter(users__roles__instance=request.instance).distinct()
|
|
150
150
|
|
|
151
151
|
def users_display(self, obj):
|
|
152
152
|
return ', '.join([str(u) for u in obj.users.all()])
|
|
@@ -157,13 +157,12 @@ class UserDeviceAdmin(admin.ModelAdmin):
|
|
|
157
157
|
@admin.register(InstanceInvitation)
|
|
158
158
|
class InstanceInvitationAdmin(admin.ModelAdmin):
|
|
159
159
|
list_display = (
|
|
160
|
-
'token', '
|
|
160
|
+
'token', 'from_user', 'to_email', 'role',
|
|
161
161
|
'issue_date', 'taken_by', 'taken_date'
|
|
162
162
|
)
|
|
163
163
|
readonly_fields = (
|
|
164
164
|
'token', 'issue_date', 'from_user', 'taken_by', 'taken_date'
|
|
165
165
|
)
|
|
166
|
-
list_filter = 'instance',
|
|
167
166
|
|
|
168
167
|
actions = ['send', ]
|
|
169
168
|
|
|
@@ -171,7 +170,7 @@ class InstanceInvitationAdmin(admin.ModelAdmin):
|
|
|
171
170
|
qs = super().get_queryset(request)
|
|
172
171
|
if request.user.is_master:
|
|
173
172
|
return qs
|
|
174
|
-
return qs.filter(
|
|
173
|
+
return qs.filter(instance=request.instance)
|
|
175
174
|
|
|
176
175
|
def send(self, request, queryset):
|
|
177
176
|
invitations_sent = 0
|