edu-rdm-integration 3.8.1__py3-none-any.whl → 3.9.1__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/collect_and_export_data/actions.py +121 -0
- edu_rdm_integration/collect_and_export_data/ui.py +137 -0
- edu_rdm_integration/collect_and_export_data/utils.py +130 -0
- edu_rdm_integration/collect_data/actions.py +276 -0
- edu_rdm_integration/collect_data/const.py +2 -0
- edu_rdm_integration/collect_data/generators.py +72 -8
- edu_rdm_integration/collect_data/ui.py +246 -0
- edu_rdm_integration/enum_register/mixins.py +2 -1
- edu_rdm_integration/export_data/actions.py +291 -0
- edu_rdm_integration/export_data/ui.py +199 -0
- edu_rdm_integration/helpers.py +68 -36
- edu_rdm_integration/templates/ui-js/stage_for_export.js +25 -0
- edu_rdm_integration/templates/ui-js/start-task.js +42 -0
- {edu_rdm_integration-3.8.1.dist-info → edu_rdm_integration-3.9.1.dist-info}/METADATA +1 -1
- {edu_rdm_integration-3.8.1.dist-info → edu_rdm_integration-3.9.1.dist-info}/RECORD +18 -9
- {edu_rdm_integration-3.8.1.dist-info → edu_rdm_integration-3.9.1.dist-info}/WHEEL +0 -0
- {edu_rdm_integration-3.8.1.dist-info → edu_rdm_integration-3.9.1.dist-info}/licenses/LICENSE +0 -0
- {edu_rdm_integration-3.8.1.dist-info → edu_rdm_integration-3.9.1.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,9 @@ from datetime import (
|
|
7
7
|
from typing import (
|
8
8
|
TYPE_CHECKING,
|
9
9
|
Iterable,
|
10
|
+
List,
|
10
11
|
Optional,
|
12
|
+
Tuple,
|
11
13
|
)
|
12
14
|
|
13
15
|
from django.apps import (
|
@@ -20,6 +22,9 @@ from django.db import (
|
|
20
22
|
from educommon.utils.date import (
|
21
23
|
DatesSplitter,
|
22
24
|
)
|
25
|
+
from educommon.utils.seqtools import (
|
26
|
+
make_chunks,
|
27
|
+
)
|
23
28
|
from m3_db_utils.models import (
|
24
29
|
ModelEnumValue,
|
25
30
|
)
|
@@ -27,6 +32,9 @@ from m3_db_utils.models import (
|
|
27
32
|
from edu_rdm_integration import (
|
28
33
|
consts,
|
29
34
|
)
|
35
|
+
from edu_rdm_integration.collect_data.const import (
|
36
|
+
ALL_UNITS_IN_COMMAND,
|
37
|
+
)
|
30
38
|
from edu_rdm_integration.consts import (
|
31
39
|
DATE_FORMAT,
|
32
40
|
)
|
@@ -108,7 +116,8 @@ class BaseFirstCollectModelsDataCommandsGenerator:
|
|
108
116
|
|
109
117
|
self.splitter = (
|
110
118
|
DatesSplitter(split_by=split_by, split_mode=split_mode, split_by_quantity=split_by_quantity)
|
111
|
-
if split_by
|
119
|
+
if split_by
|
120
|
+
else None
|
112
121
|
)
|
113
122
|
|
114
123
|
self.batch_size = batch_size
|
@@ -116,8 +125,7 @@ class BaseFirstCollectModelsDataCommandsGenerator:
|
|
116
125
|
self.generation_id = uuid.uuid4()
|
117
126
|
|
118
127
|
# Правую дату нужно увеличивать на одну секунду, т.к. подрезались миллисекунды
|
119
|
-
self.get_logs_periods_sql =
|
120
|
-
"""
|
128
|
+
self.get_logs_periods_sql = """
|
121
129
|
select min(created),
|
122
130
|
max(created) + interval '1 second',
|
123
131
|
row_batched
|
@@ -140,22 +148,18 @@ class BaseFirstCollectModelsDataCommandsGenerator:
|
|
140
148
|
group by row_batched
|
141
149
|
order by row_batched;
|
142
150
|
"""
|
143
|
-
)
|
144
151
|
|
145
|
-
self.ordered_rows_query =
|
146
|
-
"""
|
152
|
+
self.ordered_rows_query = """
|
147
153
|
select distinct date_trunc('second', created) as created
|
148
154
|
from "{table_name}"
|
149
155
|
where created between '{period_started_at}' and '{period_ended_at}'
|
150
156
|
"""
|
151
|
-
)
|
152
157
|
|
153
158
|
def generate(self) -> list:
|
154
159
|
"""Генерирует список данных для формирования команд для сбора данных РВД."""
|
155
160
|
params_for_commands = []
|
156
161
|
|
157
162
|
for rdm_model in self.regional_data_mart_models:
|
158
|
-
|
159
163
|
# Если не заполнен creating_trigger_models и plugins_info, то список не формируется
|
160
164
|
if not rdm_model.creating_trigger_models and not getattr(rdm_model, 'plugins_info', None):
|
161
165
|
continue
|
@@ -224,3 +228,63 @@ class BaseFirstCollectModelsDataCommandsGenerator:
|
|
224
228
|
)
|
225
229
|
|
226
230
|
return params_for_commands
|
231
|
+
|
232
|
+
|
233
|
+
class FirstCollectModelsDataCommandsGenerator(BaseFirstCollectModelsDataCommandsGenerator):
|
234
|
+
"""Генерирует команды collect_models_by_generating_logs."""
|
235
|
+
|
236
|
+
def generate_with_split(
|
237
|
+
self,
|
238
|
+
by_institutes: bool,
|
239
|
+
institute_ids: Optional[Iterable[int]],
|
240
|
+
institute_count: Optional[int],
|
241
|
+
actual_institute_ids: Iterable[int],
|
242
|
+
):
|
243
|
+
"""Генерирует команды с разделением по организациям.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
by_institutes: Разделение по организациям.
|
247
|
+
institute_ids: ID организаций по которым генерируются команды.
|
248
|
+
institute_count: Разделение по кол-ву организаций.
|
249
|
+
actual_institute_ids: Текущие организации в системе.
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
Cписок данных для формирования команд.
|
253
|
+
"""
|
254
|
+
|
255
|
+
result_commands: List[dict] = []
|
256
|
+
institute_ids_chunks: Tuple[Optional[Iterable]] = (None,)
|
257
|
+
|
258
|
+
raw_commands = self.generate()
|
259
|
+
|
260
|
+
if by_institutes and not institute_ids:
|
261
|
+
# Если указано разбиение по организациям, без перечисления организаций:
|
262
|
+
if institute_count == ALL_UNITS_IN_COMMAND:
|
263
|
+
# Если указано -1, то в команде будут указаны все
|
264
|
+
# существующие организации в параметре institute_ids.
|
265
|
+
institute_ids_chunks = ((),)
|
266
|
+
else:
|
267
|
+
# Если значение, отличное от -1, то в каждой команде будет
|
268
|
+
# указано institute_count кол-во организаций в параметре institute_ids.
|
269
|
+
institute_ids_chunks = make_chunks(
|
270
|
+
iterable=actual_institute_ids,
|
271
|
+
size=institute_count,
|
272
|
+
is_list=True,
|
273
|
+
)
|
274
|
+
elif institute_ids:
|
275
|
+
# Если указано разбиение по организациям и/или перечислены организации:
|
276
|
+
if institute_count == ALL_UNITS_IN_COMMAND:
|
277
|
+
institute_ids_chunks = (institute_ids,)
|
278
|
+
else:
|
279
|
+
institute_ids_chunks = make_chunks(
|
280
|
+
iterable=institute_ids,
|
281
|
+
size=institute_count,
|
282
|
+
is_list=True,
|
283
|
+
)
|
284
|
+
|
285
|
+
# Переформируются команды с учетом организаций и их кол-ва для каждой команды:
|
286
|
+
for institute_ids_chunk in institute_ids_chunks:
|
287
|
+
for command in raw_commands:
|
288
|
+
result_commands.append({**command, 'institute_ids': institute_ids_chunk})
|
289
|
+
|
290
|
+
return result_commands
|
@@ -0,0 +1,246 @@
|
|
1
|
+
from typing import (
|
2
|
+
Any,
|
3
|
+
Dict,
|
4
|
+
)
|
5
|
+
|
6
|
+
from m3_ext.ui.fields.simple import (
|
7
|
+
ExtCheckBox,
|
8
|
+
ExtComboBox,
|
9
|
+
ExtDateTimeField,
|
10
|
+
ExtDisplayField,
|
11
|
+
ExtNumberField,
|
12
|
+
ExtStringField,
|
13
|
+
)
|
14
|
+
from m3_ext.ui.misc import (
|
15
|
+
ExtDataStore,
|
16
|
+
)
|
17
|
+
from objectpack.ui import (
|
18
|
+
allow_blank,
|
19
|
+
)
|
20
|
+
|
21
|
+
from educommon.objectpack.ui import (
|
22
|
+
BaseEditWindow,
|
23
|
+
)
|
24
|
+
from educommon.utils.date import (
|
25
|
+
DatesSplitter,
|
26
|
+
)
|
27
|
+
|
28
|
+
from edu_rdm_integration.collect_and_export_data.ui import (
|
29
|
+
BaseCreateCommandWindow,
|
30
|
+
)
|
31
|
+
from edu_rdm_integration.collect_data.const import (
|
32
|
+
ALL_UNITS_IN_COMMAND,
|
33
|
+
)
|
34
|
+
from edu_rdm_integration.consts import (
|
35
|
+
BATCH_SIZE,
|
36
|
+
)
|
37
|
+
from edu_rdm_integration.models import (
|
38
|
+
RegionalDataMartModelEnum,
|
39
|
+
)
|
40
|
+
|
41
|
+
|
42
|
+
class CreateCollectCommandWindow(BaseCreateCommandWindow):
|
43
|
+
"""Окно создания команды сбора данных модели РВД."""
|
44
|
+
|
45
|
+
def _init_components(self):
|
46
|
+
"""Инициализация компонентов."""
|
47
|
+
|
48
|
+
super()._init_components()
|
49
|
+
|
50
|
+
model = ExtComboBox(
|
51
|
+
name='model_id',
|
52
|
+
label='Модель',
|
53
|
+
display_field='model',
|
54
|
+
anchor='100%',
|
55
|
+
editable=False,
|
56
|
+
trigger_action_all=True,
|
57
|
+
allow_blank=False,
|
58
|
+
)
|
59
|
+
model.set_store(
|
60
|
+
ExtDataStore((idx, key) for idx, key in enumerate(RegionalDataMartModelEnum.get_model_enum_keys()))
|
61
|
+
)
|
62
|
+
logs_period_started_at = ExtDateTimeField(
|
63
|
+
name='logs_period_started_at',
|
64
|
+
label='Начало периода',
|
65
|
+
anchor='100%',
|
66
|
+
allow_blank=False,
|
67
|
+
)
|
68
|
+
logs_period_ended_at = ExtDateTimeField(
|
69
|
+
name='logs_period_ended_at',
|
70
|
+
label='Конец периода',
|
71
|
+
anchor='100%',
|
72
|
+
allow_blank=False,
|
73
|
+
)
|
74
|
+
split_by = ExtComboBox(
|
75
|
+
name='split_by',
|
76
|
+
display_field='split_by',
|
77
|
+
label='Единица подпериода',
|
78
|
+
anchor='100%',
|
79
|
+
editable=True,
|
80
|
+
trigger_action_all=True,
|
81
|
+
)
|
82
|
+
split_by.set_store(ExtDataStore(enumerate(DatesSplitter.get_split_by_modes())))
|
83
|
+
split_by_quantity = ExtNumberField(
|
84
|
+
name='split_by_quantity',
|
85
|
+
label='Размер подпериода',
|
86
|
+
allow_blank=False,
|
87
|
+
allow_decimals=False,
|
88
|
+
allow_negative=False,
|
89
|
+
anchor='100%',
|
90
|
+
value=1,
|
91
|
+
)
|
92
|
+
split_mode = ExtComboBox(
|
93
|
+
name='split_mode',
|
94
|
+
display_field='split_mode',
|
95
|
+
label='Режим разбиения на подпериоды',
|
96
|
+
anchor='100%',
|
97
|
+
editable=False,
|
98
|
+
allow_blank=True,
|
99
|
+
trigger_action_all=True,
|
100
|
+
value=DatesSplitter.WW_MODE,
|
101
|
+
)
|
102
|
+
split_mode.set_store(
|
103
|
+
ExtDataStore(enumerate(DatesSplitter.get_modes())),
|
104
|
+
)
|
105
|
+
batch_size = ExtNumberField(
|
106
|
+
name='batch_size',
|
107
|
+
label='Размер чанка',
|
108
|
+
allow_blank=False,
|
109
|
+
allow_decimals=False,
|
110
|
+
allow_negative=False,
|
111
|
+
anchor='100%',
|
112
|
+
value=BATCH_SIZE,
|
113
|
+
)
|
114
|
+
by_institutes = ExtCheckBox(
|
115
|
+
anchor='100%',
|
116
|
+
label='Разбить по организациям',
|
117
|
+
name='by_institutes',
|
118
|
+
)
|
119
|
+
institute_ids = ExtStringField(
|
120
|
+
label='id организаций',
|
121
|
+
name='institute_ids',
|
122
|
+
allow_blank=True,
|
123
|
+
anchor='100%',
|
124
|
+
)
|
125
|
+
institute_count = ExtNumberField(
|
126
|
+
name='institute_count',
|
127
|
+
label='Кол-во организаций в одной команде',
|
128
|
+
allow_decimals=False,
|
129
|
+
anchor='100%',
|
130
|
+
min_value=ALL_UNITS_IN_COMMAND,
|
131
|
+
value=ALL_UNITS_IN_COMMAND,
|
132
|
+
)
|
133
|
+
hint_text = ExtDisplayField(
|
134
|
+
value=(
|
135
|
+
f'Данные можно разбить или по "{batch_size.label}" или по "{split_by.label}"! '
|
136
|
+
f'Если выбрать оба варианта, то будет выбрано разбиение по "{split_by.label}".'
|
137
|
+
),
|
138
|
+
read_only=True,
|
139
|
+
label_style='width: 0px',
|
140
|
+
style={'text-align': 'center'},
|
141
|
+
)
|
142
|
+
just_or_hint_text = ExtDisplayField(
|
143
|
+
label='или',
|
144
|
+
)
|
145
|
+
self.items_ = (
|
146
|
+
model,
|
147
|
+
logs_period_started_at,
|
148
|
+
logs_period_ended_at,
|
149
|
+
by_institutes,
|
150
|
+
institute_ids,
|
151
|
+
institute_count,
|
152
|
+
hint_text,
|
153
|
+
batch_size,
|
154
|
+
just_or_hint_text,
|
155
|
+
split_by,
|
156
|
+
split_by_quantity,
|
157
|
+
split_mode,
|
158
|
+
)
|
159
|
+
|
160
|
+
|
161
|
+
class DetailCollectCommandWindow(BaseEditWindow):
|
162
|
+
"""Окно просмотра команды сбора данных модели РВД."""
|
163
|
+
|
164
|
+
def set_params(self, params: Dict[str, Any]) -> None:
|
165
|
+
"""Устанавливает параметры окна."""
|
166
|
+
|
167
|
+
super().set_params(params)
|
168
|
+
|
169
|
+
self.height = 'auto'
|
170
|
+
self.logs_link_field.value = params.get('log_url', '')
|
171
|
+
|
172
|
+
def _init_components(self) -> None:
|
173
|
+
"""Инициализирует компоненты окна."""
|
174
|
+
|
175
|
+
super()._init_components()
|
176
|
+
|
177
|
+
self.model_field = ExtStringField(
|
178
|
+
name='model_id',
|
179
|
+
label='Модель',
|
180
|
+
anchor='100%',
|
181
|
+
)
|
182
|
+
self.created_field = ExtDateTimeField(
|
183
|
+
name='created',
|
184
|
+
label='Дата создания',
|
185
|
+
anchor='100%',
|
186
|
+
)
|
187
|
+
self.generation_id_field = ExtStringField(
|
188
|
+
name='generation_id',
|
189
|
+
label='Идентификатор генерации',
|
190
|
+
anchor='100%',
|
191
|
+
)
|
192
|
+
self.task_id_field = ExtStringField(
|
193
|
+
name='task_id',
|
194
|
+
label='Идентификатор задачи',
|
195
|
+
anchor='100%',
|
196
|
+
)
|
197
|
+
self.institute_ids_field = ExtStringField(
|
198
|
+
name='institute_ids',
|
199
|
+
label='Идентификаторы организаций',
|
200
|
+
anchor='100%',
|
201
|
+
)
|
202
|
+
self.status_field = ExtStringField(
|
203
|
+
name='stage.status.key',
|
204
|
+
label='Статус сбора',
|
205
|
+
anchor='100%',
|
206
|
+
)
|
207
|
+
self.started_at_field = ExtDateTimeField(
|
208
|
+
name='stage.started_at',
|
209
|
+
label='Время начала сбора',
|
210
|
+
anchor='100%',
|
211
|
+
)
|
212
|
+
self.logs_link_field = ExtDisplayField(
|
213
|
+
name='log_url',
|
214
|
+
label='Ссылка на логи',
|
215
|
+
anchor='100%',
|
216
|
+
)
|
217
|
+
self.logs_period_started_at_field = ExtDateTimeField(
|
218
|
+
name='logs_period_started_at',
|
219
|
+
label='Начало периода',
|
220
|
+
anchor='100%',
|
221
|
+
)
|
222
|
+
self.logs_period_ended_at_field = ExtDateTimeField(
|
223
|
+
name='logs_period_ended_at',
|
224
|
+
label='Конец периода',
|
225
|
+
anchor='100%',
|
226
|
+
)
|
227
|
+
|
228
|
+
def _do_layout(self) -> None:
|
229
|
+
"""Располагает компоненты окна."""
|
230
|
+
|
231
|
+
super()._do_layout()
|
232
|
+
|
233
|
+
self.form.items.extend(
|
234
|
+
(
|
235
|
+
self.model_field,
|
236
|
+
self.created_field,
|
237
|
+
self.generation_id_field,
|
238
|
+
self.task_id_field,
|
239
|
+
self.institute_ids_field,
|
240
|
+
self.status_field,
|
241
|
+
self.started_at_field,
|
242
|
+
self.logs_link_field,
|
243
|
+
self.logs_period_started_at_field,
|
244
|
+
self.logs_period_ended_at_field,
|
245
|
+
)
|
246
|
+
)
|
@@ -73,13 +73,14 @@ class EntityEnumRegisterMixin(BaseEnumRegisterMixin):
|
|
73
73
|
|
74
74
|
# TODO EDUSCHL-20938 Удалить в рамках задачи
|
75
75
|
@classmethod
|
76
|
-
def get_additional_model_enums(cls) ->
|
76
|
+
def get_additional_model_enums(cls) -> tuple[ModelEnumValue, ...]:
|
77
77
|
"""Возвращает кортеж значений модели-перечисления основной модели сущности.
|
78
78
|
|
79
79
|
В классе определяется поле additional_model_enums или данный метод.
|
80
80
|
|
81
81
|
!!! Временное решение. Будет удалено в рамках EDUSCHL-20938.
|
82
82
|
"""
|
83
|
+
return ()
|
83
84
|
|
84
85
|
|
85
86
|
class ModelEnumRegisterMixin(BaseEnumRegisterMixin):
|
@@ -0,0 +1,291 @@
|
|
1
|
+
from functools import (
|
2
|
+
partial,
|
3
|
+
)
|
4
|
+
|
5
|
+
from django.db.models import (
|
6
|
+
F,
|
7
|
+
Func,
|
8
|
+
IntegerField,
|
9
|
+
OuterRef,
|
10
|
+
Q,
|
11
|
+
Subquery,
|
12
|
+
)
|
13
|
+
from django.db.transaction import (
|
14
|
+
atomic,
|
15
|
+
)
|
16
|
+
from m3.actions.results import (
|
17
|
+
OperationResult,
|
18
|
+
)
|
19
|
+
from objectpack.actions import (
|
20
|
+
BaseAction,
|
21
|
+
)
|
22
|
+
|
23
|
+
from educommon.async_task.actions import (
|
24
|
+
RevokeAsyncTaskAction,
|
25
|
+
)
|
26
|
+
from educommon.async_task.models import (
|
27
|
+
AsyncTaskStatus,
|
28
|
+
)
|
29
|
+
from educommon.utils.ui import (
|
30
|
+
ChoicesFilter,
|
31
|
+
DatetimeFilterCreator,
|
32
|
+
)
|
33
|
+
|
34
|
+
from edu_rdm_integration.collect_and_export_data.actions import (
|
35
|
+
BaseCommandProgressPack,
|
36
|
+
BaseStartTaskAction,
|
37
|
+
)
|
38
|
+
from edu_rdm_integration.collect_and_export_data.models import (
|
39
|
+
EduRdmExportDataCommandProgress,
|
40
|
+
)
|
41
|
+
from edu_rdm_integration.enums import (
|
42
|
+
CommandType,
|
43
|
+
)
|
44
|
+
from edu_rdm_integration.export_data.generators import (
|
45
|
+
BaseFirstExportEntitiesDataCommandsGenerator,
|
46
|
+
)
|
47
|
+
from edu_rdm_integration.export_data.ui import (
|
48
|
+
CreateExportCommandWindow,
|
49
|
+
DetailExportCommandWindow,
|
50
|
+
ExportCommandProgressListWindow,
|
51
|
+
)
|
52
|
+
from edu_rdm_integration.helpers import (
|
53
|
+
make_download_link,
|
54
|
+
)
|
55
|
+
from edu_rdm_integration.models import (
|
56
|
+
ExportingDataStageStatus,
|
57
|
+
ExportingDataSubStage,
|
58
|
+
ExportingDataSubStageStatus,
|
59
|
+
RegionalDataMartEntityEnum,
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
class BaseExportingDataProgressPack(BaseCommandProgressPack):
|
64
|
+
"""Базоый пак команд экспорта данных сущностей РВД."""
|
65
|
+
|
66
|
+
model = EduRdmExportDataCommandProgress
|
67
|
+
title = 'Экспорт данных сущностей РВД'
|
68
|
+
|
69
|
+
add_window = CreateExportCommandWindow
|
70
|
+
edit_window = DetailExportCommandWindow
|
71
|
+
list_window = ExportCommandProgressListWindow
|
72
|
+
|
73
|
+
need_check_permission = True
|
74
|
+
|
75
|
+
select_related = ['task', 'task__status']
|
76
|
+
|
77
|
+
list_sort_order = ('-created', 'entity__order_number', 'generation_id')
|
78
|
+
date_filter = partial(DatetimeFilterCreator, model)
|
79
|
+
|
80
|
+
columns = [
|
81
|
+
{
|
82
|
+
'data_index': 'entity.pk',
|
83
|
+
'header': 'Сущность',
|
84
|
+
'sortable': True,
|
85
|
+
'filter': ChoicesFilter(
|
86
|
+
choices=[(key, key) for key in RegionalDataMartEntityEnum.get_model_enum_keys()],
|
87
|
+
parser=str,
|
88
|
+
lookup=lambda key: Q(entity=key) if key else Q(),
|
89
|
+
),
|
90
|
+
},
|
91
|
+
{
|
92
|
+
'data_index': 'task.status.title',
|
93
|
+
'header': 'Статус асинхронной задачи',
|
94
|
+
'sortable': True,
|
95
|
+
'filter': ChoicesFilter(
|
96
|
+
choices=[(value.key, value.title) for value in AsyncTaskStatus.get_model_enum_values()],
|
97
|
+
parser=str,
|
98
|
+
lookup='task__status_id',
|
99
|
+
),
|
100
|
+
},
|
101
|
+
{
|
102
|
+
'data_index': 'type',
|
103
|
+
'header': 'Тип команды',
|
104
|
+
'filter': ChoicesFilter(
|
105
|
+
choices=CommandType.get_choices(),
|
106
|
+
parser=int,
|
107
|
+
lookup='type',
|
108
|
+
),
|
109
|
+
'width': 60,
|
110
|
+
},
|
111
|
+
{
|
112
|
+
'data_index': 'finished_sub_stages',
|
113
|
+
'header': 'Подэтапов <br> выполнено',
|
114
|
+
'width': 50,
|
115
|
+
},
|
116
|
+
{
|
117
|
+
'data_index': 'ready_sub_stages',
|
118
|
+
'header': 'Подэтапов <br> подготовлено <br> к выгрузке',
|
119
|
+
'width': 50,
|
120
|
+
},
|
121
|
+
{
|
122
|
+
'data_index': 'process_errors_sub_stages',
|
123
|
+
'header': 'Подэтапов <br> с ошибкой <br> обработки <br> запроса',
|
124
|
+
'width': 50,
|
125
|
+
},
|
126
|
+
{
|
127
|
+
'data_index': 'stage.status.key',
|
128
|
+
'header': 'Статус экспорта',
|
129
|
+
'sortable': True,
|
130
|
+
'filter': ChoicesFilter(
|
131
|
+
choices=[(key, key) for key in ExportingDataStageStatus.get_model_enum_keys()],
|
132
|
+
parser=str,
|
133
|
+
lookup=lambda key: Q(stage__status=key) if key else Q(),
|
134
|
+
),
|
135
|
+
'width': 50,
|
136
|
+
},
|
137
|
+
{
|
138
|
+
'data_index': 'stage.started_at',
|
139
|
+
'header': 'Время начала экспорта',
|
140
|
+
'sortable': True,
|
141
|
+
'filter': date_filter('stage__started_at').filter,
|
142
|
+
},
|
143
|
+
{
|
144
|
+
'data_index': 'log_url',
|
145
|
+
'header': 'Ссылка на логи',
|
146
|
+
'width': 60,
|
147
|
+
},
|
148
|
+
{
|
149
|
+
'data_index': 'period_started_at',
|
150
|
+
'header': 'Начало периода',
|
151
|
+
'sortable': True,
|
152
|
+
'filter': date_filter('period_started_at').filter,
|
153
|
+
},
|
154
|
+
{
|
155
|
+
'data_index': 'period_ended_at',
|
156
|
+
'header': 'Конец периода',
|
157
|
+
'sortable': True,
|
158
|
+
'filter': date_filter('period_ended_at').filter,
|
159
|
+
},
|
160
|
+
{
|
161
|
+
'data_index': 'generation_id',
|
162
|
+
'header': 'ID генерации',
|
163
|
+
'sortable': True,
|
164
|
+
},
|
165
|
+
{
|
166
|
+
'data_index': 'created',
|
167
|
+
'header': 'Дата создания',
|
168
|
+
'sortable': True,
|
169
|
+
},
|
170
|
+
]
|
171
|
+
|
172
|
+
_start_task_action_cls: BaseStartTaskAction
|
173
|
+
_revoke_task_action_cls: RevokeAsyncTaskAction
|
174
|
+
|
175
|
+
def __init__(self):
|
176
|
+
super().__init__()
|
177
|
+
|
178
|
+
self.start_task_action = self._start_task_action_cls()
|
179
|
+
self.revoke_task_action = self._revoke_task_action_cls()
|
180
|
+
self.prepare_sub_stage_for_export_action = PrepareSubStageForExportAction()
|
181
|
+
|
182
|
+
self.actions.extend((self.start_task_action, self.revoke_task_action, self.prepare_sub_stage_for_export_action))
|
183
|
+
|
184
|
+
def get_list_window_params(self, params, request, context):
|
185
|
+
"""Получает параметры окна списка."""
|
186
|
+
params = super().get_list_window_params(params, request, context)
|
187
|
+
|
188
|
+
params['revoke_url'] = self.revoke_task_action.get_absolute_url()
|
189
|
+
params['start_task_url'] = self.start_task_action.get_absolute_url()
|
190
|
+
params['sub_stage_for_export_url'] = self.prepare_sub_stage_for_export_action.get_absolute_url()
|
191
|
+
|
192
|
+
return params
|
193
|
+
|
194
|
+
def declare_context(self, action):
|
195
|
+
"""Декларирует контекст экшна."""
|
196
|
+
context = super().declare_context(action)
|
197
|
+
|
198
|
+
if action is self.save_action:
|
199
|
+
context['period_started_at'] = {'type': 'datetime'}
|
200
|
+
context['period_ended_at'] = {'type': 'datetime'}
|
201
|
+
context['batch_size'] = {'type': 'int'}
|
202
|
+
elif action in (self.start_task_action, self.prepare_sub_stage_for_export_action):
|
203
|
+
context['commands'] = {'type': 'int_list'}
|
204
|
+
context['queue_level'] = {'type': int, 'default': None}
|
205
|
+
elif action is self.revoke_task_action:
|
206
|
+
context['async_task_ids'] = {'type': 'str', 'default': ''}
|
207
|
+
|
208
|
+
return context
|
209
|
+
|
210
|
+
def get_rows_query(self, request, context):
|
211
|
+
"""Возвращает выборку из БД для получения списка данных."""
|
212
|
+
query = super().get_rows_query(request, context)
|
213
|
+
|
214
|
+
return query.annotate(
|
215
|
+
finished_sub_stages=Subquery(
|
216
|
+
ExportingDataSubStage.objects.filter(
|
217
|
+
stage_id=OuterRef('stage_id'),
|
218
|
+
status=ExportingDataSubStageStatus.FINISHED.key,
|
219
|
+
)
|
220
|
+
.annotate(
|
221
|
+
finished_count=Func(F('id'), function='Count'),
|
222
|
+
)
|
223
|
+
.values('finished_count'),
|
224
|
+
),
|
225
|
+
ready_sub_stages=Subquery(
|
226
|
+
ExportingDataSubStage.objects.filter(
|
227
|
+
stage_id=OuterRef('stage_id'),
|
228
|
+
status=ExportingDataSubStageStatus.READY_FOR_EXPORT.key,
|
229
|
+
)
|
230
|
+
.annotate(
|
231
|
+
ready_count=Func(F('id'), function='Count', output_field=IntegerField()),
|
232
|
+
)
|
233
|
+
.values('ready_count'),
|
234
|
+
),
|
235
|
+
process_errors_sub_stages=Subquery(
|
236
|
+
ExportingDataSubStage.objects.filter(
|
237
|
+
stage_id=OuterRef('stage_id'),
|
238
|
+
status=ExportingDataSubStageStatus.PROCESS_ERROR.key,
|
239
|
+
)
|
240
|
+
.annotate(process_errors_count=Func(F('id'), function='Count', output_field=IntegerField()))
|
241
|
+
.values('process_errors_count'),
|
242
|
+
),
|
243
|
+
)
|
244
|
+
|
245
|
+
def prepare_row(self, obj, request, context):
|
246
|
+
"""Подготовка данных для отображения в реестре."""
|
247
|
+
obj.log_url = make_download_link(obj.logs_link)
|
248
|
+
|
249
|
+
return obj
|
250
|
+
|
251
|
+
@atomic
|
252
|
+
def save_row(self, obj, create_new, request, context, *args, **kwargs):
|
253
|
+
"""Сохраняет объекты."""
|
254
|
+
commands = BaseFirstExportEntitiesDataCommandsGenerator(
|
255
|
+
entities=[obj.entity_id],
|
256
|
+
period_started_at=context.period_started_at,
|
257
|
+
period_ended_at=context.period_ended_at,
|
258
|
+
batch_size=context.batch_size,
|
259
|
+
).generate()
|
260
|
+
|
261
|
+
for command in commands:
|
262
|
+
obj = self.model(
|
263
|
+
entity_id=obj.entity_id,
|
264
|
+
period_started_at=command['period_started_at'],
|
265
|
+
period_ended_at=command['period_ended_at'],
|
266
|
+
generation_id=command['generation_id'],
|
267
|
+
type=CommandType.MANUAL,
|
268
|
+
)
|
269
|
+
super().save_row(obj, create_new, request, context, *args, **kwargs)
|
270
|
+
|
271
|
+
|
272
|
+
class PrepareSubStageForExportAction(BaseAction):
|
273
|
+
"""Смена статусов у подэтапов для переотправки."""
|
274
|
+
|
275
|
+
def run(self, request, context):
|
276
|
+
"""Обновление статусов подэтапов не принятых витриной."""
|
277
|
+
command_ids = context.commands
|
278
|
+
stage_ids = EduRdmExportDataCommandProgress.objects.filter(id__in=command_ids).values_list(
|
279
|
+
'stage_id', flat=True
|
280
|
+
)
|
281
|
+
|
282
|
+
updated_count = ExportingDataSubStage.objects.filter(
|
283
|
+
stage_id__in=stage_ids,
|
284
|
+
status=ExportingDataSubStageStatus.PROCESS_ERROR.key,
|
285
|
+
).update(status=ExportingDataSubStageStatus.READY_FOR_EXPORT.key)
|
286
|
+
if updated_count:
|
287
|
+
message = f'Будет переотправлено {updated_count} подэтапов.'
|
288
|
+
else:
|
289
|
+
message = 'Подэтапов для переотправления не найдено.'
|
290
|
+
|
291
|
+
return OperationResult(success=True, message=message)
|