Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code Test and Questions updates #100

Merged
merged 7 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 85 additions & 17 deletions tasky/src/handler/assignment.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io::Read;
use std::{collections::HashMap, io::Read};

use super::file_structure::*;
use actix_multipart::form::{json::Json, tempfile::TempFile, MultipartForm};
Expand Down Expand Up @@ -61,8 +61,90 @@ pub async fn handle_create_multipart(
true,
)?;

create_files_and_update_ids(&mut actual_files, mongodb, filename_map, &assignment).await;

let file_structure_value =
serde_json::to_value(file_structure).map_err(|_| ApiError::InternalServerError {
message: "Cannot convert file structure to JSON".to_string(),
})?;

assignment.file_structure = Some(file_structure_value);
assignment.runner_cpu = form.runner_config.runner_cpu.clone();
assignment.runner_memory = form.runner_config.runner_memory.clone();
assignment.runner_timeout = form.runner_config.runner_timeout.clone();
assignment.runner_cmd = form.runner_config.runner_cmd.clone();
AssignmentRepository::update_assignment(assignment.clone(), db);

Ok(assignment)
}

/// Handles file structure updates
/// This means storing data in postgres, files in mongo
/// and validating the input
pub async fn handle_update_multipart(
form: CreateCodeTestMultipart,
mongodb: &Database,
db: &mut DB,
mut assignment: Assignment,
) -> Result<Assignment, ApiError> {
let mut new_file_structure = form.file_structure.0;
if !file_structure_contains_files(&new_file_structure) {
return Err(ApiError::BadRequest {
message: "File structure does not contain any file".to_string(),
});
}

if let Some(ref file_structure_value) = assignment.file_structure {
let file_structure: AssignmentFileStructure =
serde_json::from_value(file_structure_value.clone()).unwrap();

let mut filename_map = build_filename_map(&form.files)?;
let mut new_files: Vec<&mut AssignmentFile> = vec![];

compare_structures(&file_structure, &new_file_structure)?;

// Validates file structures and gets new files to persist
validate_test_file_structure(
&mut new_file_structure,
&mut filename_map,
&mut new_files,
true,
)?;

create_files_and_update_ids(&mut new_files, mongodb, filename_map, &assignment.clone())
.await;

let file_structure_value = serde_json::to_value(new_file_structure).map_err(|_| {
ApiError::InternalServerError {
message: "Cannot convert file structure to JSON".to_string(),
}
})?;

assignment.file_structure = Some(file_structure_value);
assignment.runner_cpu = form.runner_config.runner_cpu.clone();
assignment.runner_memory = form.runner_config.runner_memory.clone();
assignment.runner_timeout = form.runner_config.runner_timeout.clone();
assignment.runner_cmd = form.runner_config.runner_cmd.clone();
AssignmentRepository::update_assignment(assignment.clone(), db);

return Ok(assignment);
}
Err(ApiError::BadRequest {
message: "The assignment does not have a file structure".to_string(),
})
}

