simo 2.0.42__py3-none-any.whl → 2.1.2__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 (124) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/settings.cpython-38.pyc +0 -0
  3. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  4. simo/asgi.py +1 -1
  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__/auto_urls.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/managers.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  21. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  22. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  23. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  24. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  25. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  26. simo/core/admin.py +26 -26
  27. simo/core/api.py +22 -2
  28. simo/core/api_meta.py +23 -13
  29. simo/core/app_widgets.py +6 -0
  30. simo/core/apps.py +13 -0
  31. simo/core/auto_urls.py +2 -3
  32. simo/core/base_types.py +1 -0
  33. simo/core/controllers.py +57 -0
  34. simo/core/dynamic_settings.py +0 -8
  35. simo/core/form_fields.py +93 -0
  36. simo/core/forms.py +16 -101
  37. simo/core/gateways.py +1 -1
  38. simo/core/managers.py +14 -1
  39. simo/core/migrations/0037_auto_20240606_1057.py +33 -0
  40. simo/core/migrations/0038_remove_instance_cover_image_and_more.py +30 -0
  41. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
  42. simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-38.pyc +0 -0
  43. simo/core/models.py +30 -16
  44. simo/core/permissions.py +6 -3
  45. simo/core/serializers.py +77 -5
  46. simo/core/signal_receivers.py +25 -0
  47. simo/core/static/admin/css/simo.css +14 -0
  48. simo/core/tasks.py +82 -49
  49. simo/core/templates/admin/controller_widgets/button.html +8 -0
  50. simo/core/templates/admin/core/component_change_form.html +97 -0
  51. simo/core/templates/admin/formset_widget.html +88 -118
  52. simo/core/templates/admin/formset_widget_old.html +122 -0
  53. simo/core/templates/admin/user_tools.html +0 -3
  54. simo/core/templates/admin/wizard/wizard_add.html +16 -9
  55. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  56. simo/core/utils/__pycache__/cache.cpython-38.pyc +0 -0
  57. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  58. simo/core/utils/admin.py +11 -0
  59. simo/core/utils/cache.py +15 -0
  60. simo/core/utils/formsets.py +11 -18
  61. simo/core/views.py +2 -85
  62. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  63. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  64. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  65. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  66. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  67. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  68. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  69. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  70. simo/fleet/auto_urls.py +7 -1
  71. simo/fleet/controllers.py +194 -31
  72. simo/fleet/forms.py +223 -87
  73. simo/fleet/gateways.py +53 -2
  74. simo/fleet/migrations/0036_auto_20240605_0702.py +68 -0
  75. simo/fleet/migrations/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.py +27 -0
  76. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
  77. simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-38.pyc +0 -0
  78. simo/fleet/models.py +35 -6
  79. simo/fleet/socket_consumers.py +1 -1
  80. simo/fleet/templates/fleet/controllers_info/button.md +16 -0
  81. simo/fleet/utils.py +31 -1
  82. simo/fleet/views.py +45 -0
  83. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  84. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  85. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  86. simo/generic/controllers.py +61 -16
  87. simo/generic/forms.py +0 -3
  88. simo/generic/gateways.py +2 -0
  89. simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
  90. simo/generic/templates/admin/controller_widgets/weather_forecast.html +1 -1
  91. simo/generic/templates/generic/controllers_info/dummy.md +3 -0
  92. simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
  93. simo/management/__init__.py +0 -0
  94. simo/management/__pycache__/__init__.cpython-38.pyc +0 -0
  95. simo/management/__pycache__/on_http_start.cpython-38.pyc +0 -0
  96. simo/{_hub_template → management/_hub_template}/hub/nginx.conf +2 -2
  97. simo/{auto_update.py → management/auto_update.py} +3 -0
  98. simo/{cli.py → management/copy_template.py} +3 -16
  99. simo/management/install.py +258 -0
  100. simo/{on_http_start.py → management/on_http_start.py} +22 -2
  101. simo/settings.py +20 -4
  102. simo/users/__init__.py +1 -0
  103. simo/users/__pycache__/__init__.cpython-38.pyc +0 -0
  104. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  105. simo/users/__pycache__/apps.cpython-38.pyc +0 -0
  106. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  107. simo/users/apps.py +9 -0
  108. simo/users/migrations/__pycache__/0029_alter_instanceuser_options_instanceuser_order.cpython-38.pyc +0 -0
  109. simo/users/migrations/__pycache__/0030_alter_instanceuser_options_remove_instanceuser_order.cpython-38.pyc +0 -0
  110. simo/users/models.py +16 -3
  111. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/METADATA +5 -3
  112. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/RECORD +122 -95
  113. simo-2.1.2.dist-info/entry_points.txt +2 -0
  114. simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
  115. simo/wsgi.py +0 -7
  116. /simo/{_hub_template → management/_hub_template}/hub/asgi.py +0 -0
  117. /simo/{_hub_template → management/_hub_template}/hub/celeryc.py +0 -0
  118. /simo/{_hub_template → management/_hub_template}/hub/manage.py +0 -0
  119. /simo/{_hub_template → management/_hub_template}/hub/settings.py +0 -0
  120. /simo/{_hub_template → management/_hub_template}/hub/supervisor.conf +0 -0
  121. /simo/{_hub_template → management/_hub_template}/hub/urls.py +0 -0
  122. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/LICENSE.md +0 -0
  123. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/WHEEL +0 -0
  124. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/top_level.txt +0 -0
@@ -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
+ }
simo/core/tasks.py CHANGED
@@ -2,20 +2,23 @@ import time
2
2
  import os
3
3
  import io
4
4
  import json
5
- import base64
6
5
  import datetime
7
6
  import requests
8
7
  import subprocess
9
8
  import threading
10
9
  import pkg_resources
10
+ import sys
11
+ import traceback
11
12
  from django.db.models import Q
12
- from django.db import connection
13
+ from django.db import connection, transaction
13
14
  from django.template.loader import render_to_string
14
15
  from celeryc import celery_app
15
16
  from django.utils import timezone
17
+ from actstream.models import Action
16
18
  from easy_thumbnails.files import get_thumbnailer
17
19
  from simo.conf import dynamic_settings
18
20
  from simo.core.utils.helpers import get_self_ip
21
+ from simo.users.models import PermissionsRole, InstanceUser
19
22
  from .models import Instance, Component, ComponentHistory, HistoryAggregate
20
23
 
21
24
 
@@ -99,19 +102,15 @@ def save_config(data):
99
102
  def sync_with_remote():
100
103
  from simo.users.models import User
101
104
 
102
- instances = Instance.objects.all()
103
- if not instances:
104
- # No initial configuration yet
105
- return
106
-
107
105
  report_data = {
108
106
  'simo_version': pkg_resources.get_distribution('simo').version,
109
107
  'local_http': 'https://%s' % get_self_ip(),
110
108
  'hub_uid': dynamic_settings['core__hub_uid'],
111
109
  'hub_secret': dynamic_settings['core__hub_secret'],
110
+ 'remote_conn_version': dynamic_settings['core__remote_conn_version'],
112
111
  'instances': []
113
112
  }
