edu-rdm-integration 3.21.0__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.
- edu_rdm_integration/core/consts.py +3 -0
- edu_rdm_integration/pipelines/cleanup_outdated_data/management/commands/rdm_cleanup_outdated_data.py +32 -1
- edu_rdm_integration/pipelines/transfer/tasks.py +5 -0
- edu_rdm_integration/rdm_entities/models.py +5 -0
- edu_rdm_integration/rdm_models/models.py +29 -11
- edu_rdm_integration/stages/collect_data/functions/base/runners.py +7 -0
- edu_rdm_integration/stages/collect_data/migrations/0005_alter_rdmcollectingdatasubstage_previous.py +22 -0
- edu_rdm_integration/stages/collect_data/migrations/0006_fix_fk_constraints.py +59 -0
- edu_rdm_integration/stages/collect_data/models.py +5 -0
- edu_rdm_integration/stages/collect_data/registry/actions.py +4 -4
- edu_rdm_integration/stages/collect_data/registry/templates/ui-js/collect-command-window.js +8 -19
- edu_rdm_integration/stages/collect_data/registry/templates/ui-js/validators.js +0 -15
- edu_rdm_integration/stages/export_data/functions/base/runners.py +11 -0
- edu_rdm_integration/stages/export_data/migrations/0003_alter_rdmexportingdatasubstageattachment_exporting_data_sub_stage.py +22 -0
- edu_rdm_integration/stages/export_data/migrations/0004_fix_fk_constraints.py +76 -0
- edu_rdm_integration/stages/export_data/models.py +12 -1
- edu_rdm_integration/stages/export_data/registry/actions.py +5 -4
- edu_rdm_integration/stages/export_data/registry/templates/ui-js/create-export-command-win.js +15 -31
- edu_rdm_integration/stages/service/service_outdated_data/cleaners/__init__.py +0 -0
- edu_rdm_integration/stages/service/service_outdated_data/cleaners/base.py +305 -0
- edu_rdm_integration/stages/service/service_outdated_data/cleaners/collect_data.py +119 -0
- edu_rdm_integration/stages/service/service_outdated_data/cleaners/consts.py +1 -0
- edu_rdm_integration/stages/service/service_outdated_data/cleaners/export_data.py +174 -0
- edu_rdm_integration/stages/service/service_outdated_data/cleaners/upload_data.py +64 -0
- edu_rdm_integration/stages/service/service_outdated_data/managers.py +80 -0
- edu_rdm_integration/stages/upload_data/enums.py +2 -0
- edu_rdm_integration/stages/upload_data/export_managers.py +3 -1
- edu_rdm_integration/stages/upload_data/management/commands/custom_check_upload_status.py +59 -0
- edu_rdm_integration/stages/upload_data/management/commands/custom_upload_files.py +45 -0
- edu_rdm_integration/stages/upload_data/migrations/0003_auto_20251006_1417.py +28 -0
- edu_rdm_integration/stages/upload_data/migrations/0004_fix_fk_constraints.py +79 -0
- edu_rdm_integration/stages/upload_data/models.py +5 -2
- edu_rdm_integration/stages/upload_data/queues.py +50 -2
- edu_rdm_integration/stages/upload_data/tasks.py +2 -2
- edu_rdm_integration/stages/utils.py +61 -0
- edu_rdm_integration/templates/ui-js/collect-and-export-validators.js +54 -0
- {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.23.0.dist-info}/METADATA +86 -59
- {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.23.0.dist-info}/RECORD +41 -24
- {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.23.0.dist-info}/WHEEL +0 -0
- {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.23.0.dist-info}/licenses/LICENSE +0 -0
- {edu_rdm_integration-3.21.0.dist-info → edu_rdm_integration-3.23.0.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
|
@@ -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"]}')
|
|
@@ -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('uploader_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=
|
|
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
|
-
|
|
163
|
+
null=True,
|
|
164
|
+
on_delete=SET_NULL,
|
|
162
165
|
related_name='upload_status_request_log',
|
|
163
166
|
)
|
|
164
167
|
request_status = ForeignKey(
|
|
@@ -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
|
-
|
|
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 =
|
|
84
|
+
queue = RdmDictBasedSubStageAttachmentQueue()
|
|
85
85
|
upload_data = UploadData(
|
|
86
86
|
data_cache=cache,
|
|
87
87
|
queue=queue,
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
# Проверяем существование таблиц
|
|
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
|
+
|
|
39
|
+
# Найти имя constraint'а
|
|
40
|
+
cursor.execute(f"""
|
|
41
|
+
SELECT conname
|
|
42
|
+
FROM pg_constraint
|
|
43
|
+
WHERE conrelid = '{table_name}'::regclass
|
|
44
|
+
AND confrelid = '{target_table}'::regclass
|
|
45
|
+
AND contype = 'f'
|
|
46
|
+
""")
|
|
47
|
+
row = cursor.fetchone()
|
|
48
|
+
if not row:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
constraint_name = row[0]
|
|
52
|
+
|
|
53
|
+
# Удалить старый constraint, Добавить новый constraint с нужным ON DELETE
|
|
54
|
+
cursor.execute(f'''
|
|
55
|
+
ALTER TABLE "{table_name}"
|
|
56
|
+
DROP CONSTRAINT IF EXISTS "{constraint_name}",
|
|
57
|
+
ADD CONSTRAINT "{constraint_name}"
|
|
58
|
+
FOREIGN KEY ("{field_name}")
|
|
59
|
+
REFERENCES "{target_table}"(id)
|
|
60
|
+
ON DELETE {on_delete};
|
|
61
|
+
''')
|
|
@@ -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
|
+
}
|