simo 2.0.41__py3-none-any.whl → 2.1.0__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 (94) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
  3. simo/__pycache__/settings.cpython-38.pyc +0 -0
  4. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  5. simo/core/__init__.py +1 -0
  6. simo/core/__pycache__/__init__.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/apps.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/managers.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  21. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  22. simo/core/admin.py +28 -10
  23. simo/core/api.py +24 -5
  24. simo/core/api_meta.py +23 -13
  25. simo/core/app_widgets.py +6 -0
  26. simo/core/apps.py +10 -0
  27. simo/core/base_types.py +1 -0
  28. simo/core/controllers.py +57 -0
  29. simo/core/form_fields.py +93 -0
  30. simo/core/forms.py +15 -3
  31. simo/core/gateways.py +1 -1
  32. simo/core/managers.py +14 -1
  33. simo/core/migrations/0037_auto_20240606_1057.py +33 -0
  34. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
  35. simo/core/models.py +28 -9
  36. simo/core/permissions.py +6 -3
  37. simo/core/serializers.py +77 -5
  38. simo/core/signal_receivers.py +25 -0
  39. simo/core/static/admin/css/simo.css +14 -0
  40. simo/core/templates/admin/controller_widgets/button.html +8 -0
  41. simo/core/templates/admin/core/component_change_form.html +97 -0
  42. simo/core/templates/admin/formset_widget.html +88 -118
  43. simo/core/templates/admin/formset_widget_old.html +122 -0
  44. simo/core/templates/admin/wizard/wizard_add.html +16 -9
  45. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  46. simo/core/utils/__pycache__/cache.cpython-38.pyc +0 -0
  47. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  48. simo/core/utils/admin.py +11 -0
  49. simo/core/utils/cache.py +15 -0
  50. simo/core/utils/formsets.py +11 -18
  51. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  52. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  53. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  54. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  55. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  56. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  57. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  58. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  59. simo/fleet/auto_urls.py +7 -1
  60. simo/fleet/controllers.py +193 -30
  61. simo/fleet/forms.py +223 -87
  62. simo/fleet/gateways.py +53 -2
  63. simo/fleet/migrations/0036_auto_20240605_0702.py +68 -0
  64. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
  65. simo/fleet/models.py +35 -6
  66. simo/fleet/socket_consumers.py +1 -1
  67. simo/fleet/templates/fleet/controllers_info/button.md +16 -0
  68. simo/fleet/utils.py +31 -1
  69. simo/fleet/views.py +45 -0
  70. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  71. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  72. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  73. simo/generic/controllers.py +59 -14
  74. simo/generic/forms.py +4 -3
  75. simo/generic/gateways.py +2 -0
  76. simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
  77. simo/generic/templates/generic/controllers_info/dummy.md +3 -0
  78. simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
  79. simo/settings.py +20 -4
  80. simo/users/__init__.py +1 -0
  81. simo/users/__pycache__/__init__.cpython-38.pyc +0 -0
  82. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  83. simo/users/__pycache__/apps.cpython-38.pyc +0 -0
  84. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  85. simo/users/apps.py +9 -0
  86. simo/users/migrations/__pycache__/0029_alter_instanceuser_options_instanceuser_order.cpython-38.pyc +0 -0
  87. simo/users/migrations/__pycache__/0030_alter_instanceuser_options_remove_instanceuser_order.cpython-38.pyc +0 -0
  88. simo/users/models.py +16 -3
  89. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/METADATA +5 -3
  90. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/RECORD +93 -74
  91. simo/wsgi.py +0 -7
  92. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/LICENSE.md +0 -0
  93. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/WHEEL +0 -0
  94. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,33 @@
1
+ # Generated by Django 4.2.10 on 2024-06-06 10:57
2
+
3
+ from django.db import migrations
4
+
5
+ def forwards_func(apps, schema_editor):
6
+
7
+ Component = apps.get_model("core", "Component")
8
+ Gateway = apps.get_model('core', "Gateway")
9
+
10
+ generic_gateway = Gateway.objects.filter(
11
+ type='simo.generic.gateways.GenericGatewayHandler'
12
+ ).first()
13
+ if not generic_gateway:
14
+ return
15
+
16
+ Component.objects.filter(
17
+ controller_uid='simo.generic.controllers.StateSelect'
18
+ ).update(gateway=generic_gateway)
19
+
20
+
21
+ def reverse_func(apps, schema_editor):
22
+ pass
23
+
24
+
25
+ class Migration(migrations.Migration):
26
+
27
+ dependencies = [
28
+ ('core', '0036_auto_20240521_0823'),
29
+ ]
30
+
31
+ operations = [
32
+ migrations.RunPython(forwards_func, reverse_func, elidable=True),
33
+ ]
simo/core/models.py CHANGED
@@ -12,6 +12,7 @@ from timezone_utils.choices import ALL_TIMEZONES_CHOICES
12
12
  from location_field.models.plain import PlainLocationField
