Statistieken (Backend)
Technisch Ontwerp
De backend van de module Statistieken in Remedice is een aggregatielaag over de overige modules (medicatiebeoordelingen, nieuws, agenda, werkafspraken, Atlas en nazendingen). Per kalenderjaar levert de module een dashboard met tellingen, status- en maandverdelingen, rol- en categorie-uitsplitsingen, en een Excel-export.
De module houdt twee eigen tabellen bij in plaats van alles live te berekenen:
YearlyStatisticis een ledger met een teller per(jaar, item_type). Voor gebeurtenissen die zich verspreid over de codebase voordoen (nieuws geplaatst, agendapunt aangemaakt, Atlas-vraag gesteld, nazending toegevoegd) wordt de teller opgehoogd via signalen, zodat het dashboard geen brede live-aggregaties hoeft te draaien.ReviewStatisticsRecordis een gedenormaliseerde feitentabel: per reviewpatient een rij met aanmaakmaand, soort review en huidige status. Hierdoor blijven reviewstatistieken correct ook nadat de bronreview is opgeruimd.
De kernlogica staat in services.py. get_year_statistics(tenant, year) cachet het resultaat 1 uur per tenant en jaar (cache-sleutel stats_{schema_name}_{year}, STATS_CACHE_TTL = 3600) en stelt het dashboard samen uit drie deelfuncties:
_compute_review_block(year)leest uitsluitend uitReviewStatisticsRecord(gefilterd opcreated_year) en berekent in een doorpas de statusverdeling, de twaalf-maands-reeks, de eerste actieve maand en de jaartotalen (totaal, patient, afdeling). Verwijderde reviews (gemarkeerd viasource_deleted_at) tellen bewust mee; statistieken zijn permanent._compute_users_block(tenant)telt actieve gebruikers en de verdeling per rol._compute_content_block(year)haalt nieuws, agenda, werkafspraken, Atlas-vragen en nazendingen uit deYearlyStatistic-ledger (jaartelling of cumulatief) en vult de categorie-uitsplitsingen aan met live queries.
De synchronisatie tussen de bronmodules en deze twee tabellen verloopt via signals.py en review_statistics.py. Een nieuw NewsItem, AgendaItem, WorkAgreementItem, ChatMessage (alleen role="user") of Nazending hoogt de bijbehorende YearlyStatistic-teller op en invalideert de cache. Een nieuwe of gewijzigde ReviewPatient roept ensure_review_statistics_record aan; een verwijderde ReviewPatient roept mark_review_statistics_record_deleted aan, die source_deleted_at zet zonder de rij te verwijderen.
De export genereert via openpyxl een Excel-bestand (.xlsx) in geheugen en streamt het direct terug als HTTP-response.
flowchart TD
A[GET /api/statistieken/] --> B[YearStatisticsView.get]
B --> C{cache hit? stats_schema_year}
C -- ja --> Z[JSON Response]
C -- nee --> D[_compute_year_statistics]
D --> E[_compute_review_block: ReviewStatisticsRecord]
D --> F[_compute_users_block: member_users_qs + Role]
D --> G[_compute_content_block: YearlyStatistic + live queries]
E --> H[cache.set TTL 3600]
F --> H
G --> H
H --> Z
I[ReviewPatient/NewsItem/AgendaItem/... post_save] --> J[signals]
J --> K[YearlyStatistic.increment of ensure_review_statistics_record]
J --> L[invalidate cache]
M[GET /api/statistieken/export/] --> N[ExportXlsxView.get]
N --> O[get_all_patients_for_export]
N --> P[get_afdeling_history_for_export]
O --> Q[build_statistieken_xlsx: openpyxl, 2 sheets]
P --> Q
Q --> R[xlsx HttpResponse]
Datamodel (ERD)
De module beheert twee persistente tabellen en aggregeert daarnaast data uit andere modules.
erDiagram
YearlyStatistic {
bigint id PK
smallint year
varchar item_type
int count
}
ReviewStatisticsRecord {
bigint id PK
uuid source_review_patient_id UK
uuid source_review_id FK
smallint created_year
smallint created_month
varchar review_kind
varchar current_status
datetime review_created_at
datetime source_deleted_at "nullable, soft-delete marker"
datetime last_status_updated_at
}
ReviewStatisticsRecord }o--|| ReviewPatient : "denormaliseert"
ReviewStatisticsRecord }o--|| MedicatieReview : "denormaliseert"
YearlyStatistic ||--o{ NewsItem : "telt"
YearlyStatistic ||--o{ AgendaItem : "telt"
YearlyStatistic ||--o{ WorkAgreementItem : "telt cumulatief"
YearlyStatistic ||--o{ ChatMessage : "telt (atlas_question)"
YearlyStatistic ||--o{ Nazending : "telt"
Statistieken ||--o{ Role : "rolverdeling"
Statistieken ||--o{ User : "actieve gebruikers"
YearlyStatistic heeft een unieke constraint op (year, item_type). Ophogen verloopt via de classmethod YearlyStatistic.increment(year=..., item_type=...), die atomisch werkt via get_or_create gevolgd door een update met F("count") + 1.
Ondersteunde item_type-waarden:
newsagendawork_agreementatlas_questionnazending
ReviewStatisticsRecord heeft een unieke source_review_patient_id (een rij per reviewpatient), een geindexeerde source_review_id, een CheckConstraint op created_month tussen 1 en 12, en indexen op (created_year, created_month), (created_year, review_kind) en (created_year, current_status). review_kind is patient of afdeling; current_status is in_voorbereiding, voorbereid of uitgevoerd.
API & Communicatie
- GET /api/statistieken/: Geeft de geaggregeerde statistieken voor een jaar (query-parameter
year, standaard het huidige jaar). Retourneert eenYearStatisticsSerializer-response. - GET /api/statistieken/years/: Geeft een aflopend gesorteerde lijst van jaren waarvoor data bestaat, op basis van de unie van jaren in
MedicatieReview,ReviewStatisticsRecordenYearlyStatistic. Het huidige jaar wordt altijd toegevoegd als het ontbreekt. - GET /api/statistieken/export/: Genereert een Excel-bestand (.xlsx) voor het opgegeven jaar (query-parameter
year) met twee tabbladen.Patiëntenheeft de kolommen Patiënt ID, Naam, Geboortedatum, Leeftijd, Status, Review ID, Review aangemaakt op, Reviewer, Afdeling en AIS bron.Afdelingenheeft de kolommen Afdeling, Review ID, Aangemaakt op, Reviewer, Status, AIS bron en Aantal patiënten. De kopregel is vetgedrukt op een donkere achtergrond met bevroren bovenste rij. Celwaarden die met een formuletoken beginnen worden via_safevoorzien van een aanhalingsteken (bescherming tegen CSV-injectie). De response heeftContent-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheeten eenContent-Disposition-header met bestandsnaamstatistieken_{year}.xlsx.
Response-structuur GET /api/statistieken/
month in reviewsPerMonth is nul-geindexeerd (0 = januari, 11 = december) en bevat altijd twaalf punten; firstActiveMonth is een-geindexeerd (1 = januari) of null als er geen reviews zijn.
{
"year": 2025,
"firstActiveMonth": 2,
"reviewStatusCounts": {
"inPreparation": 12,
"prepared": 34,
"performed": 78
},
"reviewsPerMonth": [
{ "month": 0, "label": "Jan", "count": 5, "countPatient": 3, "countAfdeling": 2 }
],
"users": {
"total": 8,
"byRole": [{ "label": "Apotheker", "value": 4 }]
},
"news": {
"total": 14,
"byCategory": [{ "label": "Farmacotherapeutisch", "value": 9 }]
},
"agendaItems": 22,
"workAgreements": {
"total": 47,
"byCategory": [{ "label": "Interacties", "value": 15 }]
},
"atlasQuestions": 103,
"nazendingen": {
"currentYear": 31,
"total": 96
},
"reviews": 124,
"reviewsPatient": 56,
"reviewsAfdeling": 68
}
Foutafhandeling & Statuscodes
400 Bad Request: Niet van toepassing. Een ongeldig of ontbrekendyear-formaat valt stilzwijgend terug op het huidige jaar; de waarde wordt geklemd op het bereik 2020 tot en met het volgende jaar.403 Forbidden: Als de gebruiker niet over de permissiestatistieken.viewbeschikt.
Autorisatie & Beveiliging
- Permissie:
statistieken.viewis vereist voor alle drie de endpoints, afgedwongen viaHasPermissionCodein depermission_classesvan elke view. Er is geen step-up vereist. - Authenticatie: Alle endpoints vereisen
IsAuthenticated. - Tenant-isolatie: Statistieken worden berekend binnen het database-schema van de actieve tenant (django-tenants). Het
User-model heeft geen directe tenant-FK; actieve gebruikers worden daarom geteld viamember_users_qs(tenant)(op basis van lidmaatschap) en de rolverdeling viaRole.objects.filter(tenant=tenant).
Bestandsstructuur & Verantwoordelijkheden
backend/features/statistieken/models.py: DefinieertYearlyStatistic(met deItemType-enum en deincrement-classmethod) enReviewStatisticsRecord.backend/features/statistieken/services.py: Bevatget_year_statistics(met caching), de deelfuncties_compute_review_block,_compute_users_blocken_compute_content_block,get_available_years, de exportbronnenget_all_patients_for_exportenget_afdeling_history_for_export, en de Excel-builderbuild_statistieken_xlsx.backend/features/statistieken/review_statistics.py: Synchroniseert reviewpatienten naarReviewStatisticsRecord(ensure_review_statistics_record,sync_patient_status,sync_bulk_review_status,mark_review_statistics_record_deleted).backend/features/statistieken/signals.py: HoogtYearlyStatisticop en invalideert de cache bij wijzigingen in nieuws, agenda, werkafspraken, Atlas-berichten, nazendingen en reviewpatienten.backend/features/statistieken/views.py: BevatYearStatisticsView,AvailableYearsViewenExportXlsxView.backend/features/statistieken/serializers.py:YearStatisticsSerializermet de geneste serializers voor maandpunten, rol- en categorie-uitsplitsingen.backend/features/statistieken/apps.py: Importeert de signalen in deready-hook.backend/features/statistieken/urls.py: Definieert de routes"","years/"en"export/"onder/api/statistieken/.
Belangrijke bestanden
backend/features/statistieken/services.pybackend/features/statistieken/review_statistics.pybackend/features/statistieken/signals.pybackend/features/statistieken/models.pybackend/features/statistieken/views.py