edu-rdm-integration 3.22.1__py3-none-any.whl → 3.23.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.
Files changed (33) hide show
  1. edu_rdm_integration/core/consts.py +3 -0
  2. edu_rdm_integration/pipelines/transfer/tasks.py +5 -0
  3. edu_rdm_integration/rdm_entities/models.py +5 -0
  4. edu_rdm_integration/rdm_models/models.py +29 -11
  5. edu_rdm_integration/stages/collect_data/functions/base/runners.py +7 -0
  6. edu_rdm_integration/stages/collect_data/models.py +5 -0
  7. edu_rdm_integration/stages/collect_data/registry/actions.py +3 -3
  8. edu_rdm_integration/stages/collect_data/registry/templates/ui-js/collect-command-window.js +8 -19
  9. edu_rdm_integration/stages/collect_data/registry/templates/ui-js/validators.js +0 -15
  10. edu_rdm_integration/stages/export_data/functions/base/runners.py +11 -0
  11. edu_rdm_integration/stages/export_data/migrations/0003_alter_rdmexportingdatasubstageattachment_exporting_data_sub_stage.py +22 -0
  12. edu_rdm_integration/stages/export_data/migrations/{0003_fix_fk_constraints.py → 0004_fix_fk_constraints.py} +4 -4
  13. edu_rdm_integration/stages/export_data/models.py +12 -1
  14. edu_rdm_integration/stages/export_data/registry/actions.py +5 -4
  15. edu_rdm_integration/stages/export_data/registry/templates/ui-js/create-export-command-win.js +15 -31
  16. edu_rdm_integration/stages/service/service_outdated_data/cleaners/base.py +139 -48
  17. edu_rdm_integration/stages/service/service_outdated_data/cleaners/collect_data.py +1 -8
  18. edu_rdm_integration/stages/service/service_outdated_data/cleaners/consts.py +0 -11
  19. edu_rdm_integration/stages/service/service_outdated_data/cleaners/export_data.py +23 -3
  20. edu_rdm_integration/stages/upload_data/enums.py +2 -0
  21. edu_rdm_integration/stages/upload_data/export_managers.py +3 -1
  22. edu_rdm_integration/stages/upload_data/management/commands/custom_check_upload_status.py +59 -0
  23. edu_rdm_integration/stages/upload_data/management/commands/custom_upload_files.py +45 -0
  24. edu_rdm_integration/stages/upload_data/migrations/0004_fix_fk_constraints.py +1 -1
  25. edu_rdm_integration/stages/upload_data/queues.py +50 -2
  26. edu_rdm_integration/stages/upload_data/tasks.py +2 -2
  27. edu_rdm_integration/stages/utils.py +12 -0
  28. edu_rdm_integration/templates/ui-js/collect-and-export-validators.js +54 -0
  29. {edu_rdm_integration-3.22.1.dist-info → edu_rdm_integration-3.23.0.dist-info}/METADATA +75 -61
  30. {edu_rdm_integration-3.22.1.dist-info → edu_rdm_integration-3.23.0.dist-info}/RECORD +33 -29
  31. {edu_rdm_integration-3.22.1.dist-info → edu_rdm_integration-3.23.0.dist-info}/WHEEL +0 -0
  32. {edu_rdm_integration-3.22.1.dist-info → edu_rdm_integration-3.23.0.dist-info}/licenses/LICENSE +0 -0
  33. {edu_rdm_integration-3.22.1.dist-info → edu_rdm_integration-3.23.0.dist-info}/top_level.txt +0 -0
@@ -21,15 +21,10 @@ from educommon.utils.seqtools import (
21
21
  make_chunks,
22
22
  )
23
23
 
24
- from web_edu.plugins.regional_data_mart_integration.models import (
25
- LessonClass,
26
- )
27
-
28
24
  from .base import (
29
25
  BaseServiceOutdatedDataCleaner,
30
26
  )
31
27
  from .consts import (
32
- OLD_RDM_MODEL,
33
28
  UNION_CHUNK_SIZE,
34
29
  )
35
30
 
@@ -49,14 +44,12 @@ class CollectingDataSubStageCleaner(BaseServiceOutdatedDataCleaner):
49
44
  def _get_valid_substage_ids_subquery(self) -> Optional[Subquery]:
