tried to start on the web part
This commit is contained in:
parent
a88045226b
commit
376c49bc92
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# for web server
|
# for web server
|
||||||
rocket = { version = "0.5.0", features = ["json", "uuid"] }
|
rocket = { version = "0.5.0", features = ["json", "uuid"] }
|
||||||
|
rocket_dyn_templates = { version = "0.1.0", features = ["handlebars", "tera"] }
|
||||||
# for json serialization
|
# for json serialization
|
||||||
serde = "1.0.196"
|
serde = "1.0.196"
|
||||||
# for random number generation
|
# for random number generation
|
||||||
|
@ -20,4 +21,7 @@ uuid = { version = "1.7.0", features = ["serde", "v4"] }
|
||||||
structopt = "0.3.23"
|
structopt = "0.3.23"
|
||||||
# for csv reading
|
# for csv reading
|
||||||
csv = "1.1.6"
|
csv = "1.1.6"
|
||||||
clap = { version = "4.5.3", features = ["derive"] }
|
clap = { version = "4.3.24", features = ["derive"] }
|
||||||
|
|
||||||
|
|
||||||
|
stv-rs = "0.3.0"
|
221
src/main.rs
221
src/main.rs
|
@ -1,188 +1,61 @@
|
||||||
use std::fs::File;
|
#[macro_use]
|
||||||
use std::io::prelude::*;
|
extern crate rocket;
|
||||||
use std::fs::OpenOptions;
|
|
||||||
use std::path::Path;
|
|
||||||
//import for taking application flags
|
|
||||||
|
|
||||||
|
use rocket::form::Form;
|
||||||
|
use rocket::response::Redirect;
|
||||||
|
use rocket::State; // Add this line
|
||||||
|
use rocket::fs::FileServer;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket_dyn_templates::Template;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use csv::Writer;
|
||||||
|
|
||||||
#[derive(Clone)]
|
mod voting;
|
||||||
struct Ballot {
|
|
||||||
weight: i32,
|
#[derive(rocket::FromForm)]
|
||||||
candidates: Vec<i32>,
|
struct Vote {
|
||||||
|
option: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Candidate {
|
type Votes = Mutex<Writer<std::fs::File>>;
|
||||||
id: i32,
|
|
||||||
name: String,
|
|
||||||
|
|
||||||
|
#[post("/vote", data = "<vote>")]
|
||||||
|
fn vote(vote: Form<Vote>, votes: &State<Votes>) -> Redirect {
|
||||||
|
let mut writer = votes.lock().unwrap();
|
||||||
|
writer.write_record(&[vote.option.clone()]).unwrap();
|
||||||
|
writer.flush().unwrap();
|
||||||
|
|
||||||
|
Redirect::to("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ElectionData {
|
#[post("/results")]
|
||||||
num_candidates: i32,
|
fn results() -> Json<HashMap<String, String>> {
|
||||||
num_seats: i32,
|
let election_data = voting::csv_to_election_data("votes.csv", 1, "Election".to_string());
|
||||||
ballots: Vec<Ballot>,
|
let blt = voting::election_data_to_blt(election_data);
|
||||||
candidates: Vec<Candidate>,
|
let results = voting::run_blt(blt);
|
||||||
title: String,
|
Json(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
fn election_data_to_blt(election_data: ElectionData) -> String {
|
fn index() -> Template {
|
||||||
//convert the election data to a blt file
|
let context: HashMap<&str, &str> = HashMap::new();
|
||||||
//format
|
Template::render("index", &context)
|
||||||
// 7 4 # 7 candidates, 4 seats
|
|
||||||
//1 1 3 0 # weigth, candidate ids, 0
|
|
||||||
// ...
|
|
||||||
//0
|
|
||||||
//"a" # candidate name
|
|
||||||
//...
|
|
||||||
//"title" # election title
|
|
||||||
|
|
||||||
let mut blt = String::new();
|
|
||||||
//push the number of candidates and seats
|
|
||||||
blt.push_str(&format!("{} {}\n", election_data.num_candidates, election_data.num_seats));
|
|
||||||
|
|
||||||
|
|
||||||
for ballot in &election_data.ballots {
|
|
||||||
blt.push_str(&format!("{} ", ballot.weight));
|
|
||||||
for candidate in &ballot.candidates {
|
|
||||||
blt.push_str(&format!("{} ", candidate)); // Use candidate directly
|
|
||||||
}
|
|
||||||
blt.push_str("0\n");
|
|
||||||
}
|
|
||||||
blt.push_str("0\n");
|
|
||||||
for candidate in &election_data.candidates {
|
|
||||||
blt.push_str(&format!("\"{}\"\n", candidate.name));
|
|
||||||
}
|
|
||||||
blt.push_str(&format!("\"{}\"\n", election_data.title));
|
|
||||||
|
|
||||||
return blt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[launch]
|
||||||
|
fn rocket() -> _ {
|
||||||
|
let file = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open("votes.csv")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> ElectionData {
|
let writer = csv::Writer::from_writer(file);
|
||||||
//read the csv file and convert it to a ballot
|
|
||||||
let delimiter = ",";
|
|
||||||
|
|
||||||
//read the csv file
|
rocket::build()
|
||||||
let mut file = File::open(csv).expect("file not found");
|
.attach(Template::fairing())
|
||||||
|
.manage(Mutex::new(writer))
|
||||||
//convert the csv file to a list of lists [ ['a', 'c', 'b'], ['b', 'a', 'd'] ] where each line is a array like ['a', 'c', 'b']
|
.mount("/", routes![index, vote])
|
||||||
let mut contents = String::new();
|
.mount("/static", FileServer::from("static"))
|
||||||
file.read_to_string(&mut contents).expect("something went wrong reading the file");
|
|
||||||
let lines: Vec<&str> = contents.split("\n").collect();
|
|
||||||
|
|
||||||
let mut votes: Vec<Vec<&str>> = Vec::new();
|
|
||||||
for line in lines {
|
|
||||||
let vote: Vec<&str> = line.split(delimiter).collect();
|
|
||||||
votes.push(vote);
|
|
||||||
}
|
|
||||||
|
|
||||||
//get all the unique candidates from all the votes and create a list of candidates
|
|
||||||
let mut names: Vec<&str> = Vec::new();
|
|
||||||
for vote in &votes {
|
|
||||||
for name in vote {
|
|
||||||
if !names.contains(name) {
|
|
||||||
names.push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let num_candidates = names.len() as i32;
|
|
||||||
|
|
||||||
let mut candidates: Vec<Candidate> = Vec::new();
|
|
||||||
for (i, name) in names.iter().enumerate() {
|
|
||||||
let candidate = Candidate {
|
|
||||||
id: (i + 1) as i32, // Add 1 to make it 1-indexed
|
|
||||||
name: name.to_string(),
|
|
||||||
};
|
|
||||||
candidates.push(candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
//generate the ballots
|
|
||||||
let mut ballots: Vec<Ballot> = Vec::new();
|
|
||||||
for vote in &votes {
|
|
||||||
let mut ballot = Ballot {
|
|
||||||
weight: 1,
|
|
||||||
candidates: Vec::new(),
|
|
||||||
};
|
|
||||||
for name in vote {
|
|
||||||
for candidate in &candidates {
|
|
||||||
if candidate.name == *name {
|
|
||||||
ballot.candidates.push(candidate.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ballots.push(ballot);
|
|
||||||
}
|
|
||||||
|
|
||||||
//deduplicate the ballots
|
|
||||||
let mut deduped_ballots: Vec<Ballot> = Vec::new();
|
|
||||||
for ballot in ballots {
|
|
||||||
let mut found = false;
|
|
||||||
for deduped_ballot in &deduped_ballots {
|
|
||||||
if deduped_ballot.candidates == ballot.candidates {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
deduped_ballots.push(ballot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ballots = deduped_ballots;
|
|
||||||
|
|
||||||
let election_data = ElectionData {
|
|
||||||
num_candidates: num_candidates,
|
|
||||||
num_seats: num_seats,
|
|
||||||
ballots: ballots,
|
|
||||||
candidates: candidates,
|
|
||||||
title: title,
|
|
||||||
};
|
|
||||||
return election_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
struct Args {
|
|
||||||
//number of seats to be elected
|
|
||||||
#[clap(short, long, default_value = "1")]
|
|
||||||
seats: i32,
|
|
||||||
//title of the election
|
|
||||||
#[clap(short, long)]
|
|
||||||
title: String,
|
|
||||||
//csv file containing the votes
|
|
||||||
#[clap(short, long)]
|
|
||||||
csv: String,
|
|
||||||
//output file
|
|
||||||
#[clap(short, long, default_value = "")]
|
|
||||||
output: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Args = Args::parse();
|
|
||||||
|
|
||||||
let election_data = csv_to_election_data(&args.csv, args.seats, args.title);
|
|
||||||
let blt = election_data_to_blt(election_data);
|
|
||||||
//if output is not specified, print to stdout
|
|
||||||
if args.output == "" {
|
|
||||||
println!("{}", blt);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_path = &args.output;
|
|
||||||
if Path::new(file_path).exists() {
|
|
||||||
panic!("File already exists");
|
|
||||||
} else {
|
|
||||||
let mut file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.create_new(true)
|
|
||||||
.open(file_path)
|
|
||||||
.expect("Failed to create file");
|
|
||||||
file.write_all(blt.as_bytes()).expect("write failed");
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Voting List</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css">
|
||||||
|
<style>
|
||||||
|
#sortable { list-style-type: none; margin: 0; padding: 0; width: 60%; }
|
||||||
|
#sortable li { margin: 5px; padding: 5px; font-size: 1.2em; height: 2em; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Vote List</h2>
|
||||||
|
<ul id="sortable">
|
||||||
|
<li class="ui-state-default"><input type="checkbox"> Item 1</li>
|
||||||
|
<li class="ui-state-default"><input type="checkbox"> Item 2</li>
|
||||||
|
</ul>
|
||||||
|
<button id="add">Add Item</button>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
|
||||||
|
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||||||
|
<script>
|
||||||
|
$( function() {
|
||||||
|
$( "#sortable" ).sortable();
|
||||||
|
$( "#sortable" ).disableSelection();
|
||||||
|
|
||||||
|
let count = 3;
|
||||||
|
$('#add').click(function() {
|
||||||
|
if ($('#sortable li').length < 10) {
|
||||||
|
$('#sortable').append('<li class="ui-state-default"><input type="checkbox"> Item ' + count++ + '</li>');
|
||||||
|
} else {
|
||||||
|
alert('Threshold reached');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,210 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
||||||
|
// https://docs.rs/stv-rs/0.3.0/stv_rs/all.html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
use stv_rs::parse::parse_election;
|
||||||
|
use stv_rs::meek::stv_droop;
|
||||||
|
use stv_rs::types::Election;
|
||||||
|
use stv_rs::types::ElectionResult;
|
||||||
|
|
||||||
|
pub fn blt_to_string_results(blt: String) -> String {
|
||||||
|
// Parse the blt string into an Election object
|
||||||
|
let election: Election = parse_election(&blt).unwrap();
|
||||||
|
|
||||||
|
// Run the meek election
|
||||||
|
let result: ElectionResult = stv_droop(&election).unwrap();
|
||||||
|
|
||||||
|
// Convert the ElectionResult object to a String
|
||||||
|
let string_result = format!("{:?}", result);
|
||||||
|
|
||||||
|
string_result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Ballot {
|
||||||
|
weight: i32,
|
||||||
|
candidates: Vec<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Candidate {
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ElectionData {
|
||||||
|
num_candidates: i32,
|
||||||
|
num_seats: i32,
|
||||||
|
ballots: Vec<Ballot>,
|
||||||
|
candidates: Vec<Candidate>,
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn election_data_to_blt(election_data: ElectionData) -> String {
|
||||||
|
//convert the election data to a blt file
|
||||||
|
//format
|
||||||
|
// 7 4 # 7 candidates, 4 seats
|
||||||
|
//1 1 3 0 # weigth, candidate ids, 0
|
||||||
|
// ...
|
||||||
|
//0
|
||||||
|
//"a" # candidate name
|
||||||
|
//...
|
||||||
|
//"title" # election title
|
||||||
|
|
||||||
|
let mut blt = String::new();
|
||||||
|
//push the number of candidates and seats
|
||||||
|
blt.push_str(&format!("{} {}\n", election_data.num_candidates, election_data.num_seats));
|
||||||
|
|
||||||
|
|
||||||
|
for ballot in &election_data.ballots {
|
||||||
|
blt.push_str(&format!("{} ", ballot.weight));
|
||||||
|
for candidate in &ballot.candidates {
|
||||||
|
blt.push_str(&format!("{} ", candidate)); // Use candidate directly
|
||||||
|
}
|
||||||
|
blt.push_str("0\n");
|
||||||
|
}
|
||||||
|
blt.push_str("0\n");
|
||||||
|
for candidate in &election_data.candidates {
|
||||||
|
blt.push_str(&format!("\"{}\"\n", candidate.name));
|
||||||
|
}
|
||||||
|
blt.push_str(&format!("\"{}\"\n", election_data.title));
|
||||||
|
|
||||||
|
return blt;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> ElectionData {
|
||||||
|
//read the csv file and convert it to a ballot
|
||||||
|
let delimiter = ",";
|
||||||
|
|
||||||
|
//read the csv file
|
||||||
|
let mut file = File::open(csv).expect("file not found");
|
||||||
|
|
||||||
|
//convert the csv file to a list of lists [ ['a', 'c', 'b'], ['b', 'a', 'd'] ] where each line is a array like ['a', 'c', 'b']
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).expect("something went wrong reading the file");
|
||||||
|
let lines: Vec<&str> = contents.split("\n").collect();
|
||||||
|
|
||||||
|
let mut votes: Vec<Vec<&str>> = Vec::new();
|
||||||
|
for line in lines {
|
||||||
|
let vote: Vec<&str> = line.split(delimiter).collect();
|
||||||
|
votes.push(vote);
|
||||||
|
}
|
||||||
|
|
||||||
|
//get all the unique candidates from all the votes and create a list of candidates
|
||||||
|
let mut names: Vec<&str> = Vec::new();
|
||||||
|
for vote in &votes {
|
||||||
|
for name in vote {
|
||||||
|
if !names.contains(name) {
|
||||||
|
names.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let num_candidates = names.len() as i32;
|
||||||
|
|
||||||
|
let mut candidates: Vec<Candidate> = Vec::new();
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
|
let candidate = Candidate {
|
||||||
|
id: (i + 1) as i32, // Add 1 to make it 1-indexed
|
||||||
|
name: name.to_string(),
|
||||||
|
};
|
||||||
|
candidates.push(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//generate the ballots
|
||||||
|
let mut ballots: Vec<Ballot> = Vec::new();
|
||||||
|
for vote in &votes {
|
||||||
|
let mut ballot = Ballot {
|
||||||
|
weight: 1,
|
||||||
|
candidates: Vec::new(),
|
||||||
|
};
|
||||||
|
for name in vote {
|
||||||
|
for candidate in &candidates {
|
||||||
|
if candidate.name == *name {
|
||||||
|
ballot.candidates.push(candidate.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ballots.push(ballot);
|
||||||
|
}
|
||||||
|
|
||||||
|
//deduplicate the ballots
|
||||||
|
let mut deduped_ballots: Vec<Ballot> = Vec::new();
|
||||||
|
for ballot in ballots {
|
||||||
|
let mut found = false;
|
||||||
|
for deduped_ballot in &deduped_ballots {
|
||||||
|
if deduped_ballot.candidates == ballot.candidates {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
deduped_ballots.push(ballot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ballots = deduped_ballots;
|
||||||
|
|
||||||
|
let election_data = ElectionData {
|
||||||
|
num_candidates: num_candidates,
|
||||||
|
num_seats: num_seats,
|
||||||
|
ballots: ballots,
|
||||||
|
candidates: candidates,
|
||||||
|
title: title,
|
||||||
|
};
|
||||||
|
return election_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
//number of seats to be elected
|
||||||
|
#[clap(short, long, default_value = "1")]
|
||||||
|
seats: i32,
|
||||||
|
//title of the election
|
||||||
|
#[clap(short, long)]
|
||||||
|
title: String,
|
||||||
|
//csv file containing the votes
|
||||||
|
#[clap(short, long)]
|
||||||
|
csv: String,
|
||||||
|
//output file
|
||||||
|
#[clap(short, long, default_value = "")]
|
||||||
|
output: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Args = Args::parse();
|
||||||
|
|
||||||
|
let election_data = csv_to_election_data(&args.csv, args.seats, args.title);
|
||||||
|
let blt = election_data_to_blt(election_data);
|
||||||
|
//if output is not specified, print to stdout
|
||||||
|
if args.output == "" {
|
||||||
|
println!("{}", blt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = &args.output;
|
||||||
|
if Path::new(file_path).exists() {
|
||||||
|
panic!("File already exists");
|
||||||
|
} else {
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(file_path)
|
||||||
|
.expect("Failed to create file");
|
||||||
|
file.write_all(blt.as_bytes()).expect("write failed");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue