Examtracker 1.2.1__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.1/src/Examtracker.egg-info → examtracker-1.2.3}/PKG-INFO +17 -3
- {examtracker-1.2.1 → examtracker-1.2.3}/README.md +12 -1
- {examtracker-1.2.1 → examtracker-1.2.3}/pyproject.toml +2 -2
- {examtracker-1.2.1 → examtracker-1.2.3}/requirements.txt +3 -0
- {examtracker-1.2.1 → examtracker-1.2.3/src/Examtracker.egg-info}/PKG-INFO +17 -3
- {examtracker-1.2.1 → examtracker-1.2.3}/src/Examtracker.egg-info/SOURCES.txt +2 -1
- {examtracker-1.2.1 → examtracker-1.2.3}/src/Examtracker.egg-info/requires.txt +3 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/app.py +4 -5
- examtracker-1.2.3/src/examtracker/data/style.css +44 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/database.py +32 -21
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/database_scheme.py +8 -8
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/main.py +1 -1
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/screens/classscreen.py +45 -17
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/screens/examscreen.py +68 -32
- examtracker-1.2.3/src/examtracker/screens/inputscreens.py +32 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/screens/semesterscreen.py +15 -12
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/settings.py +7 -6
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/textual_utils/vimtable.py +0 -1
- examtracker-1.2.3/test/test_database.py +281 -0
- examtracker-1.2.1/src/examtracker/data/style.css +0 -30
- examtracker-1.2.1/src/examtracker/screens/inputscreens.py +0 -56
- {examtracker-1.2.1 → examtracker-1.2.3}/LICENSE +0 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/setup.cfg +0 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/Examtracker.egg-info/dependency_links.txt +0 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/Examtracker.egg-info/entry_points.txt +0 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/Examtracker.egg-info/top_level.txt +0 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/__init__.py +0 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/__main__.py +0 -0
- {examtracker-1.2.1 → examtracker-1.2.3}/src/examtracker/py.typed +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
|
|
@@ -25,7 +25,7 @@ License: MIT License
|
|
|
25
25
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
|
|
28
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
License-File: LICENSE
|
|
31
31
|
Requires-Dist: textual
|
|
@@ -35,8 +35,12 @@ Requires-Dist: pydantic
|
|
|
35
35
|
Requires-Dist: pydantic-settings
|
|
36
36
|
Requires-Dist: pathlib
|
|
37
37
|
Requires-Dist: pyyaml
|
|
38
|
+
Requires-Dist: rich
|
|
39
|
+
Requires-Dist: pymysql
|
|
40
|
+
Requires-Dist: cryptography
|
|
38
41
|
Dynamic: license-file
|
|
39
42
|
|
|
43
|
+

|
|
40
44
|
[](https://choosealicense.com/licenses/mit/)
|
|
41
45
|
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
|
42
46
|

|
|
@@ -92,6 +96,7 @@ By default, the program searches for:
|
|
|
92
96
|
|
|
93
97
|
```yaml
|
|
94
98
|
database_path: "~/.config/examtracker/examtracker.db"
|
|
99
|
+
database_uri: "sqllite:///~/.config/examtracker/examtracker.db"
|
|
95
100
|
```
|
|
96
101
|
|
|
97
102
|
</details>
|
|
@@ -132,6 +137,7 @@ Environment variables override values defined in `config.yml`.
|
|
|
132
137
|
- `id`
|
|
133
138
|
- `name`
|
|
134
139
|
- `semester_id`
|
|
140
|
+
- `exam_grade`
|
|
135
141
|
|
|
136
142
|
### `semester`
|
|
137
143
|
- `id`
|
|
@@ -141,10 +147,18 @@ Environment variables override values defined in `config.yml`.
|
|
|
141
147
|
|
|
142
148
|
# TODO
|
|
143
149
|
|
|
150
|
+
- [x] Allow for floating point values for the points scored
|
|
144
151
|
- [x] Handle SQL errors
|
|
145
152
|
- [x] Initialize database
|
|
146
153
|
- [x] Configuration file support
|
|
147
154
|
- [x] Abort edit and add screens
|
|
148
155
|
- [x] Cross-platform config discovery
|
|
149
|
-
- [
|
|
156
|
+
- [x] Improve CSS
|
|
150
157
|
- [x] enable Editing Exams and Semester
|
|
158
|
+
- [x] Feature where you can add the grade you got in the exams
|
|
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
|
|
163
|
+
- [x] write Tests
|
|
164
|
+
- [x] automagicly push new pypi versions and make release upon github tag push tag push
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+

|
|
1
2
|
[](https://choosealicense.com/licenses/mit/)
|
|
2
3
|
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
|
3
4
|

|
|
@@ -53,6 +54,7 @@ By default, the program searches for:
|
|
|
53
54
|
|
|
54
55
|
```yaml
|
|
55
56
|
database_path: "~/.config/examtracker/examtracker.db"
|
|
57
|
+
database_uri: "sqllite:///~/.config/examtracker/examtracker.db"
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
</details>
|
|
@@ -93,6 +95,7 @@ Environment variables override values defined in `config.yml`.
|
|
|
93
95
|
- `id`
|
|
94
96
|
- `name`
|
|
95
97
|
- `semester_id`
|
|
98
|
+
- `exam_grade`
|
|
96
99
|
|
|
97
100
|
### `semester`
|
|
98
101
|
- `id`
|
|
@@ -102,10 +105,18 @@ Environment variables override values defined in `config.yml`.
|
|
|
102
105
|
|
|
103
106
|
# TODO
|
|
104
107
|
|
|
108
|
+
- [x] Allow for floating point values for the points scored
|
|
105
109
|
- [x] Handle SQL errors
|
|
106
110
|
- [x] Initialize database
|
|
107
111
|
- [x] Configuration file support
|
|
108
112
|
- [x] Abort edit and add screens
|
|
109
113
|
- [x] Cross-platform config discovery
|
|
110
|
-
- [
|
|
114
|
+
- [x] Improve CSS
|
|
111
115
|
- [x] enable Editing Exams and Semester
|
|
116
|
+
- [x] Feature where you can add the grade you got in the exams
|
|
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
|
|
121
|
+
- [x] write Tests
|
|
122
|
+
- [x] automagicly push new pypi versions and make release upon github tag push tag push
|
|
@@ -7,12 +7,12 @@ 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"
|
|
14
14
|
|
|
15
|
-
requires-python = ">=3.
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
16
|
license = { file = "LICENSE" }
|
|
17
17
|
|
|
18
18
|
authors = [{ name = "Samuel Huwiler", email = "samuel.huwiler@gmx.ch" }]
|
|
@@ -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
|
|
@@ -25,7 +25,7 @@ License: MIT License
|
|
|
25
25
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
|
|
28
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
License-File: LICENSE
|
|
31
31
|
Requires-Dist: textual
|
|
@@ -35,8 +35,12 @@ Requires-Dist: pydantic
|
|
|
35
35
|
Requires-Dist: pydantic-settings
|
|
36
36
|
Requires-Dist: pathlib
|
|
37
37
|
Requires-Dist: pyyaml
|
|
38
|
+
Requires-Dist: rich
|
|
39
|
+
Requires-Dist: pymysql
|
|
40
|
+
Requires-Dist: cryptography
|
|
38
41
|
Dynamic: license-file
|
|
39
42
|
|
|
43
|
+

|
|
40
44
|
[](https://choosealicense.com/licenses/mit/)
|
|
41
45
|
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
|
42
46
|

|
|
@@ -92,6 +96,7 @@ By default, the program searches for:
|
|
|
92
96
|
|
|
93
97
|
```yaml
|
|
94
98
|
database_path: "~/.config/examtracker/examtracker.db"
|
|
99
|
+
database_uri: "sqllite:///~/.config/examtracker/examtracker.db"
|
|
95
100
|
```
|
|
96
101
|
|
|
97
102
|
</details>
|
|
@@ -132,6 +137,7 @@ Environment variables override values defined in `config.yml`.
|
|
|
132
137
|
- `id`
|
|
133
138
|
- `name`
|
|
134
139
|
- `semester_id`
|
|
140
|
+
- `exam_grade`
|
|
135
141
|
|
|
136
142
|
### `semester`
|
|
137
143
|
- `id`
|
|
@@ -141,10 +147,18 @@ Environment variables override values defined in `config.yml`.
|
|
|
141
147
|
|
|
142
148
|
# TODO
|
|
143
149
|
|
|
150
|
+
- [x] Allow for floating point values for the points scored
|
|
144
151
|
- [x] Handle SQL errors
|
|
145
152
|
- [x] Initialize database
|
|
146
153
|
- [x] Configuration file support
|
|
147
154
|
- [x] Abort edit and add screens
|
|
148
155
|
- [x] Cross-platform config discovery
|
|
149
|
-
- [
|
|
156
|
+
- [x] Improve CSS
|
|
150
157
|
- [x] enable Editing Exams and Semester
|
|
158
|
+
- [x] Feature where you can add the grade you got in the exams
|
|
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
|
|
163
|
+
- [x] write Tests
|
|
164
|
+
- [x] automagicly push new pypi versions and make release upon github tag push tag push
|
|
@@ -21,4 +21,5 @@ src/examtracker/screens/classscreen.py
|
|
|
21
21
|
src/examtracker/screens/examscreen.py
|
|
22
22
|
src/examtracker/screens/inputscreens.py
|
|
23
23
|
src/examtracker/screens/semesterscreen.py
|
|
24
|
-
src/examtracker/textual_utils/vimtable.py
|
|
24
|
+
src/examtracker/textual_utils/vimtable.py
|
|
25
|
+
test/test_database.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from sqlalchemy import Engine # type:ignore
|
|
2
2
|
from sqlalchemy.orm import Session
|
|
3
|
-
from textual.app import App
|
|
3
|
+
from textual.app import App
|
|
4
4
|
|
|
5
5
|
from examtracker.screens.semesterscreen import SemesterScreen
|
|
6
6
|
from examtracker.settings import Settings
|
|
@@ -11,12 +11,11 @@ class ExamTracker(App):
|
|
|
11
11
|
("q", "quit", "Quit"),
|
|
12
12
|
("ctrl+c", "quit"),
|
|
13
13
|
]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def compose(self) -> ComposeResult:
|
|
17
|
-
yield SemesterScreen()
|
|
14
|
+
ENABLE_COMMAND_PALETTE = False
|
|
15
|
+
# CSS_PATH = "/Users/samuel/Repositories/Examtracker/src/examtracker/data/style.css"
|
|
18
16
|
|
|
19
17
|
def on_mount(self) -> None:
|
|
18
|
+
self.theme = "gruvbox"
|
|
20
19
|
self.push_screen(SemesterScreen())
|
|
21
20
|
self.stylesheet.read(self.config.css_path)
|
|
22
21
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
DataTable {
|
|
2
|
+
border-title-align: center;
|
|
3
|
+
border-title-style: bold;
|
|
4
|
+
border-title-color: $foreground;
|
|
5
|
+
margin: 3;
|
|
6
|
+
padding: 2;
|
|
7
|
+
width: 75%;
|
|
8
|
+
border: white;
|
|
9
|
+
text-style: bold;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DataTable .datatable--header {
|
|
14
|
+
text-style: bold;
|
|
15
|
+
background: $primary;
|
|
16
|
+
color: black;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
DataTable .datatable--cursor {
|
|
20
|
+
background: $accent 60%;
|
|
21
|
+
text-style: italic underline;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Screen {
|
|
25
|
+
position: relative;
|
|
26
|
+
align: center top;
|
|
27
|
+
margin: 3;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Vertical {
|
|
31
|
+
margin: 5;
|
|
32
|
+
align: center top;
|
|
33
|
+
width: 75%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Input {
|
|
37
|
+
width: 75%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Label {
|
|
42
|
+
width: 75%;
|
|
43
|
+
padding: 1;
|
|
44
|
+
}
|
|
@@ -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:
|
|
@@ -43,37 +42,48 @@ def get_exam_by_id(session: Session, exam_id: int) -> Exam:
|
|
|
43
42
|
|
|
44
43
|
|
|
45
44
|
def add_class_to_semester(
|
|
46
|
-
session: Session,
|
|
45
|
+
session: Session,
|
|
46
|
+
class_name: str,
|
|
47
|
+
semster_obj: Semester,
|
|
48
|
+
exam_grade: Optional[float] = None,
|
|
47
49
|
) -> None:
|
|
48
|
-
class_obj = Class(
|
|
50
|
+
class_obj = Class(
|
|
51
|
+
name=class_name, semester_id=semster_obj.semester_id, exam_grade=exam_grade
|
|
52
|
+
)
|
|
49
53
|
session.add(class_obj)
|
|
50
54
|
session.flush()
|
|
51
55
|
|
|
52
56
|
|
|
53
57
|
def add_exam_to_class(
|
|
54
|
-
session: Session,
|
|
58
|
+
session: Session,
|
|
59
|
+
name: str,
|
|
60
|
+
max_points: float,
|
|
61
|
+
scored_points: float,
|
|
62
|
+
notes: str,
|
|
63
|
+
class_obj: Class,
|
|
55
64
|
) -> None:
|
|
56
65
|
exam_obj = Exam(
|
|
57
66
|
name=name,
|
|
58
67
|
max_points=max_points,
|
|
59
68
|
scored_points=scored_points,
|
|
69
|
+
notes=notes,
|
|
60
70
|
class_id=class_obj.class_id,
|
|
61
71
|
)
|
|
62
72
|
session.add(exam_obj)
|
|
63
73
|
session.flush()
|
|
64
74
|
|
|
65
75
|
|
|
66
|
-
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]:
|
|
67
77
|
return session.query(Exam).filter_by(class_id=class_obj.class_id).all()
|
|
68
78
|
|
|
69
79
|
|
|
70
|
-
def get_all_semester(session: Session) ->
|
|
71
|
-
return session.query(Semester).all()
|
|
80
|
+
def get_all_semester(session: Session) -> list[Semester]:
|
|
81
|
+
return session.query(Semester).all()[::-1]
|
|
72
82
|
|
|
73
83
|
|
|
74
84
|
def get_all_classes_for_semester(
|
|
75
85
|
session: Session, semster_obj: Semester
|
|
76
|
-
) ->
|
|
86
|
+
) -> list[Class]:
|
|
77
87
|
return session.query(Class).filter_by(semester_id=semster_obj.semester_id).all()
|
|
78
88
|
|
|
79
89
|
|
|
@@ -93,15 +103,16 @@ def remove_exam_by_id(session: Session, exam_id: int) -> None:
|
|
|
93
103
|
|
|
94
104
|
|
|
95
105
|
def main() -> int:
|
|
96
|
-
engine: Engine = create_database_engine("test.db")
|
|
97
|
-
|
|
106
|
+
engine: Engine = create_database_engine("sqlite:///test.db")
|
|
107
|
+
create_tables(engine)
|
|
98
108
|
session = Session(engine)
|
|
99
|
-
|
|
109
|
+
add_semester(session, "FS25")
|
|
100
110
|
# # remove_semester_by_name(session, "FS25")
|
|
101
111
|
# # print(get_all_semester(session))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
sem = get_semester_by_name(session, "FS25")
|
|
113
|
+
add_class_to_semester(session, "wEprog", sem)
|
|
114
|
+
cls = get_all_classes_for_semester(session, sem)[0]
|
|
115
|
+
print(cls.exam_grade)
|
|
105
116
|
# # print(get_all_exams_for_class(session, cls))
|
|
106
117
|
session.commit()
|
|
107
118
|
return 0
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
Defining the database tables and entries
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import List
|
|
6
|
-
|
|
7
5
|
from sqlalchemy import ForeignKey, String
|
|
8
|
-
from sqlalchemy.orm import (
|
|
6
|
+
from sqlalchemy.orm import ( # type: ignore
|
|
9
7
|
DeclarativeBase,
|
|
10
|
-
Mapped,
|
|
8
|
+
Mapped,
|
|
11
9
|
mapped_column,
|
|
12
10
|
relationship,
|
|
13
11
|
)
|
|
@@ -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,10 +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))
|
|
36
|
+
exam_grade: Mapped[float | None]
|
|
37
37
|
semester_id: Mapped[int] = mapped_column(ForeignKey("semester.semester_id"))
|
|
38
38
|
semester: Mapped["Semester"] = relationship(back_populates="classes") # type:ignore
|
|
39
39
|
|
|
40
|
-
exams: Mapped[
|
|
40
|
+
exams: Mapped[list["Exam"]] = relationship(
|
|
41
41
|
back_populates="class_",
|
|
42
42
|
cascade="all, delete-orphan",
|
|
43
43
|
) # type: ignore
|
|
@@ -50,7 +50,7 @@ class Semester(Base):
|
|
|
50
50
|
__tablename__ = "semester"
|
|
51
51
|
semester_id: Mapped[int] = mapped_column(primary_key=True)
|
|
52
52
|
name: Mapped[str] = mapped_column(String(30), unique=True)
|
|
53
|
-
classes: Mapped[
|
|
53
|
+
classes: Mapped[list["Class"]] = relationship(
|
|
54
54
|
back_populates="semester",
|
|
55
55
|
cascade="all, delete-orphan",
|
|
56
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,57 +15,75 @@ 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(
|
|
21
|
-
|
|
22
|
+
class AddClassScreen(MultiInputScreen):
|
|
22
23
|
def __init__(self, semester_name: str, **kwargs) -> None:
|
|
23
|
-
super().__init__(**kwargs)
|
|
24
|
+
super().__init__(2, **kwargs)
|
|
24
25
|
self.semester_name = semester_name
|
|
25
26
|
self.label_text = f"Add class to: {self.semester_name}"
|
|
26
27
|
|
|
27
28
|
def on_mount(self) -> None:
|
|
28
|
-
self.
|
|
29
|
-
self.
|
|
30
|
-
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()
|
|
31
34
|
|
|
32
35
|
# Submit on Enter from any Input
|
|
33
36
|
def submit(self) -> None:
|
|
34
|
-
name = self.
|
|
37
|
+
name = self.buttons[0].value.strip()
|
|
38
|
+
exam_score = self.buttons[1].value.strip()
|
|
39
|
+
try:
|
|
40
|
+
score = float(exam_score)
|
|
41
|
+
except ValueError:
|
|
42
|
+
score = None
|
|
35
43
|
if not name:
|
|
36
44
|
return # Require class name
|
|
37
45
|
semester = get_semester_by_name(self.db_session, self.semester_name)
|
|
38
46
|
# Add the class
|
|
39
|
-
add_class_to_semester(self.db_session, name, semester) # type: ignore
|
|
47
|
+
add_class_to_semester(self.db_session, name, semester, score) # type: ignore
|
|
40
48
|
self.db_session.commit()
|
|
41
49
|
|
|
42
50
|
# Pop the screen and return
|
|
43
51
|
self.app.pop_screen()
|
|
44
52
|
|
|
45
53
|
|
|
46
|
-
class EditClassScreen(
|
|
47
|
-
|
|
54
|
+
class EditClassScreen(MultiInputScreen):
|
|
48
55
|
def __init__(self, class_id: int, **kwargs) -> None:
|
|
49
|
-
super().__init__(**kwargs)
|
|
56
|
+
super().__init__(2, **kwargs)
|
|
50
57
|
self.class_id = class_id
|
|
51
58
|
self.label_text = "Edit class"
|
|
52
59
|
|
|
53
60
|
def on_mount(self) -> None:
|
|
54
61
|
class_obj = get_class_by_id(self.db_session, self.class_id)
|
|
55
|
-
self.
|
|
56
|
-
|
|
62
|
+
self.buttons[0].value = class_obj.name
|
|
63
|
+
if class_obj.exam_grade is not None:
|
|
64
|
+
self.buttons[1].value = str(class_obj.exam_grade)
|
|
65
|
+
else:
|
|
66
|
+
self.buttons[1].value = ""
|
|
67
|
+
self.buttons[0].focus()
|
|
57
68
|
|
|
58
69
|
# Submit on Enter from any Input
|
|
59
70
|
def submit(self) -> None:
|
|
60
|
-
name = self.
|
|
71
|
+
name = self.buttons[0].value.strip()
|
|
72
|
+
exam_score = self.buttons[1].value.strip()
|
|
73
|
+
try:
|
|
74
|
+
score = float(exam_score)
|
|
75
|
+
except ValueError:
|
|
76
|
+
score = None
|
|
77
|
+
|
|
78
|
+
if isinstance(score, float) and not math.isfinite(score):
|
|
79
|
+
score = None
|
|
61
80
|
if not name:
|
|
62
81
|
return # Require class name
|
|
63
82
|
|
|
64
83
|
class_obj = get_class_by_id(self.db_session, self.class_id)
|
|
65
84
|
# Add the class
|
|
66
85
|
class_obj.name = name
|
|
86
|
+
class_obj.exam_grade = score
|
|
67
87
|
self.db_session.commit()
|
|
68
88
|
|
|
69
89
|
# Pop the screen and return
|
|
@@ -86,11 +106,12 @@ class ClassScreen(Screen):
|
|
|
86
106
|
super().__init__()
|
|
87
107
|
self.db_session: Session = self.app.db_session # type: ignore
|
|
88
108
|
self.semester_name = semester_name
|
|
109
|
+
self.latest_row = 0
|
|
89
110
|
|
|
90
111
|
def compose(self) -> ComposeResult:
|
|
91
112
|
yield Header()
|
|
92
113
|
self.class_table: VimTable = VimTable()
|
|
93
|
-
self.class_table.add_columns("ID", "Name")
|
|
114
|
+
self.class_table.add_columns("ID", "Name", "Exam grade")
|
|
94
115
|
self.class_table.cursor_type = "row"
|
|
95
116
|
self.class_table.border_title = f"classes for: {self.semester_name}"
|
|
96
117
|
yield self.class_table
|
|
@@ -129,7 +150,13 @@ class ClassScreen(Screen):
|
|
|
129
150
|
|
|
130
151
|
self.class_table.clear()
|
|
131
152
|
for cls in get_all_classes_for_semester(self.db_session, semester):
|
|
132
|
-
|
|
153
|
+
score = cls.exam_grade
|
|
154
|
+
str_score = ""
|
|
155
|
+
if score is not None:
|
|
156
|
+
str_score = str(cls.exam_grade)
|
|
157
|
+
self.class_table.add_row(cls.class_id, cls.name, str_score)
|
|
158
|
+
|
|
159
|
+
self.class_table.move_cursor(row=self.latest_row, column=0)
|
|
133
160
|
|
|
134
161
|
def on_screen_resume(self) -> None:
|
|
135
162
|
self.refresh_table()
|
|
@@ -139,6 +166,7 @@ class ClassScreen(Screen):
|
|
|
139
166
|
row_index = self.class_table.cursor_row
|
|
140
167
|
if row_index is None:
|
|
141
168
|
return
|
|
169
|
+
self.latest_row = row_index
|
|
142
170
|
|
|
143
171
|
class_id = self.class_table.get_row_at(row_index)[0]
|
|
144
172
|
self.app.push_screen(ExamScreen(class_id))
|