tried to start on the web part

This commit is contained in:
Adrian Gunnar Lauterer 2024-04-28 03:19:37 +02:00
parent a88045226b
commit 376c49bc92
Signed by: adriangl
GPG Key ID: D33368A59745C2F0
5 changed files with 1139 additions and 196 deletions

861
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
# for web server
rocket = { version = "0.5.0", features = ["json", "uuid"] }
rocket_dyn_templates = { version = "0.1.0", features = ["handlebars", "tera"] }
# for json serialization
serde = "1.0.196"
# for random number generation
@ -20,4 +21,7 @@ uuid = { version = "1.7.0", features = ["serde", "v4"] }
structopt = "0.3.23"
# for csv reading
csv = "1.1.6"
clap = { version = "4.5.3", features = ["derive"] }
clap = { version = "4.3.24", features = ["derive"] }
stv-rs = "0.3.0"

View File

@ -1,188 +1,61 @@
use std::fs::File;
use std::io::prelude::*;
use std::fs::OpenOptions;
use std::path::Path;
//import for taking application flags
#[macro_use]
extern crate rocket;
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)]
struct Ballot {
weight: i32,
candidates: Vec<i32>,
mod voting;
#[derive(rocket::FromForm)]
struct Vote {
option: String,
}
struct Candidate {
id: i32,
name: String,
type Votes = Mutex<Writer<std::fs::File>>;
#[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 {
num_candidates: i32,
num_seats: i32,
ballots: Vec<Ballot>,
candidates: Vec<Candidate>,
title: String,
#[post("/results")]
fn results() -> Json<HashMap<String, String>> {
let election_data = voting::csv_to_election_data("votes.csv", 1, "Election".to_string());
let blt = voting::election_data_to_blt(election_data);
let results = voting::run_blt(blt);
Json(results)
}
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;
#[get("/")]
fn index() -> Template {
let context: HashMap<&str, &str> = HashMap::new();
Template::render("index", &context)
}
#[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 {
//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");
let writer = csv::Writer::from_writer(file);
//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");
}
rocket::build()
.attach(Template::fairing())
.manage(Mutex::new(writer))
.mount("/", routes![index, vote])
.mount("/static", FileServer::from("static"))
}

37
src/templates/main.html Normal file
View File

@ -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>

210
src/voting.rs Normal file
View File

@ -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");
}
}