diff --git a/locales/en/csb.yml b/locales/en/csb.yml index eb341fdd5..24339fe5d 100644 --- a/locales/en/csb.yml +++ b/locales/en/csb.yml @@ -73,6 +73,7 @@ import: not_found: No data was found for the entered hash code. index: closes_on: Closes on + download_election_definition: Download election definition feature: add_omissions: Add omissions and corrections create_i1_letters: Create I 1 and omission letters diff --git a/locales/nl/csb.yml b/locales/nl/csb.yml index 84cd9781f..200245933 100644 --- a/locales/nl/csb.yml +++ b/locales/nl/csb.yml @@ -73,6 +73,7 @@ import: not_found: Er is geen data gevonden voor de ingevoerde hash code. index: closes_on: Sluit op + download_election_definition: Verkiezings\u00ADdefinitie downloaden feature: add_omissions: Verzuimen en correcties toevoegen create_i1_letters: I 1 en verzuimbrieven aanmaken diff --git a/src/core/election/configs.rs b/src/core/election/configs.rs index 04aebaa02..6c476af89 100644 --- a/src/core/election/configs.rs +++ b/src/core/election/configs.rs @@ -17,7 +17,7 @@ super::define_elections! { en: "Election of the Senate of the States General 2027", }, electoral_districts: ElectoralDistrict::ek27(), - nineteen_or_more_seats: true, + number_of_seats: 75, frisian_export_allowed: false, eligible_date_of_birth: NaiveDate::from_ymd_opt(2014, 4, 20).unwrap(), // TODO: determine definitive date nomination_day_date: NaiveDate::from_ymd_opt(2027, 4, 20).unwrap(), @@ -57,7 +57,20 @@ super::define_elections! { Province::NB => &[ElectoralDistrict::PsTilburg, ElectoralDistrict::PsDenBosch], Province::LI => &[ElectoralDistrict::PsMaastricht, ElectoralDistrict::PsVenlo], }, - nineteen_or_more_seats: true, // for this election, all provinces have >= 19 seats + number_of_seats: match province { + Province::GR => 43, + Province::FR => 43, + Province::DR => 43, + Province::OV => 47, + Province::FL => 41, + Province::GE => 55, + Province::UT => 49, + Province::NH => 55, + Province::ZH => 55, + Province::ZE => 39, + Province::NB => 55, + Province::LI => 47, + }, frisian_export_allowed: matches!(province, Province::FR), eligible_date_of_birth: NaiveDate::from_ymd_opt(2014, 2, 1).unwrap(), // TODO: determine definitive date nomination_day_date: NaiveDate::from_ymd_opt(2027, 2, 1).unwrap(), @@ -105,7 +118,29 @@ super::define_elections! { WaterCouncil::Limburg => &[ElectoralDistrict::WsLimburg], WaterCouncil::Zuiderzeeland => &[ElectoralDistrict::WsZuiderzeeland], }, - nineteen_or_more_seats: true, // for this election, all councils have >= 19 seats + number_of_seats: match water_council { + WaterCouncil::AaEnMaas => 26, + WaterCouncil::AmstelGooiEnVecht => 26, + WaterCouncil::BrabantseDelta => 26, + WaterCouncil::DeDommel => 26, + WaterCouncil::DeStichtseRijnlanden => 26, + WaterCouncil::Delfland => 26, + WaterCouncil::DrentsOverijsselseDelta => 25, + WaterCouncil::HollandseDelta => 26, + WaterCouncil::HollandsNoorderkwartier => 26, + WaterCouncil::HunzeEnAas => 19, + WaterCouncil::Fryslan => 21, + WaterCouncil::Limburg => 26, + WaterCouncil::Noorderzijlvest => 19, + WaterCouncil::RijnEnIJssel => 30, + WaterCouncil::Rijnland => 26, + WaterCouncil::Rivierenland => 26, + WaterCouncil::SchielandEnDeKrimpenerwaard => 26, + WaterCouncil::Scheldestromen => 26, + WaterCouncil::ValleiEnVeluwe => 26, + WaterCouncil::Vechtstromen => 23, + WaterCouncil::Zuiderzeeland => 21, + }, frisian_export_allowed: matches!(water_council, WaterCouncil::Fryslan), eligible_date_of_birth: NaiveDate::from_ymd_opt(2014, 2, 1).unwrap(), // TODO: determine definitive date nomination_day_date: NaiveDate::from_ymd_opt(2027, 2, 1).unwrap(), @@ -248,6 +283,10 @@ impl ElectionConfig { pub fn has_only_one_district(&self) -> bool { self.electoral_districts().len() == 1 } + + pub fn nineteen_or_more_seats(&self) -> bool { + self.number_of_seats() >= 19 + } } #[cfg(test)] diff --git a/src/core/election/macros.rs b/src/core/election/macros.rs index d4c586e6a..1981efde3 100644 --- a/src/core/election/macros.rs +++ b/src/core/election/macros.rs @@ -10,7 +10,7 @@ macro_rules! define_elections { en: $title_en:expr $(,)? }, electoral_districts: $electoral_districts:expr, - nineteen_or_more_seats: $nineteen_or_more_seats:expr, + number_of_seats: $number_of_seats:expr, frisian_export_allowed: $frisian_export_allowed:expr, eligible_date_of_birth: $eligible_date_of_birth:expr, nomination_day_date: $nomination_day_date:expr, @@ -126,11 +126,11 @@ macro_rules! define_elections { } } - pub fn nineteen_or_more_seats(&self) -> bool { + pub fn number_of_seats(&self) -> u64 { #[allow(unused)] match self { $( - Self::$name $(($binding))? => $nineteen_or_more_seats, + Self::$name $(($binding))? => $number_of_seats, )* } } diff --git a/src/csb/index/mod.rs b/src/csb/index/mod.rs index b3e644cbb..f74886561 100644 --- a/src/csb/index/mod.rs +++ b/src/csb/index/mod.rs @@ -2,4 +2,4 @@ pub mod pages; mod paths; pub use pages::router; -pub use paths::CsbIndexPath; +pub use paths::{CsbElectionDefinitionDownloadPath, CsbIndexPath}; diff --git a/src/csb/index/pages/election_definition.rs b/src/csb/index/pages/election_definition.rs new file mode 100644 index 000000000..50d32b916 --- /dev/null +++ b/src/csb/index/pages/election_definition.rs @@ -0,0 +1,83 @@ +use axum::{extract::State, http::HeaderValue, response::IntoResponse}; + +use crate::{ + AppError, CsbMainStore, CsbStoreData, + csb::{WithCorrections, index::CsbElectionDefinitionDownloadPath}, + models::eml::eml110a::eml110a, + store::StoreRegistry, + utils::no_cache_headers, +}; + +const XML_CONTENT_TYPE: &str = "application/xml"; + +pub async fn download_election_definition( + _: CsbElectionDefinitionDownloadPath, + main_store: CsbMainStore, + State(csb_registry): State>, +) -> Result { + let election = main_store.election; + + // TODO: get registered party from a proper source + // Blank lists probably shouldn't be included + let registered_party_names = csb_registry + .stores_by_scope() + .await? + .into_iter() + .map(|store| store.get_display_name(WithCorrections::All)) + .collect(); + + let bytes = eml110a(&election, registered_party_names)?; + + let headers = no_cache_headers::generate_attachment_headers( + "eml110a.eml.xml", + HeaderValue::from_static(XML_CONTENT_TYPE), + )?; + + Ok((headers, bytes).into_response()) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + extract::FromRef, + http::{StatusCode, header}, + response::IntoResponse, + }; + + use crate::{AppState, store::StoreRegistry}; + + #[tokio::test] + async fn download_election_definition_returns_xml_response() -> Result<(), AppError> { + let main_store = CsbMainStore::new_for_test(); + let state = AppState::new_for_tests().await; + let csb_registry = StoreRegistry::::from_ref(&state); + + let response = download_election_definition( + CsbElectionDefinitionDownloadPath, + main_store, + State(csb_registry), + ) + .await? + .into_response(); + + assert_eq!(response.status(), StatusCode::OK); + let headers = response.headers(); + assert_eq!( + headers.get(header::CONTENT_TYPE).expect("content type"), + "application/xml" + ); + assert_eq!( + headers + .get(header::CONTENT_DISPOSITION) + .expect("content disposition"), + "attachment; filename=\"eml110a.eml.xml\"" + ); + assert_eq!( + headers.get(header::CACHE_CONTROL).expect("cache control"), + "no-store, no-cache, must-revalidate, max-age=0" + ); + + Ok(()) + } +} diff --git a/src/csb/index/pages/index.html b/src/csb/index/pages/index.html index 32a454518..6cf8494ce 100644 --- a/src/csb/index/pages/index.html +++ b/src/csb/index/pages/index.html @@ -3,6 +3,10 @@ {% let election = "election"|election_value %} {{ (&election)|election_title }} {% endblock %} +{% block header_action %} + {{ "csb.index.download_election_definition"|trans }} +{% endblock %} {% block content %} {% let election = "election"|election_value %}
diff --git a/src/csb/index/pages/mod.rs b/src/csb/index/pages/mod.rs index b2ca407f4..d8821967e 100644 --- a/src/csb/index/pages/mod.rs +++ b/src/csb/index/pages/mod.rs @@ -2,8 +2,11 @@ use crate::AppState; use axum::Router; use axum_extra::routing::RouterExt; +mod election_definition; mod index; pub fn router() -> Router { - Router::new().typed_get(index::index) + Router::new() + .typed_get(index::index) + .typed_get(election_definition::download_election_definition) } diff --git a/src/csb/index/paths.rs b/src/csb/index/paths.rs index 3a2c07686..b3063ea68 100644 --- a/src/csb/index/paths.rs +++ b/src/csb/index/paths.rs @@ -7,3 +7,7 @@ use crate::AppError; #[derive(TypedPath)] #[typed_path("/csb", rejection(AppError))] pub struct CsbIndexPath; + +#[derive(TypedPath)] +#[typed_path("/csb/eml110a.eml.xml", rejection(AppError))] +pub struct CsbElectionDefinitionDownloadPath; diff --git a/src/models/documents.rs b/src/models/documents.rs index 5a55a9b22..4b1f71853 100644 --- a/src/models/documents.rs +++ b/src/models/documents.rs @@ -4,7 +4,7 @@ use super::{ Pdf, - eml210::eml210, + eml::eml210::eml210, h1::H1, h3::H3, h4::H4, diff --git a/src/models/eml/eml110a.rs b/src/models/eml/eml110a.rs new file mode 100644 index 000000000..5fe0a3875 --- /dev/null +++ b/src/models/eml/eml110a.rs @@ -0,0 +1,132 @@ +//! The EML Election Definition (`110a`) export, built with [`eml_nl`]. + +use std::num::NonZeroU64; + +use eml_nl::{ + common::ContestIdentifier, + documents::{ + EML, ElectionIdentifierBuilder, + election_definition::{ElectionDefinition, ElectionDefinitionRegisteredParty}, + }, + io::EMLWrite, + utils::{ElectionSubcategory, VotingMethod}, +}; + +use crate::{AppError, ElectionConfig}; + +/// Build the EML 110a election definition XML for the given election and +/// the list of registered party names. +pub fn eml110a( + election: &ElectionConfig, + registered_party_names: Vec, +) -> Result, AppError> { + let subcategory = eml_nl::utils::ElectionSubcategory::from(election); + + let contest_identifier = if election.has_only_one_district() { + ContestIdentifier::geen() + } else { + ContestIdentifier::alle() + }; + + // The "voorkeursdrempel", see Kieswet P 15 + let preference_threshold: u64 = if subcategory == ElectionSubcategory::GR1 { + 50 + } else { + 25 + }; + + let now = chrono::Utc::now(); + let definition = ElectionDefinition::builder() + .transaction_id(1) + .issue_date(now.date_naive()) + .creation_date_time(now) + .election_identifier( + ElectionIdentifierBuilder::try_from(*election)?.build_for_definition()?, + ) + .contest_identifier(contest_identifier) + .voting_method(VotingMethod::SPV) + .max_votes(NonZeroU64::new(1).unwrap()) // 1 is the default max votes => always empty + .number_of_seats(election.number_of_seats()) + .preference_threshold(preference_threshold) + .registered_parties( + registered_party_names + .into_iter() + .map(ElectionDefinitionRegisteredParty::new) + .collect::>(), + ) + .build()?; + + Ok(EML::from_election_definition_doc(definition).write_eml_root(true, true)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ElectionConfig, Province, WaterCouncil}; + + fn check_eml(response: &str, expected: &str) { + let stringify_election_event = |eml: EML| { + format!( + "{:?}", + eml.as_election_definition_doc().unwrap().election_event + ) + }; + + let received = stringify_election_event(response.parse().unwrap()); + let expected = stringify_election_event(expected.parse().unwrap()); + + assert_eq!(received, expected, "received XML:\n{}", response); + } + + #[test] + fn ek_export() { + let eml = eml110a( + &ElectionConfig::EK27, + vec!["Kiesraad Demo".to_string(), "Andere Partij".to_string()], + ) + .unwrap(); + check_eml( + &String::from_utf8(eml).unwrap(), + include_str!("testdata/110a-ek27.eml.xml"), + ); + } + + #[test] + fn ps1_export() { + let eml = eml110a( + &ElectionConfig::PS27(Province::GR), + vec!["Kiesraad Demo".to_string(), "Andere Partij".to_string()], + ) + .unwrap(); + check_eml( + &String::from_utf8(eml).unwrap(), + include_str!("testdata/110a-ps27-1.eml.xml"), + ); + } + + #[test] + fn ps2_export() { + let eml = eml110a( + &ElectionConfig::PS27(Province::LI), + vec!["Kiesraad Demo".to_string(), "Andere Partij".to_string()], + ) + .unwrap(); + check_eml( + &String::from_utf8(eml).unwrap(), + include_str!("testdata/110a-ps27-2.eml.xml"), + ); + } + + #[test] + fn ws_export() { + let eml = eml110a( + &ElectionConfig::WS27(WaterCouncil::Fryslan), + vec!["Water Water".to_string()], + ) + .unwrap(); + check_eml( + &String::from_utf8(eml).unwrap(), + include_str!("testdata/110a-ws27.eml.xml"), + ); + } +} diff --git a/src/models/eml210.rs b/src/models/eml/eml210.rs similarity index 80% rename from src/models/eml210.rs rename to src/models/eml/eml210.rs index 211e01db3..e5f7a9cc3 100644 --- a/src/models/eml210.rs +++ b/src/models/eml/eml210.rs @@ -1,6 +1,5 @@ -//! The EML 2.10 candidate nomination export, built with [`eml_nl`]. +//! The EML 210 candidate nomination export, built with [`eml_nl`]. -use chrono::Datelike; use eml_nl::{ common::{ AuthorityIdentifier, CandidateIdentifier, CountryNameCode, CreatedByAuthority, FirstName, @@ -8,15 +7,15 @@ use eml_nl::{ PersonName, }, documents::{ - EML, + EML, ElectionIdentifierBuilder, candidate_lists::QualifyingAddress, nomination::{ AgentIdentifier, Nomination, NominationAffiliation, NominationContestIdentifier, - NominationElectionIdentifier, NominationNominate, + NominationNominate, }, }, io::EMLWrite, - utils::{AffiliationType, AuthorityId, CandidateId, ContestId, ElectionId, StringValue}, + utils::{AffiliationType, AuthorityId, CandidateId, ContestId, StringValue}, }; use crate::{ @@ -24,94 +23,12 @@ use crate::{ candidate_lists::{CandidateListId, FullCandidateList}, candidates::Candidate, common::{Address, BsnOrNoneConfirmed, DutchAddress, FullName, Gender}, - core::{ElectionType, ModelLocale}, + core::ModelLocale, list_submitters::ListSubmitter, persons::Representative, political_groups::PoliticalGroup, - utils::slugify_teletex, }; -impl From for eml_nl::utils::ElectionCategory { - fn from(value: ElectionType) -> Self { - match value { - ElectionType::Tk => eml_nl::utils::ElectionCategory::TK, - ElectionType::Ek => eml_nl::utils::ElectionCategory::EK, - ElectionType::Gr => eml_nl::utils::ElectionCategory::GR, - ElectionType::Ps => eml_nl::utils::ElectionCategory::PS, - ElectionType::Ws => eml_nl::utils::ElectionCategory::AB, - ElectionType::Ep => eml_nl::utils::ElectionCategory::EP, - ElectionType::Kc | ElectionType::Kcni => { - todo!("Kiescolleges don't have an official code yet in EML-NL") - } - ElectionType::Er => eml_nl::utils::ElectionCategory::ER, - } - } -} - -impl From<&ElectionConfig> for eml_nl::utils::ElectionSubcategory { - fn from(value: &ElectionConfig) -> Self { - match value.election_type() { - ElectionType::Tk => eml_nl::utils::ElectionSubcategory::TK, - ElectionType::Ek => eml_nl::utils::ElectionSubcategory::EK, - ElectionType::Gr => { - if value.nineteen_or_more_seats() { - eml_nl::utils::ElectionSubcategory::GR2 - } else { - eml_nl::utils::ElectionSubcategory::GR1 - } - } - ElectionType::Ps => { - if value.has_only_one_district() { - eml_nl::utils::ElectionSubcategory::PS1 - } else { - eml_nl::utils::ElectionSubcategory::PS2 - } - } - ElectionType::Ws => { - if value.nineteen_or_more_seats() { - eml_nl::utils::ElectionSubcategory::AB2 - } else { - eml_nl::utils::ElectionSubcategory::AB1 - } - } - ElectionType::Ep => eml_nl::utils::ElectionSubcategory::EP, - ElectionType::Kc | ElectionType::Kcni => { - todo!("Kiescolleges don't have an official code yet in EML-NL") - } - ElectionType::Er => eml_nl::utils::ElectionSubcategory::ER1, - } - } -} - -impl TryFrom for eml_nl::documents::nomination::NominationElectionIdentifier { - type Error = AppError; - - fn try_from(value: ElectionConfig) -> Result { - let category = eml_nl::utils::ElectionCategory::from(value.election_type()); - let year = value.election_date().year(); - - let id = if let Some(region) = value.region_title(crate::AnyLocale::Nl) { - format!( - "{}{}_{}", - category.to_eml_value(), - year, - slugify_teletex(region, false) - ) - } else { - format!("{}{}", category.to_eml_value(), year) - }; - - Ok(NominationElectionIdentifier::builder() - .name(value.full_formal_title(crate::core::ModelLocale::Nl)) - .category(category) - .subcategory(&value) - .election_date(value.election_date()) - .nomination_date(value.nomination_day_date()) - .id(ElectionId::new(id)?) - .build_for_nomination()?) - } -} - impl From<&FullName> for eml_nl::common::PersonNameStructure { fn from(val: &FullName) -> Self { eml_nl::common::PersonNameStructure::new(PersonName { @@ -250,7 +167,7 @@ fn nomination_proposer( }) } -/// Build the EML 2.10 candidate nomination XML for a candidate list. +/// Build the EML 210 candidate nomination XML for a candidate list. pub fn eml210( store: &PgStore, election: &ElectionConfig, @@ -310,7 +227,9 @@ pub fn eml210( ) .issue_date(now.date_naive()) .creation_date_time(now) - .election_identifier(NominationElectionIdentifier::try_from(*election)?) + .election_identifier( + ElectionIdentifierBuilder::try_from(*election)?.build_for_nomination()?, + ) .contest_identifier(if election.has_only_one_district() { NominationContestIdentifier::new(ContestId::geen(), "") } else if list.contains_all_districts(election) { @@ -430,7 +349,7 @@ mod tests { // verify check_eml( &String::from_utf8(eml).unwrap(), - include_str!("testdata/ek27.eml.xml"), + include_str!("testdata/210-ek27.eml.xml"), ) .await; } @@ -458,7 +377,7 @@ mod tests { // verify check_eml( &String::from_utf8(eml).unwrap(), - include_str!("testdata/ps27-1.eml.xml"), + include_str!("testdata/210-ps27-1.eml.xml"), ) .await; } @@ -487,7 +406,7 @@ mod tests { // verify check_eml( &String::from_utf8(eml).unwrap(), - include_str!("testdata/ps27-2.eml.xml"), + include_str!("testdata/210-ps27-2.eml.xml"), ) .await; } @@ -515,7 +434,7 @@ mod tests { // verify check_eml( &String::from_utf8(eml).unwrap(), - include_str!("testdata/ws27.eml.xml"), + include_str!("testdata/210-ws27.eml.xml"), ) .await; } diff --git a/src/models/eml/mod.rs b/src/models/eml/mod.rs new file mode 100644 index 000000000..fbcc926a2 --- /dev/null +++ b/src/models/eml/mod.rs @@ -0,0 +1,94 @@ +pub(crate) mod eml110a; +pub(crate) mod eml210; + +use chrono::Datelike; +use eml_nl::{ + documents::ElectionIdentifierBuilder, + utils::{ElectionCategory, ElectionId, ElectionSubcategory}, +}; + +use crate::{ + AnyLocale, AppError, ElectionConfig, + core::{ElectionType, ModelLocale}, + utils::slugify_teletex, +}; + +impl From for ElectionCategory { + fn from(value: ElectionType) -> Self { + match value { + ElectionType::Tk => ElectionCategory::TK, + ElectionType::Ek => ElectionCategory::EK, + ElectionType::Gr => ElectionCategory::GR, + ElectionType::Ps => ElectionCategory::PS, + ElectionType::Ws => ElectionCategory::AB, + ElectionType::Ep => ElectionCategory::EP, + ElectionType::Kc | ElectionType::Kcni => { + todo!("Kiescolleges don't have an official code yet in EML-NL") + } + ElectionType::Er => ElectionCategory::ER, + } + } +} + +impl From<&ElectionConfig> for ElectionSubcategory { + fn from(value: &ElectionConfig) -> Self { + match value.election_type() { + ElectionType::Tk => ElectionSubcategory::TK, + ElectionType::Ek => ElectionSubcategory::EK, + ElectionType::Gr => { + if value.nineteen_or_more_seats() { + ElectionSubcategory::GR2 + } else { + ElectionSubcategory::GR1 + } + } + ElectionType::Ps => { + if value.has_only_one_district() { + ElectionSubcategory::PS1 + } else { + ElectionSubcategory::PS2 + } + } + ElectionType::Ws => { + if value.nineteen_or_more_seats() { + ElectionSubcategory::AB2 + } else { + ElectionSubcategory::AB1 + } + } + ElectionType::Ep => ElectionSubcategory::EP, + ElectionType::Kc | ElectionType::Kcni => { + todo!("Kiescolleges don't have an official code yet in EML-NL") + } + ElectionType::Er => ElectionSubcategory::ER1, + } + } +} + +impl TryFrom for ElectionIdentifierBuilder { + type Error = AppError; + + fn try_from(value: ElectionConfig) -> Result { + let category = ElectionCategory::from(value.election_type()); + let year = value.election_date().year(); + + let id = if let Some(region) = value.region_title(AnyLocale::Nl) { + format!( + "{}{}_{}", + category.to_eml_value(), + year, + slugify_teletex(region, false) + ) + } else { + format!("{}{}", category.to_eml_value(), year) + }; + + Ok(ElectionIdentifierBuilder::new() + .id(ElectionId::new(id)?) + .name(value.full_formal_title(ModelLocale::Nl)) + .category(category) + .subcategory(&value) + .election_date(value.election_date()) + .nomination_date(value.nomination_day_date())) + } +} diff --git a/src/models/eml/testdata/110a-ek27.eml.xml b/src/models/eml/testdata/110a-ek27.eml.xml new file mode 100644 index 000000000..e6b18cb56 --- /dev/null +++ b/src/models/eml/testdata/110a-ek27.eml.xml @@ -0,0 +1,35 @@ + + + 1 + 2026-07-28T10:20:41.452788305+00:00 + 2026-07-28 + + + + + Verkiezing van de Eerste Kamer der Staten-Generaal 2027 + EK + EK + 2027-05-24 + 2027-04-20 + + + + SPV + + + + + 75 + 25 + + + Kiesraad Demo + + + Andere Partij + + + + + diff --git a/src/models/eml/testdata/110a-ps27-1.eml.xml b/src/models/eml/testdata/110a-ps27-1.eml.xml new file mode 100644 index 000000000..09ca2bba5 --- /dev/null +++ b/src/models/eml/testdata/110a-ps27-1.eml.xml @@ -0,0 +1,35 @@ + + + 1 + 2026-07-28T10:20:41.460114746+00:00 + 2026-07-28 + + + + + Verkiezing van de provinciale staten van Groningen 2027 + PS + PS1 + 2027-03-17 + 2027-02-01 + + + + SPV + + + + + 43 + 25 + + + Kiesraad Demo + + + Andere Partij + + + + + diff --git a/src/models/eml/testdata/110a-ps27-2.eml.xml b/src/models/eml/testdata/110a-ps27-2.eml.xml new file mode 100644 index 000000000..782534c0d --- /dev/null +++ b/src/models/eml/testdata/110a-ps27-2.eml.xml @@ -0,0 +1,35 @@ + + + 1 + 2026-07-28T10:20:41.460389664+00:00 + 2026-07-28 + + + + + Verkiezing van de provinciale staten van Limburg 2027 + PS + PS2 + 2027-03-17 + 2027-02-01 + + + + SPV + + + + + 47 + 25 + + + Kiesraad Demo + + + Andere Partij + + + + + diff --git a/src/models/eml/testdata/110a-ws27.eml.xml b/src/models/eml/testdata/110a-ws27.eml.xml new file mode 100644 index 000000000..edaae9bed --- /dev/null +++ b/src/models/eml/testdata/110a-ws27.eml.xml @@ -0,0 +1,32 @@ + + + 1 + 2026-07-28T10:20:41.460522024+00:00 + 2026-07-28 + + + + + Verkiezing van het algemeen bestuur van het waterschap Fryslân 2027 + AB + AB2 + 2027-03-17 + 2027-02-01 + + + + SPV + + + + + 21 + 25 + + + Water Water + + + + + diff --git a/src/models/testdata/ek27.eml.xml b/src/models/eml/testdata/210-ek27.eml.xml similarity index 100% rename from src/models/testdata/ek27.eml.xml rename to src/models/eml/testdata/210-ek27.eml.xml diff --git a/src/models/testdata/ps27-1.eml.xml b/src/models/eml/testdata/210-ps27-1.eml.xml similarity index 100% rename from src/models/testdata/ps27-1.eml.xml rename to src/models/eml/testdata/210-ps27-1.eml.xml diff --git a/src/models/testdata/ps27-2.eml.xml b/src/models/eml/testdata/210-ps27-2.eml.xml similarity index 100% rename from src/models/testdata/ps27-2.eml.xml rename to src/models/eml/testdata/210-ps27-2.eml.xml diff --git a/src/models/testdata/ws27.eml.xml b/src/models/eml/testdata/210-ws27.eml.xml similarity index 100% rename from src/models/testdata/ws27.eml.xml rename to src/models/eml/testdata/210-ws27.eml.xml diff --git a/src/models/mod.rs b/src/models/mod.rs index e49c08264..fc24dfd1e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -11,11 +11,11 @@ //! round-trip test and the `pdf_diff` development tool. //! //! [`mod@documents`] collects the store data for a candidate list and streams -//! the rendered models plus the [`mod@eml210`] nomination export as a ZIP +//! the rendered models plus the [`mod@eml::eml210`] nomination export as a ZIP //! download. pub(crate) mod documents; -pub(crate) mod eml210; +pub(crate) mod eml; pub mod examples; mod fonts; pub mod h1;