educommon 3.13.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 +4 -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.0.dist-info → educommon-3.13.2.dist-info}/METADATA +26 -14
- educommon-3.13.2.dist-info/RECORD +354 -0
- educommon/utils/patches.py +0 -27
- educommon/version.conf +0 -11
- educommon-3.13.0.dist-info/RECORD +0 -357
- educommon-3.13.0.dist-info/dependency_links.txt +0 -1
- {educommon-3.13.0.dist-info → educommon-3.13.2.dist-info}/WHEEL +0 -0
- {educommon-3.13.0.dist-info → educommon-3.13.2.dist-info}/top_level.txt +0 -0
@@ -28,6 +28,7 @@
|
|
28
28
|
Подробнее о партиционировании можно почитать в дкоументации PostgreSQL
|
29
29
|
(раздел 5.9).
|
30
30
|
"""
|
31
|
+
|
31
32
|
import re
|
32
33
|
from contextlib import (
|
33
34
|
closing,
|
@@ -86,24 +87,18 @@ _MESSAGE_PREFIX = '[Partitioning] '
|
|
86
87
|
def _check_system_settings(database_alias):
|
87
88
|
"""Проверка конфигурации системы. Перечень проверок:
|
88
89
|
|
89
|
-
|
90
|
-
|
90
|
+
1. Указанный алиас БД есть в конфигурации системы.
|
91
|
+
2. Указанная БД управляется СУБД PostgreSQL.
|
91
92
|
"""
|
92
93
|
if database_alias not in settings.DATABASES:
|
93
|
-
raise ImproperlyConfigured(
|
94
|
-
_MESSAGE_PREFIX +
|
95
|
-
'"{0}" database not found.'.format(database_alias)
|
96
|
-
)
|
94
|
+
raise ImproperlyConfigured(f'{_MESSAGE_PREFIX}"{database_alias}" database not found.')
|
97
95
|
|
98
96
|
database_engine = settings.DATABASES[database_alias]['ENGINE']
|
99
97
|
if database_engine not in (
|
100
98
|
'django.db.backends.postgresql_psycopg2',
|
101
99
|
'django.db.backends.postgresql',
|
102
100
|
):
|
103
|
-
raise ImproperlyConfigured(
|
104
|
-
_MESSAGE_PREFIX +
|
105
|
-
'only PostgreSQL DBMS supported.'
|
106
|
-
)
|
101
|
+
raise ImproperlyConfigured(f'{_MESSAGE_PREFIX}only PostgreSQL DBMS supported.')
|
107
102
|
|
108
103
|
|
109
104
|
def is_initialized(database_alias):
|
@@ -116,9 +111,7 @@ def is_initialized(database_alias):
|
|
116
111
|
"""
|
117
112
|
# Проверка наличия схемы partitioning.
|
118
113
|
with closing(connections[database_alias].cursor()) as cursor:
|
119
|
-
cursor.execute(
|
120
|
-
"select 1 from pg_namespace where nspname = 'partitioning'"
|
121
|
-
)
|
114
|
+
cursor.execute("select 1 from pg_namespace where nspname = 'partitioning'")
|
122
115
|
if cursor.fetchone() is None:
|
123
116
|
return False
|
124
117
|
|
@@ -127,12 +120,12 @@ def is_initialized(database_alias):
|
|
127
120
|
for function_name in function_names:
|
128
121
|
with closing(connections[database_alias].cursor()) as cursor:
|
129
122
|
cursor.execute(
|
130
|
-
|
131
|
-
|
132
|
-
|
123
|
+
'select 1 '
|
124
|
+
'from pg_proc proc '
|
125
|
+
'inner join pg_namespace ns on ns.oid = proc.pronamespace '
|
133
126
|
"where proc.proname = %s and ns.nspname = 'partitioning' "
|
134
|
-
|
135
|
-
[function_name]
|
127
|
+
'limit 1',
|
128
|
+
[function_name],
|
136
129
|
)
|
137
130
|
if cursor.fetchone() is None:
|
138
131
|
return False
|
@@ -141,6 +134,7 @@ def is_initialized(database_alias):
|
|
141
134
|
|
142
135
|
|
143
136
|
def _execute_sql_file(database_alias, file_name, params=None):
|
137
|
+
"""Выполняет SQL-скрипт из файла с подстановкой параметров."""
|
144
138
|
cursor = connections[database_alias].cursor()
|
145
139
|
|
146
140
|
file_path = path.join(path.dirname(__file__), file_name)
|
@@ -173,16 +167,19 @@ def init(database_alias=DEFAULT_DB_ALIAS, force=False):
|
|
173
167
|
_check_system_settings(database_alias)
|
174
168
|
|
175
169
|
if not force and is_initialized(database_alias):
|
176
|
-
raise ImproperlyConfigured(
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
170
|
+
raise ImproperlyConfigured(f'{_MESSAGE_PREFIX}always initialized')
|
171
|
+
|
172
|
+
_execute_sql_file(
|
173
|
+
database_alias,
|
174
|
+
'partitioning.sql',
|
175
|
+
dict(
|
176
|
+
view_name_suffix=PartitioningObserver.view_name_suffix,
|
177
|
+
),
|
178
|
+
)
|
183
179
|
|
184
180
|
|
185
181
|
def _get_model_params(model):
|
182
|
+
"""Возвращает параметры модели: алиас БД, имя таблицы и имя PK-колонки."""
|
186
183
|
database_alias = router.db_for_write(model)
|
187
184
|
table_name = model._meta.db_table
|
188
185
|
pk_column_name = model._meta.pk.name
|
@@ -198,16 +195,12 @@ def is_model_partitioned(model):
|
|
198
195
|
database_alias, table_name, _ = _get_model_params(model)
|
199
196
|
|
200
197
|
with closing(connections[database_alias].cursor()) as cursor:
|
201
|
-
cursor.execute(
|
202
|
-
"select 1 from pg_namespace where nspname = 'partitioning' limit 1"
|
203
|
-
)
|
198
|
+
cursor.execute("select 1 from pg_namespace where nspname = 'partitioning' limit 1")
|
204
199
|
if cursor.fetchone() is None:
|
205
200
|
return False
|
206
201
|
|
207
|
-
cursor.execute(
|
208
|
-
|
209
|
-
(table_name,)
|
210
|
-
)
|
202
|
+
cursor.execute('select partitioning.is_table_partitioned(%s)', (table_name,))
|
203
|
+
|
211
204
|
return cursor.fetchone()[0]
|
212
205
|
|
213
206
|
|
@@ -236,14 +229,12 @@ def set_partitioning_for_model(model, column_name, force=False):
|
|
236
229
|
ModelOptions(model).get_field(column_name)
|
237
230
|
|
238
231
|
if not force and not is_initialized(database_alias):
|
239
|
-
raise ImproperlyConfigured(
|
240
|
-
_MESSAGE_PREFIX + 'not initialized'
|
241
|
-
)
|
232
|
+
raise ImproperlyConfigured(f'{_MESSAGE_PREFIX}not initialized')
|
242
233
|
|
243
234
|
_execute_sql_file(database_alias, 'triggers.sql', locals())
|
244
235
|
|
245
236
|
|
246
|
-
def split_table(model, column_name: str, timeout: float = 0,
|
237
|
+
def split_table(model, column_name: str, timeout: float = 0, cursor_itersize: Optional[int] = None):
|
247
238
|
"""Переносит записи из разбиваемой таблицы в ее разделы.
|
248
239
|
|
249
240
|
Недостающие разделы будут созданы автоматически.
|
@@ -271,20 +262,13 @@ def split_table(model, column_name: str, timeout: float = 0, cursor_itersize: O
|
|
271
262
|
ModelOptions(model).get_field(column_name)
|
272
263
|
|
273
264
|
if not is_initialized(database_alias):
|
274
|
-
raise ImproperlyConfigured(
|
275
|
-
_MESSAGE_PREFIX + 'not initialized'
|
276
|
-
)
|
265
|
+
raise ImproperlyConfigured(f'{_MESSAGE_PREFIX}not initialized')
|
277
266
|
|
278
267
|
if not is_model_partitioned(model):
|
279
|
-
raise ImproperlyConfigured(
|
280
|
-
_MESSAGE_PREFIX + 'not applyed for {table_name}'.format(**locals())
|
281
|
-
)
|
268
|
+
raise ImproperlyConfigured(f'{_MESSAGE_PREFIX}not applyed for {table_name}')
|
282
269
|
|
283
270
|
if settings.DATABASES[database_alias]['DISABLE_SERVER_SIDE_CURSORS']:
|
284
|
-
raise ImproperlyConfigured(
|
285
|
-
_MESSAGE_PREFIX + 'split_table does not '
|
286
|
-
'support DISABLE_SERVER_SIDE_CURSORS.'
|
287
|
-
)
|
271
|
+
raise ImproperlyConfigured(f'{_MESSAGE_PREFIX}split_table does not support DISABLE_SERVER_SIDE_CURSORS.')
|
288
272
|
|
289
273
|
connection = connections[database_alias]
|
290
274
|
|
@@ -297,9 +281,7 @@ def split_table(model, column_name: str, timeout: float = 0, cursor_itersize: O
|
|
297
281
|
|
298
282
|
move_cursor = connection.cursor()
|
299
283
|
|
300
|
-
results = cursor_iter(
|
301
|
-
ids_cursor, connection.features.empty_fetchmany_value, 1, cursor_itersize
|
302
|
-
)
|
284
|
+
results = cursor_iter(ids_cursor, connection.features.empty_fetchmany_value, 1, cursor_itersize)
|
303
285
|
for rows in results:
|
304
286
|
# Если всего одна запись, то используем строку, чтобы не получать ошибку sql-запроса на обновление
|
305
287
|
if len(rows) == 1:
|
@@ -308,11 +290,13 @@ def split_table(model, column_name: str, timeout: float = 0, cursor_itersize: O
|
|
308
290
|
pk_column_values = tuple(pkv for (pkv,) in rows)
|
309
291
|
# Этот update выполняется для того, чтобы сработала триггерная
|
310
292
|
# функция partitioning.before_update.
|
311
|
-
move_cursor.execute(
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
293
|
+
move_cursor.execute(
|
294
|
+
(
|
295
|
+
'update {table_name} '
|
296
|
+
'set {pk_column_name} = {pk_column_name} '
|
297
|
+
'where {pk_column_name} in {pk_column_values}'
|
298
|
+
).format(**locals())
|
299
|
+
)
|
316
300
|
|
317
301
|
if timeout:
|
318
302
|
sleep(timeout)
|
@@ -344,23 +328,22 @@ def clear_table(model, column_name: str, column_value: str, timeout=0, cursor_it
|
|
344
328
|
connection = connections[database_alias]
|
345
329
|
|
346
330
|
if settings.DATABASES[database_alias]['DISABLE_SERVER_SIDE_CURSORS']:
|
347
|
-
raise ImproperlyConfigured(
|
348
|
-
_MESSAGE_PREFIX + 'clear_table does not '
|
349
|
-
'support DISABLE_SERVER_SIDE_CURSORS.'
|
350
|
-
)
|
331
|
+
raise ImproperlyConfigured(f'{_MESSAGE_PREFIX}clear_table does not support DISABLE_SERVER_SIDE_CURSORS.')
|
351
332
|
|
352
333
|
ids_cursor = connection.chunked_cursor()
|
353
334
|
ids_cursor.execute(
|
354
335
|
# сырой SQL используется для того, чтобы извлечь только записи
|
355
336
|
# из родительской таблицы без записей, уже размещенных в разделах
|
356
|
-
"select {pk_column_name} from only {table_name} "
|
357
|
-
"where {column_name} < '{column_value}'".format(**locals())
|
337
|
+
"select {pk_column_name} from only {table_name} where {column_name} < '{column_value}'".format(**locals())
|
358
338
|
)
|
359
339
|
|
360
340
|
delete_cursor = connection.cursor()
|
361
341
|
|
362
342
|
results = cursor_iter(
|
363
|
-
ids_cursor,
|
343
|
+
ids_cursor,
|
344
|
+
connection.features.empty_fetchmany_value,
|
345
|
+
1,
|
346
|
+
cursor_itersize,
|
364
347
|
)
|
365
348
|
for rows in results:
|
366
349
|
# Если всего одна запись, то используем строку, чтобы не получать ошибку sql-запроса на удаление
|
@@ -368,10 +351,9 @@ def clear_table(model, column_name: str, column_value: str, timeout=0, cursor_it
|
|
368
351
|
pk_column_values = f'{rows[0]}'.replace(',', '')
|
369
352
|
else:
|
370
353
|
pk_column_values = tuple(pkv for (pkv,) in rows)
|
371
|
-
delete_cursor.execute(
|
372
|
-
'delete from {table_name} '
|
373
|
-
|
374
|
-
).format(**locals()))
|
354
|
+
delete_cursor.execute(
|
355
|
+
('delete from {table_name} where {pk_column_name} in {pk_column_values}').format(**locals())
|
356
|
+
)
|
375
357
|
|
376
358
|
if timeout:
|
377
359
|
sleep(timeout)
|
@@ -391,17 +373,14 @@ def get_model_partitions(model):
|
|
391
373
|
cursor = connection.cursor()
|
392
374
|
|
393
375
|
cursor.execute(
|
394
|
-
|
395
|
-
|
376
|
+
'select inhrelid::regclass::text as partition_name '
|
377
|
+
'from pg_inherits '
|
396
378
|
"where inhparent = '{}'::regclass::oid "
|
397
|
-
|
398
|
-
.format(table_name)
|
399
|
-
)
|
400
|
-
return tuple(
|
401
|
-
partition_name
|
402
|
-
for (partition_name,) in cursor
|
379
|
+
'order by partition_name'.format(table_name)
|
403
380
|
)
|
404
381
|
|
382
|
+
return tuple(partition_name for (partition_name,) in cursor)
|
383
|
+
|
405
384
|
|
406
385
|
def reset_partition_constraints(model, column_name, partition_name):
|
407
386
|
"""Переустанавливает ограничения для указанного раздела.
|
@@ -422,8 +401,7 @@ def reset_partition_constraints(model, column_name, partition_name):
|
|
422
401
|
cursor = connection.cursor()
|
423
402
|
|
424
403
|
cursor.execute(
|
425
|
-
'select partitioning.set_partition_constraint(%s, %s, %s, %s)',
|
426
|
-
(partition_name, column_name, year, month)
|
404
|
+
'select partitioning.set_partition_constraint(%s, %s, %s, %s)', (partition_name, column_name, year, month)
|
427
405
|
)
|
428
406
|
|
429
407
|
commit_unless_managed(database_alias)
|
@@ -440,29 +418,27 @@ def drop_partitions_before_date(model, date):
|
|
440
418
|
if is_model_partitioned(model):
|
441
419
|
database_alias, table_name, _ = _get_model_params(model)
|
442
420
|
all_partitions = get_model_partitions(model)
|
443
|
-
filter_partition_name = table_name
|
444
|
-
|
445
|
-
)
|
446
|
-
filtered_partitions = filter(
|
447
|
-
lambda p: (p <= filter_partition_name), all_partitions
|
448
|
-
)
|
421
|
+
filter_partition_name = f'{table_name}_y{date.year}m{date.strftime("%m")}'
|
422
|
+
filtered_partitions = filter(lambda p: (p <= filter_partition_name), all_partitions)
|
449
423
|
connection = connections[database_alias]
|
450
424
|
with connection.cursor() as cursor:
|
451
425
|
for partition in filtered_partitions:
|
452
|
-
cursor.execute(
|
453
|
-
'DROP TABLE IF EXISTS {};'.format(partition)
|
454
|
-
)
|
426
|
+
cursor.execute('DROP TABLE IF EXISTS {};'.format(partition))
|
455
427
|
|
456
428
|
|
457
429
|
def set_partitioned_function_search_path(database_alias: str, schema_names: Optional[str] = None):
|
458
|
-
""""Проставляет параметры поиска для существующих функций партицирования.
|
430
|
+
""" "Проставляет параметры поиска для существующих функций партицирования.
|
459
431
|
|
460
432
|
Это необходимо для корректной работы с таблицами к которым обращаются как к внешним через postgres_fdw.
|
461
433
|
"""
|
462
434
|
schema_names = schema_names or 'public'
|
463
|
-
_execute_sql_file(
|
464
|
-
|
465
|
-
|
435
|
+
_execute_sql_file(
|
436
|
+
database_alias,
|
437
|
+
'partitioning_set_search_path.sql',
|
438
|
+
dict(
|
439
|
+
schema_names=schema_names,
|
440
|
+
),
|
441
|
+
)
|
466
442
|
|
467
443
|
|
468
444
|
class PartitioningObserver(ModelObserverBase):
|
@@ -492,10 +468,8 @@ class PartitioningObserver(ModelObserverBase):
|
|
492
468
|
|
493
469
|
@cached_property
|
494
470
|
def _partitioning_ready(self):
|
495
|
-
|
496
|
-
|
497
|
-
for database_alias in connections
|
498
|
-
}
|
471
|
+
"""Кэширует информацию о том, проинициализировано ли партиционирование в БД."""
|
472
|
+
return {database_alias: is_initialized(database_alias) for database_alias in connections}
|
499
473
|
|
500
474
|
def _is_observable(self, model):
|
501
475
|
"""Возвращает True только для моделей с включенным партиционированием.
|
@@ -28,8 +28,8 @@ class Command(BaseCommand):
|
|
28
28
|
для БД, в которой хранится переданная модель, а затем создает необходимые
|
29
29
|
триггеры. Подробнее см. в `educommon.django.db.partitioning.init` и
|
30
30
|
`educommon.django.db.partitioning.set_partitioning_for_model`.
|
31
|
-
|
32
31
|
"""
|
32
|
+
|
33
33
|
help = 'Applies partitioning to the table.' # noqa: A003
|
34
34
|
|
35
35
|
def add_arguments(self, parser):
|
@@ -49,18 +49,8 @@ class Command(BaseCommand):
|
|
49
49
|
type=str,
|
50
50
|
help='Field name. It will be the partition key.',
|
51
51
|
)
|
52
|
-
parser.add_argument(
|
53
|
-
|
54
|
-
type=bool,
|
55
|
-
default=False,
|
56
|
-
help='Партицирование для внешних таблиц'
|
57
|
-
)
|
58
|
-
parser.add_argument(
|
59
|
-
'--schemas_names',
|
60
|
-
type=str,
|
61
|
-
default=None,
|
62
|
-
help='Cхемы внешних таблиц при партицировании.'
|
63
|
-
)
|
52
|
+
parser.add_argument('--is_foreign_table', type=bool, default=False, help='Партицирование для внешних таблиц')
|
53
|
+
parser.add_argument('--schemas_names', type=str, default=None, help='Cхемы внешних таблиц при партицировании.')
|
64
54
|
|
65
55
|
def handle(self, *args, **options):
|
66
56
|
"""Выполнение команды."""
|
@@ -1,6 +1,3 @@
|
|
1
|
-
from django.core.exceptions import (
|
2
|
-
FieldDoesNotExist,
|
3
|
-
)
|
4
1
|
from django.core.management.base import (
|
5
2
|
CommandError,
|
6
3
|
)
|
@@ -24,14 +21,12 @@ class Command(BaseCommand):
|
|
24
21
|
С помощью данной команды удаляются записи из основной (не секционированной)
|
25
22
|
таблицы, у которых значение в field_name меньше значения из before_value.
|
26
23
|
Подробнее см. в `educommon.django.db.partitioning.clear_table`.
|
27
|
-
|
28
24
|
"""
|
29
|
-
|
30
|
-
|
31
|
-
'field_name < before_value.'
|
32
|
-
)
|
25
|
+
|
26
|
+
help = 'Command deletes all the records from database table when field_name < before_value.'
|
33
27
|
|
34
28
|
def add_arguments(self, parser):
|
29
|
+
"""Добавляет аргументы командной строки для команды очистки таблицы."""
|
35
30
|
parser.add_argument(
|
36
31
|
'--app_label',
|
37
32
|
type=str,
|
@@ -53,19 +48,28 @@ class Command(BaseCommand):
|
|
53
48
|
help='Deleting rows before this value.',
|
54
49
|
)
|
55
50
|
parser.add_argument(
|
56
|
-
'--timeout',
|
57
|
-
|
58
|
-
|
59
|
-
|
51
|
+
'--timeout',
|
52
|
+
action='store',
|
53
|
+
dest='timeout',
|
54
|
+
default=0.0,
|
55
|
+
type=float,
|
56
|
+
help=('Timeout (in seconds) between the data removes iterations. It used to reduce the database load.'),
|
60
57
|
)
|
61
58
|
parser.add_argument(
|
62
|
-
'--cursor_itersize',
|
59
|
+
'--cursor_itersize',
|
60
|
+
action='store',
|
61
|
+
dest='cursor_itersize',
|
63
62
|
type=int,
|
64
63
|
default=None,
|
65
|
-
help='Количество строк загруженных за раз при загрузке строк при работе команды.'
|
64
|
+
help='Количество строк загруженных за раз при загрузке строк при работе команды.',
|
66
65
|
)
|
67
66
|
|
68
67
|
def handle(self, *args, **options):
|
68
|
+
"""Основная логика команды.
|
69
|
+
|
70
|
+
Выполняет проверку модели и поля, затем вызывает функцию очистки
|
71
|
+
записей по условию field_name < before_value.
|
72
|
+
"""
|
69
73
|
app_label = options['app_label']
|
70
74
|
model_name = options['model_name']
|
71
75
|
field_name = options['field_name']
|
@@ -1,6 +1,3 @@
|
|
1
|
-
from django.core.exceptions import (
|
2
|
-
FieldDoesNotExist,
|
3
|
-
)
|
4
1
|
from django.core.management.base import (
|
5
2
|
CommandError,
|
6
3
|
)
|
@@ -29,12 +26,11 @@ class Command(BaseCommand):
|
|
29
26
|
Подробнее см. в `educommon.django.db.partitioning.split_table`.
|
30
27
|
|
31
28
|
"""
|
32
|
-
|
33
|
-
|
34
|
-
'this table.'
|
35
|
-
)
|
29
|
+
|
30
|
+
help = 'Command moves all the records from database table to partitions of this table.'
|
36
31
|
|
37
32
|
def add_arguments(self, parser):
|
33
|
+
"""Добавляет аргументы командной строки для команды переноса данных в разделы."""
|
38
34
|
parser.add_argument(
|
39
35
|
'--app_label',
|
40
36
|
type=str,
|
@@ -51,19 +47,28 @@ class Command(BaseCommand):
|
|
51
47
|
help='Field name. It will be the partition key.',
|
52
48
|
)
|
53
49
|
parser.add_argument(
|
54
|
-
'--timeout',
|
55
|
-
|
56
|
-
|
57
|
-
|
50
|
+
'--timeout',
|
51
|
+
action='store',
|
52
|
+
dest='timeout',
|
53
|
+
default=0.0,
|
54
|
+
type=float,
|
55
|
+
help=('Timeout (in seconds) between the data transfer iterations. It used to reduce the database load.'),
|
58
56
|
)
|
59
57
|
parser.add_argument(
|
60
|
-
'--cursor_itersize',
|
58
|
+
'--cursor_itersize',
|
59
|
+
action='store',
|
60
|
+
dest='cursor_itersize',
|
61
61
|
type=int,
|
62
62
|
default=None,
|
63
|
-
help='Количество строк загруженных за раз при загрузке строк при работе команды.'
|
63
|
+
help='Количество строк загруженных за раз при загрузке строк при работе команды.',
|
64
64
|
)
|
65
65
|
|
66
66
|
def handle(self, *args, **options):
|
67
|
+
"""Основная логика команды.
|
68
|
+
|
69
|
+
Проверяет наличие модели и заданного поля, затем запускает
|
70
|
+
процесс переноса данных в секции таблицы.
|
71
|
+
"""
|
67
72
|
app_label = options['app_label']
|
68
73
|
model_name = options['model_name']
|
69
74
|
field_name = options['field_name']
|
educommon/django/db/routers.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
"""Роутеры для приложений Django."""
|
2
|
+
|
2
3
|
from abc import (
|
3
4
|
ABCMeta,
|
4
5
|
)
|
@@ -43,19 +44,14 @@ class ServiceDbRouterBase(DatabaseRouterBase, metaclass=ABCMeta):
|
|
43
44
|
aliases = list(settings.DATABASES)
|
44
45
|
if len(aliases) != 2:
|
45
46
|
# Роутер поддерживает только конфигурации с двумя БД.
|
46
|
-
raise ImproperlyConfigured(
|
47
|
-
'Database router support only two databases'
|
48
|
-
)
|
47
|
+
raise ImproperlyConfigured('Database router support only two databases')
|
49
48
|
|
50
49
|
self.default_db_alias = DEFAULT_DB_ALIAS
|
51
50
|
|
52
51
|
alias_index = 1 if aliases[0] == DEFAULT_DB_ALIAS else 0
|
53
52
|
self.service_db_alias = aliases[alias_index]
|
54
53
|
|
55
|
-
self.service_db_model_names = {
|
56
|
-
model_name.lower()
|
57
|
-
for model_name in self.service_db_model_names
|
58
|
-
}
|
54
|
+
self.service_db_model_names = {model_name.lower() for model_name in self.service_db_model_names}
|
59
55
|
|
60
56
|
def _db_for_model(self, model, **hints):
|
61
57
|
"""Возвращает имя БД для чтения/записи данных из модели *model*."""
|
@@ -71,6 +67,7 @@ class ServiceDbRouterBase(DatabaseRouterBase, metaclass=ABCMeta):
|
|
71
67
|
db_for_write = _db_for_model
|
72
68
|
|
73
69
|
def _allow(self, db, app_label, model_name):
|
70
|
+
"""Определяет, разрешён ли доступ к модели в указанной БД."""
|
74
71
|
assert db in (self.default_db_alias, self.service_db_alias)
|
75
72
|
|
76
73
|
if app_label == self.app_name:
|
@@ -78,12 +75,6 @@ class ServiceDbRouterBase(DatabaseRouterBase, metaclass=ABCMeta):
|
|
78
75
|
return True
|
79
76
|
else:
|
80
77
|
model_name = model_name.lower()
|
81
|
-
return (
|
82
|
-
|
83
|
-
db == self.default_db_alias and
|
84
|
-
model_name not in self.service_db_model_names
|
85
|
-
) or (
|
86
|
-
db == self.service_db_alias and
|
87
|
-
model_name in self.service_db_model_names
|
88
|
-
)
|
78
|
+
return (db == self.default_db_alias and model_name not in self.service_db_model_names) or (
|
79
|
+
db == self.service_db_alias and model_name in self.service_db_model_names
|
89
80
|
)
|
educommon/django/db/signals.py
CHANGED
@@ -56,7 +56,7 @@ class BaseBeforeMigrateHandler:
|
|
56
56
|
def __call__(self, sender, *args, **kwargs):
|
57
57
|
migrating_database = self._get_migrating_database(sender)
|
58
58
|
if self._is_accessing_non_migrating_databases(migrating_database):
|
59
|
-
logger.
|
59
|
+
logger.debug('Предотвращена попытка доступа к базе данных, по которой не производится миграция')
|
60
60
|
return
|
61
61
|
|
62
62
|
return self.handler(sender, *args, **kwargs)
|
@@ -104,7 +104,7 @@ class BaseAfterMigrateHandler:
|
|
104
104
|
def __call__(self, sender, *args, **kwargs):
|
105
105
|
migrating_database = self._get_migrating_database(sender)
|
106
106
|
if self._is_accessing_non_migrating_databases(migrating_database):
|
107
|
-
logger.
|
107
|
+
logger.debug('Предотвращена попытка доступа к базе данных, по которой не производится миграция')
|
108
108
|
return
|
109
109
|
|
110
110
|
return self.handler(sender, *args, **kwargs)
|
@@ -120,6 +120,7 @@ class BeforeHandleMigrateSignal(Signal):
|
|
120
120
|
weak: bool = True,
|
121
121
|
dispatch_uid: Optional[str] = None,
|
122
122
|
) -> None:
|
123
|
+
"""Регистрирует обработчик, если он является допустимым типом."""
|
123
124
|
if not isinstance(receiver, self._handler_base_class):
|
124
125
|
logger.warning(
|
125
126
|
f'Обработчик сигнала before_handle_migrate_signal {receiver} не зарегистрирован, поскольку '
|
@@ -139,6 +140,7 @@ class AfterHandleMigrateSignal(Signal):
|
|
139
140
|
weak: bool = True,
|
140
141
|
dispatch_uid: Optional[str] = None,
|
141
142
|
) -> None:
|
143
|
+
"""Регистрирует обработчик, если он является допустимым типом."""
|
142
144
|
if not isinstance(receiver, self._handler_base_class):
|
143
145
|
logger.warning(
|
144
146
|
f'Обработчик сигнала after_handle_migrate_signal {receiver} не зарегистрирован, поскольку '
|
educommon/django/db/utils.py
CHANGED
@@ -68,11 +68,10 @@ def model_modifier_metaclass(meta_base=ModelBase, **params):
|
|
68
68
|
class Meta:
|
69
69
|
verbose_name = 'Образец справочника'
|
70
70
|
"""
|
71
|
+
|
71
72
|
class ModifiedModelBase(meta_base):
|
72
73
|
def __new__(cls, name, bases, attrs):
|
73
|
-
model = super(ModifiedModelBase, cls).__new__(
|
74
|
-
cls, name, bases, attrs
|
75
|
-
)
|
74
|
+
model = super(ModifiedModelBase, cls).__new__(cls, name, bases, attrs)
|
76
75
|
|
77
76
|
# Переопределения имен атрибутов (см. Field.deconstruct).
|
78
77
|
attr_overrides = {
|
@@ -167,39 +166,31 @@ class LazyModel:
|
|
167
166
|
def model(self):
|
168
167
|
return self._model.get_model()
|
169
168
|
|
169
|
+
|
170
170
|
mp1 = ModelProcessor('person.Person')
|
171
171
|
mp2 = ModelProcessor(('person', 'Person'))
|
172
172
|
mp3 = ModelProcessor(Person)
|
173
173
|
"""
|
174
174
|
|
175
175
|
def __init__(self, model):
|
176
|
-
if (
|
177
|
-
isinstance(model, str) and
|
178
|
-
'.' in model and model.index('.') == model.rindex('.')
|
179
|
-
):
|
176
|
+
if isinstance(model, str) and '.' in model and model.index('.') == model.rindex('.'):
|
180
177
|
self.app_label, self.model_name = model.split('.')
|
181
178
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
182
|
-
elif (
|
183
|
-
isinstance(model, tuple) and
|
184
|
-
len(model) == 2 and
|
185
|
-
all(isinstance(s, str) for s in model)
|
186
|
-
):
|
179
|
+
elif isinstance(model, tuple) and len(model) == 2 and all(isinstance(s, str) for s in model):
|
187
180
|
self.app_label, self.model_name = model
|
188
181
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
189
182
|
elif (
|
190
|
-
isclass(model)
|
191
|
-
hasattr(model, '_meta')
|
192
|
-
hasattr(model._meta, 'app_label')
|
193
|
-
hasattr(model._meta, 'model_name')
|
183
|
+
isclass(model)
|
184
|
+
and hasattr(model, '_meta')
|
185
|
+
and hasattr(model._meta, 'app_label')
|
186
|
+
and hasattr(model._meta, 'model_name')
|
194
187
|
):
|
195
188
|
self._model = model
|
196
189
|
self.app_label = model._meta.app_label
|
197
190
|
self.model_name = model.__name__
|
198
191
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
199
192
|
else:
|
200
|
-
raise ValueError(
|
201
|
-
'"model" argument has invalid value: ' + repr(model)
|
202
|
-
)
|
193
|
+
raise ValueError(f'"model" argument has invalid value: {repr(model)}')
|
203
194
|
|
204
195
|
def get_model(self):
|
205
196
|
"""Возвращает класс модели, заданной при инициализации."""
|
@@ -214,6 +205,7 @@ class SmartExact(Func):
|
|
214
205
|
|
215
206
|
template = "TRANSLATE(%(expressions)s, 'ёЁ ', 'еЕ')"
|
216
207
|
|
208
|
+
|
217
209
|
class SmartExactLookup(Lookup):
|
218
210
|
"""Удаляет пробелы из строки и заменяет буквы ё на е."""
|
219
211
|
|
@@ -228,11 +220,13 @@ class SmartExactLookup(Lookup):
|
|
228
220
|
|
229
221
|
return sql % (lhs, rhs), lhs_params + rhs_params
|
230
222
|
|
223
|
+
|
231
224
|
class SmartIExact(Func):
|
232
225
|
"""Переводит в верхний регистр, удаляет пробелы, заменяет Ё на Е."""
|
233
226
|
|
234
227
|
template = "TRANSLATE(UPPER(%(expressions)s), 'Ё ', 'Е')"
|
235
228
|
|
229
|
+
|
236
230
|
class SmartIExactLookup(Lookup):
|
237
231
|
"""Переводит в верхний регистр, удаляет пробелы, заменяет Ё на Е."""
|
238
232
|
|
@@ -247,6 +241,7 @@ class SmartIExactLookup(Lookup):
|
|
247
241
|
|
248
242
|
return sql % (lhs, rhs), lhs_params + rhs_params
|
249
243
|
|
244
|
+
|
250
245
|
class SmartIContainsLookup(Lookup):
|
251
246
|
"""Регистронезависимый поиск.
|
252
247
|
|