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.
- acv_django_core-0.9.0/PKG-INFO +436 -0
- acv_django_core-0.9.0/README.md +403 -0
- acv_django_core-0.9.0/pyproject.toml +81 -0
- acv_django_core-0.9.0/setup.cfg +4 -0
- acv_django_core-0.9.0/src/acv_core/__init__.py +3 -0
- acv_django_core-0.9.0/src/acv_core/_opcionales.py +51 -0
- acv_django_core-0.9.0/src/acv_core/actividad.py +124 -0
- acv_django_core-0.9.0/src/acv_core/admin.py +13 -0
- acv_django_core-0.9.0/src/acv_core/archivos/__init__.py +14 -0
- acv_django_core-0.9.0/src/acv_core/archivos/comando.py +265 -0
- acv_django_core-0.9.0/src/acv_core/archivos/entrega.py +86 -0
- acv_django_core-0.9.0/src/acv_core/archivos/exportables.py +47 -0
- acv_django_core-0.9.0/src/acv_core/archivos/exportacion_csv.py +87 -0
- acv_django_core-0.9.0/src/acv_core/archivos/exportacion_xlsx.py +130 -0
- acv_django_core-0.9.0/src/acv_core/archivos/importacion.py +574 -0
- acv_django_core-0.9.0/src/acv_core/archivos/mixins.py +220 -0
- acv_django_core-0.9.0/src/acv_core/archivos/modelos.py +832 -0
- acv_django_core-0.9.0/src/acv_core/archivos/paginas.py +730 -0
- acv_django_core-0.9.0/src/acv_core/archivos/plantilla.py +343 -0
- acv_django_core-0.9.0/src/acv_core/archivos/progreso.py +60 -0
- acv_django_core-0.9.0/src/acv_core/archivos/serializers.py +166 -0
- acv_django_core-0.9.0/src/acv_core/archivos/trabajos.py +270 -0
- acv_django_core-0.9.0/src/acv_core/archivos/viewsets.py +677 -0
- acv_django_core-0.9.0/src/acv_core/autenticacion/__init__.py +13 -0
- acv_django_core-0.9.0/src/acv_core/autenticacion/modelos.py +100 -0
- acv_django_core-0.9.0/src/acv_core/autenticacion/permisos.py +72 -0
- acv_django_core-0.9.0/src/acv_core/autenticacion/serializers.py +150 -0
- acv_django_core-0.9.0/src/acv_core/autenticacion/sesiones.py +70 -0
- acv_django_core-0.9.0/src/acv_core/autenticacion/tokens.py +55 -0
- acv_django_core-0.9.0/src/acv_core/autenticacion/views.py +342 -0
- acv_django_core-0.9.0/src/acv_core/campos.py +257 -0
- acv_django_core-0.9.0/src/acv_core/channels_auth.py +42 -0
- acv_django_core-0.9.0/src/acv_core/exceptions.py +104 -0
- acv_django_core-0.9.0/src/acv_core/filters.py +86 -0
- acv_django_core-0.9.0/src/acv_core/funciones.py +32 -0
- acv_django_core-0.9.0/src/acv_core/middleware.py +43 -0
- acv_django_core-0.9.0/src/acv_core/mixins.py +165 -0
- acv_django_core-0.9.0/src/acv_core/modelos.py +208 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/__init__.py +12 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/comando.py +119 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/consumers.py +69 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/correos.py +134 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/modelos.py +365 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/paginas.py +277 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/push.py +138 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/senales.py +46 -0
- acv_django_core-0.9.0/src/acv_core/notificaciones/trabajos.py +50 -0
- acv_django_core-0.9.0/src/acv_core/pagination.py +103 -0
- acv_django_core-0.9.0/src/acv_core/parsers.py +24 -0
- acv_django_core-0.9.0/src/acv_core/permisos.py +14 -0
- acv_django_core-0.9.0/src/acv_core/proyecto.py +21 -0
- acv_django_core-0.9.0/src/acv_core/pruebas/__init__.py +6 -0
- acv_django_core-0.9.0/src/acv_core/pruebas/red.py +59 -0
- acv_django_core-0.9.0/src/acv_core/pruebas/settings.py +117 -0
- acv_django_core-0.9.0/src/acv_core/renderers.py +35 -0
- acv_django_core-0.9.0/src/acv_core/secciones.py +123 -0
- acv_django_core-0.9.0/src/acv_core/serializers.py +675 -0
- acv_django_core-0.9.0/src/acv_core/sistema/__init__.py +12 -0
- acv_django_core-0.9.0/src/acv_core/sistema/acciones.py +279 -0
- acv_django_core-0.9.0/src/acv_core/sistema/tablero.py +344 -0
- acv_django_core-0.9.0/src/acv_core/tipos.py +15 -0
- acv_django_core-0.9.0/src/acv_core/viewsets.py +122 -0
- acv_django_core-0.9.0/src/acv_core/vistas.py +96 -0
- acv_django_core-0.9.0/src/acv_django_core.egg-info/PKG-INFO +436 -0
- acv_django_core-0.9.0/src/acv_django_core.egg-info/SOURCES.txt +82 -0
- acv_django_core-0.9.0/src/acv_django_core.egg-info/dependency_links.txt +1 -0
- acv_django_core-0.9.0/src/acv_django_core.egg-info/requires.txt +30 -0
- acv_django_core-0.9.0/src/acv_django_core.egg-info/top_level.txt +1 -0
- acv_django_core-0.9.0/tests/test_actividad.py +90 -0
- acv_django_core-0.9.0/tests/test_archivos.py +325 -0
- acv_django_core-0.9.0/tests/test_autenticacion.py +232 -0
- acv_django_core-0.9.0/tests/test_campos.py +54 -0
- acv_django_core-0.9.0/tests/test_channels_auth.py +31 -0
- acv_django_core-0.9.0/tests/test_exceptions.py +66 -0
- acv_django_core-0.9.0/tests/test_filters.py +48 -0
- acv_django_core-0.9.0/tests/test_modelos.py +78 -0
- acv_django_core-0.9.0/tests/test_notificaciones.py +251 -0
- acv_django_core-0.9.0/tests/test_pagination.py +88 -0
- acv_django_core-0.9.0/tests/test_pruebas.py +58 -0
- acv_django_core-0.9.0/tests/test_secciones.py +73 -0
- acv_django_core-0.9.0/tests/test_serializers.py +234 -0
- acv_django_core-0.9.0/tests/test_sistema.py +170 -0
- acv_django_core-0.9.0/tests/test_utilidades.py +59 -0
- 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).
|