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.
@@ -0,0 +1,121 @@
1
+ from django.conf import (
2
+ settings,
3
+ )
4
+ from m3 import (
5
+ OperationResult,
6
+ )
7
+ from m3_ext.ui.containers import (
8
+ ExtGridCheckBoxSelModel,
9
+ )
10
+ from objectpack.actions import (
11
+ BaseAction,
12
+ BaseWindowAction,
13
+ ObjectPack,
14
+ )
15
+ from objectpack.filters import (
16
+ ColumnFilterEngine,
17
+ )
18
+
19
+ from edu_rdm_integration.collect_and_export_data.ui import (
20
+ CommandQueueSelectWindow,
21
+ )
22
+ from edu_rdm_integration.collect_and_export_data.utils import (
23
+ BaseTaskStarter,
24
+ )
25
+ from edu_rdm_integration.export_data.ui import (
26
+ CommandProgressListWindow,
27
+ )
28
+ from edu_rdm_integration.helpers import (
29
+ make_download_link,
30
+ )
31
+
32
+
33
+ class BaseCommandProgressPack(ObjectPack):
34
+ """Базовый пак прогресса выполнения команд сбора/экспорта данных."""
35
+
36
+ list_window = CommandProgressListWindow
37
+ can_delete = False
38
+
39
+ select_related = ['task', 'stage__status']
40
+
41
+ filter_engine_clz = ColumnFilterEngine
42
+
43
+ def __init__(self):
44
+ super().__init__()
45
+
46
+ self.queue_select_win_action = QueueLevelSelectWinAction()
47
+
48
+ self.actions.append(self.queue_select_win_action)
49
+
50
+ def get_list_window_params(self, params, request, context):
51
+ """Получает параметры окна списка."""
52
+ params = super().get_list_window_params(params, request, context)
53
+
54
+ params['queue_select_win_url'] = self.queue_select_win_action.get_absolute_url()
55
+
56
+ return params
57
+
58
+ def get_edit_window_params(self, params, request, context):
59
+ """Возвращает словарь параметров, которые будут переданы окну редактирования."""
60
+ params = super().get_edit_window_params(params, request, context)
61
+
62
+ if not params['create_new']:
63
+ params['read_only'] = True
64
+ obj = params['object']
65
+ params['log_url'] = make_download_link(obj.logs_link)
66
+
67
+ return params
68
+
69
+ def configure_grid(self, grid, *args, **kwargs):
70
+ """Конфигурирует грид."""
71
+ super().configure_grid(grid, *args, **kwargs)
72
+
73
+ grid.sm = ExtGridCheckBoxSelModel()
74
+ grid.top_bar.button_new.text = 'Сгенерировать команды'
75
+ grid.top_bar.button_edit.text = 'Просмотр'
76
+ grid.top_bar.button_edit.icon_cls = 'icon-application-view-detail'
77
+ grid.context_menu_row.menuitem_edit.text = 'Просмотр'
78
+ grid.context_menu_row.menuitem_edit.icon_cls = 'icon-application-view-detail'
79
+
80
+ def extend_menu(self, menu):
81
+ """Расширяет главное меню."""
82
+ if settings.RDM_MENU_ITEM:
83
+ return menu.SubMenu(
84
+ 'Администрирование',
85
+ menu.SubMenu(
86
+ 'Региональная витрина данных',
87
+ menu.Item(
88
+ self.title,
89
+ self.list_window_action,
90
+ ),
91
+ icon='menu-dicts-16',
92
+ ),
93
+ )
94
+
95
+
96
+ class BaseStartTaskAction(BaseAction):
97
+ """
98
+ Базовый экшн создания асинхронных задач для выгрузки РВД.
99
+ """
100
+
101
+ url: str = None
102
+
103
+ task_starter: BaseTaskStarter = None
104
+
105
+ def run(self, request, context):
106
+ """Непосредственное исполнение запроса."""
107
+ queue_level = getattr(context, 'queue_level', None)
108
+ result = self.task_starter().run(command_ids=context.commands, queue_level=queue_level) # noqa pylint: disable=not-callable
109
+
110
+ return OperationResult(
111
+ success=True,
112
+ message=result,
113
+ )
114
+
115
+
116
+ class QueueLevelSelectWinAction(BaseWindowAction):
117
+ """Экшен окна выбора очереди для запуска ручной команды собра/выгрузки."""
118
+
119
+ def create_window(self):
120
+ """Создание окна."""
121
+ self.win = CommandQueueSelectWindow()
@@ -0,0 +1,137 @@
1
+ from typing import (
2
+ Iterable,
3
+ )
4
+
5
+ from m3_ext.ui import (
6
+ all_components as ext,
7
+ )
8
+ from m3_ext.ui.icons import (
9
+ Icons,
10
+ )
11
+ from objectpack.ui import (
12
+ BaseEditWindow,
13
+ BaseWindow,
14
+ ComboBoxWithStore,
15
+ )
16
+
17
+ from educommon.objectpack.ui import (
18
+ BaseListWindow,
19
+ )
20
+ from educommon.utils.ui import (
21
+ append_template_globals,
22
+ )
23
+
24
+ from edu_rdm_integration.enums import (
25
+ EntityLevelQueueTypeEnum,
26
+ )
27
+
28
+
29
+ class CommandProgressListWindow(BaseListWindow):
30
+ """Окно списка команд на сбор/экспорт данных."""
31
+
32
+ def set_params(self, params):
33
+ """Устанавливает параметры окна."""
34
+ super().set_params(params)
35
+
36
+ self.maximized = True
37
+ append_template_globals(self, 'ui-js/start-task.js')
38
+ append_template_globals(self, 'ui-js/async-task-revoke.js')
39
+
40
+ self.start_task_url = params['start_task_url']
41
+ self.revoke_url = params['revoke_url']
42
+ self.queue_select_win_url = params['queue_select_win_url']
43
+
44
+ def _init_components(self):
45
+ """Инициализирует компоненты окна."""
46
+ super()._init_components()
47
+
48
+ self.start_task_button = ext.ExtButton(
49
+ text='Запустить команду',
50
+ icon_cls=Icons.APPLICATION_GO,
51
+ handler='startTask',
52
+ )
53
+ self.revoke_task_button = ext.ExtButton(
54
+ text='Отменить',
55
+ icon_cls=Icons.CANCEL,
56
+ handler='revokeTask',
57
+ )
58
+
59
+ def _do_layout(self):
60
+ """Располагает компоненты окна."""
61
+ super()._do_layout()
62
+
63
+ self.grid.top_bar.items.insert(1, self.start_task_button)
64
+ self.grid.top_bar.items.append(self.revoke_task_button)
65
+
66
+
67
+ class BaseCreateCommandWindow(BaseEditWindow):
68
+ """Базовое окно создания команды на сбор/экспорт данных."""
69
+
70
+ def _init_components(self):
71
+ """Инициализация компонентов."""
72
+ super()._init_components()
73
+
74
+ # Поля, которые нужно добавить на форму:
75
+ self.items_: Iterable = ()
76
+
77
+ def _do_layout(self):
78
+ """Расположение компонентов."""
79
+ super()._do_layout()
80
+
81
+ self.form.items.extend(self.items_)
82
+
83
+ def set_params(self, params):
84
+ """Параметры окна."""
85
+ super().set_params(params)
86
+
87
+ self.form.label_width = 150
88
+ self.width = 400
89
+ self.height = 'auto'
90
+
91
+
92
+ class CommandQueueSelectWindow(BaseWindow):
93
+ """Окно выбора очереди для запуска периодических задач команд."""
94
+
95
+ def _init_components(self):
96
+ """Инициализация компонентов."""
97
+ super()._init_components()
98
+
99
+ self.queue_level_combobox = ComboBoxWithStore(
100
+ label='Очередь выполнения команд',
101
+ name='queue_level',
102
+ data=EntityLevelQueueTypeEnum.get_choices(),
103
+ value=EntityLevelQueueTypeEnum.BASE,
104
+ allow_blank=False,
105
+ editable=False,
106
+ anchor='100%',
107
+ )
108
+ self.close_btn = self.btn_close = ext.ExtButton(
109
+ name='close_btn',
110
+ text='Закрыть',
111
+ handler='function(){Ext.getCmp("%s").close();}' % self.client_id,
112
+ )
113
+ self.start_task_button = ext.ExtButton(
114
+ name='start_task',
115
+ text='Запустить команду',
116
+ handler='function(){ win.fireEvent("closed_ok");}',
117
+ )
118
+
119
+ def _do_layout(self):
120
+ """Расположение компонентов."""
121
+ super()._do_layout()
122
+
123
+ self.items.append(self.queue_level_combobox)
124
+ self.buttons.extend(
125
+ (
126
+ self.start_task_button,
127
+ self.btn_close,
128
+ )
129
+ )
130
+
131
+ def set_params(self, params):
132
+ """Параметры окна."""
133
+ super().set_params(params)
134
+
135
+ self.width = 400
136
+ self.height = 'auto'
137
+ self.title = 'Очередь для выполнения команды'
@@ -1,6 +1,26 @@
1
+ from abc import (
2
+ ABC,
3
+ abstractmethod,
4
+ )
5
+ from typing import (
6
+ Dict,
7
+ Iterable,
8
+ Optional,
9
+ )
10
+
11
+ from django.db.models import (
12
+ QuerySet,
13
+ )
14
+
15
+ from educommon.async_task.exceptions import (
16
+ TaskUniqueException,
17
+ )
1
18
  from educommon.async_task.models import (
2
19
  AsyncTaskType,
3
20
  )