13
13
  from model_utils import FieldTracker
14
14
  from dirtyfields import DirtyFieldsMixin
15
+ from actstream import action
15
16
  from simo.core.utils.mixins import SimoAdminMixin
16
17
  from simo.core.storage import OverwriteStorage
17
18
  from simo.core.utils.validators import validate_svg
@@ -399,6 +400,8 @@ def is_in_alarm(self):
399
400
 
400
401
  controller_cls = None
401
402
 
403
+ _controller_initiated = False
404
+
402
405
  _mqtt_client = None
403
406
  _on_change_function = None
404
407
  _obj_ct_id = 0
@@ -408,15 +411,21 @@ def is_in_alarm(self):
408
411
  verbose_name_plural = _("Components")
409
412
  ordering = 'zone', 'base_type', 'name'
410
413
 
411
- def __init__(self, *args, **kwargs):
412
- super().__init__(*args, **kwargs)
413
- self.prepare_controller()
414
-
415
414
  def __str__(self):
416
415
  if self.zone:
417
416
  return '%s | %s' % (self.zone.name, self.name)
418
417
  return self.name
419
418
 
419
+ def __getattribute__(self, attr):
420
+ try:
421
+ return super().__getattribute__(attr)
422
+ except Exception as e:
423
+ if not attr.startswith('_') and not self._controller_initiated:
424
+ self._controller_initiated = True
425
+ self.prepare_controller()
426
+ return super().__getattribute__(attr)
427
+ raise e
428
+
420
429
  @cached_property
421
430
  def controller(self):
422
431
  from .utils.type_constants import (
@@ -425,13 +434,13 @@ def is_in_alarm(self):
425
434
  )
426
435
  self._meta.get_field('controller_uid').choices = CONTROLLER_TYPES_CHOICES
427
436
  if self.controller_uid:
428
- controller_cls = None
429
- if not controller_cls:
430
- controller_cls = CONTROLLERS_BY_GATEWAY.get(
437
+ self.controller_cls = None
438
+ if not self.controller_cls:
439
+ self.controller_cls = CONTROLLERS_BY_GATEWAY.get(
431
440
  self.gateway.type, {}
432
441
  ).get(self.controller_uid)
433
- if controller_cls:
434
- return controller_cls(self)
442
+ if self.controller_cls:
443
+ return self.controller_cls(self)
435
444
 
436
445
  def prepare_controller(self):
437
446
  if self.controller:
@@ -483,6 +492,11 @@ def is_in_alarm(self):
483
492
  component=self, type='value', value=self.value,
484
493
  user=actor
485
494
  )
495
+ action.send(
496
+ actor, target=self, verb="value change",
497
+ instance_id=self.zone.instance.id,
498
+ action_type='comp_value', value=self.value
499
+ )
486
500
  action_performed = True
487
501
  self.last_change = timezone.now()
488
502
  if 'arm_status' in dirty_fields:
@@ -490,6 +504,11 @@ def is_in_alarm(self):
490
504
  component=self, type='security',
491
505
  value=self.arm_status, user=actor
492
506
  )
507
+ action.send(
508
+ actor, target=self, verb="security event",
509
+ instance_id=self.zone.instance.id,
510
+ action_type='security', value=self.value
511
+ )
493
512
  action_performed = True
494
513
  self.last_change = timezone.now()
495
514
  if action_performed:
simo/core/permissions.py CHANGED
@@ -10,9 +10,12 @@ class InstancePermission(BasePermission):
10
10
  if not request.user.is_active:
11
11
  return False
12
12
 
13
- instance = Instance.objects.filter(
14
- slug=request.resolver_match.kwargs.get('instance_slug')
15
- ).first()
13
+ instance = getattr(view, 'instance', None)
14
+ if not instance:
15
+ instance = Instance.objects.filter(
16
+ slug=request.resolver_match.kwargs.get('instance_slug')
17
+ ).first()
18
+
16
19
  if not instance:
17
20
  raise Http404()
18
21
 
