diff --git a/src/bin/rusty-hektor.rs b/src/bin/rusty-hektor.rs index 47fa5a405f3170a9fcd25fee5871849f0bdcd0c7..e301991776950f99fe87789780ae44cf4d7b7741 100644 --- a/src/bin/rusty-hektor.rs +++ b/src/bin/rusty-hektor.rs @@ -10,7 +10,7 @@ use semver::Version; use serde_derive::Deserialize; use structopt::StructOpt; -use rusty_hektor::exam::{Exam, ExamSerializable, merge_exams}; +use rusty_hektor::exam::{merge_exams, Exam, ExamSerializable}; use rusty_hektor::module::Module; use rusty_hektor::parser::ipynb_parser::notebook::Notebook; use rusty_hektor::parser::xml_parser::XMLParser; @@ -47,6 +47,10 @@ struct Opt { #[structopt(long = "skip-text")] skip_text: bool, + /// Parse cloze type questions + #[structopt(long = "parse-cloze")] + parse_cloze: bool, + /// Disable latex rendering #[structopt(long = "no-latex")] no_latex: bool, @@ -102,6 +106,7 @@ fn main() -> Result<(), Box<dyn Error>> { let opt = Opt::from_args(); let parse_text = opt.skip_text.not(); + let parse_cloze = opt.parse_cloze; let render_latex = opt.no_latex.not(); let parsed_data: Result<Vec<Exam>, Box<dyn Error>> = opt @@ -118,7 +123,12 @@ fn main() -> Result<(), Box<dyn Error>> { ); std::process::exit(1) } else if extension == "zip" { - Ok(XMLParser::parse(path, parse_text, render_latex)?) + Ok(XMLParser::parse( + path, + parse_text, + parse_cloze, + render_latex, + )?) } else { Err(ParserError::new(format!( "Unsupported filetype: {:?}", @@ -129,7 +139,7 @@ fn main() -> Result<(), Box<dyn Error>> { .collect(); let parsed_data = parsed_data?; - let parsed_data= merge_exams(&parsed_data)?; + let parsed_data = merge_exams(&parsed_data)?; let mut serializable = parsed_data.into_serializable()?; diff --git a/src/errors.rs b/src/errors.rs index 53afd835456827b8562fe603d1461f3173bce44f..e0f208db1684b6e23c674eb853a1e3f6c1e04560 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,6 +5,7 @@ pub enum ValidationError { EmptyField(EmptyFieldError), InvalidStringLength(InvalidStringLengthError), InvalidMatNo(InvalidMatrNumber), + AttributeNotPresent(AttributeNotPresent), } impl fmt::Display for ValidationError { @@ -13,6 +14,7 @@ impl fmt::Display for ValidationError { ValidationError::EmptyField(ref e) => e.fmt(f), ValidationError::InvalidMatNo(ref e) => e.fmt(f), ValidationError::InvalidStringLength(ref e) => e.fmt(f), + ValidationError::AttributeNotPresent(ref e) => e.fmt(f), } } } @@ -107,3 +109,22 @@ impl fmt::Display for MergeError { } impl Error for MergeError {} + +#[derive(Debug)] +pub struct AttributeNotPresent { + attribute: &'static str, +} + +impl AttributeNotPresent { + pub fn new(attribute: &'static str) -> Self { + Self { attribute } + } +} + +impl fmt::Display for AttributeNotPresent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "attribute not found: \"{}\" ", self.attribute) + } +} + +impl Error for AttributeNotPresent {} diff --git a/src/exam.rs b/src/exam.rs index 7266d61e4ce28e11e4fa67f1455db9a5cd7027b1..4e303a793c752de77eb1ed47e0006bc76779a1a3 100644 --- a/src/exam.rs +++ b/src/exam.rs @@ -1,15 +1,15 @@ +use std::collections::{BTreeMap}; use std::collections::BTreeSet; -use std::collections::{BTreeMap, HashSet}; use std::error::Error; -use crate::errors::{EmptyFieldError, MergeError}; +use itertools::Itertools; + +use crate::errors::{EmptyFieldError}; use crate::meta_information::MetaInformation; use crate::module::Module; use crate::student::Student; use crate::student::StudentSerializable; use crate::submission_type::SubmissionType; -use crate::yes_or_no; -use itertools::Itertools; #[derive(Debug, Default)] pub struct Exam { @@ -74,7 +74,7 @@ pub fn merge_exams(exams: &[Exam]) -> Result<Exam, Box<dyn Error>> { Ok(Exam { module: None, submission_types, - students + students, }) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e9dba4bef1948707e6df952f3e3a39c337bebba8..b9fa35c19a7cdc667e6060838b75a6d582499efd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10,6 +10,7 @@ pub trait Parser { fn parse<'a>( path: &Path, parse_text_questions: bool, + parse_cloze_question: bool, render_latex: bool, ) -> result::Result<Exam, Box<dyn Error>>; } diff --git a/src/parser/xml_parser.rs b/src/parser/xml_parser.rs index bac39269a79459fb2aa637a23e8088fd189e6805..0e4a0b238bdd89dce0189059608219367e1794c2 100644 --- a/src/parser/xml_parser.rs +++ b/src/parser/xml_parser.rs @@ -16,7 +16,8 @@ use zip::ZipArchive; use lazy_static::lazy_static; use crate::errors::{ - EmptyFieldError, InvalidMatrNumber, InvalidStringLengthError, ValidationError, + AttributeNotPresent, EmptyFieldError, InvalidMatrNumber, InvalidStringLengthError, + ValidationError, }; use crate::exam::Exam; use crate::parser::{Parser, ParserError}; @@ -89,18 +90,26 @@ impl Parser for XMLParser { fn parse( path: &Path, parse_text_questions: bool, + parse_cloze_question: bool, render_latex: bool, ) -> result::Result<Exam, Box<dyn Error>> { let file = File::open(path)?; let mut archive = ZipArchive::new(file)?; let xml_files = XMLFiles::new(&mut archive)?; - let allowed_question_types: &[&str] = if parse_text_questions { - &["assSourceCode", "TEXT QUESTION"] - } else { - &["assSourceCode"] + let allowed_question_types: Vec<&str> = { + let mut allowed = vec!["assSourceCode"]; + if parse_text_questions { + allowed.push("TEXT QUESTION"); + } + if parse_cloze_question { + allowed.push("CLOZE QUESTION"); + } + allowed }; - let sub_types = extract_submission_types(&xml_files, allowed_question_types, render_latex)?; + + let sub_types = + extract_submission_types(&xml_files, &allowed_question_types, render_latex)?; let students = extract_students(&xml_files, &sub_types)?; Ok(Exam { @@ -195,9 +204,7 @@ fn extract_students( let element = item .element() .expect(&format!("No element for {:?} present", item)); - let mut matr_nr = element - .attribute_value("matr_nr") - .expect("No matr_nr attr found"); + let matr_nr = element.attribute_value("matr_nr"); let active_id = element .attribute_value("active_id") .expect("No active_id attr found"); @@ -205,22 +212,25 @@ fn extract_students( .attribute_value("fullname") .expect("No fullname attr found"); - match validate_mat_no(&matr_nr) { - Err(ValidationError::EmptyField(_)) => { + let matr_nr = match validate_mat_no(matr_nr) { + Err(ValidationError::EmptyField(_)) | Err(ValidationError::AttributeNotPresent(_)) => { warn!( "matr_nr for student {} is empty, falling back to active_id", fullname ); - matr_nr = active_id; + active_id } - Err(e) => warn!( - "matr_no of {} has wrong format: {}\n{}", - fullname, - matr_nr, - e.description() - ), - _ => {} - } + Err(e) => { + warn!( + "matr_no of {} has wrong format: {:?}\n Falling back to active_id\n{}", + fullname, + matr_nr, + e.description() + ); + active_id + } + Ok(()) => matr_nr.unwrap(), + }; let submissions = extract_submissions_for_student(results_doc, active_id, submission_types)?; @@ -238,7 +248,16 @@ fn extract_students( Ok(results) } -fn validate_mat_no(mat_no: &str) -> Result<(), ValidationError> { +fn validate_mat_no(mat_no: Option<&str>) -> Result<(), ValidationError> { + let mat_no = match mat_no { + Some(val) => val, + None => { + return Err(ValidationError::AttributeNotPresent( + AttributeNotPresent::new("matr_no"), + )) + } + }; + if mat_no.len() == 0 { Err(ValidationError::EmptyField(EmptyFieldError::new( "Unknown student", @@ -314,18 +333,28 @@ fn extract_submission_for_student( .into_iter() .last(); - let code = match oldest { - Some(node) => { - let elem = node - .element() - .expect(&format!("No element for {:?} present", node)); - String::from_utf8(decode( - elem.attribute_value("value1") - .expect("No value1 attr found, unable to parse code"), - )?)? + let result_el = oldest.map(|node| { + node.element() + .expect(&format!("No element for {:?} present", node)) + }); + + let encoded_attr = result_el.map(|el| { + let mut val = el + .attribute_value("value1") + .expect("No value1 attr found, unable to parse code"); + if val == "0" { + val = el + .attribute_value("value2") + .expect("value1 is 0 but value2 attr not found, unable to parse code"); } - None => "".to_owned(), + val + }); + + let code = match encoded_attr { + Some(val) => String::from_utf8(decode(val)?)?, + None => "".to_string(), }; + let submission_type = sub_type.name.clone(); Ok(Submission { diff --git a/src/submission_type.rs b/src/submission_type.rs index e5b36a3a1ccc60b99c29f5505a869c378df939e8..9b0e0568e9c867dbacc563a824c67c2572b008c7 100644 --- a/src/submission_type.rs +++ b/src/submission_type.rs @@ -67,7 +67,13 @@ impl SubmissionType { } } if self.programming_language.is_none() { - self.programming_language = Some(input("Programming language")); + self.programming_language = Some(input( + format!( + "Programming language (available: {})", + ProgrammingLang::list_available() + ) + .as_str(), + )); } Ok(()) @@ -116,11 +122,18 @@ pub enum ProgrammingLang { haskell, python, plaintext, + markdown, +} + +impl ProgrammingLang { + fn list_available() -> &'static str { + return "c, java, mipsasm, haskell, plaintext, markdown, python"; + } } #[derive(Debug, Display)] #[display( - fmt = "Unparseable programming language: {}. Allowed: c, java, mipsasm, haskell, python, plaintext", + fmt = "Unparseable programming language: {}. Allowed: c, java, mipsasm, haskell, python, plaintext, markdown", input )] pub struct ParseProgrammingLangError { @@ -139,6 +152,7 @@ impl FromStr for ProgrammingLang { "mipsasm" => ProgrammingLang::mipsasm, "haskell" => ProgrammingLang::haskell, "plaintext" => ProgrammingLang::plaintext, + "markdown" => ProgrammingLang::markdown, "python" => ProgrammingLang::python, _ => Err(ParseProgrammingLangError { input: s.to_owned(), diff --git a/tests/test_xml_parser.rs b/tests/test_xml_parser.rs index 788edd68772ac18f24859bc668dedad2ae3a7ca8..a95626d0028fd7817d465742ccde969b157c3830 100644 --- a/tests/test_xml_parser.rs +++ b/tests/test_xml_parser.rs @@ -7,7 +7,7 @@ use std::collections::{BTreeSet, HashSet}; #[test] fn can_parse_zipped_xml_data() -> Result<(), Box<dyn Error>> { - let parsed = XMLParser::parse(Path::new("tests/test.zip"), false, true)?; + let parsed = XMLParser::parse(Path::new("tests/test.zip"), false, false,true)?; let serializable = parsed.into_serializable()?; assert_eq!(1, serializable.submission_types.len()); assert_eq!(1, serializable.students.len()); @@ -16,7 +16,7 @@ fn can_parse_zipped_xml_data() -> Result<(), Box<dyn Error>> { #[test] fn parsed_xml_contains_correct_submission_types() -> Result<(), Box<dyn Error>> { - let parsed = XMLParser::parse(Path::new("tests/test.zip"), false, true)?; + let parsed = XMLParser::parse(Path::new("tests/test.zip"), false, false, true)?; let serializable = parsed.into_serializable()?; let submission_type_names: HashSet<String> = serializable @@ -37,7 +37,7 @@ fn parsed_xml_contains_correct_submission_types() -> Result<(), Box<dyn Error>> #[test] fn parsed_xls_contains_correct_students() -> Result<(), Box<dyn Error>> { - let parsed = XMLParser::parse(Path::new("tests/test.zip"), false, true)?; + let parsed = XMLParser::parse(Path::new("tests/test.zip"), false, false,true)?; let serializable = parsed.into_serializable()?; let students: BTreeSet<StudentSerializable> = serializable @@ -68,7 +68,7 @@ fn parsed_xls_contains_correct_students() -> Result<(), Box<dyn Error>> { fn correct_mapping_is_generated() -> Result<(), Box<dyn Error>> { use rusty_hektor::anonymizer; - let parsed = XMLParser::parse(Path::new("tests/test.zip"), false, true)?; + let parsed = XMLParser::parse(Path::new("tests/test.zip"), false, false,true)?; let mut serializable = parsed.into_serializable()?; let map = anonymizer::replace_students(&mut serializable.students);