Guide
Guía — Schemas organized-app en Pydantic v2 (Fase 51)
Modelos Pydantic v2 portados verbatim de
sws2apps/organized-app(MIT) — la PWA React que cientos de congregaciones usan para gestionar programas de reunión, asignaciones y reportes S-21. El toolkit ahora habla el mismo dialecto de datos sin depender de su runtime.
¿Por qué?
Antes de F51, jw_core tenía sus propios modelos para WorkbookWeek,
MonthlyReport, etc. Esos modelos:
- No estaban validados por una comunidad amplia.
- No interoperaban con backups producidos por la PWA organized-app (que ya está adoptada por congregaciones reales).
- Duplicaban modelado: cada nueva feature creaba su propia estructura.
F51 importa los tipos TypeScript del src/definition/ de organized-app
como modelos Pydantic, conservando exactamente la misma forma JSON. Eso
habilita:
- Leer y escribir backups producidos por la PWA (ver F55.5).
- Compartir validación con cientos de despliegues reales.
- Tener una fuente de verdad común para conceptos como S-21, schedule semanal, person.
Estructura del módulo
packages/jw-core/src/jw_core/models_organized/
├── __init__.py ← re-exports + docstring
├── common.py ← Timestamped[T] (CRDT envelope)
├── assignment.py ← AssignmentCode IntEnum + Literal types
├── person.py ← PersonType + sub-shapes
├── week.py ← Week IntEnum + WeekType
├── meeting_attendance.py ← MeetingAttendanceType (mes con 5 semanas)
├── field_service_groups.py ← FieldServiceGroupType
├── field_service_report.py ← UserFieldService{Daily,Monthly}ReportType
└── schedule.py ← SchedWeekType (mid-week + weekend)
El patrón CRDT: Timestamped[T]
organized-app sincroniza estado entre dispositivos sin servidor central.
Cada campo mutable lleva su propio updatedAt para resolver conflictos
last-write-wins por atributo:
from jw_core.models_organized import Timestamped
# JSON shape: {"value": true, "updatedAt": "2026-06-02T10:00:00Z"}
flag: Timestamped[bool] = Timestamped(value=True, updatedAt="2026-06-02T10:00:00Z")
Eso aparece en PersonType, MeetingAttendanceType, etc. en
prácticamente cada campo no-id.
Tipos clave
Week (enum)
from jw_core.models_organized import Week
assert Week.NORMAL == 1
assert Week.MEMORIAL == 5
assert Week.WATCHTOWER_STUDY == 13
assert Week.NO_MEETING == 20
Valores numéricos idénticos al TS source. Si los cambias, rompes sync con la PWA.
AssignmentCode (enum)
from jw_core.models_organized import AssignmentCode
assert AssignmentCode.MM_BibleReading == 100
assert AssignmentCode.WM_WTStudyConductor == 130
assert AssignmentCode.MINISTRY_HOURS_CREDIT == 300
100 = mid-week parts. 110+ = roles. 300 = horas de servicio acreditadas (pioneros).
PersonType
Estructura completa del registro de un publicador:
from jw_core.models_organized import PersonType
person = PersonType.model_validate({
"_deleted": {"value": False, "updatedAt": "2026-06-01T00:00:00Z"},
"person_uid": "uid-abc-123",
"person_data": {
"person_firstname": {"value": "Ana", "updatedAt": "..."},
"person_lastname": {"value": "García", "updatedAt": "..."},
"person_display_name": {"value": "Ana García", "updatedAt": "..."},
"male": {"value": False, "updatedAt": "..."},
"female": {"value": True, "updatedAt": "..."},
"birth_date": {"value": None, "updatedAt": "..."},
"assignments": [],
"timeAway": [],
"archived": {"value": False, "updatedAt": "..."},
"disqualified": {"value": False, "updatedAt": "..."},
"email": {"value": "", "updatedAt": "..."},
"address": {"value": "", "updatedAt": "..."},
"phone": {"value": "", "updatedAt": "..."},
"publisher_baptized": {
"active": {"value": True, "updatedAt": "..."},
"anointed": {"value": False, "updatedAt": "..."},
"other_sheep": {"value": True, "updatedAt": "..."},
"baptism_date": {"value": None, "updatedAt": "..."},
"history": [],
},
"publisher_unbaptized": {"active": {...}, "history": []},
"midweek_meeting_student": {"active": {...}, "history": []},
"privileges": [],
"enrollments": [],
"emergency_contacts": [],
"family_members": {"head": True, "members": [], "updatedAt": "..."},
},
})
print(person.person_data.person_display_name.value) # "Ana García"
Notas de diseño:
_deletedse renombró adeleteden Python para evitar la convención de “atributos privados con_” — pero el alias preserva el JSON original:model_dump(by_alias=True)emite_deleted.first_reportes opcional. Algunos backups no lo traen.StatusHistorymodela toda la historia del publicador (cuando estuvo activo, inactivo, bautizado).
SchedWeekType
Estado autoritativo de una semana de reunión:
from jw_core.models_organized import SchedWeekType
sched = SchedWeekType.model_validate({
"weekOf": "2026-06-01",
"midweek_meeting": {
"chairman": {
"main_hall": [{"type": "main", "name": "Carlos M.", "value": "uid-1", "updatedAt": "..."}],
"aux_class_1": {"type": "aux1", "name": "", "value": "", "updatedAt": "..."},
},
"opening_prayer": [{"type": "main", "name": "Pedro V.", "value": "uid-2", "updatedAt": "..."}],
"tgw_talk": [...],
"tgw_gems": [...],
"tgw_bible_reading": {
"main_hall": [...],
"aux_class_1": {...},
"aux_class_2": {...},
},
"ayf_part1": {
"main_hall": {"student": [...], "assistant": [...]},
"aux_class_1": {"student": {...}, "assistant": {...}},
"aux_class_2": {"student": {...}, "assistant": {...}},
},
"ayf_part2": {...},
"ayf_part3": {...},
"ayf_part4": {...},
"lc_part1": [...],
"lc_part2": [...],
"lc_part3": [...],
"lc_cbs": {"conductor": [...], "reader": [...]},
"closing_prayer": [...],
"circuit_overseer": {...},
"week_type": [...],
},
"weekend_meeting": {
"chairman": [...],
"opening_prayer": [...],
"public_talk_type": [...],
"speaker": {"part_1": [...], "part_2": [...], "substitute": [...]},
"wt_study": {"conductor": [...], "reader": [...]},
"closing_prayer": [...],
"circuit_overseer": {...},
"week_type": [...],
"outgoing_talks": [],
},
})
Cada slot es un AssignmentCongregation con type/name/value/updatedAt,
opcionalmente solo, id, _deleted. El value típicamente es un
person_uid apuntando a PersonType.
UserFieldServiceMonthlyReportType (S-21 post-2023)
from jw_core.models_organized import UserFieldServiceMonthlyReportType
report = UserFieldServiceMonthlyReportType.model_validate({
"report_date": "2026-06",
"report_data": {
"deleted": False,
"updatedAt": "2026-06-30T23:59:00Z",
"shared_ministry": True,
"hours": {
"field_service": {"daily": "0", "monthly": "12"},
"credit": {"daily": "0", "monthly": "2"},
},
"bible_studies": {"daily": 0, "monthly": 3, "records": ["uid-a", "uid-b"]},
"comments": "",
"record_type": "monthly",
"status": "submitted",
},
})
Notas de la S-21 post-2023:
- Publicadores no-pioneros reportan solo bible_studies y did-something.
hours.field_service.monthlyqueda en"0". - Pioneros sí reportan horas como string (legacy, evita float drift).
status:"pending"→"submitted"→"confirmed"por el secretary.
Re-exports vs. duplicación
F51 NO migra models_meeting.py ni ministry/field_report.py a usar
estos modelos directamente. Sus formas siguen siendo apropiadas para:
WorkbookWeek(contenido del workbook semana JW): no es schedule, es contenido de la publicación.MonthlyReportlocal: aggregate keyed por columnas SQLite del store local; no necesita CRDT envelopes.
En cambio, F55.6 añade un bridge converter (organized_bridge.py):
to_organized_monthly_report(local_report, *, pioneer, status, ...)
convierte cuando hace falta interoperar.
Tests
packages/jw-core/tests/test_organized_schemas.py (10 tests):
WeekyAssignmentCodenuméricos verbatim TS.Timestamped[T]envelope JSON correcto.PersonTypebuild desde minimal payload._deletedalias preservado enmodel_dump(by_alias=True).MeetingAttendanceTypecon 5 semanas siempre.FieldServiceGroupTypecon members.UserFieldServiceMonthlyReportTypecon status submitted.SchedWeekTypeskeleton mínimo válido.
Crédito y licencia
Schemas portados de sws2apps/organized-app src/definition/
(TypeScript, MIT). El runtime React/Firebase/IndexedDB NO se porta — el
toolkit habla solo el formato de datos.
Ver README.md raíz para atribución completa.
Edit this page on docs/guias/organized-app-schemas.md