114
- for instance in instances:
113
+ for instance in Instance.objects.all():
115
114
  instance_data = {
116
115
  'uid': instance.uid,
117
116
  'name': instance.name,
@@ -131,10 +130,14 @@ def sync_with_remote():
131
130
  user_role = user.get_role(instance)
132
131
  if user_role and user_role.is_superuser:
133
132
  is_superuser = True
133
+ is_owner = False
134
+ if user_role and user_role.is_owner:
135
+ is_owner = True
134
136
  instance_data['users'].append({
135
137
  'email': user.email,
136
138
  'is_hub_master': user.is_master,
137
139
  'is_superuser': is_superuser,
140
+ 'is_owner': is_owner,
138
141
  'device_token': user.primary_device_token
139
142
  })
140
143
 
@@ -144,15 +147,6 @@ def sync_with_remote():
144
147
  if last_event:
145
148
  instance_data['last_event'] = last_event.date.timestamp()
146
149
 
147
- if instance.cover_image and not instance.cover_image_synced:
148
- thumbnailer = get_thumbnailer(instance.cover_image.path)
149
- cover_imb_path = thumbnailer.get_thumbnail(
150
- {'size': (880, 490), 'crop': True}
151
- ).path
152
- with open(cover_imb_path, 'rb') as img:
153
- instance_data['cover_image'] = base64.b64encode(
154
- img.read()
155
- ).decode()
156
150
  report_data['instances'].append(instance_data)
157
151
 
158
152
  print("Sync UP with remote: ", json.dumps(report_data))
@@ -169,11 +163,7 @@ def sync_with_remote():
169
163
  if 'hub_uid' in r_json:
170
164
  dynamic_settings['core__hub_uid'] = r_json['hub_uid']
171
165
 
172
- for instance in instances:
173
- instance.cover_image_synced = True
174
- instance.save()
175
-
176
- dynamic_settings['core__remote_http'] = r_json.get('hub_remote_http')
166
+ dynamic_settings['core__remote_http'] = r_json.get('hub_remote_http', '')
177
167
  if 'new_secret' in r_json:
178
168
  dynamic_settings['core__hub_secret'] = r_json['new_secret']
179
169
 
@@ -182,9 +172,14 @@ def sync_with_remote():
182
172
  dynamic_settings['core__remote_conn_version'] = r_json['remote_conn_version']
183
173
 
184
174
  for data in r_json['instances']:
185
- instance = Instance.objects.get(uid=data['uid'])
175
+ users_data = data.pop('users', {})
176
+ instance_uid = data.pop('uid')
177
+ weather_forecast = data.pop('weather_forecast', None)
178
+ instance, new_instance = Instance.objects.update_or_create(
179
+ uid=instance_uid, defaults=data
180
+ )
186
181
 
187
- if 'weather_forecast' in data:
182
+ if weather_forecast:
188
183
  from simo.generic.controllers import WeatherForecast
189
184
  weather_component = Component.objects.filter(
190
185
  zone__instance=instance,
@@ -192,27 +187,55 @@ def sync_with_remote():
192
187
  ).first()
193
188
  if weather_component:
194
189
  weather_component.track_history = False
195
- weather_component.controller.set(data['weather_forecast'])
196
-
197
- instance.save()
198
-
199
- for user_data in r_json['users']:
200
- try:
201
- user = User.objects.get(email=user_data['email'])
202
- except User.DoesNotExist:
203
- continue
204
- user.name = user_data['name']
205
- if user_data.get('avatar_url') \
206
- and user.avatar_url != user_data.get('avatar_url'):
207
- user.avatar_url = user_data.get('avatar_url')
208
- resp = requests.get(user.avatar_url)
209
- user.avatar.save(
210
- os.path.basename(user.avatar_url), io.BytesIO(resp.content)
211
- )
212
- user.avatar_url = user_data.get('avatar_url')
213
- user.avatar_last_change = timezone.now()
214
- user.ssh_key = user_data.get('ssh_key')
215
- user.save()
190
+ weather_component.controller.set(weather_forecast)
191
+
192
+
193
+ for email, options in users_data.items():
194
+ with transaction.atomic():
195
+ if new_instance:
196
+ # Create users for new instance!
197
+ user, new_user = User.objects.update_or_create(
198
+ email=email, defaults={
199
+ 'name': options.get('name'),
200
+ 'is_master': options.get('is_hub_master', False),
201
+ 'ssh_key': options.get('ssh_key')
202
+ })
203
+ role = None
204
+ if options.get('is_superuser'):
205
+ role = PermissionsRole.objects.filter(
206
+ instance=new_instance, is_superuser=True
207
+ ).first()
208
+ elif options.get('is_owner'):
209
+ role = PermissionsRole.objects.filter(
210
+ instance=new_instance, is_owner=True
211
+ ).first()
212
+ InstanceUser.objects.update_or_create(
213
+ user=user, instance=new_instance, defaults={
214
+ 'is_active': True, 'role': role
215
+ }
216
+ )
217
+ else:
218
+ user = User.objects.filter(email=email).first()
219
+
220
+ if not user:
221
+ continue
222
+
223
+ if user.name != options.get('name'):
224
+ user.name = options['name']
225
+ user.save()
226
+ if user.ssh_key != options.get('ssh_key'):
227
+ user.ssh_key = options['ssh_key']
228
+ user.save()
229
+
230
+ avatar_url = options.get('avatar_url')
231
+ if avatar_url and user.avatar_url != avatar_url:
232
+ resp = requests.get(avatar_url)
233
+ user.avatar.save(
234
+ os.path.basename(avatar_url), io.BytesIO(resp.content)
235
+ )
236
+ user.avatar_url = avatar_url
237
+ user.avatar_last_change = timezone.now()
238
+ user.save()
216
239
 
217
240
 
218
241
  @celery_app.task
@@ -223,7 +246,10 @@ def watch_timers():
223
246
  component.meta['timer_to'] = 0
224
247
  component.meta['timer_start'] = 0
225
248
  component.save()
226
- component.controller._on_timer_end()
249
+ try:
250
+ component.controller._on_timer_end()
251
+ except Exception as e:
252
+ print(traceback.format_exc(), file=sys.stderr)
227
253
 
228
254
 
229
255
  @celery_app.task
@@ -232,8 +258,15 @@ def clear_history():
232
258
  old_times = timezone.now() - datetime.timedelta(
233
259
  days=instance.history_days
234
260
  )
235
- ComponentHistory.objects.filter(date__lt=old_times).delete()
236
- HistoryAggregate.objects.filter(start__lt=old_times).delete()
261
+ ComponentHistory.objects.filter(
262
+ component__zone__instance=instance, date__lt=old_times
263
+ ).delete()
264
+ HistoryAggregate.objects.filter(
265
+ component__zone__instance=instance, start__lt=old_times
266
+ ).delete()
267
+ Action.objects.filter(
268
+ data__instance_id=instance.id, timestamp__lt=old_times
269
+ )
237
270
 
238
271
 
239
272
  @celery_app.task
@@ -300,6 +333,7 @@ def update_latest_version_available():
300
333
  return
301
334
  latest = list(resp.json()['releases'].keys())[-1]
302
335
  dynamic_settings['core__latest_version_available'] = latest
336
+ print("Got the latest version available!")
303
337
 
304
338
 
305
339
  @celery_app.task
@@ -358,7 +392,6 @@ def low_battery_notifications():
358
392
  )
