diff --git a/src/worker/politics.rs b/src/worker/politics.rs index 44d7d2e..21bdc56 100644 --- a/src/worker/politics.rs +++ b/src/worker/politics.rs @@ -213,6 +213,27 @@ const QUERY_INSERT_CANDIDATES: &str = r#" ) AS sub(id); "#; +/// Wahlen finden, für die noch keine Kandidaten existieren. +/// Das umfasst sowohl frisch eingetragene Wahlen als auch manuell +/// angelegte Wahlen, solange: +/// - das Wahl-Datum heute oder in der Zukunft liegt und +/// - noch kein Eintrag in `falukant_data.candidate` existiert. +const QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES: &str = r#" + SELECT + e.id AS election_id, + e.region_id AS region_id, + e.posts_to_fill + FROM falukant_data.election AS e + WHERE e.region_id IS NOT NULL + AND e.posts_to_fill > 0 + AND e.date::date >= CURRENT_DATE + AND NOT EXISTS ( + SELECT 1 + FROM falukant_data.candidate AS c + WHERE c.election_id = e.id + ); +"#; + const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#" WITH expired_offices AS ( @@ -536,26 +557,39 @@ impl PoliticsWorker { Self::notify_office_filled(pool, broker, &new_offices_direct)?; } - // 5) Neue Wahlen planen und Kandidaten eintragen - let elections = Self::schedule_elections(pool)?; - if !elections.is_empty() { - Self::insert_candidates_for_elections(pool, &elections)?; + // 5) Neue Wahlen planen + let new_elections = Self::schedule_elections(pool)?; + + // 6) Für alle Wahlen ohne Kandidaten (inkl. manuell + // angelegter) Kandidaten eintragen. + let mut elections_to_fill = Self::find_elections_needing_candidates(pool)?; + // Die gerade neu angelegten Wahlen sind typischerweise auch + // in obiger Liste enthalten. Falls nicht, fügen wir sie + // sicherheitshalber hinzu. + for e in new_elections.iter() { + if !elections_to_fill.iter().any(|x| x.election_id == e.election_id) { + elections_to_fill.push(e.clone()); + } + } + + if !elections_to_fill.is_empty() { + Self::insert_candidates_for_elections(pool, &elections_to_fill)?; // Benachrichtige User in betroffenen Regionen let region_ids: HashSet = - elections.iter().map(|e| e.region_id).collect(); + elections_to_fill.iter().map(|e| e.region_id).collect(); let user_ids = Self::get_user_ids_in_cities_of_regions(pool, ®ion_ids)?; Self::notify_election_created(pool, broker, &user_ids)?; } - // 6) Wahlen auswerten und neu besetzte Ämter melden + // 7) Wahlen auswerten und neu besetzte Ämter melden let new_offices_from_elections = Self::process_elections(pool)?; if !new_offices_from_elections.is_empty() { Self::notify_office_filled(pool, broker, &new_offices_from_elections)?; } - // 7) Als letzter Schritt sicherstellen, dass es für keinen + // 8) Als letzter Schritt sicherstellen, dass es für keinen // Amtstyp/Region-Kombi mehr besetzte Ämter gibt als laut // `seats_per_region` erlaubt. Dieser Abgleich wird nach allen // Lösch- und Besetzungsvorgängen ausgeführt. @@ -741,6 +775,36 @@ impl PoliticsWorker { Ok(elections) } + /// Findet alle existierenden Wahlen (inkl. manuell angelegter), + /// für die noch keine Kandidaten eingetragen wurden. + fn find_elections_needing_candidates(pool: &ConnectionPool) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "select_elections_needing_candidates", + QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES, + )?; + let rows = conn.execute("select_elections_needing_candidates", &[])?; + + let mut elections = Vec::with_capacity(rows.len()); + for row in rows { + let election_id = parse_i32(&row, "election_id", -1); + let region_id = parse_i32(&row, "region_id", -1); + let posts_to_fill = parse_i32(&row, "posts_to_fill", 0); + if election_id >= 0 && region_id >= 0 && posts_to_fill > 0 { + elections.push(Election { + election_id, + region_id, + posts_to_fill, + }); + } + } + + Ok(elections) + } + fn insert_candidates_for_elections( pool: &ConnectionPool, elections: &[Election],