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.
Files changed (221) hide show
  1. educommon/__init__.py +0 -1
  2. educommon/about/ui/actions.py +16 -30
  3. educommon/about/ui/ui.py +3 -12
  4. educommon/about/utils.py +6 -5
  5. educommon/async_task/__init__.py +0 -1
  6. educommon/async_task/actions.py +18 -13
  7. educommon/async_task/apps.py +4 -0
  8. educommon/async_task/locker.py +2 -5
  9. educommon/async_task/migrations/0001_initial.py +55 -9
  10. educommon/async_task/migrations/0002_task_type_and_status_data.py +94 -89
  11. educommon/async_task/migrations/0003_alter_runningtask_options.py +0 -1
  12. educommon/async_task/models.py +9 -6
  13. educommon/async_task/tasks.py +11 -7
  14. educommon/async_task/ui.py +16 -35
  15. educommon/async_tasks/__init__.py +0 -1
  16. educommon/async_tasks/apps.py +4 -0
  17. educommon/async_tasks/locks.py +11 -21
  18. educommon/async_tasks/migrations/0001_initial.py +68 -8
  19. educommon/async_tasks/migrations/0002_load_initial_data.py +0 -1
  20. educommon/async_tasks/models.py +9 -29
  21. educommon/async_tasks/tasks.py +25 -54
  22. educommon/audit_log/__init__.py +1 -0
  23. educommon/audit_log/actions.py +27 -36
  24. educommon/audit_log/app_meta.py +7 -4
  25. educommon/audit_log/apps.py +44 -29
  26. educommon/audit_log/constants.py +7 -4
  27. educommon/audit_log/error_log/actions.py +1 -3
  28. educommon/audit_log/helpers.py +2 -4
  29. educommon/audit_log/management/commands/reinstall_audit_log.py +11 -7
  30. educommon/audit_log/migrations/0001_initial.py +91 -16
  31. educommon/audit_log/migrations/0002_install_audit_log.py +13 -13
  32. educommon/audit_log/migrations/0003_logproxy.py +1 -3
  33. educommon/audit_log/migrations/0004_reinstall_audit_log.py +1 -4
  34. educommon/audit_log/migrations/0005_postgresql_error.py +4 -2
  35. educommon/audit_log/migrations/0006_auto_20200806_1707.py +3 -4
  36. educommon/audit_log/migrations/0007_create_selective_tables_function.py +8 -5
  37. educommon/audit_log/migrations/0008_table_logged.py +0 -1
  38. educommon/audit_log/migrations/0009_reinstall_audit_log.py +0 -1
  39. educommon/audit_log/models.py +36 -42
  40. educommon/audit_log/permissions.py +11 -9
  41. educommon/audit_log/proxies.py +12 -23
  42. educommon/audit_log/ui.py +18 -15
  43. educommon/audit_log/utils/__init__.py +28 -60
  44. educommon/audit_log/utils/operations.py +16 -2
  45. educommon/auth/__init__.py +0 -3
  46. educommon/auth/rbac/__init__.py +2 -4
  47. educommon/auth/rbac/actions.py +148 -145
  48. educommon/auth/rbac/app_meta.py +9 -6
  49. educommon/auth/rbac/backends/base.py +2 -8
  50. educommon/auth/rbac/backends/caching.py +27 -37
  51. educommon/auth/rbac/backends/simple.py +1 -4
  52. educommon/auth/rbac/checker.py +1 -3
  53. educommon/auth/rbac/management/commands/rbac.py +6 -11
  54. educommon/auth/rbac/manager.py +18 -47
  55. educommon/auth/rbac/migrations/0001_initial.py +73 -12
  56. educommon/auth/rbac/migrations/0002_model_modifier_metaclass_fix.py +7 -6
  57. educommon/auth/rbac/migrations/0003_permission_hidden.py +1 -5
  58. educommon/auth/rbac/migrations/0004_auto_20171024_1245.py +26 -19
  59. educommon/auth/rbac/models.py +63 -68
  60. educommon/auth/rbac/permissions.py +6 -7
  61. educommon/auth/rbac/ui.py +83 -84
  62. educommon/auth/rbac/utils.py +10 -11
  63. educommon/auth/rbac/validators.py +4 -5
  64. educommon/auth/simple_auth/__init__.py +1 -5
  65. educommon/auth/simple_auth/actions.py +79 -92
  66. educommon/auth/simple_auth/app_meta.py +2 -9
  67. educommon/auth/simple_auth/checkers.py +3 -3
  68. educommon/auth/simple_auth/migrations/0001_initial.py +23 -4
  69. educommon/auth/simple_auth/validators.py +0 -1
  70. educommon/contingent/actions.py +7 -7
  71. educommon/contingent/app_meta.py +1 -4
  72. educommon/contingent/base.py +10 -15
  73. educommon/contingent/catalogs.py +424 -540
  74. educommon/contingent/contingent_plugin/actions.py +4 -15
  75. educommon/contingent/contingent_plugin/apps.py +10 -4
  76. educommon/contingent/contingent_plugin/migrations/0001_initial.py +5 -6
  77. educommon/contingent/contingent_plugin/migrations/0002_add_contingent_model_deleted.py +6 -11
  78. educommon/contingent/contingent_plugin/model_views.py +2 -12
  79. educommon/contingent/contingent_plugin/models.py +2 -7
  80. educommon/contingent/contingent_plugin/observer.py +14 -13
  81. educommon/contingent/contingent_plugin/plugin_meta.py +1 -3
  82. educommon/contingent/contingent_plugin/storage.py +8 -7
  83. educommon/contingent/contingent_plugin/utils.py +6 -6
  84. educommon/django/db/fields.py +72 -86
  85. educommon/django/db/migration/__init__.py +3 -7
  86. educommon/django/db/migration/operations.py +29 -51
  87. educommon/django/db/mixins/__init__.py +16 -10
  88. educommon/django/db/mixins/date_interval.py +47 -75
  89. educommon/django/db/mixins/validation.py +26 -26
  90. educommon/django/db/model_view/__init__.py +18 -22
  91. educommon/django/db/models.py +9 -8
  92. educommon/django/db/observer.py +9 -27
  93. educommon/django/db/partitioning/__init__.py +66 -92
  94. educommon/django/db/partitioning/management/commands/apply_partitioning.py +3 -13
  95. educommon/django/db/partitioning/management/commands/clear_table.py +18 -14
  96. educommon/django/db/partitioning/management/commands/split_table.py +18 -13
  97. educommon/django/db/routers.py +6 -15
  98. educommon/django/db/signals.py +149 -2
  99. educommon/django/db/utils.py +14 -19
  100. educommon/django/db/validators/__init__.py +1 -0
  101. educommon/django/db/validators/simple.py +72 -100
  102. educommon/django/storages/atcfs/api.py +39 -53
  103. educommon/django/storages/atcfs/app_meta.py +1 -1
  104. educommon/django/storages/atcfs/management/commands/atcfs_migrate.py +42 -55
  105. educommon/django/storages/atcfs/models.py +0 -3
  106. educommon/django/storages/atcfs/monkey_patching.py +18 -12
  107. educommon/django/storages/atcfs/storage.py +14 -23
  108. educommon/extjs/fields/input_params.py +15 -45
  109. educommon/importer/XLSReader.py +143 -241
  110. educommon/importer/__init__.py +86 -4
  111. educommon/importer/api.py +53 -84
  112. educommon/importer/constants.py +4 -14
  113. educommon/importer/loggers.py +16 -26
  114. educommon/importer/proxy.py +131 -176
  115. educommon/importer/proxy_import.py +11 -12
  116. educommon/importer/report.py +4 -6
  117. educommon/importer/ui.py +32 -26
  118. educommon/importer/validators.py +4 -7
  119. educommon/integration_entities/helpers.py +14 -18
  120. educommon/ioc/__init__.py +3 -6
  121. educommon/logger/loggers.py +10 -14
  122. educommon/m3/__init__.py +20 -38
  123. educommon/m3/extensions/__init__.py +1 -0
  124. educommon/m3/extensions/listeners/__init__.py +22 -38
  125. educommon/m3/extensions/listeners/delete_check/listeners.py +31 -41
  126. educommon/m3/extensions/listeners/delete_check/mixins.py +20 -25
  127. educommon/m3/extensions/listeners/delete_check/signals.py +2 -2
  128. educommon/m3/extensions/listeners/delete_check/ui.py +15 -14
  129. educommon/m3/extensions/listeners/delete_check/utils.py +9 -11
  130. educommon/m3/extensions/ui.py +15 -33
  131. educommon/m3/transaction_context.py +17 -19
  132. educommon/objectpack/actions.py +70 -88
  133. educommon/objectpack/apps.py +5 -0
  134. educommon/objectpack/filters.py +9 -15
  135. educommon/objectpack/ui.py +59 -77
  136. educommon/report/__init__.py +9 -5
  137. educommon/report/actions.py +29 -32
  138. educommon/report/constructor/__init__.py +5 -8
  139. educommon/report/constructor/app_meta.py +1 -3
  140. educommon/report/constructor/apps.py +1 -0
  141. educommon/report/constructor/base.py +33 -80
  142. educommon/report/constructor/builders/excel/_base.py +138 -286
  143. educommon/report/constructor/builders/excel/_header.py +2 -9
  144. educommon/report/constructor/builders/excel/product.py +13 -34
  145. educommon/report/constructor/builders/excel/with_merged_cells.py +18 -14
  146. educommon/report/constructor/config.py +2 -0
  147. educommon/report/constructor/editor/actions.py +101 -215
  148. educommon/report/constructor/editor/ui.py +71 -93
  149. educommon/report/constructor/exceptions.py +6 -12
  150. educommon/report/constructor/migrations/0001_initial.py +36 -44
  151. educommon/report/constructor/migrations/0002_report_filters.py +86 -72
  152. educommon/report/constructor/migrations/0003_reportfilter_exclude.py +5 -5
  153. educommon/report/constructor/migrations/0004_reportfilter_fields.py +22 -18
  154. educommon/report/constructor/migrations/0005_reportcolumn_visible.py +5 -4
  155. educommon/report/constructor/migrations/0006_reportsorting.py +21 -17
  156. educommon/report/constructor/migrations/0007_include_available_units.py +14 -14
  157. educommon/report/constructor/migrations/0008_auto_20170407_1318.py +4 -5
  158. educommon/report/constructor/migrations/0009_auto_20180405_0642.py +1 -4
  159. educommon/report/constructor/migrations/0010_add_aggregate_fields.py +7 -8
  160. educommon/report/constructor/mixins.py +14 -15
  161. educommon/report/constructor/models.py +76 -124
  162. educommon/report/constructor/utils.py +3 -8
  163. educommon/report/constructor/validators.py +1 -3
  164. educommon/report/reporter.py +25 -43
  165. educommon/report/utils.py +14 -40
  166. educommon/rest/actions.py +7 -11
  167. educommon/rest/context.py +6 -16
  168. educommon/rest/controllers.py +10 -10
  169. educommon/rest/mixins.py +29 -27
  170. educommon/secure_media/app_meta.py +9 -9
  171. educommon/utils/__init__.py +3 -2
  172. educommon/utils/caching.py +1 -3
  173. educommon/utils/conversion.py +1 -3
  174. educommon/utils/crypto.py +1 -2
  175. educommon/utils/date.py +13 -26
  176. educommon/utils/db/__init__.py +17 -26
  177. educommon/utils/db/postgresql.py +1 -4
  178. educommon/utils/fonts/__init__.py +3 -4
  179. educommon/utils/licence/__init__.py +5 -16
  180. educommon/utils/misc.py +9 -18
  181. educommon/utils/object_grid.py +55 -62
  182. educommon/utils/phone_number/modelfields.py +1 -3
  183. educommon/utils/phone_number/phone_number.py +5 -8
  184. educommon/utils/phone_number/validators.py +8 -23
  185. educommon/utils/plugins.py +15 -28
  186. educommon/utils/registry.py +2 -1
  187. educommon/utils/seqtools.py +1 -3
  188. educommon/utils/serializer.py +9 -16
  189. educommon/utils/storage.py +3 -2
  190. educommon/utils/system.py +1 -3
  191. educommon/utils/system_app/management/commands/delete_objects.py +17 -34
  192. educommon/utils/ui.py +87 -84
  193. educommon/utils/xml/__init__.py +2 -7
  194. educommon/utils/xml/resolver.py +1 -0
  195. educommon/ws_log/actions.py +31 -76
  196. educommon/ws_log/base.py +6 -20
  197. educommon/ws_log/migrations/0001_initial.py +25 -8
  198. educommon/ws_log/migrations/0002_auto_20160628_1334.py +0 -1
  199. educommon/ws_log/migrations/0003_add_fields_to_smev_logs.py +20 -4
  200. educommon/ws_log/migrations/0004_auto_20160727_1600.py +7 -6
  201. educommon/ws_log/migrations/0005_auto_20161130_1615.py +14 -4
  202. educommon/ws_log/migrations/0006_auto_20170327_1027.py +3 -2
  203. educommon/ws_log/migrations/0007_auto_20180607_1040.py +8 -9
  204. educommon/ws_log/migrations/0008_auto_20180713_1445.py +23 -10
  205. educommon/ws_log/migrations/0009_auto_20201130_1553.py +7 -2
  206. educommon/ws_log/models.py +21 -35
  207. educommon/ws_log/provider.py +2 -1
  208. educommon/ws_log/report.py +8 -13
  209. educommon/ws_log/smev/applications.py +12 -27
  210. educommon/ws_log/smev/exceptions.py +2 -3
  211. educommon/ws_log/ui.py +32 -32
  212. educommon/ws_log/utils.py +1 -3
  213. educommon-3.13.2.dist-info/METADATA +57 -0
  214. educommon-3.13.2.dist-info/RECORD +354 -0
  215. {educommon-3.12.0.dist-info → educommon-3.13.2.dist-info}/WHEEL +1 -1
  216. educommon/utils/patches.py +0 -27
  217. educommon/version.conf +0 -11
  218. educommon-3.12.0.dist-info/METADATA +0 -47
  219. educommon-3.12.0.dist-info/RECORD +0 -357
  220. educommon-3.12.0.dist-info/dependency_links.txt +0 -1
  221. {educommon-3.12.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
- 1. Указанный алиас БД есть в конфигурации системы.
90
- 2. Указанная БД управляется СУБД PostgreSQL.
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
- "select 1 "
131
- "from pg_proc proc "
132
- "inner join pg_namespace ns on ns.oid = proc.pronamespace "
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
- "limit 1",
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
- _MESSAGE_PREFIX + 'always initialized'
178
- )
179
-
180
- _execute_sql_file(database_alias, 'partitioning.sql', dict(
181
- view_name_suffix=PartitioningObserver.view_name_suffix,
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
- "select partitioning.is_table_partitioned(%s)",
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, cursor_itersize: Optional[int] = None):
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
- 'update {table_name} '
313
- 'set {pk_column_name} = {pk_column_name} '
314
- 'where {pk_column_name} in {pk_column_values}'
315
- ).format(**locals()))
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, connection.features.empty_fetchmany_value, 1, cursor_itersize,
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
- 'where {pk_column_name} in {pk_column_values}'
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
- "select inhrelid::regclass::text as partition_name "
395
- "from pg_inherits "
376
+ 'select inhrelid::regclass::text as partition_name '
377
+ 'from pg_inherits '
396
378
  "where inhparent = '{}'::regclass::oid "