359
393
 
360
394
 
361
-
362
395
  @celery_app.on_after_finalize.connect
363
396
  def setup_periodic_tasks(sender, **kwargs):
364
397
  sender.add_periodic_task(1, watch_timers.s())
@@ -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 %}
@@ -1,122 +1,92 @@
1
1
  {% load i18n admin_urls static admin_modify %}
2
- <style>
3
- .formset-table th, .formset-table th .label{
4
- font-weight: normal;
5
- color: var(--body-quiet-color);
6
- }
7
- .formset-table .label.required{
8
- font-weight: bold;
9
- }
10
- form .aligned .formset-table ul{
11
- margin-left:0;
12
- }
13
- .errors .formset-table input, .errors .formset-table select,
14
- .errors .formset-table textarea{
15
- border: 1px solid var(--border-color);
16
- }
17
- .errors .formset-table .select2-selection{
18
- border-color: #aaa;
19
- }
20
- .formset-table td.errors input, .formset-table td.errors select,
21
- .formset-table td.errors textarea{
22
- border: 1px solid var(--error-fg);
23
- }
24
- .formset-table td.errors .select2-selection{
25
- border-color: var(--error-fg);
26
- }
27
- .formset-table td.drag {
28
- cursor: pointer;
29
- background-image: url(/static/adminsortable2/icons/drag.png);
30
- background-repeat: repeat;
31
- width: 20px;
32
- }
33
- .formset-table .inline-deletelink{
34
- float: left
35
- }
36
- </style>
2
+ <div class="js-inline-admin-formset inline-group" id="{{ formset.prefix }}-group"
3
+ data-inline-type="tabular"
4
+ data-inline-formset="{{ inline_formset_data }}"
5
+ style="width: 100%">
6
+ <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
7
+ {{ formset.management_form }}
8
+ <fieldset class="module sortable">
9
+ {{ formset.non_form_errors }}
10
+ <table>
11
+ <thead><tr>
12
+ <th class="original"></th>
13
+ {% for field in empty_form %}
14
+ {% if not field.is_hidden and not field.name == 'DELETE' %}
15
+ <th class="column-{{ field.name }}{% if field.required %} required{% endif %}{% if field.widget.is_hidden %} hidden{% endif %}">
16
+ {{ field.label|capfirst }}
17
+ {% if field.help_text %}
18
+ <img src="{% static "admin/img/icon-unknown.svg" %}" class="help help-tooltip" width="10" height="10"
19
+ alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}">
20
+ {% endif %}
21
+ </th>
22
+ {% endif %}
23
+ {% endfor %}
24
+ {% if formset.can_delete %}<th>{% translate "Delete?" %}</th>{% endif %}
25
+ </tr></thead>
26
+
27
+ <tbody>
28
+ {% for form in formset %}
29
+ {% if form.non_field_errors %}
30
+ <tr class="row-form-errors"><td colspan="{{ inline_admin_form|cell_count }}">{{ form.non_field_errors }}</td></tr>
31
+ {% endif %}
32
+ <tr class="form-row has_original" id="{{ formset.prefix }}-{{ forloop.counter0 }}">
33
+
34
+ <td class="original">
35
+ <div style="display:none">
36
+ {% for field in form %}
37
+ {% if field.is_hidden %} {{ field }} {% endif %}
38
+ {% endfor %}
39
+ </div>
40
+ <p>
41
+ <span class="sort"><i class="move-begin" role="button" {% translate "Move to first position" as move_begin %}aria-label="{{ move_begin }}" title="{{ move_begin }}"></i><i class="move-end" role="button" {% translate "Move to last position" as move_end %}aria-label="{{ move_end }}" title="{{ move_end }}"></i></span>
42
+ </p>
43
+ </td>
44
+
45
+ {% for field in form %}
46
+ {% if not field.is_hidden and not field.name == 'DELETE' %}
47
+ <td{% if field.name %} class="field-{{ field.name }}"{% endif %}>
48
+ {{ field }}
49
+ {{ field.errors.as_ul }}
50
+ </td>
51
+ {% endif %}
52
+ {% endfor %}
53
+
54
+
55
+ {% if formset.can_delete %}
56
+ <td class="delete">{% if total_org_forms >= forloop.counter0 %}{{ form.DELETE }}{% endif %}</td>
57
+ {% endif %}
58
+ </tr>
59
+ {% endfor %}
60
+
61
+ <tr class="form-row has_original empty-form" id="{{ formset.prefix }}-empty">
37
62
 
