acv-django-core 0.9.0__tar.gz

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 (84) hide show
  1. acv_django_core-0.9.0/PKG-INFO +436 -0
  2. acv_django_core-0.9.0/README.md +403 -0
  3. acv_django_core-0.9.0/pyproject.toml +81 -0
  4. acv_django_core-0.9.0/setup.cfg +4 -0
  5. acv_django_core-0.9.0/src/acv_core/__init__.py +3 -0
  6. acv_django_core-0.9.0/src/acv_core/_opcionales.py +51 -0
  7. acv_django_core-0.9.0/src/acv_core/actividad.py +124 -0
  8. acv_django_core-0.9.0/src/acv_core/admin.py +13 -0
  9. acv_django_core-0.9.0/src/acv_core/archivos/__init__.py +14 -0
  10. acv_django_core-0.9.0/src/acv_core/archivos/comando.py +265 -0
  11. acv_django_core-0.9.0/src/acv_core/archivos/entrega.py +86 -0
  12. acv_django_core-0.9.0/src/acv_core/archivos/exportables.py +47 -0
  13. acv_django_core-0.9.0/src/acv_core/archivos/exportacion_csv.py +87 -0
  14. acv_django_core-0.9.0/src/acv_core/archivos/exportacion_xlsx.py +130 -0
  15. acv_django_core-0.9.0/src/acv_core/archivos/importacion.py +574 -0
  16. acv_django_core-0.9.0/src/acv_core/archivos/mixins.py +220 -0
  17. acv_django_core-0.9.0/src/acv_core/archivos/modelos.py +832 -0
  18. acv_django_core-0.9.0/src/acv_core/archivos/paginas.py +730 -0
  19. acv_django_core-0.9.0/src/acv_core/archivos/plantilla.py +343 -0
  20. acv_django_core-0.9.0/src/acv_core/archivos/progreso.py +60 -0
  21. acv_django_core-0.9.0/src/acv_core/archivos/serializers.py +166 -0
  22. acv_django_core-0.9.0/src/acv_core/archivos/trabajos.py +270 -0
  23. acv_django_core-0.9.0/src/acv_core/archivos/viewsets.py +677 -0
  24. acv_django_core-0.9.0/src/acv_core/autenticacion/__init__.py +13 -0
  25. acv_django_core-0.9.0/src/acv_core/autenticacion/modelos.py +100 -0
  26. acv_django_core-0.9.0/src/acv_core/autenticacion/permisos.py +72 -0
  27. acv_django_core-0.9.0/src/acv_core/autenticacion/serializers.py +150 -0
  28. acv_django_core-0.9.0/src/acv_core/autenticacion/sesiones.py +70 -0
  29. acv_django_core-0.9.0/src/acv_core/autenticacion/tokens.py +55 -0
  30. acv_django_core-0.9.0/src/acv_core/autenticacion/views.py +342 -0
  31. acv_django_core-0.9.0/src/acv_core/campos.py +257 -0
  32. acv_django_core-0.9.0/src/acv_core/channels_auth.py +42 -0
  33. acv_django_core-0.9.0/src/acv_core/exceptions.py +104 -0
  34. acv_django_core-0.9.0/src/acv_core/filters.py +86 -0
  35. acv_django_core-0.9.0/src/acv_core/funciones.py +32 -0
  36. acv_django_core-0.9.0/src/acv_core/middleware.py +43 -0
  37. acv_django_core-0.9.0/src/acv_core/mixins.py +165 -0
  38. acv_django_core-0.9.0/src/acv_core/modelos.py +208 -0
  39. acv_django_core-0.9.0/src/acv_core/notificaciones/__init__.py +12 -0
  40. acv_django_core-0.9.0/src/acv_core/notificaciones/comando.py +119 -0
  41. acv_django_core-0.9.0/src/acv_core/notificaciones/consumers.py +69 -0
  42. acv_django_core-0.9.0/src/acv_core/notificaciones/correos.py +134 -0
  43. acv_django_core-0.9.0/src/acv_core/notificaciones/modelos.py +365 -0
  44. acv_django_core-0.9.0/src/acv_core/notificaciones/paginas.py +277 -0
  45. acv_django_core-0.9.0/src/acv_core/notificaciones/push.py +138 -0
  46. acv_django_core-0.9.0/src/acv_core/notificaciones/senales.py +46 -0
  47. acv_django_core-0.9.0/src/acv_core/notificaciones/trabajos.py +50 -0
  48. acv_django_core-0.9.0/src/acv_core/pagination.py +103 -0
  49. acv_django_core-0.9.0/src/acv_core/parsers.py +24 -0
  50. acv_django_core-0.9.0/src/acv_core/permisos.py +14 -0
  51. acv_django_core-0.9.0/src/acv_core/proyecto.py +21 -0
  52. acv_django_core-0.9.0/src/acv_core/pruebas/__init__.py +6 -0
  53. acv_django_core-0.9.0/src/acv_core/pruebas/red.py +59 -0
  54. acv_django_core-0.9.0/src/acv_core/pruebas/settings.py +117 -0
  55. acv_django_core-0.9.0/src/acv_core/renderers.py +35 -0
  56. acv_django_core-0.9.0/src/acv_core/secciones.py +123 -0
  57. acv_django_core-0.9.0/src/acv_core/serializers.py +675 -0
  58. acv_django_core-0.9.0/src/acv_core/sistema/__init__.py +12 -0
  59. acv_django_core-0.9.0/src/acv_core/sistema/acciones.py +279 -0
  60. acv_django_core-0.9.0/src/acv_core/sistema/tablero.py +344 -0
  61. acv_django_core-0.9.0/src/acv_core/tipos.py +15 -0
  62. acv_django_core-0.9.0/src/acv_core/viewsets.py +122 -0
  63. acv_django_core-0.9.0/src/acv_core/vistas.py +96 -0
  64. acv_django_core-0.9.0/src/acv_django_core.egg-info/PKG-INFO +436 -0
  65. acv_django_core-0.9.0/src/acv_django_core.egg-info/SOURCES.txt +82 -0
  66. acv_django_core-0.9.0/src/acv_django_core.egg-info/dependency_links.txt +1 -0
  67. acv_django_core-0.9.0/src/acv_django_core.egg-info/requires.txt +30 -0
  68. acv_django_core-0.9.0/src/acv_django_core.egg-info/top_level.txt +1 -0
  69. acv_django_core-0.9.0/tests/test_actividad.py +90 -0
  70. acv_django_core-0.9.0/tests/test_archivos.py +325 -0
  71. acv_django_core-0.9.0/tests/test_autenticacion.py +232 -0
  72. acv_django_core-0.9.0/tests/test_campos.py +54 -0
  73. acv_django_core-0.9.0/tests/test_channels_auth.py +31 -0
  74. acv_django_core-0.9.0/tests/test_exceptions.py +66 -0
  75. acv_django_core-0.9.0/tests/test_filters.py +48 -0
  76. acv_django_core-0.9.0/tests/test_modelos.py +78 -0
  77. acv_django_core-0.9.0/tests/test_notificaciones.py +251 -0
  78. acv_django_core-0.9.0/tests/test_pagination.py +88 -0
  79. acv_django_core-0.9.0/tests/test_pruebas.py +58 -0
  80. acv_django_core-0.9.0/tests/test_secciones.py +73 -0
  81. acv_django_core-0.9.0/tests/test_serializers.py +234 -0
  82. acv_django_core-0.9.0/tests/test_sistema.py +170 -0
  83. acv_django_core-0.9.0/tests/test_utilidades.py +59 -0
  84. acv_django_core-0.9.0/tests/test_viewsets.py +115 -0
