use std::collections::HashMap;
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use reqwest::blocking::Response;
use crate::dropbox::results_file::AsCsv;
use crate::rubric::Rubric;
use crate::helpers::web;
use crate::dropbox::fingerprint::Fingerprint;
use crate::TIMESTAMP_FORMAT;
pub type TestData = HashMap<String, String>;
fn default_timestamp_format() -> String {
String::from(TIMESTAMP_FORMAT)
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct Submission {
pub time: DateTime<Local>,
pub grade: isize,
pub data: TestData,
pub late: bool,
pub passed: Vec<String>,
pub failed: Vec<String>,
#[serde(default = "default_timestamp_format")]
timestamp_format: String,
fingerprint: Option<Fingerprint>
}
impl Submission {
pub fn new() -> Submission {
Submission {
time: Local::now(),
grade: 0,
data: TestData::new(),
passed: Vec::new(),
failed: Vec::new(),
timestamp_format: default_timestamp_format(),
late: false,
fingerprint: None
}
}
pub fn use_data(&mut self, data: TestData) {
self.data = data;
}
pub fn from_data(data: TestData) -> Self {
let mut sub = Submission::new();
sub.use_data(data);
sub
}
pub fn set_fingerprint(&mut self, secret: &str) {
self.fingerprint = Some(Fingerprint::from_secret(secret));
}
pub fn fingerprint(&self) -> &Option<Fingerprint> {
&self.fingerprint
}
fn addition(&mut self, to_add: isize, message: &str) {
self.grade += to_add;
self.passed.push(format!("{} (+{})", message, to_add));
}
fn penalty(&mut self, to_penalize: isize, message: &str) {
self.grade -= to_penalize;
self.failed.push(format!("{} (-{})", message, to_penalize));
}
pub fn grade_against(&mut self, rubric: &mut Rubric) {
if rubric.past_final_deadline() {
eprintln!("Final deadline ({}) has passed.", rubric.final_deadline.unwrap());
eprintln!("Your instructor has chosen to not allow late submission");
eprintln!("This submission will be recorded, but with a grade of 0");
self.penalty(self.grade, "Past final deadline");
return;
}
if rubric.past_due() {
self.late = true;
self.penalty(rubric.late_penalty, "Late submission");
let how_late = rubric.deadline
.unwrap()
.signed_duration_since(Local::now())
.num_days()
.abs() + 1;
let daily_penalty = rubric.daily_penalty * how_late as isize;
self.penalty(daily_penalty, &format!("{} days late", how_late));
if !rubric.allow_late {
eprintln!("Deadline ({}) has passed.", rubric.deadline.unwrap());
eprintln!("Your instructor has chosen to not allow late submission");
eprintln!("This submission will be recorded, but with a grade of 0");
self.penalty(self.grade, "Past deadline");
return;
}
}
for crit in &mut rubric.sorted().into_iter() {
if crit.test_with_data(&self.data) {
self.addition(crit.worth, &crit.name);
} else {
self.penalty(0, &crit.name);
}
}
}
pub fn submit(&self, url: &str) -> Result<Response, reqwest::Error> {
web::post_json(url, self)
}
pub fn set_timestamp_format(&mut self, new_format: &str) {
self.timestamp_format = String::from(new_format);
}
}
impl AsCsv for TestData {
fn as_csv(&self) -> String {
let mut v: Vec<_> = self.into_iter().collect();
v.sort_by(|x,y| x.0.cmp(&y.0));
v.iter().map(|v| v.1.replace(",", ";") ).collect::<Vec<_>>().join(",")
}
fn filename(&self) -> String {
String::from("submission_data.csv")
}
fn header(&self) -> String {
let mut v: Vec<_> = self.into_iter().collect();
v.sort_by(|x,y| x.0.cmp(&y.0));
v.iter().map(|v| v.0.to_owned() ).collect::<Vec<_>>().join(",")
}
}
impl AsCsv for Submission {
fn as_csv(&self) -> String {
let mut csv = format!(
"{},{},{},{},{},{}",
self.time.format(&self.timestamp_format),
self.late,
self.grade,
self.passed.join(";"),
self.failed.join(";"),
self.data.as_csv()
);
if let Some(fp) = &self.fingerprint {
csv = format!("{},{}", csv, fp.as_csv());
}
csv
}
fn filename(&self) -> String {
String::from("submissions.csv")
}
fn header(&self) -> String {
let mut header = format!("time,late,grade,passed,failed,{}", self.data.header());
if let Some(fp) = &self.fingerprint {
header = format!("{},{}", header, fp.header());
}
header
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{data, yaml, attach};
#[test]
fn test_new_submission() {
let sub = Submission::new();
assert!(sub.data.len() == 0);
}
#[test]
fn test_submission_use_data() {
let data = data! {
"key" => "value"
};
let mut sub = Submission::new();
sub.use_data(data);
assert!(sub.data.len() == 1);
assert_eq!(sub.data["key"], "value");
let sub2 = Submission::from_data(data! {
"key" => "value"
});
assert_eq!(sub2.data["key"], "value");
}
#[test]
fn test_submission_as_csv() {
let sub = Submission::from_data(data! { "a" => "v", "b" => "v2" });
assert!((&sub).as_csv().contains("v,v2"));
let sub2 = Submission::new();
let expected = "0,,,";
assert!((&sub2).as_csv().contains(expected));
}
#[test]
fn test_serialize_deserialize_json() {
let mut sub = Submission::from_data(data! { "k2" => "v2", "k" => "v" });
sub.passed.push(String::from("something"));
sub.failed.push(String::from("something"));
assert!(serde_json::to_string(&sub).unwrap().contains(r#""k2":"v2""#));
let data = r#"{"time":"2020-05-01T22:23:21.180875-05:00","late":false,"grade":0,"passed":["something"],"failed":["something"],"data":{"k2":"v2","k":"v"}}"#;
let built_sub: Submission = serde_json::from_str(data).unwrap();
assert_eq!(built_sub.grade, sub.grade);
}
#[test]
fn test_grade_against_rubric() {
let yaml = yaml!("../../test_data/test_rubric.yml").unwrap();
let mut rubric = Rubric::from_yaml(yaml).unwrap();
let test = |_: &TestData| true;
attach! {
rubric,
"first_crit" => test
};
let mut sub = Submission::new();
sub.grade_against(&mut rubric);
assert_eq!(sub.grade, 50);
}
#[test]
fn test_test_data_as_csv() {
let d = data! {
"b2" => "value2",
"a1" => "value1"
};
let expected_header = "a1,b2";
let expected_values = "value1,value2";
let expected_filename = "submission_data.csv";
assert_eq!(d.header(), expected_header);
assert_eq!(d.as_csv(), expected_values);
assert_eq!(d.filename(), expected_filename);
}
#[test]
fn test_as_csv_replaces_commas() {
let sub = Submission::from_data(data! {
"key" => "value with, comma"
});
assert!(sub.as_csv().contains("value with; comma"));
}
#[test]
fn test_test_data_gets_sorted() {
let data = data! {
"a" => "something",
"b" => "else"
};
let csv = data.as_csv();
assert!(csv.contains("something,else"));
}
#[test]
fn test_custom_timestamp_format() {
let mut sub = Submission::new();
assert_eq!(sub.timestamp_format, TIMESTAMP_FORMAT);
assert!(format!("{}", sub.time.format(&sub.timestamp_format)).len() > 0);
sub.set_timestamp_format("some other format");
assert_eq!(sub.timestamp_format, "some other format");
}
#[test]
fn test_grading_past_due() {
let yaml = yaml!("../../test_data/past_due_rubric.yml").unwrap();
let mut past_due_rubric = Rubric::from_yaml(yaml).unwrap();
let mut sub = Submission::new();
assert_eq!(sub.grade, 0);
sub.grade_against(&mut past_due_rubric);
assert_eq!(sub.grade, -5);
}
#[test]
fn test_add_fingerprint() {
let mut sub = Submission::new();
assert!(sub.fingerprint.is_none());
sub.set_fingerprint("secret key");
assert!(sub.fingerprint.is_some());
}
#[test]
fn test_submission_as_csv_with_fingerprint() {
let mut sub = Submission::new();
sub.set_fingerprint("secret key");
assert!(sub.header().contains("secret,platform"));
}
}