edu-rdm-integration 3.21.0__py3-none-any.whl → 3.22.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 edu-rdm-integration might be problematic. Click here for more details.

Files changed (21) hide show
  1. edu_rdm_integration/pipelines/cleanup_outdated_data/management/commands/rdm_cleanup_outdated_data.py +32 -1
  2. edu_rdm_integration/stages/collect_data/migrations/0005_alter_rdmcollectingdatasubstage_previous.py +22 -0
  3. edu_rdm_integration/stages/collect_data/migrations/0006_fix_fk_constraints.py +59 -0
  4. edu_rdm_integration/stages/collect_data/registry/actions.py +1 -1
  5. edu_rdm_integration/stages/export_data/migrations/0003_fix_fk_constraints.py +76 -0
  6. edu_rdm_integration/stages/service/service_outdated_data/cleaners/__init__.py +0 -0
  7. edu_rdm_integration/stages/service/service_outdated_data/cleaners/base.py +214 -0
  8. edu_rdm_integration/stages/service/service_outdated_data/cleaners/collect_data.py +126 -0
  9. edu_rdm_integration/stages/service/service_outdated_data/cleaners/consts.py +12 -0
  10. edu_rdm_integration/stages/service/service_outdated_data/cleaners/export_data.py +154 -0
  11. edu_rdm_integration/stages/service/service_outdated_data/cleaners/upload_data.py +64 -0
  12. edu_rdm_integration/stages/service/service_outdated_data/managers.py +80 -0
  13. edu_rdm_integration/stages/upload_data/migrations/0003_auto_20251006_1417.py +28 -0
  14. edu_rdm_integration/stages/upload_data/migrations/0004_fix_fk_constraints.py +79 -0
  15. edu_rdm_integration/stages/upload_data/models.py +5 -2
  16. edu_rdm_integration/stages/utils.py +49 -0
  17. {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.22.0.dist-info}/METADATA +11 -1
  18. {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.22.0.dist-info}/RECORD +21 -8
  19. {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.22.0.dist-info}/WHEEL +0 -0
  20. {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.22.0.dist-info}/licenses/LICENSE +0 -0
  21. {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.22.0.dist-info}/top_level.txt +0 -0
@@ -11,6 +11,9 @@ from edu_rdm_integration.rdm_models.models import (
11
11
  from edu_rdm_integration.stages.service.model_outdated_data.managers import (
12
12
  ModelOutdatedDataCleanerManager,
13
13
  )
14
+ from edu_rdm_integration.stages.service.service_outdated_data.managers import (
15
+ ServiceOutdatedDataCleanerManager,
16
+ )
14
17
 
15
18
 
16
19
  class Command(BaseCommand):
@@ -33,10 +36,26 @@ class Command(BaseCommand):
33
36
  '--models',
34
37
  action='store',
35
38
  dest='models',
36
- type=lambda ml: [m.strip().upper() for m in ml.strip().split(',')] if ml else None,
39
+ type=lambda ml: [m.strip().upper() for m in ml.split(',')] if ml else None,
37
40
  help=models_help_text,
38
41
  )
39
42
 
43
+ # Аргумент для сервисных стадий
44
+ stages_help_text = (
45
+ 'Значением параметра является перечисление стадий сервисных данных, '
46
+ 'для которых должна быть произведена зачистка устаревших данных. '
47
+ 'Перечисление этапов: collect, export, upload. '
48
+ 'Если стадии не указываются, то зачистка будет произведена для всех стадий. '
49
+ 'Стадии перечисляются через запятую без пробелов.'
50
+ )
51
+ parser.add_argument(
52
+ '--stages',
53
+ action='store',
54
+ dest='stages',
55
+ type=lambda st: [s.strip().lower() for s in st.split(',')] if st else None,
56
+ help=stages_help_text,
57
+ )
58
+
40
59
  parser.add_argument(
41
60
  '--safe',
42
61
  action='store_true',
@@ -62,7 +81,19 @@ class Command(BaseCommand):
62
81
  )
63
82
  model_data_cleaner_manager.run()
64
83
 
84
+ def _cleanup_service_outdated_data(self, options):
85
+ """Очистка устаревших данных сервисных моделей РВД."""
86
+ service_data_cleaner_manager = ServiceOutdatedDataCleanerManager(
87
+ stages=options['stages'],
88
+ safe=options['safe'],
89
+ log_sql=options['log_sql'],
90
+ )
91
+ service_data_cleaner_manager.run()
92
+
65
93
  def handle(self, *args, **options):
66
94
  """Запуск очистки устаревших данных РВД."""
67
95
  if settings.RDM_ENABLE_CLEANUP_MODELS_OUTDATED_DATA:
68
96
  self._cleanup_model_outdated_data(options)
97
+
98
+ if settings.RDM_ENABLE_CLEANUP_SERVICE_OUTDATED_DATA:
99
+ self._cleanup_service_outdated_data(options)
@@ -0,0 +1,22 @@
1
+ # Generated by Django 3.2.24 on 2025-10-08 14:17
2
+
3
+ import django.db.models.deletion
4
+ from django.db import (
5
+ migrations,
6
+ models,
7
+ )
8
+
9
+
10
+ class Migration(migrations.Migration):
11
+
12
+ dependencies = [
13
+ ('edu_rdm_integration_collect_data_stage', '0004_auto_20250704_0825'),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.AlterField(
18
+ model_name='rdmcollectingdatasubstage',
19
+ name='previous',
20
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='edu_rdm_integration_collect_data_stage.rdmcollectingdatasubstage', verbose_name='Предыдущий сбор данных'),
21
+ ),
22
+ ]
@@ -0,0 +1,59 @@
1
+ # Generated by Django 3.2.24 on 2025-10-08 14:43
2
+
3
+ from django.db import (
4
+ migrations,
5
+ )
6
+
7
+ from edu_rdm_integration.stages.utils import (
8
+ update_foreign_key_constraint,
9
+ )
10
+
11
+
12
+ def apply_fk_updates(apps, schema_editor):
13
+ """
14
+ Применяет обновления внешних ключей для конкретных моделей.
15
+
16
+ Здесь задаются таблицы и поведение при удалении записей,
17
+ чтобы синхронизировать фактическое состояние БД с логикой моделей.
18
+ """
19
+ RDMCollectingDataSubStage = apps.get_model('edu_rdm_integration_collect_data_stage', 'RDMCollectingDataSubStage')
20
+ RDMCollectingDataStage = apps.get_model('edu_rdm_integration_collect_data_stage', 'RDMCollectingDataStage')
21
+
22
+ # Модель: RDMCollectingDataSubStage
23
+ # Поле previous_id должно иметь ON DELETE SET NULL
24
+ update_foreign_key_constraint(
25
+ table_name=RDMCollectingDataSubStage._meta.db_table,
26
+ field_name="previous_id",
27
+ target_table=RDMCollectingDataSubStage._meta.db_table,
28
+ on_delete="SET NULL",
29
+ )
30
+
31
+ RDMCollectingDataCommandProgress = apps.get_model('edu_rdm_integration_collect_data_stage', 'RDMCollectingDataCommandProgress')
32
+ # Модель: RDMCollectingDataCommandProgress
33
+ # Поле stage_id должно иметь ON DELETE SET NULL
34
+ update_foreign_key_constraint(
35
+ table_name=RDMCollectingDataCommandProgress._meta.db_table,
36
+ field_name="stage_id",
37
+ target_table=RDMCollectingDataStage._meta.db_table,
38
+ on_delete="SET NULL",
39
+ )
40
+
41
+ # Таблица: rdm_collecting_data_command_progress
42
+ # Поле stage_id должно иметь ON DELETE SET NULL
43
+ update_foreign_key_constraint(
44
+ table_name='rdm_collecting_data_command_progress',
45
+ field_name="stage_id",
46
+ target_table=RDMCollectingDataStage._meta.db_table,
47
+ on_delete="SET NULL",
48
+ )
49
+
50
+
51
+ class Migration(migrations.Migration):
52
+
53
+ dependencies = [
54
+ ('edu_rdm_integration_collect_data_stage', '0005_alter_rdmcollectingdatasubstage_previous'),
55
+ ]
56
+
57
+ operations = [
58
+ migrations.RunPython(apply_fk_updates, reverse_code=migrations.RunPython.noop),
59
+ ]
@@ -278,4 +278,4 @@ class BaseCollectingDataProgressPack(BaseCommandProgressPack):
278
278
  )