/// Creates files and updates the correlated object_ids
/// Also updates files in file_structure, because the actual_files are referenced from the file structure
async fn create_files_and_update_ids(
#[allow(clippy::ptr_arg)]
actual_files: &mut Vec<&mut AssignmentFile>,
mongodb: &Database,
filename_map: HashMap<String, (bool, &TempFile)>,
assignment: &Assignment,
) {
let mut file_data: Vec<(String, String, usize)> = vec![];
for file in &mut actual_files {
for file in actual_files.iter_mut() {
let mut content = String::new();
let size = filename_map
.get(&file.filename)
Expand Down Expand Up @@ -91,21 +173,7 @@ pub async fn handle_create_multipart(
)
.await;

for (i, file) in actual_files.into_iter().enumerate() {
for (i, file) in actual_files.iter_mut().enumerate() {
file.object_id = Some(mongo_files.get(i).unwrap().to_hex());
}

let file_structure_value =
serde_json::to_value(file_structure).map_err(|_| ApiError::InternalServerError {
message: "Cannot convert file structure to JSON".to_string(),
})?;

assignment.file_structure = Some(file_structure_value);
assignment.runner_cpu = form.runner_config.runner_cpu.clone();
assignment.runner_memory = form.runner_config.runner_memory.clone();
assignment.runner_timeout = form.runner_config.runner_timeout.clone();
assignment.runner_cmd = form.runner_config.runner_cmd.clone();
AssignmentRepository::update_assignment(assignment.clone(), db);

Ok(assignment)
}
72 changes: 70 additions & 2 deletions tasky/src/handler/file_structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ pub fn validate_test_file_structure<'a>(
if structure.files.is_some() {
let folder_files = structure.files.as_mut().unwrap();
for file in folder_files {
// Only search for test files in this case
if (test_structure && file.is_test_file) || (!test_structure && !file.is_test_file) {
if (test_structure && file.is_test_file && file.object_id.is_none())
|| (!test_structure && !file.is_test_file)
{
let file_option = files.get(&file.filename);
if file_option.is_none() {
return Err(ApiError::BadRequest {
Expand All @@ -36,6 +37,7 @@ pub fn validate_test_file_structure<'a>(
}

let file_unwrapped = file_option.unwrap();
// File exists already in filename_map or exists in old file structure
if file_unwrapped.0 {
return Err(ApiError::BadRequest { message: format!("File {} exists twice in file structure. Even if the files are in different folders, they need to be named differently", file.filename) });
}
Expand Down Expand Up @@ -83,3 +85,69 @@ pub fn build_filename_map(
}
Ok(map)
}

/// Compares existing file structures and checks object_ids of existing files in from structure
/// and to structure.
pub fn compare_structures(
from: &AssignmentFileStructure,
to: &AssignmentFileStructure,
) -> Result<(), ApiError> {
for folder in to.folders.as_ref().unwrap_or(&default_structure_vec()) {
if let Some(ref from_folder) = get_folder_by_name(from, folder.current_folder_name.clone())
{
compare_structures(from_folder, folder)?;
}
}

for file in to.files.clone().unwrap_or(default_vec()) {
if let Some(from_file) = get_file_by_name(from, file.filename) {
if from_file.object_id.is_some()
&& file.object_id.is_some()
&& from_file.object_id.unwrap() != file.object_id.unwrap()
{
return Err(ApiError::BadRequest {
message: "Invalid carry object_id".to_string(),
});
}
}
}

Ok(())
}

fn get_folder_by_name(
structure: &AssignmentFileStructure,
name_option: Option<String>,
) -> Option<AssignmentFileStructure> {
if let Some(name) = name_option {
for folder in structure
.folders
.as_ref()
.unwrap_or(&default_structure_vec())
{
if let Some(ref folder_name) = folder.current_folder_name {
if *folder_name == name {
return Some(folder.clone());
}
}
}
}
None
}

fn get_file_by_name(structure: &AssignmentFileStructure, name: String) -> Option<AssignmentFile> {
for file in structure.files.as_ref().unwrap_or(&Vec::new()) {
if file.filename == name {
return Some(file.clone());
}
}
None
}

fn default_vec() -> Vec<AssignmentFile> {
vec![]
}

fn default_structure_vec() -> Vec<AssignmentFileStructure> {
vec![]
}
2 changes: 2 additions & 0 deletions tasky/src/response/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub struct AssignmentResponse {
pub runner_cpu: String,
pub runner_memory: String,
pub runner_timeout: String,
pub runnner_cmd: String,
}

/// Minified response returned for list views
Expand Down Expand Up @@ -159,6 +160,7 @@ impl Enrich<Assignment> for AssignmentResponse {
runner_cpu: from.runner_cpu.clone(),
runner_memory: from.runner_memory.clone(),
runner_timeout: from.runner_timeout.clone(),
runnner_cmd: from.runner_cmd.clone(),
})
}
}
Expand Down
35 changes: 32 additions & 3 deletions tasky/src/routes/assignment.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::PaginationParams;
use crate::handler::assignment::handle_update_multipart;
use crate::models::assignment::QuestionCatalogueElement;
use crate::AppState;
use actix_multipart::form::MultipartForm;
Expand Down Expand Up @@ -217,6 +218,36 @@ pub async fn create_assignment_test(
Ok(HttpResponse::Ok().json(enriched))
}

#[post("/groups/{group_id}/assignments/{id}/code_test/update")]
pub async fn update_assignment_test(
data: web::Data<AppState>,
user: web::ReqData<UserData>,
path: web::Path<(i32, i32)>,
MultipartForm(form): MultipartForm<CreateCodeTestMultipart>,
) -> Result<HttpResponse, ApiError> {
let user_data = user.into_inner();
let path_data = path.into_inner();
let conn = &mut data.db.db.get().unwrap();

let (_, mut assignment) = get_group_and_assignment(&user_data, path_data, conn)?;
if !assignment.is_granted(SecurityAction::Update, &user_data) {
return Err(ApiError::Forbidden {
message: "You are not allowed to create code tests".to_string(),
});
}

if assignment.language == AssignmentLanguage::QuestionBased {
return Err(ApiError::BadRequest {
message: "Cannot create code tests on question based assignment".to_string(),
});
}
let updated = handle_update_multipart(form, &data.mongodb, conn, assignment).await?;
let mut enriched =
AssignmentResponse::enrich(&updated, &mut data.user_api.clone(), conn).await?;
enriched.authorize(&user_data);
Ok(HttpResponse::Ok().json(enriched))
}

#[derive(Deserialize)]
struct CodeTestQuery {
pub object_ids: String,
Expand Down Expand Up @@ -272,9 +303,7 @@ pub async fn create_question_catalogue(
});
}

if assignment.file_structure.is_some()
|| assignment.language != AssignmentLanguage::QuestionBased
{
if assignment.language != AssignmentLanguage::QuestionBased {
return Err(ApiError::BadRequest {
message: "The assigment is not question based".to_string(),
});
Expand Down
1 change: 1 addition & 0 deletions tasky/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub fn init_services(cfg: &mut web::ServiceConfig) {
.service(assignment::create_assignment_test)
.service(assignment::view_assignment_test)
.service(assignment::create_question_catalogue)
.service(assignment::update_assignment_test)
.service(solution::create_solution)
.service(solution::get_solution)
.service(solution::get_solutions_for_assignment)
Expand Down
32 changes: 15 additions & 17 deletions web/app/groups/[groupId]/assignments/[assignmentId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import FileStructureDisplay from "@/components/FileStructureDisplay";
import AssignmentDetailsTaskTab from "@/components/assignments/AssignmentDetailsTaskTab";
import AssignmentSolutionsTab from "@/components/assignments/AssignmentSolutionsTab";
import AssignmentCompletedByTab from "@/components/assignments/AssignmentCompletedByTab";
import CreateQuestionsModal from "@/components/assignments/CreateQuestionsModal";
import CreateOrUpdateQuestionsModal from "@/components/assignments/CreateOrUpdateQuestionsModal";
import QuestionAnswersDisplay from "@/components/solution/questions/QuestionAnswersDisplay";
import { useSpotlightStage2 } from "@/hooks/spotlight/stage2";

Expand Down Expand Up @@ -68,17 +68,15 @@ const AssignmentDetailsPage = ({
<Button onClick={() => setUpdateModalOpen(true)}>Edit</Button>
)}
{isGranted(user, [UserRoles.Tutor, UserRoles.Admin]) &&
assignment.file_structure === null &&
assignment.language !== AssignmentLanguage.QuestionBased && (
<Button onClick={() => setFileStructureModalOpen(true)}>
Create code tests
Code tests
</Button>
)}
{isGranted(user, [UserRoles.Tutor, UserRoles.Admin]) &&
assignment.question_catalogue === null &&
assignment.language === AssignmentLanguage.QuestionBased && (
<Button onClick={() => setQuestionsModalOpen(true)}>
Create questions
Questions
</Button>
)}
</Group>
Expand All @@ -104,14 +102,14 @@ const AssignmentDetailsPage = ({
</Tabs.List>
<Tabs.Panel mt={20} value="task">
<AssignmentDetailsTaskTab
assignment={Object.assign({}, assignment)}
assignment={structuredClone(assignment)}
/>
</Tabs.Panel>
{assignment.file_structure !== null &&
isGranted(user, [UserRoles.Tutor, UserRoles.Admin]) && (
<Tabs.Panel value="codeTests" mt={20}>
<FileStructureDisplay
structure={Object.assign({}, assignment.file_structure)}
structure={structuredClone(assignment.file_structure)}
groupId={groupId}
assignmentId={assignmentId}
/>
Expand Down Expand Up @@ -146,18 +144,18 @@ const AssignmentDetailsPage = ({
assignment={assignment ?? undefined}
/>
)}
{fileStructureModalOpen && (
<AssignmentCreateOrUpdateCodeTestModal
onClose={() => setFileStructureModalOpen(false)}
groupId={groupId}
assignmentId={assignmentId}
refetch={refetch}
/>
{fileStructureModalOpen && assignment && (
<AssignmentCreateOrUpdateCodeTestModal
onClose={() => setFileStructureModalOpen(false)}
groupId={groupId}
assignment={structuredClone(assignment)}
refetch={refetch}
/>
)}
{questionsModalOpen && (
<CreateQuestionsModal
{questionsModalOpen && assignment && (
<CreateOrUpdateQuestionsModal
groupId={groupId}
assignmentId={assignmentId}
assignment={assignment}
refetch={refetch}
onClose={() => setQuestionsModalOpen(false)}
/>
Expand Down
Loading
Loading