educommon 3.12.0__py3-none-any.whl → 3.13.2__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.
- educommon/__init__.py +0 -1
- educommon/about/ui/actions.py +16 -30
- educommon/about/ui/ui.py +3 -12
- educommon/about/utils.py +6 -5
- educommon/async_task/__init__.py +0 -1
- educommon/async_task/actions.py +18 -13
- educommon/async_task/apps.py +4 -0
- educommon/async_task/locker.py +2 -5
- educommon/async_task/migrations/0001_initial.py +55 -9
- educommon/async_task/migrations/0002_task_type_and_status_data.py +94 -89
- educommon/async_task/migrations/0003_alter_runningtask_options.py +0 -1
- educommon/async_task/models.py +9 -6
- educommon/async_task/tasks.py +11 -7
- educommon/async_task/ui.py +16 -35
- educommon/async_tasks/__init__.py +0 -1
- educommon/async_tasks/apps.py +4 -0
- educommon/async_tasks/locks.py +11 -21
- educommon/async_tasks/migrations/0001_initial.py +68 -8
- educommon/async_tasks/migrations/0002_load_initial_data.py +0 -1
- educommon/async_tasks/models.py +9 -29
- educommon/async_tasks/tasks.py +25 -54
- educommon/audit_log/__init__.py +1 -0
- educommon/audit_log/actions.py +27 -36
- educommon/audit_log/app_meta.py +7 -4
- educommon/audit_log/apps.py +44 -29
- educommon/audit_log/constants.py +7 -4
- educommon/audit_log/error_log/actions.py +1 -3
- educommon/audit_log/helpers.py +2 -4
- educommon/audit_log/management/commands/reinstall_audit_log.py +11 -7
- educommon/audit_log/migrations/0001_initial.py +91 -16
- educommon/audit_log/migrations/0002_install_audit_log.py +13 -13
- educommon/audit_log/migrations/0003_logproxy.py +1 -3
- educommon/audit_log/migrations/0004_reinstall_audit_log.py +1 -4
- educommon/audit_log/migrations/0005_postgresql_error.py +4 -2
- educommon/audit_log/migrations/0006_auto_20200806_1707.py +3 -4
- educommon/audit_log/migrations/0007_create_selective_tables_function.py +8 -5
- educommon/audit_log/migrations/0008_table_logged.py +0 -1
- educommon/audit_log/migrations/0009_reinstall_audit_log.py +0 -1
- educommon/audit_log/models.py +36 -42
- educommon/audit_log/permissions.py +11 -9
- educommon/audit_log/proxies.py +12 -23
- educommon/audit_log/ui.py +18 -15
- educommon/audit_log/utils/__init__.py +28 -60
- educommon/audit_log/utils/operations.py +16 -2
- educommon/auth/__init__.py +0 -3
- educommon/auth/rbac/__init__.py +2 -4
- educommon/auth/rbac/actions.py +148 -145
- educommon/auth/rbac/app_meta.py +9 -6
- educommon/auth/rbac/backends/base.py +2 -8
- educommon/auth/rbac/backends/caching.py +27 -37
- educommon/auth/rbac/backends/simple.py +1 -4
- educommon/auth/rbac/checker.py +1 -3
- educommon/auth/rbac/management/commands/rbac.py +6 -11
- educommon/auth/rbac/manager.py +18 -47
- educommon/auth/rbac/migrations/0001_initial.py +73 -12
- educommon/auth/rbac/migrations/0002_model_modifier_metaclass_fix.py +7 -6
- educommon/auth/rbac/migrations/0003_permission_hidden.py +1 -5
- educommon/auth/rbac/migrations/0004_auto_20171024_1245.py +26 -19
- educommon/auth/rbac/models.py +63 -68
- educommon/auth/rbac/permissions.py +6 -7
- educommon/auth/rbac/ui.py +83 -84
- educommon/auth/rbac/utils.py +10 -11
- educommon/auth/rbac/validators.py +4 -5
- educommon/auth/simple_auth/__init__.py +1 -5
- educommon/auth/simple_auth/actions.py +79 -92
- educommon/auth/simple_auth/app_meta.py +2 -9
- educommon/auth/simple_auth/checkers.py +3 -3
- educommon/auth/simple_auth/migrations/0001_initial.py +23 -4
- educommon/auth/simple_auth/validators.py +0 -1
- educommon/contingent/actions.py +7 -7
- educommon/contingent/app_meta.py +1 -4
- educommon/contingent/base.py +10 -15
- educommon/contingent/catalogs.py +424 -540
- educommon/contingent/contingent_plugin/actions.py +4 -15
- educommon/contingent/contingent_plugin/apps.py +10 -4
- educommon/contingent/contingent_plugin/migrations/0001_initial.py +5 -6
- educommon/contingent/contingent_plugin/migrations/0002_add_contingent_model_deleted.py +6 -11
- educommon/contingent/contingent_plugin/model_views.py +2 -12
- educommon/contingent/contingent_plugin/models.py +2 -7
- educommon/contingent/contingent_plugin/observer.py +14 -13
- educommon/contingent/contingent_plugin/plugin_meta.py +1 -3
- educommon/contingent/contingent_plugin/storage.py +8 -7
- educommon/contingent/contingent_plugin/utils.py +6 -6
- educommon/django/db/fields.py +72 -86
- educommon/django/db/migration/__init__.py +3 -7
- educommon/django/db/migration/operations.py +29 -51
- educommon/django/db/mixins/__init__.py +16 -10
- educommon/django/db/mixins/date_interval.py +47 -75
- educommon/django/db/mixins/validation.py +26 -26
- educommon/django/db/model_view/__init__.py +18 -22
- educommon/django/db/models.py +9 -8
- educommon/django/db/observer.py +9 -27
- educommon/django/db/partitioning/__init__.py +66 -92
- educommon/django/db/partitioning/management/commands/apply_partitioning.py +3 -13
- educommon/django/db/partitioning/management/commands/clear_table.py +18 -14
- educommon/django/db/partitioning/management/commands/split_table.py +18 -13
- educommon/django/db/routers.py +6 -15
- educommon/django/db/signals.py +149 -2
- educommon/django/db/utils.py +14 -19
- educommon/django/db/validators/__init__.py +1 -0
- educommon/django/db/validators/simple.py +72 -100
- educommon/django/storages/atcfs/api.py +39 -53
- educommon/django/storages/atcfs/app_meta.py +1 -1
- educommon/django/storages/atcfs/management/commands/atcfs_migrate.py +42 -55
- educommon/django/storages/atcfs/models.py +0 -3
- educommon/django/storages/atcfs/monkey_patching.py +18 -12
- educommon/django/storages/atcfs/storage.py +14 -23
- educommon/extjs/fields/input_params.py +15 -45
- educommon/importer/XLSReader.py +143 -241
- educommon/importer/__init__.py +86 -4
- educommon/importer/api.py +53 -84
- educommon/importer/constants.py +4 -14
- educommon/importer/loggers.py +16 -26
- educommon/importer/proxy.py +131 -176
- educommon/importer/proxy_import.py +11 -12
- educommon/importer/report.py +4 -6
- educommon/importer/ui.py +32 -26
- educommon/importer/validators.py +4 -7
- educommon/integration_entities/helpers.py +14 -18
- educommon/ioc/__init__.py +3 -6
- educommon/logger/loggers.py +10 -14
- educommon/m3/__init__.py +20 -38
- educommon/m3/extensions/__init__.py +1 -0
- educommon/m3/extensions/listeners/__init__.py +22 -38
- educommon/m3/extensions/listeners/delete_check/listeners.py +31 -41
- educommon/m3/extensions/listeners/delete_check/mixins.py +20 -25
- educommon/m3/extensions/listeners/delete_check/signals.py +2 -2
- educommon/m3/extensions/listeners/delete_check/ui.py +15 -14
- educommon/m3/extensions/listeners/delete_check/utils.py +9 -11
- educommon/m3/extensions/ui.py +15 -33
- educommon/m3/transaction_context.py +17 -19
- educommon/objectpack/actions.py +70 -88
- educommon/objectpack/apps.py +5 -0
- educommon/objectpack/filters.py +9 -15
- educommon/objectpack/ui.py +59 -77
- educommon/report/__init__.py +9 -5
- educommon/report/actions.py +29 -32
- educommon/report/constructor/__init__.py +5 -8
- educommon/report/constructor/app_meta.py +1 -3
- educommon/report/constructor/apps.py +1 -0
- educommon/report/constructor/base.py +33 -80
- educommon/report/constructor/builders/excel/_base.py +138 -286
- educommon/report/constructor/builders/excel/_header.py +2 -9
- educommon/report/constructor/builders/excel/product.py +13 -34
- educommon/report/constructor/builders/excel/with_merged_cells.py +18 -14
- educommon/report/constructor/config.py +2 -0
- educommon/report/constructor/editor/actions.py +101 -215
- educommon/report/constructor/editor/ui.py +71 -93
- educommon/report/constructor/exceptions.py +6 -12
- educommon/report/constructor/migrations/0001_initial.py +36 -44
- educommon/report/constructor/migrations/0002_report_filters.py +86 -72
- educommon/report/constructor/migrations/0003_reportfilter_exclude.py +5 -5
- educommon/report/constructor/migrations/0004_reportfilter_fields.py +22 -18
- educommon/report/constructor/migrations/0005_reportcolumn_visible.py +5 -4
- educommon/report/constructor/migrations/0006_reportsorting.py +21 -17
- educommon/report/constructor/migrations/0007_include_available_units.py +14 -14
- educommon/report/constructor/migrations/0008_auto_20170407_1318.py +4 -5
- educommon/report/constructor/migrations/0009_auto_20180405_0642.py +1 -4
- educommon/report/constructor/migrations/0010_add_aggregate_fields.py +7 -8
- educommon/report/constructor/mixins.py +14 -15
- educommon/report/constructor/models.py +76 -124
- educommon/report/constructor/utils.py +3 -8
- educommon/report/constructor/validators.py +1 -3
- educommon/report/reporter.py +25 -43
- educommon/report/utils.py +14 -40
- educommon/rest/actions.py +7 -11
- educommon/rest/context.py +6 -16
- educommon/rest/controllers.py +10 -10
- educommon/rest/mixins.py +29 -27
- educommon/secure_media/app_meta.py +9 -9
- educommon/utils/__init__.py +3 -2
- educommon/utils/caching.py +1 -3
- educommon/utils/conversion.py +1 -3
- educommon/utils/crypto.py +1 -2
- educommon/utils/date.py +13 -26
- educommon/utils/db/__init__.py +17 -26
- educommon/utils/db/postgresql.py +1 -4
- educommon/utils/fonts/__init__.py +3 -4
- educommon/utils/licence/__init__.py +5 -16
- educommon/utils/misc.py +9 -18
- educommon/utils/object_grid.py +55 -62
- educommon/utils/phone_number/modelfields.py +1 -3
- educommon/utils/phone_number/phone_number.py +5 -8
- educommon/utils/phone_number/validators.py +8 -23
- educommon/utils/plugins.py +15 -28
- educommon/utils/registry.py +2 -1
- educommon/utils/seqtools.py +1 -3
- educommon/utils/serializer.py +9 -16
- educommon/utils/storage.py +3 -2
- educommon/utils/system.py +1 -3
- educommon/utils/system_app/management/commands/delete_objects.py +17 -34
- educommon/utils/ui.py +87 -84
- educommon/utils/xml/__init__.py +2 -7
- educommon/utils/xml/resolver.py +1 -0
- educommon/ws_log/actions.py +31 -76
- educommon/ws_log/base.py +6 -20
- educommon/ws_log/migrations/0001_initial.py +25 -8
- educommon/ws_log/migrations/0002_auto_20160628_1334.py +0 -1
- educommon/ws_log/migrations/0003_add_fields_to_smev_logs.py +20 -4
- educommon/ws_log/migrations/0004_auto_20160727_1600.py +7 -6
- educommon/ws_log/migrations/0005_auto_20161130_1615.py +14 -4
- educommon/ws_log/migrations/0006_auto_20170327_1027.py +3 -2
- educommon/ws_log/migrations/0007_auto_20180607_1040.py +8 -9
- educommon/ws_log/migrations/0008_auto_20180713_1445.py +23 -10
- educommon/ws_log/migrations/0009_auto_20201130_1553.py +7 -2
- educommon/ws_log/models.py +21 -35
- educommon/ws_log/provider.py +2 -1
- educommon/ws_log/report.py +8 -13
- educommon/ws_log/smev/applications.py +12 -27
- educommon/ws_log/smev/exceptions.py +2 -3
- educommon/ws_log/ui.py +32 -32
- educommon/ws_log/utils.py +1 -3
- educommon-3.13.2.dist-info/METADATA +57 -0
- educommon-3.13.2.dist-info/RECORD +354 -0
- {educommon-3.12.0.dist-info → educommon-3.13.2.dist-info}/WHEEL +1 -1
- educommon/utils/patches.py +0 -27
- educommon/version.conf +0 -11
- educommon-3.12.0.dist-info/METADATA +0 -47
- educommon-3.12.0.dist-info/RECORD +0 -357
- educommon-3.12.0.dist-info/dependency_links.txt +0 -1
- {educommon-3.12.0.dist-info → educommon-3.13.2.dist-info}/top_level.txt +0 -0
educommon/audit_log/models.py
CHANGED
@@ -66,15 +66,10 @@ if TYPE_CHECKING:
|
|
66
66
|
|
67
67
|
|
68
68
|
class Table(ReadOnlyMixin, BaseModel):
|
69
|
+
"""Модель для хранения информации о таблицах, отслеживаемых системой аудита."""
|
69
70
|
|
70
|
-
name = models.CharField(
|
71
|
-
|
72
|
-
verbose_name='Имя таблицы'
|
73
|
-
)
|
74
|
-
schema = models.CharField(
|
75
|
-
max_length=250,
|
76
|
-
verbose_name='Схема таблицы'
|
77
|
-
)
|
71
|
+
name = models.CharField(max_length=250, verbose_name='Имя таблицы')
|
72
|
+
schema = models.CharField(max_length=250, verbose_name='Схема таблицы')
|
78
73
|
logged = models.BooleanField(
|
79
74
|
default=True,
|
80
75
|
verbose_name='Отслеживаемость таблицы',
|
@@ -87,6 +82,18 @@ class Table(ReadOnlyMixin, BaseModel):
|
|
87
82
|
|
88
83
|
|
89
84
|
class AuditLog(ReadOnlyMixin, BaseModel):
|
85
|
+
"""Модель для хранения записей журнала изменений.
|
86
|
+
|
87
|
+
Каждая запись описывает изменение конкретного объекта определённой модели,
|
88
|
+
с указанием пользователя, действия, IP-адреса и зафиксированных данных.
|
89
|
+
|
90
|
+
Поля:
|
91
|
+
- table: ссылка на таблицу, в которой произошло изменение;
|
92
|
+
- data: сериализованные значения объекта до изменения;
|
93
|
+
- changes: изменения (только изменённые поля);
|
94
|
+
- operation: тип действия (создание, изменение, удаление);
|
95
|
+
- user_id, user_type_id, ip, time: информация об авторе действия.
|
96
|
+
"""
|
90
97
|
|
91
98
|
OPERATION_CREATE = 1
|
92
99
|
OPERATION_UPDATE = 2
|
@@ -94,7 +101,7 @@ class AuditLog(ReadOnlyMixin, BaseModel):
|
|
94
101
|
OPERATION_CHOICES = (
|
95
102
|
(OPERATION_CREATE, 'Создание'),
|
96
103
|
(OPERATION_UPDATE, 'Изменение'),
|
97
|
-
(OPERATION_DELETE, 'Удаление')
|
104
|
+
(OPERATION_DELETE, 'Удаление'),
|
98
105
|
)
|
99
106
|
|
100
107
|
user_id = models.IntegerField(
|
@@ -107,36 +114,17 @@ class AuditLog(ReadOnlyMixin, BaseModel):
|
|
107
114
|
db_index=True,
|
108
115
|
verbose_name='Тип пользователя',
|
109
116
|
)
|
110
|
-
ip = models.GenericIPAddressField(
|
111
|
-
|
112
|
-
verbose_name='IP адрес'
|
113
|
-
)
|
114
|
-
time = models.DateTimeField(
|
115
|
-
auto_now_add=True,
|
116
|
-
db_index=True,
|
117
|
-
verbose_name='Дата, время'
|
118
|
-
)
|
117
|
+
ip = models.GenericIPAddressField(null=True, verbose_name='IP адрес')
|
118
|
+
time = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата, время')
|
119
119
|
table = models.ForeignKey(
|
120
120
|
Table,
|
121
121
|
verbose_name='Таблица',
|
122
122
|
on_delete=models.CASCADE,
|
123
123
|
)
|
124
|
-
object_id = models.IntegerField(
|
125
|
-
|
126
|
-
|
127
|
-
)
|
128
|
-
data = HStoreField(
|
129
|
-
null=True,
|
130
|
-
verbose_name='Объект'
|
131
|
-
)
|
132
|
-
changes = HStoreField(
|
133
|
-
null=True,
|
134
|
-
verbose_name='Изменения'
|
135
|
-
)
|
136
|
-
operation = models.SmallIntegerField(
|
137
|
-
choices=OPERATION_CHOICES,
|
138
|
-
verbose_name='Действие'
|
139
|
-
)
|
124
|
+
object_id = models.IntegerField(db_index=True, verbose_name='Объект модели')
|
125
|
+
data = HStoreField(null=True, verbose_name='Объект')
|
126
|
+
changes = HStoreField(null=True, verbose_name='Изменения')
|
127
|
+
operation = models.SmallIntegerField(choices=OPERATION_CHOICES, verbose_name='Действие')
|
140
128
|
|
141
129
|
@property
|
142
130
|
def transformed_data(self) -> Dict[str, Any]:
|
@@ -153,6 +141,7 @@ class AuditLog(ReadOnlyMixin, BaseModel):
|
|
153
141
|
return True
|
154
142
|
|
155
143
|
def get_read_only_error_message(self, delete):
|
144
|
+
"""Возвращает сообщение об ошибке при попытке изменить или удалить запись."""
|
156
145
|
action_text = 'удалить' if delete else 'изменить'
|
157
146
|
result = 'Нельзя {} запись лога.'.format(action_text)
|
158
147
|
return result
|
@@ -170,10 +159,7 @@ class AuditLog(ReadOnlyMixin, BaseModel):
|
|
170
159
|
"""
|
171
160
|
model = self.model
|
172
161
|
if model:
|
173
|
-
result = {
|
174
|
-
field.get_attname_column()[1]: field
|
175
|
-
for field in model._meta.fields
|
176
|
-
}
|
162
|
+
result = {field.get_attname_column()[1]: field for field in model._meta.fields}
|
177
163
|
return result
|
178
164
|
|
179
165
|
@property
|
@@ -248,10 +234,7 @@ class AuditLog(ReadOnlyMixin, BaseModel):
|
|
248
234
|
|
249
235
|
items = value[1:-1]
|
250
236
|
if items:
|
251
|
-
return [
|
252
|
-
base_field.to_python(item)
|
253
|
-
for item in items.split(',')
|
254
|
-
]
|
237
|
+
return [base_field.to_python(item) for item in items.split(',')]
|
255
238
|
|
256
239
|
class Meta:
|
257
240
|
verbose_name = 'Запись журнала изменений'
|
@@ -290,10 +273,13 @@ class PostgreSQLError(BaseModel):
|
|
290
273
|
|
291
274
|
class LoggableModelMixin(models.Model):
|
292
275
|
"""Делает модель логируемой."""
|
276
|
+
|
293
277
|
need_to_log = True
|
294
278
|
|
295
279
|
class Meta:
|
296
280
|
abstract = True
|
281
|
+
|
282
|
+
|
297
283
|
# -----------------------------------------------------------------------------
|
298
284
|
# Передача параметров контекста журналирования изменений в задания Celery.
|
299
285
|
|
@@ -341,13 +327,21 @@ def _set_audit_log_context_for_task(kwargs, **_):
|
|
341
327
|
|
342
328
|
@task_postrun.connect(dispatch_uid=_package_name + 'unset')
|
343
329
|
def _unset_audit_log_context_for_task(task, kwargs, **_):
|
330
|
+
"""Очищает параметры журнала изменений после выполнения Celery-задания."""
|
344
331
|
if hasattr(thread_data, 'audit_log_params'):
|
345
332
|
del thread_data.audit_log_params
|
346
333
|
|
347
334
|
|
348
335
|
@receiver(connection_created, dispatch_uid=_package_name + 'send')
|
349
336
|
def _send_audit_log_context_to_db(**kwargs):
|
337
|
+
"""Передаёт параметры контекста журнала изменений в БД при подключении.
|
338
|
+
|
339
|
+
Используется для установки параметров в PostgreSQL через set_config,
|
340
|
+
чтобы аудит знал, кто инициировал изменения.
|
341
|
+
"""
|
350
342
|
if hasattr(thread_data, 'audit_log_params'):
|
351
343
|
for name, value in thread_data.audit_log_params.items():
|
352
344
|
set_db_param('audit_log.' + name, value)
|
345
|
+
|
346
|
+
|
353
347
|
# -----------------------------------------------------------------------------
|
@@ -8,15 +8,17 @@ PERM__AUDIT_LOG__ERRORS__DELETE = PERM_GROUP__AUDIT_LOG_ERRORS + '/delete'
|
|
8
8
|
|
9
9
|
|
10
10
|
permissions = (
|
11
|
-
(PERM__AUDIT_LOG__VIEW,
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
(
|
18
|
-
|
19
|
-
|
11
|
+
(PERM__AUDIT_LOG__VIEW, 'Просмотр', 'Разрешает просмотр журнала изменений.'),
|
12
|
+
(
|
13
|
+
PERM__AUDIT_LOG__ERRORS__VIEW,
|
14
|
+
'Просмотр журнала ошибок PostgreSQL',
|
15
|
+
'Разрешает просмотр журнала ошибок PostgreSQL.',
|
16
|
+
),
|
17
|
+
(
|
18
|
+
PERM__AUDIT_LOG__ERRORS__DELETE,
|
19
|
+
'Удаление записей журнала ошибок PostgreSQL',
|
20
|
+
'Разрешает удаление записей из журнала ошибок PostgreSQL.',
|
21
|
+
),
|
20
22
|
)
|
21
23
|
|
22
24
|
|
educommon/audit_log/proxies.py
CHANGED
@@ -56,11 +56,10 @@ class LogProxy(AuditLog):
|
|
56
56
|
try:
|
57
57
|
result = model.objects.get(id=user_id).person.fullname
|
58
58
|
except model.DoesNotExist:
|
59
|
-
result = '{}({})'
|
60
|
-
model._meta.verbose_name, user_id
|
61
|
-
)
|
59
|
+
result = f'{model._meta.verbose_name}({user_id})'
|
62
60
|
else:
|
63
61
|
result = ''
|
62
|
+
|
64
63
|
return result
|
65
64
|
|
66
65
|
@property
|
@@ -113,7 +112,7 @@ class LogProxy(AuditLog):
|
|
113
112
|
{
|
114
113
|
'name': self.get_field_string(key),
|
115
114
|
'old': self.convert_field_value(key, data.get(key, '')),
|
116
|
-
'new': self.convert_field_value(key, new_data.get(key, ''))
|
115
|
+
'new': self.convert_field_value(key, new_data.get(key, '')),
|
117
116
|
}
|
118
117
|
for key in keys
|
119
118
|
]
|
@@ -132,10 +131,12 @@ class LogProxy(AuditLog):
|
|
132
131
|
field = self.fields.get(column_name)
|
133
132
|
if field and field.verbose_name:
|
134
133
|
name = force_str(field.verbose_name)
|
134
|
+
|
135
135
|
return name
|
136
136
|
|
137
137
|
def convert_field_value(self, column_name, value):
|
138
138
|
"""Возвращает значение поля."""
|
139
|
+
|
139
140
|
def get_choice(choices, choice_id):
|
140
141
|
if choice_id:
|
141
142
|
choice_id = int(choice_id)
|
@@ -153,18 +154,14 @@ class LogProxy(AuditLog):
|
|
153
154
|
related = get_related(field)
|
154
155
|
model = related.parent_model
|
155
156
|
field_name = related.relation.field_name
|
156
|
-
qs = model._default_manager.filter(
|
157
|
-
**{field_name: value}
|
158
|
-
)[:1]
|
157
|
+
qs = model._default_manager.filter(**{field_name: value})[:1]
|
159
158
|
if qs:
|
160
159
|
value = '{{{}}} {}'.format(
|
161
160
|
qs[0].id,
|
162
161
|
self._get_object_verbose_name(qs[0]),
|
163
162
|
)
|
164
163
|
elif isinstance(field, BooleanField):
|
165
|
-
value_map = {
|
166
|
-
't': 'Да', 'f': 'Нет'
|
167
|
-
}
|
164
|
+
value_map = {'t': 'Да', 'f': 'Нет'}
|
168
165
|
value = value_map.get(value, value)
|
169
166
|
elif isinstance(field, IntegerField) and field.choices:
|
170
167
|
value = get_choice(field.choices, value)
|
@@ -173,11 +170,8 @@ class LogProxy(AuditLog):
|
|
173
170
|
return force_str(value)
|
174
171
|
|
175
172
|
@property
|
176
|
-
def object_string(self):
|
177
|
-
"""Отображаемое имя экземпляра модели.
|
178
|
-
|
179
|
-
:rtype str
|
180
|
-
"""
|
173
|
+
def object_string(self) -> str:
|
174
|
+
"""Отображаемое имя экземпляра модели."""
|
181
175
|
instance = self.instance
|
182
176
|
if instance:
|
183
177
|
return self._get_object_verbose_name(instance)
|
@@ -190,10 +184,7 @@ class LogProxy(AuditLog):
|
|
190
184
|
|
191
185
|
if self.model:
|
192
186
|
result = self.model()
|
193
|
-
fields_dict = {
|
194
|
-
field.name: field for field in
|
195
|
-
self.model._meta.fields
|
196
|
-
}
|
187
|
+
fields_dict = {field.name: field for field in self.model._meta.fields}
|
197
188
|
for key, value in self.data.items():
|
198
189
|
field = fields_dict.get(key)
|
199
190
|
converted_value = value
|
@@ -210,15 +201,13 @@ class LogProxy(AuditLog):
|
|
210
201
|
elif isinstance(field, FloatField):
|
211
202
|
converted_value = float(value)
|
212
203
|
elif isinstance(field, FileField):
|
213
|
-
file_path = os.path.join(
|
214
|
-
settings.MEDIA_ROOT,
|
215
|
-
converted_value
|
216
|
-
)
|
204
|
+
file_path = os.path.join(settings.MEDIA_ROOT, converted_value)
|
217
205
|
if not os.path.exists(file_path):
|
218
206
|
converted_value = None
|
219
207
|
except (ValueError, TypeError):
|
220
208
|
pass
|
221
209
|
setattr(result, key, converted_value)
|
210
|
+
|
222
211
|
return result
|
223
212
|
|
224
213
|
@staticmethod
|
educommon/audit_log/ui.py
CHANGED
@@ -21,7 +21,12 @@ class ViewChangeWindow(BaseWindow):
|
|
21
21
|
"""Окно просмотра изменений."""
|
22
22
|
|
23
23
|
def _init_components(self):
|
24
|
-
|
24
|
+
"""Метод создаёт визуальные компоненты.
|
25
|
+
|
26
|
+
Метод отражает поля модели, но не определяет расположение компонентов в окне.
|
27
|
+
"""
|
28
|
+
super()._init_components()
|
29
|
+
|
25
30
|
self.grid = ExtObjectGrid(region='center')
|
26
31
|
self.grid.add_column(data_index='name', header='Поле')
|
27
32
|
self.grid.add_column(data_index='old', header='Старое значение')
|
@@ -30,35 +35,33 @@ class ViewChangeWindow(BaseWindow):
|
|
30
35
|
|
31
36
|
self.user_field = ExtStringField(label='Пользователь')
|
32
37
|
self.unit_field = ExtStringField(label='Учреждение')
|
33
|
-
self.top_region = ExtContainer(
|
34
|
-
region='north', layout='hbox', height=32
|
35
|
-
)
|
38
|
+
self.top_region = ExtContainer(region='north', layout='hbox', height=32)
|
36
39
|
|
37
40
|
def _do_layout(self):
|
38
|
-
|
41
|
+
"""Метод располагает уже созданные визуальные компоненты на окне."""
|
42
|
+
super()._do_layout()
|
39
43
|
|
40
44
|
self.layout = 'border'
|
41
45
|
self.width, self.height = 750, 400
|
42
46
|
|
43
47
|
self.grid.cls = 'word-wrap-grid'
|
44
48
|
|
45
|
-
self.top_region.items.extend(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
self.top_region.items.extend(
|
50
|
+
(
|
51
|
+
formed(self.user_field, flex=1, style=dict(padding='5px')),
|
52
|
+
formed(self.unit_field, flex=1, style=dict(padding='5px')),
|
53
|
+
)
|
54
|
+
)
|
49
55
|
self.items.extend((self.top_region, self.grid))
|
50
56
|
|
51
57
|
def set_params(self, params):
|
58
|
+
"""Метод принимает словарь, содержащий параметры окна, передаваемые в окно слоем экшнов."""
|
52
59
|
self.grid.action_data = params['grid_action']
|
53
60
|
log_record = params['object']
|
54
|
-
self.title = '{}: {}'.format(
|
55
|
-
log_record.get_operation_display(),
|
56
|
-
log_record.model_name
|
57
|
-
)
|
61
|
+
self.title = '{}: {}'.format(log_record.get_operation_display(), log_record.model_name)
|
58
62
|
if log_record.user:
|
59
63
|
self.user_field.value = '{} / {}'.format(
|
60
|
-
log_record.user.person.fullname,
|
61
|
-
log_record.user.person.user.username
|
64
|
+
log_record.user.person.fullname, log_record.user.person.user.username
|
62
65
|
)
|
63
66
|
unit = getattr(log_record.user, 'unit', None)
|
64
67
|
if unit:
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# TODO - EDUSCHL-23454
|
1
2
|
import os
|
2
3
|
from contextlib import (
|
3
4
|
closing,
|
@@ -72,11 +73,7 @@ def configure(force_update_triggers: bool = False):
|
|
72
73
|
params['need_to_update_triggers'] = force_update_triggers or changed_table
|
73
74
|
params['lock_id'] = PG_LOCK_ID
|
74
75
|
|
75
|
-
execute_sql_file(
|
76
|
-
settings.DEFAULT_DB_ALIAS,
|
77
|
-
os.path.join(SQL_FILES_DIR, 'configure_audit_log.sql'),
|
78
|
-
params
|
79
|
-
)
|
76
|
+
execute_sql_file(settings.DEFAULT_DB_ALIAS, os.path.join(SQL_FILES_DIR, 'configure_audit_log.sql'), params)
|
80
77
|
|
81
78
|
|
82
79
|
def get_all_table_names(db_alias: str, schema: str) -> Set[str]:
|
@@ -97,11 +94,7 @@ def get_all_table_names(db_alias: str, schema: str) -> Set[str]:
|
|
97
94
|
|
98
95
|
def get_need_to_log_table_names() -> Set[str]:
|
99
96
|
"""Возвращает перечень наименований таблиц моделей, которые отмечены как отслеживаемые."""
|
100
|
-
table_names = {
|
101
|
-
model._meta.db_table
|
102
|
-
for model in apps.get_models()
|
103
|
-
if getattr(model, 'need_to_log', False)
|
104
|
-
}
|
97
|
+
table_names = {model._meta.db_table for model in apps.get_models() if getattr(model, 'need_to_log', False)}
|
105
98
|
|
106
99
|
return table_names
|
107
100
|
|
@@ -114,9 +107,7 @@ def update_or_create_tables(need_to_log_table_names: Iterable[str]) -> bool:
|
|
114
107
|
Table = apps.get_model('audit_log', 'Table')
|
115
108
|
|
116
109
|
need_to_log_table_names = set(need_to_log_table_names)
|
117
|
-
existed_table_names = set(
|
118
|
-
Table.objects.filter(schema='public').values_list('name', flat=True)
|
119
|
-
)
|
110
|
+
existed_table_names = set(Table.objects.filter(schema='public').values_list('name', flat=True))
|
120
111
|
|
121
112
|
to_create_table_names = need_to_log_table_names - existed_table_names
|
122
113
|
to_disable_table_names = existed_table_names - need_to_log_table_names
|
@@ -141,10 +132,7 @@ def update_or_create_tables(need_to_log_table_names: Iterable[str]) -> bool:
|
|
141
132
|
|
142
133
|
# Создание записей таблиц, которые теперь отслеживаемые
|
143
134
|
Table.objects.bulk_create(
|
144
|
-
objs=[
|
145
|
-
Table(name=table, schema='public')
|
146
|
-
for table in to_create_table_names
|
147
|
-
],
|
135
|
+
objs=[Table(name=table, schema='public') for table in to_create_table_names],
|
148
136
|
)
|
149
137
|
|
150
138
|
if to_create_table_names or enabled_count or disabled_count:
|
@@ -229,10 +217,8 @@ def is_initialized(database_alias):
|
|
229
217
|
# Проверка наличия таблицы postgresql_errors
|
230
218
|
with closing(connections[database_alias].cursor()) as cursor:
|
231
219
|
cursor.execute(
|
232
|
-
'select 1 '
|
233
|
-
'
|
234
|
-
'where table_schema = %s and table_name = %s',
|
235
|
-
('audit', 'postgresql_errors')
|
220
|
+
'select 1 from information_schema.tables where table_schema = %s and table_name = %s',
|
221
|
+
('audit', 'postgresql_errors'),
|
236
222
|
)
|
237
223
|
if cursor.fetchone() is None:
|
238
224
|
return False
|
@@ -251,12 +237,12 @@ def is_initialized(database_alias):
|
|
251
237
|
for function_name in function_names:
|
252
238
|
with closing(connections[database_alias].cursor()) as cursor:
|
253
239
|
cursor.execute(
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
[function_name, 'audit']
|
240
|
+
'select 1 '
|
241
|
+
'from pg_proc proc '
|
242
|
+
'inner join pg_namespace ns on ns.oid = proc.pronamespace '
|
243
|
+
'where proc.proname = %s and ns.nspname = %s '
|
244
|
+
'limit 1',
|
245
|
+
[function_name, 'audit'],
|
260
246
|
)
|
261
247
|
if cursor.fetchone() is None:
|
262
248
|
return False
|
@@ -304,7 +290,7 @@ def get_db_connection_params():
|
|
304
290
|
dbname=target_db_conf['NAME'],
|
305
291
|
port=target_db_conf['PORT'],
|
306
292
|
user=target_db_conf['USER'],
|
307
|
-
password=target_db_conf['PASSWORD']
|
293
|
+
password=target_db_conf['PASSWORD'],
|
308
294
|
)
|
309
295
|
|
310
296
|
|
@@ -355,10 +341,7 @@ def make_hstore_filter(field, value):
|
|
355
341
|
:param str value: значение, по которому фильтруется queryset.
|
356
342
|
Если строка, то разбивается на отдельные слова.
|
357
343
|
"""
|
358
|
-
result = reduce(
|
359
|
-
and_,
|
360
|
-
(Q(**{'%s__values__icontains' % field: x}) for x in value.split(' '))
|
361
|
-
)
|
344
|
+
result = reduce(and_, (Q(**{f'{field}__values__icontains': x}) for x in value.split(' ')))
|
362
345
|
return result
|
363
346
|
|
364
347
|
|
@@ -375,11 +358,7 @@ def make_name_filter(field, value):
|
|
375
358
|
result = None
|
376
359
|
for model in (Employee, SysAdmin):
|
377
360
|
type_id = ContentType.objects.get_for_model(model).id
|
378
|
-
user_ids = list(
|
379
|
-
model.objects.filter(
|
380
|
-
**{'person__{}__icontains'.format(field): value}
|
381
|
-
).values_list('id', flat=True)
|
382
|
-
)
|
361
|
+
user_ids = list(model.objects.filter(**{f'person__{field}__icontains': value}).values_list('id', flat=True))
|
383
362
|
qobj = Q(user_id__in=user_ids, user_type_id=type_id)
|
384
363
|
if result:
|
385
364
|
result |= qobj
|
@@ -389,6 +368,13 @@ def make_name_filter(field, value):
|
|
389
368
|
|
390
369
|
|
391
370
|
class ModelRegistry:
|
371
|
+
"""Реестр моделей Django по имени таблицы.
|
372
|
+
|
373
|
+
Позволяет получать класс модели по имени таблицы из базы данных.
|
374
|
+
Использует кэшируемое свойство для построения соответствия
|
375
|
+
между именами таблиц и их моделями, включая автоматически
|
376
|
+
создаваемые модели, но исключая proxy-модели.
|
377
|
+
"""
|
392
378
|
|
393
379
|
@cached_property
|
394
380
|
def table_model(self):
|
@@ -415,16 +401,9 @@ def get_model_choices(excluded=None):
|
|
415
401
|
table_class = apps.get_model('audit_log', 'Table')
|
416
402
|
if excluded:
|
417
403
|
total_exclude += tuple(excluded)
|
418
|
-
tables = (
|
419
|
-
table
|
420
|
-
for table in table_class.objects.iterator()
|
421
|
-
if (table.schema, table.name) not in total_exclude
|
422
|
-
)
|
404
|
+
tables = (table for table in table_class.objects.iterator() if (table.schema, table.name) not in total_exclude)
|
423
405
|
|
424
|
-
result = sorted(
|
425
|
-
((table.id, get_table_name(table)) for table in tables),
|
426
|
-
key=lambda x: x[1]
|
427
|
-
)
|
406
|
+
result = sorted(((table.id, get_table_name(table)) for table in tables), key=lambda x: x[1])
|
428
407
|
|
429
408
|
return tuple(result)
|
430
409
|
|
@@ -435,12 +414,7 @@ def _get_m2m_model_fields(model):
|
|
435
414
|
:return Два поля типа ForeignKey или None, если таблица не
|
436
415
|
соответствует автоматически созданной.
|
437
416
|
"""
|
438
|
-
result = [
|
439
|
-
field
|
440
|
-
for field
|
441
|
-
in model._meta.get_fields()
|
442
|
-
if isinstance(field, related.ForeignKey)
|
443
|
-
]
|
417
|
+
result = [field for field in model._meta.get_fields() if isinstance(field, related.ForeignKey)]
|
444
418
|
if len(result) == 2:
|
445
419
|
return result
|
446
420
|
|
@@ -454,16 +428,10 @@ def get_table_name(table):
|
|
454
428
|
if model._meta.auto_created:
|
455
429
|
fields = _get_m2m_model_fields(model)
|
456
430
|
if fields:
|
457
|
-
names = [
|
458
|
-
get_related(f).parent_model._meta.verbose_name
|
459
|
-
for f in fields
|
460
|
-
]
|
431
|
+
names = [get_related(f).parent_model._meta.verbose_name for f in fields]
|
461
432
|
verbose_name = 'Связь {}, {}'.format(names[0], names[1])
|
462
433
|
|
463
|
-
return '{} - {}'
|
464
|
-
verbose_name,
|
465
|
-
class_name
|
466
|
-
)
|
434
|
+
return f'{verbose_name} - {class_name}'
|
467
435
|
else:
|
468
436
|
return table.name
|
469
437
|
|
@@ -32,6 +32,7 @@ class ReinstallAuditLog(Operation):
|
|
32
32
|
|
33
33
|
@staticmethod
|
34
34
|
def _read_sql(filename):
|
35
|
+
"""Читает SQL-файл и экранирует знаки процента."""
|
35
36
|
sql_file_path = os.path.join(SQL_FILES_DIR, filename)
|
36
37
|
with codecs.open(sql_file_path, 'r', 'utf-8') as sql_file:
|
37
38
|
sql = sql_file.read().replace('%', '%%')
|
@@ -39,17 +40,30 @@ class ReinstallAuditLog(Operation):
|
|
39
40
|
|
40
41
|
@property
|
41
42
|
def _install_sql(self):
|
43
|
+
"""Генерирует SQL-скрипт для установки схемы audit_log.
|
44
|
+
|
45
|
+
Подставляет параметры подключения к БД и lock_id
|
46
|
+
в шаблон SQL-файла install_audit_log.sql.
|
47
|
+
"""
|
42
48
|
params = get_db_connection_params()
|
43
49
|
params['lock_id'] = PG_LOCK_ID
|
44
50
|
return self._read_sql('install_audit_log.sql').format(**params)
|
45
51
|
|
46
52
|
def state_forwards(self, app_label, state):
|
53
|
+
"""Не изменяет состояние проекта в памяти.
|
54
|
+
|
55
|
+
Метод требуется по контракту абстрактного базового класса Operation.
|
56
|
+
"""
|
47
57
|
pass
|
48
58
|
|
49
|
-
def database_forwards(self, app_label, schema_editor, from_state,
|
50
|
-
|
59
|
+
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
60
|
+
"""Применяет SQL-скрипт установки audit_log, если используется основная БД."""
|
51
61
|
if schema_editor.connection.alias == settings.DEFAULT_DB_ALIAS:
|
52
62
|
schema_editor.execute(self._install_sql)
|
53
63
|
|
54
64
|
def database_backwards(self, app_label, schema_editor, from_state, to_state):
|
65
|
+
"""Откат миграции не реализован (no-op).
|
66
|
+
|
67
|
+
Метод присутствует, чтобы соответствовать контракту Django.
|
68
|
+
"""
|
55
69
|
return None
|
educommon/auth/__init__.py
CHANGED
educommon/auth/rbac/__init__.py
CHANGED
@@ -3,12 +3,10 @@
|
|
3
3
|
.. note::
|
4
4
|
RBAC - Role Based Access Control.
|
5
5
|
"""
|
6
|
+
|
6
7
|
from django import (
|
7
8
|
VERSION as _django_version,
|
8
9
|
)
|
9
10
|
|
10
11
|
|
11
|
-
assert _django_version >= (1, 8), (
|
12
|
-
"{} app doesn't support Django {}.{}."
|
13
|
-
.format(__name__, *_django_version[:2])
|
14
|
-
)
|
12
|
+
assert _django_version >= (1, 8), "{} app doesn't support Django {}.{}.".format(__name__, *_django_version[:2])
|