Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6c8a79e31 | ||
|
|
2419e6bf01 | ||
|
|
3ab9127548 |
@@ -2,6 +2,15 @@
|
||||
|
||||
Alle betekenisvolle wijzigingen worden hier bijgehouden. Het project volgt voorlopig een pre-1.0 semantische versieaanpak.
|
||||
|
||||
## 0.3.16 - 2026-08-12
|
||||
|
||||
### Hersteld
|
||||
|
||||
- De publieke demo herstelt bij iedere nieuwe demosessie zes actuele fictieve vacatures, hun laatste scores en twee toekomstige opvolgmomenten zonder duplicaten of onbeperkte scorehistoriek.
|
||||
- Negen fictieve `example.test`-platformmailboxen maken de mailboxflow zichtbaar zonder echte accounts, credentials of externe polls.
|
||||
- Mailboxingress wordt uitsluitend door de mailboxscheduler verwerkt; e-mailbronnen komen niet meer in URL-fetchplanning, dashboardscanstatus of de algemene bronrunactiviteit terecht.
|
||||
- Statuscopy onderscheidt een actief zoekprofiel van operationele brongezondheid en het lege loginwachtwoordveld lijkt niet langer vooraf ingevuld.
|
||||
|
||||
## 0.3.14 - 2026-07-29
|
||||
|
||||
### Gewijzigd
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
VacatureRadar is een persoonlijke vacature-assistent voor België. De applicatie volgt toegestane werkgeversbronnen, ATS-pagina’s, feeds en vacaturemails, verwijdert dubbels en irrelevante resultaten en rangschikt de overblijvende kansen tegen jouw zoekprofiel. Jij behoudt altijd de controle: VacatureRadar helpt zoeken en opvolgen, maar solliciteert nooit automatisch.
|
||||
|
||||
**Actuele release: 0.3.16.** De volledige P0/P1-releasebasis en de publieke
|
||||
demo-hardening zijn afgerond: 327 tests zijn groen, Microsoft 365 verwerkt platformmails via
|
||||
app-only OAuth/XOAUTH2 en de productie-installatie draait vanaf een immutable
|
||||
image in de private Gitea-containerregistry. Het controleerbare acceptatiebewijs
|
||||
staat in [Finale acceptance](docs/audit/FINAL_ACCEPTANCE.md).
|
||||
|
||||

