Exams page more code
This commit is contained in:
1 parent
4b9e257cdf
commit
79c42d0feb
6 files changed
+391
-17
No files matched your search
@@ -64,19 +64,19 @@ pub struct ExamEntryQuerySet;
|
||||
|
||||
impl TableAccessQuerySet<ExamEntry> for ExamEntryQuerySet {
|
||||
fn load() -> &'static str {
|
||||
"SELECT id, exam_id, word_id, status FROM exam_entry"
|
||||
"SELECT id, exam_id, word_id, status FROM exam_entries"
|
||||
}
|
||||
|
||||
fn save() -> &'static str {
|
||||
"UPDATE exam_entry set status = $1 WHERE id = $2"
|
||||
"UPDATE exam_entries set status = $1 WHERE id = $2"
|
||||
}
|
||||
|
||||
fn add() -> &'static str {
|
||||
"INSERT into exam_entry (exam_id, word_id, status) VALUES ($1, $2, $3)"
|
||||
"INSERT into exam_entries (exam_id, word_id, status) VALUES ($1, $2, $3)"
|
||||
}
|
||||
|
||||
fn name() -> &'static str {
|
||||
"exam_entry"
|
||||
"exam_entries"
|
||||
}
|
||||
|
||||
fn from_sql(row: &Row) -> Result<ExamEntry, Error> {
|
||||
|
||||
+10
-4
@@ -1,4 +1,4 @@
|
||||
use crate::app::{AppState, fill_state};
|
||||
use crate::app::{fill_state, AppState};
|
||||
use crate::data_provider::sqlite::create_db;
|
||||
use crate::data_provider::web_api::{
|
||||
get_local_version, get_web_version, load_data, set_local_version,
|
||||
@@ -12,6 +12,7 @@ use crate::notification::{
|
||||
};
|
||||
use crate::pages::dictionary::{DictionaryMessage, DictionaryState};
|
||||
use crate::pages::dictionary_quiz::{DictionaryQuizMessage, DictionaryQuizState};
|
||||
use crate::pages::exams::{ExamsMessage, ExamsState};
|
||||
use crate::pages::history::{HistoryMessage, HistoryState};
|
||||
use crate::pages::import::{ImportMessage, ImportState};
|
||||
use crate::pages::quiz::{QuizMessage, QuizState};
|
||||
@@ -59,6 +60,7 @@ pub enum RootMessage {
|
||||
History(HistoryMessage),
|
||||
RepetitionSettings(RepetitionSettingsMessage),
|
||||
Import(ImportMessage),
|
||||
Exams(ExamsMessage),
|
||||
Keyboard(Event),
|
||||
PushNotification(NotificationRequest),
|
||||
Notification(NotificationMessage),
|
||||
@@ -80,6 +82,7 @@ pub enum Page {
|
||||
History(HistoryState),
|
||||
RepetitionSettings(RepetitionSettingsState),
|
||||
Import(ImportState),
|
||||
Exams(ExamsState),
|
||||
PreviousPage,
|
||||
}
|
||||
|
||||
@@ -182,7 +185,8 @@ impl ScreenState {
|
||||
Sync,
|
||||
History,
|
||||
RepetitionSettings,
|
||||
Import
|
||||
Import,
|
||||
Exams
|
||||
);
|
||||
}
|
||||
debug!("Update took {:?}", time.elapsed());
|
||||
@@ -205,7 +209,8 @@ impl ScreenState {
|
||||
Sync,
|
||||
History,
|
||||
RepetitionSettings,
|
||||
Import
|
||||
Import,
|
||||
Exams
|
||||
)
|
||||
}
|
||||
|
||||
@@ -225,7 +230,8 @@ impl ScreenState {
|
||||
Sync,
|
||||
History,
|
||||
RepetitionSettings,
|
||||
Import
|
||||
Import,
|
||||
Exams
|
||||
);
|
||||
|
||||
let notifications = self.notification_manager.draw().map(Notification);
|
||||
|
||||
@@ -11,3 +11,4 @@ pub mod selector;
|
||||
pub mod sync;
|
||||
pub mod word;
|
||||
pub mod writing;
|
||||
pub mod exams;
|
||||
@@ -0,0 +1,359 @@
|
||||
use crate::app::AppState;
|
||||
use crate::data::entities::{DeckSettings, EntityHandle, Exam, ExamEntry};
|
||||
use crate::data::repositories::{BatchedRepository, Repository};
|
||||
use crate::data::Id;
|
||||
use crate::navigation::Page::PreviousPage;
|
||||
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||
use crate::notification::MaybeError;
|
||||
use crate::pages::exams::ExamsMessage::*;
|
||||
use crate::pages::repetitions::Style;
|
||||
use crate::push_error;
|
||||
use crate::styling::{back_overlay, BUTTON_RADIUS, DEFAULT_SPACING, HALF_SPACING};
|
||||
use crate::{jl_button, push_error_task};
|
||||
use chrono::Utc;
|
||||
use iced::widget::button::Status;
|
||||
use iced::widget::{
|
||||
button, center_x, column, container, row, scrollable, space, text, tooltip, Column,
|
||||
};
|
||||
use iced::{Element, Task};
|
||||
use iced_core::Alignment::*;
|
||||
use iced_core::Length::{Fill, FillPortion};
|
||||
use iced_core::{Background, Border, Color, Shadow, Theme};
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ExamsState {
|
||||
state: AppState,
|
||||
deck_id: EntityHandle<DeckSettings>,
|
||||
deck: DeckSettings,
|
||||
selected_exam_id: Option<EntityHandle<Exam>>,
|
||||
exams: BTreeMap<EntityHandle<Exam>, ExamExtended>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub enum ExamsMessage {
|
||||
Back,
|
||||
SelectExam(EntityHandle<Exam>),
|
||||
DeleteExam(EntityHandle<Exam>),
|
||||
CreateExam,
|
||||
GotoExam,
|
||||
}
|
||||
|
||||
impl NavigatedPage<ExamsMessage> for ExamsState {
|
||||
fn navigate(&mut self, message: &ExamsMessage) -> Option<Page> {
|
||||
if let Back = message {
|
||||
return Some(PreviousPage);
|
||||
}
|
||||
if let Some(selected_exam_id) = self.selected_exam_id
|
||||
&& GotoExam == *message
|
||||
{
|
||||
return Some(PreviousPage);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn navigated(&mut self) {}
|
||||
|
||||
fn update(&mut self, message: ExamsMessage) -> Task<RootMessage> {
|
||||
match message {
|
||||
Back => {}
|
||||
GotoExam => {}
|
||||
SelectExam(id) => {
|
||||
self.selected_exam_id = Some(id);
|
||||
}
|
||||
DeleteExam(id) => {
|
||||
self.selected_exam_id = None;
|
||||
}
|
||||
CreateExam => {
|
||||
let exam = Exam {
|
||||
id: Default::default(),
|
||||
timestamp: Utc::now(),
|
||||
deck_id: self.deck_id.id,
|
||||
};
|
||||
let mut exam_repo = self.state.exam.write();
|
||||
let handle = exam_repo.create_now(exam);
|
||||
if let Ok(handle) = handle {
|
||||
drop(exam_repo);
|
||||
if let Err(err) = self.append_exam_entries(handle) {
|
||||
return push_error_task!(err);
|
||||
}
|
||||
self.exams = Self::get_exams_list(&self.state, self.deck_id.id);
|
||||
self.selected_exam_id = Some(handle);
|
||||
} else {
|
||||
return push_error_task!(handle.err().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, ExamsMessage> {
|
||||
back_overlay(
|
||||
column![
|
||||
text(format!("Экзамены для колоды {}", self.deck.name)),
|
||||
row![row![
|
||||
column![
|
||||
scrollable(self.exams_list()).height(Fill),
|
||||
jl_button!("Добавить").width(Fill).on_press(CreateExam),
|
||||
]
|
||||
.spacing(DEFAULT_SPACING)
|
||||
.width(FillPortion(1)),
|
||||
self.selected_exam_view(),
|
||||
]]
|
||||
]
|
||||
.into(),
|
||||
Back,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExamsState {
|
||||
pub fn new(deck_id: EntityHandle<DeckSettings>, state: AppState) -> Self {
|
||||
let deck = state.decks.read().get_static(deck_id).unwrap();
|
||||
|
||||
Self {
|
||||
exams: Self::get_exams_list(&state, deck_id.id),
|
||||
state,
|
||||
deck_id,
|
||||
deck,
|
||||
selected_exam_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_exam(&self) -> Option<&ExamExtended> {
|
||||
self.exams.get(&self.selected_exam_id?)
|
||||
}
|
||||
fn selected_exam_mut(&mut self) -> Option<&mut ExamExtended> {
|
||||
self.exams.get_mut(&self.selected_exam_id?)
|
||||
}
|
||||
|
||||
fn append_exam_entries(&mut self, handle: EntityHandle<Exam>) -> MaybeError {
|
||||
let deck_id = self.deck_id.id;
|
||||
let words: Vec<_> = self
|
||||
.state
|
||||
.card_states
|
||||
.read()
|
||||
.iter()
|
||||
.filter(|state| state.deck_id == deck_id)
|
||||
.map(|state| state.word_id)
|
||||
.collect();
|
||||
let mut entries = words
|
||||
.iter()
|
||||
.map(|word_id| ExamEntry {
|
||||
id: Default::default(),
|
||||
exam_id: handle.id,
|
||||
word_id: *word_id,
|
||||
status: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.state
|
||||
.exam_entries
|
||||
.write()
|
||||
.create_now_many(entries.as_mut_slice())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_exams_list(state: &AppState, deck_id: Id) -> BTreeMap<EntityHandle<Exam>, ExamExtended> {
|
||||
let mut query = state
|
||||
.exam
|
||||
.read()
|
||||
.query(Arc::new(move |exam| exam.deck_id == deck_id));
|
||||
let handles: Vec<_> = query.handles().collect();
|
||||
query
|
||||
.iter(&state.exam.read())
|
||||
.cloned()
|
||||
.zip(handles)
|
||||
.map(|(e, h)| (h, ExamExtended::from(e, &state)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExamsState {
|
||||
pub fn exams_list(&self) -> Column<'_, ExamsMessage> {
|
||||
let mut column = Column::new();
|
||||
|
||||
for (handle, deck) in self.exams.iter() {
|
||||
column = column.push(
|
||||
button(text!("{}", &deck.timestamp.format("%d/%m/%Y %H:%M")))
|
||||
.on_press_with(move || SelectExam(*handle))
|
||||
.style(move |_x: &Theme, status: Status| Style {
|
||||
background: if status == Status::Hovered {
|
||||
Some(Background::Color(Color::WHITE.scale_alpha(0.2)))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
text_color: _x.palette().primary,
|
||||
border: Border {
|
||||
color: Default::default(),
|
||||
width: 0.0,
|
||||
radius: BUTTON_RADIUS.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
column
|
||||
}
|
||||
pub fn selected_exam_view(&self) -> Element<'_, ExamsMessage> {
|
||||
if let Some(exam) = self.selected_exam() {
|
||||
return column![
|
||||
center_x(text!(
|
||||
"Экзамен от {}",
|
||||
exam.timestamp.format("%d/%m/%Y %H:%M")
|
||||
)),
|
||||
row![text!("{} карточек", exam.total), Self::exam_bar(exam)]
|
||||
.height(30)
|
||||
.align_y(Center)
|
||||
.width(Fill).spacing(HALF_SPACING),
|
||||
self.exam_variable_view(exam),
|
||||
]
|
||||
.spacing(DEFAULT_SPACING)
|
||||
.width(FillPortion(2))
|
||||
.into();
|
||||
}
|
||||
|
||||
space().width(FillPortion(3)).into()
|
||||
}
|
||||
|
||||
fn exam_bar(exam: &ExamExtended) -> Element<'_, ExamsMessage> {
|
||||
let total = 1000.0;
|
||||
let answered = exam.answered as f64 / exam.total as f64 * total;
|
||||
let unknown = exam.unknown as f64 / exam.total as f64 * total;
|
||||
let remaining = exam.remaining() as f64 / exam.total as f64 * total;
|
||||
row![
|
||||
tooltip(
|
||||
container(space())
|
||||
.height(Fill)
|
||||
.width(FillPortion(answered as u16))
|
||||
.style(|theme: &Theme| {
|
||||
container::Style {
|
||||
background: Some(Background::Color(theme.palette().success)),
|
||||
text_color: Default::default(),
|
||||
border: Default::default(),
|
||||
shadow: Default::default(),
|
||||
snap: false,
|
||||
}
|
||||
}),
|
||||
text!("Правильно: {}%", answered / 10.0),
|
||||
tooltip::Position::Bottom
|
||||
),
|
||||
tooltip(
|
||||
container(space())
|
||||
.height(Fill)
|
||||
.width(FillPortion(unknown as u16))
|
||||
.style(|theme: &Theme| {
|
||||
container::Style {
|
||||
background: Some(Background::Color(theme.palette().danger)),
|
||||
text_color: Default::default(),
|
||||
border: Default::default(),
|
||||
shadow: Default::default(),
|
||||
snap: false,
|
||||
}
|
||||
}),
|
||||
text!("Не получилось: {}%", unknown / 10.0),
|
||||
tooltip::Position::Bottom
|
||||
),
|
||||
tooltip(
|
||||
container(space())
|
||||
.height(Fill)
|
||||
.width(FillPortion(remaining as u16))
|
||||
.style(|theme: &Theme| {
|
||||
container::Style {
|
||||
background: Some(Background::Color(theme.palette().primary)),
|
||||
text_color: Default::default(),
|
||||
border: Default::default(),
|
||||
shadow: Default::default(),
|
||||
snap: false,
|
||||
}
|
||||
}),
|
||||
text!("Осталось {}%", remaining / 10.0),
|
||||
tooltip::Position::Bottom
|
||||
)
|
||||
]
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn exam_variable_view(&self, exam: &ExamExtended) -> Element<'_, ExamsMessage> {
|
||||
if exam.finished() {
|
||||
self.exam_finished_view(exam)
|
||||
} else {
|
||||
self.exam_ongoing_view(exam)
|
||||
}
|
||||
}
|
||||
|
||||
fn exam_finished_view(&self, exam: &ExamExtended) -> Element<'_, ExamsMessage> {
|
||||
column![].spacing(DEFAULT_SPACING).into()
|
||||
}
|
||||
|
||||
fn exam_ongoing_view(&self, exam: &ExamExtended) -> Element<'_, ExamsMessage> {
|
||||
center_x(button("Продолжить экзамен").on_press(GotoExam)).into()
|
||||
}
|
||||
}
|
||||
|
||||
struct ExamExtended {
|
||||
exam: Exam,
|
||||
total: u32,
|
||||
answered: u32,
|
||||
unknown: u32,
|
||||
}
|
||||
|
||||
impl ExamExtended {
|
||||
fn from(exam: Exam, state: &AppState) -> Self {
|
||||
let entries = state.exam_entries.read();
|
||||
let mut total = 0;
|
||||
let mut answered = 0;
|
||||
let mut unknown = 0;
|
||||
entries.iter().filter(|e| e.exam_id == e.id).for_each(|e| {
|
||||
total += 1;
|
||||
match e.status {
|
||||
None => {}
|
||||
Some(s) => {
|
||||
if s {
|
||||
answered += 1;
|
||||
} else {
|
||||
unknown += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
exam,
|
||||
|
||||
total,
|
||||
answered,
|
||||
unknown,
|
||||
}
|
||||
}
|
||||
fn solved(&self) -> u32 {
|
||||
self.answered + self.unknown
|
||||
}
|
||||
|
||||
fn finished(&self) -> bool {
|
||||
self.solved() == self.total
|
||||
}
|
||||
|
||||
fn remaining(&self) -> u32 {
|
||||
self.total - self.solved()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ExamExtended {
|
||||
type Target = Exam;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.exam
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ExamExtended {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.exam
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@ use crate::core::deck::DeckData;
|
||||
use crate::core::srs::{AppendMode, OrderMode};
|
||||
use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData};
|
||||
use crate::data::repositories::{BatchedRepository, Repository};
|
||||
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
|
||||
use crate::navigation::Page::{Exams, History, PreviousPage, Repetition, RepetitionSettings};
|
||||
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||
use crate::notification::MaybeError;
|
||||
use crate::pages::exams::ExamsState;
|
||||
use crate::pages::history::HistoryState;
|
||||
use crate::pages::repetition::RepetitionState;
|
||||
use crate::pages::repetition_settings::RepetitionSettingsState;
|
||||
@@ -16,17 +17,16 @@ use crate::styling::*;
|
||||
use crate::{jl_button, jl_text_input};
|
||||
use chrono::{Days, Local, Utc};
|
||||
use hashbrown::HashSet;
|
||||
use iced::widget::button::{danger, Status};
|
||||
pub use iced::widget::button::{Catalog, Style};
|
||||
use iced::widget::button::{Status, danger};
|
||||
use iced::widget::tooltip::Position::Top;
|
||||
use iced::widget::{
|
||||
Column, Row, button, column, container, lazy, radio, row, scrollable, space, svg, text,
|
||||
text_input, tooltip,
|
||||
button, column, container, lazy, radio, row, scrollable, space, svg, text, tooltip, Column, Row,
|
||||
};
|
||||
use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Task, Theme};
|
||||
use iced_core::Padding;
|
||||
use iced_core::border::Radius;
|
||||
use iced_core::svg::Handle;
|
||||
use iced_core::Padding;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
@@ -74,7 +74,10 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
|
||||
self.selected_deck_handle.unwrap(),
|
||||
self.state.clone(),
|
||||
)))
|
||||
} else {
|
||||
} else if let GoToExams = message {
|
||||
Some(Exams(ExamsState::new(self.selected_deck_handle.unwrap(), self.state.clone())))
|
||||
} else
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -219,6 +222,7 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
|
||||
return push_error_task!(err);
|
||||
}
|
||||
}
|
||||
GoToExams => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -355,7 +359,10 @@ impl RepetitionsState {
|
||||
Some(deck.open_mode),
|
||||
SetOpenMode
|
||||
),
|
||||
jl_button!("История").on_press(GoToHistory),
|
||||
row![
|
||||
jl_button!("История").on_press(GoToHistory),
|
||||
jl_button!("Экзамены").on_press(GoToExams),
|
||||
].spacing(DEFAULT_SPACING),
|
||||
]
|
||||
.spacing(HALF_SPACING)
|
||||
}
|
||||
@@ -630,6 +637,7 @@ pub enum RepetitionsMessage {
|
||||
SetOpenMode(OrderMode),
|
||||
SetAppendMode(AppendMode),
|
||||
GoToHistory,
|
||||
GoToExams,
|
||||
GoToSettings,
|
||||
AppendWords(usize),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user