Commit 407dc9d4 authored by Robert Czechowski's avatar Robert Czechowski
Browse files

Database structure and migration mechanism

parents
//#[cfg(feature = "preserve_order")]
//pub type Map<K, V> = linked_hash_map::LinkedHashMap<K, V>;
extern crate linked-hash-map;
use linked_hash_map::LinkedHashMap
#[derive(Serialize, Deserialize)]
struct TaskgroupJson {
location: String,
stars: Option<u8>,
}
#[derive(Serialize, Deserialize)]
struct TaskgroupJson {
name: String,
location: Option<String>,
variants: Option<Vec<Task>>,
locations: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)]
struct ContestJson {
name: String,
participation_start: Option<DateTime<Utc>>,
participation_end: Option<DateTime<Utc>>,
duration_minutes: u32,
public_listing: Option<bool>,
tasks: LinkedHashMap<String, serde_json::Value>,
}
use std::fs;
use std::io::Read;
use db_conn::MedalConnection;
pub fn test<C: MedalConnection>(conn: &mut C) {
let mut paths: Vec<_> =
fs::read_dir(format!("migrations/{}", conn.dbtype()))
.unwrap()
.map(|r| r.unwrap())
.filter(|r| r.path()
.display()
.to_string()
.ends_with(".sql"))
.collect();
paths.sort_by_key(|dir| dir.path());
for path in paths {
println!("Found: {}.", path.path().display());
let filename = path.file_name().into_string().unwrap();
if ! conn.migration_already_applied(&filename) {
let mut file = fs::File::open(path.path()).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
conn.apply_migration(&filename, contents);
} else {
println!("Already applied.")
}
}
}
use db_objects::*;
pub trait MedalConnection {
fn create() -> Self;
fn dbtype(&self) -> &'static str;
fn migration_already_applied(&mut self, name: &str) -> bool;
fn apply_migration(&mut self, name: &str, contents: String);
fn get_session(&mut self, key: String) -> Option<SessionUser>;
fn new_session(&mut self) -> SessionUser;
fn get_session_or_new(&mut self, key: String) -> SessionUser;
fn login(&mut self, session: &SessionUser, username: String, password: String) -> Result<SessionUser,()>;
fn login_with_code(&mut self, session: &SessionUser, logincode: String) -> Result<SessionUser,()>;
fn logout(&mut self, session: &SessionUser);
fn load_submission(&mut self, session: &SessionUser, task: String, subtask: Option<String>) -> Submission;
fn submit_submission(&mut self, session: &SessionUser, task: String, subtask: Option<String>, submission: Submission);
fn get_contest_by_id(&mut self, contest_id : u32) -> Contest;
fn get_contest_by_id_complete(&mut self, contest_id : u32) -> Contest;
fn get_task_by_id(&mut self, task_id : u32) -> Task;
fn get_task_by_id_complete(&mut self, task_id : u32) -> (Task, Taskgroup, Contest);
fn get_submission_to_validate(&mut self, tasklocation: String, subtask: Option<String>) -> u32;
fn find_next_submission_to_validate(&mut self, userid: u32, taskgroupid: u32);
}
pub trait MedalObject<T: MedalConnection> {
fn save(&mut self, conn: &mut T);
}
extern crate rusqlite;
use self::rusqlite::Connection;
use db_conn::{MedalConnection, MedalObject};
use db_objects::*;
impl MedalConnection for Connection {
fn create() -> Connection {
Connection::open("blub.db").unwrap()
}
fn dbtype(&self) -> &'static str {
return "sqlite";
}
fn migration_already_applied(&mut self, name: &str) -> bool {
let create_string = "CREATE TABLE IF NOT EXISTS migrations (name TEXT PRIMARY KEY);";
self.execute(create_string, &[]).unwrap();
let mut stmt = self.prepare("SELECT name FROM migrations WHERE name = ?1").unwrap();
stmt.exists(&[&name]).unwrap()
}
fn apply_migration(&mut self, name: &str, contents: String) {
print!("Applying migration `{}` … ", name);
let tx = self.transaction().unwrap();
tx.execute(&contents, &[]).unwrap();
tx.execute("INSERT INTO migrations (name) VALUES (?1)", &[&name]).unwrap();
tx.commit().unwrap();
println!("OK.");
}
fn get_session(&mut self, key: String) -> Option<SessionUser> {
self.query_row("SELECT id, session_token, csrf_token, last_login, last_activity, permanent_login, username, password, logincode, email, email_unconfirmed, email_confirmation_code, firstname, lastname, street, zip, city, nation, grade, is_teacher, managed_by, pms_id, pms_school_id FROM session_user WHERE session_token = ?1", &[&key], |row| {
SessionUser {
id: row.get(0),
session_token: Some(key.clone()),
csrf_token: row.get(1),
last_login: row.get(2),
last_activity: row.get(3),
permanent_login: row.get(4),
username: row.get(5),
password: row.get(6),
logincode: row.get(7),
email: row.get(8),
email_unconfirmed: row.get(9),
email_confirmationcode: row.get(10),
firstname: row.get(11),
lastname: row.get(12),
street: row.get(13),
zip: row.get(14),
city: row.get(15),
nation: row.get(16),
grade: row.get(17),
is_teacher: row.get(18),
managed_by: row.get(19),
pms_id: row.get(20),
pms_school_id: row.get(21),
}
}).ok()
}
fn new_session(&mut self) -> SessionUser {
let session_token = "123".to_string();
let csrf_token = "123".to_string();
self.execute("INSERT INTO session_user (session_token, csrf_token)
VALUES (?1, ?2)",
&[&session_token, &csrf_token]).unwrap();
let id = self.query_row("SELECT last_insert_rowid()", &[], |row| {row.get(0)}).unwrap();
SessionUser::minimal(id, session_token, csrf_token)
}
fn get_session_or_new(&mut self, key: String) -> SessionUser {
self.get_session(key).unwrap_or_else(|| self.new_session())
}
fn login(&mut self, session: &SessionUser, username: String, password: String) -> Result<SessionUser,()> {unimplemented!()}
fn login_with_code(&mut self, session: &SessionUser, logincode: String) -> Result<SessionUser,()> {unimplemented!()}
fn logout(&mut self, session: &SessionUser) {
self.execute("UPDATE session_user SET session_token = NULL WHERE id = ?1", &[&session.id]).unwrap();
}
fn load_submission(&mut self, session: &SessionUser, task: String, subtask: Option<String>) -> Submission {unimplemented!()}
fn submit_submission(&mut self, session: &SessionUser, task: String, subtask: Option<String>, submission: Submission) {unimplemented!()}
fn get_contest_by_id(&mut self, contest_id : u32) -> Contest {
self.query_row("SELECT location, filename, name, duration, public, start, end FROM contest WHERE id = ?1", &[&contest_id], |row| {
Contest {
id: Some(contest_id),
location: row.get(0),
filename: row.get(1),
name: row.get(2),
duration: row.get(3),
public: row.get(4),
start: row.get(5),
end: row.get(6),
taskgroups: Vec::new(),
}
}).unwrap()
}
fn get_contest_by_id_complete(&mut self, contest_id : u32) -> Contest {
let mut stmt = self.prepare("SELECT contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, taskgroup.id, taskgroup.name, task.id, task.location, task.stars FROM contest JOIN taskgroup ON contest.id = taskgroup.contest JOIN task ON taskgroup.id = task.taskgroup WHERE contest.id = ?1").unwrap();
let mut taskgroupcontest_iter = stmt.query_map(&[&contest_id], |row| {
(Contest {
id: Some(contest_id),
location: row.get(0),
filename: row.get(1),
name: row.get(2),
duration: row.get(3),
public: row.get(4),
start: row.get(5),
end: row.get(6),
taskgroups: Vec::new(),
},Taskgroup {
id: Some(row.get(7)),
contest: contest_id,
name: row.get(8),
tasks: Vec::new(),
},Task {
id: Some(row.get(9)),
taskgroup: row.get(7),
location: row.get(10),
stars: row.get(11),
})
}).unwrap();
let (mut contest, mut taskgroup, task) = taskgroupcontest_iter.next().unwrap().unwrap();
taskgroup.tasks.push(task);
for tgc in taskgroupcontest_iter {
if let Ok((_, tg, t)) = tgc {
if tg.id != taskgroup.id {
contest.taskgroups.push(taskgroup);
taskgroup = tg;
}
taskgroup.tasks.push(t);
}
}
contest.taskgroups.push(taskgroup);
contest
}
fn get_task_by_id(&mut self, task_id : u32) -> Task {
self.query_row(
"SELECT location, stars, taskgroup WHERE id = ?1",
&[&task_id],
|row| {
Task {
id: Some(task_id),
taskgroup: row.get(2),
location: row.get(0),
stars: row.get(1)
}
}).unwrap()
}
fn get_task_by_id_complete(&mut self, task_id : u32) -> (Task, Taskgroup, Contest) {
self.query_row(
"SELECT task.location, task.stars, taskgroup.id, taskgroup.name, contest.id, contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date JOIN taskgroup ON taskgroup = taskgroup.id JOIN contest ON taskgroup.contest = contest.id WHERE task.id = ?1",
&[&task_id],
|row| {
(Task {
id: Some(task_id),
taskgroup: row.get(2),
location: row.get(0),
stars: row.get(1),
}, Taskgroup {
id: Some(row.get(2)),
contest: row.get(4),
name: row.get(3),
tasks: Vec::new(),
}, Contest {
id: Some(row.get(4)),
location: row.get(5),
filename: row.get(6),
name: row.get(7),
duration: row.get(8),
public: row.get(9),
start: row.get(10),
end: row.get(11),
taskgroups: Vec::new(),
})
}).unwrap()
}
fn get_submission_to_validate(&mut self, tasklocation: String, subtask: Option<String>) -> u32{
match subtask {
Some(st) => self.query_row("SELECT id FROM submission JOIN task ON submission.task = task.id WHERE task.location = ?1 AND subtask_identifier = ?2 AND needs_validation = 1 LIMIT 1", &[&tasklocation, &st], |row| {row.get(0)}).unwrap(),
None => self.query_row("SELECT id FROM submission JOIN task ON submission.task = task.id WHERE task.location = ?1 AND needs_validation = 1 LIMIT 1", &[&tasklocation], |row| {row.get(0)}).unwrap(),
}
}
fn find_next_submission_to_validate(&mut self, userid: u32, taskgroupid: u32) {
let (id, validated) : (u32, bool) = self.query_row("SELECT id, validated FROM submission JOIN task ON submission.task = task.id WHERE task.taskgroup = ?1 AND submission.user = ?2 ORDER BY value DESC id DESC LIMIT 1", &[&taskgroupid, &userid], |row| {(row.get(0), row.get(1))}).unwrap();;
if !validated {
self.execute("UPDATE submission SET needs_validation = 1 WHERE id = ?1", &[&id]).unwrap();
}
}
}
impl MedalObject<Connection> for Task {
fn save(&mut self, conn: &mut Connection) {
conn.query_row("SELECT id FROM task WHERE taskgroup = ?1 AND location = ?2", &[&self.taskgroup, &self.location], |row| {row.get(0)})
.and_then(|id| { self.setId(id); Ok(()) });
let id = match self.getId() {
Some(id) => {
conn.execute(
"UPDATE task SET taskgroup = ?1, location = ?2, stars = ?3
WHERE id = ?4",
&[&self.taskgroup, &self.location, &self.stars, &id]).unwrap();
id
}
None => {
conn.execute(
"INSERT INTO task (taskgroup, location, stars)
VALUES (?1, ?2, ?3)",
&[&self.taskgroup, &self.location, &self.stars]).unwrap();
conn.query_row("SELECT last_insert_rowid()", &[], |row| {row.get(0)}).unwrap()
}
};
self.setId(id);
}
}
impl MedalObject<Connection> for Taskgroup {
fn save(&mut self, conn: &mut Connection) {
conn.query_row("SELECT id FROM taskgroup WHERE contest = ?1 AND name = ?2", &[&self.contest, &self.name], |row| {row.get(0)})
.and_then(|id| { self.setId(id); Ok(()) });
let id = match self.getId() {
Some(id) => {
conn.execute(
"UPDATE taskgroup SET contest = ?1, name = ?2
WHERE id = ?3",
&[&self.contest, &self.name, &id]).unwrap();
id
}
None => {
conn.execute(
"INSERT INTO taskgroup (contest, name)
VALUES (?1, ?2)",
&[&self.contest, &self.name]).unwrap();
conn.query_row("SELECT last_insert_rowid()", &[], |row| {row.get(0)}).unwrap()
}
};
self.setId(id);
for mut task in &mut self.tasks {
task.taskgroup = id;
task.save(conn);
}
}
}
impl MedalObject<Connection> for Contest {
fn save(&mut self, conn: &mut Connection) {
conn.query_row("SELECT id FROM contest WHERE location = ?1 AND filename = ?2", &[&self.location, &self.filename], |row| {row.get(0)})
.and_then(|id| { self.setId(id); Ok(()) });
let id = match self.getId() {
Some(id) => {
conn.execute(
"UPDATE contest SET location = ?1,filename = ?2,
name = ?3, duration = ?4, public = ?5, start_date = ?6,
end_date = ?7 WHERE id = ?8",
&[&self.location, &self.filename, &self.name,
&self.duration, &self.public, &self.start, &self.end,
&id]).unwrap();
id
}
None => {
conn.execute(
"INSERT INTO contest (location, filename, name, duration, public, start_date, end_date)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
&[&self.location, &self.filename, &self.name,
&self.duration, &self.public, &self.start, &self.end]).unwrap();
conn.query_row("SELECT last_insert_rowid()", &[], |row| {row.get(0)}).unwrap()
}
};
self.setId(id);
for mut taskgroup in &mut self.taskgroups {
taskgroup.contest = id;
taskgroup.save(conn);
}
}
}
impl MedalObject<Connection> for Grade {
fn save(&mut self, conn: &mut Connection) {
conn.execute("INSERT OR REPLACE INTO grade (taskgroup, user, grade, validated) VALUES (?1, ?2, ?3, ?4)", &[&self.taskgroup, &self.user, &self.grade, &self.validated]).unwrap();
}
}
impl MedalObject<Connection> for Participation {
fn save(&mut self, conn: &mut Connection) {
conn.execute("INSERT INTO participation (contest, user, start_date) VALUES (?1, ?2, ?3)", &[&self.contest, &self.user, &self.start]).unwrap();
}
}
extern crate time;
use self::time::Timespec;
pub struct SessionUser {
pub id: u32,
pub session_token: Option<String>, // delete this to log out
pub csrf_token: String,
pub last_login: Option<Timespec>,
pub last_activity: Option<Timespec>,
pub permanent_login: bool,
pub username: Option<String>,
pub password: Option<String>,
pub logincode: Option<String>,
pub email: Option<String>,
pub email_unconfirmed: Option<String>,
pub email_confirmationcode: Option<String>,
pub firstname: Option<String>,
pub lastname: Option<String>,
pub street: Option<String>,
pub zip: Option<String>,
pub city: Option<String>,
pub nation: Option<String>,
pub grade: u8,
pub is_teacher: bool,
pub managed_by: Option<u32>,
pub pms_id: Option<u32>,
pub pms_school_id: Option<u32>,
}
pub struct Contest {
pub id: Option<u32>,
pub location: String,
pub filename: String,
pub name: String,
pub duration: u32,
pub public: bool,
pub start: Option<Timespec>,
pub end: Option<Timespec>,
pub taskgroups: Vec<Taskgroup>,
}
pub struct Taskgroup {
pub id: Option<u32>,
pub contest: u32,
pub name: String,
pub tasks: Vec<Task>,
}
pub struct Task {
pub id: Option<u32>,
pub taskgroup: u32,
pub location: String,
pub stars: u8,
}
pub struct Submission {
id: Option<u32>,
session_user: u32,
task: u32,
contest: u32,
grade: u32,
validated: bool,
nonvalidated_grade: u32,
subtask_identifier: Option<String>,
value: String,
date: Timespec,
}
pub struct Grade {
pub taskgroup: u32,
pub user: u32,
pub grade: u8,
pub validated: bool,
}
pub struct Participation {
pub contest: u32,
pub user: u32,
pub start: Timespec,
}
pub trait HasId { fn getId(&self) -> Option<u32>; fn setId(&mut self, id: u32); }
impl HasId for Submission { fn getId(&self) -> Option<u32> { self.id } fn setId(&mut self, id: u32) { self.id = Some(id);} }
impl HasId for Task { fn getId(&self) -> Option<u32> { self.id } fn setId(&mut self, id: u32) { self.id = Some(id);} }
impl HasId for Taskgroup { fn getId(&self) -> Option<u32> { self.id } fn setId(&mut self, id: u32) { self.id = Some(id);} }
impl HasId for Contest { fn getId(&self) -> Option<u32> { self.id } fn setId(&mut self, id: u32) { self.id = Some(id);} }
impl Contest {
pub fn new(location: String, filename: String, name: String, duration: u32, public: bool, start: Option<Timespec>, end: Option<Timespec>) -> Self {
Contest {
id: None,
location: location,
filename: filename,
name: name,
duration: duration,
public: public,
start: start,
end: end,
taskgroups: Vec::new(),
}
}
}
impl SessionUser {
pub fn minimal(id: u32, session_token: String, csrf_token: String) -> Self {
SessionUser {
id: id,
session_token: Some(session_token),
csrf_token: csrf_token,
last_login: None,
last_activity: None, // now?
permanent_login: false,
username: None,
password: None,
logincode: None,
email: None,
email_unconfirmed: None,
email_confirmationcode: None,
firstname: None,
lastname: None,
street: None,
zip: None,
city: None,
nation: None,
grade: 0,
is_teacher: false,
managed_by: None,
pms_id: None,
pms_school_id: None,
}
}
}
impl Taskgroup {
pub fn new(name: String) -> Self {
Taskgroup {
id: None,
contest: 0,
name: name,
tasks: Vec::new(),
}
}
}
impl Task {
pub fn new(location: String, stars: u8) -> Self {
Task {
id: None,
taskgroup: 0,
location: location,
stars: stars,
}
}
}
extern crate rusqlite;
use rusqlite::Connection;
mod db_apply_migrations;
mod db_conn_sqlite;
mod db_conn;
mod db_objects;
use db_conn::{MedalConnection, MedalObject};
use db_objects::*;
fn main() {
let mut conn = Connection::create();
db_apply_migrations::test(&mut conn);
let mut contest = Contest::new("./".to_string(), "blub.json".to_string(), "Wettbewerb IX".to_string(), 45, true, None, None);
let mut taskgroup = Taskgroup::new("Lustige Aufgabe".to_string());
let mut task = Task::new("blub".to_string(), 1);
taskgroup.tasks.push(task);