diff --git a/sql/exam.sql b/sql/exam.sql new file mode 100644 index 0000000..33f01ea --- /dev/null +++ b/sql/exam.sql @@ -0,0 +1,5 @@ +create table exams( + id integer primary key, + timestamp integer, + deck_id integer not null references decks on delete cascade +); \ No newline at end of file diff --git a/sql/exam_entry.sql b/sql/exam_entry.sql new file mode 100644 index 0000000..99d5923 --- /dev/null +++ b/sql/exam_entry.sql @@ -0,0 +1,7 @@ +create table exam_entry +( + id integer primary key, + exam_id integer not null references exam on delete cascade, + word_id integer not null references words on delete cascade, + status integer default null +); \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 514794d..3d81e75 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,6 +18,7 @@ use log::info; use parking_lot::{Mutex, RwLock}; use rusqlite::Connection; use std::sync::Arc; +use crate::data_provider::exam::{ExamEntryRepository, ExamRepository}; pub const USER_FONT: Font = Font::with_name("Noto Sans JP"); @@ -70,6 +71,8 @@ pub struct AppState { pub connection: Arc>, pub sync_data: Arc>, pub history: HistoryRepository, + pub exam: ExamRepository, + pub exam_entries: ExamEntryRepository } impl Default for AppState { @@ -87,7 +90,9 @@ impl AppState { word_groups: WordGroupRepository::new(connection.clone()), card_states: CardStateRepository::new(connection.clone()), history: HistoryRepository::new(connection.clone()), + exam: ExamRepository::new(connection.clone()), custom_fields: CustomFieldRepository::new(connection.clone()), + exam_entries: ExamEntryRepository::new(connection.clone()), sync_data: Arc::new(RwLock::new(AppSettings { key: None, auto_web_fetch: false, @@ -108,7 +113,8 @@ pub fn fill_state(state: &mut AppState) { state.history.write().start().unwrap(); state.card_states.write().start().unwrap(); state.custom_fields.write().start().unwrap(); - + state.exam.write().start().unwrap(); + state.exam_entries.write().start().unwrap(); state.sync_data = Arc::new(RwLock::new(setting)) } diff --git a/src/data/entities.rs b/src/data/entities.rs index cbac506..43cc726 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -216,6 +216,40 @@ impl EntityData for CustomField { } } +#[derive(Clone)] +pub struct Exam { + pub id: Id, + pub timestamp: DateTime, + pub deck_id: Id, +} + +impl EntityData for Exam { + fn id(&self) -> Id { + self.id + } + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +#[derive(Clone)] +pub struct ExamEntry{ + pub id: Id, + pub exam_id: Id, + pub word_id: Id, + pub status: Option +} + +impl EntityData for ExamEntry { + fn id(&self) -> Id { + self.id + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + pub struct EntityHandle { pub(crate) id: Id, index: u32, diff --git a/src/data_provider/card_states.rs b/src/data_provider/card_states.rs index 1504295..b7cb00c 100644 --- a/src/data_provider/card_states.rs +++ b/src/data_provider/card_states.rs @@ -1,15 +1,22 @@ -use crate::data::Id; use crate::data::entities::CardState; use crate::data::repositories::{DataRepository, DataRepositoryInner, Query}; +use crate::data::Id; +use crate::data_access_methods; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use chrono::NaiveDate; use fsrs::MemoryState; use rusqlite::{Error, Params, Row}; use std::sync::Arc; -pub type CardStateRepository = DataRepository; -pub type CardStateRepositoryInner = DataRepositoryInner; -pub type CardStateDbLayer = SqliteDbLayer; +data_access_methods!( + CardStateDbLayer, + CardStateRepository, + CardStateRepositoryInner, + CardState, + CardStatesQuerySet, + CardStateQuery +); + #[derive(Clone)] pub struct CardStatesQuerySet; impl TableAccessQuerySet for CardStatesQuerySet { diff --git a/src/data_provider/custom_fields.rs b/src/data_provider/custom_fields.rs index 4ea23a9..acc49f1 100644 --- a/src/data_provider/custom_fields.rs +++ b/src/data_provider/custom_fields.rs @@ -2,10 +2,9 @@ use crate::data::entities::CustomField; use crate::data::repositories::{DataRepository, DataRepositoryInner}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use rusqlite::{Error, Params, Row}; +use crate::data_access_methods; -pub type CustomFieldRepository = DataRepository; -pub type CustomFieldRepositoryInner = DataRepositoryInner; -pub type CustomFieldDbLayer = SqliteDbLayer; +data_access_methods!(CustomFieldDbLayer, CustomFieldRepository, CustomFieldRepositoryInner, CustomField, CustomFieldsQuerySet, CustomFieldQuery); #[derive(Clone)] pub struct CustomFieldsQuerySet; impl TableAccessQuerySet for CustomFieldsQuerySet { diff --git a/src/data_provider/exam.rs b/src/data_provider/exam.rs new file mode 100644 index 0000000..f5ef353 --- /dev/null +++ b/src/data_provider/exam.rs @@ -0,0 +1,98 @@ +use crate::data::entities::{Exam, ExamEntry}; +use crate::data::repositories::*; +use crate::data_access_methods; +use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; +use rusqlite::{Error, Params, Row}; + +data_access_methods!( + ExamDataLayer, + ExamRepository, + ExamRepositoryInner, + Exam, + ExamQuerySet, + ExamQuery +); + +data_access_methods!( + ExamEntryLayer, + ExamEntryRepository, + ExamEntryRepositoryInner, + ExamEntry, + ExamEntryQuerySet, + ExamEntryQuery +); + +#[derive(Clone)] +pub struct ExamQuerySet; + +impl TableAccessQuerySet for ExamQuerySet { + fn load() -> &'static str { + "SELECT id, timestamp, deck_id FROM exams" + } + + fn save() -> &'static str { + "UPDATE exams set deck_id = $1, timestamp = $2 WHERE id = $3" + } + + fn add() -> &'static str { + "INSERT INTO exams (timestamp, deck_id) values ($1, $2)" + } + + fn name() -> &'static str { + "exams" + } + + fn from_sql(row: &Row) -> Result { + Ok(Exam { + id: row.get(0)?, + timestamp: row.get(1)?, + deck_id: row.get(2)?, + }) + } + + fn insert_params(entity: &Exam) -> impl Params { + (entity.timestamp.timestamp(), entity.deck_id) + } + + fn update_params(entity: &Exam) -> impl Params { + (entity.timestamp.timestamp(), entity.deck_id, entity.id) + } +} + +#[derive(Clone)] +pub struct ExamEntryQuerySet; + +impl TableAccessQuerySet for ExamEntryQuerySet { + fn load() -> &'static str { + "SELECT id, exam_id, word_id, status FROM exam_entry" + } + + fn save() -> &'static str { + "UPDATE exam_entry set status = $1 WHERE id = $2" + } + + fn add() -> &'static str { + "INSERT into exam_entry (exam_id, word_id, status) VALUES ($1, $2, $3)" + } + + fn name() -> &'static str { + "exam_entry" + } + + fn from_sql(row: &Row) -> Result { + Ok(ExamEntry { + id: row.get(0)?, + exam_id: row.get(1)?, + word_id: row.get(2)?, + status: row.get(3)?, + }) + } + + fn insert_params(entity: &ExamEntry) -> impl Params { + (entity.exam_id, entity.word_id, entity.status) + } + + fn update_params(entity: &ExamEntry) -> impl Params { + (entity.status, entity.id) + } +} diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 1a14eba..0c7bcca 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -4,10 +4,9 @@ use crate::data::repositories::{DataRepository, DataRepositoryInner}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::{Error, Params, Row, ToSql}; +use crate::data_access_methods; -pub type HistoryRepository = DataRepository; -pub type HistoryRepositoryInner = DataRepositoryInner; -pub type HistoryDataLayer = SqliteDbLayer; +data_access_methods!(HistoryDataLayer, HistoryRepository, HistoryRepositoryInner, HistoryNote, HistoryQuerySet, HistoryQuery); #[derive(Clone)] pub struct HistoryQuerySet; diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index d769188..53fa5fc 100644 --- a/src/data_provider/mod.rs +++ b/src/data_provider/mod.rs @@ -19,6 +19,7 @@ pub(crate) mod voice; pub(crate) mod web_api; pub mod word_groups; pub(crate) mod words; +pub(crate) mod exam; pub trait TableAccessQuerySet { fn load() -> &'static str; @@ -190,3 +191,12 @@ impl> BatchedDbLayer for SqliteDbLay Ok(()) } } + +#[macro_export] +macro_rules! data_access_methods { + ($layer:ident, $repo:ident, $repoinner:ident, $data:ty, $queryset:ty, $query:ident) => { +pub type $repo = DataRepository<$data, $layer>; +pub type $repoinner = DataRepositoryInner<$data, $layer>; +pub type $layer = SqliteDbLayer<$data, $queryset>; + }; +} \ No newline at end of file diff --git a/src/data_provider/word_groups.rs b/src/data_provider/word_groups.rs index 450f73c..5d87761 100644 --- a/src/data_provider/word_groups.rs +++ b/src/data_provider/word_groups.rs @@ -2,10 +2,9 @@ use crate::data::entities::WordGroup; use crate::data::repositories::{DataRepository, DataRepositoryInner}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use rusqlite::{Error, Params, Row}; +use crate::data_access_methods; -pub type WordGroupRepository = DataRepository; -pub type WordGroupRepositoryInner = DataRepositoryInner; -pub type WordGroupDataLayer = SqliteDbLayer; +data_access_methods!(WordGroupDataLayer, WordGroupRepository, WordGroupRepositoryInner, WordGroup, WordGroupQuerySet, WordGroupQuery); #[derive(Clone)] pub struct WordGroupQuerySet; diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 6a842c8..736e82a 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -4,10 +4,10 @@ use crate::data::repositories::{DataRepository, DataRepositoryInner}; use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use litemap::LiteMap; use rusqlite::{Error, Params, Row}; +use crate::data_access_methods; + +data_access_methods!(WordDataLayer, WordRepository, WordRepositoryInner, WordData, WordQuerySet, WordQuery); -pub type WordRepository = DataRepository; -pub type WordRepositoryInner = DataRepositoryInner; -pub type WordDataLayer = SqliteDbLayer; #[derive(Clone)] pub struct WordQuerySet; impl TableAccessQuerySet for WordQuerySet { diff --git a/src/pages/sync.rs b/src/pages/sync.rs index bb39611..6205d35 100644 --- a/src/pages/sync.rs +++ b/src/pages/sync.rs @@ -218,6 +218,8 @@ impl SyncState { self.state.decks.write().reload()?; self.state.dictionary.write().reload()?; self.state.history.write().reload()?; + self.state.exam.write().reload()?; + self.state.exam_entries.write().reload()?; Ok(()) }