38
-
39
- <div class="inline-group sortable" id="{{ formset.prefix }}-group">
40
- <div class="tabular inline-related">
41
- {{ formset.management_form }}
42
- <fieldset class="module">
43
- {{ formset.non_form_errors }}
44
- <table class="formset-table">
45
- <thead><tr>
46
- <th>{% trans "Sort" %}</th>
47
- {% for field in empty_form %}
48
- {% if not field.is_hidden and not field.name == 'DELETE' %}
49
- <th{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}
50
- {% if field.help_text %}&nbsp;<img src="{% static "admin/img/icon-unknown.svg" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}" />{% endif %}
51
- </th>
52
- {% endif %}
53
- {% endfor %}
54
- {% if formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
55
- </tr></thead>
56
-
57
- <tbody>
58
- {% for form in formset %}
59
- {% if form.non_field_errors %}
60
- <tr><td colspan="{{ total_org_forms }}">{{ form.non_field_errors }}</td></tr>
61
- {% endif %}
62
-
63
- <tr class="form-row has_original" id="{{ formset.prefix }}-{{ forloop.counter0 }}">
64
- <td class="drag">&nbsp;</td>
65
- <td class="original hidden">
66
- {% for field in form %}
67
- {% if field.is_hidden %} {{ field }} {% endif %}
68
- {% endfor %}
69
- </td>
70
-
71
- {% for field in form %}
72
- {% if not field.is_hidden and not field.name == 'DELETE' %}
73
- <td{% if field.name %} class="field-{{ field.name }}"{% endif %}>
74
- {{ field }}
75
- {{ field.errors.as_ul }}
76
- </td>
77
- {% endif %}
78
- {% endfor %}
79
-
80
- {% if formset.can_delete %}
81
- <td class="delete">{% if total_org_forms >= forloop.counter0 %}{{ form.DELETE }}{% endif %}</td>
82
- {% endif %}
83
- </tr>
84
-
85
- {% endfor %}
86
-
87
- <tr class="form-row empty-form" id="{{ formset.prefix }}-empty">
88
- <td class="drag">&nbsp;</td>
89
- <td class="original hidden">
90
- {% for field in empty_form %}
91
- {% if field.is_hidden %} {{ field }} {% endif %}
92
- {% endfor %}
93
- </td>
94
-
95
- {% for field in empty_form %}
96
- {% if not field.is_hidden and not field.name == 'DELETE' %}
97
- <td{% if field.name %} class="field-{{ field.name }}"{% endif %}>
98
- {{ field }}
99
- {{ field.errors.as_ul }}
100
- </td>
101
- {% endif %}
102
- {% endfor %}
103
-
104
- {% if formset.can_delete %}
105
- <td class="delete"></td>
106
- {% endif %}
107
- </tr>
108
-
109
- </tbody>
110
- </table>
111
- </fieldset>
63
+ <td class="original">
64
+ <div style="display:none">
65
+ {% for field in empty_form %}
66
+ {% if field.is_hidden %} {{ field }} {% endif %}
67
+ {% endfor %}
68
+ </div>
69
+ <p>
70
+ <span class="sort"><i class="move-begin" role="button" {% translate "Move to first position" as move_begin %}aria-label="{{ move_begin }}" title="{{ move_begin }}"></i><i class="move-end" role="button" {% translate "Move to last position" as move_end %}aria-label="{{ move_end }}" title="{{ move_end }}"></i></span>
71
+ </p>
72
+ </td>
73
+
74
+ {% for field in empty_form %}
75
+ {% if not field.is_hidden and not field.name == 'DELETE' %}
76
+ <td{% if field.name %} class="field-{{ field.name }}"{% endif %}>
77
+ {{ field }}
78
+ {{ field.errors.as_ul }}
79
+ </td>
80
+ {% endif %}
81
+ {% endfor %}
82
+
83
+ {% if formset.can_delete %}
84
+ <td class="delete"></td>
85
+ {% endif %}
86
+ </tr>
87
+
88
+ </tbody>
89
+ </table>
90
+ </fieldset>
112
91
  </div>
113
92
  </div>
114
-
115
- <script type="application/json" class="inline-tabular-config">
116
- {
117
- "prefix": "{{ formset.prefix|escapejs }}",
118
- "addText": "{% filter escapejs %}Add another{% endfilter %}",
119
- "deleteText": "{% filter escapejs %}{% trans 'Remove' %}{% endfilter %}"
120
- }
121
- </script>
122
- <div class="default_order_field" default_order_field="ORDER" default_order_direction=""></div>