397
- "order by partition_name"
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 + '_y{}m{}'.format(
444
- date.year, date.strftime('%m')
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(database_alias, 'partitioning_set_search_path.sql', dict(
464
- schema_names=schema_names,
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
- return {
496
- database_alias: is_initialized(database_alias)
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
- '--is_foreign_table',
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
- help = (
30
- 'Command deletes all the records from database table when '
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', action='store', dest='timeout',
57
- default=.0, type=float,
58
- help=('Timeout (in seconds) between the data removes iterations. '
59
- 'It used to reduce the database load.')
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', action='store', dest='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
- help = (
33
- 'Command moves all the records from database table to partitions of '
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', action='store', dest='timeout',
55
- default=.0, type=float,
56
- help=('Timeout (in seconds) between the data transfer iterations. '
57
- 'It used to reduce the database load.')
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', action='store', dest='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']
@@ -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
  )
@@ -1,7 +1,154 @@
1
+ from typing import (
2
+ Callable,
3
+ Optional,
4
+ )
5
+
6
+ from django.db.models import (
7
+ Model,
8
+ )
1
9
  from django.dispatch.dispatcher import (
2
10
  Signal,
3
11
  )
12
+ from educommon import (
13
+ logger,
14
+ )
15
+
16
+
17
+ class BaseBeforeMigrateHandler:
18
+ """
19
+ Базовый обработчик сигнала before_handle_migrate_signal.
20
+
21
+ Должен быть унаследован и инстанс наследника регистрироваться
22
+ как обработчик сигнала.
23
+ """
24
+
25
+ def _is_accessing_non_migrating_databases(self, migrating_database: str) -> bool:
26
+ for model_cls in self._get_working_models():
27
+ db = self._get_model_db(model_cls)
28
+ if db != migrating_database:
29
+ return True
30
+
31
+ return False
32
+
33
+ def _get_model_db(self, model_cls: type[Model]) -> str:
34
+ """
35
+ Возвращает псевдоним базы данных, к которой пойдет запрос
36
+ по данной модели.
37
+ """
38
+
39
+ return model_cls.objects.all().db
40
+
41
+ def _get_migrating_database(self, sender) -> str:
42
+ """Возвращает псевдоним базы данных, с которой работает миграция."""
43
+
44
+ return sender.database
45
+
46
+ def _get_working_models(self) -> set[type[Model]]:
47
+ """Модели, с которым будет работать обработчик сигнала."""
48
+
49
+ raise NotImplementedError()
50
+
51
+ def handler(self, sender, *args, **kwargs):
52
+ """Непосредственный обработчик сигнала."""
53
+
54
+ raise NotImplementedError()
55
+
56
+ def __call__(self, sender, *args, **kwargs):
57
+ migrating_database = self._get_migrating_database(sender)
58
+ if self._is_accessing_non_migrating_databases(migrating_database):
59
+ logger.debug('Предотвращена попытка доступа к базе данных, по которой не производится миграция')
60
+ return
61
+
62
+ return self.handler(sender, *args, **kwargs)
63
+
64
+
65
+ class BaseAfterMigrateHandler:
66
+ """
67
+ Базовый обработчик сигнала after_handle_migrate_signal.
68
+
69
+ Должен быть унаследован и инстанс наследника регистрироваться
70
+ как обработчик сигнала.
71
+ """
72
+
73
+ def _is_accessing_non_migrating_databases(self, migrating_database: str) -> bool:
74
+ for model_cls in self._get_working_models():
75
+ db = self._get_model_db(model_cls)
76
+ if db != migrating_database:
77
+ return True
78
+
79
+ return False
80
+
81
+ def _get_model_db(self, model_cls: type[Model]) -> str:
82
+ """
83
+ Возвращает псевдоним базы данных, к которой пойдет запрос
84
+ по данной модели.
85
+ """
86
+
87
+ return model_cls.objects.all().db
88
+
89
+ def _get_migrating_database(self, sender) -> str:
90
+ """Возвращает псевдоним базы данных, с которой работает миграция."""
91
+
92
+ return sender.database
93
+
94
+ def _get_working_models(self) -> set[type[Model]]:
95
+ """Модели, с которым будет работать обработчик сигнала."""
96
+
97
+ raise NotImplementedError()
98
+
99
+ def handler(self, sender, *args, **kwargs):
100
+ """Непосредственный обработчик сигнала."""
101
+
102
+ raise NotImplementedError()
103
+
104
+ def __call__(self, sender, *args, **kwargs):
105
+ migrating_database = self._get_migrating_database(sender)
106
+ if self._is_accessing_non_migrating_databases(migrating_database):
107
+ logger.debug('Предотвращена попытка доступа к базе данных, по которой не производится миграция')
108
+ return
109
+
110
+ return self.handler(sender, *args, **kwargs)
111
+
112
+
113
+ class BeforeHandleMigrateSignal(Signal):
114
+ _handler_base_class = BaseBeforeMigrateHandler
115
+
116
+ def connect(
117
+ self,
118
+ receiver: Callable,
119
+ sender: Optional = None,
120
+ weak: bool = True,
121
+ dispatch_uid: Optional[str] = None,
122
+ ) -> None:
123
+ """Регистрирует обработчик, если он является допустимым типом."""
124
+ if not isinstance(receiver, self._handler_base_class):
125
+ logger.warning(
126
+ f'Обработчик сигнала before_handle_migrate_signal {receiver} не зарегистрирован, поскольку '
127
+ f'он не является подклассом {self._handler_base_class.__name__}'
128
+ )
129
+
130
+ return super().connect(receiver, sender, weak, dispatch_uid)
131
+
132
+
133
+ class AfterHandleMigrateSignal(Signal):
134
+ _handler_base_class = BaseBeforeMigrateHandler
135
+
136
+ def connect(
137
+ self,
138
+ receiver: Callable,
139
+ sender: Optional = None,
140
+ weak: bool = True,
141
+ dispatch_uid: Optional[str] = None,
142
+ ) -> None:
143
+ """Регистрирует обработчик, если он является допустимым типом."""
144
+ if not isinstance(receiver, self._handler_base_class):
145
+ logger.warning(
146
+ f'Обработчик сигнала after_handle_migrate_signal {receiver} не зарегистрирован, поскольку '
147
+ f'он не является подклассом {self._handler_base_class.__name__}'
148
+ )
149
+
150
+ return super().connect(receiver, sender, weak, dispatch_uid)
4
151
 
5
152
 
6
- before_handle_migrate_signal = Signal()
7
- after_handle_migrate_signal = Signal()
153
+ before_handle_migrate_signal = BeforeHandleMigrateSignal()
154
+ after_handle_migrate_signal = AfterHandleMigrateSignal()