# M&EL Module

Implements PRD §5.3 — "M&E and feedback (MFL)". Four submodules:

| Submodule | Purpose | Key models |
|---|---|---|
| `indicators` | Results hierarchy + SMART indicators + baselines/targets + data points | `LogFrame`, `LogFrameRow`, `Indicator`, `IndicatorTarget`, `DataPoint`, `RagThreshold`, `IndicatorVariance`, `OutcomeAssessment`, `ImpactEvaluation` |
| `tracking` | Upstream event log + Activities + Outputs + Corrective actions | `UpstreamEvent`, `Activity`, `ActivityDependency`, `OutputDeliverable`, `CorrectiveAction` |
| `reports` | Versioned, audit-trailed reports exported as PDF / XLSX / HTML + distribution | `ReportTemplate`, `Report`, `ReportArtifact`, `ReportDistribution`, `ComplianceAlert` |
| `feedback` | Stakeholder / beneficiary / alumni / course feedback loop | `FeedbackChannel`, `FeedbackSubmission`, `TracerStudy`, `TracerResponse`, `CourseEvaluation` |

## RAG thresholds

Per-period progress is traffic-light-coded using `percent = actual / target * 100`.

- Default: `MEL_RAG_DEFAULT = {"green": 85, "amber": 60}` (set in `config/settings/base.py`).
- Override per logframe via `RagThreshold` — `green_min > amber_min` enforced at `clean()`.
- Service: `apps.mel.indicators.services.get_rag_thresholds(logframe)` returns the effective pair.

## Alert routes

The alert backbone is signal-driven and terminates in `apps.core.notifications.services.send_notification`.

| Emitted signal | Source | Receiver action |
|---|---|---|
| `mel_data_point_recorded` | `record_data_point` (on commit) | Re-score RAG; emit `mel_indicator_threshold_crossed` |
| `mel_indicator_threshold_crossed` | `on_data_point_recorded` receiver | On RED → `dispatch_indicator_alert` → notify responsible + logframe owner (`MEL_INDICATOR_OFF_TRACK` verb) |
| `mel_activity_delayed` | `record_activity_progress` (when status → DELAYED), or `scan_delayed_activities` beat task | Notify responsible + logframe owner (`MEL_ACTIVITY_DELAYED` verb) |
| `mel_report_published` | `publish_report` (on commit) | Feedback submodule seeds a `ReportReview` channel for the published report |

High- and critical-severity `FeedbackSubmission` rows whose channel has a subject auto-create a `CorrectiveAction` linked to that subject (Report, Indicator, Activity, …).

## Export formats

Every report is rendered to PDF, XLSX, and HTML via:

- `apps.mel.reports.services.render_pdf` (WeasyPrint → `apps.core.utils.pdf`).
- `apps.mel.reports.services.render_xlsx` (openpyxl directly).
- `apps.mel.reports.services.render_html_artifact` (Django template `reports/_report_body.html`).
- `apps.mel.reports.services.render_all` runs all three; PDF failures (missing native deps) degrade gracefully.

Artifacts are stored under `MEDIA/mel/reports/…` with a SHA-256 checksum on `ReportArtifact.checksum`.

## Distribution

`apps.mel.reports.services.distribute_report` creates `ReportDistribution` rows and enqueues email via `apps.core.notifications.services.send_notification`. Scheduled quarterly generation runs through the `apps.mel.reports.generate_scheduled_reports` Celery task; assign a `django-celery-beat` schedule to it.

## REST API

Aggregate router at `/mel/api/v1/` (see `apps/mel/api.py`). Every submodule exposes a read-only `ReadOnlyModelViewSet`. Mutations go through the service layer to keep idempotency, signal emission, and audit trails consistent.

Auth: session + DRF permission classes in `apps.mel.permissions` (`IsMelOfficer`, `CanViewMelData`) mirroring `RoleRequiredMixin`.

## How `FeedbackChannel`s are created

Four paths — use the right one for the job:

| Path | Who | When | Notes |
|---|---|---|---|
| **`seed_mel` management command** | Ops / deployment | On first boot (and re-runs) | Creates the 5 standing channels (`beneficiary-voice`, `industry-partners`, `alumni-pulse`, `rep-course-evaluations`, `mel-suggestions`) plus sample submissions. Idempotent. |
| **`mel_report_published` receiver** | System | Automatically when a Report is published | `apps/mel/feedback/receivers.py::on_report_published` calls `ensure_report_review_channel(report)` — one ReportReview channel per published report, so stakeholders can comment on that specific version. |
| **Officer UI** — `/mel/feedback/channels/new/` | MEL officers / admins | On demand, for surveys or intake windows | Preferred path for non-report channels (new beneficiary cohort, tracer follow-up, industry roundtable). Token is auto-generated. |
| **Django admin** | System admins only | Edge cases | Only needed when you must attach a Generic-FK subject other than a Report (e.g. bind a channel to a specific Indicator or Activity) — the officer UI intentionally doesn't expose that field. |

## Default report templates

`seed_mel` provisions five templates (all idempotent):

| Slug | Type | Schedule | Sections |
|---|---|---|---|
| `ruforum-quarterly-portfolio` | Quarterly | 06:00 on 1st of every 3rd month | indicator rollup, activity summary, variance table, output snapshot, compliance |
| `ruforum-annual-donor` | Annual | 07:00 on 10 Jan | indicator rollup, variance table, output snapshot, compliance |
| `ruforum-thematic-spotlight` | Thematic | On demand | indicator rollup, variance table |
| `ruforum-project-closeout` | Project | On demand | full set minus finance |
| `ruforum-compliance-watch` | Ad-hoc | 05:00 every Monday | compliance, variance table |

`ruforum-quarterly-portfolio` is auto-generated for the current quarter on first seed, rendered to PDF / XLSX / HTML, and transitioned to PUBLISHED — which in turn seeds its ReportReview feedback channel, giving the full loop a working example end-to-end.

Cron expressions are honoured by the `apps.mel.reports.generate_scheduled_reports` Celery beat task; assign a `django-celery-beat` `PeriodicTask` pointing at that task at deploy time.

## Running tests

```sh
docker compose exec web pytest apps/mel -q
```

Tests covering signal-based flows (`mel_data_point_recorded`, `mel_report_published`) use `@pytest.mark.django_db(transaction=True)` so on-commit callbacks fire.