279
279
  for command in commands_to_save
280
280
  ]
281
- self.model.objects.bulk_create(objs, batch_size=settings.COLLECT_PROGRESS_BATCH_SIZE)
281
+ self.model.objects.bulk_create(objs, batch_size=settings.RDM_COLLECT_PROGRESS_BATCH_SIZE)
@@ -0,0 +1,76 @@
1
+ # Generated by Django 3.2.24 on 2025-10-08 14:44
2
+
3
+ from django.db import (
4
+ migrations,
5
+ )
6
+
7
+ from edu_rdm_integration.stages.utils import (
8
+ update_foreign_key_constraint,
9
+ )
10
+
11
+
12
+ def apply_fk_updates(apps, schema_editor):
13
+ """
14
+ Применяет обновления внешних ключей для конкретных моделей.
15
+
16
+ Здесь задаются таблицы и поведение при удалении записей,
17
+ чтобы синхронизировать фактическое состояние БД с логикой моделей.
18
+ """
19
+ RDMExportingDataSubStage = apps.get_model('edu_rdm_integration_export_data_stage', 'RDMExportingDataSubStage')
20
+ RDMExportingDataStage = apps.get_model('edu_rdm_integration_export_data_stage', 'RDMExportingDataStage')
21
+
22
+ RDMExportingDataSubStageAttachment = apps.get_model(
23
+ 'edu_rdm_integration_export_data_stage', 'RDMExportingDataSubStageAttachment'
24
+ )
25
+ # Модель: RDMExportingDataSubStageAttachment
26
+ # Поле exporting_data_sub_stage_id должно иметь ON DELETE CASCADE
27
+ update_foreign_key_constraint(
28
+ table_name=RDMExportingDataSubStageAttachment._meta.db_table,
29
+ field_name='exporting_data_sub_stage_id',
30
+ target_table=RDMExportingDataSubStage._meta.db_table,
31
+ on_delete='CASCADE',
32
+ )
33
+
34
+ RDMExportingDataSubStageEntity = apps.get_model(
35
+ 'edu_rdm_integration_export_data_stage', 'RDMExportingDataSubStageEntity'
36
+ )
37
+ # Модель: CollectDataCommandProgress
38
+ # Поле exporting_data_sub_stage_id должно иметь ON DELETE CASCADE
39
+ update_foreign_key_constraint(
40
+ table_name=RDMExportingDataSubStageEntity._meta.db_table,
41
+ field_name='exporting_data_sub_stage_id',
42
+ target_table=RDMExportingDataSubStage._meta.db_table,
43
+ on_delete='CASCADE',
44
+ )
45
+
46
+ RDMExportingDataCommandProgress = apps.get_model(
47
+ 'edu_rdm_integration_export_data_stage', 'RDMExportingDataCommandProgress'
48
+ )
49
+ # Модель: RDMExportingDataCommandProgress
50
+ # Поле stage_id должно иметь ON DELETE SET NULL
51
+ update_foreign_key_constraint(
52
+ table_name=RDMExportingDataCommandProgress._meta.db_table,
53
+ field_name='stage_id',
54
+ target_table=RDMExportingDataStage._meta.db_table,
55
+ on_delete='SET NULL',
56
+ )
57
+
58
+ # Таблица: rdm_exporting_data_command_progress
59
+ # Поле stage_id должно иметь ON DELETE SET NULL
60
+ update_foreign_key_constraint(
61
+ table_name='rdm_exporting_data_command_progress',
62
+ field_name="stage_id",
63
+ target_table=RDMExportingDataStage._meta.db_table,
64
+ on_delete="SET NULL",
65
+ )
66
+
67
+
68
+ class Migration(migrations.Migration):
69
+
70
+ dependencies = [
71
+ ('edu_rdm_integration_export_data_stage', '0002_auto_20250704_0810'),
72
+ ]
73
+
74
+ operations = [
75
+ migrations.RunPython(apply_fk_updates, reverse_code=migrations.RunPython.noop),
76
+ ]
@@ -0,0 +1,214 @@
1
+ from abc import (
2
+ ABCMeta,
3
+ abstractmethod,
4
+ )
5
+
6
+ from django.conf import (
7
+ settings,
8
+ )
9
+ from django.db import (
10
+ connection,
11
+ )
12
+
13
+ from educommon import (
14
+ logger,
15
+ )
16
+
17
+
18
+ class BaseServiceOutdatedDataCleaner(metaclass=ABCMeta):
19
+ """Базовый класс уборщика устаревших сервисных данных."""
20
+
21
+ model = None
22
+
23
+ SELECT_RDM_CHUNK_BOUNDED_SQL = """
24
+ DO $$
25
+ DECLARE
26
+ chunk_size INT := {chunk_size};
27
+ last_id INT := 0;
28
+ first_id INT;
29
+ last_chunk_id INT;
30
+ BEGIN
31
+ DROP TABLE IF EXISTS rdm_chunk_bounds;
32
+ CREATE TEMP TABLE rdm_chunk_bounds (
33
+ chunk_number INT,
34
+ first_id INT,
35
+ last_id INT
36
+ );
37
+
38
+ DROP TABLE IF EXISTS tmp_chunk;
39
+ CREATE TEMP TABLE tmp_chunk (id INT) ON COMMIT DROP;
40
+
41
+ WHILE TRUE LOOP
42
+ TRUNCATE tmp_chunk;
43
+
44
+ INSERT INTO tmp_chunk (id)
45
+ SELECT id
46
+ FROM {table_name}
47
+ WHERE id > last_id
48
+ ORDER BY id
49
+ LIMIT chunk_size;
50
+
51
+ IF NOT FOUND THEN
52
+ EXIT;
53
+ END IF;
54
+
55
+ SELECT MIN(id), MAX(id)
56
+ INTO first_id, last_chunk_id
57
+ FROM tmp_chunk;
58
+
59
+ INSERT INTO rdm_chunk_bounds (chunk_number, first_id, last_id)
60
+ VALUES (
61
+ (SELECT COUNT(*) FROM rdm_chunk_bounds) + 1,
62
+ first_id,
63
+ last_chunk_id
64
+ );
65
+
66
+ last_id := last_chunk_id;
67
+ END LOOP;
68
+ END $$;
69
+
70
+ SELECT * FROM rdm_chunk_bounds ORDER BY chunk_number;
71
+ """
72
+
73
+ REMOVE_OUTDATED_DATA_SQL = """
74
+ WITH deleted_rows AS (
75
+ DELETE FROM {table_name}
76
+ WHERE id IN (
77
+ WITH tbl AS (
78
+ SELECT *
79
+ FROM {table_name}
80
+ WHERE id >= {first_id}
81
+ AND id <= {last_id}
82
+ )
83
+ SELECT tbl.id
84
+ FROM tbl
85
+ WHERE {conditions}
86
+ )
87
+ RETURNING id
88
+ )
89
+ SELECT COUNT(*) AS deleted_count FROM deleted_rows;
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ *args,
95
+ safe: bool = False,
96
+ log_sql: bool = False,
97
+ **kwargs
98
+ ):
99
+ """Инициализация уборщика."""
100
+ self._safe = safe
101
+ self._log_sql = log_sql
102
+ self._deleted_count = 0
103
+
104
+ super().__init__(*args, **kwargs)
105
+
106
+ @abstractmethod
107
+ def get_merged_conditions(self) -> str:
108
+ """Возвращает условия для удаления устаревших данных."""
109
+
110
+ @classmethod
111
+ def get_table_name(cls) -> str:
112
+ """Возвращает имя таблицы в базе данных."""
113
+ if cls.model is None:
114
+ raise NotImplementedError('Необходимо задать атрибут "model"')
115
+ return cls.model._meta.db_table
116
+
117
+ def get_orphan_reference_condition(
118
+ self,
119
+ reference_table: str,
120
+ reference_field: str,
121
+ local_field: str = 'id'
122
+ ) -> str:
123
+ """Условие проверки отсутствия записей в связанной таблице."""
124
+ return f"""
125
+ NOT EXISTS (
126
+ SELECT 1
127
+ FROM {reference_table} ref
128
+ WHERE ref.{reference_field} = tbl.{local_field}
129
+ )
130
+ """
131
+
132
+ def get_status_condition(
133
+ self,
134
+ related_table: str,
135
+ related_field: str,
136
+ status_value: str,
137
+ days: int,
138
+ local_field: str = 'id'
139
+ ) -> str:
140
+ """Условие проверки записи с заданным статусом и возрастом."""
141
+ return f"""
142
+ EXISTS (
143
+ SELECT 1
144
+ FROM {related_table} sub
145
+ WHERE sub.{related_field} = tbl.{local_field}
146
+ AND sub.status_id = '{status_value}'
147
+ AND sub.ended_at <= NOW() - INTERVAL '{days} days'
148
+ )
149
+ """
150
+
151
+ def get_chunk_bounds(self):
152
+ """Возвращает границы чанков для текущей таблицы."""
153
+ sql = self.SELECT_RDM_CHUNK_BOUNDED_SQL.format(
154
+ table_name=self.get_table_name(),
155
+ chunk_size=settings.CLEANUP_MODELS_OUTDATED_DATA_CHUNK_SIZE,
156
+ )
157
+ with connection.cursor() as cursor:
158
+ cursor.execute(sql)
159
+ return cursor.fetchall()
160
+
161
+ def _log_query(self, sql: str):
162
+ """Логирует SQL-запрос."""
163
+ try:
164
+ import sqlparse
165
+ sql = sqlparse.format(sql, reindent=True, strip_comments=True)
166
+ except ImportError:
167
+ pass
168
+
169
+ logger.info(
170
+ f'Запрос для удаления устаревших данных модели {self.get_table_name()}:\n{sql}\n'
171
+ )
172
+
173
+ def _execute_delete_sql(self, delete_sql: str) -> int:
174
+ """Выполняет SQL-запрос на удаление (или только логирует в safe-режиме)."""
175
+ deleted = 0
176
+ if self._log_sql:
177
+ self._log_query(delete_sql)
178
+
179
+ if self._safe:
180
+ logger.info(
181
+ f'Безопасный режим включен — запрос удаления для {self.get_table_name()} не выполнен.'
182
+ )
183
+ return 0
184
+ else:
185
+ with connection.cursor() as cursor:
186
+ cursor.execute(delete_sql)
187
+ result = cursor.fetchone()
188
+ deleted = result[0] if result else 0
189
+
190
+ return deleted
191
+
192
+ def run(self):
193
+ """Запуск очистки данных."""
194
+ conditions = self.get_merged_conditions()
195
+
196
+ # Разделяем по чанкам
197
+ chunk_bounded = self.get_chunk_bounds()
198
+ for chunk_number, first_id, last_id in chunk_bounded:
199
+ while True:
200
+ delete_sql = self.REMOVE_OUTDATED_DATA_SQL.format(
201
+ table_name=self.get_table_name(),
202
+ first_id=first_id,
203
+ last_id=last_id,
204
+ conditions=conditions,
205
+ )
206
+ deleted = self._execute_delete_sql(delete_sql)
207
+ self._deleted_count += deleted
208
+
209
+ if deleted < self.chunk_size:
210
+ break
211
+
212
+ logger.info(
213
+ f'Удалено устаревших записей сервисной модели {self.model.__name__}: {self._deleted_count}'
214
+ )
@@ -0,0 +1,126 @@
1
+ from typing import (
2
+ Optional,
3
+ )
4
+
5
+ from django.core.exceptions import (
6
+ FieldDoesNotExist,
7
+ )
8
+ from django.db.models import (
9
+ Subquery,
10
+ )
11
+
12
+ from edu_rdm_integration.rdm_models.models import (
13
+ RDMModelEnum,
14
+ )
15
+ from edu_rdm_integration.stages.collect_data.models import (
16
+ RDMCollectingDataCommandProgress,
17
+ RDMCollectingDataStage,
18
+ RDMCollectingDataSubStage,
19
+ )
20
+ from educommon.utils.seqtools import (
21
+ make_chunks,
22
+ )
23
+
24
+ from web_edu.plugins.regional_data_mart_integration.models import (
25
+ LessonClass,
26
+ )
27
+
28
+ from .base import (
29
+ BaseServiceOutdatedDataCleaner,
30
+ )
31
+ from .consts import (
32
+ OLD_RDM_MODEL,
33
+ UNION_CHUNK_SIZE,
34
+ )
35
+
36
+
37
+ class CollectingDataSubStageCleaner(BaseServiceOutdatedDataCleaner):
38
+ """Очистка подэтапов сбора данных, которые не ссылаются ни на одну модель РВД.
39
+
40
+ Подход:
41
+ - Проходим по всем моделям, зарегистрированным в RDMModelEnum, и собираем
42
+ значения полей collecting_sub_stage_id (если модель содержит такое поле).
43
+ - Объединяем запросы по моделям в UNION, получая набор валидных id.
44
+ - Удаляем те подэтапы, id которых отсутствуют в полученном наборе.
45
+ """
46
+
47
+ model = RDMCollectingDataSubStage
48
+
49
+ def _get_valid_substage_ids_subquery(self) -> Optional[Subquery]:
50
+ """Подзапрос, возвращающий все допустимые collecting_sub_stage_id из моделей, описанных в RDMModelEnum."""
51
+ model_enum_values = RDMModelEnum.get_model_enum_values()
52
+ all_model = [model_enum.model for model_enum in model_enum_values] + OLD_RDM_MODEL
53
+ chunk_queries = []
54
+
55
+ for enum_values_chunk in make_chunks(all_model, UNION_CHUNK_SIZE, is_list=True):
56
+ qs_list = []
57
+ for model_cls in enum_values_chunk:
58
+ if model_cls in [LessonClass]:
59
+ continue
60
+ try:
61
+ model_cls._meta.get_field('collecting_sub_stage_id')
62
+ except FieldDoesNotExist:
63
+ continue
64
+
65
+ qs_list.append(model_cls.objects.values('collecting_sub_stage_id'))
66
+
67
+ if qs_list:
68
+ chunk_union = qs_list[0].union(*qs_list[1:])
69
+ chunk_queries.append(chunk_union)
70
+
71
+ if not chunk_queries:
72
+ return
73
+
74
+ # Объединяем все чанки в один общий UNION
75
+ full_union = chunk_queries[0].union(*chunk_queries[1:])
76
+
77
+ return Subquery(full_union)
78
+
79
+ def get_merged_conditions(self) -> str:
80
+ """Формирует условие удаления для устаревших данных.
81
+
82
+ Удаляем подэтапы, которых нет в объединённом наборе валидных collecting_sub_stage_id
83
+ (т.е. подэтапы, не используемые ни одной моделью данных).
84
+ """
85
+ conditions = ''
86
+ subquery = self._get_valid_substage_ids_subquery()
87
+ if subquery:
88
+ conditions = f"""
89
+ NOT EXISTS (
90
+ SELECT collecting_sub_stage_id
91
+ FROM ({str(subquery.query)}) AS valid
92
+ WHERE valid.collecting_sub_stage_id = tbl.id
93
+ )
94
+ """
95
+
96
+ return conditions
97
+
98
+
99
+ class CollectingDataStageCleaner(BaseServiceOutdatedDataCleaner):
100
+ """Очистка этапов сбора данных, у которых нет связанных подэтапов."""
101
+
102
+ model = RDMCollectingDataStage
103
+
104
+ def get_merged_conditions(self) -> str:
105
+ """Формирует условие удаления для устаревших данных."""
106
+ sub_stage_table = CollectingDataSubStageCleaner.get_table_name()
107
+
108
+ return self.get_orphan_reference_condition(sub_stage_table, 'stage_id')
109
+
110
+
111
+ class CollectingDataCommandProgressCleaner(BaseServiceOutdatedDataCleaner):
112
+ """Очистка устаревших хранящихся задач по сбору данных."""
113
+
114
+ model = RDMCollectingDataCommandProgress
115
+
116
+ def get_merged_conditions(self) -> str:
117
+ """Формирует условие удаления для устаревших данных."""
118
+ stage_table = CollectingDataStageCleaner.get_table_name()
119
+ conditions = [
120
+ 'stage_id IS NULL',
121
+ f'({self.get_status_condition(stage_table, "id", "FINISHED", 7, "stage_id")})',
122
+ f'({self.get_status_condition(stage_table, "id", "FAILED", 30, "stage_id")})',
123
+ f'({self.get_orphan_reference_condition(stage_table, "id", "stage_id")})',
124
+ ]
125
+
126
+ return " OR ".join(conditions)
@@ -0,0 +1,12 @@
1
+ from web_edu.plugins.regional_data_mart_integration.models.homework import Homework
2
+ from web_edu.plugins.regional_data_mart_integration.models.homework_material import HomeworkMaterial
3
+ from web_edu.plugins.regional_data_mart_integration.models.homework_student import HomeworkStudent
4
+
5
+
6
+ UNION_CHUNK_SIZE = 5
7
+
8
+ OLD_RDM_MODEL = [
9
+ HomeworkStudent,
10
+ HomeworkMaterial,
11
+ Homework,
12
+ ]
@@ -0,0 +1,154 @@
1
+ from typing import (
2
+ Optional,
3
+ )
4
+
5
+ from django.core.exceptions import (
6
+ FieldDoesNotExist,
7
+ )
8
+ from django.db.models import (
9
+ Subquery,
10
+ )
11
+
12
+ from edu_rdm_integration.rdm_models.models import (
13
+ RDMModelEnum,
14
+ )
15
+ from edu_rdm_integration.stages.export_data.models import (
16
+ RDMExportingDataCommandProgress,
17
+ RDMExportingDataStage,
18
+ RDMExportingDataSubStage,
19
+ RDMExportingDataSubStageAttachment,
20
+ RDMExportingDataSubStageEntity,
21
+ )
22
+ from educommon.utils.seqtools import (
23
+ make_chunks,
24
+ )
25
+
26
+ from .base import (
27
+ BaseServiceOutdatedDataCleaner,
28
+ )
29
+ from .consts import (
30
+ OLD_RDM_MODEL,
31
+ UNION_CHUNK_SIZE,
32
+ )
33
+
34
+
35
+ class ExportingDataSubStageCleaner(BaseServiceOutdatedDataCleaner):
36
+ """Очистка подэтапов выгрузки данных, которые не ссылаются ни на одну модель РВД.
37
+
38
+ Подход:
39
+ - Проходим по всем моделям, зарегистрированным в RDMModelEnum, и собираем
40
+ значения полей exporting_sub_stage_id (если модель содержит такое поле).
41
+ - Объединяем запросы по моделям в UNION, получая набор валидных id.
42
+ - Удаляем те подэтапы, id которых отсутствуют в полученном наборе.
43
+ """
44
+ model = RDMExportingDataSubStage
45
+
46
+ def _get_valid_substage_ids_subquery(self) -> Optional[Subquery]:
47
+ """Подзапрос, возвращающий все допустимые exporting_sub_stage_id из моделей, описанных в RDMModelEnum."""
48
+ model_enum_values = RDMModelEnum.get_model_enum_values()
49
+ all_model = [model_enum.model for model_enum in model_enum_values] + OLD_RDM_MODEL
50
+ chunk_queries = []
51
+
52
+ for enum_values_chunk in make_chunks(all_model, UNION_CHUNK_SIZE, is_list=True):
53
+ qs_list = []
54
+ for model_cls in enum_values_chunk:
55
+ try:
56
+ model_cls._meta.get_field('exporting_sub_stage_id')
57
+ except FieldDoesNotExist:
58
+ continue
59
+ qs_list.append(model_cls.objects.values('exporting_sub_stage_id'))
60
+
61
+ if qs_list:
62
+ chunk_union = qs_list[0].union(*qs_list[1:])
63
+ chunk_queries.append(chunk_union)
64
+
65
+ if not chunk_queries:
66
+ return
67
+
68
+ # Объединяем все чанки в один общий UNION
69
+ full_union = chunk_queries[0].union(*chunk_queries[1:])
70
+
71
+ return Subquery(full_union.values('exporting_sub_stage_id'))
72
+
73
+ def get_merged_conditions(self) -> str:
74
+ """Формирует условие удаления для устаревших данных.
75
+
76
+ Удаляем подэтапы, которых нет в объединённом наборе валидных exporting_sub_stage_id
77
+ (т.е. подэтапы, не используемые ни одной моделью данных).
78
+ """
79
+ conditions = ''
80
+ subquery = self._get_valid_substage_ids_subquery()
81
+ if subquery:
82
+ conditions = f"""
83
+ NOT EXISTS (
84
+ SELECT exporting_sub_stage_id
85
+ FROM ({str(subquery.query)}) AS valid
86
+ WHERE valid.exporting_sub_stage_id = tbl.id
87
+ )
88
+ """
89
+
90
+ return conditions
91
+
92
+
93
+ class ExportingDataStageCleaner(BaseServiceOutdatedDataCleaner):
94
+ """Очистка этапов выгрузки данных без подэтапов."""
95
+
96
+ model = RDMExportingDataStage
97
+
98
+ def get_merged_conditions(self) -> str:
99
+ """Формирует условие удаления для устаревших данных."""
100
+ sub_stage_table = ExportingDataSubStageCleaner.get_table_name()
101
+
102
+ return self.get_orphan_reference_condition(sub_stage_table, 'stage_id')
103
+
104
+
105
+ class ExportingDataSubStageAttachmentCleaner(BaseServiceOutdatedDataCleaner):
106
+ """Очистка вложений подэтапов выгрузки данных."""
107
+
108
+ model = RDMExportingDataSubStageAttachment
109
+
110
+ def get_merged_conditions(self) -> str:
111
+ """Формирует условие удаления для устаревших данных."""
112
+ sub_stage_table = ExportingDataSubStageCleaner.get_table_name()
113
+ conditions = [
114
+ f'({self.get_status_condition(sub_stage_table, "id", "FINISHED", 7, "exporting_data_sub_stage_id")})',
115
+ f'({self.get_status_condition(sub_stage_table, "id", "FAILED",30, "exporting_data_sub_stage_id")})',
116
+ f'({self.get_orphan_reference_condition(sub_stage_table, "id", "exporting_data_sub_stage_id")})',
117
+ ]
118
+
119
+ return ' OR '.join(conditions)
120
+
121
+
122
+ class ExportingDataSubStageEntityCleaner(BaseServiceOutdatedDataCleaner):
123
+ """Очистка связей сущности и подэтапов выгрузки данных."""
124
+
125
+ model = RDMExportingDataSubStageEntity
126
+
127
+ def get_merged_conditions(self) -> str:
128
+ """Формирует условие удаления для устаревших данных."""
129
+ sub_stage_table = ExportingDataSubStageCleaner.get_table_name()
130
+ conditions = [
131
+ f'({self.get_status_condition(sub_stage_table, "id", "FINISHED", 7, "exporting_data_sub_stage_id")})',
132
+ f'({self.get_status_condition(sub_stage_table, "id", "FAILED",30, "exporting_data_sub_stage_id")})',
133
+ f'({self.get_orphan_reference_condition(sub_stage_table, "id", "exporting_data_sub_stage_id")})',
134
+ ]
135
+
136
+ return ' OR '.join(conditions)
137
+
138
+
139
+ class ExportingDataCommandProgressCleaner(BaseServiceOutdatedDataCleaner):
140
+ """Очистка устаревших хранящихся задач по экспорту данных."""
141
+
142
+ model = RDMExportingDataCommandProgress
143
+
144
+ def get_merged_conditions(self) -> str:
145
+ """Формирует условие удаления для устаревших данных."""
146
+ stage_table = ExportingDataStageCleaner.get_table_name()
147
+ conditions = [
148
+ 'stage_id IS NULL',
149
+ f'({self.get_status_condition(stage_table, "id", "FINISHED", 7, "stage_id")})',
150
+ f'({self.get_status_condition(stage_table, "id", "FAILED",30, "stage_id")})',
151
+ f'({self.get_orphan_reference_condition(stage_table, "id", "stage_id")})',
152
+ ]
153
+
154
+ return ' OR '.join(conditions)
@@ -0,0 +1,64 @@
1
+ from edu_rdm_integration.stages.upload_data.models import (
2
+ RDMExportingDataSubStageUploaderClientLog,
3
+ RDMUploadStatusRequestLog,
4
+ )
5
+ from uploader_client.models import (
6
+ Entry,
7
+ )
8
+
9
+ from .base import (
10
+ BaseServiceOutdatedDataCleaner,
11
+ )
12
+ from .export_data import (
13
+ ExportingDataSubStageAttachmentCleaner,
14
+ ExportingDataSubStageCleaner,
15
+ )
16
+
17
+
18
+ class ExportingDataSubStageUploaderClientLogCleaner(BaseServiceOutdatedDataCleaner):
19
+ """Очистка логов загрузчика подэтапов выгрузки данных без связи с подэтапами или файлами."""
20
+
21
+ model = RDMExportingDataSubStageUploaderClientLog
22
+
23
+ def get_merged_conditions(self) -> str:
24
+ """Формирует условие удаления для устаревших данных."""
25
+ sub_stage_table = ExportingDataSubStageCleaner.get_table_name()
26
+ attachment_table = ExportingDataSubStageAttachmentCleaner.get_table_name()
27
+
28
+ conditions = [
29
+ f'({self.get_status_condition(sub_stage_table, "id", "FINISHED", 7, "sub_stage_id")})',
30
+ f'({self.get_status_condition(sub_stage_table, "id", "FAILED", 30, "sub_stage_id")})',
31
+ f'({self.get_orphan_reference_condition(sub_stage_table, "id", local_field="sub_stage_id")})',
32
+ f'({self.get_orphan_reference_condition(attachment_table, "id", local_field="attachment_id")})',
33
+ ]
34
+
35
+ return ' OR '.join(conditions)
36
+
37
+
38
+ class UploadStatusRequestLogCleaner(BaseServiceOutdatedDataCleaner):
39
+ """Очистка логов статуса загрузки файла в витрину без связей upload."""
40
+
41
+ model = RDMUploadStatusRequestLog
42
+
43
+ def get_merged_conditions(self) -> str:
44
+ """Формирует условие удаления для устаревших данных."""
45
+ uploader_client_log_table = ExportingDataSubStageUploaderClientLogCleaner.get_table_name()
46
+
47
+ return self.get_orphan_reference_condition(uploader_client_log_table, 'id', local_field='upload_id')
48
+
49
+
50
+ class EntryCleaner(BaseServiceOutdatedDataCleaner):
51
+ """Очистка записей журнала, не связанные ни с upload, ни с логами."""
52
+
53
+ model = Entry
54
+
55
+ def get_merged_conditions(self) -> str:
56
+ """Формирует условие удаления для устаревших данных."""
57
+ uploader_client_log_table = ExportingDataSubStageUploaderClientLogCleaner.get_table_name()
58
+ upload_status_log_table = UploadStatusRequestLogCleaner.get_table_name()
59
+ conditions = [
60
+ f'({self.get_orphan_reference_condition(uploader_client_log_table, "entry_id")})',
61
+ f'({self.get_orphan_reference_condition(upload_status_log_table, "entry_id")})',
62
+ ]
63
+
64
+ return ' AND '.join(conditions)
@@ -0,0 +1,80 @@
1
+ from typing import (
2
+ Optional,
3
+ )
4
+
5
+ from edu_rdm_integration.stages.service.service_outdated_data.cleaners.collect_data import (
6
+ CollectingDataCommandProgressCleaner,
7
+ CollectingDataStageCleaner,
8
+ CollectingDataSubStageCleaner,
9
+ )
10
+ from edu_rdm_integration.stages.service.service_outdated_data.cleaners.export_data import (
11
+ ExportingDataCommandProgressCleaner,
12
+ ExportingDataStageCleaner,
13
+ ExportingDataSubStageAttachmentCleaner,
14
+ ExportingDataSubStageCleaner,
15
+ ExportingDataSubStageEntityCleaner,
16
+ )
17
+ from edu_rdm_integration.stages.service.service_outdated_data.cleaners.upload_data import (
18
+ EntryCleaner,
19
+ ExportingDataSubStageUploaderClientLogCleaner,
20
+ UploadStatusRequestLogCleaner,
21
+ )
22
+ from educommon import (
23
+ logger,
24
+ )
25
+
26
+
27
+
28
+ class ServiceOutdatedDataCleanerManager:
29
+ """Управляет очисткой устаревших сервисных данных по разным этапам."""
30
+
31
+ STAGES_MAPPING = {
32
+ 'collect': [
33
+ CollectingDataSubStageCleaner,
34
+ CollectingDataStageCleaner,
35
+ CollectingDataCommandProgressCleaner,
36
+ ],
37
+ 'export': [
38
+ ExportingDataSubStageCleaner,
39
+ ExportingDataStageCleaner,
40
+ ExportingDataSubStageAttachmentCleaner,
41
+ ExportingDataSubStageEntityCleaner,
42
+ ExportingDataCommandProgressCleaner,
43
+ ],
44
+ 'upload': [
45
+ ExportingDataSubStageUploaderClientLogCleaner,
46
+ UploadStatusRequestLogCleaner,
47
+ EntryCleaner,
48
+ ],
49
+ }
50
+
51
+ def __init__(
52
+ self,
53
+ *args,
54
+ stages: Optional[list[str]]= None,
55
+ safe: bool = False,
56
+ log_sql: bool = False,
57
+ **kwargs
58
+ ):
59
+ """Инициализация менеджера."""
60
+ self._stages = stages
61
+ self._safe = safe
62
+ self._log_sql = log_sql
63
+
64
+ super().__init__(*args, **kwargs)
65
+
66
+ def _process_stage(self, stage: str):
67
+ """Запускает все зарегистрированные уборщики для указанного этапа."""
68
+ cleaners = self.STAGES_MAPPING.get(stage, [])
69
+ if not cleaners:
70
+ logger.info(f'Для этапа "{stage}" нет зарегистрированных уборщиков.')
71
+ return
72
+
73
+ for cleaner_cls in cleaners:
74
+ cleaner_cls(safe=self._safe, log_sql=self._log_sql).run()
75
+
76
+ def run(self):
77
+ """Запускает очистку устаревших данных сервисных моделей РВД."""
78
+ stages_to_process = self._stages or self.STAGES_MAPPING.keys()
79
+ for stage in stages_to_process:
80
+ self._process_stage(stage)
@@ -0,0 +1,28 @@
1
+ # Generated by Django 3.2.24 on 2025-10-08 14:17
2
+
3
+ import django.db.models.deletion
4
+ from django.db import (
5
+ migrations,
6
+ models,
7
+ )
8
+
9
+
10
+ class Migration(migrations.Migration):
11
+
12
+ dependencies = [
13
+ ('uploader_client', '0001_initial'),
14
+ ('edu_rdm_integration_upload_data_stage', '0002_auto_20250704_0810'),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.AlterField(
19
+ model_name='rdmexportingdatasubstageuploaderclientlog',
20
+ name='entry',
21
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploader_client_log', to='uploader_client.entry', verbose_name='Лог запроса и ответа'),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='rdmuploadstatusrequestlog',
25
+ name='entry',
26
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='upload_status_request_log', to='uploader_client.entry', verbose_name='Cвязь запроса статуса с запросом в витрину'),
27
+ ),
28
+ ]
@@ -0,0 +1,79 @@
1
+ # Generated by Django 3.2.24 on 2025-10-08 14:43
2
+
3
+ from django.db import (
4
+ migrations,
5
+ )
6
+
7
+ from edu_rdm_integration.stages.utils import (
8
+ update_foreign_key_constraint,
9
+ )
10
+
11
+
12
+ def apply_fk_updates(apps, schema_editor):
13
+ """
14
+ Применяет обновления внешних ключей для конкретных моделей.
15
+
16
+ Здесь задаются таблицы и поведение при удалении записей,
17
+ чтобы синхронизировать фактическое состояние БД с логикой моделей.
18
+ """
19
+ Entry = apps.get_model('smev_agent_client', 'Entry')
20
+ RDMExportingDataSubStage = apps.get_model('edu_rdm_integration_export_data_stage', 'RDMExportingDataSubStage')
21
+ RDMExportingDataSubStageAttachment = apps.get_model(
22
+ 'edu_rdm_integration_export_data_stage', 'RDMExportingDataSubStageAttachment'
23
+ )
24
+ RDMExportingDataSubStageUploaderClientLog = apps.get_model(
25
+ 'edu_rdm_integration_upload_data_stage', 'RDMExportingDataSubStageUploaderClientLog'
26
+ )
27
+ # Модель: RDMExportingDataSubStageUploaderClientLog
28
+ # Поле sub_stage_id должно иметь ON DELETE CASCADE
29
+ update_foreign_key_constraint(
30
+ table_name=RDMExportingDataSubStageUploaderClientLog._meta.db_table,
31
+ field_name="sub_stage_id",
32
+ target_table=RDMExportingDataSubStage._meta.db_table,
33
+ on_delete="CASCADE",
34
+ )
35
+ # Модель: RDMExportingDataSubStageUploaderClientLog
36
+ # Поле attachment_id должно иметь ON DELETE CASCADE
37
+ update_foreign_key_constraint(
38
+ table_name=RDMExportingDataSubStageUploaderClientLog._meta.db_table,
39
+ field_name="attachment_id",
40
+ target_table=RDMExportingDataSubStageAttachment._meta.db_table,
41
+ on_delete="CASCADE",
42
+ )
43
+ # Модель: RDMExportingDataSubStageUploaderClientLog
44
+ # Поле entry_id должно иметь ON DELETE SET NULL
45
+ update_foreign_key_constraint(
46
+ table_name=RDMExportingDataSubStageUploaderClientLog._meta.db_table,
47
+ field_name="entry_id",
48
+ target_table=Entry._meta.db_table,
49
+ on_delete="SET NULL",
50
+ )
51
+
52
+ RDMUploadStatusRequestLog = apps.get_model('edu_rdm_integration_upload_data_stage', 'RDMUploadStatusRequestLog')
53
+ # Модель: RDMUploadStatusRequestLog
54
+ # Поле upload_id должно иметь ON DELETE CASCADE
55
+ update_foreign_key_constraint(
56
+ table_name=RDMUploadStatusRequestLog._meta.db_table,
57
+ field_name="upload_id",
58
+ target_table=RDMExportingDataSubStageUploaderClientLog._meta.db_table,
59
+ on_delete="CASCADE",
60
+ )
61
+ # Модель: RDMUploadStatusRequestLog
62
+ # Поле entry_id должно иметь ON DELETE SET NULL
63
+ update_foreign_key_constraint(
64
+ table_name=RDMUploadStatusRequestLog._meta.db_table,
65
+ field_name="entry_id",
66
+ target_table=Entry._meta.db_table,
67
+ on_delete="SET NULL",
68
+ )
69
+
70
+
71
+ class Migration(migrations.Migration):
72
+
73
+ dependencies = [
74
+ ('edu_rdm_integration_upload_data_stage', '0003_auto_20251006_1417'),
75
+ ]
76
+
77
+ operations = [
78
+ migrations.RunPython(apply_fk_updates, reverse_code=migrations.RunPython.noop),
79
+ ]
@@ -1,6 +1,7 @@
1
1
  from django.db.models import (
2
2
  CASCADE,
3
3
  PROTECT,
4
+ SET_NULL,
4
5
  BooleanField,
5
6
  CharField,
6
7
  DateTimeField,
@@ -91,7 +92,8 @@ class RDMExportingDataSubStageUploaderClientLog(ReprStrPreModelMixin, BaseObject
91
92
  entry = ForeignKey(
92
93
  to=Entry,
93
94
  verbose_name='Лог запроса и ответа',
94
- on_delete=CASCADE,
95
+ on_delete=SET_NULL,
96
+ null=True,
95
97
  related_name='uploader_client_log',
96
98
  )
97
99
 
@@ -158,7 +160,8 @@ class RDMUploadStatusRequestLog(ReprStrPreModelMixin, BaseObjectModel):
158
160
  entry = ForeignKey(
159
161
  verbose_name='Cвязь запроса статуса с запросом в витрину',
160
162
  to=Entry,
161
- on_delete=CASCADE,
163
+ null=True,
164
+ on_delete=SET_NULL,
162
165
  related_name='upload_status_request_log',
163
166
  )
164
167
  request_status = ForeignKey(
@@ -0,0 +1,49 @@
1
+ from django.db import (
2
+ connection,
3
+ )
4
+
5
+
6
+ def update_foreign_key_constraint(
7
+ table_name: str,
8
+ field_name: str,
9
+ target_table: str,
10
+ on_delete: str,
11
+ ):
12
+ """
13
+ Обновляет внешний ключ (FOREIGN KEY) в PostgreSQL.
14
+
15
+ Выполняет:
16
+ 1. Поиск существующего constraint'а по таблице и целевой модели.
17
+ 2. Удаление найденного constraint'а.
18
+ 3. Создание нового constraint'а с заданным ON DELETE.
19
+
20
+ Args:
21
+ table_name (str): Имя таблицы, содержащей внешний ключ.
22
+ field_name (str): Имя поля внешнего ключа.
23
+ target_table (str): Таблица, на которую указывает внешний ключ.
24
+ on_delete (str): Поведение при удалении связанной записи.
25
+ """
26
+ with connection.cursor() as cursor:
27
+ # Найти имя constraint'а
28
+ cursor.execute(f"""
29
+ SELECT conname
30
+ FROM pg_constraint
31
+ WHERE conrelid = '{table_name}'::regclass
32
+ AND confrelid = '{target_table}'::regclass
33
+ AND contype = 'f'
34
+ """)
35
+ row = cursor.fetchone()
36
+ if not row:
37
+ return
38
+
39
+ constraint_name = row[0]
40
+
41
+ # Удалить старый constraint, Добавить новый constraint с нужным ON DELETE
42
+ cursor.execute(f'''
43
+ ALTER TABLE "{table_name}"
44
+ DROP CONSTRAINT IF EXISTS "{constraint_name}",
45
+ ADD CONSTRAINT "{constraint_name}"
46
+ FOREIGN KEY ("{field_name}")
47
+ REFERENCES "{target_table}"(id)
48
+ ON DELETE {on_delete};
49
+ ''')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edu-rdm-integration
3
- Version: 3.21.0
3
+ Version: 3.22.0
4
4
  Summary: Интеграция с Региональной витриной данных
5
5
  Author-email: BARS Group <education_dev@bars.group>
6
6
  Project-URL: Homepage, https://stash.bars-open.ru/projects/EDUBASE/repos/edu-rdm-integration/browse
@@ -153,6 +153,7 @@ INSTALLED_APPS = (
153
153
  ('rdm_general', 'EXPORT_CHUNK_SIZE'): 500,
154
154
  ('rdm_general', 'UPLOAD_QUEUE_MAX_SIZE'): 500_000_000,
155
155
  ('rdm_general', 'RDM_MENU_ITEM'): False,
156
+ ('rdm_general', 'COLLECT_PROGRESS_BATCH_SIZE'): 10000,
156
157
  ('rdm_transfer_task', 'MINUTE'): '0',
157
158
  ('rdm_transfer_task', 'HOUR'): '*/4',
158
159
  ('rdm_transfer_task', 'DAY_OF_WEEK'): '*',
@@ -230,6 +231,9 @@ INSTALLED_APPS = (
230
231
  # Пункт меню "Региональная витрина данных" - Спрятать (False) / Отображать (True)
231
232
  RDM_MENU_ITEM = conf.get_bool('rdm_general', 'RDM_MENU_ITEM')
232
233
 
234
+ # Размер батча для bulk_create операций при сборе данных РВД.
235
+ RDM_COLLECT_PROGRESS_BATCH_SIZE = conf.get_int('rmd_general', 'COLLECT_PROGRESS_BATCH_SIZE')
236
+
233
237
  # Настройка запуска периодической задачи выгрузки данных:
234
238
  RDM_TRANSFER_TASK_MINUTE = conf.get('rdm_transfer_task', 'MINUTE')
235
239
  RDM_TRANSFER_TASK_HOUR = conf.get('rdm_transfer_task', 'HOUR')
@@ -315,6 +319,8 @@ INSTALLED_APPS = (
315
319
 
316
320
  # Включить зачистку устаревших данных моделей РВД
317
321
  RDM_ENABLE_CLEANUP_MODELS_OUTDATED_DATA = conf.get_bool('rdm_cleanup_outdated_data', 'ENABLE_CLEANUP_MODELS_OUTDATED_DATA')
322
+ # Включить зачистку устаревших данных сервисных моделей РВД
323
+ RDM_ENABLE_CLEANUP_SERVICE_OUTDATED_DATA = conf.get_bool('rdm_cleanup_outdated_data', 'ENABLE_CLEANUP_MODELS_OUTDATED_DATA')
318
324
  # Размер чанка записей зачистки устаревших данных моделей РВД
319
325
  RDM_CLEANUP_MODELS_OUTDATED_DATA_CHUNK_SIZE = conf.get_int('rdm_cleanup_outdated_data', 'CLEANUP_MODELS_OUTDATED_DATA_CHUNK_SIZE')
320
326
  # Количество подключений к БД для зачистки устаревших данных моделей РВД
@@ -332,6 +338,7 @@ INSTALLED_APPS = (
332
338
  | RDM_UPDATE_NON_EXPORTED_CHUNK_SIZE | # Количество не экспортированных записей моделей обрабатываемых за одну итерацию обновления поля modified | 5000 |
333
339
  | RDM_UPLOAD_QUEUE_MAX_SIZE | Объем очереди файлов в витрину (в байтах). | 500_000_000 |
334
340
  | RDM_MENU_ITEM | Отображение пункта меню Пункт меню "Региональная витрина данных" | False |
341
+ | RDM_COLLECT_PROGRESS_BATCH_SIZE | Размер батча для сохранений записей в базу при сборе данных РВД | 10000 |
335
342
  | RDM_TRANSFER_TASK_MINUTE | Настройка запуска периодической задачи выгрузки данных. Минута | '0' |
336
343
  | RDM_TRANSFER_TASK_HOUR | Настройка запуска периодической задачи выгрузки данных. Час | '*/4' |
337
344
  | RDM_TRANSFER_TASK_DAY_OF_WEEK | Настройка запуска периодической задачи выгрузки данных. День недели | '*' |
@@ -379,6 +386,7 @@ INSTALLED_APPS = (
379
386
  | RDM_EXPORT_LOG_DIR | Директория логов экспорта данных, доступных для скачивания | |
380
387
  | RDM_UPLOAD_LOG_DIR | Директория логов отправки данных в витрину, доступных для скачивания | |
381
388
  | RDM_ENABLE_CLEANUP_MODELS_OUTDATED_DATA | Включение зачистки устаревших данных моделей РВД | False |
389
+ | RDM_ENABLE_CLEANUP_SERVICE_OUTDATED_DATA | Включение зачистки устаревших данных сервисных моделей РВД | False |
382
390
  | RDM_CLEANUP_MODELS_OUTDATED_DATA_CHUNK_SIZE | Размер чанка записей зачистки устаревших данных моделей РВД | 10000 |
383
391
  | RDM_CLEANUP_MODELS_OUTDATED_DATA_POOL_SIZE | Количество подключений к БД для зачистки устаревших данных моделей РВД | 10 |
384
392
 
@@ -395,6 +403,8 @@ INSTALLED_APPS = (
395
403
  EXPORT_CHUNK_SIZE = 500
396
404
  # Количество записей моделей ЭШ обрабатываемых за одну итерацию сбора данных
397
405
  COLLECT_CHUNK_SIZE = 500
406
+ # Размер батча для bulk_create операций при сборе данных РВД
407
+ COLLECT_PROGRESS_BATCH_SIZE = 10000
398
408
  # Количество не экспортированных записей моделей обрабатываемых за одну итерацию обновления поля modified
399
409
  UPDATE_NON_EXPORTED_CHUNK_SIZE = 5000
400
410
  # Объем очереди файлов в витрину (в байтах) - по умолчанию 512 Мбайт.
@@ -48,7 +48,7 @@ edu_rdm_integration/pipelines/cleanup_outdated_data/__init__.py,sha256=47DEQpj8H
48
48
  edu_rdm_integration/pipelines/cleanup_outdated_data/apps.py,sha256=tyTlr1Wt574eby2vIV68FMm7SPFSZKYw6zNkD8x6COE,507
49
49
  edu_rdm_integration/pipelines/cleanup_outdated_data/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  edu_rdm_integration/pipelines/cleanup_outdated_data/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
- edu_rdm_integration/pipelines/cleanup_outdated_data/management/commands/rdm_cleanup_outdated_data.py,sha256=JsmMCv9dQpy0flyJr2gviG0A57FG7JcLAHKO-If_VGY,2909
51
+ edu_rdm_integration/pipelines/cleanup_outdated_data/management/commands/rdm_cleanup_outdated_data.py,sha256=DJU_T6Ezz48Rq6OoScjZ3ZyfHn6drlUo4_Wdj2OGAPY,4500
52
52
  edu_rdm_integration/pipelines/transfer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  edu_rdm_integration/pipelines/transfer/actions.py,sha256=e94NVtTcFIqBBTZ9vbSfh_0oXUWK9ZOx2pDYnIePJVc,5920
54
54
  edu_rdm_integration/pipelines/transfer/app_meta.py,sha256=jshfepDDJrbCACtJBJBPuidAVJ6rcziQiet27wqOIjk,373
@@ -79,6 +79,7 @@ edu_rdm_integration/rdm_models/migrations/0001_initial.py,sha256=qXgObuG2nfOLEnG
79
79
  edu_rdm_integration/rdm_models/migrations/0002_rename_regionaldatamartmodelenum_rdmmodelenum.py,sha256=hNTLriOc9r9WEVKahJURA3yXhZ3ivbwJJ_HaMC46PpI,451
80
80
  edu_rdm_integration/rdm_models/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
81
  edu_rdm_integration/stages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
+ edu_rdm_integration/stages/utils.py,sha256=XTOXXP1KeLCURjkjObP-lcIEARIBkgguk4MfN2dXivQ,1780
82
83
  edu_rdm_integration/stages/collect_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
84
  edu_rdm_integration/stages/collect_data/apps.py,sha256=PhrxzAMXvzXajSlgwLudAgasVjh9IFS5M7a7pGbEKC0,339
84
85
  edu_rdm_integration/stages/collect_data/consts.py,sha256=tzaK9oxzdMRq3oTEocPz4umoXSJWUtFc7YhyXucCNbs,127
@@ -154,9 +155,11 @@ edu_rdm_integration/stages/collect_data/migrations/0001_initial.py,sha256=gujp1W
154
155
  edu_rdm_integration/stages/collect_data/migrations/0002_edurdmcollectdatacommandprogress.py,sha256=TMePcN2Y1wuT1ZKcXgsFTClGWNsf82Ut_mDsPE0BR8Q,4841
155
156
  edu_rdm_integration/stages/collect_data/migrations/0003_auto_20250704_0810.py,sha256=5JkymsRshIKN3az4bbpL_xQxjs6XiknwwCRISS7c2Ts,1079
156
157
  edu_rdm_integration/stages/collect_data/migrations/0004_auto_20250704_0825.py,sha256=B6SUsxlhQvWoD8lFGNwaMUCFDzhPj91bsMdmAcSuEDg,1379
158
+ edu_rdm_integration/stages/collect_data/migrations/0005_alter_rdmcollectingdatasubstage_previous.py,sha256=Q7OBeKXMRf1W_BHcz99UXYjvnav85vYdjoyiDitSXnk,670
159
+ edu_rdm_integration/stages/collect_data/migrations/0006_fix_fk_constraints.py,sha256=H58FBhu43KHy1aw7whdA-1ZktIXIWKwp4Nx3tpLz2nI,2284
157
160
  edu_rdm_integration/stages/collect_data/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
158
161
  edu_rdm_integration/stages/collect_data/registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
159
- edu_rdm_integration/stages/collect_data/registry/actions.py,sha256=ZMk8H1adcI2ycQ0a0ECBVRNKrkeYDc64UEHKV8drpUU,10260
162
+ edu_rdm_integration/stages/collect_data/registry/actions.py,sha256=PiNoiIR6svxPwH9J5Flj9KJdwlYfh9RUqysIlGDNk-Y,10264
160
163
  edu_rdm_integration/stages/collect_data/registry/apps.py,sha256=K5f97YXKMmdM7m33qgQYvJjrA8_eGAJ4VWyuRjJ0gwQ,439
161
164
  edu_rdm_integration/stages/collect_data/registry/ui.py,sha256=pw13DAASxqnX_E5D4RG9CywtnQKQeljXHief7mojVgk,8398
162
165
  edu_rdm_integration/stages/collect_data/registry/templates/ui-js/collect-command-window.js,sha256=QfxVSAA0282-41K0XGtyPa9WPzpoX_uClke8pHdAzBo,3112
@@ -209,6 +212,7 @@ edu_rdm_integration/stages/export_data/management/commands/export_entities_data.
209
212
  edu_rdm_integration/stages/export_data/management/commands/export_latest_entities_data.py,sha256=eYjBs_tZxcUAIseCyvsy5Jk-8k9Gm3xrG2dCuWNnrEs,1163
210
213
  edu_rdm_integration/stages/export_data/migrations/0001_initial.py,sha256=h7HIT-QkWONCzIergDp2c861Lw_-f9TZQ5FPIREeHTk,16864
211
214
  edu_rdm_integration/stages/export_data/migrations/0002_auto_20250704_0810.py,sha256=yjGlH25Mke2_VAe188LEkWZ04Xy1qenai7D2zOA-ecY,1558
215
+ edu_rdm_integration/stages/export_data/migrations/0003_fix_fk_constraints.py,sha256=BjTE7YGk3Qtjqo0MsEJsuR0MeFzolBSm87ivqdrsr4A,2978
212
216
  edu_rdm_integration/stages/export_data/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
213
217
  edu_rdm_integration/stages/export_data/registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
214
218
  edu_rdm_integration/stages/export_data/registry/actions.py,sha256=C5qLwGQeVd9ddOKo2zBJKBLtYn5x_8XV2MaTjK-SGFM,10940
@@ -223,6 +227,13 @@ edu_rdm_integration/stages/service/model_outdated_data/__init__.py,sha256=47DEQp
223
227
  edu_rdm_integration/stages/service/model_outdated_data/cleaners.py,sha256=enlEPTj5_elo-vQTXfq0ar8i1aiN4BzfZQLXNGBsZ68,16795
224
228
  edu_rdm_integration/stages/service/model_outdated_data/managers.py,sha256=0LNjvycTtSMGsN37U-otlPL_vlYJKZXtNXxwkseK1wA,2353
225
229
  edu_rdm_integration/stages/service/service_outdated_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
230
+ edu_rdm_integration/stages/service/service_outdated_data/managers.py,sha256=zKGQWIvv1go5OlBIkgBDLNyEKkkHvm0qKFfCatddd68,2715
231
+ edu_rdm_integration/stages/service/service_outdated_data/cleaners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
232
+ edu_rdm_integration/stages/service/service_outdated_data/cleaners/base.py,sha256=boVxqk2OHYN1-UkOjr3aOx-ClX2YX6OOUnYPrTkZ2d8,6733
233
+ edu_rdm_integration/stages/service/service_outdated_data/cleaners/collect_data.py,sha256=zol47C4RQFnHd_MNldOHTtEjzeWsIZji1nDzKVVkJRg,4894
234
+ edu_rdm_integration/stages/service/service_outdated_data/cleaners/consts.py,sha256=ONLW_NJFbnYU1CbDF4mzBXXqDt7fyKVfep3Qlf5sJOM,384
235
+ edu_rdm_integration/stages/service/service_outdated_data/cleaners/export_data.py,sha256=A0denkwa7N7GxkRZ0tV4FUZAjfWkXpd7UOdYjHLD1Z4,6514
236
+ edu_rdm_integration/stages/service/service_outdated_data/cleaners/upload_data.py,sha256=a8oJ8Sg1eLZf1NemehREkJsvdtmyLwtOIuN5iXpzSb4,2846
226
237
  edu_rdm_integration/stages/upload_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
227
238
  edu_rdm_integration/stages/upload_data/apps.py,sha256=aFhVPK-65b35CGKoAeAgQ0mm3STaWtZg7rqk3eL-b7s,620
228
239
  edu_rdm_integration/stages/upload_data/consts.py,sha256=yTygXxS5dBRCvrE7Q3D0jEGSC5apIKvVAAViDM8QcKA,223
@@ -230,7 +241,7 @@ edu_rdm_integration/stages/upload_data/dataclasses.py,sha256=zwxlED6yEeMStfc-Y6p
230
241
  edu_rdm_integration/stages/upload_data/enums.py,sha256=uGRqcflf6moPWfFa8KKbeSY2y88-AegAcRJKlQ1PA_Y,453
231
242
  edu_rdm_integration/stages/upload_data/export_managers.py,sha256=DYHX0LwB6RkRqOEaB5EIWqbSJqjaUfR09AjRTjI2CR4,13646
232
243
  edu_rdm_integration/stages/upload_data/helpers.py,sha256=lCvM8QsERXRXP9rcpgaSQOKUkvi3bxkeHEC_zZruQy4,6183
233
- edu_rdm_integration/stages/upload_data/models.py,sha256=fUh_QeAsRajZYweefdDmlAaZh6Q25TSn8e8M3autrKE,5166
244
+ edu_rdm_integration/stages/upload_data/models.py,sha256=4XiodbnbK2MRBi8wTfU0beZQW3dLPT4_5NAh9DM-9gw,5220
234
245
  edu_rdm_integration/stages/upload_data/operations.py,sha256=CVQYaucerJbOAE3JDO05FaX4Bz9hBMKBXYqJohXKZpI,6462
235
246
  edu_rdm_integration/stages/upload_data/queues.py,sha256=MbDnltQLjTCXLnNQVO0KA4SaAzyU8xO51CRid6Cle4g,6130
236
247
  edu_rdm_integration/stages/upload_data/tasks.py,sha256=SYoQjL4sl0W0yOBFczlnQW-N4YcFiFgdMlEQUDO7IsQ,3573
@@ -243,6 +254,8 @@ edu_rdm_integration/stages/upload_data/management/commands/datamart_status.py,sh
243
254
  edu_rdm_integration/stages/upload_data/management/commands/datamart_upload.py,sha256=VF94eqO3nIbvhPFwtUlYLAMwPXEU0uGt9m5xnIjLu68,1525
244
255
  edu_rdm_integration/stages/upload_data/migrations/0001_initial.py,sha256=4dRroCI8ZxBuyRJC81si-LVAfzLMn3mYDccy48gUzCo,8080
245
256
  edu_rdm_integration/stages/upload_data/migrations/0002_auto_20250704_0810.py,sha256=OLXfvs_x_S9W4wS6GKikNaaLUmI4h-LyLBdOs3K_e9I,818
257
+ edu_rdm_integration/stages/upload_data/migrations/0003_auto_20251006_1417.py,sha256=C32q9tAHXWgZjCH05yxON7fXELBR24oor1Xsf7Ms2Wc,1081
258
+ edu_rdm_integration/stages/upload_data/migrations/0004_fix_fk_constraints.py,sha256=OdVgXHEdr-EMdP0N2Qf5Aq4yShNiua90fmPpHjgvfAM,3245
246
259
  edu_rdm_integration/stages/upload_data/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
247
260
  edu_rdm_integration/stages/upload_data/uploader_log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
248
261
  edu_rdm_integration/stages/upload_data/uploader_log/actions.py,sha256=stXHS_CB93AtnuToimsnsbl0gqOn_DbPk6jnCwJs9NQ,8030
@@ -254,8 +267,8 @@ edu_rdm_integration/stages/upload_data/uploader_log/ui.py,sha256=mU3XA9zVKHGqzNk
254
267
  edu_rdm_integration/stages/upload_data/uploader_log/migrations/0001_initial.py,sha256=r5oOB7DBK9-mfuqPAgjXUJY5-hEcmMdILCwDTpaLnBc,753
255
268
  edu_rdm_integration/stages/upload_data/uploader_log/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
256
269
  edu_rdm_integration/stages/upload_data/uploader_log/templates/ui-js/object-grid-buttons.js,sha256=2xyGe0wdVokM0RhpzRzcRvJPBkBmPe3SlZry4oP4Nzs,6201
257
- edu_rdm_integration-3.21.0.dist-info/licenses/LICENSE,sha256=uw43Gjjj-1vXWCItfSrNDpbejnOwZMrNerUh8oWbq8Q,3458
258
- edu_rdm_integration-3.21.0.dist-info/METADATA,sha256=kCdgMvokM8bolCEYgWhQYIDpWFV3hkfWK4MXL65gsHA,42284
259
- edu_rdm_integration-3.21.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
260
- edu_rdm_integration-3.21.0.dist-info/top_level.txt,sha256=nRJV0O14UtNE-jGIYG03sohgFnZClvf57H5m6VBXe9Y,20
261
- edu_rdm_integration-3.21.0.dist-info/RECORD,,
270
+ edu_rdm_integration-3.22.0.dist-info/licenses/LICENSE,sha256=uw43Gjjj-1vXWCItfSrNDpbejnOwZMrNerUh8oWbq8Q,3458
271
+ edu_rdm_integration-3.22.0.dist-info/METADATA,sha256=vp4iuD-eIo0xhQSo9ejHP-7VwjEw3rBrspf49RGl5TE,43463
272
+ edu_rdm_integration-3.22.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
273
+ edu_rdm_integration-3.22.0.dist-info/top_level.txt,sha256=nRJV0O14UtNE-jGIYG03sohgFnZClvf57H5m6VBXe9Y,20
274
+ edu_rdm_integration-3.22.0.dist-info/RECORD,,