use crate::notebook::testing::types::Notebook;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Assignment {
pub id: String,
pub title: String,
pub description: String,
pub notebook_template: Notebook,
pub due_date: Option<chrono::DateTime<chrono::Utc>>,
pub points: u32,
pub rubric: Vec<RubricItem>,
pub test_cases: Vec<TestCase>,
}
#[derive(Debug, Clone)]
pub struct RubricItem {
pub id: String,
pub description: String,
pub points: u32,
pub criteria: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct TestCase {
pub id: String,
pub cell_id: String,
pub input: String,
pub expected_output: String,
pub points: u32,
pub hidden: bool,
}
#[derive(Debug, Clone)]
pub struct StudentSubmission {
pub student_id: String,
pub assignment_id: String,
pub notebook: Notebook,
pub submitted_at: chrono::DateTime<chrono::Utc>,
pub grade: Option<Grade>,
}
#[derive(Debug, Clone)]
pub struct Grade {
pub total_points: u32,
pub max_points: u32,
pub percentage: f64,
pub feedback: Vec<Feedback>,
pub rubric_scores: HashMap<String, u32>,
}
#[derive(Debug, Clone)]
pub struct Feedback {
pub cell_id: String,
pub message: String,
pub severity: FeedbackSeverity,
}
#[derive(Debug, Clone)]
pub enum FeedbackSeverity {
Success,
Warning,
Error,
Info,
}
pub struct EducationalPlatform {
assignments: Vec<Assignment>,
submissions: std::collections::HashMap<String, Vec<StudentSubmission>>,
peer_reviews: Vec<PeerReview>,
analytics: LearningAnalytics,
}
impl Default for EducationalPlatform {
fn default() -> Self {
Self::new()
}
}
impl EducationalPlatform {
pub fn new() -> Self {
Self {
assignments: Vec::new(),
submissions: HashMap::new(),
peer_reviews: Vec::new(),
analytics: LearningAnalytics::new(),
}
}
pub fn create_assignment(&mut self, assignment: Assignment) -> Result<(), String> {
if self.assignments.iter().any(|a| a.id == assignment.id) {
return Err("Assignment ID already exists".to_string());
}
self.assignments.push(assignment);
Ok(())
}
pub fn get_assignments(&self) -> &[Assignment] {
&self.assignments
}
pub fn submit_assignment(
&mut self,
student_id: &str,
mut submission: StudentSubmission,
) -> Result<(), String> {
if !self
.assignments
.iter()
.any(|a| a.id == submission.assignment_id)
{
return Err("Assignment not found".to_string());
}
if let Some(assignment) = self
.assignments
.iter()
.find(|a| a.id == submission.assignment_id)
{
if !assignment.test_cases.is_empty() {
submission.grade = Some(self.auto_grade(&submission));
}
}
self.submissions
.entry(student_id.to_string())
.or_default()
.push(submission);
self.analytics.track_event(LearningEvent {
student_id: student_id.to_string(),
event_type: EventType::AssignmentSubmit,
cell_id: String::new(),
timestamp: chrono::Utc::now(),
success: true,
duration_ms: 0,
});
Ok(())
}
pub fn auto_grade(&self, submission: &StudentSubmission) -> Grade {
let assignment = self
.assignments
.iter()
.find(|a| a.id == submission.assignment_id)
.expect("Assignment ID must exist in assignments list for auto-grading");
let mut total_points = 0;
let mut feedback = Vec::new();
for test_case in &assignment.test_cases {
if let Some(cell) = submission
.notebook
.cells
.iter()
.find(|c| c.id == test_case.cell_id)
{
if cell.source.contains(&test_case.expected_output) {
total_points += test_case.points;
feedback.push(Feedback {
cell_id: test_case.cell_id.clone(),
message: format!("Test {} passed", test_case.id),
severity: FeedbackSeverity::Success,
});
} else {
feedback.push(Feedback {
cell_id: test_case.cell_id.clone(),
message: format!("Test {} failed", test_case.id),
severity: FeedbackSeverity::Error,
});
}
}
}
Grade {
total_points,
max_points: assignment.points,
percentage: (f64::from(total_points) / f64::from(assignment.points)) * 100.0,
feedback,
rubric_scores: HashMap::new(),
}
}
pub fn submit_peer_review(&mut self, review: PeerReview) -> Result<(), String> {
self.peer_reviews.push(review);
Ok(())
}
pub fn get_analytics(&self) -> &LearningAnalytics {
&self.analytics
}
}
#[derive(Debug, Clone)]
pub struct LearningAnalytics {
events: Vec<LearningEvent>,
}
#[derive(Debug, Clone)]
pub struct LearningEvent {
pub student_id: String,
pub event_type: EventType,
pub cell_id: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub success: bool,
pub duration_ms: u64,
}
#[derive(Debug, Clone)]
pub enum EventType {
CellExecution,
TestRun,
AssignmentSubmit,
AssignmentComplete,
TutorialStep,
HintRequested,
}
#[derive(Debug, Clone)]
pub struct StudentMetrics {
pub total_executions: usize,
pub success_rate: f64,
pub avg_execution_time_ms: u64,
pub assignments_completed: usize,
}
#[derive(Debug, Clone)]
pub struct ClassMetrics {
pub total_students: usize,
pub completion_rate: f64,
pub avg_score: f64,
}
impl Default for LearningAnalytics {
fn default() -> Self {
Self::new()
}
}
impl LearningAnalytics {
pub fn new() -> Self {
Self { events: Vec::new() }
}
pub fn track_event(&mut self, event: LearningEvent) {
self.events.push(event);
}
pub fn get_student_metrics(&self, student_id: &str) -> StudentMetrics {
let student_events: Vec<_> = self
.events
.iter()
.filter(|e| e.student_id == student_id)
.collect();
let total = student_events.len();
let successful = student_events.iter().filter(|e| e.success).count();
let completions = student_events
.iter()
.filter(|e| matches!(e.event_type, EventType::AssignmentComplete))
.count();
let avg_time = if total > 0 {
let total_time: u64 = student_events.iter().map(|e| e.duration_ms).sum();
total_time / total as u64
} else {
0
};
StudentMetrics {
total_executions: total,
success_rate: if total > 0 {
successful as f64 / total as f64
} else {
0.0
},
avg_execution_time_ms: avg_time,
assignments_completed: completions,
}
}
pub fn get_class_metrics(&self) -> ClassMetrics {
let unique_students: std::collections::HashSet<_> =
self.events.iter().map(|e| &e.student_id).collect();
let completions = self
.events
.iter()
.filter(|e| matches!(e.event_type, EventType::AssignmentComplete))
.count();
ClassMetrics {
total_students: unique_students.len(),
completion_rate: if unique_students.is_empty() {
0.0
} else {
completions as f64 / unique_students.len() as f64
},
avg_score: 0.0, }
}
}
#[derive(Debug, Clone)]
pub struct PeerReview {
pub id: String,
pub assignment_id: String,
pub reviewer_id: String,
pub reviewee_id: String,
pub feedback: Vec<ReviewComment>,
pub rating: u8,
}
#[derive(Debug, Clone)]
pub struct ReviewComment {
pub cell_id: String,
pub line_number: Option<usize>,
pub comment: String,
pub category: CommentCategory,
}
#[derive(Debug, Clone)]
pub enum CommentCategory {
Style,
Correctness,
Efficiency,
Positive,
Suggestion,
}
impl Default for Assignment {
fn default() -> Self {
Self {
id: String::new(),
title: String::new(),
description: String::new(),
notebook_template: Notebook {
cells: vec![],
metadata: None,
},
due_date: None,
points: 100,
rubric: vec![],
test_cases: vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_assignment_creation() {
let assignment = Assignment {
id: "assign1".to_string(),
title: "Test Assignment".to_string(),
description: "Learn basics".to_string(),
notebook_template: Notebook {
cells: vec![],
metadata: None,
},
due_date: Some(Utc::now()),
points: 100,
rubric: vec![],
test_cases: vec![],
};
assert_eq!(assignment.id, "assign1");
assert_eq!(assignment.title, "Test Assignment");
assert_eq!(assignment.points, 100);
assert!(assignment.due_date.is_some());
}
#[test]
fn test_rubric_item() {
let rubric = RubricItem {
id: "rubric1".to_string(),
description: "Code quality".to_string(),
points: 25,
criteria: vec!["Clean code".to_string(), "Proper naming".to_string()],
};
assert_eq!(rubric.id, "rubric1");
assert_eq!(rubric.points, 25);
assert_eq!(rubric.criteria.len(), 2);
}
#[test]
fn test_test_case_visible() {
let test = TestCase {
id: "test1".to_string(),
cell_id: "cell1".to_string(),
input: "2 + 2".to_string(),
expected_output: "4".to_string(),
points: 10,
hidden: false,
};
assert_eq!(test.id, "test1");
assert!(!test.hidden);
assert_eq!(test.points, 10);
}
#[test]
fn test_test_case_hidden() {
let test = TestCase {
id: "test2".to_string(),
cell_id: "cell2".to_string(),
input: "secret".to_string(),
expected_output: "result".to_string(),
points: 20,
hidden: true,
};
assert!(test.hidden);
assert_eq!(test.points, 20);
}
#[test]
fn test_student_submission() {
let submission = StudentSubmission {
student_id: "student1".to_string(),
assignment_id: "assign1".to_string(),
notebook: Notebook {
cells: vec![],
metadata: None,
},
submitted_at: Utc::now(),
grade: None,
};
assert_eq!(submission.student_id, "student1");
assert_eq!(submission.assignment_id, "assign1");
assert!(submission.grade.is_none());
}
#[test]
fn test_grade_creation() {
let mut rubric_scores = HashMap::new();
rubric_scores.insert("rubric1".to_string(), 20);
let grade = Grade {
total_points: 85,
max_points: 100,
percentage: 85.0,
feedback: vec![],
rubric_scores,
};
assert_eq!(grade.total_points, 85);
assert_eq!(grade.percentage, 85.0);
assert_eq!(grade.rubric_scores.len(), 1);
}
#[test]
fn test_feedback_types() {
let severities = vec![
FeedbackSeverity::Success,
FeedbackSeverity::Warning,
FeedbackSeverity::Error,
FeedbackSeverity::Info,
];
for severity in severities {
let feedback = Feedback {
cell_id: "cell1".to_string(),
message: "Test feedback".to_string(),
severity: severity.clone(),
};
match feedback.severity {
FeedbackSeverity::Success => {}
FeedbackSeverity::Warning => {}
FeedbackSeverity::Error => {}
FeedbackSeverity::Info => {}
}
}
}
#[test]
fn test_educational_platform_new() {
let platform = EducationalPlatform::new();
assert!(platform.assignments.is_empty());
assert!(platform.submissions.is_empty());
assert!(platform.peer_reviews.is_empty());
}
#[test]
fn test_educational_platform_default() {
let platform = EducationalPlatform::default();
assert!(platform.assignments.is_empty());
}
#[test]
fn test_learning_analytics_new() {
let analytics = LearningAnalytics::new();
assert!(analytics.events.is_empty());
}
#[test]
fn test_peer_review() {
let review = PeerReview {
id: "review1".to_string(),
assignment_id: "assign1".to_string(),
reviewer_id: "student2".to_string(),
reviewee_id: "student1".to_string(),
feedback: vec![],
rating: 4,
};
assert_eq!(review.id, "review1");
assert_eq!(review.assignment_id, "assign1");
assert_eq!(review.rating, 4);
}
#[test]
fn test_assignment_default() {
let assignment = Assignment::default();
assert_eq!(assignment.id, "");
assert_eq!(assignment.points, 100);
assert!(assignment.due_date.is_none());
}
#[test]
fn test_clone_implementations() {
let assignment = Assignment::default();
let cloned = assignment.clone();
assert_eq!(cloned.id, assignment.id);
let rubric = RubricItem {
id: "r1".to_string(),
description: "desc".to_string(),
points: 10,
criteria: vec![],
};
let cloned_rubric = rubric.clone();
assert_eq!(cloned_rubric.id, rubric.id);
}
}