308 lines
9.7 KiB
Rust
308 lines
9.7 KiB
Rust
use crate::util::surrealdb::DB;
|
|
|
|
use super::{Member, MembersMigration};
|
|
|
|
use once_cell::sync::Lazy;
|
|
use std::collections::HashMap;
|
|
use surrealdb::sql::statements::{BeginStatement, CommitStatement};
|
|
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: Vec::new(),
|
|
diploma: self.diploma.clone(),
|
|
registration_token: None,
|
|
}
|
|
}
|
|
|
|
// Get the hour data from the raw string
|
|
fn generate_hours(&self) -> Vec<String> {
|
|
let mut hours: Vec<String> = Vec::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.push(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(" ")
|
|
}
|
|
}
|
|
|
|
impl MembersMigration {
|
|
// 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 {
|
|
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(¤t_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.registration_token,
|
|
})
|
|
} else {
|
|
// Remove member
|
|
removed_members.push(current_member);
|
|
}
|
|
}
|
|
|
|
for new_member in new_members_list {
|
|
// Insert new member
|
|
if !current_members_map.contains_key(&new_member.id) {
|
|
inserted_members.push(new_member);
|
|
}
|
|
}
|
|
|
|
Self {
|
|
inserted: inserted_members,
|
|
updated: updated_members,
|
|
removed: removed_members,
|
|
}
|
|
}
|
|
|
|
pub async fn migrate_proposal(
|
|
csv: String,
|
|
) -> Result<(u16, MembersMigration), Box<dyn std::error::Error>> {
|
|
let rows = Self::csv_to_rows(csv)?;
|
|
|
|
let new_members = Self::rows_to_members(rows);
|
|
|
|
let current_members = Member::fetch_all().await?;
|
|
|
|
let members_migration = Self::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 mut 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 transaction = DB.query(BeginStatement::default());
|
|
|
|
for member in members_migration.updated.clone() {
|
|
let id = member.id.clone();
|
|
|
|
transaction = transaction.query(format!("UPDATE type::thing('member', $id_{id}) SET name.first = $name_first_{id}, name.full = $name_full_{id}, hours = $hours_{id}, groups = $groups_{id}, diploma = $diploma_{id};"))
|
|
.bind((format!("id_{id}"), member.id))
|
|
.bind((format!("name_first_{id}"), member.name.first))
|
|
.bind((format!("name_full_{id}"), member.name.full))
|
|
.bind((format!("hours_{id}"), member.hours))
|
|
.bind((format!("groups_{id}"), member.groups))
|
|
.bind((format!("diploma_{id}"), member.diploma));
|
|
}
|
|
|
|
for member in members_migration.inserted.clone() {
|
|
let id = member.id.clone();
|
|
|
|
transaction = transaction.query(format!("CREATE type::thing('member', $id_{id}) SET name.first = $name_first_{id}, name.full = $name_full_{id}, hours = $hours_{id}, groups = $groups_{id}, diploma = $diploma_{id};"))
|
|
.bind((format!("id_{id}"), member.id))
|
|
.bind((format!("name_first_{id}"), member.name.first))
|
|
.bind((format!("name_full_{id}"), member.name.full))
|
|
.bind((format!("hours_{id}"), member.hours))
|
|
.bind((format!("groups_{id}"), member.groups))
|
|
.bind((format!("diploma_{id}"), member.diploma));
|
|
}
|
|
|
|
for member in members_migration.removed.clone() {
|
|
let id = member.id.clone();
|
|
|
|
transaction = transaction
|
|
.query(format!("DELETE type::thing('member', $id_{id});"))
|
|
.bind((format!("id_{id}"), member.id));
|
|
}
|
|
|
|
transaction
|
|
.query(CommitStatement::default())
|
|
.await?
|
|
.check()?;
|
|
|
|
members_store.remove(&id);
|
|
|
|
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: Vec::from([
|
|
"Wedstrijd".to_string(),
|
|
"Z5".to_string(),
|
|
"Zaterdag".to_string(),
|
|
]),
|
|
groups: vec![],
|
|
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: Vec::from(["Z5".to_string(), "Zaterdag".to_string()]),
|
|
groups: Vec::new(),
|
|
diploma: Some("ZR4".to_string()),
|
|
registration_token: None,
|
|
},
|
|
];
|
|
|
|
let rows = match MembersMigration::csv_to_rows(data) {
|
|
Ok(r) => r,
|
|
Err(err) => return Err(err.to_string()),
|
|
};
|
|
|
|
let members = MembersMigration::rows_to_members(rows);
|
|
|
|
assert_eq!(expected, members);
|
|
|
|
Ok(())
|
|
}
|
|
}
|