Examtracker 1.2.2__tar.gz → 1.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {examtracker-1.2.2/src/Examtracker.egg-info → examtracker-1.2.3}/PKG-INFO +8 -1
- {examtracker-1.2.2 → examtracker-1.2.3}/README.md +5 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/pyproject.toml +1 -1
- {examtracker-1.2.2 → examtracker-1.2.3}/requirements.txt +2 -0
- {examtracker-1.2.2 → examtracker-1.2.3/src/Examtracker.egg-info}/PKG-INFO +8 -1
- {examtracker-1.2.2 → examtracker-1.2.3}/src/Examtracker.egg-info/requires.txt +2 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/database.py +19 -14
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/database_scheme.py +6 -7
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/main.py +1 -1
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/screens/classscreen.py +28 -19
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/screens/examscreen.py +55 -30
- examtracker-1.2.3/src/examtracker/screens/inputscreens.py +32 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/screens/semesterscreen.py +15 -12
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/settings.py +7 -6
- {examtracker-1.2.2 → examtracker-1.2.3}/test/test_database.py +17 -11
- examtracker-1.2.2/src/examtracker/screens/inputscreens.py +0 -81
- {examtracker-1.2.2 → examtracker-1.2.3}/LICENSE +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/setup.cfg +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/Examtracker.egg-info/SOURCES.txt +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/Examtracker.egg-info/dependency_links.txt +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/Examtracker.egg-info/entry_points.txt +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/Examtracker.egg-info/top_level.txt +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/__init__.py +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/__main__.py +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/app.py +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/data/style.css +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/py.typed +0 -0
- {examtracker-1.2.2 → examtracker-1.2.3}/src/examtracker/textual_utils/vimtable.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Examtracker
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: A Python exam tracker for the Lernphase .It allows you to keep track of all exams you have completed and the scores you achieved.
|
|
5
5
|
Author-email: Samuel Huwiler <samuel.huwiler@gmx.ch>
|
|
6
6
|
License: MIT License
|
|
@@ -36,6 +36,8 @@ Requires-Dist: pydantic-settings
|
|
|
36
36
|
Requires-Dist: pathlib
|
|
37
37
|
Requires-Dist: pyyaml
|
|
38
38
|
Requires-Dist: rich
|
|
39
|
+
Requires-Dist: pymysql
|
|
40
|
+
Requires-Dist: cryptography
|
|
39
41
|
Dynamic: license-file
|
|
40
42
|
|
|
41
43
|

|
|
@@ -94,6 +96,7 @@ By default, the program searches for:
|
|
|
94
96
|
|
|
95
97
|
```yaml
|
|
96
98
|
database_path: "~/.config/examtracker/examtracker.db"
|
|
99
|
+
database_uri: "sqllite:///~/.config/examtracker/examtracker.db"
|
|
97
100
|
```
|
|
98
101
|
|
|
99
102
|
</details>
|
|
@@ -144,6 +147,7 @@ Environment variables override values defined in `config.yml`.
|
|
|
144
147
|
|
|
145
148
|
# TODO
|
|
146
149
|
|
|
150
|
+
- [x] Allow for floating point values for the points scored
|
|
147
151
|
- [x] Handle SQL errors
|
|
148
152
|
- [x] Initialize database
|
|
149
153
|
- [x] Configuration file support
|
|
@@ -153,5 +157,8 @@ Environment variables override values defined in `config.yml`.
|
|
|
153
157
|
- [x] enable Editing Exams and Semester
|
|
154
158
|
- [x] Feature where you can add the grade you got in the exams
|
|
155
159
|
- [ ] Enable externaly hosted database in config
|
|
160
|
+
- [x] Add a Notes section to the Exams for adding notes
|
|
161
|
+
- [ ] Fix sizing issue when removing a large note from an exam
|
|
162
|
+
- [x] Save the collumn when entering a submenu to set the cursor to the same place as before instead of to the top
|
|
156
163
|
- [x] write Tests
|
|
157
164
|
- [x] automagicly push new pypi versions and make release upon github tag push tag push
|
|
@@ -54,6 +54,7 @@ By default, the program searches for:
|
|
|
54
54
|
|
|
55
55
|
```yaml
|
|
56
56
|
database_path: "~/.config/examtracker/examtracker.db"
|
|
57
|
+
database_uri: "sqllite:///~/.config/examtracker/examtracker.db"
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
</details>
|
|
@@ -104,6 +105,7 @@ Environment variables override values defined in `config.yml`.
|
|
|
104
105
|
|
|
105
106
|
# TODO
|
|
106
107
|
|
|
108
|
+
- [x] Allow for floating point values for the points scored
|
|
107
109
|
- [x] Handle SQL errors
|
|
108
110
|
- [x] Initialize database
|
|
109
111
|
- [x] Configuration file support
|
|
@@ -113,5 +115,8 @@ Environment variables override values defined in `config.yml`.
|
|
|
113
115
|
- [x] enable Editing Exams and Semester
|
|
114
116
|
- [x] Feature where you can add the grade you got in the exams
|
|
115
117
|
- [ ] Enable externaly hosted database in config
|
|
118
|
+
- [x] Add a Notes section to the Exams for adding notes
|
|
119
|
+
- [ ] Fix sizing issue when removing a large note from an exam
|
|
120
|
+
- [x] Save the collumn when entering a submenu to set the cursor to the same place as before instead of to the top
|
|
116
121
|
- [x] write Tests
|
|
117
122
|
- [x] automagicly push new pypi versions and make release upon github tag push tag push
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" # If not defined, then legacy behavior c
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "Examtracker" # REQUIRED, is the only field that cannot be marked as dynamic.
|
|
10
|
-
version = "1.2.
|
|
10
|
+
version = "1.2.3" # REQUIRED, although can be dynamic
|
|
11
11
|
description = "A Python exam tracker for the Lernphase .It allows you to keep track of all exams you have completed and the scores you achieved."
|
|
12
12
|
|
|
13
13
|
readme = "README.md"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Examtracker
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: A Python exam tracker for the Lernphase .It allows you to keep track of all exams you have completed and the scores you achieved.
|
|
5
5
|
Author-email: Samuel Huwiler <samuel.huwiler@gmx.ch>
|
|
6
6
|
License: MIT License
|
|
@@ -36,6 +36,8 @@ Requires-Dist: pydantic-settings
|
|
|
36
36
|
Requires-Dist: pathlib
|
|
37
37
|
Requires-Dist: pyyaml
|
|
38
38
|
Requires-Dist: rich
|
|
39
|
+
Requires-Dist: pymysql
|
|
40
|
+
Requires-Dist: cryptography
|
|
39
41
|
Dynamic: license-file
|
|
40
42
|
|
|
41
43
|

|
|
@@ -94,6 +96,7 @@ By default, the program searches for:
|
|
|
94
96
|
|
|
95
97
|
```yaml
|
|
96
98
|
database_path: "~/.config/examtracker/examtracker.db"
|
|
99
|
+
database_uri: "sqllite:///~/.config/examtracker/examtracker.db"
|
|
97
100
|
```
|
|
98
101
|
|
|
99
102
|
</details>
|
|
@@ -144,6 +147,7 @@ Environment variables override values defined in `config.yml`.
|
|
|
144
147
|
|
|
145
148
|
# TODO
|
|
146
149
|
|
|
150
|
+
- [x] Allow for floating point values for the points scored
|
|
147
151
|
- [x] Handle SQL errors
|
|
148
152
|
- [x] Initialize database
|
|
149
153
|
- [x] Configuration file support
|
|
@@ -153,5 +157,8 @@ Environment variables override values defined in `config.yml`.
|
|
|
153
157
|
- [x] enable Editing Exams and Semester
|
|
154
158
|
- [x] Feature where you can add the grade you got in the exams
|
|
155
159
|
- [ ] Enable externaly hosted database in config
|
|
160
|
+
- [x] Add a Notes section to the Exams for adding notes
|
|
161
|
+
- [ ] Fix sizing issue when removing a large note from an exam
|
|
162
|
+
- [x] Save the collumn when entering a submenu to set the cursor to the same place as before instead of to the top
|
|
156
163
|
- [x] write Tests
|
|
157
164
|
- [x] automagicly push new pypi versions and make release upon github tag push tag push
|
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
Database related functions for getting and adding entries
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Optional
|
|
6
6
|
|
|
7
|
-
from sqlalchemy import Engine
|
|
7
|
+
from sqlalchemy import Engine # type:ignore
|
|
8
8
|
from sqlalchemy.orm import Session
|
|
9
9
|
|
|
10
10
|
from examtracker.database_scheme import Base, Class, Exam, Semester
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def create_database_engine(
|
|
14
|
-
|
|
15
|
-
inspector = inspect(engine)
|
|
13
|
+
def create_database_engine(uri: str) -> Engine:
|
|
14
|
+
from sqlalchemy import create_engine
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
engine = create_engine(uri, echo=False)
|
|
17
|
+
create_tables(engine)
|
|
19
18
|
|
|
20
19
|
return engine
|
|
21
20
|
|
|
@@ -31,7 +30,7 @@ def get_semester_by_name(session: Session, name: str) -> Semester:
|
|
|
31
30
|
def add_semester(session: Session, name: str) -> None:
|
|
32
31
|
semester_obj = Semester(name=name)
|
|
33
32
|
session.add(semester_obj)
|
|
34
|
-
session.flush
|
|
33
|
+
session.flush()
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
def get_class_by_id(session: Session, class_id: int) -> Class:
|
|
@@ -56,29 +55,35 @@ def add_class_to_semester(
|
|
|
56
55
|
|
|
57
56
|
|
|
58
57
|
def add_exam_to_class(
|
|
59
|
-
session: Session,
|
|
58
|
+
session: Session,
|
|
59
|
+
name: str,
|
|
60
|
+
max_points: float,
|
|
61
|
+
scored_points: float,
|
|
62
|
+
notes: str,
|
|
63
|
+
class_obj: Class,
|
|
60
64
|
) -> None:
|
|
61
65
|
exam_obj = Exam(
|
|
62
66
|
name=name,
|
|
63
67
|
max_points=max_points,
|
|
64
68
|
scored_points=scored_points,
|
|
69
|
+
notes=notes,
|
|
65
70
|
class_id=class_obj.class_id,
|
|
66
71
|
)
|
|
67
72
|
session.add(exam_obj)
|
|
68
73
|
session.flush()
|
|
69
74
|
|
|
70
75
|
|
|
71
|
-
def get_all_exams_for_class(session: Session, class_obj: Class) ->
|
|
76
|
+
def get_all_exams_for_class(session: Session, class_obj: Class) -> list[Exam]:
|
|
72
77
|
return session.query(Exam).filter_by(class_id=class_obj.class_id).all()
|
|
73
78
|
|
|
74
79
|
|
|
75
|
-
def get_all_semester(session: Session) ->
|
|
76
|
-
return session.query(Semester).all()
|
|
80
|
+
def get_all_semester(session: Session) -> list[Semester]:
|
|
81
|
+
return session.query(Semester).all()[::-1]
|
|
77
82
|
|
|
78
83
|
|
|
79
84
|
def get_all_classes_for_semester(
|
|
80
85
|
session: Session, semster_obj: Semester
|
|
81
|
-
) ->
|
|
86
|
+
) -> list[Class]:
|
|
82
87
|
return session.query(Class).filter_by(semester_id=semster_obj.semester_id).all()
|
|
83
88
|
|
|
84
89
|
|
|
@@ -98,7 +103,7 @@ def remove_exam_by_id(session: Session, exam_id: int) -> None:
|
|
|
98
103
|
|
|
99
104
|
|
|
100
105
|
def main() -> int:
|
|
101
|
-
engine: Engine = create_database_engine("test.db")
|
|
106
|
+
engine: Engine = create_database_engine("sqlite:///test.db")
|
|
102
107
|
create_tables(engine)
|
|
103
108
|
session = Session(engine)
|
|
104
109
|
add_semester(session, "FS25")
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
Defining the database tables and entries
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import List, Optional
|
|
6
|
-
|
|
7
5
|
from sqlalchemy import ForeignKey, String
|
|
8
6
|
from sqlalchemy.orm import ( # type: ignore
|
|
9
7
|
DeclarativeBase,
|
|
@@ -21,8 +19,9 @@ class Exam(Base):
|
|
|
21
19
|
__tablename__ = "exams"
|
|
22
20
|
exam_id: Mapped[int] = mapped_column(primary_key=True)
|
|
23
21
|
name: Mapped[str] = mapped_column(String(30))
|
|
24
|
-
max_points: Mapped[
|
|
25
|
-
scored_points: Mapped[
|
|
22
|
+
max_points: Mapped[float]
|
|
23
|
+
scored_points: Mapped[float]
|
|
24
|
+
notes: Mapped[str] = mapped_column(String(100))
|
|
26
25
|
class_id: Mapped[int] = mapped_column(ForeignKey("classes.class_id"))
|
|
27
26
|
class_: Mapped["Class"] = relationship(back_populates="exams") # type:ignore
|
|
28
27
|
|
|
@@ -34,11 +33,11 @@ class Class(Base):
|
|
|
34
33
|
__tablename__ = "classes"
|
|
35
34
|
class_id: Mapped[int] = mapped_column(primary_key=True)
|
|
36
35
|
name: Mapped[str] = mapped_column(String(30))
|
|
37
|
-
exam_grade: Mapped[
|
|
36
|
+
exam_grade: Mapped[float | None]
|
|
38
37
|
semester_id: Mapped[int] = mapped_column(ForeignKey("semester.semester_id"))
|
|
39
38
|
semester: Mapped["Semester"] = relationship(back_populates="classes") # type:ignore
|
|
40
39
|
|
|
41
|
-
exams: Mapped[
|
|
40
|
+
exams: Mapped[list["Exam"]] = relationship(
|
|
42
41
|
back_populates="class_",
|
|
43
42
|
cascade="all, delete-orphan",
|
|
44
43
|
) # type: ignore
|
|
@@ -51,7 +50,7 @@ class Semester(Base):
|
|
|
51
50
|
__tablename__ = "semester"
|
|
52
51
|
semester_id: Mapped[int] = mapped_column(primary_key=True)
|
|
53
52
|
name: Mapped[str] = mapped_column(String(30), unique=True)
|
|
54
|
-
classes: Mapped[
|
|
53
|
+
classes: Mapped[list["Class"]] = relationship(
|
|
55
54
|
back_populates="semester",
|
|
56
55
|
cascade="all, delete-orphan",
|
|
57
56
|
) # type: ignore
|
|
@@ -7,7 +7,7 @@ from examtracker.settings import Settings
|
|
|
7
7
|
|
|
8
8
|
def main() -> int:
|
|
9
9
|
config: Settings = Settings()
|
|
10
|
-
db_engine = create_database_engine(config.
|
|
10
|
+
db_engine = create_database_engine(config.database_uri)
|
|
11
11
|
app = ExamTracker(db_engine, config)
|
|
12
12
|
app.run()
|
|
13
13
|
return 0
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import math
|
|
4
|
+
|
|
3
5
|
from sqlalchemy.orm import Session
|
|
4
6
|
from textual import on
|
|
5
7
|
from textual.app import ComposeResult, Screen
|
|
@@ -13,27 +15,27 @@ from examtracker.database import (
|
|
|
13
15
|
remove_class_by_id,
|
|
14
16
|
)
|
|
15
17
|
from examtracker.screens.examscreen import ExamScreen
|
|
16
|
-
from examtracker.screens.inputscreens import
|
|
18
|
+
from examtracker.screens.inputscreens import MultiInputScreen
|
|
17
19
|
from examtracker.textual_utils.vimtable import VimTable
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
class AddClassScreen(
|
|
22
|
+
class AddClassScreen(MultiInputScreen):
|
|
21
23
|
def __init__(self, semester_name: str, **kwargs) -> None:
|
|
22
|
-
super().__init__(**kwargs)
|
|
24
|
+
super().__init__(2, **kwargs)
|
|
23
25
|
self.semester_name = semester_name
|
|
24
26
|
self.label_text = f"Add class to: {self.semester_name}"
|
|
25
27
|
|
|
26
28
|
def on_mount(self) -> None:
|
|
27
|
-
self.
|
|
28
|
-
self.
|
|
29
|
-
self.
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
29
|
+
self.buttons[0].placeholder = "class name"
|
|
30
|
+
self.buttons[0].value = ""
|
|
31
|
+
self.buttons[1].placeholder = "exam_score"
|
|
32
|
+
self.buttons[1].value = ""
|
|
33
|
+
self.buttons[0].focus()
|
|
32
34
|
|
|
33
35
|
# Submit on Enter from any Input
|
|
34
36
|
def submit(self) -> None:
|
|
35
|
-
name = self.
|
|
36
|
-
exam_score = self.
|
|
37
|
+
name = self.buttons[0].value.strip()
|
|
38
|
+
exam_score = self.buttons[1].value.strip()
|
|
37
39
|
try:
|
|
38
40
|
score = float(exam_score)
|
|
39
41
|
except ValueError:
|
|
@@ -49,29 +51,32 @@ class AddClassScreen(DoubleInputScreen):
|
|
|
49
51
|
self.app.pop_screen()
|
|
50
52
|
|
|
51
53
|
|
|
52
|
-
class EditClassScreen(
|
|
54
|
+
class EditClassScreen(MultiInputScreen):
|
|
53
55
|
def __init__(self, class_id: int, **kwargs) -> None:
|
|
54
|
-
super().__init__(**kwargs)
|
|
56
|
+
super().__init__(2, **kwargs)
|
|
55
57
|
self.class_id = class_id
|
|
56
58
|
self.label_text = "Edit class"
|
|
57
59
|
|
|
58
60
|
def on_mount(self) -> None:
|
|
59
61
|
class_obj = get_class_by_id(self.db_session, self.class_id)
|
|
60
|
-
self.
|
|
62
|
+
self.buttons[0].value = class_obj.name
|
|
61
63
|
if class_obj.exam_grade is not None:
|
|
62
|
-
self.
|
|
64
|
+
self.buttons[1].value = str(class_obj.exam_grade)
|
|
63
65
|
else:
|
|
64
|
-
self.
|
|
65
|
-
self.
|
|
66
|
+
self.buttons[1].value = ""
|
|
67
|
+
self.buttons[0].focus()
|
|
66
68
|
|
|
67
69
|
# Submit on Enter from any Input
|
|
68
70
|
def submit(self) -> None:
|
|
69
|
-
name = self.
|
|
70
|
-
exam_score = self.
|
|
71
|
+
name = self.buttons[0].value.strip()
|
|
72
|
+
exam_score = self.buttons[1].value.strip()
|
|
71
73
|
try:
|
|
72
74
|
score = float(exam_score)
|
|
73
75
|
except ValueError:
|
|
74
76
|
score = None
|
|
77
|
+
|
|
78
|
+
if isinstance(score, float) and not math.isfinite(score):
|
|
79
|
+
score = None
|
|
75
80
|
if not name:
|
|
76
81
|
return # Require class name
|
|
77
82
|
|
|
@@ -101,6 +106,7 @@ class ClassScreen(Screen):
|
|
|
101
106
|
super().__init__()
|
|
102
107
|
self.db_session: Session = self.app.db_session # type: ignore
|
|
103
108
|
self.semester_name = semester_name
|
|
109
|
+
self.latest_row = 0
|
|
104
110
|
|
|
105
111
|
def compose(self) -> ComposeResult:
|
|
106
112
|
yield Header()
|
|
@@ -147,9 +153,11 @@ class ClassScreen(Screen):
|
|
|
147
153
|
score = cls.exam_grade
|
|
148
154
|
str_score = ""
|
|
149
155
|
if score is not None:
|
|
150
|
-
str_score = cls.exam_grade
|
|
156
|
+
str_score = str(cls.exam_grade)
|
|
151
157
|
self.class_table.add_row(cls.class_id, cls.name, str_score)
|
|
152
158
|
|
|
159
|
+
self.class_table.move_cursor(row=self.latest_row, column=0)
|
|
160
|
+
|
|
153
161
|
def on_screen_resume(self) -> None:
|
|
154
162
|
self.refresh_table()
|
|
155
163
|
|
|
@@ -158,6 +166,7 @@ class ClassScreen(Screen):
|
|
|
158
166
|
row_index = self.class_table.cursor_row
|
|
159
167
|
if row_index is None:
|
|
160
168
|
return
|
|
169
|
+
self.latest_row = row_index
|
|
161
170
|
|
|
162
171
|
class_id = self.class_table.get_row_at(row_index)[0]
|
|
163
172
|
self.app.push_screen(ExamScreen(class_id))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import math
|
|
2
|
+
|
|
2
3
|
from textual import on
|
|
3
4
|
from textual.app import ComposeResult, Screen
|
|
4
5
|
from textual.widgets import Footer, Header
|
|
@@ -10,7 +11,7 @@ from examtracker.database import (
|
|
|
10
11
|
get_exam_by_id,
|
|
11
12
|
remove_exam_by_id,
|
|
12
13
|
)
|
|
13
|
-
from examtracker.screens.inputscreens import
|
|
14
|
+
from examtracker.screens.inputscreens import MultiInputScreen
|
|
14
15
|
from examtracker.textual_utils.vimtable import VimTable
|
|
15
16
|
|
|
16
17
|
|
|
@@ -23,93 +24,108 @@ def proc_color(value: float) -> str:
|
|
|
23
24
|
return "#fc1c03"
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
class EditExamScreen(
|
|
27
|
+
class EditExamScreen(MultiInputScreen):
|
|
27
28
|
def __init__(self, exam_id: int, **kwargs):
|
|
28
|
-
super().__init__(**kwargs)
|
|
29
|
+
super().__init__(4, **kwargs)
|
|
29
30
|
self.label_text = "Edit exam"
|
|
30
31
|
self.exam_id = exam_id
|
|
31
|
-
self.db_session = self.app.db_session # type:ignore
|
|
32
|
+
self.db_session = self.app.db_session # type: ignore
|
|
32
33
|
|
|
33
34
|
def on_mount(self) -> None:
|
|
34
35
|
exam = get_exam_by_id(self.db_session, self.exam_id)
|
|
35
36
|
|
|
36
|
-
self.
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
37
|
+
self.buttons[0].value = exam.name
|
|
38
|
+
self.buttons[1].value = str(exam.max_points)
|
|
39
|
+
self.buttons[2].value = str(exam.scored_points)
|
|
40
|
+
self.buttons[3].value = str(exam.notes)
|
|
39
41
|
|
|
40
|
-
self.
|
|
42
|
+
self.buttons[0].focus()
|
|
41
43
|
|
|
42
44
|
def submit(self) -> None:
|
|
43
|
-
name = self.
|
|
45
|
+
name = self.buttons[0].value.strip()
|
|
44
46
|
if not name:
|
|
45
47
|
return # Require class name
|
|
46
48
|
|
|
47
49
|
# Optional exam points
|
|
48
50
|
try:
|
|
49
|
-
max_points =
|
|
51
|
+
max_points = float(self.buttons[1].value.strip())
|
|
50
52
|
except ValueError:
|
|
51
53
|
# Ignore invalid numbers for now
|
|
52
54
|
max_points = 0
|
|
53
55
|
|
|
54
56
|
try:
|
|
55
|
-
scored_points =
|
|
57
|
+
scored_points = float(self.buttons[2].value.strip())
|
|
56
58
|
except ValueError:
|
|
57
59
|
# Ignore invalid numbers for now
|
|
58
60
|
scored_points = 0
|
|
61
|
+
if not math.isfinite(scored_points):
|
|
62
|
+
scored_points = 0
|
|
63
|
+
|
|
64
|
+
notes = self.buttons[3].value.strip()
|
|
59
65
|
|
|
60
66
|
exam = get_exam_by_id(self.db_session, self.exam_id)
|
|
61
67
|
exam.name = name
|
|
62
68
|
exam.max_points = max_points
|
|
63
69
|
exam.scored_points = scored_points
|
|
70
|
+
exam.notes = notes
|
|
64
71
|
|
|
65
72
|
self.db_session.commit()
|
|
66
73
|
|
|
67
74
|
self.app.pop_screen()
|
|
68
75
|
|
|
69
76
|
|
|
70
|
-
class AddExamScreen(
|
|
77
|
+
class AddExamScreen(MultiInputScreen):
|
|
71
78
|
BINDINGS = [
|
|
72
79
|
("escape", "app.pop_screen", "Cancel"),
|
|
73
80
|
]
|
|
74
81
|
|
|
75
82
|
def __init__(self, class_id: int, **kwargs):
|
|
76
|
-
super().__init__(**kwargs)
|
|
83
|
+
super().__init__(4, **kwargs)
|
|
77
84
|
self.db_session = self.app.db_session # type: ignore
|
|
78
85
|
self.class_id = class_id
|
|
79
86
|
self.class_name = get_class_by_id(self.db_session, self.class_id).name
|
|
80
87
|
self.label_text = f"Add exam to: {self.class_name}"
|
|
81
88
|
|
|
82
89
|
def on_mount(self) -> None:
|
|
83
|
-
self.
|
|
84
|
-
self.
|
|
85
|
-
self.
|
|
86
|
-
self.
|
|
90
|
+
self.buttons[0].focus()
|
|
91
|
+
self.buttons[0].placeholder = "Exam Name"
|
|
92
|
+
self.buttons[1].placeholder = "Max Points"
|
|
93
|
+
self.buttons[2].placeholder = "Scored Points"
|
|
94
|
+
self.buttons[3].placeholder = "Additional Notes"
|
|
87
95
|
|
|
88
|
-
self.
|
|
89
|
-
self.
|
|
90
|
-
self.
|
|
96
|
+
self.buttons[0].value = ""
|
|
97
|
+
self.buttons[1].value = ""
|
|
98
|
+
self.buttons[2].value = ""
|
|
99
|
+
self.buttons[3].value = ""
|
|
91
100
|
|
|
92
101
|
def submit(self) -> None:
|
|
93
|
-
name = self.
|
|
102
|
+
name = self.buttons[0].value.strip()
|
|
94
103
|
if not name:
|
|
95
104
|
return # Require class name
|
|
96
105
|
|
|
97
106
|
try:
|
|
98
|
-
max_points =
|
|
107
|
+
max_points = float(self.buttons[1].value.strip())
|
|
99
108
|
except ValueError:
|
|
100
109
|
# Ignore invalid numbers for now
|
|
101
110
|
max_points = 0
|
|
102
111
|
|
|
103
112
|
try:
|
|
104
|
-
scored_points =
|
|
113
|
+
scored_points = float(self.buttons[2].value.strip())
|
|
105
114
|
except ValueError:
|
|
106
115
|
# Ignore invalid numbers for now
|
|
107
116
|
scored_points = 0
|
|
108
117
|
|
|
118
|
+
if not math.isfinite(scored_points):
|
|
119
|
+
scored_points = 0
|
|
120
|
+
|
|
121
|
+
notes = self.buttons[3].value.strip()
|
|
122
|
+
|
|
109
123
|
class_obj = get_class_by_id(self.db_session, self.class_id)
|
|
110
124
|
|
|
111
125
|
# Add the class
|
|
112
|
-
add_exam_to_class(
|
|
126
|
+
add_exam_to_class(
|
|
127
|
+
self.db_session, name, max_points, scored_points, notes, class_obj
|
|
128
|
+
)
|
|
113
129
|
self.db_session.commit()
|
|
114
130
|
|
|
115
131
|
# Pop the screen and return
|
|
@@ -134,7 +150,9 @@ class ExamScreen(Screen):
|
|
|
134
150
|
yield Header()
|
|
135
151
|
|
|
136
152
|
self.exam_table: VimTable = VimTable()
|
|
137
|
-
self.exam_table.add_columns(
|
|
153
|
+
self.exam_table.add_columns(
|
|
154
|
+
"ID", "Name", "Max_points", "Scored_points", "%", "Additional Notes"
|
|
155
|
+
)
|
|
138
156
|
self.exam_table.cursor_type = "cell"
|
|
139
157
|
self.exam_table.border_title = f"Exams completed for: {self.class_name}"
|
|
140
158
|
yield self.exam_table
|
|
@@ -161,18 +179,25 @@ class ExamScreen(Screen):
|
|
|
161
179
|
self.refresh_table()
|
|
162
180
|
|
|
163
181
|
def refresh_table(self) -> None:
|
|
182
|
+
from rich.text import Text
|
|
183
|
+
|
|
164
184
|
class_obj = get_class_by_id(self.db_session, self.class_id)
|
|
165
185
|
|
|
166
186
|
self.exam_table.clear()
|
|
167
|
-
for
|
|
168
|
-
if
|
|
187
|
+
for exm in get_all_exams_for_class(self.db_session, class_obj):
|
|
188
|
+
if exm.max_points == 0:
|
|
169
189
|
proc = 0.0
|
|
170
190
|
else:
|
|
171
|
-
proc = round((
|
|
191
|
+
proc = round((exm.scored_points / exm.max_points) * 100, 2)
|
|
172
192
|
|
|
173
193
|
proc_text = Text(str(proc), style=proc_color(proc))
|
|
174
194
|
self.exam_table.add_row(
|
|
175
|
-
|
|
195
|
+
exm.exam_id,
|
|
196
|
+
exm.name,
|
|
197
|
+
exm.max_points,
|
|
198
|
+
exm.scored_points,
|
|
199
|
+
proc_text,
|
|
200
|
+
exm.notes,
|
|
176
201
|
)
|
|
177
202
|
|
|
178
203
|
def on_screen_resume(self) -> None:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual import on
|
|
4
|
+
from textual.app import ComposeResult, Screen
|
|
5
|
+
from textual.containers import Vertical
|
|
6
|
+
from textual.widgets import Footer, Header, Input, Label
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MultiInputScreen(Screen):
|
|
10
|
+
BINDINGS = [
|
|
11
|
+
("escape", "app.pop_screen", "Cancel"),
|
|
12
|
+
]
|
|
13
|
+
label_text: str = ""
|
|
14
|
+
|
|
15
|
+
def __init__(self, n: int, **kwargs) -> None:
|
|
16
|
+
super().__init__(**kwargs)
|
|
17
|
+
self.buttons: list[Input] = []
|
|
18
|
+
self.db_session = self.app.db_session # type: ignore
|
|
19
|
+
for i in range(n):
|
|
20
|
+
self.buttons.append(Input())
|
|
21
|
+
|
|
22
|
+
def compose(self) -> ComposeResult:
|
|
23
|
+
yield Header()
|
|
24
|
+
with Vertical():
|
|
25
|
+
yield Label(self.label_text)
|
|
26
|
+
for i in range(len(self.buttons)):
|
|
27
|
+
yield self.buttons[i]
|
|
28
|
+
yield Footer()
|
|
29
|
+
|
|
30
|
+
@on(Input.Submitted)
|
|
31
|
+
def input_submitted(self, event: Input.Submitted) -> None:
|
|
32
|
+
self.submit() # type: ignore
|
|
@@ -12,23 +12,23 @@ from examtracker.database import (
|
|
|
12
12
|
remove_semester_by_name,
|
|
13
13
|
)
|
|
14
14
|
from examtracker.screens.classscreen import ClassScreen
|
|
15
|
-
from examtracker.screens.inputscreens import
|
|
15
|
+
from examtracker.screens.inputscreens import MultiInputScreen
|
|
16
16
|
from examtracker.textual_utils.vimtable import VimTable
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class AddSemesterScreen(
|
|
19
|
+
class AddSemesterScreen(MultiInputScreen):
|
|
20
20
|
|
|
21
21
|
def __init__(self, *args, **kwargs) -> None:
|
|
22
|
-
super().__init__(*args, **kwargs)
|
|
22
|
+
super().__init__(1, *args, **kwargs)
|
|
23
23
|
self.label_text = "Add new semester"
|
|
24
24
|
|
|
25
25
|
def on_mount(self) -> None:
|
|
26
|
-
self.
|
|
27
|
-
self.
|
|
28
|
-
self.
|
|
26
|
+
self.buttons[0].focus()
|
|
27
|
+
self.buttons[0].placeholder = "semester name"
|
|
28
|
+
self.buttons[0].value = ""
|
|
29
29
|
|
|
30
30
|
def submit(self) -> None:
|
|
31
|
-
name = self.
|
|
31
|
+
name = self.buttons[0].value.strip()
|
|
32
32
|
if not name:
|
|
33
33
|
return
|
|
34
34
|
try:
|
|
@@ -42,20 +42,20 @@ class AddSemesterScreen(SingleInputScreen):
|
|
|
42
42
|
self.app.pop_screen()
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
class EditSemesterScreen(
|
|
45
|
+
class EditSemesterScreen(MultiInputScreen):
|
|
46
46
|
|
|
47
47
|
def __init__(self, semester_name: str, **kwargs) -> None:
|
|
48
|
-
super().__init__(**kwargs)
|
|
48
|
+
super().__init__(1, **kwargs)
|
|
49
49
|
self.semester_name = semester_name
|
|
50
50
|
self.label_text = "Edit semester"
|
|
51
51
|
|
|
52
52
|
def on_mount(self) -> None:
|
|
53
53
|
semester = get_semester_by_name(self.db_session, self.semester_name)
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
54
|
+
self.buttons[0].value = semester.name
|
|
55
|
+
self.buttons[0].focus()
|
|
56
56
|
|
|
57
57
|
def submit(self) -> None:
|
|
58
|
-
name = self.
|
|
58
|
+
name = self.buttons[0].value.strip()
|
|
59
59
|
if not name:
|
|
60
60
|
return
|
|
61
61
|
semester = get_semester_by_name(self.db_session, self.semester_name)
|
|
@@ -79,6 +79,7 @@ class SemesterScreen(Screen):
|
|
|
79
79
|
|
|
80
80
|
def __init__(self) -> None:
|
|
81
81
|
super().__init__()
|
|
82
|
+
self.latest_row = 0
|
|
82
83
|
|
|
83
84
|
def compose(self) -> ComposeResult:
|
|
84
85
|
yield Header()
|
|
@@ -98,6 +99,7 @@ class SemesterScreen(Screen):
|
|
|
98
99
|
self.semester_table.clear()
|
|
99
100
|
for sem in get_all_semester(self.db_session):
|
|
100
101
|
self.semester_table.add_row(sem.name)
|
|
102
|
+
self.semester_table.move_cursor(row=self.latest_row, column=0)
|
|
101
103
|
|
|
102
104
|
def action_edit(self) -> None:
|
|
103
105
|
row_index = self.semester_table.cursor_row
|
|
@@ -133,6 +135,7 @@ class SemesterScreen(Screen):
|
|
|
133
135
|
row_index = self.semester_table.cursor_row
|
|
134
136
|
if row_index is None:
|
|
135
137
|
return
|
|
138
|
+
self.latest_row = row_index
|
|
136
139
|
|
|
137
140
|
semester_name = self.semester_table.get_row_at(row_index)[0]
|
|
138
141
|
self.app.push_screen(ClassScreen(semester_name))
|
|
@@ -4,9 +4,8 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
from importlib.resources import files
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
|
-
import yaml # type: ignore
|
|
10
9
|
from platformdirs import PlatformDirs
|
|
11
10
|
from pydantic import Field
|
|
12
11
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
@@ -26,12 +25,12 @@ def get_config_dir() -> Path:
|
|
|
26
25
|
return Path(dirs.user_config_dir)
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
def
|
|
28
|
+
def get_default_database_uri() -> str:
|
|
30
29
|
"""Return a writable database path in the user data directory."""
|
|
31
30
|
|
|
32
31
|
db_dir = get_config_dir()
|
|
33
32
|
db_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
-
return db_dir / "examtracker.db"
|
|
33
|
+
return "sqlite:///" + str(db_dir / "examtracker.db")
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
def get_default_css_path() -> Path:
|
|
@@ -39,7 +38,9 @@ def get_default_css_path() -> Path:
|
|
|
39
38
|
return Path(files("examtracker").joinpath("data/style.css")) # type:ignore
|
|
40
39
|
|
|
41
40
|
|
|
42
|
-
def yaml_config_settings_source(settings_cls) ->
|
|
41
|
+
def yaml_config_settings_source(settings_cls) -> dict[str, Any]:
|
|
42
|
+
import yaml # type: ignore
|
|
43
|
+
|
|
43
44
|
config_path = os.getenv("EXAMTRACKER_CONFIG")
|
|
44
45
|
|
|
45
46
|
if config_path:
|
|
@@ -58,7 +59,7 @@ def yaml_config_settings_source(settings_cls) -> Dict[str, Any]:
|
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
class Settings(BaseSettings):
|
|
61
|
-
|
|
62
|
+
database_uri: str = Field(default_factory=lambda: str(get_default_database_uri()))
|
|
62
63
|
css_path: str = Field(default_factory=lambda: str(get_default_css_path()))
|
|
63
64
|
model_config = SettingsConfigDict(
|
|
64
65
|
env_prefix="EXAMTRACKER_",
|
|
@@ -87,10 +87,8 @@ def test_semester_name_uniqueness(name: str) -> None:
|
|
|
87
87
|
session.commit()
|
|
88
88
|
|
|
89
89
|
# Second insertion of the exact same name
|
|
90
|
-
add_semester(session, name)
|
|
91
|
-
|
|
92
90
|
with pytest.raises(IntegrityError):
|
|
93
|
-
session
|
|
91
|
+
add_semester(session, name)
|
|
94
92
|
|
|
95
93
|
|
|
96
94
|
# ------------- Class tests -------------
|
|
@@ -139,11 +137,18 @@ def test_get_nonexistant_class() -> None:
|
|
|
139
137
|
|
|
140
138
|
@given(
|
|
141
139
|
name=valid_text,
|
|
142
|
-
max_points=
|
|
143
|
-
|
|
140
|
+
max_points=floats(
|
|
141
|
+
min_value=0, max_value=10000, allow_infinity=False, allow_nan=False
|
|
142
|
+
),
|
|
143
|
+
scored_points=floats(
|
|
144
|
+
min_value=0, max_value=10000, allow_infinity=False, allow_nan=False
|
|
145
|
+
),
|
|
146
|
+
notes=text(),
|
|
144
147
|
)
|
|
145
148
|
@settings(deadline=None)
|
|
146
|
-
def test_add_get_exam(
|
|
149
|
+
def test_add_get_exam(
|
|
150
|
+
name: str, max_points: float, scored_points: float, notes: str
|
|
151
|
+
) -> None:
|
|
147
152
|
with temp_db() as session:
|
|
148
153
|
# Setup Hierarchy: Semester -> Class
|
|
149
154
|
add_semester(session, "S1")
|
|
@@ -151,7 +156,7 @@ def test_add_get_exam(name: str, max_points: int, scored_points: int) -> None:
|
|
|
151
156
|
add_class_to_semester(session, "C1", sem)
|
|
152
157
|
cls = sem.classes[0]
|
|
153
158
|
|
|
154
|
-
add_exam_to_class(session, name, max_points, scored_points, cls)
|
|
159
|
+
add_exam_to_class(session, name, max_points, scored_points, notes, cls)
|
|
155
160
|
session.commit()
|
|
156
161
|
|
|
157
162
|
# Verify exam data
|
|
@@ -160,6 +165,7 @@ def test_add_get_exam(name: str, max_points: int, scored_points: int) -> None:
|
|
|
160
165
|
assert exams[0].name == name
|
|
161
166
|
assert exams[0].max_points == max_points
|
|
162
167
|
assert exams[0].scored_points == scored_points
|
|
168
|
+
assert exams[0].notes == notes
|
|
163
169
|
|
|
164
170
|
|
|
165
171
|
def test_get_nonexistant_exam() -> None:
|
|
@@ -181,7 +187,7 @@ def test_remove_semester_cascades_to_classes_and_exams() -> None:
|
|
|
181
187
|
add_class_to_semester(session, "Nested Class", sem)
|
|
182
188
|
cls = sem.classes[0]
|
|
183
189
|
|
|
184
|
-
add_exam_to_class(session, "Nested Exam", 100, 90, cls)
|
|
190
|
+
add_exam_to_class(session, "Nested Exam", 100, 90, "notes", cls)
|
|
185
191
|
session.commit()
|
|
186
192
|
|
|
187
193
|
# Capture IDs to verify deletion later
|
|
@@ -229,7 +235,7 @@ def test_remove_exam_by_id_logic() -> None:
|
|
|
229
235
|
sem = get_semester_by_name(session, "S1")
|
|
230
236
|
add_class_to_semester(session, "C1", sem)
|
|
231
237
|
cls = sem.classes[0]
|
|
232
|
-
add_exam_to_class(session, "Final", 100, 100, cls)
|
|
238
|
+
add_exam_to_class(session, "Final", 100, 100, "notes", cls)
|
|
233
239
|
session.commit()
|
|
234
240
|
|
|
235
241
|
exam_id = cls.exams[0].exam_id
|
|
@@ -259,7 +265,7 @@ def test_empty_relationships_return_empty_list() -> None:
|
|
|
259
265
|
assert get_all_exams_for_class(session, cls) == []
|
|
260
266
|
|
|
261
267
|
|
|
262
|
-
@given(pts=
|
|
268
|
+
@given(pts=floats(allow_infinity=False, allow_nan=False)) # Max 32-bit signed int
|
|
263
269
|
def test_exam_score_boundaries(pts: int) -> None:
|
|
264
270
|
"""Tests that the database handles large point values correctly."""
|
|
265
271
|
with temp_db() as session:
|
|
@@ -268,7 +274,7 @@ def test_exam_score_boundaries(pts: int) -> None:
|
|
|
268
274
|
add_class_to_semester(session, "Math", sem)
|
|
269
275
|
cls = sem.classes[0]
|
|
270
276
|
|
|
271
|
-
add_exam_to_class(session, "Big Score", pts, pts, cls)
|
|
277
|
+
add_exam_to_class(session, "Big Score", pts, pts, "notes", cls)
|
|
272
278
|
session.commit()
|
|
273
279
|
|
|
274
280
|
exam = get_all_exams_for_class(session, cls)[0]
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from textual import on
|
|
4
|
-
from textual.app import ComposeResult, Screen
|
|
5
|
-
from textual.containers import Vertical
|
|
6
|
-
from textual.widgets import Footer, Header, Input, Label
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class SingleInputScreen(Screen):
|
|
10
|
-
BINDINGS = [
|
|
11
|
-
("escape", "app.pop_screen", "Cancel"),
|
|
12
|
-
]
|
|
13
|
-
input_button: Input = Input()
|
|
14
|
-
label_text: str = ""
|
|
15
|
-
|
|
16
|
-
def __init__(self) -> None:
|
|
17
|
-
super().__init__()
|
|
18
|
-
self.db_session = self.app.db_session # type: ignore
|
|
19
|
-
|
|
20
|
-
def compose(self) -> ComposeResult:
|
|
21
|
-
yield Header()
|
|
22
|
-
with Vertical():
|
|
23
|
-
yield Label(self.label_text)
|
|
24
|
-
yield self.input_button
|
|
25
|
-
yield Footer()
|
|
26
|
-
|
|
27
|
-
@on(Input.Submitted)
|
|
28
|
-
def input_submitted(self, event: Input.Submitted) -> None:
|
|
29
|
-
self.submit() # type: ignore
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class TrippleInputScreen(Screen):
|
|
33
|
-
BINDINGS = [
|
|
34
|
-
("escape", "app.pop_screen", "Cancel"),
|
|
35
|
-
]
|
|
36
|
-
input_button1: Input = Input()
|
|
37
|
-
input_button2: Input = Input()
|
|
38
|
-
input_button3: Input = Input()
|
|
39
|
-
label_text: str = ""
|
|
40
|
-
|
|
41
|
-
def __init__(self) -> None:
|
|
42
|
-
super().__init__()
|
|
43
|
-
self.db_session = self.app.db_session # type: ignore
|
|
44
|
-
|
|
45
|
-
def compose(self) -> ComposeResult:
|
|
46
|
-
yield Header()
|
|
47
|
-
with Vertical():
|
|
48
|
-
yield Label(self.label_text)
|
|
49
|
-
yield self.input_button1
|
|
50
|
-
yield self.input_button2
|
|
51
|
-
yield self.input_button3
|
|
52
|
-
yield Footer()
|
|
53
|
-
|
|
54
|
-
@on(Input.Submitted)
|
|
55
|
-
def input_submitted(self, event: Input.Submitted) -> None:
|
|
56
|
-
self.submit() # type: ignore
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class DoubleInputScreen(Screen):
|
|
60
|
-
BINDINGS = [
|
|
61
|
-
("escape", "app.pop_screen", "Cancel"),
|
|
62
|
-
]
|
|
63
|
-
input_button1: Input = Input()
|
|
64
|
-
input_button2: Input = Input()
|
|
65
|
-
label_text: str = ""
|
|
66
|
-
|
|
67
|
-
def __init__(self) -> None:
|
|
68
|
-
super().__init__()
|
|
69
|
-
self.db_session = self.app.db_session # type: ignore
|
|
70
|
-
|
|
71
|
-
def compose(self) -> ComposeResult:
|
|
72
|
-
yield Header()
|
|
73
|
-
with Vertical():
|
|
74
|
-
yield Label(self.label_text)
|
|
75
|
-
yield self.input_button1
|
|
76
|
-
yield self.input_button2
|
|
77
|
-
yield Footer()
|
|
78
|
-
|
|
79
|
-
@on(Input.Submitted)
|
|
80
|
-
def input_submitted(self, event: Input.Submitted) -> None:
|
|
81
|
-
self.submit() # type: ignore
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|