Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "time", "fmt", "std"] }
dotenv = "0.15.0"
time = { version = "0.3.44", features = ["formatting"] }
csv = "1.3"
5 changes: 5 additions & 0 deletions docs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ Database seeding can be enabled by setting `SEEDING_ENABLED` to `true` in the `.
# Seed toggle
SEEDING_ENABLED=true
```
If CSV file for member list is available, you can import it using
```
cargo run --bin import_csv "Member List.csv"
```
You may need to create appropriate columns if it doesn't exist.

## Core Features
### Member Management
Expand Down
8 changes: 8 additions & 0 deletions migrations/20251114124741_add_social_media.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Add migration script here
-- Add social media and contact columns to Member table
ALTER TABLE Member
ADD COLUMN phone_number VARCHAR(20),
ADD COLUMN gitlab_user VARCHAR(255),
ADD COLUMN instagram_handle VARCHAR(255),
ADD COLUMN twitter_handle VARCHAR(255),
ADD COLUMN linkedin_url TEXT;
31 changes: 31 additions & 0 deletions src/bin/import_csv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use sqlx::PgPool;
use std::env;

include!("../database_seeder/import_members.rs");

#[tokio::main]
async fn main() {
let _ = dotenv::dotenv();

let database_url = env::var("ROOT_DB_URL").expect("ROOT_DB_URL must be set");
let csv_path = env::args()
.nth(1)
.unwrap_or_else(|| "Member List.csv".to_string());

println!("Starting CSV import from: {}", csv_path);
println!("Connecting to database...");

let pool = PgPool::connect(&database_url)
.await
.expect("Failed to connect to database");

println!("Connected to database");
println!("Importing members...\n");

match import_members_from_csv(&pool, &csv_path).await {
Ok(_) => println!("\nImport completed successfully!"),
Err(e) => eprintln!("\nImport failed: {}", e),
}

pool.close().await;
}
90 changes: 90 additions & 0 deletions src/database_seeder/import_members.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use csv::ReaderBuilder;
use std::error::Error;

pub async fn import_members_from_csv(pool: &sqlx::PgPool, csv_path: &str) -> Result<(), Box<dyn Error>> {
let mut reader = ReaderBuilder::new()
.has_headers(true)
.from_path(csv_path)?;

let mut success_count = 0;
let mut error_count = 0;

for result in reader.records() {
let record = result?;

if record.iter().all(|field| field.trim().is_empty()) {
continue;
}

let roll_no = record.get(0).unwrap_or("").trim();
let email = record.get(1).unwrap_or("").trim();
let phone_number = record.get(2).unwrap_or("").trim();
let github = record.get(3).unwrap_or("").trim();
let gitlab = record.get(4).unwrap_or("").trim();
let _discord_username = record.get(5).unwrap_or("").trim();
let instagram = record.get(6).unwrap_or("").trim();
let twitter = record.get(7).unwrap_or("").trim();
let linkedin = record.get(8).unwrap_or("").trim();

if roll_no.is_empty() || email.is_empty() {
continue;
}

let github_user = get_username(github);
let gitlab_user = get_username(gitlab);

let result = sqlx::query!(
r#"
UPDATE Member
SET
phone_number = $2,
github_user = $3,
gitlab_user = $4,
instagram_handle = $5,
twitter_handle = $6,
linkedin_url = $7
WHERE LOWER(roll_no) = LOWER($1)
"#,
roll_no,
if phone_number.is_empty() { None } else { Some(phone_number) },
github_user,
gitlab_user,
if instagram.is_empty() { None } else { Some(instagram) },
if twitter.is_empty() { None } else { Some(twitter) },
if linkedin.is_empty() { None } else { Some(linkedin) }
)
.execute(pool)
.await;

match result {
Ok(query_result) => {
if query_result.rows_affected() > 0 {
success_count += 1;
println!("Updated member: {}", roll_no);
} else {
println!("Member not found: {}", roll_no);
}
}
Err(e) => {
error_count += 1;
eprintln!("Error updating {}: {}", roll_no, e);
}
}
}

println!("Successfully updated: {}", success_count);
println!("Errors: {}", error_count);

Ok(())
}

fn get_username(url: &str) -> Option<String> {
if url.is_empty() {
return None;
}

let parts: Vec<&str> = url.trim_end_matches('/').split('/').collect();
parts.last()
.filter(|s| !s.is_empty() && **s != "github.com" && **s != "gitlab.com")
.map(|s| s.to_string())
}
5 changes: 5 additions & 0 deletions src/models/member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ pub struct Member {
pub group_id: i32,
pub track: Option<String>,
pub github_user: Option<String>,
pub phone_number: Option<String>,
pub gitlab_user: Option<String>,
pub instagram_handle: Option<String>,
pub twitter_handle: Option<String>,
pub linkedin_url: Option<String>,
pub created_at: NaiveDateTime,
}

Expand Down
Loading