edu-rdm-integration 3.8.0__py3-none-any.whl → 3.9.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.
@@ -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 else None
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
+ )
@@ -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)