@@ -0,0 +1,436 @@
1
+ Metadata-Version: 2.4
2
+ Name: acv-django-core
3
+ Version: 0.9.0
4
+ Summary: Piezas compartidas de los backends Django de ACV.
5
+ Author: mario.aftimos
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: Django<6,>=5.2
9
+ Requires-Dist: djangorestframework<3.17,>=3.16
10
+ Requires-Dist: djangorestframework-simplejwt<6,>=5.5
11
+ Requires-Dist: channels<5,>=4.2
12
+ Requires-Dist: django-filter<26,>=25.1
13
+ Requires-Dist: django-simple-history<4,>=3.10
14
+ Requires-Dist: drf-simple-api-errors<3,>=2.1
15
+ Requires-Dist: requests<3,>=2.32
16
+ Requires-Dist: Pillow<13,>=11
17
+ Provides-Extra: spectacular
18
+ Requires-Dist: drf-spectacular<1,>=0.29; extra == "spectacular"
19
+ Provides-Extra: paises
20
+ Requires-Dist: django-countries<8,>=7.6; extra == "paises"
21
+ Provides-Extra: google
22
+ Requires-Dist: google-auth<3,>=2.40; extra == "google"
23
+ Provides-Extra: archivos
24
+ Requires-Dist: pandas<3,>=2.2; extra == "archivos"
25
+ Requires-Dist: numpy<3,>=2; extra == "archivos"
26
+ Requires-Dist: openpyxl<4,>=3.1; extra == "archivos"
27
+ Requires-Dist: PyYAML<7,>=6; extra == "archivos"
28
+ Provides-Extra: dev
29
+ Requires-Dist: ruff==0.16.3; extra == "dev"
30
+ Requires-Dist: fakeredis<3,>=2.26; extra == "dev"
31
+ Requires-Dist: celery<6,>=5.5; extra == "dev"
32
+ Requires-Dist: django-celery-beat<3,>=2.8; extra == "dev"
33
+
34
+ # acv-django-core
35
+
36
+ Piezas compartidas de los backends Django de ACV: **acv-tms**, **acv-wms** y **acv-suma**.
37
+ Es el equivalente de `@acvl/frontend-components` para el backend: lo que los tres repos
38
+ tenían copiado a mano, y que con el tiempo se fue separando, vive aquí una sola vez.
39
+
40
+ Se importa como `acv_core`.
41
+
42
+ ## Qué trae
43
+
44
+ | Módulo | Qué es | Antes vivía en |
45
+ |---|---|---|
46
+ | `acv_core.renderers` | `BrowsableAPIRendererWithoutForms`, `PlainTextRenderer` | `core/custom/renderers.py` (tms, wms), `core/shared/renderers.py` (suma) |
47
+ | `acv_core.tipos` | `ValueLabel`, `ObjectPermissions`, `ModelPermissions` | `core/shared/classes.py` |
48
+ | `acv_core.admin` | `CustomModelAdmin` | `core/custom/admin.py` |
49
+ | `acv_core.parsers` | `data_helper`, `parse` | `core/helpers/parsers.py` |
50
+ | `acv_core.funciones` | `remove_accents`, `datetime_to_cron` | `core/helpers/functions.py` |
51
+ | `acv_core.filters` | `NumberInFilter`, `CharInFilter`, `ConDisponibleFilterSet`, `ConMiasFilterSet` | `core/custom/filters.py` |
52
+ | `acv_core.channels_auth` | `JWTAuthMiddleware` para WebSockets | `core/custom/channels_auth.py` |
53
+ | `acv_core.actividad` + `acv_core.middleware` | Presencia de usuarios en Redis y `ActividadUsuarioMiddleware` | `core/helpers/actividad.py` + `core/middleware.py` |
54
+ | `acv_core.pruebas` | `configurar_pruebas`, `bloquear_red` | `core/settings/testing.py` + `core/bloqueo_de_red.py` |
55
+ | `acv_core.pagination` | `CustomPagination` (con `datos_extra` para extender la respuesta), `PaginacionSiSePide`, `NotificationsPagination` | `core/custom/pagination.py` (tms, wms), `core/shared/pagination.py` (suma) |
56
+ | `acv_core.exceptions` | `exception_handler` (bajas bloqueadas y errores de listas), `motivo_de_baja_bloqueada`, `enumerar` | `core/custom/exceptions.py` (tms, wms); suma usaba el de la librería directo |
57
+ | `acv_core.serializers` | Bases (`DynamicFieldsModelSerializer`, `ReadOnlyModelSerializer`, `WithEditPermissionModelSerializer`, `WriteModelSerializerWithFieldPermissions`, `RestrictedWritableFieldsMixin`), campos (`ChoiceValueLabelField`, `ChoiceLabelField`, `CountryValueLabelField`, `CommaSeparatedSlugField`, `DireccionField`), `ComoOpcionesSerializer`, `BusquedaSerializer`, `EstadoSerializer` e historial (`BaseHistorySerializer`, `HistorySerializerWithLabels`) | `core/custom/serializers.py` + `core/shared/serializers.py` (tms, wms), `core/shared/serializers.py` (suma) |
58
+ | `acv_core.secciones` | `remover_seccion`, `crear_seccion_con_serializador`, `actualizar_seccion_con_serializador`, `actualizar_seccion_por_campos`, `actualizar_campos_incluidos` | `core/helpers/serializers.py` (tms, wms), `core/utils/serializers.py` (suma) |
59
+ | `acv_core.mixins` | `GetSerializerClassMixin`, `GetQuerysetMixin`, `ConReferenciasEnLaLectura`, `RespuestaDetalladaUpdateMixin` | `core/custom/mixins.py` (tms, wms), `core/shared/mixins.py` (suma) |
60
+ | `acv_core.viewsets` | `ConComoOpciones`, `ConContador`, `Historial` | `core/custom/viewsets.py` (tms, wms), `core/shared/viewsets.py` (suma) |
61
+ | `acv_core.modelos` | `ConActivo`, `ConActivoQuerySet`, `ComoOpciones`, `ConFrontendURL`, `IsRefed` (con `anotar_referencias`), `ClonableAOtroModelo` | `core/shared/models.py` |
62
+ | `acv_core.campos` | `CompressedImageField` (con `miniatura_field` opcional), `CompressedFileField`, `comprimir_imagen`, `comprimir_con_miniatura` | `core/shared/fields.py` |
63
+ | `acv_core.vistas` | `armar_vista`, `instanciar_sin_request`, `ruta_de`, `vistas_del_urlconf` | `core/helpers/vistas.py` |
64
+ | `acv_core.archivos` | El intercambio de archivos con los listados (extra `archivos`), ver abajo | `core/custom/{importacion,plantilla,csv_export,xlsx_export}.py`, `core/custom/viewsets.py`, `core/custom/mixins.py`, `general/` (tms, wms) |
65
+ | `acv_core.notificaciones` | La campana, el push de Expo y el correo, ver abajo | `general/` (tms), `notificaciones/` (wms) |
66
+ | `acv_core.sistema` | El tablero de administración (`tablero.TableroView`) y sus acciones (`acciones`: tareas periódicas, mantenimiento, silencio de avisos) | `configuracion/views/{dashboard,tareas,servicios,silencio,sistema}.py` |
67
+ | `acv_core.permisos` | `EsSuperUsuario` | `core/custom/permissions.py` (tms), `configuracion/views/dashboard.py` (wms, suma) |
68
+ | `acv_core.proyecto` | `prefijo()`: el `ACV_PREFIJO` del proyecto | — |
69
+ | `acv_core.autenticacion` | El acceso: `views` (login, Google, refresco, salir, cambio y restablecimiento de contraseña), `tokens.TokenDelUsuarioSerializer`, `permisos.get_frontend_permissions`, `sesiones.cerrar_sesiones` y la base `modelos.CodigoDeRestablecimientoBase` | `authentication/` (tms, wms), `configuracion/views/auth.py` (suma) |
70
+
71
+ `acv_core.archivos` se reparte así:
72
+
73
+ | Módulo | Qué es |
74
+ |---|---|
75
+ | `modelos` | Las bases abstractas `DocumentBase`, `PreferenciaDeExportacionBase`, `ExportacionCSVBase`, `ImportacionCSVBase`, `FilaDeImportacionBase` y `ConArchivoOrigen`; `modelo_de` encuentra los concretos |
76
+ | `viewsets` | `ConPlantilla`, `ConArchivos` (validar y subir) y `ExportarCSV`, que cada recurso suma por herencia |
77
+ | `mixins` | `GetCSVMixin`: el contrato `csv` de cada vista |
78
+ | `serializers` | `ValidarArchivoSerializer`, `SubirArchivoSerializer`, `ConLlaveNatural` |
79
+ | `importacion`, `plantilla` | El motor de importación y la plantilla xlsx |
80
+ | `exportacion_csv`, `exportacion_xlsx`, `entrega` | Los escritores y cómo se entrega un archivo guardado |
81
+ | `paginas` | Filtros, serializadores y vistas de Documentos, Exportaciones, Importaciones y Preferencias |
82
+ | `trabajos` | Lo que corren las tareas de Celery, sin el `@shared_task` |
83
+ | `comando` | La base de `manage.py importar_csv` |
84
+ | `progreso`, `exportables` | El avance por el socket de notificaciones y las columnas exportables por recurso |
85
+
86
+ `acv_core.notificaciones`:
87
+
88
+ | Módulo | Qué es |
89
+ |---|---|
90
+ | `modelos` | Las bases `NotificacionBase`, `DispositivoBase` y `SilencioDeNotificacionesBase`; `esta_silenciado`, `avisar` y `grupo_del_usuario` |
91
+ | `push` | El envío a Expo, en lotes y con baja de tokens muertos |
92
+ | `correos` | `send_template_email`, `send_html_email` y el desvío a `EMAIL_DE_PRUEBAS` |
93
+ | `consumers`, `senales` | El socket de la campana y la señal que empuja cada aviso nuevo |
94
+ | `paginas` | Las vistas de la campana y de los dispositivos, con `registrar` y `dar_de_baja` para la app |
95
+ | `trabajos`, `comando` | Lo que corren sus tareas, y la base de `manage.py correo_de_prueba` |
96
+
97
+ `ProjectBaseViewset` sigue en cada proyecto, porque cada uno lo arma con mixins distintos, pero ya se
98
+ compone sólo con piezas del paquete. Lo que es de un solo proyecto se queda en él: en tms `Moneda`,
99
+ las vigencias, `Adjunto` con `ConDescarga` y `ConArchivoSubido`; en wms `EliminarVarios`, el
100
+ recorte por agencia o cliente y los reportes.
101
+
102
+ ## Instalación
103
+
104
+ En el `requirements.txt` del backend, desde PyPI y fijado a una versión exacta. tms y wms con los
105
+ extras; suma sin ellos por ahora:
106
+
107
+ ```
108
+ acv-django-core[spectacular,paises,archivos,google]==0.9.0
109
+ ```
110
+
111
+ Cada tag `v*` se publica solo en PyPI (`.github/workflows/publish.yml`, con trusted publishing):
112
+ la versión del tag tiene que ser la de `pyproject.toml`.
113
+
114
+ | Extra | Trae | Sin él |
115
+ |---|---|---|
116
+ | `spectacular` | `drf-spectacular` | Los decoradores del esquema OpenAPI no hacen nada |
117
+ | `paises` | `django-countries` | No hay `CountryField` que mapear |
118
+ | `archivos` | pandas, numpy, openpyxl, PyYAML | `acv_core.archivos` no se puede importar |
119
+ | `google` | `google-auth` | `GoogleLoginView` truena al recibir un login |
120
+
121
+ Para desarrollar contra el checkout local (el equivalente de `npm run dev:lib`), monta el
122
+ checkout en el contenedor e instálalo en modo editable:
123
+
124
+ ```
125
+ pip install -e /ruta/al/checkout/acv-django-core
126
+ ```
127
+
128
+ ## Settings
129
+
130
+ | Setting | Obligatorio | Para qué |
131
+ |---|---|---|
132
+ | `ACV_PREFIJO` | Sí, si se usa `ActividadUsuarioMiddleware` o el tablero del sistema | Prefijo del proyecto en las llaves de Redis (`"tms"`, `"wms"`, `"suma"`). Sin él el proceso no arranca. |
133
+ | `ACV_ACTIVIDAD_RUTAS_IGNORADAS` | No | Prefijos de ruta que se suman a los ignorados de fábrica (suma: `("/iclock/",)`). |
134
+ | `ACV_APPS_SIN_SUPERUSUARIO` | No | Apps donde ser superusuario no basta para que la ficha ofrezca editar y borrar. Default `("auth", "authentication")`. |
135
+ | `ACV_APP_DE_ARCHIVOS` | No | La app con los modelos concretos de archivos. Default `"general"`. |
136
+ | `ACV_TAREA_DE_IMPORTACION`, `ACV_TAREA_DE_EXPORTACION` | No | Ruta de la tarea de Celery que procesa una importación o una exportación grande. Default `<app de archivos>.tasks.procesar_importacion_csv` y `.generar_exportacion_csv`. |
137
+ | `IMPORT_FILAS_SINCRONAS`, `EXPORT_FILAS_SINCRONAS` | No | Arriba de cuántas hileras el trabajo se va a segundo plano. Default 200 y 50,000. |
138
+ | `ACV_APP_DE_NOTIFICACIONES` | No | La app con los modelos concretos de notificaciones. Default `"notificaciones"`; tms pone `"general"`. |
139
+ | `ACV_ACCIONES_DE_SERVICIO` | No | Qué acciones de mantenimiento deja correr el tablero. Default las tres (`makemigrations`, `migrate`, `flush_cache`); en producción conviene `("migrate", "flush_cache")`. |
140
+ | `SIMPLE_JWT["TOKEN_OBTAIN_SERIALIZER"]` | Sí, para el acceso | El serializador del token del proyecto (subclase de `TokenDelUsuarioSerializer`). Lo usan el login, Google y el cambio de contraseña. |
141
+ | `ACV_ACCIONES_DE_PERMISO` | No | Las acciones fuera del CRUD que viajan en el claim `permissions` (la lista blanca `ACCIONES_EXTRA` de cada repo). |
142
+ | `REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]` | Sí, para el acceso | Los scopes `auth`, `refresco` y, con el restablecimiento, `restablecer_contrasena_solicitud` y `restablecer_contrasena_codigo`. Sin la tasa de un scope que pide una vista, DRF responde 500. |
143
+ | `GOOGLE_OAUTH_CLIENT_ID`, `SSO_ALLOWED_EMAIL_DOMAINS` | Para Google | El cliente OAuth y los dominios de correo aceptados. |
144
+ | `ACV_APP_DE_AUTENTICACION`, `ACV_TAREA_DE_CODIGO_DE_RESTABLECIMIENTO` | Para el restablecimiento | La app con `CodigoDeRestablecimiento` (default `"authentication"`) y la tarea que manda el código por correo (`.delay(usuario_id, codigo)`). |
145
+ | `EXPO_PUSH_URL`, `EXPO_ACCESS_TOKEN` | No | El API de Expo y su token. |
146
+ | `EMAIL_DE_PRUEBAS`, `LOGO_DE_CORREOS`, `NOMBRE_DE_REMITENTE`, `DEFAULT_FROM_EMAIL` | No (el último, de Django) | El desvío de correos en desarrollo, el logo de las plantillas y el remitente. |
147
+
148
+ La configuración por modelo va en su dict `_custom_config`: `subsections_requiring_permission`,
149
+ `fields_requiring_permission` y `nested_labels` (ver el docstring de `acv_core.serializers`).
150
+
151
+ ## Uso
152
+
153
+ ```python
154
+ # core/settings/base.py
155
+ MIDDLEWARE = [..., "acv_core.middleware.ActividadUsuarioMiddleware"]
156
+ ACV_PREFIJO = "tms"
157
+ REST_FRAMEWORK = {
158
+ "DEFAULT_RENDERER_CLASSES": [
159
+ "rest_framework.renderers.JSONRenderer",
160
+ "acv_core.renderers.BrowsableAPIRendererWithoutForms",
161
+ ],
162
+ "DEFAULT_PAGINATION_CLASS": "acv_core.pagination.CustomPagination",
163
+ "EXCEPTION_HANDLER": "acv_core.exceptions.exception_handler",
164
+ }
165
+
166
+ # core/asgi.py
167
+ from acv_core.channels_auth import JWTAuthMiddleware
168
+
169
+ # core/settings/testing.py
170
+ from .base import * # noqa: F401,F403
171
+
172
+ from acv_core.pruebas import configurar_pruebas
173
+
174
+ configurar_pruebas(globals())
175
+ ```
176
+
177
+ ## Puntos de extensión
178
+
179
+ Lo que un solo proyecto necesita no vive aquí: se sobrescribe en una subclase.
180
+
181
+ | Dónde | Método | Quién lo usa |
182
+ |---|---|---|
183
+ | `CustomPagination` | `datos_extra(view, user)` | wms: `eliminar_varios` |
184
+ | `WithHasHistorySerializer` (y sus hijas) | `puede_ver_historial(user)` | Por default se le niega al usuario externo (`es_externo`, el de wms) |
185
+ | `RespuestaDetalladaUpdateMixin` | `preparar_lectura_de_lista(queryset)` | Por default anota `is_refed`; un proyecto lo extiende para anotar más |
186
+
187
+ ## Migraciones
188
+
189
+ Las migraciones de cada proyecto citan los mixins de modelo y los campos por su ruta vieja:
190
+ `bases=(core.shared.models.IsRefed, models.Model)`, `core.shared.fields.CompressedImageField(...)`.
191
+ Al adoptar el paquete, **`core/shared/models.py` y `core/shared/fields.py` se quedan reexportando**
192
+ lo de `acv_core.modelos` y `acv_core.campos`:
193
+
194
+ ```python
195
+ # core/shared/fields.py
196
+ from acv_core.campos import * # noqa: F401,F403 -- las migraciones citan esta ruta
197
+ ```
198
+
199
+ La ruta vieja resuelve a la misma clase, así que el `deconstruct()` de la migración y el del modelo
200
+ coinciden y `makemigrations` no genera nada (lo prueba `tests/legado`). Esos módulos no se pueden
201
+ borrar mientras exista una migración que los cite, o sea hasta un squash. Las migraciones nuevas
202
+ ya escriben la ruta del paquete.
203
+
204
+ ## Archivos: cómo lo adopta un proyecto
205
+
206
+ 1. **Modelos.** En `general/models.py` cada modelo hereda su base, sin `class Meta` propio
207
+ (`DocumentBase` redeclara `Categories` y `category` con las del proyecto; ver el docstring de
208
+ `acv_core.archivos.modelos`). Lo que sólo tiene un proyecto se agrega en su subclase: wms
209
+ conserva así `ExportacionCSV.definicion_de_reporte`. Las funciones `upload_to` y
210
+ `exportacion_upload_to` se quedan importables en `general/models.py` (reexportadas), porque las
211
+ migraciones viejas las citan.
212
+ 2. **Migración.** `makemigrations general` genera una sola migración por proyecto. En tms trae
213
+ un `AddField` de `ExportacionCSV.formato` (con default `"csv"`), que es el único cambio con SQL;
214
+ lo demás son `AlterField` y `AlterModelOptions` sin SQL (la ruta de `upload_to`, textos de ayuda,
215
+ `verbose_name`). Conviene revisarlo con `sqlmigrate` antes de aplicarlo.
216
+ 3. **Tareas.** `general/tasks.py` conserva sus `@shared_task` con los mismos nombres, cada uno de
217
+ una línea sobre `acv_core.archivos.trabajos`, más el aviso con su propio `Notificacion` (ver el
218
+ docstring de `trabajos`).
219
+ 4. **Comando.** `general/management/commands/importar_csv.py` queda en
220
+ `class Command(ImportarCSV): pass`.
221
+ 5. **Rutas.** Las vistas de `acv_core.archivos.paginas` se registran en el router; las que el
222
+ proyecto amplía (wms les suma `EliminarVarios` y sus permisos de portal; tms acota
223
+ `DocumentoView.categorias` a `("csv_upload",)`) se registran por su subclase.
224
+ 6. **Socket.** El consumer de notificaciones maneja el tipo `enviar_progreso`, que es por donde
225
+ viaja el avance de una importación (tms y wms ya lo tienen).
226
+
227
+ ## Acceso: cómo lo adopta un proyecto
228
+
229
+ El modelo de usuario no se comparte: todo va contra `get_user_model()`, y lo que un usuario pueda
230
+ no tener se pregunta (`cambiar_contrasena`, `sesiones_cerradas_en`).
231
+
232
+ 1. **Token.** El `MyTokenObtainPairSerializer` del proyecto hereda `TokenDelUsuarioSerializer` y
233
+ deja en `claims_extra` sólo lo suyo (tms: `operador_id`, `guardia_yardas`; wms: `iniciales`,
234
+ `es_externo`, `agencias`, `cliente`, `idioma`, `csr`...). `SIMPLE_JWT["TOKEN_OBTAIN_SERIALIZER"]`
235
+ lo nombra.
236
+ 2. **Permisos.** `ACV_ACCIONES_DE_PERMISO` lleva la lista `ACCIONES_EXTRA` que hoy vive en
237
+ `authentication/permisos.py` (tms) o `configuracion/serializers/usuario.py` (wms).
238
+ 3. **Vistas.** Se montan en las URLs que ya tiene cada uno (`token/`, `token/refresh/`, `google/`...),
239
+ así que los frontends y las apps no cambian. Un proyecto que guarda el cambio obligatorio en
240
+ otro lado sobrescribe `LoginView.debe_cambiar_contrasena` (suma: `user.extra_info`).
241
+ 4. **Restablecimiento**, opcional: el modelo `CodigoDeRestablecimiento(CodigoDeRestablecimientoBase)`
242
+ en su app de autenticación, la tarea que manda el correo con su plantilla y sus dos scopes.
243
+
244
+ ## Tablero del sistema: cómo lo adopta un proyecto
245
+
246
+ Las vistas se montan en las URLs que cada proyecto ya tiene (`configuracion/dashboard/`,
247
+ `tareas/toggle/`...), así que el frontend no cambia. Lo propio va en una subclase de `TableroView`:
248
+ tms le suma Samsara (`servicios_extra`) y sus interruptores (`paneles_extra`). El panel de
249
+ `notificaciones` sale solo cuando el proyecto declara `SilencioDeNotificaciones`. El ping a Celery
250
+ usa la app en curso (`celery.current_app`), no el `core.celery` de cada repo.
251
+
252
+ ## Notificaciones: cómo lo adopta un proyecto
253
+
254
+ 1. **Modelos.** En su app de notificaciones, `Notificacion(NotificacionBase)` y
255
+ `Dispositivo(DispositivoBase)`, sin `class Meta`. `SilencioDeNotificaciones` es opcional: sin él
256
+ ningún canal se silencia. wms lo declara **con su `history = HistoricalRecords()`**, que la base
257
+ no trae; sin esa línea la migración le borraría la tabla de historial.
258
+ 2. **Migración.** Sin SQL en tms; en wms sólo `AlterField`/`AlterModelOptions` (las etiquetas de
259
+ `Notificacion.Tipo` con acentos, `verbose_name`). Si tms adopta el silencio, esa sí es una tabla
260
+ nueva.
261
+ 3. **Señal y socket.** El `ready()` de la app llama `conectar_senales()` (en lugar del `@receiver`),
262
+ y `asgi.py` monta `rutas_de_websocket` detrás de `JWTAuthMiddleware`.
263
+ 4. **Tareas.** `enviar_push` y `limpiar_notificaciones_antiguas` se quedan en `tasks.py` con su
264
+ nombre, de una línea cada una sobre `acv_core.notificaciones.trabajos`.
265
+ 5. **Rutas.** `NotificacionView` y `DispositivoView` en el router; wms le suma a la segunda sus
266
+ permisos internos y `EliminarVarios` en una subclase.
267
+ 6. **Correos.** Las plantillas siguen en `templates/correos/` del proyecto. `correo_de_prueba` es
268
+ una subclase de `CorreoDePrueba` con los `EJEMPLOS` de esas plantillas.
269
+
270
+ ## Lo que cambia al adoptarlo
271
+
272
+ Cada repo tenía arreglos que los otros no; al pasar al paquete, cada uno recibe los de los
273
+ demás. Lo marcado con ⚠ cambia lo que ve un cliente del API y hay que revisarlo en su frontend.
274
+
275
+ ### tms
276
+
277
+ - Los settings de pruebas ahora también silencian el logging, apagan el throttling y usan un
278
+ `MEDIA_ROOT` temporal. Para ver los logs al depurar: `configurar_pruebas(globals(),
279
+ silenciar_logs=False)`.
280
+ - `WriteModelSerializerWithFieldPermissions` **por fin aplica** `fields_requiring_permission`:
281
+ la copia leía `_custom_config` con `getattr` y nunca restringía. Hoy ningún modelo de tms lo
282
+ declara, así que no cambia nada todavía.
283
+ - `ConComoOpciones`: el serializador de opciones se declara en
284
+ `serializer_action_classes["como_opciones"]`, no en `serializer_de_opciones` (14 vistas).
285
+ - `CompressedImageField` gana el `miniatura_field` opcional de wms; sin declararlo comprime igual
286
+ que antes.
287
+ - El motor de archivos gana lo que wms ya tenía: la exportación a **Excel** (`formato`, con su
288
+ migración), el modo **`ACTUALIZAR`** (`ConLlaveNatural.SOLO_ACTUALIZA`), los errores de
289
+ serializadores anidados con su ruta (`direccion.cp`), las plantillas de variantes y descritas a
290
+ mano, y el borrado de archivos que no se detiene si el storage falla.
291
+ - `validar_archivo` publica además `llave_natural` y `modos`, y `subir_archivo` rechaza un modo que
292
+ el recurso no admite.
293
+ - Las páginas de Documentos y Exportaciones ganan la acción `enlace` junto a la `descargar` que ya
294
+ usa su frontend.
295
+ - Las notificaciones ganan lo que wms ya tenía: el **silencio por canal** (opcional, con tabla
296
+ nueva), adjuntos en `send_template_email` y el comando `correo_de_prueba`.
297
+
298
+ ### wms
299
+
300
+ - Al quitar el pool de la base de datos en pruebas, el resto de `OPTIONS` se conserva.
301
+ - `ConMiasFilterSet` usa `"usuario"` por defecto en vez de `"csr"`; los dos FilterSets de wms que
302
+ lo usan ya declaran el suyo.
303
+ - La paginación anuncia `eliminar_varios` desde una subclase que sobrescribe `datos_extra` (y
304
+ `DEFAULT_PAGINATION_CLASS` apunta a ella), porque `EliminarVarios` sigue en wms.
305
+ - `WithHasHistorySerializer`: el `es_externo` lo respeta ya el `puede_ver_historial` del paquete.
306
+ - ⚠ **El historial baja como lo lee el usuario**: las llaves foráneas por el nombre del registro
307
+ (o `"12 (eliminado)"`) y los choices por su etiqueta, en lugar del id y el código (99
308
+ historiales).
309
+ - ⚠ `DireccionField` devuelve la dirección en las llaves de Google Places (`formatted_address`,
310
+ `administrative_area_level_1`, `lat`), que es lo que lee el componente de dirección; la copia de
311
+ wms devolvía las de django-address (`raw`, `state`, `latitude`).
312
+ - ⚠ `CommaSeparatedSlugField` exporta con `", "` y por su `slug_field` (antes `","` y `str()`);
313
+ un solo uso, `destinos_no_agrupar`.
314
+ - `ChoiceLabelField` también acepta el valor del choice, y `ReadOnlyModelSerializer` acepta
315
+ requisitos por campo (`"fields.<codename>"`); los dos sólo aceptan más.
316
+ - `IsRefed` gana `RELACIONES_PROPIAS` y la anotación de tms: el update ya regresa el renglón con
317
+ `is_refed` anotado, y `ConReferenciasEnLaLectura` queda disponible para sus listados (que hoy
318
+ pagan un `EXISTS` por relación y renglón). `clonar_a` acepta `solo=`.
319
+ - **La purga de documentos sólo barre los CSV del import** (`old_files`): los de categoría
320
+ `transfer` y `odc` dejan de borrarse a los 7 días. No hay código que los cree hoy.
321
+ - ⚠ **El import lee como texto toda columna que el serializador no tipa** como número, fecha o
322
+ booleano, en vez de dejar que pandas adivine: un código `0400` ya no llega como `400` aunque la
323
+ vista no lo haya nombrado en `text_columns`.
324
+ - La página de Importaciones cuenta el resumen de cada renglón en el mismo query del listado
325
+ (`con_conteos`), y las de Documentos y Exportaciones ganan `descargar` (con el nombre del archivo
326
+ dentro de la URL firmada de GCS) junto al `enlace` que ya usa su frontend.
327
+ - `PreferenciaDeExportacion.administra_compartidas` es método de clase y arma el permiso con la
328
+ etiqueta de la app, en vez de `general.` escrito a mano.
329
+ - `DispositivoView` gana `registrar` y `dar_de_baja`, las acciones con que la app de tms registra
330
+ su teléfono; la app de wms puede seguir usando `UsuarioView.dispositivo`.
331
+
332
+ ### wms y suma
333
+
334
+ - ⚠ Los errores de validación de listas cambian de forma: con `many=True` salen por campo y
335
+ posición (`lecturas.1.ts`), y un `ListField` inválido responde 400 en lugar de 500.
336
+ - "Todos" (`page_size=-1`) sobre algo paginado que no es queryset ya no revienta.
337
+
338
+ ### suma
339
+
340
+ - Gana los settings de pruebas, el bloqueo de red y el manejador de excepciones (una baja
341
+ bloqueada por `PROTECT` responde 400 con el motivo en vez de 500).
342
+ - Sus 2 modelos con `MyMeta` pasan esa configuración a `_custom_config`.
343
+ - ⚠ **`is_refed` revisa todas las relaciones**: la copia de suma contestaba con la primera relación
344
+ inversa y no miraba las demás, así que un registro referenciado sólo por la segunda se ofrecía
345
+ como borrable. Ahora la ficha deja de ofrecer el borrado en esos casos, que es lo correcto.
346
+ - `ComoOpciones` y `ConFrontendURL` dejan de ser modelos abstractos y pasan a mixins, como en tms
347
+ y wms: `AccesoEmpleado(ComoOpciones)`, que no tiene otra base, necesita agregar `models.Model`.
348
+ Ninguna de las dos declara campos, así que no hay migración.
349
+ - Para conservar su regla de superusuario, declara sus apps en `ACV_APPS_SIN_SUPERUSUARIO` (su
350
+ copia excluía con la regex `configuracion$|auth`).
351
+ - ⚠ `como_opciones` se corta en 50 y se ordena (`ORDEN_DE_OPCIONES`, `Meta.ordering` o `pk`): un
352
+ selector que hoy recibe todas las opciones de golpe necesita buscar en el servidor.
353
+ - ⚠ `BusquedaSerializer.modelo` es el `verbose_name` del modelo, no el nombre de la clase.
354
+ - ⚠ Las fichas agregan `actividad` (`{value, label}` de `activo`/`is_active`).
355
+ - `actualizar_seccion_con_serializador` guarda cada renglón con el `update()` de su serializador,
356
+ como tms y wms; la copia de suma asignaba campos con `actualizar_campos_incluidos` y saltaba los
357
+ renglones que sólo traían `id`.
358
+
359
+ ### Los tres
360
+
361
+ - `datetime_to_cron` toma la zona de `TIME_ZONE` (los tres usan `America/Los_Angeles`).
362
+ - `remove_accents` acepta las cadenas diferidas de Django (un `verbose_name_plural` por default),
363
+ que antes tronaban con `TypeError`.
364
+
365
+ - ⚠ **Las tres dejan correr `makemigrations` desde el tablero**, también en producción: escribe
366
+ archivos de migración dentro del contenedor, fuera del repo, y un `migrate` posterior aplicaría
367
+ un esquema que no está en git. El paquete lo conserva para no cambiar nada al adoptarlo, pero
368
+ `ACV_ACCIONES_DE_SERVICIO = ("migrate", "flush_cache")` en `production.py` lo quita.
369
+ - El tablero de wms y suma describe también por `codigo` el objeto cambiado en la bitácora (como
370
+ tms), y el de tms gana la protección de wms para correr sin `django_celery_beat`.
371
+
372
+ ### El acceso, por repo
373
+
374
+ - **tms**: lo suyo pasa al paquete casi tal cual (es la base de la fusión). Gana, opcional, el
375
+ restablecimiento por código de wms: una tabla nueva, su plantilla y sus dos scopes.
376
+ - **wms**:
377
+ - ⚠ Antes de adoptarlo hay que **agregar los scopes `auth` y `refresco`** a
378
+ `DEFAULT_THROTTLE_RATES` (tms usa 60/min y 300/min): login, Google y refresco dejan de contar
379
+ contra el `anon` de 10/min, y sin las tasas DRF responde 500.
380
+ - ⚠ Su `SIMPLE_JWT["TOKEN_OBTAIN_SERIALIZER"]` apunta a `TR_api.serializers...`, que no existe.
381
+ Hoy no muerde porque su vista declara el serializador a mano; el paquete lee el setting, así
382
+ que hay que corregirlo a `authentication.serializers.MyTokenObtainPairSerializer`.
383
+ - Gana `token/salir/` (cierre de sesión con blacklist y baja del teléfono) y el cambio de
384
+ contraseña del primer acceso, si monta las vistas.
385
+ - Restablecer la contraseña ahora también apaga los teléfonos de la cuenta, no sólo sus tokens,
386
+ y sólo cuenta los refresh vivos (`cerrar_sesiones` de tms, en lugar de `User.cerrar_sesiones`).
387
+ - **suma** sí puede adoptarlo ya: sobrescribe `debe_cambiar_contrasena` para leer
388
+ `extra_info.cambiar_contrasena`, y gana el aviso de cuenta desactivada y los throttles (con sus
389
+ scopes). Su serializador de token ya sólo pone los claims comunes.
390
+
391
+ El acceso es también donde vivirá la otra mitad del portal único de SSO cuando se construya: la
392
+ vista que recibe el boleto firmado del portal y entrega el par de tokens con `par_de_tokens`.
393
+
394
+ ### suma y el tablero
395
+
396
+ suma sí puede adoptar el tablero ya: su versión es casi idéntica a la de wms y sólo depende de la
397
+ actividad, que es del paquete desde la 0.1.0. Sin `SilencioDeNotificaciones` el tablero no le
398
+ enseña el panel de avisos, igual que hoy.
399
+
400
+ ### suma y las notificaciones
401
+
402
+ Tampoco por ahora: su `Notificacion` tiene otro esquema (`mensaje` es `TextField`, sus propios
403
+ tipos), no tiene `Dispositivo` y manda push con `django-expo-notifications`. Adoptar las bases es
404
+ una migración de datos y un cambio de app móvil; va con el resto de suma.
405
+
406
+ ### suma y el motor de archivos
407
+
408
+ suma no adopta `acv_core.archivos` por ahora: hacerlo pide dos tablas nuevas (`ImportacionCSV`,
409
+ `FilaDeImportacion`), cambiar su `Document` y pasar su frontend al flujo asíncrono del import. Lo
410
+ que sí urge allá es aparte del paquete: **su purga de documentos no funciona**. `DocumentManager.
411
+ old_files()` se llama a sí mismo (`RecursionError` en cada corrida de `clean_old_files`), y debajo
412
+ el filtro es `uploaded_at__gte=hace_un_día`, que el día que se arregle la recursión borraría los
413
+ documentos de las últimas 24 horas en vez de los viejos.
414
+
415
+ Al migrar un repo, deja el módulo viejo como reexportación (`from acv_core.filters import *`)
416
+ mientras haya código, o migraciones, que lo importen por su ruta anterior.
417
+
418
+ ## Desarrollo
419
+
420
+ ```
421
+ python -m venv .venv
422
+ .venv/Scripts/pip install -e ".[dev,spectacular,paises,archivos,google]" # Linux/macOS: .venv/bin/pip
423
+
424
+ python runtests.py # pruebas (acepta los argumentos de manage.py test)
425
+ ruff format . # formato
426
+ ruff check . # lint
427
+ ```
428
+
429
+ CI corre las tres cosas en cada PR y en cada push a `main`, sin `spectacular` ni `paises` (como
430
+ suma) y con ellos (como tms y wms); las dos variantes con `archivos`, que la suite prueba.
431
+
432
+ ## Versiones
433
+
434
+ Semver con tags `vX.Y.Z`. Cada proyecto fija el tag en su `requirements.txt`, así que una
435
+ versión nueva no llega a nadie hasta que el proyecto sube su tag. Los cambios van en
436
+ [CHANGELOG.md](CHANGELOG.md).