simo/core/serializers.py CHANGED
@@ -1,9 +1,8 @@
1
1
  import inspect
2
2
  import datetime
3
- import json
3
+ import re
4
4
  from django import forms
5
5
  from collections import OrderedDict
6
- from django.forms.utils import ErrorDict
7
6
  from django.conf import settings
8
7
  from collections.abc import Iterable
9
8
  from easy_thumbnails.files import get_thumbnailer
@@ -11,7 +10,13 @@ from simo.core.middleware import get_current_request
11
10
  from rest_framework import serializers
12
11
  from rest_framework.fields import SkipField
13
12
  from rest_framework.relations import Hyperlink, PKOnlyObject
13
+ from actstream.models import Action
14
14
  from simo.core.forms import HiddenField, FormsetField
15
+ from simo.core.form_fields import (
16
+ Select2ListChoiceField, Select2ListChoiceField,
17
+ Select2ModelChoiceField, Select2ListMultipleChoiceField
18
+ )
19
+ from simo.core.models import Component
15
20
  from rest_framework.relations import PrimaryKeyRelatedField, ManyRelatedField
16
21
  from .drf_braces.serializers.form_serializer import (
17
22
  FormSerializer, FormSerializerBase, reduce_attr_dict_from_instance,
@@ -106,6 +111,7 @@ class ComponentFormsetField(FormSerializer):
106
111
  form = forms.Form
107
112
  field_mapping = {
108
113
  HiddenField: HiddenSerializerField,
114
+ Select2ListChoiceField: serializers.ChoiceField,
109
115
  forms.ModelChoiceField: FormsetPrimaryKeyRelatedField,
110
116
  forms.TypedChoiceField: serializers.ChoiceField,
111
117
  forms.FloatField: serializers.FloatField,
@@ -249,6 +255,9 @@ class ComponentSerializer(FormSerializer):
249
255
  arm_status = ObjectSerializerMethodField()
250
256
  battery_level = ObjectSerializerMethodField()
251
257
  controller_methods = serializers.SerializerMethodField()
258
+ info = serializers.SerializerMethodField()
259
+
260
+ _forms = {}
252
261
 
253
262
  class Meta:
254
263
  form = ComponentAdminForm
@@ -258,11 +267,27 @@ class ComponentSerializer(FormSerializer):
258
267
  forms.FloatField: serializers.FloatField,
259
268
  forms.SlugField: serializers.CharField,
260
269
  forms.ModelChoiceField: ComponentPrimaryKeyRelatedField,
270
+ Select2ModelChoiceField: ComponentPrimaryKeyRelatedField,
261
271
  forms.ModelMultipleChoiceField: ComponentManyToManyRelatedField,
272
+ Select2ListMultipleChoiceField: ComponentManyToManyRelatedField,
262
273
  FormsetField: ComponentFormsetField,
263
274
  }
264
275
 
276
+
277
+
278
+ def __init__(self, *args, **kwargs):
279
+ super().__init__(*args, **kwargs)
280
+ # Set proper instance for OPTIONS request
281
+ if not self.instance:
282
+ res = re.findall(
283
+ r'.*\/core\/components\/(?P<component_id>[0-9]+)\/',
284
+ self.context['request'].path
285
+ )
286
+ if res:
287
+ self.instance = Component.objects.filter(id=res[0]).first()
288
+
265
289
  def get_fields(self):
290
+
266
291
  self.set_form_cls()
267
292
 
268
293
  ret = OrderedDict()
@@ -322,6 +347,7 @@ class ComponentSerializer(FormSerializer):
322
347
  if name in ret:
323
348
  continue
324
349
  ret[name] = field
350
+
325
351
  return ret
326
352
 
327
353
  def _get_field_kwargs(self, form_field, serializer_field_class):
@@ -359,7 +385,15 @@ class ComponentSerializer(FormSerializer):
359
385
  if controller:
360
386
  self.Meta.form = controller.config_form
361
387
 
362
- def get_form(self, data=None, **kwargs):
388
+ def get_form(self, data=None, instance=None, **kwargs):
389
+ form_key = None
390
+ if not data:
391
+ form_key = 0
392
+ if instance:
393
+ form_key = instance.id
394
+ if form_key in self._forms:
395
+ return self._forms[form_key]
396
+
363
397
  self.set_form_cls()
364
398
  if not self.instance or isinstance(self.instance, Iterable):
365
399
  #controller_uid = 'simo.generic.controllers.AlarmClock'
@@ -368,7 +402,7 @@ class ComponentSerializer(FormSerializer):
368
402
  controller_uid = self.instance.controller_uid
369
403
  form = self.Meta.form(
370
404
  data=data, request=self.context['request'],
371
- controller_uid=controller_uid,
405
+ controller_uid=controller_uid, instance=instance,
372
406
  **kwargs
373
407
  )
374
408
  if not self.context['request'].user.is_master:
@@ -380,6 +414,10 @@ class ComponentSerializer(FormSerializer):
380
414
  if field_name not in form.basic_fields:
381
415
  print("DELETE FIELD: ", field_name)
382
416
  del form.fields[field_name]
417
+
418
+ if form_key is not None:
419
+ self._forms[form_key] = form
420
+
383
421
  return form
384
422
 
385
423
  def accomodate_formsets(self, form, data):
@@ -426,7 +464,6 @@ class ComponentSerializer(FormSerializer):
426
464
  a_data = self.accomodate_formsets(form, validated_data)
427
465
  form = self.get_form(instance=instance, data=a_data)
428
466
  if form.is_valid():
429
- print("FORM FIELDS", form.fields)
430
467
  instance = form.save(commit=True)
431
468
  return instance
432
469
  raise serializers.ValidationError(form.errors)
@@ -450,6 +487,10 @@ class ComponentSerializer(FormSerializer):
450
487
  c_methods.extend(['arm', 'disarm'])
451
488
  return c_methods
452
489
 
490
+ def get_info(self, obj):
491
+ if obj.controller:
492
+ return obj.controller.info()
493
+
453
494
  def get_read_only(self, obj):
454
495
  user = self.context.get('user')
455
496
  if not user:
@@ -507,3 +548,34 @@ class ComponentHistorySerializer(serializers.ModelSerializer):
507
548
  class Meta:
508
549
  model = ComponentHistory
509
550
  fields = '__all__'
551
+
552
+
553
+ class ActionSerializer(serializers.ModelSerializer):
554
+ timestamp = TimestampField()
555
+ actor = serializers.SerializerMethodField()
556
+ target = serializers.SerializerMethodField()
557
+ action_type = serializers.SerializerMethodField()
558
+ value = serializers.SerializerMethodField()
559
+
560
+ class Meta:
561
+ model = Action
562
+ fields = (
563
+ 'id', 'timestamp', 'actor', 'target', 'verb',
564
+ 'action_type', 'value'
565
+ )
566
+
567
+ def get_actor(self, obj):
568
+ if obj.actor:
569
+ return str(obj.actor)
570
+
571
+ def get_target(self, obj):
572
+ if obj.target:
573
+ return str(obj.target)
574
+
575
+
576
+ def get_action_type(self, obj):
577
+ return obj.data.get('action_type')
578
+
579
+ def get_value(self, obj):
580
+ return obj.data.get('value')
581
+
@@ -5,6 +5,7 @@ from django.db.models.signals import post_save, post_delete
5
5
  from django.dispatch import receiver
6
6
  from django.utils import timezone
7
7
  from django.conf import settings
8
+ from actstream import action
8
9
  from simo.users.models import PermissionsRole
9
10
  from .models import Instance, Gateway, Component, Icon, Zone, Category
10
11
 
@@ -14,6 +15,14 @@ def create_instance_defaults(sender, instance, created, **kwargs):
14
15
  if not created:
15
16
  return
16
17
 
18
+ from simo.users.middleware import get_current_user
19
+ actor = get_current_user()
20
+ action.send(
21
+ actor, target=instance, verb="instance created",
22
+ instance_id=instance.id,
23
+ action_type='management_event'
24
+ )
25
+
17
26
  # Create default zones
18
27
 
19
28
  for zone_name in (
@@ -86,6 +95,22 @@ def create_instance_defaults(sender, instance, created, **kwargs):
86
95
  )
87
96
 
88
97
 
98
+ @receiver(post_save, sender=Zone)
99
+ @receiver(post_save, sender=Category)
100
+ def post_save_actions_dispatcher(sender, instance, created, **kwargs):
101
+ from simo.users.middleware import get_current_user
102
+ actor = get_current_user()
103
+ if created:
104
+ verb = 'created'
105
+ else:
106
+ verb = 'modified'
107
+ action.send(
108
+ actor, target=instance, verb=verb,
109
+ instance_id=instance.instance.id,
110
+ action_type='management_event'
111
+ )
112
+
113
+
89
114
  @receiver(post_save, sender=Component)
90
115
  @receiver(post_save, sender=Gateway)
91
116
  def post_save_change_events(sender, instance, created, **kwargs):
@@ -363,3 +363,17 @@ body .submit-row a.deletelink{
363
363
  }
364
364
  }
