educommon 3.9.4__py3-none-any.whl → 3.9.6__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.
@@ -26,14 +26,14 @@ from django.db.models import (
26
26
  from m3.db import (
27
27
  BaseObjectModel,
28
28
  )
29
- from m3_django_compat.models import (
30
- GenericForeignKey,
31
- )
32
29
 
33
30
  from m3_db_utils.models import (
34
31
  ModelEnumValue,
35
32
  TitledModelEnum,
36
33
  )
34
+ from m3_django_compat.models import (
35
+ GenericForeignKey,
36
+ )
37
37
 
38
38
  from educommon.async_task.consts import (
39
39
  TASK_DEFAULT_USER_NAME,
@@ -5,6 +5,7 @@ from datetime import (
5
5
  )
6
6
  from typing import (
7
7
  Optional,
8
+ Union,
8
9
  )
9
10
 
10
11
  from celery import (
@@ -210,7 +211,7 @@ class AsyncTask(Task):
210
211
  if self.logging_in_db:
211
212
  update_running_task(task_id, **params)
212
213
 
213
- def after_return(self, status, retval, task_id, args, kwargs, einfo):
214
+ def after_return(self, status: str, retval: Union[dict, Exception], task_id: str, args, kwargs, einfo):
214
215
  """Завершение задачи."""
215
216
  self.debug(f'Task {self.__name__} (id = {task_id}) finished')
216
217
 
@@ -219,12 +220,11 @@ class AsyncTask(Task):
219
220
  self.release_lock(lock_id)
220
221
 
221
222
  if isinstance(retval, dict):
222
- self.update_state(
223
- state=retval.get('task_state', status),
224
- meta=retval,
225
- )
223
+ state = retval.get('task_state', status)
226
224
  else:
227
- self.update_state(state=status)
225
+ state = status
226
+
227
+ self.update_state(state=state, meta=retval)
228
228
 
229
229
  def update_state(self, task_id=None, state=None, meta=None, **kwargs):
230
230
  """Обновление состояния задачи.
@@ -25,10 +25,14 @@ from m3 import (
25
25
  from m3.db import (
26
26
  safe_delete,
27
27
  )
28
+
28
29
  from m3_django_compat import (
29
30
  ModelOptions,
30
31
  atomic,
31
32
  )
33
+ from m3_django_compat.exceptions import (
34
+ FieldDoesNotExist,
35
+ )
32
36
  from m3_django_compat.models import (
33
37
  GenericForeignKey,
34
38
  )
@@ -2,15 +2,24 @@ from importlib import (
2
2
  import_module,
3
3
  )
4
4
 
5
+ from django import (
6
+ VERSION,
7
+ )
5
8
  from django.apps.config import (
6
9
  AppConfig,
7
10
  )
8
11
 
9
12
 
13
+ _VERSION = VERSION[:2]
14
+
15
+
10
16
  class ContingentPluginAppConfig(AppConfig):
11
17
 
12
18
  name = __package__
13
19
 
20
+ if _VERSION >= (3, 2):
21
+ default = False
22
+
14
23
  def _register_related_objects_views(self):
15
24
  """Добавляет представления для моделей приложения."""
16
25
  from educommon.django.db.model_view import (
@@ -1,13 +1,14 @@
1
1
  """Классы-примеси для моделей Django."""
2
- from django.core.exceptions import (
3
- FieldDoesNotExist,
4
- )
5
2
  from django.db import (
6
3
  models,
7
4
  )
5
+
8
6
  from m3_django_compat import (
9
7
  atomic,
10
8
  )
9
+ from m3_django_compat.exceptions import (
10
+ FieldDoesNotExist,
11
+ )
11
12
 
12
13
 
13
14
  class DeferredActionsMixin(models.Model):
@@ -55,11 +55,22 @@ management-команды
55
55
  секционирования для конкретной таблицы можно с помощью команды ``apply_partitioning``:
56
56
 
57
57
  ```bash
58
- $ ./manage.py apply_partitioning app_label model_name field_name
58
+ $ ./manage.py apply_partitioning --app_label --model_name --field_name --is_foreign_table --schema_name
59
59
  ```
60
60
 
61
- где ``app_label`` -- лейбл приложения, ``model_name`` -- имя модели, а ``field_label`` -- название поля.
62
- ``field_name`` выступает в качестве ключа секционирования.
61
+ где обязательные параметры
62
+ ``app_label`` -- лейбл приложения, ``model_name`` -- имя модели, а ``field_label`` -- название поля.
63
+ ``field_name`` выступает в качестве ключа секционирования,
64
+
65
+ необязательные параметры
66
+ ``is_foreign_table`` -- признак того, что в базе есть таблицы, на которые ссылаются из сторонних баз (внешние таблицы)
67
+ ``schemas_names`` -- строка со списком схем таблиц, на которые ссылаются из сторонних баз (внешние таблицы) - важно
68
+ перечислять все необходимые схемы - например, --schemas_names=public,pg_temp
69
+
70
+
71
+ Пример
72
+ 1) apply_partitioning --app_label=mark --model_name=MarkLog --field_name=timestamp
73
+ 2) apply_partitioning --app_label=mark --model_name=MarkLog --field_name=timestamp --is_foreign_table=True --schemas_names=public,pg_temp
63
74
 
64
75
  Перенос записей из родительской таблицы в ее разделы
65
76
  ----------------------------------------------------
@@ -68,8 +79,38 @@ $ ./manage.py apply_partitioning app_label model_name field_name
68
79
  секции можно с помощью management-команды ``split_table``:
69
80
 
70
81
  ```bash
71
- $ ./manage.py split_table app_label model_name field_name --timeout 5
82
+ $ ./manage.py split_table --app_label --model_name --field_name --timeout=5 --cursor_itersize=1000
83
+ ```
84
+
85
+ Необязательный параметр
86
+ ``--timeout`` задает время ожидания между переносом записей из родительской таблицы
87
+ в соответствующую секцию.
88
+ ``--cursor_itersize``- задает количесвто удаляемых записей за одну итерацию, по умолчанию 100
89
+
90
+ Пример
91
+ ```bash
92
+ $ ./manage.py split_table --app_label=mark --model_name=MarkLog --field_name=timestamp
93
+ ```
94
+
95
+ Удаление записей из родительской таблицы по условию до ее партицирования
96
+ ----------------------------------------------------
97
+
98
+ Удаление записей из родительской таблицы по условию
99
+ ```bash
100
+ $ ./manage.py clear_table --app_label --model_name --field_name --before_value --timeout --cursor_itersize
72
101
  ```
73
102
 
103
+ где обязательные аргументы -``app_label`` -- лейбл приложения, ``model_name`` -- имя модели,
104
+ ``field_name`` -- название поля, ``before_value`` - значение до которого удаляем все данные
105
+
106
+ Необязательный параметр
74
107
  ``--timeout`` задает время ожидания между переносом каждой записи из родительской таблицы
75
108
  в соответствующую секцию.
109
+ ``--cursor_itersize``- задает количество удаляемых записей за одну итерацию, по умолчанию 10 000
110
+
111
+
112
+ Пример
113
+
114
+ ```bash
115
+ $ ./manage.py clear_table --app_label=mark --model_name=MarkLog --field_name=timestamp --before_value='2022-09-01' --timeout=2 --cursor_itersize=1000
116
+ ```
@@ -41,6 +41,9 @@ from time import (
41
41
  from types import (
42
42
  MethodType,
43
43
  )
44
+ from typing import (
45
+ Optional,
46
+ )
44
47
 
45
48
  from django.conf import (
46
49
  settings,
@@ -61,6 +64,7 @@ from django.db.utils import (
61
64
  from django.utils.functional import (
62
65
  cached_property,
63
66
  )
67
+
64
68
  from m3_django_compat import (
65
69
  ModelOptions,
66
70
  commit_unless_managed,
@@ -69,6 +73,11 @@ from m3_django_compat import (
69
73
  from educommon.django.db.observer import (
70
74
  ModelObserverBase,
71
75
  )
76
+ from educommon.django.db.partitioning.const import (
77
+ CURSOR_ITERSIZE,
78
+ INSERT_CURSOR_ITERSIZE,
79
+ PARTITION_TRIGGERS_FUNCTIONS,
80
+ )
72
81
 
73
82
 
74
83
  _MESSAGE_PREFIX = '[Partitioning] '
@@ -114,17 +123,7 @@ def is_initialized(database_alias):
114
123
  return False
115
124
 
116
125
  # Проверка наличия всех функций схемы.
117
- function_names = set((
118
- 'getattr',
119
- 'get_partition_name',
120
- 'set_partition_constraint',
121
- 'table_exists',
122
- 'is_table_partitioned',
123
- 'create_partition',
124
- 'before_insert',
125
- 'instead_of_insert',
126
- 'before_update',
127
- ))
126
+ function_names = PARTITION_TRIGGERS_FUNCTIONS
128
127
  for function_name in function_names:
129
128
  with closing(connections[database_alias].cursor()) as cursor:
130
129
  cursor.execute(
@@ -227,7 +226,7 @@ def set_partitioning_for_model(model, column_name, force=False):
227
226
  значение будет определять раздел таблицы, в который будет помещена
228
227
  запись.
229
228
 
230
- :raises django.core.exceptions.FieldDoesNotExist: если модель *model* не
229
+ :raises m3_django_compat.exceptions.FieldDoesNotExist: если модель *model* не
231
230
  содержит поля *column_name*
232
231
  """
233
232
  database_alias, table_name, pk_column_name = _get_model_params(model)
@@ -244,7 +243,7 @@ def set_partitioning_for_model(model, column_name, force=False):
244
243
  _execute_sql_file(database_alias, 'triggers.sql', locals())
245
244
 
246
245
 
247
- def split_table(model, column_name, timeout=0):
246
+ def split_table(model, column_name: str, timeout: float = 0, cursor_itersize: Optional[int] = None):
248
247
  """Переносит записи из разбиваемой таблицы в ее разделы.
249
248
 
250
249
  Недостающие разделы будут созданы автоматически.
@@ -257,12 +256,17 @@ def split_table(model, column_name, timeout=0):
257
256
  запись.
258
257
  :param float timeout: Время ожидания в секундах между переносом записей
259
258
  (можно использовать для снижения нагрузки на СУБД).
259
+ :param int cursor_itersize: Количество записей, попадающих в итератор курсора бд
260
+ при запросе разбиения таблиц.
260
261
 
261
- :raises django.core.exceptions.FieldDoesNotExist: если модель *model* не
262
+ :raises m3_django_compat.exceptions.FieldDoesNotExist: если модель *model* не
262
263
  содержит поля *column_name*
263
264
  """
264
265
  database_alias, table_name, pk_column_name = _get_model_params(model)
265
266
 
267
+ if not cursor_itersize:
268
+ cursor_itersize = INSERT_CURSOR_ITERSIZE
269
+
266
270
  # для проверки наличия поля в модели
267
271
  ModelOptions(model).get_field(column_name)
268
272
 
@@ -294,10 +298,14 @@ def split_table(model, column_name, timeout=0):
294
298
  move_cursor = connection.cursor()
295
299
 
296
300
  results = cursor_iter(
297
- ids_cursor, connection.features.empty_fetchmany_value, 1
301
+ ids_cursor, connection.features.empty_fetchmany_value, 1, cursor_itersize
298
302
  )
299
303
  for rows in results:
300
- pk_column_values = tuple(pkv for (pkv,) in rows)
304
+ # Если всего одна запись, то используем строку, чтобы не получать ошибку sql-запроса на обновление
305
+ if len(rows) == 1:
306
+ pk_column_values = f'{rows[0]}'.replace(',', '')
307
+ else:
308
+ pk_column_values = tuple(pkv for (pkv,) in rows)
301
309
  # Этот update выполняется для того, чтобы сработала триггерная
302
310
  # функция partitioning.before_update.
303
311
  move_cursor.execute((
@@ -306,13 +314,11 @@ def split_table(model, column_name, timeout=0):
306
314
  'where {pk_column_name} in {pk_column_values}'
307
315
  ).format(**locals()))
308
316
 
309
- commit_unless_managed(database_alias)
310
-
311
317
  if timeout:
312
318
  sleep(timeout)
313
319
 
314
320
 
315
- def clear_table(model, column_name, column_value, timeout=0):
321
+ def clear_table(model, column_name: str, column_value: str, timeout=0, cursor_itersize: Optional[int] = None):
316
322
  """Удаление записей по условию.
317
323
 
318
324
  С помощью данной команды удаляются записи из основной (не секционированной)
@@ -327,9 +333,14 @@ def clear_table(model, column_name, column_value, timeout=0):
327
333
  будут удаляться записи.
328
334
  :param float timeout: Время ожидания в секундах между удалением 100
329
335
  записей (можно использовать для снижения нагрузки на СУБД).
336
+ :param int cursor_itersize: Количество записей, попадающих в итератор курсора бд
337
+ при запросе удаления таблиц.
330
338
  """
331
339
  database_alias, table_name, pk_column_name = _get_model_params(model)
332
340
 
341
+ if not cursor_itersize:
342
+ cursor_itersize = CURSOR_ITERSIZE
343
+
333
344
  connection = connections[database_alias]
334
345
 
335
346
  if settings.DATABASES[database_alias]['DISABLE_SERVER_SIDE_CURSORS']:
@@ -349,17 +360,19 @@ def clear_table(model, column_name, column_value, timeout=0):
349
360
  delete_cursor = connection.cursor()
350
361
 
351
362
  results = cursor_iter(
352
- ids_cursor, connection.features.empty_fetchmany_value, 1
363
+ ids_cursor, connection.features.empty_fetchmany_value, 1, cursor_itersize,
353
364
  )
354
365
  for rows in results:
355
- pk_column_values = tuple(pkv for (pkv,) in rows)
366
+ # Если всего одна запись, то используем строку, чтобы не получать ошибку sql-запроса на удаление
367
+ if len(rows) == 1:
368
+ pk_column_values = f'{rows[0]}'.replace(',', '')
369
+ else:
370
+ pk_column_values = tuple(pkv for (pkv,) in rows)
356
371
  delete_cursor.execute((
357
372
  'delete from {table_name} '
358
373
  'where {pk_column_name} in {pk_column_values}'
359
374
  ).format(**locals()))
360
375
 
361
- commit_unless_managed(database_alias)
362
-
363
376
  if timeout:
364
377
  sleep(timeout)
365
378
 
@@ -441,6 +454,17 @@ def drop_partitions_before_date(model, date):
441
454
  )
442
455
 
443
456
 
457
+ def set_partitioned_function_search_path(database_alias: str, schema_names: Optional[str] = None):
458
+ """"Проставляет параметры поиска для существующих функций партицирования.
459
+
460
+ Это необходимо для корректной работы с таблицами к которым обращаются как к внешним через postgres_fdw.
461
+ """
462
+ schema_names = schema_names or 'public'
463
+ _execute_sql_file(database_alias, 'partitioning_set_search_path.sql', dict(
464
+ schema_names=schema_names,
465
+ ))
466
+
467
+
444
468
  class PartitioningObserver(ModelObserverBase):
445
469
  """Оптимизирует операции вставки в партиционированные таблицы.
446
470
 
@@ -0,0 +1,24 @@
1
+ # Количество записей, попадающих в итератор курсора бд при запросе удаления таблиц
2
+ CURSOR_ITERSIZE = 10_000
3
+ # Количество записей, попадающих в итератор курсора бд при запросе разбиения таблиц.
4
+ INSERT_CURSOR_ITERSIZE = 100
5
+
6
+
7
+ # Стандартный список функций при инициализации партицирования
8
+ BASE_PARTITION_FUNCTIONS = (
9
+ 'getattr',
10
+ 'get_partition_name',
11
+ 'set_partition_constraint',
12
+ 'table_exists',
13
+ 'is_table_partitioned',
14
+ 'create_partition',
15
+ 'trigger_exists',
16
+ )
17
+
18
+ # Функции для работы триггеров партицированных таблиц
19
+ PARTITION_TRIGGERS_FUNCTIONS = BASE_PARTITION_FUNCTIONS + (
20
+ 'before_insert',
21
+ 'instead_of_insert',
22
+ 'before_update',
23
+ 'after_insert',
24
+ )
@@ -1,6 +1,13 @@
1
+ from django.core.exceptions import (
2
+ FieldDoesNotExist,
3
+ )
4
+ from django.core.management.base import (
5
+ CommandError,
6
+ )
1
7
  from django.db import (
2
8
  router,
3
9
  )
10
+
4
11
  from m3_django_compat import (
5
12
  BaseCommand,
6
13
  get_model,
@@ -9,6 +16,9 @@ from m3_django_compat import (
9
16
  from educommon.django.db import (
10
17
  partitioning,
11
18
  )
19
+ from educommon.logger import (
20
+ info as logger_info,
21
+ )
12
22
 
13
23
 
14
24
  class Command(BaseCommand):
@@ -20,30 +30,66 @@ class Command(BaseCommand):
20
30
  `educommon.django.db.partitioning.set_partitioning_for_model`.
21
31
 
22
32
  """
23
- help = 'Applies partitioning to the table.'
33
+ help = 'Applies partitioning to the table.' # noqa: A003
24
34
 
25
35
  def add_arguments(self, parser):
36
+ """Обработка аргументов команды."""
26
37
  parser.add_argument(
27
- 'app_label',
38
+ '--app_label',
39
+ type=str,
28
40
  help='App label of an application.',
29
41
  )
30
42
  parser.add_argument(
31
- 'model_name',
43
+ '--model_name',
44
+ type=str,
32
45
  help='Model name.',
33
46
  )
34
47
  parser.add_argument(
35
- 'field_name',
48
+ '--field_name',
49
+ type=str,
36
50
  help='Field name. It will be the partition key.',
37
51
  )
52
+ parser.add_argument(
53
+ '--is_foreign_table',
54
+ type=bool,
55
+ default=False,
56
+ help='Партицирование для внешних таблиц'
57
+ )
58
+ parser.add_argument(
59
+ '--schemas_names',
60
+ type=str,
61
+ default=None,
62
+ help='Cхемы внешних таблиц при партицировании.'
63
+ )
38
64
 
39
65
  def handle(self, *args, **options):
66
+ """Выполнение команды."""
40
67
  app_label = options['app_label']
41
68
  model_name = options['model_name']
42
69
  field_name = options['field_name']
43
- Model = get_model(app_label, model_name)
44
- db_alias = router.db_for_write(Model)
70
+ is_foreign_table = options['is_foreign_table']
71
+ schemas_names = options['schemas_names']
72
+
73
+ logger_info('Apply partitioning started\n')
74
+ try:
75
+ django_db_model = get_model(app_label, model_name)
76
+ except LookupError as e:
77
+ raise CommandError(e.message)
78
+
79
+ try:
80
+ django_db_model._meta.get_field(field_name)
81
+ except FieldDoesNotExist:
82
+ raise CommandError('Invalid field name ({0})'.format(field_name))
83
+
84
+ db_alias = router.db_for_write(django_db_model)
45
85
 
46
86
  if not partitioning.is_initialized(db_alias):
47
87
  partitioning.init(db_alias)
48
88
 
49
- partitioning.set_partitioning_for_model(Model, field_name, force=True)
89
+ # Дополнительно пробросим схемы для работ с внешними таблицами
90
+ if is_foreign_table:
91
+ partitioning.set_partitioned_function_search_path(db_alias, schemas_names)
92
+
93
+ partitioning.set_partitioning_for_model(django_db_model, field_name, force=True)
94
+
95
+ logger_info('Apply partitioning ended\n')
@@ -9,6 +9,9 @@ from m3_django_compat import (
9
9
  BaseCommand,
10
10
  get_model,
11
11
  )
12
+ from m3_django_compat.exceptions import (
13
+ FieldDoesNotExist,
14
+ )
12
15
 
13
16
  from educommon.django.db import (
14
17
  partitioning,
@@ -30,19 +33,23 @@ class Command(BaseCommand):
30
33
 
31
34
  def add_arguments(self, parser):
32
35
  parser.add_argument(
33
- 'app_label',
36
+ '--app_label',
37
+ type=str,
34
38
  help='App label of an application.',
35
39
  )
36
40
  parser.add_argument(
37
- 'model_name',
41
+ '--model_name',
42
+ type=str,
38
43
  help='Model name.',
39
44
  )
40
45
  parser.add_argument(
41
- 'field_name',
46
+ '--field_name',
47
+ type=str,
42
48
  help='Field name. It will be a check column.',
43
49
  )
44
50
  parser.add_argument(
45
- 'before_value',
51
+ '--before_value',
52
+ type=str,
46
53
  help='Deleting rows before this value.',
47
54
  )
48
55
  parser.add_argument(
@@ -51,6 +58,12 @@ class Command(BaseCommand):
51
58
  help=('Timeout (in seconds) between the data removes iterations. '
52
59
  'It used to reduce the database load.')
53
60
  )
61
+ parser.add_argument(
62
+ '--cursor_itersize', action='store', dest='cursor_itersize',
63
+ type=int,
64
+ default=None,
65
+ help='Количество строк загруженных за раз при загрузке строк при работе команды.'
66
+ )
54
67
 
55
68
  def handle(self, *args, **options):
56
69
  app_label = options['app_label']
@@ -58,6 +71,7 @@ class Command(BaseCommand):
58
71
  field_name = options['field_name']
59
72
  before_value = options['before_value']
60
73
  timeout = options['timeout']
74
+ cursor_itersize = options['cursor_itersize']
61
75
 
62
76
  try:
63
77
  model = get_model(app_label, model_name)
@@ -69,4 +83,4 @@ class Command(BaseCommand):
69
83
  except FieldDoesNotExist:
70
84
  raise CommandError('Invalid field name ({0})'.format(field_name))
71
85
 
72
- partitioning.clear_table(model, field_name, before_value, timeout)
86
+ partitioning.clear_table(model, field_name, before_value, timeout, cursor_itersize)
@@ -9,10 +9,16 @@ from m3_django_compat import (
9
9
  BaseCommand,
10
10
  get_model,
11
11
  )
12
+ from m3_django_compat.exceptions import (
13
+ FieldDoesNotExist,
14
+ )
12
15
 
13
16
  from educommon.django.db import (
14
17
  partitioning,
15
18
  )
19
+ from educommon.logger import (
20
+ info as logger_info,
21
+ )
16
22
 
17
23
 
18
24
  class Command(BaseCommand):
@@ -30,15 +36,18 @@ class Command(BaseCommand):
30
36
 
31
37
  def add_arguments(self, parser):
32
38
  parser.add_argument(
33
- 'app_label',
39
+ '--app_label',
40
+ type=str,
34
41
  help='App label of an application.',
35
42
  )
36
43
  parser.add_argument(
37
- 'model_name',
44
+ '--model_name',
45
+ type=str,
38
46
  help='Model name.',
39
47
  )
40
48
  parser.add_argument(
41
- 'field_name',
49
+ '--field_name',
50
+ type=str,
42
51
  help='Field name. It will be the partition key.',
43
52
  )
44
53
  parser.add_argument(
@@ -47,12 +56,19 @@ class Command(BaseCommand):
47
56
  help=('Timeout (in seconds) between the data transfer iterations. '
48
57
  'It used to reduce the database load.')
49
58
  )
59
+ parser.add_argument(
60
+ '--cursor_itersize', action='store', dest='cursor_itersize',
61
+ type=int,
62
+ default=None,
63
+ help='Количество строк загруженных за раз при загрузке строк при работе команды.'
64
+ )
50
65
 
51
66
  def handle(self, *args, **options):
52
67
  app_label = options['app_label']
53
68
  model_name = options['model_name']
54
69
  field_name = options['field_name']
55
70
  timeout = options['timeout']
71
+ cursor_itersize = options['cursor_itersize']
56
72
 
57
73
  try:
58
74
  model = get_model(app_label, model_name)
@@ -64,4 +80,8 @@ class Command(BaseCommand):
64
80
  except FieldDoesNotExist:
65
81
  raise CommandError('Invalid field name ({0})'.format(field_name))
66
82
 
67
- partitioning.split_table(model, field_name, timeout)
83
+ logger_info('Split table started\n')
84
+
85
+ partitioning.split_table(model, field_name, timeout, cursor_itersize)
86
+
87
+ logger_info('Split table ended\n')
@@ -134,6 +134,23 @@ create or replace function partitioning.table_exists(
134
134
  $BODY$ language plpgsql immutable;
135
135
 
136
136
 
137
+ create or replace function partitioning.trigger_exists(
138
+ table_name text, trigger_name text
139
+ ) returns boolean as $BODY$
140
+ ---------------------------------------------------------------------------
141
+ -- Проверяет, существует ли в таблице table_name триггер с именем trigger_name.
142
+ ---------------------------------------------------------------------------
143
+ begin
144
+ return exists(
145
+ select 1
146
+ from pg_catalog.pg_trigger
147
+ where tgrelid = table_name::regclass::oid and
148
+ tgname = trigger_name
149
+ );
150
+ end;
151
+ $BODY$ language plpgsql immutable;
152
+
153
+
137
154
  create or replace function partitioning.is_table_partitioned(
138
155
  table_name text
139
156
  ) returns boolean as $BODY$
@@ -197,6 +214,7 @@ create or replace function partitioning.create_partition(
197
214
  partition_name text;
198
215
  tablespace_name name;
199
216
  tablespace_sql text;
217
+ trigger_name text;
200
218
  begin
201
219
  -- имя создаваемой таблицы
202
220
  partition_name := partitioning.get_partition_name(
@@ -235,14 +253,18 @@ create or replace function partitioning.create_partition(
235
253
 
236
254
  -- создание триггера для раздела обрабатывающего изменения, которые
237
255
  -- требуют переноса записи из одного раздела таблицы в другой
238
- execute
239
- 'create trigger partitioning__' || partition_name || '__update '
256
+ -- перед созданием проверяем, что такого триггера нет для партиции
257
+ trigger_name = 'partitioning__' || partition_name || '__update';
258
+ if not partitioning.trigger_exists(partition_name, trigger_name) then
259
+ execute
260
+ 'create trigger ' || trigger_name || ' '
240
261
  'before update on ' ||
241
262
  schema_name || '.' || partition_name || ' ' ||
242
263
  'for each row ' ||
243
264
  'execute procedure partitioning.before_update(' ||
244
265
  '''' || pk_column_name || ''', ''' || column_name || ''''
245
266
  ')';
267
+ end if;
246
268
  end;
247
269
  $BODY$ language plpgsql;
248
270
 
@@ -0,0 +1,11 @@
1
+ begin;
2
+
3
+ alter function partitioning.before_insert() set search_path = '{schema_names}';
4
+
5
+ alter function partitioning.after_insert() set search_path = '{schema_names}';
6
+
7
+ alter function partitioning.instead_of_insert() set search_path = '{schema_names}';
8
+
9
+ alter function partitioning.before_update() set search_path = '{schema_names}';
10
+
11
+ commit;
@@ -152,7 +152,10 @@ class CascadeDeleteMixin:
152
152
 
153
153
  @staticmethod
154
154
  def skip_field(field):
155
- return field in getattr(field.model, 'cascade_delete_for', ())
155
+ cascade_delete_for = getattr(field.model, 'cascade_delete_for', ())
156
+ if isinstance(cascade_delete_for, dict):
157
+ cascade_delete_for = tuple(cascade_delete_for.keys())
158
+ return field in cascade_delete_for
156
159
 
157
160
  @atomic
158
161
  def safe_delete(self):
@@ -2,6 +2,7 @@ from django.db.models import (
2
2
  SET_DEFAULT,
3
3
  SET_NULL,
4
4
  )
5
+
5
6
  from m3.db import (
6
7
  safe_delete,
7
8
  )
@@ -53,7 +54,11 @@ def get_custom_on_delete_function(field):
53
54
  not isinstance(field.model.cascade_delete_for, dict)
54
55
  ):
55
56
  return None
56
- on_delete_params = field.model.cascade_delete_for[field]
57
+
58
+ on_delete_params = {}
59
+ for _field, _field_params in field.model.cascade_delete_for.items():
60
+ if _field == field:
61
+ on_delete_params = _field_params
57
62
  return on_delete_params.get('on_delete')
58
63
 
59
64
 
@@ -1,6 +1,5 @@
1
1
  # pylint: disable=protected-access
2
2
  from django.core.exceptions import (
3
- FieldDoesNotExist,
4
3
  ValidationError,
5
4
  )
6
5
  from django.db import (
@@ -24,6 +23,9 @@ from m3_django_compat import (
24
23
  ModelOptions,
25
24
  get_related,
26
25
  )
26
+ from m3_django_compat.exceptions import (
27
+ FieldDoesNotExist,
28
+ )
27
29
 
28
30
  from educommon.django.db.utils import (
29
31
  LazyModel,
@@ -10,9 +10,6 @@ from itertools import (
10
10
  from django.contrib.contenttypes.models import (
11
11
  ContentType,
12
12
  )
13
- from django.core.exceptions import (
14
- FieldDoesNotExist,
15
- )
16
13
  from django.db.transaction import (
17
14
  atomic,
18
15
  )
@@ -27,6 +24,9 @@ from m3_ext.ui.results import (
27
24
  ExtUIScriptResult,
28
25
  )
29
26
 
27
+ from m3_django_compat.exceptions import (
28
+ FieldDoesNotExist,
29
+ )
30
30
  from objectpack.actions import (
31
31
  BaseAction,
32
32
  ObjectPack,
@@ -5,9 +5,6 @@ from inspect import (
5
5
  isclass,
6
6
  )
7
7
 
8
- from django.core.exceptions import (
9
- FieldDoesNotExist,
10
- )
11
8
  from django.db.models.fields import (
12
9
  BooleanField,
13
10
  CharField,
@@ -26,9 +23,13 @@ from django.db.models.fields.related import (
26
23
  from django.db.models.fields.reverse_related import (
27
24
  ForeignObjectRel,
28
25
  )
26
+
29
27
  from m3_django_compat import (
30
28
  get_related,
31
29
  )
30
+ from m3_django_compat.exceptions import (
31
+ FieldDoesNotExist,
32
+ )
32
33
 
33
34
  from educommon.report.constructor.constants import (
34
35
  CT_BOOLEAN,
educommon/version.conf CHANGED
@@ -4,8 +4,8 @@
4
4
  # нормальной установки обновлений.
5
5
 
6
6
  [version]
7
- BRANCH = tags/3.9.4^0
8
- VERSION = 3.9.4
9
- REVISION = b525751553abee2f4deac7b1783499fb00ef29f5
10
- VERSION_DATE = 03.04.2024
11
- REVISION_DATE = 03.04.2024
7
+ BRANCH = tags/3.9.6
8
+ VERSION = 3.9.6
9
+ REVISION = 1db60d8567b7d574828c7217b421726deb3ab035
10
+ VERSION_DATE = 12.04.2024
11
+ REVISION_DATE = 12.04.2024
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: educommon
3
- Version: 3.9.4
3
+ Version: 3.9.6
4
4
  Summary: Общая кодовая база для проектов БЦ Образование
5
5
  Home-page: https://stash.bars-open.ru/projects/EDUBASE/repos/educommon
6
6
  Author: BARS Group
@@ -17,8 +17,10 @@ Classifier: Programming Language :: Python :: 3.8
17
17
  Classifier: Programming Language :: Python :: 3.9
18
18
  Classifier: Framework :: Django :: 2.2
19
19
  Classifier: Framework :: Django :: 3.0
20
+ Classifier: Framework :: Django :: 3.1
21
+ Classifier: Framework :: Django :: 3.2
20
22
  Requires-Dist: packaging <24,>=21.3
21
- Requires-Dist: Django <3.2,>=2.2
23
+ Requires-Dist: Django <4,>=2.2
22
24
  Requires-Dist: django-mptt
23
25
  Requires-Dist: python-dateutil
24
26
  Requires-Dist: termcolor
@@ -28,7 +30,7 @@ Requires-Dist: celery
28
30
  Requires-Dist: spyne
29
31
  Requires-Dist: xlsxwriter <1,>=0.9.3
30
32
  Requires-Dist: m3-builder <2,>=1.2
31
- Requires-Dist: m3-db-utils >=0.3.11
33
+ Requires-Dist: m3-db-utils >=0.3.13
32
34
  Requires-Dist: m3-django-compat <2,>=1.10.2
33
35
  Requires-Dist: m3-core <3,>=2.2.16
34
36
  Requires-Dist: m3-ui <3,>=2.2.40
@@ -1,6 +1,6 @@
1
1
  educommon/__init__.py,sha256=fvsBDL7g8HgOTd-JHOh7TSvMcnUauvGVgPuyA2Z9hUI,419
2
2
  educommon/thread_data.py,sha256=n0XtdesP9H92O3rJ8K6fVnJLiHqyJEfh2xpuT36wzxs,61
3
- educommon/version.conf,sha256=45BReSu8CR7Bw853URhL6tptlqRIL7Bo_BbW3NJj4VY,450
3
+ educommon/version.conf,sha256=l7UfRQw8wmcaWx-1EAAFDaIke-pX2sbG3vJcPUn4CEA,448
4
4
  educommon/about/README.rst,sha256=U48UW5jv-8qHyaV56atzzkNMvzHKXVcWSb_NR06PnMo,2685
5
5
  educommon/about/__init__.py,sha256=H1W0IgW-qX9LCZ49GOJzHdmQGHhh-MA6U1xmNx7WnfM,132
6
6
  educommon/about/apps.py,sha256=GrpJAOE2sF0ukWsqugP_WJS88DO4aL-T3kTLprrJrcA,259
@@ -20,8 +20,8 @@ educommon/async_task/consts.py,sha256=LulbNKSlS_T8sNjFqMS7DtaotVI0HibD1a0Iz9iDo8
20
20
  educommon/async_task/exceptions.py,sha256=PaaG2nqP0G2eZ-p0yssca2hhxIccil6MYKH6ZteezP8,117
21
21
  educommon/async_task/helpers.py,sha256=HihuIHrszhVQAb-Cs6-l4npnlNAgctItGBJ-oIPQX74,2706
22
22
  educommon/async_task/locker.py,sha256=TwVFhuEGsK0JHPXsWuqx7t2Op2mUKETY5EvHSeuKjc8,3844
23
- educommon/async_task/models.py,sha256=5c7eYSvr8dLI0DEbDblSjVb9W5UlWA32rwvL2MNig-A,7430
24
- educommon/async_task/tasks.py,sha256=QYMhmpIB8zXu_JrXdbFy6Y39zubn2Ao6VYuHKggGIj0,11533
23
+ educommon/async_task/models.py,sha256=pICrDFr4Vq6KWpa28NIALP45aLs0lqP7YC2p1IQDFSc,7430
24
+ educommon/async_task/tasks.py,sha256=A6tIuDyqMo9pPEezluyO8_Gs_XerMvWdFmluZ6VjFwI,11537
25
25
  educommon/async_task/ui.py,sha256=x7SeQZ_JXCy7gbUtVTcDXqmS-mCWKH_BgJc2gK1i_hw,5446
26
26
  educommon/async_task/migrations/0001_initial.py,sha256=W0HfBmhatmQ7Dy4giGeqaZNtLHiEVAvJqx0AvHaxGZ0,3677
27
27
  educommon/async_task/migrations/0002_task_type_and_status_data.py,sha256=rDGxKnbwu4OqfcodFwA_TR-kfDGRKJhivEBFKuurM30,3359
@@ -79,7 +79,7 @@ educommon/auth/rbac/checker.py,sha256=8A583rwsXDeb4dCegN5mufMFhyDwvd72zfug90usvF
79
79
  educommon/auth/rbac/config.py,sha256=mRpOzD29ZHc0zX-CxhUyyICj3h0xj9gUM6N1OoLEWm0,1074
80
80
  educommon/auth/rbac/constants.py,sha256=Hm4kuG7HZ9krTwF6HIj3eMe8v98ui3EMueToBTeOh4A,502
81
81
  educommon/auth/rbac/manager.py,sha256=3WecXF0SU-uBGMxAWnsVLGmVI9QmHS7zDQ9wVgu6mZE,16250
82
- educommon/auth/rbac/models.py,sha256=NwRoHkK8z_yK3TIlt2ws0sE_kzIM9Yo0p0q8u5o95mo,15146
82
+ educommon/auth/rbac/models.py,sha256=fnLEYsr22ylGdefhT8bZz3mO8MZv8yAM5k6K8GXZYLQ,15214
83
83
  educommon/auth/rbac/permissions.py,sha256=rvAYUhWiB4xln39BgxRaXs7T5IzHAg0ZAtKgU__uPk8,865
84
84
  educommon/auth/rbac/ui.py,sha256=RK3YpoKyxKoJyK_qaBzV6HpwffA_S99ZzQbNKS134Ng,15735
85
85
  educommon/auth/rbac/utils.py,sha256=L9F5yXfDhkrgVHjgqsO7r5gYrfbM32OVAMiH_bMU4hQ,7819
@@ -122,7 +122,7 @@ educommon/contingent/base.py,sha256=jo-8ViLDsb7-AgIVDbkfw6DMvYG-AfMFtO4WGsROCr8,
122
122
  educommon/contingent/catalogs.py,sha256=OafO1jDi7q7wm_7cnu8k-xiUc6mwKhXE36HklJX2Sb4,61449
123
123
  educommon/contingent/contingent_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
124
  educommon/contingent/contingent_plugin/actions.py,sha256=SXjFCDGyFHrOZNcroEouRLZB7NqFSs7M_1Gw89bfe48,717
125
- educommon/contingent/contingent_plugin/apps.py,sha256=ctT2ZbSh_7t0MT9apHAynxw22JHQ2YSrSnAJzXx4giM,697
125
+ educommon/contingent/contingent_plugin/apps.py,sha256=2RustoCdwosXEIXpXlgK1UVfFvx729K_lP6F6l8xCr8,810
126
126
  educommon/contingent/contingent_plugin/model_views.py,sha256=Xk-Xck9GsiB2AGdW750fsGtT2p1hybZmGI5hE2JhIwM,769
127
127
  educommon/contingent/contingent_plugin/models.py,sha256=Du9Ks2irXNLDmH0yE5fmID--eMyWufHYJihOdvEbceA,2506
128
128
  educommon/contingent/contingent_plugin/observer.py,sha256=JywNusbhDhTHe1sbWS_joSvK2sB_-b3yYFdEY4ZYX08,5181
@@ -145,20 +145,22 @@ educommon/django/db/signals.py,sha256=pbBr31q3hK6aBW6FZXYmATxKp8KrCuqmrNdDZNnpw3
145
145
  educommon/django/db/utils.py,sha256=S9rZhfLn4GYWXiVOPSsEGl382NzUFeOxh-G7OtB43vE,9206
146
146
  educommon/django/db/migration/__init__.py,sha256=FwICNxrFGFwzy5klIKzYIhVpsunLipS4W8ysS8C0z5k,1882
147
147
  educommon/django/db/migration/operations.py,sha256=R_SCK8R1IF6x5qjJPoTnGjiAedqnBT-OpaEjyyJaxVM,9527
148
- educommon/django/db/mixins/__init__.py,sha256=Ogjjua1VNYQNlKQJjevrby7qgwlvC9noQ5v3TJ6eViA,14740
148
+ educommon/django/db/mixins/__init__.py,sha256=SnFhMU701HuRcmS8QuIx4SS3B4vRdVsfwK7SgrqDD0U,14746
149
149
  educommon/django/db/mixins/date_interval.py,sha256=9Ro7KxqSf-dskU9MjAra3aS2a09_11cv1Tol7JWQw3g,24715
150
150
  educommon/django/db/mixins/validation.py,sha256=u3SDRSDvEkOjEMgfgCIFtnTUk4LRPdru0swzTGDSqIY,12173
151
151
  educommon/django/db/model_view/__init__.py,sha256=x0rHvR0q6RSDtuMWDGASrtCIYsytfNh-jHcsra0BTZY,12363
152
152
  educommon/django/db/model_view/table-view.html,sha256=EEz_tSJagl-mT-_bJaELshAEnFnuhPerpWegb3XeGO8,631
153
- educommon/django/db/partitioning/README.md,sha256=w4_eFovGlBYAMt-zN7ENv7zHpJyK7L1Jlmbdqt0OiXY,4455
154
- educommon/django/db/partitioning/__init__.py,sha256=PqHa7IymPftrIXtDo4iWKBVw1PAIizl0iI2-dONFwQM,22376
155
- educommon/django/db/partitioning/partitioning.sql,sha256=PKKYKgn9MXQtt1vg498XdWD0vGYkzxc_sPKIWmpaSrk,20345
153
+ educommon/django/db/partitioning/README.md,sha256=OgmFZwK_wD1EFKRprnFkeF0Rt7igcEuBncsOF9CsBjI,6927
154
+ educommon/django/db/partitioning/__init__.py,sha256=SGUfTkw3iWcWXjOk7tcXc7RiYpeACssc5O8AMie0CbI,24083
155
+ educommon/django/db/partitioning/const.py,sha256=52Ue7NylmDUMfV0kbk5fqCaE045IhMIh5ZX9juedYbY,927
156
+ educommon/django/db/partitioning/partitioning.sql,sha256=UfPrtxy82tIr3XjXg_qTg-rUS0KqUyBexp3TBPdU0gk,21304
157
+ educommon/django/db/partitioning/partitioning_set_search_path.sql,sha256=oGlljp2A6xd3FctJAqPAO7RCBSar7C_nsXEu8Ssi0WM,342
156
158
  educommon/django/db/partitioning/triggers.sql,sha256=OEwQqMwzRB79LcFM0-cWH8x_UdLKw-iZTSPILTGJ1Uk,3401
157
159
  educommon/django/db/partitioning/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
158
160
  educommon/django/db/partitioning/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
159
- educommon/django/db/partitioning/management/commands/apply_partitioning.py,sha256=oAsPDLD1YAQ0zU2zGPNlwDaNGCrnVBNC86U1I5xcuoM,1612
160
- educommon/django/db/partitioning/management/commands/clear_table.py,sha256=c3sWVCsxOcrIhPqck1om5_Od7IKe8SjMVwRNa0cPMaE,2235
161
- educommon/django/db/partitioning/management/commands/split_table.py,sha256=UOFbOWNTvjJLIRzByCv_VpGCsTct-oGONsyYXJgCSpo,2082
161
+ educommon/django/db/partitioning/management/commands/apply_partitioning.py,sha256=iDpw7Fl_PvuQK0R6yfcwKM8s6ctRi7eLMaW-qEaJLNM,3174
162
+ educommon/django/db/partitioning/management/commands/clear_table.py,sha256=dohXtylU31ZxOJ1XPOrDcYi9DCA1fU13HWGJ_LgHqW4,2785
163
+ educommon/django/db/partitioning/management/commands/split_table.py,sha256=gZwTsewKmToaeHh1MpDjttKUrCGYAxAKEy63e7EyPfA,2756
162
164
  educommon/django/db/validators/__init__.py,sha256=Hyx-L4suVzUuCEb-HQB9g8LnaxKQp2ymv-DtaOW6qbw,1986
163
165
  educommon/django/db/validators/simple.py,sha256=cwG-OLVPdD146A9dzLOaGyzZOoUWgQoFu9KDQ7_k8xI,38886
164
166
  educommon/django/storages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -214,11 +216,11 @@ educommon/m3/extensions/listeners/__init__.py,sha256=ax097TgSn7rWH8L3DSiVCc1MOKn
214
216
  educommon/m3/extensions/listeners/delete_check/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
215
217
  educommon/m3/extensions/listeners/delete_check/cancel-confirm-window.js,sha256=Xh0Rzk9ua9NtR9TR0x7oPn0Q96IKqBQF0XkkFzTOUVI,916
216
218
  educommon/m3/extensions/listeners/delete_check/listeners.py,sha256=r_PhNLlt16Tcikl4lOmhG-E7Hl7KL596ZQL3cEr3f8w,6131
217
- educommon/m3/extensions/listeners/delete_check/mixins.py,sha256=DmmTepSHbPBZ49_hdueqHtD2oHjkR_JldlmozdfpDhQ,7660
219
+ educommon/m3/extensions/listeners/delete_check/mixins.py,sha256=kelvluswqu8u2h5QyVs_VEfwatkVWrSVbRlv4skScmw,7823
218
220
  educommon/m3/extensions/listeners/delete_check/related-objects-window.html,sha256=Do_SnmWfexnhDzgvr7o7JJubH4N8pvPNa268cd-JIcA,94
219
221
  educommon/m3/extensions/listeners/delete_check/signals.py,sha256=QnfevsAKpKtOE_EBc7h1qg4hTV4LxXYL5ivvpHyc704,467
220
222
  educommon/m3/extensions/listeners/delete_check/ui.py,sha256=W3sBsZGbF3q0rxuMUEmycBvFngUh--UQ-77xdjgvDP0,3507
221
- educommon/m3/extensions/listeners/delete_check/utils.py,sha256=m4vlKh7zhLbIRV2A8KDS54n1u_t1cL70p6o5E3VwQyQ,2979
223
+ educommon/m3/extensions/listeners/delete_check/utils.py,sha256=87OmVAzx2lX_7M12Tsgr6cZocrnvuwnDMpIrx23Xmzw,3092
222
224
  educommon/objectpack/__init__.py,sha256=TwmrbrumJoH9KjYhbLnKExWmo4A0UQeyNYTK__93Vr8,69
223
225
  educommon/objectpack/actions.py,sha256=KanZUKMl4aUSRPZV-SJB3JwjDTYzuGweI9L5cE1KziI,14735
224
226
  educommon/objectpack/apps.py,sha256=LZl_kQ9s7G9pQ4a5_mWNPN52mFWJ7RGvGIHCHwhKYPo,220
@@ -238,7 +240,7 @@ educommon/report/constructor/README.rst,sha256=M6bnt3eAdRMUSIbRXdbuleron11RajRmT
238
240
  educommon/report/constructor/__init__.py,sha256=sMDEr17Etf69p4NKRelFv9ySrNT6JulFO1IByVfZA1w,1252
239
241
  educommon/report/constructor/app_meta.py,sha256=4hFKVlGCO4Mlu46fCw5k7iD9kSse8b9MQt3tRV5WyfE,200
240
242
  educommon/report/constructor/apps.py,sha256=NUp1dCkzHduZFXT6XPDQPasAei3Bs3TYdEQzXhqn1J8,371
241
- educommon/report/constructor/base.py,sha256=QUwT-hksW-ho0t-fEXslJ6cpTVRrjLFAU6guCDcqc7Y,27289
243
+ educommon/report/constructor/base.py,sha256=ABSNm1QrUyN4E6RybN7NM72D5W_4U6bz4vvxyTkIsNA,27333
242
244
  educommon/report/constructor/config.py,sha256=6PjPv4A3zww_DnZqE-aj72zduW7cZqbzVwuCP8JDsYg,1058
243
245
  educommon/report/constructor/constants.py,sha256=VF3tu6vEWrN37oPyipD3LGCElbaY2JHGaKy7AxUbQIU,3381
244
246
  educommon/report/constructor/exceptions.py,sha256=NL2mQggBHbkHaQFKpr9mTj7R5IVtxtCRKzJ00dgfTqM,1287
@@ -246,7 +248,7 @@ educommon/report/constructor/mixins.py,sha256=4Oh3JB3VRr8Es6yLS3W1sXVPrV61SVPm-v
246
248
  educommon/report/constructor/models.py,sha256=g-fbs_kM16GBh_5MY0NenXc8cVwihDLVXz3uS_JN1Mg,18764
247
249
  educommon/report/constructor/plugin_meta.py,sha256=52IdIKr7ArMiHOnyZlKV5Fn6PxMhSbttipXz_Vbfkmg,114
248
250
  educommon/report/constructor/registries.py,sha256=aTFeW0htAeDYWd9HlLqn3GTK3mttjS8I8LddPc6Ea7M,1883
249
- educommon/report/constructor/utils.py,sha256=KR2wDuvxOZXqHeq1IklpNXyLRrLSJEv5jsd3w2fKIqA,6557
251
+ educommon/report/constructor/utils.py,sha256=lXfKGlqPhmlnSLg4O0FTxRd3kLQtx3WlLQ7z84lbres,6563
250
252
  educommon/report/constructor/validators.py,sha256=E_VKNz0Z3YqJOJTjd7OdZXFMDnW8UUiVUIH9Jk6Irtw,524
251
253
  educommon/report/constructor/builders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
252
254
  educommon/report/constructor/builders/excel/__init__.py,sha256=ZjiilnR4FRJm0DM06G3yKHBEDAPXs5JvuPO3JOg2FB8,104
@@ -256,7 +258,7 @@ educommon/report/constructor/builders/excel/constants.py,sha256=SM5OEkd7CMrGhuCp
256
258
  educommon/report/constructor/builders/excel/product.py,sha256=NsfjhUViZyi_XpDjiUJDwfgHVRlE98v40RMAa6mpAzk,6229
257
259
  educommon/report/constructor/builders/excel/with_merged_cells.py,sha256=mOlzkfqkbjTc3u9VsUyoNUAgH3AKc351E4Jz-5J6OTs,5150
258
260
  educommon/report/constructor/editor/__init__.py,sha256=WS6vGDp9CjGE-e_-AgywqN-W67tv5F_WcPgebLHwKbg,41
259
- educommon/report/constructor/editor/actions.py,sha256=_9ZKrUPxQTN1lfJaPRIuYpYLC39t3Yuq273TrUAAST4,38810
261
+ educommon/report/constructor/editor/actions.py,sha256=EHJKR3AxwAPcK02YmL2U8ti-brstJK0ZzqulhoJ6oyk,38815
260
262
  educommon/report/constructor/editor/edit-window.js,sha256=AcbwuumR3k6Nzh8PNz8Rz35hREjl0cyenPI_3BcLq6o,39225
261
263
  educommon/report/constructor/editor/list-window.js,sha256=S2KQGqg5GnFMMYmPNEm_Yf9nXTbfwvIfs4E3LgWO9-0,3247
262
264
  educommon/report/constructor/editor/ui.py,sha256=sZjfiiQa2LIcfoyczPlMz3ImzO7TSNNAaV2We5hywIc,24628
@@ -342,8 +344,8 @@ educommon/ws_log/smev/exceptions.py,sha256=lmy7o2T3dJkqgIhG07qyh5yPqO3qZAYABuT4J
342
344
  educommon/ws_log/templates/report/smev_logs.xlsx,sha256=nnYgB0Z_ix8HoxsRICjsZfFRQBdra-5Gd8nWhCxTjYg,10439
343
345
  educommon/ws_log/templates/ui-js/smev-logs-list-window.js,sha256=AGup3D8GTJSY9WdDPj0zBJeYQBFOmGgcbxPOJbKK-nY,513
344
346
  educommon/ws_log/templates/ui-js/smev-logs-report-setting-window.js,sha256=nQ7QYK9frJcE7g7kIt6INg9TlEEJAPPayBJgRaoTePA,1103
345
- educommon-3.9.4.dist-info/METADATA,sha256=2hmH1DlhM909yL-K__dPJVJVZr_yCDrhO9HBNGZsXu8,1486
346
- educommon-3.9.4.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
347
- educommon-3.9.4.dist-info/dependency_links.txt,sha256=RNlr4t-BxZRm7e_IfVo1ikr5ln-7viimzLHvQMO1C_Q,43
348
- educommon-3.9.4.dist-info/top_level.txt,sha256=z5fbW7bz_0V1foUm_FGcZ9_MTpW3N1dBN7-kEmMowl4,10
349
- educommon-3.9.4.dist-info/RECORD,,
347
+ educommon-3.9.6.dist-info/METADATA,sha256=S0A8Y2rjK_IgCSYD5Gs3bpq3RSLUYzUioswDSuDlANs,1562
348
+ educommon-3.9.6.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
349
+ educommon-3.9.6.dist-info/dependency_links.txt,sha256=RNlr4t-BxZRm7e_IfVo1ikr5ln-7viimzLHvQMO1C_Q,43
350
+ educommon-3.9.6.dist-info/top_level.txt,sha256=z5fbW7bz_0V1foUm_FGcZ9_MTpW3N1dBN7-kEmMowl4,10
351
+ educommon-3.9.6.dist-info/RECORD,,