Skip to content

Commit 14acf0a

Browse files
committed
added download-or-update-course-exercises and fixed related issues
1 parent b14ba62 commit 14acf0a

File tree

10 files changed

+129
-15
lines changed

10 files changed

+129
-15
lines changed

tmc-langs-cli/src/app.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,16 @@ fn create_core_app() -> App<'static, 'static> {
357357
.long("submission-url")
358358
.takes_value(true)))
359359

360+
.subcommand(SubCommand::with_name("download-or-update-course-exercises")
361+
.about("Downloads exercises. If downloading an exercise that has been downloaded before, the student file policy will be used to avoid overwriting student files, effectively just updating the exercise files")
362+
.long_about(SCHEMA_NULL)
363+
.arg(Arg::with_name("exercise-id")
364+
.help("Exercise id of an exercise that should be downloaded. Multiple ids can be given.")
365+
.long("exercise-id")
366+
.required(true)
367+
.takes_value(true)
368+
.multiple(true)))
369+
360370
.subcommand(SubCommand::with_name("download-or-update-exercises")
361371
.about("Downloads exercises. If downloading an exercise on top of an existing one, the student file policy will be used to avoid overwriting student files, effectively just updating the exercise files")
362372
.long_about(SCHEMA_NULL)

tmc-langs-cli/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ mod projects_config;
55
mod tmc_config;
66

77
pub use credentials::Credentials;
8-
pub use projects_config::ProjectsConfig;
8+
pub use projects_config::{CourseConfig, Exercise, ProjectsConfig};
99
pub use tmc_config::{ConfigValue, TmcConfig};
1010

1111
use anyhow::{Context, Error};

tmc-langs-cli/src/config/projects_config.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,17 @@ pub struct CourseConfig {
6060
}
6161

6262
impl CourseConfig {
63-
pub fn save_to_projects_dir(self, projects_dir: &Path) -> Result<()> {
64-
let target = projects_dir.join(&self.course);
63+
pub fn save_to_projects_dir(&self, projects_dir: &Path) -> Result<()> {
64+
let course_dir = projects_dir.join(&self.course);
65+
if !course_dir.exists() {
66+
fs::create_dir_all(&course_dir).with_context(|| {
67+
format!(
68+
"Failed to create course directory at {}",
69+
course_dir.display()
70+
)
71+
})?;
72+
}
73+
let target = course_dir.join("course_config.toml");
6574
let s = toml::to_string_pretty(&self)?;
6675
fs::write(&target, s.as_bytes())
6776
.with_context(|| format!("Failed to write course config to {}", target.display()))?;

tmc-langs-cli/src/main.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ mod output;
77

88
use anyhow::{Context, Result};
99
use clap::{ArgMatches, Error, ErrorKind};
10-
use config::{Credentials, TmcConfig};
10+
use config::ProjectsConfig;
11+
use config::{CourseConfig, Credentials, Exercise, TmcConfig};
1112
use error::{InvalidTokenError, SandboxTestError};
1213
use output::{
1314
CombinedCourseData, ErrorData, Kind, Output, OutputData, OutputResult, Status, Warnings,
@@ -789,6 +790,84 @@ fn run_core(
789790
});
790791
print_output(&output, pretty, &warnings)?
791792
}
793+
("download-or-update-course-exercises", Some(matches)) => {
794+
let exercise_ids = matches.values_of("exercise-id").unwrap();
795+
796+
// collect exercise into (id, path) pairs
797+
let exercises = exercise_ids
798+
.into_iter()
799+
.map(into_usize)
800+
.collect::<Result<_>>()?;
801+
let exercises_details = core.get_exercises_details(exercises)?;
802+
803+
let tmc_config = TmcConfig::load(client_name)?;
804+
805+
let projects_dir = tmc_config.projects_dir;
806+
let mut projects_config = ProjectsConfig::load(&projects_dir)?;
807+
808+
let mut course_data = HashMap::<String, Vec<(String, String)>>::new();
809+
let mut exercises_and_paths = vec![];
810+
for exercise_detail in exercises_details {
811+
// get course and exercise name from server
812+
let ex_details = core.get_exercise_details(exercise_detail.id)?;
813+
// check if the checksum is different from what's already on disk
814+
if let Some(course_config) = projects_config.courses.get(&ex_details.course_name) {
815+
if let Some(exercise) = course_config.exercises.get(&ex_details.exercise_name) {
816+
if exercise_detail.checksum == exercise.checksum {
817+
// skip this exercise
818+
log::info!(
819+
"Skipping exercise {} ({} in {}) due to identical checksum",
820+
exercise_detail.id,
821+
ex_details.course_name,
822+
ex_details.exercise_name
823+
);
824+
continue;
825+
}
826+
}
827+
}
828+
829+
let target = ProjectsConfig::get_exercise_download_target(
830+
&projects_dir,
831+
&ex_details.course_name,
832+
&ex_details.exercise_name,
833+
);
834+
835+
let entry = course_data.entry(ex_details.course_name);
836+
let course_exercises = entry.or_default();
837+
course_exercises.push((ex_details.exercise_name, exercise_detail.checksum));
838+
839+
exercises_and_paths.push((exercise_detail.id, target));
840+
}
841+
core.download_or_update_exercises(exercises_and_paths)
842+
.context("Failed to download exercises")?;
843+
844+
for (course_name, exercise_names) in course_data {
845+
let mut exercises = HashMap::new();
846+
for (exercise_name, checksum) in exercise_names {
847+
exercises.insert(exercise_name, Exercise { checksum });
848+
}
849+
850+
if let Some(course_config) = projects_config.courses.get_mut(&course_name) {
851+
course_config.exercises.extend(exercises);
852+
course_config.save_to_projects_dir(&projects_dir)?;
853+
} else {
854+
let course_config = CourseConfig {
855+
course: course_name,
856+
exercises,
857+
};
858+
course_config.save_to_projects_dir(&projects_dir)?;
859+
};
860+
}
861+
862+
let output = Output::OutputData::<()>(OutputData {
863+
status: Status::Finished,
864+
message: None,
865+
result: OutputResult::RetrievedData,
866+
percent_done: 1.0,
867+
data: None,
868+
});
869+
print_output(&output, pretty, &warnings)?
870+
}
792871
("download-or-update-exercises", Some(matches)) => {
793872
let mut exercise_args = matches.values_of("exercise").unwrap();
794873

tmc-langs-core/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub enum CoreError {
4444
NotLoggedIn,
4545
#[error("Failed to find cache directory")]
4646
CacheDir,
47+
#[error("No values found in exercise details map returned by server")]
48+
MissingDetailsValue,
4749

4850
#[error(transparent)]
4951
SystemTime(#[from] std::time::SystemTimeError),

tmc-langs-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub use oauth2;
1919
pub use request::FeedbackAnswer;
2020
pub use response::{
2121
Course, CourseData, CourseDataExercise, CourseDataExercisePoint, CourseDetails, CourseExercise,
22-
Exercise, ExerciseDetails, NewSubmission, Organization, Review, Submission,
22+
Exercise, ExerciseChecksums, ExerciseDetails, NewSubmission, Organization, Review, Submission,
2323
SubmissionFeedbackResponse, SubmissionFinished, SubmissionProcessingStatus, SubmissionStatus,
2424
UpdateResult, User,
2525
};

tmc-langs-core/src/response.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ pub struct ExerciseDetails {
210210
pub submissions: Vec<ExerciseSubmission>,
211211
}
212212

213+
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
214+
pub struct ExerciseChecksums {
215+
pub id: usize,
216+
pub checksum: String,
217+
}
218+
213219
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
214220
pub struct Submission {
215221
pub id: usize,

tmc-langs-core/src/tmc_core.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ impl TmcCore {
287287
"Downloaded exercise {} to '{}'. ({} out of {})",
288288
exercise_id,
289289
target.display(),
290-
n,
290+
n + 1,
291291
exercises_len
292292
),
293293
progress,
@@ -329,7 +329,7 @@ impl TmcCore {
329329
pub fn get_exercises_details(
330330
&self,
331331
exercise_ids: Vec<usize>,
332-
) -> Result<Vec<ExerciseDetails>, CoreError> {
332+
) -> Result<Vec<ExerciseChecksums>, CoreError> {
333333
self.core_exercise_details(exercise_ids)
334334
}
335335

tmc-langs-core/src/tmc_core/api.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use crate::error::CoreError;
44
use crate::response::ErrorResponse;
55
use crate::{
66
Course, CourseData, CourseDataExercise, CourseDataExercisePoint, CourseDetails, CourseExercise,
7-
ExerciseDetails, FeedbackAnswer, NewSubmission, Organization, Review, Submission,
8-
SubmissionFeedbackResponse, TmcCore, User,
7+
ExerciseChecksums, ExerciseDetails, FeedbackAnswer, NewSubmission, Organization, Review,
8+
Submission, SubmissionFeedbackResponse, TmcCore, User,
99
};
1010
use oauth2::TokenResponse;
1111
use reqwest::{
@@ -610,7 +610,7 @@ impl TmcCore {
610610
pub(super) fn core_exercise_details(
611611
&self,
612612
exercise_ids: Vec<usize>,
613-
) -> Result<Vec<ExerciseDetails>, CoreError> {
613+
) -> Result<Vec<ExerciseChecksums>, CoreError> {
614614
if self.token.is_none() {
615615
return Err(CoreError::NotLoggedIn);
616616
}
@@ -623,7 +623,15 @@ impl TmcCore {
623623
.collect::<Vec<_>>()
624624
.join(","),
625625
);
626-
self.get_json_with_params(&url_tail, &[exercise_ids])
626+
627+
// returns map with result in key "exercises"
628+
let res: HashMap<String, Vec<ExerciseChecksums>> =
629+
self.get_json_with_params(&url_tail, &[exercise_ids])?;
630+
if let Some((_, val)) = res.into_iter().next() {
631+
// just return whatever value is found first
632+
return Ok(val);
633+
}
634+
Err(CoreError::MissingDetailsValue)
627635
}
628636

629637
pub(super) fn download_solution(

tmc-langs-core/tests/compare_server_results.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
use dotenv::dotenv;
55
use std::env;
6-
use std::path::Path;
6+
use std::path::PathBuf;
77
use std::thread;
88
use std::time::Duration;
99
use tmc_langs_core::{CoreError, Exercise, SubmissionProcessingStatus, SubmissionStatus, TmcCore};
@@ -60,7 +60,7 @@ fn dl_test_submit_course_solutions(course_id: usize) {
6060
.join("solution/download")
6161
.unwrap();
6262
dl_test_submit_exercise(&core, exercise, |target| {
63-
core.download_model_solution(solution_url, target)
63+
core.download_model_solution(solution_url, &target)
6464
});
6565
}
6666

@@ -106,13 +106,13 @@ where
106106
// downloader should download the submission target to the path arg
107107
fn dl_test_submit_exercise<F>(core: &TmcCore, exercise: Exercise, downloader: F)
108108
where
109-
F: FnOnce(&Path) -> Result<(), CoreError>,
109+
F: FnOnce(PathBuf) -> Result<(), CoreError>,
110110
{
111111
log::debug!("submitting exercise {:#?}", exercise);
112112
let temp = tempfile::tempdir().unwrap();
113113
let submission_path = temp.path().join(exercise.id.to_string());
114114
log::debug!("downloading to {}", submission_path.display());
115-
downloader(&submission_path).unwrap();
115+
downloader(submission_path.clone()).unwrap();
116116

117117
log::debug!("testing locally {}", submission_path.display());
118118
let test_results = core.run_tests(&submission_path, &mut vec![]).unwrap();

0 commit comments

Comments
 (0)