50
45
  """Подзапрос, возвращающий все допустимые collecting_sub_stage_id из моделей, описанных в RDMModelEnum."""
51
46
  model_enum_values = RDMModelEnum.get_model_enum_values()
52
- all_model = [model_enum.model for model_enum in model_enum_values] + OLD_RDM_MODEL
47
+ all_model = [model_enum.model for model_enum in model_enum_values]
53
48
  chunk_queries = []
54
49
 
55
50
  for enum_values_chunk in make_chunks(all_model, UNION_CHUNK_SIZE, is_list=True):
56
51
  qs_list = []
57
52
  for model_cls in enum_values_chunk:
58
- if model_cls in [LessonClass]:
59
- continue
60
53
  try:
61
54
  model_cls._meta.get_field('collecting_sub_stage_id')
62
55
  except FieldDoesNotExist:
@@ -1,12 +1 @@
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
1
  UNION_CHUNK_SIZE = 5
7
-
8
- OLD_RDM_MODEL = [
9
- HomeworkStudent,
10
- HomeworkMaterial,
11
- Homework,
12
- ]
@@ -25,9 +25,9 @@ from educommon.utils.seqtools import (
25
25
 
26
26
  from .base import (
27
27
  BaseServiceOutdatedDataCleaner,
28
+ ServiceFileCleaner,
28
29
  )
29
30
  from .consts import (
30
- OLD_RDM_MODEL,
31
31
  UNION_CHUNK_SIZE,
32
32
  )
33
33
 
@@ -46,7 +46,7 @@ class ExportingDataSubStageCleaner(BaseServiceOutdatedDataCleaner):
46
46
  def _get_valid_substage_ids_subquery(self) -> Optional[Subquery]:
47
47
  """Подзапрос, возвращающий все допустимые exporting_sub_stage_id из моделей, описанных в RDMModelEnum."""
48
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
49
+ all_model = [model_enum.model for model_enum in model_enum_values]
50
50
  chunk_queries = []
51
51
 
52
52
  for enum_values_chunk in make_chunks(all_model, UNION_CHUNK_SIZE, is_list=True):
@@ -102,15 +102,35 @@ class ExportingDataStageCleaner(BaseServiceOutdatedDataCleaner):
102
102
  return self.get_orphan_reference_condition(sub_stage_table, 'stage_id')
103
103
 
104
104
 
105
- class ExportingDataSubStageAttachmentCleaner(BaseServiceOutdatedDataCleaner):
105
+ class ExportingDataSubStageAttachmentCleaner(ServiceFileCleaner, BaseServiceOutdatedDataCleaner):
106
106
  """Очистка вложений подэтапов выгрузки данных."""
107
107
 
108
108
  model = RDMExportingDataSubStageAttachment
109
109
 
110
+ REMOVE_OUTDATED_DATA_SQL = """
111
+ WITH deleted_rows AS (
112
+ DELETE FROM {table_name}
113
+ WHERE id IN (
114
+ WITH tbl AS (
115
+ SELECT *
116
+ FROM {table_name}
117
+ WHERE id >= {first_id}
118
+ AND id <= {last_id}
119
+ )
120
+ SELECT tbl.id
121
+ FROM tbl
122
+ WHERE {conditions}
123
+ )
124
+ RETURNING attachment AS file_path
125
+ )
126
+ SELECT file_path FROM deleted_rows;
127
+ """
128
+
110
129
  def get_merged_conditions(self) -> str:
111
130
  """Формирует условие удаления для устаревших данных."""
112
131
  sub_stage_table = ExportingDataSubStageCleaner.get_table_name()
113
132
  conditions = [
133
+ 'exporting_data_sub_stage_id IS NULL',
114
134
  f'({self.get_status_condition(sub_stage_table, "id", "FINISHED", 7, "exporting_data_sub_stage_id")})',
115
135
  f'({self.get_status_condition(sub_stage_table, "id", "FAILED",30, "exporting_data_sub_stage_id")})',
116
136
  f'({self.get_orphan_reference_condition(sub_stage_table, "id", "exporting_data_sub_stage_id")})',
@@ -9,9 +9,11 @@ class FileUploadStatusEnum(BaseEnumerate):
9
9
  IN_PROGRESS = 1
10
10
  FINISHED = 2
11
11
  ERROR = 3
12
+ IN_CHECK = 4
12
13
 
13
14
  values = {
14
15
  IN_PROGRESS: 'В процессе загрузки в витрину',
15
16
  FINISHED: 'Загрузка в витрину закончена',
16
17
  ERROR: 'Ошибка загрузки в витрину',
18
+ IN_CHECK: 'На проверке'
17
19
  }
@@ -106,11 +106,13 @@ class ExportQueueSender:
106
106
 
107
107
  def get_sub_stages_attachments_to_export(self):
108
108
  """Выборка готовых к экспорту подэтапов."""
109
- sub_stage_ids = (
109
+ sub_stage_ids = set(
110
110
  RDMExportingDataSubStage.objects.filter(self._make_stage_filter())
111
111
  .order_by('started_at')
112
112
  .values_list('id', flat=True)[: self.limit]
113
113
  )
114
+ RDMExportingDataSubStage.objects.filter(id__in=sub_stage_ids).update(
115
+ status=RDMExportingDataSubStageStatus.IN_EXPORT.key)
114
116
 
115
117
  return (
116
118
  RDMExportingDataSubStage.objects.filter(id__in=sub_stage_ids)
@@ -0,0 +1,59 @@
1
+ from time import (
2
+ sleep,
3
+ )
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ )
8
+
9
+ from django.core.cache import (
10
+ cache,
11
+ )
12
+ from django.core.management.base import (
13
+ BaseCommand,
14
+ )
15
+
16
+ from edu_rdm_integration.core.consts import (
17
+ BATCH_SIZE,
18
+ )
19
+ from edu_rdm_integration.stages.upload_data.enums import (
20
+ FileUploadStatusEnum,
21
+ )
22
+ from edu_rdm_integration.stages.upload_data.helpers import (
23
+ UploadStatusHelper,
24
+ )
25
+ from edu_rdm_integration.stages.upload_data.models import (
26
+ RDMExportingDataSubStageUploaderClientLog,
27
+ )
28
+
29
+
30
+ class Command(BaseCommand):
31
+ """Команда для отправки данных в витрину параллельно-последовательно. В рамках скрипта последовательно,
32
+ параллельно количетвом запусков команды."""
33
+
34
+ help = 'Команда для отправки данных в витрину параллельно-последовательно' # noqa: A003
35
+
36
+
37
+ def handle(self, *args: tuple[Any], **kwargs: dict[str, Any]) -> None:
38
+ """Обработчик команды."""
39
+ while True:
40
+ self.stdout.write(f'Начало проверки статуса загрузки данных в витрину..')
41
+
42
+ # Получаем незавершенные загрузки данных в витрину
43
+ in_progress_uploads = RDMExportingDataSubStageUploaderClientLog.objects.filter(
44
+ file_upload_status=FileUploadStatusEnum.IN_PROGRESS,
45
+ is_emulation=False,
46
+ ).select_related('attachment')[:BATCH_SIZE]
47
+
48
+ for upload in in_progress_uploads:
49
+ upload.file_upload_status = FileUploadStatusEnum.IN_CHECK
50
+
51
+ RDMExportingDataSubStageUploaderClientLog.objects.bulk_update(in_progress_uploads, fields=['file_upload_status'])
52
+
53
+ self.stdout.write(f'Обновление статуса загрузки данных в витрину на {FileUploadStatusEnum.IN_CHECK}..')
54
+
55
+ UploadStatusHelper(in_progress_uploads, cache).run()
56
+
57
+ sleep(10)
58
+
59
+ self.stdout.write(f'Окончание проверки статуса загрузки данных в витрину.\n\n')
@@ -0,0 +1,45 @@
1
+ from time import sleep
2
+ from typing import (
3
+ Any,
4
+ )
5
+
6
+ from django.core.management.base import (
7
+ BaseCommand,
8
+ )
9
+
10
+ from django.core.cache import (
11
+ cache,
12
+ )
13
+
14
+ from edu_rdm_integration.stages.upload_data.operations import (
15
+ UploadData,
16
+ )
17
+ from edu_rdm_integration.stages.upload_data.queues import (
18
+ RdmDictBasedSubStageAttachmentQueue
19
+ )
20
+
21
+
22
+ class Command(BaseCommand):
23
+ """Команда для отправки данных в витрину параллельно-последовательно. В рамках скрипта последовательно,
24
+ параллельно количетвом запусков команды."""
25
+
26
+ help = 'Команда для отправки данных в витрину параллельно-последовательно' # noqa: A003
27
+
28
+
29
+ def handle(self, *args: tuple[Any], **kwargs: dict[str, Any]) -> None:
30
+ """Обработчик команды."""
31
+ while True:
32
+ self.stdout.write(f'Начало отправки данных в витрину')
33
+
34
+ queue = RdmDictBasedSubStageAttachmentQueue()
35
+ upload_data = UploadData(
36
+ data_cache=cache,
37
+ queue=queue,
38
+ )
39
+
40
+ upload_result = upload_data.upload_data()
41
+
42
+ sleep(40)
43
+
44
+ self.stdout.write(f'Общий объем отправленных файлов {upload_result["total_file_size"]}')
45
+ self.stdout.write(f'Сущности, отправленные в витрину {upload_result["uploaded_entities"]}')
@@ -16,7 +16,7 @@ def apply_fk_updates(apps, schema_editor):
16
16
  Здесь задаются таблицы и поведение при удалении записей,
17
17
  чтобы синхронизировать фактическое состояние БД с логикой моделей.
18
18
  """
19
- Entry = apps.get_model('smev_agent_client', 'Entry')
19
+ Entry = apps.get_model('uploader_client', 'Entry')
20
20
  RDMExportingDataSubStage = apps.get_model('edu_rdm_integration_export_data_stage', 'RDMExportingDataSubStage')
21
21
  RDMExportingDataSubStageAttachment = apps.get_model(
22
22
  'edu_rdm_integration_export_data_stage', 'RDMExportingDataSubStageAttachment'
@@ -1,8 +1,12 @@
1
1
  import json
2
+ import uuid
2
3
  from abc import (
3
4
  ABC,
4
5
  abstractmethod,
5
6
  )
7
+ from collections import (
8
+ defaultdict,
9
+ )
6
10
  from typing import (
7
11
  Any,
8
12
  Union,
@@ -66,8 +70,6 @@ class RdmRedisSubStageAttachmentQueue(Queue):
66
70
  (Sorted Set in Redis)
67
71
  - Информация по файлам стандартно по ключу - ключом выступает sub_stage_id
68
72
  """
69
-
70
- queue_key = 'rdm:export_sub_stage_ids_queue'
71
73
  prefix = 'rdm:'
72
74
 
73
75
  def __init__(self, *args, **kwargs):
@@ -81,6 +83,9 @@ class RdmRedisSubStageAttachmentQueue(Queue):
81
83
  password=settings.RDM_REDIS_PASSWORD,
82
84
  )
83
85
 
86
+ self.queue_key = f'rdm:export_sub_stage_ids_queue:{str(uuid.uuid4())[:4]}'
87
+
88
+
84
89
  def _make_key(self, key: Union[int, str]) -> str:
85
90
  """Формирование ключа."""
86
91
  return f'{self.prefix}{key}'
@@ -169,3 +174,46 @@ class RdmRedisSubStageAttachmentQueue(Queue):
169
174
  db = kwargs['db']
170
175
 
171
176
  return f'Redis {version} on {host}:{port}/{db}'
177
+
178
+
179
+ class RdmDictBasedSubStageAttachmentQueue(Queue):
180
+ """Очередь файлов и подэтапов на основе словаря.
181
+
182
+ Данные хранятся следующим образом:
183
+ - Словарь вида (id подэтапа, сущность): список с данными по файлам.
184
+ Данные по файлу в именнованном кортеже UpladFile
185
+ {
186
+ (sub_stage_id,entity): [UploadFile1, UploadFile2],
187
+ }
188
+ """
189
+
190
+ def __init__(self, *args, **kwargs):
191
+ """Инициализация объекта очереди Queue."""
192
+ super().__init__(*args, **kwargs)
193
+
194
+ self.data = defaultdict(list)
195
+
196
+ @property
197
+ def count(self) -> int:
198
+ """Возвращает количество подэтапов в очереди."""
199
+ return len(self.data)
200
+
201
+ def enqueue(self, stage_id, entity_name: str, attachmets: list[UploadFile]) -> None:
202
+ """Помещение в очередь.
203
+
204
+ Подэтап попадает в упорядоченную очередь."""
205
+
206
+ self.data[(stage_id, entity_name)].extend(attachmets)
207
+
208
+ def dequeue(self) -> dict[tuple[Any, Any], list[UploadFile]]:
209
+ """Возвращает все данные из очереди."""
210
+ return self.data
211
+
212
+ def delete_from_queue(self, sub_stage_id: int, entity_name: str) -> None:
213
+ """Удаление элемента из очереди."""
214
+ self.data.get((sub_stage_id, entity_name))
215
+
216
+ def clear(self) -> None:
217
+ """Очистить очередь."""
218
+ self.data.clear()
219
+
@@ -32,7 +32,7 @@ from edu_rdm_integration.stages.upload_data.operations import (
32
32
  UploadData,
33
33
  )
34
34
  from edu_rdm_integration.stages.upload_data.queues import (
35
- RdmRedisSubStageAttachmentQueue,
35
+ RdmDictBasedSubStageAttachmentQueue
36
36
  )
37
37
 
38
38
 
@@ -81,7 +81,7 @@ class UploadDataAsyncTask(UniquePeriodicAsyncTask):
81
81
  """Выполнение."""
82
82
  super().process(*args, **kwargs)
83
83
 
84
- queue = RdmRedisSubStageAttachmentQueue()
84
+ queue = RdmDictBasedSubStageAttachmentQueue()
85
85
  upload_data = UploadData(
86
86
  data_cache=cache,
87
87
  queue=queue,
@@ -24,6 +24,18 @@ def update_foreign_key_constraint(
24
24
  on_delete (str): Поведение при удалении связанной записи.
25
25
  """
26
26
  with connection.cursor() as cursor:
27
+ # Проверяем существование таблиц
28
+ cursor.execute("""
29
+ SELECT EXISTS (
30
+ SELECT FROM information_schema.tables WHERE table_name = %s AND table_schema = 'public'
31
+ ) AND EXISTS (
32
+ SELECT FROM information_schema.tables WHERE table_name = %s AND table_schema = 'public'
33
+ );
34
+ """, [table_name, target_table])
35
+
36
+ if not cursor.fetchone()[0]:
37
+ return
38
+
27
39
  # Найти имя constraint'а
28
40
  cursor.execute(f"""
29
41
  SELECT conname
@@ -0,0 +1,54 @@
1
+ function logsPeriodsValidator(startField, endField) {
2
+ if (
3
+ startField.getValue() &&
4
+ endField.getValue() &&
5
+ startField.getValue() > endField.getValue()
6
+ ) {
7
+ return 'Дата конца периода не может быть меньше даты начала периода';
8
+ }
9
+ return true;
10
+ }
11
+
12
+ function setupPeriodFields(startField, endField) {
13
+ // Функция валидации обоих полей
14
+ function validatePeriodFields() {
15
+ startField.validate();
16
+ endField.validate();
17
+ }
18
+
19
+ // Установка времени начала по умолчанию 00:00:00
20
+ function setDefaultStartTime() {
21
+ if (!startField.getValue()) {
22
+ const defaultDateTime = new Date();
23
+ defaultDateTime.setHours(0, 0, 0);
24
+ startField.setValue(defaultDateTime);
25
+ }
26
+ }
27
+
28
+ // Установка времени конца по умолчанию 23:59:59
29
+ function setDefaultEndTime() {
30
+ if (!endField.getValue()) {
31
+ const defaultDateTime = new Date();
32
+ defaultDateTime.setHours(23, 59, 59);
33
+ endField.setValue(defaultDateTime);
34
+ }
35
+ }
36
+
37
+ // Настройка обработчиков для поля начала периода
38
+ startField.menu.on('beforeshow', setDefaultStartTime);
39
+ startField.on('change', validatePeriodFields);
40
+ startField.on('select', validatePeriodFields);
41
+
42
+ // Настройка обработчиков для поля конца периода
43
+ endField.menu.on('beforeshow', setDefaultEndTime);
44
+ endField.on('change', validatePeriodFields);
45
+ endField.on('select', validatePeriodFields);
46
+
47
+ // Установка валидаторов
48
+ startField.validator = function() {
49
+ return logsPeriodsValidator(startField, endField);
50
+ };
51
+ endField.validator = function() {
52
+ return logsPeriodsValidator(startField, endField);
53
+ };
54
+ }