simo 1.7.20__py3-none-any.whl → 2.0.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.
- simo/__pycache__/asgi.cpython-38.pyc +0 -0
- simo/__pycache__/settings.cpython-38.pyc +0 -0
- simo/__pycache__/urls.cpython-38.pyc +0 -0
- simo/__pycache__/wsgi.cpython-38.pyc +0 -0
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
- simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/core/__pycache__/autocomplete_views.cpython-38.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/core/__pycache__/context.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/events.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/core/__pycache__/managers.cpython-38.pyc +0 -0
- simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/__pycache__/views.cpython-38.pyc +0 -0
- simo/core/admin.py +28 -18
- simo/core/api.py +157 -16
- simo/core/api_meta.py +87 -0
- simo/core/auto_urls.py +4 -1
- simo/core/autocomplete_views.py +8 -4
- simo/core/base_types.py +1 -0
- simo/core/context.py +3 -1
- simo/core/controllers.py +134 -36
- simo/core/db_backend/base.py +7 -22
- simo/core/drf_braces/README +3 -0
- simo/core/drf_braces/__init__.py +7 -0
- simo/core/drf_braces/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/drf_braces/__pycache__/utils.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__init__.py +5 -0
- simo/core/drf_braces/fields/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/_fields.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/custom.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/mixins.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/modified.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/_fields.py +48 -0
- simo/core/drf_braces/fields/custom.py +107 -0
- simo/core/drf_braces/fields/mixins.py +58 -0
- simo/core/drf_braces/fields/modified.py +41 -0
- simo/core/drf_braces/forms/__init__.py +0 -0
- simo/core/drf_braces/forms/fields.py +20 -0
- simo/core/drf_braces/forms/serializer_form.py +156 -0
- simo/core/drf_braces/mixins.py +52 -0
- simo/core/drf_braces/models.py +0 -0
- simo/core/drf_braces/parsers.py +72 -0
- simo/core/drf_braces/renderers.py +37 -0
- simo/core/drf_braces/serializers/__init__.py +0 -0
- simo/core/drf_braces/serializers/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc +0 -0
- simo/core/drf_braces/serializers/enforce_validation_serializer.py +214 -0
- simo/core/drf_braces/serializers/form_serializer.py +391 -0
- simo/core/drf_braces/serializers/swapping.py +48 -0
- simo/core/drf_braces/tests/__init__.py +0 -0
- simo/core/drf_braces/tests/fields/__init__.py +0 -0
- simo/core/drf_braces/tests/fields/test_custom.py +94 -0
- simo/core/drf_braces/tests/fields/test_fields.py +13 -0
- simo/core/drf_braces/tests/fields/test_mixins.py +96 -0
- simo/core/drf_braces/tests/fields/test_modified.py +40 -0
- simo/core/drf_braces/tests/forms/__init__.py +0 -0
- simo/core/drf_braces/tests/forms/test_fields.py +46 -0
- simo/core/drf_braces/tests/forms/test_serializer_form.py +256 -0
- simo/core/drf_braces/tests/serializers/__init__.py +0 -0
- simo/core/drf_braces/tests/serializers/test_enforce_validation_serializer.py +169 -0
- simo/core/drf_braces/tests/serializers/test_form_serializer.py +387 -0
- simo/core/drf_braces/tests/serializers/test_swapping.py +40 -0
- simo/core/drf_braces/tests/test_mixins.py +111 -0
- simo/core/drf_braces/tests/test_parsers.py +73 -0
- simo/core/drf_braces/tests/test_renderers.py +23 -0
- simo/core/drf_braces/tests/test_utils.py +73 -0
- simo/core/drf_braces/utils.py +209 -0
- simo/core/events.py +3 -3
- simo/core/forms.py +79 -37
- simo/core/gateways.py +31 -14
- simo/core/management/commands/gateways_manager.py +0 -1
- simo/core/managers.py +81 -0
- simo/core/middleware.py +25 -0
- simo/core/migrations/0026_category_instance.py +20 -0
- simo/core/migrations/0027_remove_component_tags.py +17 -0
- simo/core/migrations/0028_rename_subcomponents_component_slaves.py +18 -0
- simo/core/migrations/0029_auto_20240229_1331.py +33 -0
- simo/core/migrations/__pycache__/0026_category_instance.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-38.pyc +0 -0
- simo/core/models.py +103 -66
- simo/core/permissions.py +28 -2
- simo/core/serializers.py +330 -26
- simo/core/socket_consumers.py +5 -14
- simo/core/tasks.py +11 -1
- simo/core/templates/admin/base.html +37 -10
- simo/core/templates/admin/wizard/discovery.html +188 -0
- simo/core/templates/admin/wizard/wizard_add.html +5 -5
- simo/core/utils/__pycache__/serialization.cpython-38.pyc +0 -0
- simo/core/utils/admin.py +9 -2
- simo/core/utils/formsets.py +17 -16
- simo/core/utils/helpers.py +1 -0
- simo/core/utils/serialization.py +56 -0
- simo/core/utils/type_constants.py +1 -1
- simo/core/utils/validators.py +14 -1
- simo/core/views.py +13 -0
- simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/managers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
- simo/fleet/admin.py +53 -29
- simo/fleet/api.py +59 -3
- simo/fleet/auto_urls.py +2 -3
- simo/fleet/controllers.py +199 -16
- simo/fleet/forms.py +325 -483
- simo/fleet/gateways.py +44 -2
- simo/fleet/managers.py +32 -0
- simo/fleet/migrations/0025_auto_20240130_1334.py +27 -0
- simo/fleet/migrations/0026_rename_i2cinterface_scl_pin_and_more.py +64 -0
- simo/fleet/migrations/0027_auto_20240306_0802.py +170 -0
- simo/fleet/migrations/0028_remove_i2cinterface_scl_pin_no_and_more.py +21 -0
- simo/fleet/migrations/0029_alter_i2cinterface_scl_pin_and_more.py +24 -0
- simo/fleet/migrations/0030_colonelpin_label_alter_colonel_type.py +24 -0
- simo/fleet/migrations/0031_alter_colonel_type.py +18 -0
- simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-38.pyc +0 -0
- simo/fleet/models.py +140 -82
- simo/fleet/serializers.py +35 -1
- simo/fleet/socket_consumers.py +239 -76
- simo/fleet/utils.py +15 -53
- simo/fleet/views.py +28 -14
- simo/generic/controllers.py +13 -89
- simo/generic/forms.py +29 -18
- simo/generic/gateways.py +73 -2
- simo/generic/models.py +3 -3
- simo/multimedia/controllers.py +9 -8
- simo/settings.py +7 -4
- simo/urls.py +4 -8
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/users/__pycache__/sso_urls.cpython-38.pyc +0 -0
- simo/users/admin.py +8 -1
- simo/users/api.py +38 -2
- simo/users/auto_urls.py +2 -2
- simo/users/migrations/0025_rename_name_fingerprint_type_and_more.py +22 -0
- simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-38.pyc +0 -0
- simo/users/models.py +2 -3
- simo/users/serializers.py +15 -1
- simo/users/sso_urls.py +3 -3
- simo/wsgi.py +7 -0
- {simo-1.7.20.dist-info → simo-2.0.1.dist-info}/METADATA +8 -9
- {simo-1.7.20.dist-info → simo-2.0.1.dist-info}/RECORD +173 -189
- {simo-1.7.20.dist-info → simo-2.0.1.dist-info}/WHEEL +1 -1
- simo/core/db_backend/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/db_backend/__pycache__/base.cpython-38.pyc +0 -0
- simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0002_load_icons.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0010_historyaggregate.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0011_component_last_change.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0012_instance.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0014_zone_instance.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0020_component_meta.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/templatetags/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/templatetags/__pycache__/components_list.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/config_values.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/easing.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/form_fields.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/form_widgets.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/logs.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/mixins.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/model_helpers.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/relay.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/validators.cpython-38.pyc +0 -0
- simo/fleet/tasks.py +0 -25
- simo/generic/__pycache__/__init__.cpython-37.pyc +0 -0
- simo/generic/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-37.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/__pycache__/models.cpython-38.pyc +0 -0
- simo/generic/__pycache__/routing.cpython-38.pyc +0 -0
- simo/generic/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/widgets.cpython-37.pyc +0 -0
- simo/multimedia/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/admin.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/api.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/forms.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/models.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/models.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
- simo/notifications/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-38.pyc +0 -0
- simo/notifications/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0002_componentpermission.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0004_user_secret_key.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0009_remove_user_role.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0014_user_roles.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- {simo-1.7.20.dist-info → simo-2.0.1.dist-info}/LICENSE.md +0 -0
- {simo-1.7.20.dist-info → simo-2.0.1.dist-info}/top_level.txt +0 -0
simo/core/serializers.py
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
from django import forms
|
|
5
|
+
from django.forms.utils import ErrorDict
|
|
6
|
+
from collections.abc import Iterable
|
|
2
7
|
from easy_thumbnails.files import get_thumbnailer
|
|
3
8
|
from simo.core.middleware import get_current_request
|
|
4
9
|
from rest_framework import serializers
|
|
5
|
-
from .
|
|
6
|
-
from .
|
|
10
|
+
from simo.core.forms import FormsetField
|
|
11
|
+
from rest_framework.relations import PrimaryKeyRelatedField, ManyRelatedField
|
|
12
|
+
from .drf_braces.serializers.form_serializer import (
|
|
13
|
+
FormSerializer, FormSerializerBase, reduce_attr_dict_from_instance,
|
|
14
|
+
FORM_SERIALIZER_FIELD_MAPPING, set_form_partial_validation
|
|
15
|
+
)
|
|
16
|
+
from .forms import ComponentAdminForm
|
|
17
|
+
from .models import Category, Zone, Icon, ComponentHistory
|
|
7
18
|
|
|
8
19
|
|
|
9
20
|
class TimestampField(serializers.Field):
|
|
@@ -13,6 +24,9 @@ class TimestampField(serializers.Field):
|
|
|
13
24
|
return value.timestamp()
|
|
14
25
|
return value
|
|
15
26
|
|
|
27
|
+
def to_internal_value(self, data):
|
|
28
|
+
return datetime.datetime.fromtimestamp(data)
|
|
29
|
+
|
|
16
30
|
|
|
17
31
|
class IconSerializer(serializers.ModelSerializer):
|
|
18
32
|
last_modified = TimestampField()
|
|
@@ -23,13 +37,15 @@ class IconSerializer(serializers.ModelSerializer):
|
|
|
23
37
|
|
|
24
38
|
|
|
25
39
|
class CategorySerializer(serializers.ModelSerializer):
|
|
26
|
-
|
|
40
|
+
header_image_thumb = serializers.SerializerMethodField()
|
|
27
41
|
|
|
28
42
|
class Meta:
|
|
29
43
|
model = Category
|
|
30
|
-
fields =
|
|
44
|
+
fields = (
|
|
45
|
+
'id', 'name', 'all', 'icon', 'header_image', 'header_image_thumb'
|
|
46
|
+
)
|
|
31
47
|
|
|
32
|
-
def
|
|
48
|
+
def get_header_image_thumb(self, obj):
|
|
33
49
|
if obj.header_image:
|
|
34
50
|
url = get_thumbnailer(obj.header_image).get_thumbnail(
|
|
35
51
|
{'size': (830, 430), 'crop': True}
|
|
@@ -44,25 +60,318 @@ class CategorySerializer(serializers.ModelSerializer):
|
|
|
44
60
|
return
|
|
45
61
|
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
|
|
64
|
+
class ObjectSerializerMethodField(serializers.SerializerMethodField):
|
|
65
|
+
|
|
66
|
+
def bind(self, field_name, parent):
|
|
67
|
+
self.field_name = field_name
|
|
68
|
+
super().bind(field_name, parent)
|
|
69
|
+
|
|
70
|
+
def to_representation(self, value):
|
|
71
|
+
return getattr(value, self.field_name)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FormsetPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
|
75
|
+
|
|
76
|
+
def get_attribute(self, instance):
|
|
77
|
+
return self.queryset.model.objects.filter(
|
|
78
|
+
pk=instance.get(self.source_attrs[0], -1)
|
|
79
|
+
).first()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# TODO: if form field has initial value and is required, it is serialized as not required field, howerver when trying to submit it fails with a message, that field is required.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ComponentFormsetField(FormSerializer):
|
|
88
|
+
|
|
89
|
+
class Meta:
|
|
90
|
+
# fake form, but it is necessary for FormSerializer
|
|
91
|
+
# we set it to proper formset form on __init__
|
|
92
|
+
form = forms.Form
|
|
93
|
+
field_mapping = {
|
|
94
|
+
forms.ModelChoiceField: FormsetPrimaryKeyRelatedField,
|
|
95
|
+
forms.TypedChoiceField: serializers.ChoiceField,
|
|
96
|
+
forms.FloatField: serializers.FloatField,
|
|
97
|
+
forms.SlugField: serializers.CharField
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def __init__(self, formset_field, *args, **kwargs):
|
|
101
|
+
self.Meta.form = formset_field.formset_cls.form
|
|
102
|
+
super().__init__(*args, **kwargs)
|
|
103
|
+
|
|
104
|
+
def get_fields(self):
|
|
105
|
+
ret = super(FormSerializerBase, self).get_fields()
|
|
106
|
+
|
|
107
|
+
field_mapping = reduce_attr_dict_from_instance(
|
|
108
|
+
self,
|
|
109
|
+
lambda i: getattr(getattr(i, 'Meta', None), 'field_mapping', {}),
|
|
110
|
+
FORM_SERIALIZER_FIELD_MAPPING
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
form = self.Meta.form
|
|
114
|
+
for field_name, form_field in getattr(form, 'all_base_fields', form.base_fields).items():
|
|
115
|
+
|
|
116
|
+
if field_name in getattr(self.Meta, 'exclude', []):
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if field_name in ret:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
cls_type = form_field.__class__
|
|
123
|
+
try:
|
|
124
|
+
serializer_field_class = field_mapping[cls_type]
|
|
125
|
+
except KeyError:
|
|
126
|
+
try:
|
|
127
|
+
serializer_field_class = field_mapping[cls_type.__bases__[0]]
|
|
128
|
+
except KeyError:
|
|
129
|
+
raise TypeError(
|
|
130
|
+
"{field} is not mapped to a serializer field. "
|
|
131
|
+
"Please add {field} to {serializer}.Meta.field_mapping. "
|
|
132
|
+
"Currently mapped fields: {mapped}".format(
|
|
133
|
+
field=form_field.__class__.__name__,
|
|
134
|
+
serializer=self.__class__.__name__,
|
|
135
|
+
mapped=', '.join(sorted([i.__name__ for i in field_mapping.keys()]))
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
ret[field_name] = self._get_field(form_field, serializer_field_class)
|
|
140
|
+
|
|
141
|
+
return ret
|
|
142
|
+
|
|
143
|
+
def _get_field_kwargs(self, form_field, serializer_field_class):
|
|
144
|
+
kwargs = super()._get_field_kwargs(form_field, serializer_field_class)
|
|
145
|
+
kwargs['style'] = {'form_field': form_field}
|
|
146
|
+
if serializer_field_class == FormsetPrimaryKeyRelatedField:
|
|
147
|
+
kwargs['queryset'] = form_field.queryset
|
|
148
|
+
return kwargs
|
|
149
|
+
|
|
150
|
+
def to_representation(self, instance):
|
|
151
|
+
return super(FormSerializerBase, self).to_representation(instance)
|
|
152
|
+
|
|
153
|
+
def get_form(self, data=None, **kwargs):
|
|
154
|
+
form = super().get_form(data=data, **kwargs)
|
|
155
|
+
form.prefix = ''
|
|
156
|
+
return form
|
|
157
|
+
|
|
158
|
+
def create(self, validated_data):
|
|
159
|
+
return validated_data
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ComponentPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
|
163
|
+
|
|
164
|
+
def get_attribute(self, instance):
|
|
165
|
+
if self.queryset.model in (Icon, Zone, Category):
|
|
166
|
+
return super().get_attribute(instance)
|
|
167
|
+
return self.queryset.model.objects.filter(
|
|
168
|
+
pk=instance.config.get(self.source_attrs[0])
|
|
169
|
+
).first()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ComponentManyToManyRelatedField(serializers.Field):
|
|
173
|
+
|
|
174
|
+
def __init__(self, *args, **kwargs):
|
|
175
|
+
self.queryset = kwargs.pop('queryset')
|
|
176
|
+
self.choices = {obj.pk: str(obj) for obj in self.queryset}
|
|
177
|
+
self.allow_blank = kwargs.pop('allow_blank', False)
|
|
178
|
+
super().__init__(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
def to_representation(self, value):
|
|
181
|
+
return [obj.pk for obj in value]
|
|
182
|
+
|
|
183
|
+
def to_internal_value(self, data):
|
|
184
|
+
if data == [] and self.allow_blank:
|
|
185
|
+
return []
|
|
186
|
+
return self.queryset.filter(pk__in=data)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ComponentSerializer(FormSerializer):
|
|
190
|
+
id = ObjectSerializerMethodField()
|
|
48
191
|
controller_methods = serializers.SerializerMethodField()
|
|
49
|
-
last_change = TimestampField()
|
|
192
|
+
last_change = TimestampField(read_only=True)
|
|
50
193
|
read_only = serializers.SerializerMethodField()
|
|
51
194
|
app_widget = serializers.SerializerMethodField()
|
|
52
|
-
|
|
195
|
+
slaves = serializers.SerializerMethodField()
|
|
196
|
+
base_type = ObjectSerializerMethodField()
|
|
197
|
+
controller_uid = ObjectSerializerMethodField()
|
|
198
|
+
alive = ObjectSerializerMethodField()
|
|
199
|
+
value = ObjectSerializerMethodField()
|
|
200
|
+
config = ObjectSerializerMethodField()
|
|
201
|
+
meta = ObjectSerializerMethodField()
|
|
202
|
+
arm_status = ObjectSerializerMethodField()
|
|
203
|
+
battery_level = ObjectSerializerMethodField()
|
|
53
204
|
|
|
54
205
|
class Meta:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
206
|
+
form = ComponentAdminForm
|
|
207
|
+
exclude = ('instance_methods', )
|
|
208
|
+
field_mapping = {
|
|
209
|
+
forms.TypedChoiceField: serializers.ChoiceField,
|
|
210
|
+
forms.FloatField: serializers.FloatField,
|
|
211
|
+
forms.SlugField: serializers.CharField,
|
|
212
|
+
forms.ModelChoiceField: ComponentPrimaryKeyRelatedField,
|
|
213
|
+
forms.ModelMultipleChoiceField: ComponentManyToManyRelatedField,
|
|
214
|
+
FormsetField: ComponentFormsetField,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
def get_fields(self):
|
|
218
|
+
self.set_form_cls()
|
|
219
|
+
|
|
220
|
+
ret = super(FormSerializerBase, self).get_fields()
|
|
221
|
+
|
|
222
|
+
field_mapping = reduce_attr_dict_from_instance(
|
|
223
|
+
self,
|
|
224
|
+
lambda i: getattr(getattr(i, 'Meta', None), 'field_mapping', {}),
|
|
225
|
+
FORM_SERIALIZER_FIELD_MAPPING
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if not self.instance or isinstance(self.instance, Iterable):
|
|
229
|
+
form = self.Meta.form()
|
|
230
|
+
else:
|
|
231
|
+
form = self.Meta.form(instance=self.instance)
|
|
232
|
+
for field_name in form.fields:
|
|
233
|
+
# if field is specified as excluded field
|
|
234
|
+
if field_name in getattr(self.Meta, 'exclude', []):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# if field is already defined via declared fields
|
|
238
|
+
# skip mapping it from forms which then honors
|
|
239
|
+
# the custom validation defined on the DRF declared field
|
|
240
|
+
if field_name in ret:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
form_field = form[field_name]
|
|
244
|
+
|
|
245
|
+
cls = form_field.field.__class__
|
|
246
|
+
try:
|
|
247
|
+
serializer_field_class = field_mapping[cls]
|
|
248
|
+
except KeyError:
|
|
249
|
+
cls = form_field.field.__class__.__bases__[0]
|
|
250
|
+
try:
|
|
251
|
+
serializer_field_class = field_mapping[cls]
|
|
252
|
+
except KeyError:
|
|
253
|
+
raise TypeError(
|
|
254
|
+
"{field} is not mapped to a serializer field. "
|
|
255
|
+
"Please add {field} to {serializer}.Meta.field_mapping. "
|
|
256
|
+
"Currently mapped fields: {mapped}".format(
|
|
257
|
+
field=form_field.field.__class__.__name__,
|
|
258
|
+
serializer=self.__class__.__name__,
|
|
259
|
+
mapped=', '.join(sorted([i.__name__ for i in field_mapping.keys()]))
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
ret[field_name] = self._get_field(
|
|
264
|
+
form_field.field, serializer_field_class
|
|
265
|
+
)
|
|
266
|
+
ret[field_name].initial = form_field.initial
|
|
267
|
+
ret[field_name].default = form_field.initial
|
|
268
|
+
|
|
269
|
+
return ret
|
|
270
|
+
|
|
271
|
+
def _get_field_kwargs(self, form_field, serializer_field_class):
|
|
272
|
+
kwargs = super()._get_field_kwargs(form_field, serializer_field_class)
|
|
273
|
+
kwargs['style'] = {'form_field': form_field}
|
|
274
|
+
if serializer_field_class == ComponentPrimaryKeyRelatedField:
|
|
275
|
+
kwargs['queryset'] = form_field.queryset
|
|
276
|
+
elif serializer_field_class == ComponentManyToManyRelatedField:
|
|
277
|
+
kwargs['queryset'] = form_field.queryset
|
|
278
|
+
elif serializer_field_class == ComponentFormsetField:
|
|
279
|
+
kwargs['formset_field'] = form_field
|
|
280
|
+
kwargs['many'] = True
|
|
281
|
+
|
|
282
|
+
return kwargs
|
|
283
|
+
|
|
284
|
+
def set_form_cls(self):
|
|
285
|
+
self.Meta.form = ComponentAdminForm
|
|
286
|
+
if not isinstance(self.instance, Iterable):
|
|
287
|
+
from .utils.type_constants import get_controller_types_map
|
|
288
|
+
controllers_map = get_controller_types_map()
|
|
289
|
+
if not self.instance:
|
|
290
|
+
controller = controllers_map.get(
|
|
291
|
+
#'simo.generic.controllers.AlarmClock'
|
|
292
|
+
self.context['request'].META.get('HTTP_CONTROLLER')
|
|
293
|
+
)
|
|
294
|
+
if controller:
|
|
295
|
+
self.Meta.form = controller.add_form
|
|
296
|
+
else:
|
|
297
|
+
controller = controllers_map.get(
|
|
298
|
+
self.instance.controller_uid
|
|
299
|
+
)
|
|
300
|
+
if controller:
|
|
301
|
+
self.Meta.form = controller.config_form
|
|
302
|
+
|
|
303
|
+
def get_form(self, data=None, **kwargs):
|
|
304
|
+
self.set_form_cls()
|
|
305
|
+
if not self.instance:
|
|
306
|
+
#controller_uid = 'simo.generic.controllers.AlarmClock'
|
|
307
|
+
controller_uid = self.context['request'].META.get('HTTP_CONTROLLER')
|
|
308
|
+
else:
|
|
309
|
+
controller_uid = self.instance.controller_uid
|
|
310
|
+
form = self.Meta.form(
|
|
311
|
+
data=data, request=self.context['request'],
|
|
312
|
+
controller_uid=controller_uid,
|
|
313
|
+
**kwargs
|
|
314
|
+
)
|
|
315
|
+
return form
|
|
316
|
+
|
|
317
|
+
def accomodate_formsets(self, form, data):
|
|
318
|
+
new_data = {}
|
|
319
|
+
field_types = {}
|
|
320
|
+
for field_name in form.fields:
|
|
321
|
+
field_types[field_name] = form[field_name]
|
|
322
|
+
for key, val in data.items():
|
|
323
|
+
if isinstance(field_types.get(key).field, FormsetField):
|
|
324
|
+
new_data[f'{key}-TOTAL_FORMS'] = len(val)
|
|
325
|
+
new_data[f'{key}-INITIAL_FORMS'] = len(val)
|
|
326
|
+
new_data[f'{key}-MIN_NUM_FORMS'] = 0
|
|
327
|
+
new_data[f'{key}-MAX_NUM_FORMS'] = len(val)
|
|
328
|
+
for i, item in enumerate(val):
|
|
329
|
+
for k, v in item.items():
|
|
330
|
+
new_data[f'{key}-{i}-{k}'] = v
|
|
331
|
+
else:
|
|
332
|
+
new_data[key] = val
|
|
333
|
+
return new_data
|
|
334
|
+
|
|
335
|
+
def validate(self, data):
|
|
336
|
+
if not self.instance:
|
|
337
|
+
try:
|
|
338
|
+
self.context['request'].META['HTTP_CONTROLLER']
|
|
339
|
+
except:
|
|
340
|
+
raise serializers.ValidationError(
|
|
341
|
+
["Controller header is not supplied!"]
|
|
342
|
+
)
|
|
343
|
+
form = self.get_form(instance=self.instance)
|
|
344
|
+
a_data = self.accomodate_formsets(form, data)
|
|
345
|
+
form = self.get_form(
|
|
346
|
+
data=a_data, instance=self.instance
|
|
347
|
+
)
|
|
348
|
+
if not form.is_valid():
|
|
349
|
+
raise serializers.ValidationError(form.errors)
|
|
350
|
+
return data
|
|
351
|
+
|
|
352
|
+
def to_representation(self, instance):
|
|
353
|
+
return super(FormSerializerBase, self).to_representation(instance)
|
|
354
|
+
|
|
355
|
+
def update(self, instance, validated_data):
|
|
356
|
+
form = self.get_form(instance=instance)
|
|
357
|
+
a_data = self.accomodate_formsets(form, validated_data)
|
|
358
|
+
form = self.get_form(instance=instance, data=a_data)
|
|
359
|
+
if form.is_valid():
|
|
360
|
+
instance = form.save(commit=True)
|
|
361
|
+
return instance
|
|
362
|
+
raise serializers.ValidationError(form.errors)
|
|
363
|
+
|
|
364
|
+
def create(self, validated_data):
|
|
365
|
+
form = self.get_form()
|
|
366
|
+
a_data = self.accomodate_formsets(form, validated_data)
|
|
367
|
+
form = self.get_form(data=a_data)
|
|
368
|
+
if form.is_valid():
|
|
369
|
+
if form.controller.is_discoverable:
|
|
370
|
+
form.controller.init_discovery(form.cleaned_data)
|
|
371
|
+
return {'discovery': 'started'}
|
|
372
|
+
instance = form.save(commit=True)
|
|
373
|
+
return instance
|
|
374
|
+
raise serializers.ValidationError(form.errors)
|
|
66
375
|
|
|
67
376
|
def get_controller_methods(self, obj):
|
|
68
377
|
c_methods = [m[0] for m in inspect.getmembers(
|
|
@@ -92,13 +401,8 @@ class ComponentSerializer(serializers.ModelSerializer):
|
|
|
92
401
|
return {}
|
|
93
402
|
return {'type': app_widget.uid, 'size': app_widget.size}
|
|
94
403
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
return ComponentSerializer(
|
|
98
|
-
obj.subcomponents.all(), many=True, context={
|
|
99
|
-
'user': get_system_user()
|
|
100
|
-
}
|
|
101
|
-
).data
|
|
404
|
+
def get_slaves(self, obj):
|
|
405
|
+
return [c['id'] for c in obj.slaves.all().values('id')]
|
|
102
406
|
|
|
103
407
|
|
|
104
408
|
class ZoneSerializer(serializers.ModelSerializer):
|
simo/core/socket_consumers.py
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import time
|
|
3
|
-
import pytz
|
|
4
2
|
import asyncio
|
|
5
|
-
import os
|
|
6
|
-
import logging
|
|
7
3
|
from ansi2html import Ansi2HTMLConverter
|
|
8
4
|
from asgiref.sync import sync_to_async
|
|
9
|
-
from django.urls import set_script_prefix
|
|
10
|
-
from django.core.exceptions import ValidationError
|
|
11
5
|
from django.template.loader import render_to_string
|
|
12
6
|
from django.conf import settings
|
|
13
|
-
from django.utils import timezone
|
|
14
7
|
from django.contrib.contenttypes.models import ContentType
|
|
15
8
|
from channels.generic.websocket import AsyncWebsocketConsumer, WebsocketConsumer
|
|
16
9
|
from simo.core.events import ObjectChangeEvent, get_event_obj
|
|
17
10
|
import paho.mqtt.client as mqtt
|
|
18
11
|
from simo.users.middleware import introduce
|
|
19
|
-
from simo.core.api import get_components_queryset
|
|
20
12
|
from simo.core.models import Component, Gateway
|
|
21
|
-
from simo.core.serializers import ComponentSerializer
|
|
22
|
-
from simo.conf import dynamic_settings
|
|
23
|
-
from simo.users.models import User, PermissionsRole
|
|
24
|
-
from simo.users.serializers import UserSerializer
|
|
25
|
-
from simo.users.api import UsersViewSet
|
|
26
13
|
from simo.core.utils.model_helpers import get_log_file_path
|
|
14
|
+
from simo.core.middleware import introduce_instance
|
|
27
15
|
|
|
28
16
|
|
|
29
17
|
class SIMOWebsocketConsumer(WebsocketConsumer):
|
|
@@ -220,6 +208,8 @@ class ComponentController(SIMOWebsocketConsumer):
|
|
|
220
208
|
if not self.scope['user'].is_active:
|
|
221
209
|
return self.close()
|
|
222
210
|
|
|
211
|
+
introduce_instance(self.component.zone.instance)
|
|
212
|
+
|
|
223
213
|
self._mqtt_client = mqtt.Client()
|
|
224
214
|
self._mqtt_client.username_pw_set('root', settings.SECRET_KEY)
|
|
225
215
|
self._mqtt_client.on_connect = self._on_mqtt_connect
|
|
@@ -228,7 +218,6 @@ class ComponentController(SIMOWebsocketConsumer):
|
|
|
228
218
|
port=settings.MQTT_PORT)
|
|
229
219
|
self._mqtt_client.loop_start()
|
|
230
220
|
|
|
231
|
-
|
|
232
221
|
def _on_mqtt_connect(self, mqtt_client, userdata, flags, rc):
|
|
233
222
|
print("Subscribing to ComponentEvent's")
|
|
234
223
|
event = ObjectChangeEvent(self.component.zone.instance, self.component)
|
|
@@ -238,6 +227,7 @@ class ComponentController(SIMOWebsocketConsumer):
|
|
|
238
227
|
payload = json.loads(msg.payload)
|
|
239
228
|
component = get_event_obj(payload, Component)
|
|
240
229
|
if component == self.component:
|
|
230
|
+
introduce_instance(self.component.zone.instance)
|
|
241
231
|
# print("Object changed [%s], %s" % (str(component), payload))
|
|
242
232
|
self.component = component
|
|
243
233
|
if self.send_value:
|
|
@@ -254,6 +244,7 @@ class ComponentController(SIMOWebsocketConsumer):
|
|
|
254
244
|
|
|
255
245
|
def receive(self, text_data=None, bytes_data=None, **kwargs):
|
|
256
246
|
introduce(self.scope['user'])
|
|
247
|
+
introduce_instance(self.component.zone.instance)
|
|
257
248
|
json_data = json.loads(text_data)
|
|
258
249
|
self.send_value = json_data.pop('send_value', False)
|
|
259
250
|
for method, param in json_data.items():
|
simo/core/tasks.py
CHANGED
|
@@ -192,7 +192,7 @@ def sync_with_remote():
|
|
|
192
192
|
).first()
|
|
193
193
|
if weather_component:
|
|
194
194
|
weather_component.track_history = False
|
|
195
|
-
weather_component.set(data['weather_forecast'])
|
|
195
|
+
weather_component.controller.set(data['weather_forecast'])
|
|
196
196
|
|
|
197
197
|
instance.save()
|
|
198
198
|
|
|
@@ -313,6 +313,16 @@ def drop_fingerprints_learn():
|
|
|
313
313
|
)
|
|
314
314
|
|
|
315
315
|
|
|
316
|
+
@celery_app.task
|
|
317
|
+
def time_out_discoveries():
|
|
318
|
+
from .models import Gateway
|
|
319
|
+
for gw in Gateway.objects.filter(
|
|
320
|
+
discovery__has_key='start'
|
|
321
|
+
).exclude(discovery__has_key='finished'):
|
|
322
|
+
if time.time() - gw.discovery['start'] > gw.discovery['timeout']:
|
|
323
|
+
gw.finish_discovery()
|
|
324
|
+
|
|
325
|
+
|
|
316
326
|
@celery_app.on_after_finalize.connect
|
|
317
327
|
def setup_periodic_tasks(sender, **kwargs):
|
|
318
328
|
sender.add_periodic_task(1, watch_timers.s())
|
|
@@ -81,28 +81,50 @@
|
|
|
81
81
|
<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}"
|
|
82
82
|
data-admin-utc-offset="{% now "Z" %}">
|
|
83
83
|
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
#instance-select{
|
|
87
|
+
background: none;
|
|
88
|
+
color: #f3ff4a;
|
|
89
|
+
font-family: 'Russo One';
|
|
90
|
+
font-size: 1rem;
|
|
91
|
+
border: none;
|
|
92
|
+
}
|
|
93
|
+
#instance-select option{
|
|
94
|
+
background-color: #08172a;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
84
97
|
<!-- Container -->
|
|
85
98
|
<div id="container">
|
|
86
99
|
|
|
87
100
|
{% if not is_popup %}
|
|
88
101
|
<!-- Header -->
|
|
89
|
-
<div id="header">
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
|
|
102
|
+
<div id="header" style="display:flex">
|
|
103
|
+
<div style="display:flex">
|
|
104
|
+
<a href="/admin/" >
|
|
105
|
+
<div id="branding" style="background-image: url({% static 'logo/logo_color_bg_b.svg' %})"></div>
|
|
106
|
+
</a>
|
|
93
107
|
<div style="
|
|
94
108
|
color: white;
|
|
95
109
|
font-size: 16px;
|
|
96
|
-
padding:
|
|
110
|
+
padding: 7px 0px;
|
|
97
111
|
margin-top: 4px;
|
|
98
|
-
display: inline-block;
|
|
99
112
|
">
|
|
100
113
|
<span style="display: inline-block; margin-right: 6px">|</span>
|
|
101
|
-
<
|
|
102
|
-
{%
|
|
103
|
-
|
|
114
|
+
<select id="instance-select">
|
|
115
|
+
{% for instance in instances %}
|
|
116
|
+
<option
|
|
117
|
+
data-set_url="{% url 'set-instance' instance_slug=instance.slug %}"
|
|
118
|
+
{% if instance == current_instance %} selected {% endif %}
|
|
119
|
+
>{{ instance }}</option>
|
|
120
|
+
{% endfor %}
|
|
121
|
+
</select>
|
|
122
|
+
<!-- <span style="display: inline-block; font-family: 'Russo One'; color: #f3ff4a;">-->
|
|
123
|
+
<!-- {% if instances|length == 1 %}{{ instances.0.name }}{% else %}Hub Admin{% endif %}-->
|
|
124
|
+
<!-- </span>-->
|
|
104
125
|
</div>
|
|
105
|
-
</
|
|
126
|
+
</div>
|
|
127
|
+
|
|
106
128
|
{% block usertools %}
|
|
107
129
|
{% include 'admin/user_tools.html' %}
|
|
108
130
|
{% endblock %}
|
|
@@ -146,5 +168,10 @@
|
|
|
146
168
|
<script src="{% static 'third_party/precision-inputs/precision-inputs.fl-controls.js' %}"></script>
|
|
147
169
|
<script src="{% static 'admin/js/admin_scripts.js' %}"></script>
|
|
148
170
|
|
|
171
|
+
<script>
|
|
172
|
+
$('#instance-select').on('change', function(){
|
|
173
|
+
window.location = $(this).find('option:selected').data('set_url');
|
|
174
|
+
})
|
|
175
|
+
</script>
|
|
149
176
|
</body>
|
|
150
177
|
</html>
|