365
365
 
366
+ .markdownified-info{
367
+ padding: 15px;
368
+ background-color: #cfefff;
369
+ }
370
+
371
+ form .aligned .markdownified-info ul{
372
+ margin-left: 30px;
373
+ }
374
+ form .aligned .markdownified-info ul li {
375
+ list-style-type: square;
376
+ }
377
+ .markdownified-info hr {
378
+ background-color: #9f9f9f;
379
+ }
@@ -0,0 +1,8 @@
1
+ <div class="component-controller"
2
+ data-ws_url="{{ obj.get_socket_url|default_if_none:"" }}">
3
+ {% if obj.is_down %}
4
+ <i class="fas fa-rectangle-landscape" style="color: #1b6082;"></i>
5
+ {% else %}
6
+ <i class="far fa-rectangle-landscape" style="color: #a7a7a7;"></i>
7
+ {% endif %}
8
+ </div>
@@ -0,0 +1,97 @@
1
+ {% extends "admin/base_site.html" %}
2
+ {% load i18n admin_urls static admin_modify markdownify %}
3
+
4
+ {% block extrahead %}{{ block.super }}
5
+ <script src="{% url 'admin:jsi18n' %}"></script>
6
+ {{ media }}
7
+ {% endblock %}
8
+
9
+ {% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
10
+
11
+ {% block coltype %}colM{% endblock %}
12
+
13
+ {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %}
14
+
15
+ {% if not is_popup %}
16
+ {% block breadcrumbs %}
17
+ <div class="breadcrumbs">
18
+ <a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
19
+ &rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
20
+ &rsaquo; {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
21
+ &rsaquo; {% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}
22
+ </div>
23
+ {% endblock %}
24
+ {% endif %}
25
+
26
+ {% block content %}<div id="content-main">
27
+ {% block object-tools %}
28
+ {% if change and not is_popup %}
29
+ <ul class="object-tools">
30
+ {% block object-tools-items %}
31
+ {% change_form_object_tools %}
32
+ {% endblock %}
33
+ </ul>
34
+ {% endif %}
35
+ {% endblock %}
36
+ <form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" novalidate>{% csrf_token %}{% block form_top %}{% endblock %}
37
+ <div>
38
+ {% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
39
+ {% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
40
+ {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
41
+ {% if errors %}
42
+ <p class="errornote">
43
+ {% blocktranslate count counter=errors|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %}
44
+ </p>
45
+ {{ adminform.form.non_field_errors }}
46
+ {% endif %}
47
+
48
+ {% block field_sets %}
49
+ {% for fieldset in adminform %}
50
+ {% if forloop.first %}
51
+ <div style="display: flex">
52
+ <div style="flex-basis: 70%; margin-right: 15px;">
53
+ {% include "admin/includes/fieldset.html" %}
54
+ </div>
55
+ <div class="module" style="flex-basis: 30%; margin-left: 15px;">
56
+ <h2 style="background: linear-gradient(0.3turn, #5c7ca5, #5a95df);">Info</h2>
57
+ <div class="form-row">
58
+ {% if original.controller %}
59
+ {{ original.info|markdownify }}
60
+ {% endif %}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ {% else %}
65
+
66
+ {% endif %}
67
+ {% endfor %}
68
+ {% endblock %}
69
+
70
+ {% block after_field_sets %}{% endblock %}
71
+
72
+ {% block inline_field_sets %}
73
+ {% for inline_admin_formset in inline_admin_formsets %}
74
+ {% include inline_admin_formset.opts.template %}
75
+ {% endfor %}
76
+ {% endblock %}
77
+
78
+ {% block after_related_objects %}{% endblock %}
79
+
80
+ {% block submit_buttons_bottom %}{% submit_row %}{% endblock %}
81
+
82
+ {% block admin_change_form_document_ready %}
83
+ <script id="django-admin-form-add-constants"
84
+ src="{% static 'admin/js/change_form.js' %}"
85
+ {% if adminform and add %}
86
+ data-model-name="{{ opts.model_name }}"
87
+ {% endif %}
88
+ async>
89
+ </script>
90
+ {% endblock %}
91
+
92
+ {# JavaScript for prepopulated fields #}
93
+ {% prepopulated_fields_js %}
94
+
95
+ </div>
96
+ </form></div>
97
+ {% endblock %}