|
||||
|
||||
## Voor wie is dit?
|
||||
@@ -37,12 +43,15 @@ VacatureRadar is bedoeld voor één persoon of een eigen, afgeschermde installat
|
||||
| Vacatureverkenner | Zoeken en filteren met een uitlegbare detailanalyse naast de resultaten |
|
||||
| Skillsradar | Actuele marktvraag naast de skills uit je eigen profiel |
|
||||
| Sollicitatiepipeline | Dossiers, status, notities, deadlines en een auditbare tijdlijn |
|
||||
| Bronnenbeheer | Veilige bronconfiguratie, gezondheid, handmatige import en mailboxkoppelingen |
|
||||
| Bronnenbeheer | Veilige bronconfiguratie, gezondheid, handmatige import en afzonderlijke mailboxkoppelingen voor negen jobplatformen |
|
||||
| Automatisering | Eerlijke 24-uursmetingen; niet-gemeten informatie blijft expliciet onbekend |
|
||||
|
||||
## Bekijk de volledige productflow
|
||||
|
||||
De beelden komen uit de lokale demo-omgeving. De getoonde cijfers zijn omgevingsdata, geen marketingclaims. Klik op een beeld voor de volledige resolutie.
|
||||
De beelden komen uit een gesaneerde lokale demo-omgeving. De getoonde aantallen
|
||||
zijn een momentopname van die demo en bewijzen geen productie-status; daarvoor
|
||||
geldt uitsluitend de gedateerde [finale acceptance](docs/audit/FINAL_ACCEPTANCE.md).
|
||||
Klik op een beeld voor de volledige resolutie.
|
||||
|
||||
### 1. Vind de relevante kansen
|
||||
|
||||
@@ -78,7 +87,7 @@ Een analyse maakt score, datakwaliteit, beslissignalen, aandachtspunten en broni
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><strong>Sollicitatiepipeline</strong><br><sub>Bewaar status, deadlines, notities en tijdlijn per dossier; de uiteindelijke sollicitatie blijft altijd handmatig.</sub></td>
|
||||
<td width="50%"><strong>Bronnenbeheer</strong><br><sub>Zie policyreview, laatste scans, volgende runs en brongezondheid zonder technische logs te moeten doorzoeken.</sub></td>
|
||||
<td width="50%"><strong>Bronnenbeheer</strong><br><sub>Zie policyreview, laatste scans, volgende runs, brongezondheid en de status van iedere platformmailbox zonder technische logs te moeten doorzoeken.</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="docs/screenshots/readme/applications-dark.png"><img src="docs/screenshots/readme/applications-dark.png" alt="Sollicitatiepipeline met persoonlijke dossiers"></a></td>
|
||||
@@ -141,7 +150,7 @@ Na de eerste login:
|
||||
|
||||
1. Open **Zoekprofiel** en stel regio, radius, rollen, skills en werkvoorkeuren in.
|
||||
2. Open **Bronnen** en activeer alleen bronnen waarvan beleid en voorwaarden beoordeeld zijn.
|
||||
3. Configureer optioneel een eigen vacaturemailbox; credentials worden niet in de repository opgeslagen.
|
||||
3. Koppel optioneel afzonderlijke vacaturemailboxen voor VDAB, Indeed, LinkedIn, ictjob.be, Jobat, StepStone, Careerjet, Randstad en Robert Half. Credentials worden niet in de repository opgeslagen.
|
||||
4. Controleer **Automatisering** voordat je op onbemande verwerking vertrouwt.
|
||||
5. Verifieer in productie altijd HTTPS, back-ups, herstel en healthchecks.
|
||||
|
||||
@@ -158,6 +167,12 @@ De publieke URL wordt vervolgens gebruikt voor `ALLOWED_HOSTS`, CSRF, secure coo
|
||||
|
||||
Publiceer poort `1226` niet rechtstreeks op internet. Gebruik een beheerde reverse proxy met TLS en volg [Unraid-deployment](docs/operations/UNRAID_DEPLOYMENT.md) en de concrete [Nginx Proxy Manager-handleiding](docs/operations/NGINX_PROXY_MANAGER.md). Een volledig productievoorbeeld staat in [`deployment/production.env.example`](deployment/production.env.example).
|
||||
|
||||
Voor Microsoft 365 gebruikt VacatureRadar uitsluitend app-only OAuth/XOAUTH2
|
||||
voor IMAP en SMTP; Basic Auth en Microsoft 365-app-wachtwoorden worden niet
|
||||
ondersteund. Beperk de Entra-serviceprincipal tot de bedoelde mailbox en geef
|
||||
alleen `IMAP.AccessAsApp` en `SMTP.SendAsApp`. De exacte configuratie- en
|
||||
rotatiestappen staan in het [operationele runbook](docs/operations/RUNBOOK.md).
|
||||
|
||||
## Eerlijke grenzen
|
||||
|
||||
VacatureRadar:
|
||||
@@ -193,6 +208,11 @@ Die controleert formatting, lint, Django-configuratie, migraties, de volledige t
|
||||
|
||||
## Beheer en deployment
|
||||
|
||||
Productiereleases worden door Gitea Actions gebouwd, gescand en als private
|
||||
containerimage gepubliceerd. Pin Unraid op `versie@sha256:digest`, bewaar de
|
||||
vorige digest voor rollback en gebruik nooit alleen een veranderlijke tag als
|
||||
productiereferentie.
|
||||
|
||||
- [Lokaal ontwikkelen](docs/operations/LOCAL_DEVELOPMENT.md)
|
||||
- [Docker en Unraid uitrollen](docs/operations/UNRAID_DEPLOYMENT.md)
|
||||
- [Nginx Proxy Manager configureren](docs/operations/NGINX_PROXY_MANAGER.md)
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.urls import reverse
|
||||
|
||||
from apps.notifications.models import DigestOutbox, ReminderOutbox
|
||||
from apps.profiles.models import ProfileRevision
|
||||
from apps.sources.models import SourceRun
|
||||
from apps.sources.models import Source, SourceRun
|
||||
|
||||
from ..models import ApplicationTimelineEvent, Feedback, JobVersion
|
||||
|
||||
@@ -37,7 +37,8 @@ ACTIVITY_FILTERS = (
|
||||
|
||||
def _source_events() -> list[ActivityEvent]:
|
||||
events = []
|
||||
for run in SourceRun.objects.select_related("source").order_by("-started_at")[:100]:
|
||||
runs = SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
|
||||
for run in runs.select_related("source").order_by("-started_at")[:100]:
|
||||
detail = (
|
||||
f"{run.extracted_count} geëxtraheerd · {run.created_count} nieuw · "
|
||||
f"{run.duplicate_count} duplicaat"
|
||||
|
||||
@@ -57,7 +57,8 @@ def annotate_latest_profile_score(
|
||||
|
||||
def _recent_dashboard_activity(user: User) -> list[dict[str, Any]]:
|
||||
activity: list[dict[str, Any]] = []
|
||||
for run in SourceRun.objects.select_related("source").order_by("-started_at")[:4]:
|
||||
source_runs = SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
|
||||
for run in source_runs.select_related("source").order_by("-started_at")[:4]:
|
||||
activity.append(
|
||||
{
|
||||
"occurred_at": run.started_at,
|
||||
@@ -154,7 +155,9 @@ def build_dashboard_cockpit(user: User, profile: SearchProfile | None) -> dict[s
|
||||
"source_counts": source_counts,
|
||||
"recent_activity": _recent_dashboard_activity(user),
|
||||
"day_conclusion": day_conclusion,
|
||||
"latest_scan": SourceRun.objects.order_by("-started_at").first(),
|
||||
"latest_scan": SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
|
||||
.order_by("-started_at")
|
||||
.first(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -122,11 +122,10 @@ def demo_environment_seeded(user) -> bool:
|
||||
|
||||
|
||||
def seed_demo_environment(user) -> None:
|
||||
"""Maak (idempotent) een fictieve demo-omgeving voor ``user``."""
|
||||
if demo_environment_seeded(user):
|
||||
return
|
||||
|
||||
"""Maak of herstel een fictieve, tijdsbestendige demo-omgeving voor ``user``."""
|
||||
from apps.jobs.models import Application, Employer, JobPosting, ScoreRun
|
||||
from apps.sources.models import MailboxConnection, Source, SourceRun
|
||||
from apps.sources.platforms import PLATFORM_ALERTS
|
||||
|
||||
profile, _ = SearchProfile.objects.get_or_create(
|
||||
user=user,
|
||||
@@ -185,7 +184,7 @@ def seed_demo_environment(user) -> None:
|
||||
},
|
||||
)
|
||||
slug = _digest("demo-job", str(index), title, employer_name)
|
||||
job, job_created = JobPosting.objects.get_or_create(
|
||||
job, _ = JobPosting.objects.update_or_create(
|
||||
canonical_key=slug,
|
||||
defaults={
|
||||
"employer": employer,
|
||||
@@ -221,6 +220,11 @@ def seed_demo_environment(user) -> None:
|
||||
)
|
||||
created_jobs.append(job)
|
||||
|
||||
ScoreRun.objects.filter(
|
||||
job=job,
|
||||
profile=profile,
|
||||
model_version="demo-seed",
|
||||
).delete()
|
||||
ScoreRun.objects.create(
|
||||
job=job,
|
||||
profile=profile,
|
||||
@@ -244,7 +248,7 @@ def seed_demo_environment(user) -> None:
|
||||
|
||||
# Een kleine sollicitatiepijplijn voor de demo.
|
||||
if created_jobs:
|
||||
Application.objects.get_or_create(
|
||||
Application.objects.update_or_create(
|
||||
user=user,
|
||||
job=created_jobs[0],
|
||||
defaults={
|
||||
@@ -257,7 +261,7 @@ def seed_demo_environment(user) -> None:
|
||||
},
|
||||
)
|
||||
if len(created_jobs) > 3:
|
||||
Application.objects.get_or_create(
|
||||
Application.objects.update_or_create(
|
||||
user=user,
|
||||
job=created_jobs[3],
|
||||
defaults={
|
||||
@@ -267,3 +271,42 @@ def seed_demo_environment(user) -> None:
|
||||
"notes": "Sollicitatie verstuurd. Demo-data.",
|
||||
},
|
||||
)
|
||||
|
||||
# Platformmailboxen zijn zichtbare ingressconfiguratie, geen crawlbare URL-bronnen.
|
||||
# De demo gebruikt uitsluitend fictieve adressen en wordt door de scheduler overgeslagen.
|
||||
for platform in PLATFORM_ALERTS:
|
||||
MailboxConnection.objects.update_or_create(
|
||||
user=user,
|
||||
platform=platform,
|
||||
defaults={
|
||||
"provider": MailboxConnection.Provider.OUTLOOK,
|
||||
"username": "alerts@example.test",
|
||||
"mailbox": f"VacatureRadar/{platform}",
|
||||
"enabled": True,
|
||||
"poll_interval_minutes": MailboxConnection.PollInterval.THIRTY_MINUTES,
|
||||
"next_poll_at": now + timedelta(minutes=30),
|
||||
"last_polled_at": now - timedelta(minutes=5),
|
||||
"last_success_at": now - timedelta(minutes=5),
|
||||
"last_error_category": "",
|
||||
"last_error_message": "",
|
||||
"lease_token": "",
|
||||
"lease_expires_at": None,
|
||||
},
|
||||
)
|
||||
source, _ = Source.objects.update_or_create(
|
||||
source_type=Source.Type.EMAIL,
|
||||
domain=f"{platform}.mailbox.local",
|
||||
defaults={
|
||||
"name": f"{PLATFORM_ALERTS[platform].label} vacaturemailbox",
|
||||
"base_url": "",
|
||||
"status": Source.Status.ACTIVE,
|
||||
"policy": Source.Policy.ALLOW,
|
||||
"policy_reason": "Mailboxingress; niet via URL-fetch gepland.",
|
||||
"parser_key": "email-alert",
|
||||
"failure_count": 0,
|
||||
"last_failure_at": None,
|
||||
"next_run_at": None,
|
||||
"metadata": {"demo": True, "mailbox_ingress": True},
|
||||
},
|
||||
)
|
||||
SourceRun.objects.filter(source=source).delete()
|
||||
|
||||
@@ -188,7 +188,8 @@ def schedule_due_sources() -> dict[str, int]:
|
||||
now = timezone.now()
|
||||
due = Source.objects.filter(
|
||||
status__in=[Source.Status.ACTIVE, Source.Status.TRIAL],
|
||||
).filter(Q(next_run_at__isnull=True) | Q(next_run_at__lte=now))
|
||||
).exclude(source_type=Source.Type.EMAIL)
|
||||
due = due.filter(Q(next_run_at__isnull=True) | Q(next_run_at__lte=now))
|
||||
ids = list(due.values_list("id", flat=True).distinct()[:200])
|
||||
for source_id in ids:
|
||||
fetch_source.delay(source_id)
|
||||
@@ -200,6 +201,7 @@ def schedule_mailbox_polls() -> dict[str, int]:
|
||||
now = timezone.now()
|
||||
due_ids = list(
|
||||
MailboxConnection.objects.filter(enabled=True, next_poll_at__lte=now)
|
||||
.exclude(user__username=settings.DEMO_USERNAME)
|
||||
.filter(Q(lease_expires_at__isnull=True) | Q(lease_expires_at__lte=now))
|
||||
.values_list("id", flat=True)[:100]
|
||||
)
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ CSRF_TRUSTED_ORIGINS = merge_unique(
|
||||
)
|
||||
TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "Europe/Brussels")
|
||||
VACATURERADAR_OWNER_NAME = os.getenv("VACATURERADAR_OWNER_NAME", "Jens").strip()[:40] or "Jens"
|
||||
VACATURERADAR_VERSION = os.getenv("VACATURERADAR_VERSION", "0.3.14").strip() or "0.3.14"
|
||||
VACATURERADAR_VERSION = os.getenv("VACATURERADAR_VERSION", "0.3.16").strip() or "0.3.16"
|
||||
LANGUAGE_CODE = "nl-be"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
@@ -9,7 +9,7 @@ DJANGO_ALLOWED_HOSTS=vacatureradar.example.be,127.0.0.1,localhost
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://vacatureradar.example.be
|
||||
DJANGO_TIME_ZONE=Europe/Brussels
|
||||
VACATURERADAR_OWNER_NAME=Jens
|
||||
VACATURERADAR_VERSION=0.3.14
|
||||
VACATURERADAR_VERSION=0.3.16
|
||||
|
||||
POSTGRES_DB=vacatureradar
|
||||
POSTGRES_USER=vacatureradar
|
||||
|
||||
+57
-11
@@ -1,6 +1,6 @@
|
||||
schema_version: 1
|
||||
project: VacatureRadar
|
||||
updated_at: '2026-07-29'
|
||||
updated_at: '2026-08-12'
|
||||
execution_policy:
|
||||
order: file-order, then first ready task whose dependencies are done
|
||||
no_product_questions: true
|
||||
@@ -1341,7 +1341,7 @@ tasks:
|
||||
gebruikt strikt de gepinde certifi-bundel zonder bypass.
|
||||
- id: VR-202
|
||||
title: Live IMAP- en SMTP-credentials configureren
|
||||
status: blocked-external
|
||||
status: done
|
||||
priority: P0
|
||||
requirement_ids:
|
||||
- PR-016
|
||||
@@ -1361,17 +1361,19 @@ tasks:
|
||||
- Dezelfde live testmail importeert per mailbox idempotent en alle geconfigureerde intervallen worden door de
|
||||
scheduler gerespecteerd.
|
||||
- SMTP verstuurt één testdigest en outboxstatus klopt.
|
||||
- OAuth-clientcredentials en de encryptiesleutel staan alleen in de productie-secretstore/.env; eventuele
|
||||
Gmail/custom-app-wachtwoorden zijn als ciphertext opgeslagen en sleutelrotatie is live getest.
|
||||
- OAuth-clientcredentials en de encryptiesleutel staan alleen in de productie-secretstore/.env; eventuele Gmail/custom-app-wachtwoorden
|
||||
zijn als ciphertext opgeslagen en sleutelrotatie is live getest.
|
||||
verification:
|
||||
- Volg Unraid mailboxchecklist
|
||||
- Inspecteer logs op afwezigheid van secret/payload
|
||||
primary_paths:
|
||||
- /mnt/user/appdata/vacatureradar/config/.env
|
||||
- /mnt/user/appdata/VacatureRadar/.env
|
||||
- Bronnen -> Platformmailboxen
|
||||
external_blocker: De dedicated productie-alias, Entra-appregistratie, admin consent en Exchange-mailboxscope zijn
|
||||
op 2026-07-30 live ingericht; resterend zijn de productie-uitrol en echte platformalertmails voor de
|
||||
import-/digestproef.
|
||||
result:
|
||||
completed_at: '2026-07-30'
|
||||
note: M365 app-only OAuth/XOAUTH2 live ingericht met dedicated alias/map en Exchange-regel; negen platformmailboxen
|
||||
importeerden dezelfde live testmail idempotent, schedulerstatus was correct, testdigest stond sent en logs
|
||||
bleven payload-/secretvrij.
|
||||
- id: VR-203
|
||||
title: Productiedomein, TLS en reverse proxy activeren
|
||||
status: done
|
||||
@@ -1403,7 +1405,7 @@ tasks:
|
||||
containerintern bereikbaar.
|
||||
- id: VR-204
|
||||
title: Immutable containerimage publiceren
|
||||
status: blocked-external
|
||||
status: done
|
||||
priority: P1
|
||||
requirement_ids:
|
||||
- NFR-009
|
||||
@@ -1421,9 +1423,12 @@ tasks:
|
||||
- Pull image op schone host
|
||||
- Start smoke stack en rollback naar vorige digest
|
||||
primary_paths:
|
||||
- .github/workflows/release.yml
|
||||
- .gitea/workflows/release.yml
|
||||
- docker-compose.unraid.yml
|
||||
external_blocker: Vereist registrykeuze, account en credentials.
|
||||
result:
|
||||
completed_at: '2026-07-30'
|
||||
note: Gitea private registry publiceerde v0.3.15 als immutable digest met commitlabel, SPDX-SBOM, checksums
|
||||
en release notes; Unraid pull, digest-pin, healthy smoke en rollback naar 0.3.14 plus herstel zijn live geslaagd.
|
||||
- id: VR-205
|
||||
title: Gelicenseerde Belgische geodatabron laden
|
||||
status: done
|
||||
@@ -2371,3 +2376,44 @@ tasks:
|
||||
note: 'Release 0.3.14: geïsoleerde PostgreSQL-benchmark reduceert importduur 44,74% en queries minstens 63,73%;
|
||||
twee volledige gates met 318 tests groen; productie healthy op image sha256:1ee84241, transactionele importsmoke
|
||||
teruggerold en rollbackimage 0.3.13 beschikbaar.'
|
||||
- id: VR-229
|
||||
title: Herstel de publieke demo na de visuele 0.3.15-audit
|
||||
status: done
|
||||
priority: P1
|
||||
requirement_ids:
|
||||
- NFR-008
|
||||
- NFR-009
|
||||
depends_on:
|
||||
- VR-228
|
||||
summary: Maak de publieke demo opnieuw representatief en tijdsbestendig, scheid mailboxingress van URL-fetching
|
||||
en laat operationele statussen exact zeggen wat ze meten.
|
||||
acceptance_criteria:
|
||||
- Iedere nieuwe demosessie herstelt zes fictieve actuele vacatures, scores en toekomstige opvolgdatums zonder
|
||||
duplicaten of groeiende ScoreRun-historiek.
|
||||
- De demo toont negen fictieve, actieve platformmailboxkoppelingen zonder secrets of echte persoonsgegevens.
|
||||
- E-mailbronnen worden nooit door de generieke URL-fetchscheduler gepland en oude mailboxpolicyfouten vervuilen
|
||||
dashboard en activiteit niet meer.
|
||||
- Navigatie en dashboard noemen het zoekprofiel expliciet actief zonder brongezondheid te suggereren; het lege
|
||||
loginwachtwoordveld lijkt niet vooraf ingevuld.
|
||||
- De negen kernroutes blijven in dark/light en op mobiel, desktop en ultrawide zonder overflow, consolefouten
|
||||
of headingregressies bruikbaar; de volledige kwaliteitsgate slaagt.
|
||||
verification:
|
||||
- python -m pytest tests/integration/test_auth_login.py tests/integration/test_tasks_and_more_views.py
|
||||
- Browseraudit op 390x844, 1280x600, 1440x900 en 3440x1440
|
||||
- ./scripts/codex_verify.sh
|
||||
primary_paths:
|
||||
- apps/jobs/services/demo_seed.py
|
||||
- apps/jobs/services/activity.py
|
||||
- apps/jobs/services/cockpit.py
|
||||
- apps/sources/tasks.py
|
||||
- templates/base.html
|
||||
- templates/dashboard/today.html
|
||||
- templates/registration/login.html
|
||||
- tests/integration/test_auth_login.py
|
||||
- tests/integration/test_tasks_and_more_views.py
|
||||
result:
|
||||
completed_at: '2026-08-12'
|
||||
note: Publieke demo wordt bij iedere sessie idempotent hersteld met 6 actuele fictieve vacatures, tijdsbestendige
|
||||
opvolging en 9 fictieve mailboxkoppelingen; EMAIL-bronnen en demo-mailboxen zijn uit de verkeerde schedulers
|
||||
en activityfeeds uitgesloten, statuscopy en loginplaceholder zijn gecorrigeerd. Gerichte 51 tests en volledige
|
||||
gate met 327 tests groen, 84,04% dekking.
|
||||
|
||||
+57
-11
@@ -1,7 +1,7 @@
|
||||
# Projectstatus
|
||||
|
||||
- Laatst bijgewerkt: 2026-07-29
|
||||
- Repositoryversie: 0.3.14 importperformance
|
||||
- Laatst bijgewerkt: 2026-08-12
|
||||
- Repositoryversie: 0.3.16 publieke demo-hardening
|
||||
- Uitvoeringsmodus: autonome backlog
|
||||
- Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next`
|
||||
|
||||
@@ -19,6 +19,37 @@ De repository bevat een uitvoerbare Django-MVP met:
|
||||
- Celeryworker/scheduler, healthchecks, Docker/Unraidconfiguratie en back-up-/herstelscripts;
|
||||
- offline unit-, integratie- en securitytests.
|
||||
|
||||
## Laatste documentatie-update
|
||||
|
||||
Visuele demo-hardening `VR-229` op 2026-08-12:
|
||||
|
||||
- iedere publieke demosessie herstelt idempotent zes actuele fictieve vacatures,
|
||||
de laatste scores, twee toekomstige opvolgmomenten en negen geïsoleerde
|
||||
`example.test`-platformmailboxkoppelingen;
|
||||
- mailboxbronnen worden niet meer door de URL-fetchscheduler gepland, de
|
||||
publieke demo wordt uitgesloten van echte mailboxpolling en historische
|
||||
mailboxpolicyfouten vervuilen dashboard en activiteit niet langer;
|
||||
- navigatie en dashboard onderscheiden nu expliciet een actief zoekprofiel van
|
||||
bron- of systeemgezondheid; het lege loginwachtwoordveld heeft geen misleidende
|
||||
mask-placeholder meer;
|
||||
- visuele audit: negen kernroutes groen in dark/light op 390×844, 1280×600,
|
||||
1440×900 en 3440×1440 zonder overflow, heading- of consolefouten;
|
||||
- gerichte regressies: 51 tests groen; volledige gate: 327 tests, 0 failures,
|
||||
0 skips en 84,04% branch-aware dekking. Volgende uitvoerbare taak: geen.
|
||||
|
||||
README-update op 2026-07-30:
|
||||
|
||||
- de repository-ingang benoemt release 0.3.15, de groene 324-testgate, de live
|
||||
M365 app-only OAuth/XOAUTH2-flow en de immutable private Gitea-registryrelease;
|
||||
- eerste configuratie noemt alle negen ondersteunde jobplatformen en verwijst
|
||||
expliciet naar het M365 least-privilege- en rotatiepad in het runbook;
|
||||
- de bestaande negen screenshots zijn visueel herbeoordeeld. De layout is nog
|
||||
actueel; de bronafbeelding toont bewust een gesaneerde demo zonder actieve
|
||||
ingress en is daarom als demo-momentopname verduidelijkt, niet vervangen door
|
||||
een productiebeeld met accountgegevens;
|
||||
- eindverificatie: volledige gate groen met 324 tests, 0 failures, 0 skips en
|
||||
84,07% branch-aware dekking.
|
||||
|
||||
## Laatste productiehardening
|
||||
|
||||
Productieoplevering 0.3.14 op 2026-07-29:
|
||||
@@ -30,9 +61,28 @@ Productieoplevering 0.3.14 op 2026-07-29:
|
||||
- de pre-releasecustomdump, media en composeconfig staan checksum-geldig onder `/mnt/user/appdata/VacatureRadar/backups/release-0.3.14-20260729T164500Z`; rollbackimage `vacatureradar:rollback-0.3.13` wijst naar `sha256:34aef8853f77a3f311b119bd67d9f1160a97fd4a21e62c9776222be289bf3b88`;
|
||||
- productie is healthy zonder restarts, versie 0.3.14 is publiek zichtbaar, alle gastkernroutes antwoorden 200 en een echte importsmoke creëerde één job binnen een transactie waarna rollback expliciet bevestigd werd.
|
||||
|
||||
Volgende uitvoerbare taak: geen na afronding van VR-228. Alleen de optionele
|
||||
mailbox-/identityconfiguratie (`VR-202`) en publicatie naar een externe private
|
||||
registry (`VR-204`) blijven extern geblokkeerd.
|
||||
Productieoplevering 0.3.15 op 2026-07-30:
|
||||
|
||||
- `VR-202` is live afgerond met een dedicated alias, geïsoleerde Exchange-map en
|
||||
inboxregel, app-only OAuth/XOAUTH2 voor IMAP en SMTP en negen platformconnecties;
|
||||
- dezelfde echte testmail werd per connectie eenmaal geïmporteerd en bij herhaling
|
||||
als duplicate herkend; alle ingestelde intervallen zetten `next_poll_at` correct
|
||||
en de scheduler plande geen connectie te vroeg;
|
||||
- de productie-Fernet-sleutel werd live naar een nieuwe primaire MultiFernet-sleutel
|
||||
geroteerd; een tijdelijke versleutelde custom credential bleef ontsleutelbaar en
|
||||
is na de proef verwijderd;
|
||||
- één echte digest werd verzonden en de outbox eindigde op `sent`; productie-logs
|
||||
bevatten geen OAuth-secret, bearer-token of mailpayload;
|
||||
- `VR-204` publiceerde `v0.3.15` naar de private Gitea-registry en Unraid is gepind
|
||||
op `sha256:5a6dc253db7a60c42c0c806ae084ddc6ee7c98db5db71a857423b894a3662ba5`;
|
||||
- commitlabel, digest, SPDX-SBOM, checksums en release notes zijn checksum-geldig
|
||||
opgeslagen als Gitea Actions-artifact `release-evidence-v0.3.15`;
|
||||
- pull, interne en publieke readiness, rollback naar `0.3.14` en herstel naar de
|
||||
immutable `0.3.15`-digest zijn live geslaagd;
|
||||
- lokale en Gitea-releasegate zijn groen met 324 tests, 0 failures, 0 skips en
|
||||
84,05% branch-aware dekking.
|
||||
|
||||
Volgende uitvoerbare taak: geen; alle P0- en P1-releasepunten zijn afgerond.
|
||||
|
||||
## Laatste risicohardening
|
||||
|
||||
@@ -42,8 +92,7 @@ Live mailboxvoorbereiding op 2026-07-30:
|
||||
- de alias is niet primair gemaakt en bestaande adressen zijn ongewijzigd gebleven;
|
||||
- de Entra-appregistratie, admin consent en Exchange-mailboxscope zijn live ingericht
|
||||
met app-only OAuth; tijdelijke bootstraprechten en de bootstrap-app zijn na afloop verwijderd;
|
||||
- `VR-202` resteert alleen nog op de productie-uitrol en echte platformalert-/digestproeven;
|
||||
Basic Auth wordt niet geactiveerd;
|
||||
- `VR-202` is volledig afgerond; Basic Auth is niet geactiveerd;
|
||||
- de volledige lokale gate is groen met 324 tests, 0 failures, 0 skips en 84,05%
|
||||
branch-aware dekking.
|
||||
|
||||
@@ -576,16 +625,13 @@ Een toekomstige agent moet altijd de actuele waarheid opnieuw bepalen met:
|
||||
|
||||
## Nog te bouwen
|
||||
|
||||
Momenteel staan er geen `ready` taken meer over; het volgende te behandelen item is `VR-202` als `blocked-external` voor live mailbox-/SMTP-credentials en echte jobalertmails van de gekozen platformen.
|
||||
Momenteel staan er geen `ready` taken of P0/P1-releaseblokkers meer over.
|
||||
|
||||
## Externe blokkades
|
||||
|
||||
Deze blokkeren onafhankelijke code niet:
|
||||
|
||||
- verdere persoonlijke werkgevers-/bronnenkeuze en actuele menselijke review buiten de actieve VITO-/Deliverect-startercatalogus;
|
||||
- live IMAP/SMTP-account en app-password;
|
||||
- containerregistry en credentials;
|
||||
- actieve/voor deze repository geregistreerde Gitea Actions-runner;
|
||||
|
||||
## Bekende verificatiebeperking van de aangeleverde basis
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Finale acceptance 0.3.14
|
||||
# Finale acceptance 0.3.15
|
||||
|
||||
Status: productie geaccepteerd op 2026-07-29.
|
||||
Status: productie geaccepteerd op 2026-07-30.
|
||||
|
||||
## Bewijs
|
||||
|
||||
@@ -14,7 +14,15 @@ Status: productie geaccepteerd op 2026-07-29.
|
||||
- PostgreSQL custom dump plus media/configbewijs is naar een geïsoleerde database hersteld; migrations, exacte recordtellingen en een aparte applicatiestart slaagden.
|
||||
- Een productie-importsmoke extraheerde en creëerde één geldige job in een expliciete transactie; rollback werd bevestigd en liet geen smokedata achter.
|
||||
- De checksum-geldige pre-releaseback-up staat onder `backups/release-0.3.14-20260729T164500Z`; rollbacktag `vacatureradar:rollback-0.3.13` bewaart image `sha256:34aef8853f77a3f311b119bd67d9f1160a97fd4a21e62c9776222be289bf3b88`.
|
||||
- M365 IMAP en SMTP werken app-only via OAuth/XOAUTH2; negen geïsoleerde
|
||||
platformconnecties importeerden één live testmail idempotent en één testdigest
|
||||
eindigde met outboxstatus `sent`.
|
||||
- Gitea Actions publiceerde `v0.3.15` met immutable digest
|
||||
`sha256:5a6dc253db7a60c42c0c806ae084ddc6ee7c98db5db71a857423b894a3662ba5`.
|
||||
Het artifact bevat een geverifieerde SPDX-SBOM, checksums en release notes.
|
||||
- Unraid is op die digest gepind; pull, health/readiness, rollback naar `0.3.14`
|
||||
en herstel naar `0.3.15` zijn live geslaagd.
|
||||
|
||||
## Uitkomst
|
||||
|
||||
Er bestaan geen bekende P0- of P1-releaseblokkers. Alleen optionele Entra- en mailboxcredentials blijven extern; zij raken de kernrelease niet.
|
||||
Er bestaan geen bekende P0- of P1-releaseblokkers.
|
||||
|
||||
@@ -82,7 +82,7 @@ Django/Celery
|
||||
| T-40 | Verouderde lokale CA-store of ongeldige serverketen lokt een TLS-bypass uit | onderschepte vacature- of mailboxdata en credentialdiefstal | expliciet gepinde `certifi`-CA-bundel voor HTTP, robots en IMAP; hostnaamcontrole en `CERT_REQUIRED`; nooit `verify=False`; dependency- en negatieve TLS-tests bij update | `tests/unit/test_tls.py`, `tests/security/test_robots.py`, `apps/sources/services/tls.py`, `VR-201` |
|
||||
| T-41 | Gemodificeerde `next`-parameter na vacaturefeedback | open redirect naar phishingdomein na een geldige gebruikersactie | terugkeer-URL moet dezelfde host en het actuele HTTPS-beleid respecteren; anders veilige detailfallback | `tests/integration/test_tasks_and_more_views.py`, `VR-206` |
|
||||
| T-42 | Gespoofte proxyheaders of ontbrekende publieke hostconfiguratie | host-headerfouten, onjuiste HTTPS-detectie of omzeilde IP-rate-limiting | `PUBLIC_BASE_URL`, exacte host/CSRF-validatie, proxy moet headers overschrijven, forwarded client-IP alleen vanaf `TRUSTED_PROXY_CIDRS` | `tests/unit/test_public_configuration.py`, `tests/unit/test_network.py`, `VR-225` |
|
||||
| T-43 | Gedeeld demoaccount wijzigt persistente data | bezoekers beïnvloeden elkaars ervaring of echte data | server-side read-only middleware, korte sessie, geen bruikbaar wachtwoord, zichtbare UI-status en productiecheck | `tests/integration/test_auth_login.py`, `tests/unit/test_settings_security.py`, `VR-225` |
|
||||
| T-43 | Gedeeld demoaccount wijzigt persistente data of activeert echte mailboxpolling | bezoekers beïnvloeden elkaars ervaring, echte data of externe accounts | server-side read-only middleware, korte sessie, geen bruikbaar wachtwoord, uitsluitend fictieve `example.test`-mailboxen; de loginseed herstelt begrensd en idempotent en de scheduler sluit de demo-user expliciet uit | `tests/integration/test_auth_login.py`, `tests/integration/test_tasks_and_more_views.py`, `tests/unit/test_settings_security.py`, `VR-225/229` |
|
||||
|
||||
## Misbruikscenario's
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ Iedere backlogtaak die een requirement wijzigt, werkt in dezelfde commit deze ma
|
||||
|
||||
## Aanvullende traceability-opmerking
|
||||
- VR-228 levert release 0.3.14 met een fail-closed disposable PostgreSQL-benchmark, run-lokale importcaches, gebundelde veldprovenance en ongewijzigde score-snapshotcopy. De officiële drie iteraties bewijzen 3.264 queries per 250 items, parser 12/12, dedupeprecision/recall 1,0 en nul rankingchurn; bewijs staat in `docs/audit/IMPORT_PERFORMANCE_*.md` en het machineleesbare artifact.
|
||||
- VR-229 houdt de publieke demo representatief door fictieve vacatures, scores, mailboxkoppelingen en opvolgdatums bij iedere demosessie idempotent te herstellen. Mailboxingress is uitgesloten van URL-fetchplanning en operationele UI-copy onderscheidt een actief profiel van bron- en systeemgezondheid.
|
||||
- VR-227 professionaliseert release 0.3.13 integraal: gecentreerd ultrawideframe, mobiele gastbanner en verticale pipeline, begrensde vacaturepaginering, verplichte zes-viewport-Playwrightgate zonder skips, echte runtimeheartbeat, volledige generieke parserbenchmark en SBOM-generatie. Bewijs staat in `docs/audit/`, de E2E-/auth-/runtime-/adaptertests en `USER_INPUT_REQUIRED.md`.
|
||||
- VR-102 is gerealiseerd met auditbare policyreviews (`SourcePolicyReview`), robotscache met TTL/size/SSRF-controles en fail-closed policy-gating op verlopen of conflicterende reviews.
|
||||
- VR-107 is gerealiseerd met `apps/sources/views.py`, `apps/sources/services/manual_import.py`, `templates/sources/list.html`, `static/js/manual_import.js` en bijhorende unit/integration-tests.
|
||||
@@ -96,6 +97,12 @@ Iedere backlogtaak die een requirement wijzigt, werkt in dezelfde commit deze ma
|
||||
- VR-122 operationaliseert de veilige platformingress als dagelijkse, configureerbare en begrensde mailboxpoll. VDAB-alerts krijgen expliciete provenance, replay blijft idempotent en de bronpagina onderscheidt de werkende jobalertroute van de contractueel afgeschermde officiële Vacature API. Bewijs staat in de e-mailadapter-/importtests, `apps/sources/services/imap_import.py` en ADR-0005.
|
||||
- VR-123 vervangt die globale poll door gebruikersgescopeerde VDAB- en Indeed-mailboxkoppelingen met versleutelde app-wachtwoorden, afzonderlijke intervallen, per-mailboxleases en UI-beheer. Bewijs staat in `tests/unit/test_mailbox_credentials.py`, `tests/integration/test_mailbox_connections.py`, de migratie en ADR-0009.
|
||||
- VR-124 breidt dezelfde mailboxingang uit met LinkedIn, ictjob.be, Jobat, StepStone, EuroBrussels en BrusselsJobs. De platformgebonden parser accepteert uitsluitend gelabelde HTTPS-links op het gekozen jobboarddomein; fixtures, adapter-/integratietests en de centrale denylist bewijzen de afscherming.
|
||||
- VR-202 bewijst de productie-ingress met M365 app-only OAuth/XOAUTH2, een
|
||||
dedicated alias en map, negen platformconnecties, een idempotente live
|
||||
testmail, correcte schedulerstatus en een verzonden digest-outbox.
|
||||
- VR-204 bewijst de immutable releaseketen via de private Gitea-registry,
|
||||
digest-pin, commitlabel, SPDX-SBOM, checksums, release notes en een live
|
||||
Unraid pull-/rollback-/herstelsmoke.
|
||||
- VR-125 vervangt de twee Brusselse nicheboards door Careerjet, Randstad en Robert Half, retireert buitenregionale werkgevers zonder provenanceverlies en voegt host- en padgebonden adapters toe voor ACA Group, Xplore Group, UHasselt, PXL en ZOL in de kern Hasselt-Genk.
|
||||
- VR-126 herankert profiel en broncatalogus op postcode 2400 Mol en 40 km. De host-, pad- en locatiefilters voor SCK CEN, Cipal Schaubroeck, VanRoey, NTX en Thomas More worden gedekt door regionale adaptertests, source-viewtests, live bronruns en de bijgewerkte threat T-39.
|
||||
- VR-201 activeert Gemeente Mol en zeven bijkomende Kempen-werkgevers na menselijke voorwaardenreview, fixtures, fail-closed locatie-/routefilters en twee livecycli. Twaalf interessante werkgevers zonder stabiele actieve ingang staan afzonderlijk en niet-scanbaar op de UI-waaklijst. Bewijs staat in de livecatalogus, seeddata, `KempenEmployerAdapter`, bronruns en adapter-/seed-/viewtests.
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-radar-card">
|
||||
<span class="status-beacon {% if navigation_profile %}is-live{% endif %}" aria-hidden="true"></span>
|
||||
<span><strong>{% if navigation_profile %}Radar actief{% else %}Profiel nodig{% endif %}</strong><small>{% if navigation_profile %}{{ navigation_profile.name }}{% else %}Configureer je zoekprofiel{% endif %}</small></span>
|
||||
<span><strong>{% if navigation_profile %}Profiel actief{% else %}Profiel nodig{% endif %}</strong><small>{% if navigation_profile %}{{ navigation_profile.name }}{% else %}Configureer je zoekprofiel{% endif %}</small></span>
|
||||
</div>
|
||||
{% include "components/developer_credit.html" %}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
<header class="cockpit-heading dashboard-heading">
|
||||
<div><p class="eyebrow">Intelligence cockpit · vandaag</p><h1>Goedemorgen, {{ user.first_name|default:user.username }}.</h1><p>{{ day_conclusion }}</p></div>
|
||||
<div class="radar-status" aria-label="Radarstatus"><span class="status-beacon {% if active_profile %}is-live{% endif %}" aria-hidden="true"></span><span>{% if active_profile %}{{ active_profile.name }} actief{% else %}Geen actief profiel{% endif %}</span>{% if latest_scan %}<span aria-hidden="true">·</span><span>scan {{ latest_scan.started_at|timesince }} geleden</span>{% endif %}</div>
|
||||
<div class="radar-status" aria-label="Profiel- en scanstatus"><span class="status-beacon {% if active_profile %}is-live{% endif %}" aria-hidden="true"></span><span>{% if active_profile %}Profiel actief · {{ active_profile.name }}{% else %}Geen actief profiel{% endif %}</span>{% if latest_scan %}<span aria-hidden="true">·</span><span>laatste URL-scan {{ latest_scan.started_at|timesince }} geleden</span>{% endif %}</div>
|
||||
</header>
|
||||
|
||||
<section class="stats-grid" aria-label="Radar kerncijfers">
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<input class="auth-input" id="id_password" name="password" type="password"
|
||||
autocomplete="current-password" placeholder="••••••••" required
|
||||
autocomplete="current-password" required
|
||||
{% if form.errors %}aria-invalid="true"{% endif %}>
|
||||
<button class="pw-toggle" type="button" data-pw-toggle aria-label="Wachtwoord tonen">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" data-eye><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="2.7" stroke="currentColor" stroke-width="1.8"/></svg>
|
||||
|
||||
@@ -135,15 +135,54 @@ def test_demo_login_logs_in_and_seeds_environment(client):
|
||||
# Het gevulde dashboard rendert zonder fouten.
|
||||
dashboard = client.get(reverse("dashboard:today"))
|
||||
assert dashboard.status_code == 200
|
||||
dashboard_html = dashboard.content.decode("utf-8")
|
||||
assert "Profiel actief" in dashboard_html
|
||||
assert "Radar actief" not in dashboard_html
|
||||
assert "Nog geen persoonlijke matches" not in dashboard_html
|
||||
|
||||
sources = client.get(reverse("sources:list"))
|
||||
assert sources.status_code == 200
|
||||
assert "minstens één import actief" in sources.content.decode("utf-8")
|
||||
|
||||
|
||||
def test_demo_login_is_idempotent(client):
|
||||
from apps.jobs.models import JobPosting
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.jobs.models import Application, JobPosting
|
||||
from apps.profiles.models import SearchProfile
|
||||
from apps.sources.models import MailboxConnection, Source
|
||||
|
||||
client.post(reverse("demo-login"))
|
||||
first = JobPosting.objects.count()
|
||||
demo_user = get_user_model().objects.get(username=settings.DEMO_USERNAME)
|
||||
profile = SearchProfile.objects.get(user=demo_user, is_active=True)
|
||||
job = profile.scores.order_by("-score").first().job
|
||||
job.status = JobPosting.Status.EXPIRED
|
||||
job.last_seen = timezone.now() - timedelta(days=30)
|
||||
job.save(update_fields=["status", "last_seen"])
|
||||
application = Application.objects.filter(user=demo_user).first()
|
||||
application.follow_up_date = timezone.localdate() - timedelta(days=14)
|
||||
application.save(update_fields=["follow_up_date"])
|
||||
|
||||
client.logout()
|
||||
client.post(reverse("demo-login"))
|
||||
assert JobPosting.objects.count() == first
|
||||
job.refresh_from_db()
|
||||
application.refresh_from_db()
|
||||
assert job.status == JobPosting.Status.ACTIVE
|
||||
assert job.last_seen.date() == timezone.localdate()
|
||||
assert application.follow_up_date > timezone.localdate()
|
||||
assert profile.scores.count() == 6
|
||||
assert MailboxConnection.objects.filter(user=demo_user, enabled=True).count() == 9
|
||||
assert Source.objects.filter(source_type=Source.Type.EMAIL, failure_count=0).count() >= 9
|
||||
|
||||
|
||||
def test_login_password_field_has_no_mask_placeholder(client):
|
||||
html = client.get(reverse("login")).content.decode("utf-8")
|
||||
assert 'id="id_password"' in html
|
||||
assert 'placeholder="••••••••"' not in html
|
||||
|
||||
|
||||
def test_demo_account_is_read_only_and_session_is_bounded(client):
|
||||
|
||||
@@ -104,6 +104,55 @@ def test_fetch_source_resets_fail_state_on_304(db, source, monkeypatch):
|
||||
assert source.failure_count == 0
|
||||
|
||||
|
||||
def test_due_source_scheduler_never_schedules_mailbox_sources(db, monkeypatch):
|
||||
from apps.sources.tasks import schedule_due_sources
|
||||
|
||||
email_source = Source.objects.create(
|
||||
name="Mailboxbron",
|
||||
source_type=Source.Type.EMAIL,
|
||||
base_url="",
|
||||
domain="example.mailbox.local",
|
||||
status=Source.Status.ACTIVE,
|
||||
policy=Source.Policy.ALLOW,
|
||||
)
|
||||
scheduled = []
|
||||
monkeypatch.setattr(
|
||||
"apps.sources.tasks.fetch_source.delay", lambda source_id: scheduled.append(source_id)
|
||||
)
|
||||
|
||||
result = schedule_due_sources()
|
||||
|
||||
assert email_source.pk not in scheduled
|
||||
assert result["scheduled"] == len(scheduled)
|
||||
|
||||
|
||||
def test_mailbox_scheduler_excludes_public_demo_user(db, monkeypatch, settings):
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from apps.sources.models import MailboxConnection
|
||||
from apps.sources.tasks import schedule_mailbox_polls
|
||||
|
||||
demo = get_user_model().objects.create_user(username=settings.DEMO_USERNAME)
|
||||
connection = MailboxConnection.objects.create(
|
||||
user=demo,
|
||||
platform="vdab",
|
||||
provider="outlook",
|
||||
username="demo@example.test",
|
||||
mailbox="Demo/VDAB",
|
||||
enabled=True,
|
||||
next_poll_at=timezone.now() - timedelta(minutes=1),
|
||||
)
|
||||
scheduled = []
|
||||
monkeypatch.setattr(
|
||||
"apps.sources.tasks.poll_mailbox.delay",
|
||||
lambda connection_id: scheduled.append(connection_id),
|
||||
)
|
||||
|
||||
schedule_mailbox_polls()
|
||||
|
||||
assert connection.pk not in scheduled
|
||||
|
||||
|
||||
def test_fetch_source_is_blocked_by_conflicting_review(db):
|
||||
source = Source.objects.create(
|
||||
name="Blocked source",
|
||||
|
||||
Reference in New Issue
Block a user