21
+ from educommon.async_task.tasks import (
22
+ AsyncTask,
23
+ )
4
24
 
5
25
  from edu_rdm_integration.collect_and_export_data.models import (
6
26
  EduRdmCollectDataCommandProgress,
@@ -12,6 +32,9 @@ from edu_rdm_integration.collect_data.collect import (
12
32
  from edu_rdm_integration.consts import (
13
33
  TASK_QUEUE_NAME,
14
34
  )
35
+ from edu_rdm_integration.enums import (
36
+ EntityLevelQueueTypeEnum,
37
+ )
15
38
  from edu_rdm_integration.export_data.export import (
16
39
  ExportEntitiesData,
17
40
  )
@@ -92,3 +115,110 @@ class ExportCommandMixin:
92
115
 
93
116
  if command:
94
117
  save_command_log_link(command, log_dir)
118
+
119
+
120
+ class BaseTaskProgressUpdater(ABC):
121
+ """Базовый класс, который обновляет данные в таблицах, хранящих команды сбора/экспорта."""
122
+
123
+ @property
124
+ @abstractmethod
125
+ def update_model(self):
126
+ """
127
+ Основная модель для обновления.
128
+
129
+ Необходимо задать в дочернем классе.
130
+ """
131
+
132
+ @property
133
+ @abstractmethod
134
+ def async_model(self):
135
+ """
136
+ Модель асинхронных задач.
137
+
138
+ Необходимо задать в дочернем классе.
139
+ """
140
+
141
+ def set_async_task(self, commands_to_update: Dict[EduRdmCollectDataCommandProgress, str]) -> None:
142
+ """Устанавливает ссылку на асинхронную задачу."""
143
+ for command, task_uuid in commands_to_update.items():
144
+ command.task_id = task_uuid
145
+
146
+ self.update_model.objects.bulk_update(
147
+ commands_to_update,
148
+ ['task_id'],
149
+ )
150
+
151
+ def set_stage(self, command_id: int, stage_id: int) -> None:
152
+ """Устанавливает ссылку на stage."""
153
+ self.update_model.objects.filter(
154
+ id=command_id,
155
+ ).update(
156
+ stage_id=stage_id,
157
+ )
158
+
159
+
160
+ class BaseTaskStarter(ABC):
161
+ """Запускает асинхронные задачи."""
162
+
163
+ updater: BaseTaskProgressUpdater = None
164
+ async_task: AsyncTask = None
165
+ model_only_fields: Iterable[str] = ()
166
+
167
+ def run(self, command_ids: Iterable[int], queue_level: Optional[int] = None) -> str:
168
+ """Создает задачи и ставит их в очередь."""
169
+ commands_to_update = {}
170
+ skipped_commands_count = 0
171
+ commands = self._get_commands(command_ids)
172
+ queue_name = None
173
+
174
+ if queue_level:
175
+ queue_name = EntityLevelQueueTypeEnum.get_queue_name(level=queue_level)
176
+
177
+ if not queue_name:
178
+ queue_name = TASK_QUEUE_NAME
179
+
180
+ for command in commands:
181
+ if command.task_id:
182
+ # Повторный запуск команды не допускается
183
+ skipped_commands_count += 1
184
+ continue
185
+
186
+ try:
187
+ async_result = self.async_task().apply_async( # noqa pylint: disable=not-callable
188
+ args=None,
189
+ queue=queue_name,
190
+ routing_key=queue_name,
191
+ kwargs={
192
+ 'command_id': command.id,
193
+ },
194
+ lock_data={
195
+ 'lock_params': {
196
+ 'command_id': f'{self.updater.update_model.__name__}_{command.id}',
197
+ },
198
+ },
199
+ )
200
+ except TaskUniqueException:
201
+ skipped_commands_count += 1
202
+ continue
203
+ else:
204
+ commands_to_update[command] = async_result.task_id
205
+
206
+ if commands_to_update:
207
+ self.updater().set_async_task(commands_to_update) # noqa pylint: disable=not-callable
208
+
209
+ message = f'Поставлено задач в очередь: {len(commands_to_update)} из {len(commands)}.'
210
+ if skipped_commands_count:
211
+ message += (
212
+ f' Кол-во задач, которые были запущены ранее: {skipped_commands_count}. '
213
+ 'Однажды запущенные задачи не могут быть запущены снова!'
214
+ )
215
+
216
+ return message
217
+
218
+ def _get_commands(self, command_ids: Iterable[int]) -> 'QuerySet':
219
+ """Возвращает Queryset команд для создания задач."""
220
+ return self.updater.update_model.objects.filter(
221
+ id__in=command_ids,
222
+ ).only(
223
+ *self.model_only_fields,
224
+ )
@@ -0,0 +1,276 @@
1
+ from functools import (
2
+ partial,
3
+ )
4
+
5
+ from django.db.models import (
6
+ F,
7
+ Func,
8
+ OuterRef,
9
+ Q,
10
+ Subquery,
11
+ )
12
+ from django.db.transaction import (
13
+ atomic,
14
+ )
15
+ from m3.actions.exceptions import (
16
+ ApplicationLogicException,
17
+ )
18
+
19
+ from educommon.async_task.actions import (
20
+ RevokeAsyncTaskAction,
21
+ )
22
+ from educommon.async_task.models import (
23
+ AsyncTaskStatus,
24
+ )
25
+ from educommon.utils.conversion import (
26
+ int_or_none,
27
+ )
28
+ from educommon.utils.ui import (
29
+ ChoicesFilter,
30
+ DatetimeFilterCreator,
31
+ )
32
+
33
+ from edu_rdm_integration.collect_and_export_data.actions import (
34
+ BaseCommandProgressPack,
35
+ BaseStartTaskAction,
36
+ )
37
+ from edu_rdm_integration.collect_and_export_data.models import (
38
+ EduRdmCollectDataCommandProgress,
39
+ )
40
+ from edu_rdm_integration.collect_data.generators import (
41
+ FirstCollectModelsDataCommandsGenerator,
42
+ )
43
+ from edu_rdm_integration.collect_data.ui import (
44
+ CreateCollectCommandWindow,
45
+ DetailCollectCommandWindow,
46
+ )
47
+ from edu_rdm_integration.enums import (
48
+ CommandType,
49
+ )
50
+ from edu_rdm_integration.helpers import (
51
+ make_download_link,
52
+ )
53
+ from edu_rdm_integration.models import (
54
+ CollectingDataStageStatus,
55
+ CollectingDataSubStageStatus,
56
+ CollectingExportedDataSubStage,
57
+ RegionalDataMartModelEnum,
58
+ )
59
+
60
+
61
+ class BaseCollectingDataProgressPack(BaseCommandProgressPack):
62
+ """Базовый пак команд сбора данных моделей РВД."""
63
+
64
+ title = 'Сбор данных моделей РВД'
65
+ model = EduRdmCollectDataCommandProgress
66
+
67
+ add_window = CreateCollectCommandWindow
68
+ edit_window = DetailCollectCommandWindow
69
+
70
+ need_check_permission = True
71
+
72
+ select_related = ['task', 'task__status']
73
+
74
+ list_sort_order = ('-created', 'model__order_number', 'generation_id')
75
+ date_filter = partial(DatetimeFilterCreator, model)
76
+
77
+ columns = [
78
+ {
79
+ 'data_index': 'model.pk',
80
+ 'header': 'Модель',
81
+ 'sortable': True,
82
+ 'filter': ChoicesFilter(
83
+ choices=[(key, key) for key in RegionalDataMartModelEnum.get_model_enum_keys()],
84
+ parser=str,
85
+ lookup=lambda key: Q(model=key) if key else Q(),
86
+ ),
87
+ },
88
+ {
89
+ 'data_index': 'task.status.title',
90
+ 'header': 'Статус асинхронной задачи',
91
+ 'sortable': True,
92
+ 'filter': ChoicesFilter(
93
+ choices=[(value.key, value.title) for value in AsyncTaskStatus.get_model_enum_values()],
94
+ parser=str,
95
+ lookup='task__status_id',
96
+ ),
97
+ },
98
+ {
99
+ 'data_index': 'type',
100
+ 'header': 'Тип команды',
101
+ 'filter': ChoicesFilter(
102
+ choices=CommandType.get_choices(),
103
+ parser=int,
104
+ lookup='type',
105
+ ),
106
+ 'width': 60,
107
+ },
108
+ {
109
+ 'data_index': 'ready_to_export_sub_stages',
110
+ 'header': 'Подэтапов выполнено',
111
+ 'width': 50,
112
+ },
113
+ {
114
+ 'data_index': 'stage.status.key',
115
+ 'header': 'Статус сбора',
116
+ 'sortable': True,
117
+ 'filter': ChoicesFilter(
118
+ choices=[(key, key) for key in CollectingDataStageStatus.get_model_enum_keys()],
119
+ parser=str,
120
+ lookup=lambda key: Q(stage__status=key) if key else Q(),
121
+ ),
122
+ 'width': 50,
123
+ },
124
+ {
125
+ 'data_index': 'stage.started_at',
126
+ 'header': 'Время начала сбора',
127
+ 'sortable': True,
128
+ 'filter': date_filter('stage__started_at').filter,
129
+ },
130
+ {
131
+ 'data_index': 'log_url',
132
+ 'header': 'Ссылка на логи',
133
+ 'width': 60,
134
+ },
135
+ {
136
+ 'data_index': 'logs_period_started_at',
137
+ 'header': 'Начало периода',
138
+ 'sortable': True,
139
+ 'filter': date_filter('logs_period_started_at').filter,
140
+ },
141
+ {
142
+ 'data_index': 'logs_period_ended_at',
143
+ 'header': 'Конец периода',
144
+ 'sortable': True,
145
+ 'filter': date_filter('logs_period_ended_at').filter,
146
+ },
147
+ {
148
+ 'data_index': 'generation_id',
149
+ 'header': 'ID генерации',
150
+ 'sortable': True,
151
+ },
152
+ {
153
+ 'data_index': 'created',
154
+ 'header': 'Дата создания',
155
+ 'sortable': True,
156
+ },
157
+ ]
158
+
159
+ _start_task_action_cls: BaseStartTaskAction
160
+ _revoke_task_action_cls: RevokeAsyncTaskAction
161
+
162
+ def __init__(self):
163
+ super().__init__()
164
+ self.start_task_action = self._start_task_action_cls()
165
+ self.revoke_task_action = self._revoke_task_action_cls()
166
+
167
+ self.actions.extend(
168
+ (
169
+ self.start_task_action,
170
+ self.revoke_task_action,
171
+ )
172
+ )
173
+
174
+ def get_list_window_params(self, params, request, context):
175
+ """Получает параметры окна списка."""
176
+
177
+ params = super().get_list_window_params(params, request, context)
178
+
179
+ params['start_task_url'] = self.start_task_action.get_absolute_url()
180
+ params['revoke_url'] = self.revoke_task_action.get_absolute_url()
181
+
182
+ return params
183
+
184
+ def declare_context(self, action):
185
+ """Объявление контекста."""
186
+
187
+ context = super().declare_context(action)
188
+
189
+ if action is self.save_action:
190
+ context['logs_period_started_at'] = context['logs_period_ended_at'] = {'type': 'datetime'}
191
+ context['split_by_quantity'] = context['batch_size'] = {'type': 'int_or_none', 'default': None}
192
+ context['institute_count'] = {'type': 'int'}
193
+ context['split_by'] = context['split_mode'] = {'type': 'str', 'default': None}
194
+ context['by_institutes'] = {'type': 'boolean', 'default': False}
195
+ context['institute_ids'] = {'type': 'int_list'}
196
+ elif action is self.start_task_action:
197
+ context['commands'] = {'type': 'int_list'}
198
+ context['queue_level'] = {'type': int, 'default': None}
199
+ elif action is self.revoke_task_action:
200
+ context['async_task_ids'] = {'type': 'str', 'default': ''}
201
+
202
+ return context
203
+
204
+ def get_rows_query(self, request, context):
205
+ """Возвращает выборку из БД для получения списка данных."""
206
+ query = super().get_rows_query(request, context)
207
+
208
+ # Необходимо также рассчитать прогресс сбора:
209
+ query = query.annotate(
210
+ ready_to_export_sub_stages=Subquery(
211
+ CollectingExportedDataSubStage.objects.filter(
212
+ stage_id=OuterRef('stage_id'),
213
+ status_id=CollectingDataSubStageStatus.READY_TO_EXPORT.key,
214
+ )
215
+ .annotate(ready_to_export_sub_stages=Func(F('id'), function='Count'))
216
+ .values('ready_to_export_sub_stages')
217
+ )
218
+ )
219
+
220
+ return query
221
+
222
+ def prepare_row(self, obj, request, context):
223
+ """Подготовка данных для отображения в реестре."""
224
+ obj.log_url = make_download_link(obj.logs_link)
225
+
226
+ return obj
227
+
228
+ def _get_actual_institute_ids(self):
229
+ """Возвращает кортеж из идентификаторов организаций, данные по которым можно собрать."""
230
+ raise NotImplementedError
231
+
232
+ @atomic
233
+ def save_row(self, obj, create_new, request, context, *args, **kwargs):
234
+ """
235
+ Сохраняет объект.
236
+
237
+ Переопределено, т.к. на основе полученных параметров от клиента,
238
+ необходимо сформировать команды на сбор и их сохранить в модели.
239
+ """
240
+ batch_size = int_or_none(context.batch_size)
241
+ if not context.split_by and not batch_size:
242
+ raise ApplicationLogicException('Поле "Размер чанка" обязательно к заполнению')
243
+
244
+ split_by_quantity = int_or_none(context.split_by_quantity)
245
+ if context.split_by and not split_by_quantity:
246
+ raise ApplicationLogicException('Поле "Размер подпериода" обязательно к заполнению')
247
+
248
+ commands_to_save = FirstCollectModelsDataCommandsGenerator(
249
+ models=[obj.model_id],
250
+ split_by=context.split_by,
251
+ split_mode=context.split_mode,
252
+ split_by_quantity=context.split_by_quantity,
253
+ logs_period_started_at=context.logs_period_started_at,
254
+ logs_period_ended_at=context.logs_period_ended_at,
255
+ batch_size=context.batch_size,
256
+ ).generate_with_split(
257
+ by_institutes=context.by_institutes,
258
+ institute_ids=context.institute_ids,
259
+ institute_count=context.institute_count,
260
+ actual_institute_ids=self._get_actual_institute_ids(),
261
+ )
262
+
263
+ objs = [
264
+ self.model(
265
+ model_id=obj.model_id,
266
+ logs_period_started_at=command['period_started_at'],
267
+ logs_period_ended_at=command['period_ended_at'],
268
+ generation_id=command['generation_id'],
269
+ institute_ids=command['institute_ids'],
270
+ type=CommandType.MANUAL,
271
+ )
272
+ for command in commands_to_save
273
+ ]
274
+
275
+ for obj in objs:
276
+ super().save_row(obj, create_new, request, context, *args, **kwargs)
@@ -0,0 +1,2 @@
1
+ # Константа сбора по всем организациям через интерфейс
2
+ ALL_UNITS_IN_COMMAND = -1