wrbapp/src/util/model/member/migration.rs

304 lines
8.8 KiB
Rust

use crate::util::surrealdb::DB;
use super::{Member, MembersMigration};
use once_cell::sync::Lazy;
use std::collections::{BTreeSet, HashMap};
use tokio::sync::Mutex;
// Create a store for saving information when migrating to a new members list
static MEMBERS_STORE: Lazy<Mutex<MembersStore>> = Lazy::new(|| Mutex::new(MembersStore::new()));
struct MembersStore {
store: HashMap<u16, MembersMigration>,
count: u16,
}
impl MembersStore {
fn new() -> Self {
Self {
store: HashMap::new(),
count: 0,
}
}
fn insert(&mut self, input: MembersMigration) -> u16 {
let count = self.count + 1;
self.store.insert(count, input);
count
}
fn get(&self, key: &u16) -> Option<&MembersMigration> {
self.store.get(key)
}
fn remove(&mut self, key: &u16) -> Option<MembersMigration> {
self.store.remove(key)
}
}
// Create a row for the csv file
#[derive(Debug, serde::Deserialize)]
struct Row {
#[serde(rename = "Relatiecode")]
id: String,
#[serde(rename = "Roepnaam")]
first_name: String,
#[serde(rename = "Tussenvoegsel(s)")]
middle_name: String,
#[serde(rename = "Achternaam")]
last_name: String,
#[serde(rename = "E-mail")]
email: String,
#[serde(rename = "Verenigingssporten")]
hours: String,
#[serde(rename = "Diploma dropdown 1")]
diploma: Option<String>,
}
impl Row {
fn to_member(&self) -> Member {
Member {
id: self.id.trim().to_string(),
name: super::Name {
first: self.first_name.clone(),
full: self.generate_full_name(),
},
// hours: self.generate_hours(),
// groups: BTreeSet::new(),
// diploma: self.diploma.clone(),
// registration_token: None,
}
}
// Get the hour data from the raw string
fn generate_hours(&self) -> BTreeSet<String> {
let mut hours: BTreeSet<String> = BTreeSet::new();
let group_parts: Vec<&str> = self.hours.split(", ").collect();
for group in group_parts {
let hour_parts: Vec<&str> = group.split(" - ").collect();
for part in hour_parts {
if &*part != "Groep" {
hours.insert(part.to_string());
}
}
}
hours
}
// Generate the full name from 3 parts
fn generate_full_name(&self) -> String {
let mut parts: Vec<&str> = vec![];
parts.push(&self.first_name);
parts.push(&self.middle_name);
parts.push(&self.last_name);
let filtered_strings: Vec<&str> = parts.into_iter().filter(|s| !s.is_empty()).collect();
filtered_strings.join(" ")
}
}
// Convert the raw csv file to rust objects
fn csv_to_rows(input: String) -> Result<Vec<Row>, Box<dyn std::error::Error>> {
let mut members: Vec<Row> = vec![];
let mut rdr = csv::Reader::from_reader(input.as_bytes());
for result in rdr.deserialize() {
let row: Row = result?;
members.push(row);
}
Ok(members)
}
// Covert the rows to formatted members
fn rows_to_members(rows: Vec<Row>) -> Vec<Member> {
let mut members: Vec<Member> = vec![];
for row in rows {
members.push(row.to_member());
}
members
}
// Compare the new members list with the current
fn combine_members_lists(
new_members_list: Vec<Member>,
current_members_list: Vec<Member>,
) -> MembersMigration {
tracing::info!(
"Current: {}, New: {}",
current_members_list.len(),
new_members_list.len()
);
let current_members_map: HashMap<String, Member> = current_members_list
.clone()
.into_iter()
.map(|m| (m.id.clone(), m))
.collect();
let new_members_map: HashMap<String, Member> = new_members_list
.clone()
.into_iter()
.map(|m| (m.id.clone(), m))
.collect();
let mut inserted_members: Vec<Member> = vec![];
let mut updated_members: Vec<Member> = vec![];
let mut removed_members: Vec<Member> = vec![];
for current_member in current_members_list {
if let Some(new_member) = new_members_map.get(&current_member.id) {
// Update existing member
let new_member_clone = new_member.clone();
updated_members.push(Member {
id: current_member.id,
name: new_member_clone.name,
// hours: new_member_clone.hours,
// groups: current_member.groups,
// diploma: new_member_clone.diploma,
// registration_token: current_member.diploma,
})
} else {
// Remove member
removed_members.push(current_member);
}
}
for new_member in new_members_list {
// Insert new member
// TODO Generate registration token
if !current_members_map.contains_key(&new_member.id) {
inserted_members.push(new_member);
}
}
MembersMigration {
inserted: inserted_members,
updated: updated_members,
removed: removed_members,
}
}
impl Member {
pub async fn migrate_proposal(
csv: String,
) -> Result<(u16, MembersMigration), Box<dyn std::error::Error>> {
let rows = csv_to_rows(csv)?;
let new_members = rows_to_members(rows);
let current_members = Member::fetch_all().await?;
let members_migration = combine_members_lists(new_members, current_members);
let count = MEMBERS_STORE.lock().await.insert(members_migration.clone());
Ok((count, members_migration))
}
pub async fn migrate(id: u16) -> Result<(), Box<dyn std::error::Error>> {
let members_store = MEMBERS_STORE.lock().await;
let members_migration = match members_store.get(&id) {
Some(mm) => mm,
None => return Err("Could not get members from store".into()),
};
let mut query = String::from("BEGIN TRANSACTION;");
for member in members_migration.updated.clone() {
// TODO add hours and diploma support
query = query
+ format!(
"UPDATE member:{} SET name.first = \"{}\", name.full = \"{}\";",
member.id, member.name.first, member.name.full
)
.as_str();
}
for member in members_migration.inserted.clone() {
// TODO add hours and diploma support
query = query
+ format!(
"CREATE member:{} SET name.first = \"{}\", name.full = \"{}\";",
member.id, member.name.first, member.name.full
)
.as_str();
}
for member in members_migration.removed.clone() {
// TODO add hours and diploma support
query = query + format!("DELETE member:{};", member.id).as_str();
}
query = query + "COMMIT TRANSACTION;";
DB.query(query).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn csv_to_members_test() -> Result<(), String> {
let data = "Relatiecode,Volledige naam (1),Roepnaam,Tussenvoegsel(s),Achternaam,E-mail,2e E-mail,Verenigingssporten,Diploma dropdown 1
D000001,\"Last, First\",First,,Last,first.last@example.com,first.last@example.nl,\"Groep - Wedstrijd - Zaterdag, Groep - Z5 - Zaterdag\",LS1
D000002,\"Last2, First2\",First2,,Last2,first1.last@example.nl,,Groep - Z5 - Zaterdag,ZR4".to_string();
let expected = vec![
Member {
id: "D000001".to_string(),
name: super::super::Name {
first: "First".to_string(),
full: "First Last".to_string(),
},
// hours: BTreeSet::from([
// "Wedstrijd".to_string(),
// "Z5".to_string(),
// "Zaterdag".to_string(),
// ]),
// groups: BTreeSet::new(),
// diploma: Some("LS1".to_string()),
// registration_token: None,
},
Member {
id: "D000002".to_string(),
name: super::super::Name {
first: "First2".to_string(),
full: "First2 Last2".to_string(),
},
// hours: BTreeSet::from(["Z5".to_string(), "Zaterdag".to_string()]),
// groups: BTreeSet::new(),
// diploma: Some("ZR4".to_string()),
// registration_token: None,
},
];
let rows = match csv_to_rows(data) {
Ok(r) => r,
Err(err) => return Err(err.to_string()),
};
let members = rows_to_members(rows);
assert_eq!(expected, members);
Ok(())
}
}