pub mod blacklist;
pub mod course_builder;
pub mod course_library;
pub mod data;
pub mod filter_manager;
pub mod graph;
pub mod mantra_miner;
pub mod practice_stats;
pub mod review_list;
pub mod scheduler;
pub mod scorer;
pub mod testutil;
use anyhow::Result;
use parking_lot::RwLock;
use review_list::{ReviewList, ReviewListDB};
use std::{path::Path, sync::Arc};
use ustr::{Ustr, UstrMap, UstrSet};
use crate::mantra_miner::TraneMantraMiner;
use blacklist::{Blacklist, BlacklistDB};
use course_library::{CourseLibrary, GetUnitGraph, LocalCourseLibrary};
use data::{
filter::{NamedFilter, UnitFilter},
CourseManifest, ExerciseManifest, ExerciseTrial, LessonManifest, MasteryScore,
SchedulerOptions, UnitType,
};
use filter_manager::{FilterManager, LocalFilterManager};
use graph::UnitGraph;
use practice_stats::{PracticeStats, PracticeStatsDB};
use scheduler::{data::SchedulerData, DepthFirstScheduler, ExerciseScheduler};
pub const TRANE_CONFIG_DIR_PATH: &str = ".trane";
pub const PRACTICE_STATS_PATH: &str = "practice_stats.db";
pub const BLACKLIST_PATH: &str = "blacklist.db";
pub const REVIEW_LIST_PATH: &str = "review_list.db";
pub const FILTERS_DIR: &str = "filters";
pub const USER_PREFERENCES_PATH: &str = "user_preferences.json";
pub struct Trane {
library_root: String,
blacklist: Arc<RwLock<dyn Blacklist + Send + Sync>>,
course_library: Arc<RwLock<dyn CourseLibrary + Send + Sync>>,
filter_manager: Arc<RwLock<dyn FilterManager + Send + Sync>>,
practice_stats: Arc<RwLock<dyn PracticeStats + Send + Sync>>,
review_list: Arc<RwLock<dyn ReviewList + Send + Sync>>,
#[allow(dead_code)]
scheduler_data: SchedulerData,
scheduler: DepthFirstScheduler,
unit_graph: Arc<RwLock<dyn UnitGraph + Send + Sync>>,
mantra_miner: TraneMantraMiner,
}
impl Trane {
pub fn new(working_dir: &Path, library_root: &Path) -> Result<Trane> {
let config_path = library_root.join(Path::new(TRANE_CONFIG_DIR_PATH));
let course_library = Arc::new(RwLock::new(LocalCourseLibrary::new(
&working_dir.join(library_root),
)?));
let unit_graph = course_library.write().get_unit_graph();
let practice_stats = Arc::new(RwLock::new(PracticeStatsDB::new_from_disk(
config_path.join(PRACTICE_STATS_PATH).to_str().unwrap(),
)?));
let blacklist = Arc::new(RwLock::new(BlacklistDB::new_from_disk(
config_path.join(BLACKLIST_PATH).to_str().unwrap(),
)?));
let review_list = Arc::new(RwLock::new(ReviewListDB::new_from_disk(
config_path.join(REVIEW_LIST_PATH).to_str().unwrap(),
)?));
let filter_manager = Arc::new(RwLock::new(LocalFilterManager::new(
config_path.join(FILTERS_DIR).to_str().unwrap(),
)?));
let mut mantra_miner = TraneMantraMiner::default();
mantra_miner.mantra_miner.start()?;
let options = SchedulerOptions::default();
options.verify()?;
let scheduler_data = SchedulerData {
options,
course_library: course_library.clone(),
unit_graph: unit_graph.clone(),
practice_stats: practice_stats.clone(),
blacklist: blacklist.clone(),
review_list: review_list.clone(),
frequency_map: Arc::new(RwLock::new(UstrMap::default())),
};
Ok(Trane {
blacklist,
course_library,
filter_manager,
library_root: library_root.to_str().unwrap().to_string(),
practice_stats,
review_list,
scheduler_data: scheduler_data.clone(),
scheduler: DepthFirstScheduler::new(scheduler_data, SchedulerOptions::default()),
unit_graph,
mantra_miner,
})
}
pub fn library_root(&self) -> String {
self.library_root.clone()
}
pub fn mantra_count(&self) -> usize {
self.mantra_miner.mantra_miner.count()
}
#[allow(dead_code)]
fn get_scheduler_data(&self) -> SchedulerData {
self.scheduler_data.clone()
}
}
impl Blacklist for Trane {
fn add_to_blacklist(&mut self, unit_id: &Ustr) -> Result<()> {
self.scheduler.invalidate_cached_score(unit_id);
self.blacklist.write().add_to_blacklist(unit_id)
}
fn remove_from_blacklist(&mut self, unit_id: &Ustr) -> Result<()> {
self.scheduler.invalidate_cached_score(unit_id);
self.blacklist.write().remove_from_blacklist(unit_id)
}
fn blacklisted(&self, unit_id: &Ustr) -> Result<bool> {
self.blacklist.read().blacklisted(unit_id)
}
fn all_blacklist_entries(&self) -> Result<Vec<Ustr>> {
self.blacklist.read().all_blacklist_entries()
}
}
impl CourseLibrary for Trane {
fn get_course_manifest(&self, course_id: &Ustr) -> Option<CourseManifest> {
self.course_library.read().get_course_manifest(course_id)
}
fn get_lesson_manifest(&self, lesson_id: &Ustr) -> Option<LessonManifest> {
self.course_library.read().get_lesson_manifest(lesson_id)
}
fn get_exercise_manifest(&self, exercise_id: &Ustr) -> Option<ExerciseManifest> {
self.course_library
.read()
.get_exercise_manifest(exercise_id)
}
fn get_course_ids(&self) -> Vec<Ustr> {
self.course_library.read().get_course_ids()
}
fn get_lesson_ids(&self, course_id: &Ustr) -> Result<Vec<Ustr>> {
self.course_library.read().get_lesson_ids(course_id)
}
fn get_exercise_ids(&self, lesson_id: &Ustr) -> Result<Vec<Ustr>> {
self.course_library.read().get_exercise_ids(lesson_id)
}
fn get_all_exercise_ids(&self) -> Result<Vec<Ustr>> {
self.course_library.read().get_all_exercise_ids()
}
fn search(&self, query: &str) -> Result<Vec<Ustr>> {
self.course_library.read().search(query)
}
}
impl FilterManager for Trane {
fn get_filter(&self, id: &str) -> Option<NamedFilter> {
self.filter_manager.read().get_filter(id)
}
fn list_filters(&self) -> Vec<(String, String)> {
self.filter_manager.read().list_filters()
}
}
impl PracticeStats for Trane {
fn get_scores(&self, exercise_id: &Ustr, num_scores: usize) -> Result<Vec<ExerciseTrial>> {
self.practice_stats
.read()
.get_scores(exercise_id, num_scores)
}
fn record_exercise_score(
&mut self,
exercise_id: &Ustr,
score: MasteryScore,
timestamp: i64,
) -> Result<()> {
self.practice_stats
.write()
.record_exercise_score(exercise_id, score, timestamp)
}
fn trim_scores(&mut self, num_scores: usize) -> Result<()> {
self.practice_stats.write().trim_scores(num_scores)
}
}
impl ReviewList for Trane {
fn add_to_review_list(&mut self, unit_id: &Ustr) -> Result<()> {
self.review_list.write().add_to_review_list(unit_id)
}
fn remove_from_review_list(&mut self, unit_id: &Ustr) -> Result<()> {
self.review_list.write().remove_from_review_list(unit_id)
}
fn all_review_list_entries(&self) -> Result<Vec<Ustr>> {
self.review_list.read().all_review_list_entries()
}
}
impl ExerciseScheduler for Trane {
fn get_exercise_batch(
&self,
filter: Option<&UnitFilter>,
) -> Result<Vec<(Ustr, ExerciseManifest)>> {
self.scheduler.get_exercise_batch(filter)
}
fn score_exercise(
&self,
exercise_id: &Ustr,
score: MasteryScore,
timestamp: i64,
) -> Result<()> {
self.scheduler.score_exercise(exercise_id, score, timestamp)
}
fn invalidate_cached_score(&self, unit_id: &Ustr) {
self.scheduler.invalidate_cached_score(unit_id)
}
}
impl UnitGraph for Trane {
fn add_course(&mut self, course_id: &Ustr) -> Result<()> {
self.unit_graph.write().add_course(course_id)
}
fn add_lesson(&mut self, lesson_id: &Ustr, course_id: &Ustr) -> Result<()> {
self.unit_graph.write().add_lesson(lesson_id, course_id)
}
fn add_exercise(&mut self, exercise_id: &Ustr, lesson_id: &Ustr) -> Result<()> {
self.unit_graph.write().add_exercise(exercise_id, lesson_id)
}
fn add_dependencies(
&mut self,
unit_id: &Ustr,
unit_type: UnitType,
dependencies: &[Ustr],
) -> Result<()> {
self.unit_graph
.write()
.add_dependencies(unit_id, unit_type, dependencies)
}
fn get_unit_type(&self, unit_id: &Ustr) -> Option<UnitType> {
self.unit_graph.read().get_unit_type(unit_id)
}
fn get_course_lessons(&self, course_id: &Ustr) -> Option<UstrSet> {
self.unit_graph.read().get_course_lessons(course_id)
}
fn get_course_starting_lessons(&self, course_id: &Ustr) -> Option<UstrSet> {
self.unit_graph
.read()
.get_course_starting_lessons(course_id)
}
fn update_starting_lessons(&mut self) {
self.unit_graph.write().update_starting_lessons()
}
fn get_lesson_course(&self, lesson_id: &Ustr) -> Option<Ustr> {
self.unit_graph.read().get_lesson_course(lesson_id)
}
fn get_lesson_exercises(&self, lesson_id: &Ustr) -> Option<UstrSet> {
self.unit_graph.read().get_lesson_exercises(lesson_id)
}
fn get_exercise_lesson(&self, exercise_id: &Ustr) -> Option<Ustr> {
self.unit_graph.read().get_exercise_lesson(exercise_id)
}
fn get_dependencies(&self, unit_id: &Ustr) -> Option<UstrSet> {
self.unit_graph.read().get_dependencies(unit_id)
}
fn get_dependents(&self, unit_id: &Ustr) -> Option<UstrSet> {
self.unit_graph.read().get_dependents(unit_id)
}
fn get_dependency_sinks(&self) -> UstrSet {
self.unit_graph.read().get_dependency_sinks()
}
fn check_cycles(&self) -> Result<()> {
self.unit_graph.read().check_cycles()
}
fn generate_dot_graph(&self) -> String {
self.unit_graph.read().generate_dot_graph()
}
}
#[cfg(test)]
mod test {
use anyhow::Result;
use std::{fs::*, os::unix::prelude::PermissionsExt, thread, time::Duration};
use crate::Trane;
#[test]
fn library_root() -> Result<()> {
let dir = tempfile::tempdir()?;
let trane = Trane::new(dir.path(), dir.path())?;
assert_eq!(trane.library_root(), dir.path().to_str().unwrap());
Ok(())
}
#[test]
fn mantra_count() -> Result<()> {
let dir = tempfile::tempdir()?;
let trane = Trane::new(dir.path(), dir.path())?;
thread::sleep(Duration::from_millis(200));
assert!(trane.mantra_count() > 0);
Ok(())
}
#[test]
fn config_dir_is_file() -> Result<()> {
let dir = tempfile::tempdir()?;
let trane_path = dir.path().join(".trane");
File::create(&trane_path)?;
assert!(Trane::new(dir.path(), dir.path()).is_err());
Ok(())
}
#[test]
fn bad_dir_permissions() -> Result<()> {
let dir = tempfile::tempdir()?;
set_permissions(&dir, Permissions::from_mode(0o000))?;
assert!(Trane::new(dir.path(), dir.path()).is_err());
Ok(())
}
#[test]
fn bad_config_dir_permissions() -> Result<()> {
let dir = tempfile::tempdir()?;
let config_dir_path = dir.path().join(".trane");
create_dir(&config_dir_path)?;
set_permissions(&config_dir_path, Permissions::from_mode(0o000))?;
assert!(Trane::new(dir.path(), dir.path()).is_err());
Ok(())
}
#[test]
fn scheduler_data() -> Result<()> {
let dir = tempfile::tempdir()?;
let trane = Trane::new(dir.path(), dir.path())?;
trane.get_scheduler_data();
Ok(())
}
}