├── tests ├── unit │ ├── __init__.py │ ├── student │ │ ├── __init__.py │ │ ├── mock │ │ │ ├── __init__.py │ │ │ └── student_gateway.py │ │ └── conftest.py │ ├── subject │ │ ├── __init__.py │ │ └── conftest.py │ ├── teacher │ │ ├── __init__.py │ │ ├── mock │ │ │ ├── __init__.py │ │ │ └── teacher_gateway.py │ │ ├── conftest.py │ │ └── test_teacher.py │ └── conftest.py ├── common │ ├── __init__.py │ └── mock │ │ ├── __init__.py │ │ └── transaction_manager.py └── gateway │ ├── __init__.py │ ├── lesson │ ├── __init__.py │ └── conftest.py │ ├── student │ ├── __init__.py │ ├── conftest.py │ └── test_student_gateway.py │ ├── subject │ ├── __init__.py │ ├── conftest.py │ └── test_subject_gateway.py │ ├── teacher │ ├── __init__.py │ ├── conftest.py │ └── test_teacher_gateway.py │ ├── home_task │ ├── __init__.py │ ├── conftest.py │ └── test_home_task_gateway.py │ └── conftest.py ├── src └── student_journal │ ├── py.typed │ ├── __init__.py │ ├── domain │ ├── __init__.py │ ├── value_object │ │ ├── __init__.py │ │ ├── lesson_id.py │ │ ├── student_id.py │ │ ├── subject_id.py │ │ ├── task_id.py │ │ └── teacher_id.py │ ├── teacher.py │ ├── home_task.py │ ├── subject.py │ ├── lesson.py │ └── student.py │ ├── adapters │ ├── __init__.py │ ├── db │ │ ├── __init__.py │ │ ├── gateway │ │ │ ├── __init__.py │ │ │ ├── student_gateway.py │ │ │ ├── teacher_gateway.py │ │ │ ├── subject_gateway.py │ │ │ └── home_task_gateway.py │ │ ├── schema │ │ │ ├── __init__.py │ │ │ ├── load_schema.py │ │ │ └── schema.sql │ │ ├── connection_maker.py │ │ ├── transaction_manager.py │ │ └── connection_factory.py │ ├── exceptions │ │ ├── __init__.py │ │ └── ui │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── student.py │ │ │ ├── hometask.py │ │ │ ├── teacher.py │ │ │ ├── schedule.py │ │ │ ├── lesson.py │ │ │ └── subject.py │ ├── converter │ │ ├── subject.py │ │ ├── teacher.py │ │ ├── student.py │ │ ├── home_task.py │ │ ├── __init__.py │ │ └── lesson.py │ ├── config.py │ └── id_provider.py │ ├── application │ ├── __init__.py │ ├── common │ │ ├── __init__.py │ │ ├── id_provider.py │ │ ├── transaction_manager.py │ │ ├── student_gateway.py │ │ ├── teacher_gateway.py │ │ ├── home_task_gateway.py │ │ ├── subject_gateway.py │ │ └── lesson_gateway.py │ ├── hometask │ │ ├── __init__.py │ │ ├── read_home_task.py │ │ ├── read_home_tasks.py │ │ ├── delete_home_task.py │ │ ├── update_home_task.py │ │ └── create_home_task.py │ ├── lesson │ │ ├── __init__.py │ │ ├── delete_all_lessons.py │ │ ├── delete_lesson.py │ │ ├── read_lesson.py │ │ ├── read_first_lessons_of_weeks.py │ │ ├── read_lessons_for_week.py │ │ ├── delete_lessons_for_week.py │ │ ├── update_lesson.py │ │ └── create_lesson.py │ ├── models │ │ ├── __init__.py │ │ ├── teacher.py │ │ ├── subject.py │ │ ├── lesson.py │ │ ├── student.py │ │ └── home_task.py │ ├── student │ │ ├── __init__.py │ │ ├── read_student.py │ │ ├── read_current_student.py │ │ ├── update_student.py │ │ └── create_student.py │ ├── subject │ │ ├── __init__.py │ │ ├── read_subject.py │ │ ├── delete_subject.py │ │ ├── read_subjects.py │ │ ├── update_subject.py │ │ └── create_subject.py │ ├── converters │ │ ├── __init__.py │ │ └── student.py │ ├── exceptions │ │ ├── __init__.py │ │ ├── base.py │ │ ├── home_task.py │ │ ├── subject.py │ │ ├── teacher.py │ │ ├── lesson.py │ │ └── student.py │ ├── invariants │ │ ├── __init__.py │ │ ├── subject.py │ │ ├── teacher.py │ │ ├── home_task.py │ │ ├── lesson.py │ │ └── student.py │ └── teacher │ │ ├── __init__.py │ │ ├── read_teachers.py │ │ ├── read_teacher.py │ │ ├── delete_teacher.py │ │ ├── update_teacher.py │ │ └── create_teacher.py │ ├── bootstrap │ ├── __init__.py │ ├── di │ │ ├── __init__.py │ │ ├── config_provider.py │ │ ├── adapter_provider.py │ │ ├── container.py │ │ ├── gateway_provider.py │ │ ├── db_provider.py │ │ └── command_provider.py │ ├── entrypoint │ │ ├── __init__.py │ │ └── qt.py │ └── cli.py │ └── presentation │ ├── __init__.py │ ├── ui │ ├── __init__.py │ ├── raw │ │ └── __init__.py │ ├── register.py │ ├── edit_student.py │ ├── edit_lesson.py │ ├── subject_list_ui.py │ ├── teacher_list_ui.py │ ├── hometask_list_ui.py │ ├── edit_teacher_ui.py │ ├── edit_hometask_ui.py │ ├── edit_subject_ui.py │ └── about_ui.py │ ├── resource │ ├── __init__.py │ ├── favicon.ico │ └── styles.qss │ └── widget │ ├── __init__.py │ ├── help │ ├── __init__.py │ └── about.py │ ├── lesson │ └── __init__.py │ ├── utils │ ├── __init__.py │ └── month_year_picker.py │ ├── hometask │ └── __init__.py │ ├── student │ ├── __init__.py │ └── edit_student.py │ ├── subject │ ├── __init__.py │ ├── subject_list.py │ └── progress.py │ ├── teacher │ ├── __init__.py │ ├── teacher_list.py │ └── edit_teacher.py │ └── main_window.py ├── Makefile ├── docs ├── subject_list.md ├── teacher_list.md ├── hometask_list.md ├── edit_teacher.md ├── edit_subject.md ├── register.md ├── edit_hometask.md └── edit_lesson.md ├── .pre-commit-config.yaml ├── .github └── workflows │ └── lint.yml ├── LICENSE ├── student-journal.spec ├── resources └── forms │ ├── subject_list.ui │ ├── teacher_list.ui │ ├── hometask_list.ui │ ├── edit_teacher.ui │ ├── edit_hometask.ui │ ├── edit_subject.ui │ ├── progress.ui │ └── schedule.ui ├── pyproject.toml └── .gitignore /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/gateway/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/common/mock/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/gateway/lesson/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/gateway/student/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/gateway/subject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/gateway/teacher/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/student/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/subject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/teacher/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/student_journal/domain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/gateway/home_task/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/teacher/mock/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/di/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | pyinstaller student-journal.spec -------------------------------------------------------------------------------- /src/student_journal/adapters/db/gateway/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/schema/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/hometask/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/student/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/subject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/entrypoint/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/domain/value_object/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/resource/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/raw/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/converters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/invariants/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/help/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/lesson/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/hometask/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/student/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/subject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/teacher/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/student_journal/application/exceptions/base.py: -------------------------------------------------------------------------------- 1 | class ApplicationError(Exception): ... 2 | -------------------------------------------------------------------------------- /src/student_journal/domain/value_object/lesson_id.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | from uuid import UUID 3 | 4 | LessonId = NewType("LessonId", UUID) 5 | -------------------------------------------------------------------------------- /src/student_journal/domain/value_object/student_id.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | from uuid import UUID 3 | 4 | StudentId = NewType("StudentId", UUID) 5 | -------------------------------------------------------------------------------- /src/student_journal/domain/value_object/subject_id.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | from uuid import UUID 3 | 4 | SubjectId = NewType("SubjectId", UUID) 5 | -------------------------------------------------------------------------------- /src/student_journal/domain/value_object/task_id.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | from uuid import UUID 3 | 4 | HomeTaskId = NewType("HomeTaskId", UUID) 5 | -------------------------------------------------------------------------------- /src/student_journal/domain/value_object/teacher_id.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | from uuid import UUID 3 | 4 | TeacherId = NewType("TeacherId", UUID) 5 | -------------------------------------------------------------------------------- /src/student_journal/presentation/resource/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubaskinc0de/student_journal/HEAD/src/student_journal/presentation/resource/favicon.ico -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/ui/base.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.exceptions.base import ApplicationError 2 | 3 | 4 | class UIError(ApplicationError): ... 5 | -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/ui/student.py: -------------------------------------------------------------------------------- 1 | from student_journal.adapters.exceptions.ui.base import UIError 2 | 3 | 4 | class NameNotSpecifiedError(UIError): ... 5 | -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/ui/hometask.py: -------------------------------------------------------------------------------- 1 | from student_journal.adapters.exceptions.ui.base import UIError 2 | 3 | 4 | class DescriptionNotSpecifiedError(UIError): ... 5 | -------------------------------------------------------------------------------- /src/student_journal/application/exceptions/home_task.py: -------------------------------------------------------------------------------- 1 | from .base import ApplicationError 2 | 3 | 4 | class HomeTaskDescriptionError(ApplicationError): ... 5 | 6 | 7 | class HomeTaskNotFoundError(ApplicationError): ... 8 | -------------------------------------------------------------------------------- /docs/subject_list.md: -------------------------------------------------------------------------------- 1 | ### Документация к виджету SubjectList 2 | 3 | Просмотр списка предметов 4 | 5 | Элементы: 6 | - ``list_subject`` - список предметов (QListWidget) 7 | - ``add_more`` - кнопка добавления нового предмета (QPushButton) -------------------------------------------------------------------------------- /docs/teacher_list.md: -------------------------------------------------------------------------------- 1 | ### Документация к виджету TeacherList 2 | 3 | Просмотр списка учителей 4 | 5 | Элементы: 6 | - ``list_teacher`` - список учителей (QListWidget) 7 | - ``add_more`` - кнопка добавления нового учителя (QPushButton) -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/ui/teacher.py: -------------------------------------------------------------------------------- 1 | from student_journal.adapters.exceptions.ui.base import UIError 2 | 3 | 4 | class FullNameNotSpecifiedError(UIError): ... 5 | 6 | 7 | class TeacherIsNotSpecifiedError(UIError): ... 8 | -------------------------------------------------------------------------------- /docs/hometask_list.md: -------------------------------------------------------------------------------- 1 | ### Документация к виджету HometaskList 2 | 3 | Просмотр списка ДЗ 4 | 5 | Элементы: 6 | - ``list_hometask`` - список ДЗ (QListWidget) 7 | - ``show_done`` - фильтр показать ли выполненные (QCheckBox) 8 | - ``add_more`` - кнопка добавления нового задания (QPushButton) -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/ui/schedule.py: -------------------------------------------------------------------------------- 1 | from student_journal.adapters.exceptions.ui.base import UIError 2 | 3 | 4 | class WeekStartUnsetError(UIError): ... 5 | 6 | 7 | class WeekPeriodUnsetError(UIError): ... 8 | 9 | 10 | class CannotCopyScheduleError(UIError): ... 11 | -------------------------------------------------------------------------------- /src/student_journal/application/exceptions/subject.py: -------------------------------------------------------------------------------- 1 | from .base import ApplicationError 2 | 3 | 4 | class SubjectTitleError(ApplicationError): ... 5 | 6 | 7 | class SubjectAlreadyExistsError(SubjectTitleError): ... 8 | 9 | 10 | class SubjectNotFoundError(SubjectTitleError): ... 11 | -------------------------------------------------------------------------------- /src/student_journal/application/exceptions/teacher.py: -------------------------------------------------------------------------------- 1 | from .base import ApplicationError 2 | 3 | 4 | class TeacherFullNameError(ApplicationError): ... 5 | 6 | 7 | class TeacherNotFoundError(ApplicationError): ... 8 | 9 | 10 | class TeacherAlreadyExistsError(ApplicationError): ... 11 | -------------------------------------------------------------------------------- /src/student_journal/domain/teacher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.domain.value_object.teacher_id import TeacherId 4 | 5 | 6 | @dataclass(slots=True) 7 | class Teacher: 8 | teacher_id: TeacherId 9 | full_name: str 10 | avatar: str | None 11 | -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/ui/lesson.py: -------------------------------------------------------------------------------- 1 | from student_journal.adapters.exceptions.ui.base import UIError 2 | 3 | 4 | class LessonIsNotSpecifiedError(UIError): ... 5 | 6 | 7 | class LessonAtIsNotSpecifiedError(UIError): ... 8 | 9 | 10 | class SubjectIsNotSelectedError(UIError): ... 11 | -------------------------------------------------------------------------------- /src/student_journal/adapters/exceptions/ui/subject.py: -------------------------------------------------------------------------------- 1 | from student_journal.adapters.exceptions.ui.base import UIError 2 | 3 | 4 | class TeacherIsNotSelectedError(UIError): ... 5 | 6 | 7 | class TitleIsNotSpecifiedError(UIError): ... 8 | 9 | 10 | class SubjectIsNotSpecifiedError(UIError): ... 11 | -------------------------------------------------------------------------------- /tests/gateway/lesson/conftest.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | 5 | from student_journal.adapters.db.gateway.lesson_gateway import SQLiteLessonGateway 6 | 7 | 8 | @pytest.fixture 9 | def lesson_gateway(cursor: Cursor) -> SQLiteLessonGateway: 10 | return SQLiteLessonGateway(cursor) 11 | -------------------------------------------------------------------------------- /tests/gateway/student/conftest.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | 5 | from student_journal.adapters.db.gateway.student_gateway import SQLiteStudentGateway 6 | 7 | 8 | @pytest.fixture 9 | def student_gateway(cursor: Cursor) -> SQLiteStudentGateway: 10 | return SQLiteStudentGateway(cursor) 11 | -------------------------------------------------------------------------------- /tests/gateway/teacher/conftest.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | 5 | from student_journal.adapters.db.gateway.teacher_gateway import SQLiteTeacherGateway 6 | 7 | 8 | @pytest.fixture 9 | def teacher_gateway(cursor: Cursor) -> SQLiteTeacherGateway: 10 | return SQLiteTeacherGateway(cursor) 11 | -------------------------------------------------------------------------------- /tests/gateway/home_task/conftest.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | 5 | from student_journal.adapters.db.gateway.home_task_gateway import SQLiteHomeTaskGateway 6 | 7 | 8 | @pytest.fixture 9 | def home_task_gateway(cursor: Cursor) -> SQLiteHomeTaskGateway: 10 | return SQLiteHomeTaskGateway(cursor) 11 | -------------------------------------------------------------------------------- /docs/edit_teacher.md: -------------------------------------------------------------------------------- 1 | ### Документация к виджету EditTeacher 2 | 3 | Виджет редактирования и создания учителя 4 | 5 | Элементы: 6 | - ``main_label`` - заголовок формы (QLabel) 7 | - ``full_name_input`` - ввод имени учителя (QLineEdit) 8 | - ``submit_btn`` - кнопка отправки формы (QPushButton) 9 | - ``delete_btn`` - удалить обьект (QPushButton) -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/help/about.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import QWidget 2 | 3 | from student_journal.presentation.ui.about_ui import Ui_About 4 | 5 | 6 | class About(QWidget): 7 | def __init__(self) -> None: 8 | super().__init__() 9 | self.ui = Ui_About() 10 | self.ui.setupUi(self) 11 | -------------------------------------------------------------------------------- /src/student_journal/adapters/converter/subject.py: -------------------------------------------------------------------------------- 1 | from adaptix import Retort, name_mapping 2 | 3 | from student_journal.domain.subject import Subject 4 | 5 | subject_retort = Retort() 6 | subject_to_list_retort = Retort( 7 | recipe=[ 8 | name_mapping( 9 | Subject, 10 | as_list=True, 11 | ), 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /src/student_journal/adapters/converter/teacher.py: -------------------------------------------------------------------------------- 1 | from adaptix import Retort, name_mapping 2 | 3 | from student_journal.domain.teacher import Teacher 4 | 5 | teacher_retort = Retort() 6 | teacher_to_list_retort = Retort( 7 | recipe=[ 8 | name_mapping( 9 | Teacher, 10 | as_list=True, 11 | ), 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /src/student_journal/application/models/teacher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.domain.teacher import Teacher 4 | from student_journal.domain.value_object.student_id import StudentId 5 | 6 | 7 | @dataclass(slots=True, frozen=True) 8 | class TeachersReadModel: 9 | student_id: StudentId 10 | teachers: list[Teacher] 11 | -------------------------------------------------------------------------------- /src/student_journal/application/invariants/subject.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.exceptions.subject import SubjectTitleError 2 | 3 | TITLE_MAX_LENGTH = 255 4 | TITLE_MIN_LENGTH = 1 5 | 6 | 7 | def validate_subject_invariants(title: str) -> None: 8 | if (len(title) > TITLE_MAX_LENGTH) or (len(title) < TITLE_MIN_LENGTH): 9 | raise SubjectTitleError 10 | -------------------------------------------------------------------------------- /tests/unit/student/mock/__init__.py: -------------------------------------------------------------------------------- 1 | from common.mock.transaction_manager import MockedTransactionManager 2 | 3 | from unit.teacher.mock.teacher_gateway import MockedTeacherGateway 4 | 5 | from .student_gateway import MockedStudentGateway 6 | 7 | __all__ = [ 8 | "MockedStudentGateway", 9 | "MockedTeacherGateway", 10 | "MockedTransactionManager", 11 | ] 12 | -------------------------------------------------------------------------------- /src/student_journal/application/common/id_provider.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Protocol 3 | 4 | from student_journal.domain.value_object.student_id import StudentId 5 | 6 | 7 | class IdProvider(Protocol): 8 | @abstractmethod 9 | def get_id(self) -> StudentId: ... 10 | 11 | @abstractmethod 12 | def ensure_authenticated(self) -> None: ... 13 | -------------------------------------------------------------------------------- /docs/edit_subject.md: -------------------------------------------------------------------------------- 1 | ### Документация к виджету EditSubject 2 | 3 | Виджет редактирования и создания предмета 4 | 5 | Элементы: 6 | - ``main_label`` - заголовок формы (QLabel) 7 | - ``title_input`` - ввод названия предмета (QLineEdit) 8 | - ``teacher_combo`` - выбор учителя (QComboBox) 9 | - ``submit_btn`` - кнопка отправки формы (QPushButton) 10 | - ``delete_btn`` - удалить обьект (QPushButton) -------------------------------------------------------------------------------- /src/student_journal/domain/home_task.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.domain.value_object.lesson_id import LessonId 4 | from student_journal.domain.value_object.task_id import HomeTaskId 5 | 6 | 7 | @dataclass(slots=True) 8 | class HomeTask: 9 | task_id: HomeTaskId 10 | lesson_id: LessonId 11 | description: str 12 | is_done: bool = False 13 | -------------------------------------------------------------------------------- /docs/register.md: -------------------------------------------------------------------------------- 1 | ### Документация к виджету Register 2 | 3 | Виджет создания аккаунта 4 | 5 | Элементы: 6 | - ``name_input`` - ввод имени студента (QLineEdit) 7 | - ``age_input`` - ввод возраста студента (QDoubleSpinBox) 8 | - ``address_input`` - ввод адреса студента (QLineEdit) 9 | - ``timezone_input`` - ввод часового пояса студента (QDoubleSpinBox) 10 | - ``submit_btn`` - кнопка отправки формы (QPushButton) -------------------------------------------------------------------------------- /src/student_journal/domain/subject.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.domain.value_object.subject_id import SubjectId 4 | from student_journal.domain.value_object.teacher_id import TeacherId 5 | 6 | 7 | # сам предмет урока (математика, русский) 8 | @dataclass(slots=True) 9 | class Subject: 10 | subject_id: SubjectId 11 | teacher_id: TeacherId 12 | title: str 13 | -------------------------------------------------------------------------------- /docs/edit_hometask.md: -------------------------------------------------------------------------------- 1 | ### Документация к виджету EditHometask 2 | 3 | Виджет редактирования и создания ДЗ 4 | 5 | Элементы: 6 | - ``main_label`` - заголовок формы (QLabel) 7 | - ``description`` - ввод описания (QTextEdit) 8 | - ``lesson`` - выбор урока (QComboBox) 9 | - ``is_done`` - выполнено ли ДЗ (QCheckBox) 10 | - ``submit_btn`` - кнопка отправки формы (QPushButton) 11 | - ``delete_btn`` - удалить обьект (QPushButton) -------------------------------------------------------------------------------- /src/student_journal/application/invariants/teacher.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.exceptions.teacher import TeacherFullNameError 2 | 3 | FULL_NAME_MAX_LENGTH = 255 4 | FULL_NAME_MIN_LENGTH = 1 5 | 6 | 7 | def validate_teacher_invariants(full_name: str) -> None: 8 | if (len(full_name) > FULL_NAME_MAX_LENGTH) or ( 9 | len(full_name) < FULL_NAME_MIN_LENGTH 10 | ): 11 | raise TeacherFullNameError 12 | -------------------------------------------------------------------------------- /src/student_journal/application/models/subject.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.domain.teacher import Teacher 4 | from student_journal.domain.value_object.subject_id import SubjectId 5 | 6 | 7 | @dataclass(slots=True, frozen=True) 8 | class SubjectReadModel: 9 | subject_id: SubjectId 10 | teacher: Teacher 11 | title: str 12 | avg_mark: float 13 | marks_list: list[int] 14 | -------------------------------------------------------------------------------- /src/student_journal/adapters/converter/student.py: -------------------------------------------------------------------------------- 1 | from adaptix import Retort, name_mapping 2 | 3 | from student_journal.domain.student import Student 4 | 5 | student_retort = Retort() 6 | student_to_list_retort = Retort( 7 | recipe=[ 8 | name_mapping( 9 | Student, 10 | as_list=True, 11 | skip=[ 12 | "utc_offset", 13 | ], 14 | ), 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /src/student_journal/application/common/transaction_manager.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from contextlib import AbstractContextManager 3 | from typing import Protocol 4 | 5 | 6 | class TransactionManager(Protocol): 7 | @abstractmethod 8 | def commit(self) -> None: ... 9 | 10 | @abstractmethod 11 | def rollback(self) -> None: ... 12 | 13 | @abstractmethod 14 | def begin(self) -> AbstractContextManager[None]: ... 15 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/connection_maker.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from dataclasses import dataclass 3 | from sqlite3 import Connection 4 | 5 | 6 | @dataclass(slots=True, frozen=True) 7 | class DBConfig: 8 | db_path: str 9 | 10 | 11 | @dataclass(slots=True, frozen=True) 12 | class SQLiteConnectionMaker: 13 | config: DBConfig 14 | 15 | def create_connection(self) -> Connection: 16 | return sqlite3.connect(self.config.db_path) 17 | -------------------------------------------------------------------------------- /src/student_journal/domain/lesson.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | 4 | from student_journal.domain.value_object.lesson_id import LessonId 5 | from student_journal.domain.value_object.subject_id import SubjectId 6 | 7 | 8 | @dataclass(slots=True) 9 | class Lesson: 10 | lesson_id: LessonId 11 | subject_id: SubjectId 12 | at: datetime 13 | mark: int | None 14 | note: str | None 15 | room: int 16 | -------------------------------------------------------------------------------- /src/student_journal/application/exceptions/lesson.py: -------------------------------------------------------------------------------- 1 | from .base import ApplicationError 2 | 3 | 4 | class LessonSubjectError(ApplicationError): ... 5 | 6 | 7 | class LessonMarkError(ApplicationError): ... 8 | 9 | 10 | class LessonNoteError(ApplicationError): ... 11 | 12 | 13 | class LessonRoomError(ApplicationError): ... 14 | 15 | 16 | class LessonNotFoundError(ApplicationError): ... 17 | 18 | 19 | class LessonAlreadyExistError(ApplicationError): ... 20 | -------------------------------------------------------------------------------- /src/student_journal/application/models/lesson.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import date, datetime 3 | 4 | from student_journal.domain.lesson import Lesson 5 | 6 | 7 | @dataclass(frozen=True, slots=True) 8 | class WeekLessons: 9 | week_start: date 10 | lessons: dict[date, list[Lesson]] 11 | week_end: date 12 | 13 | 14 | @dataclass(frozen=True, slots=True) 15 | class LessonsByDate: 16 | lessons: dict[datetime, Lesson] 17 | -------------------------------------------------------------------------------- /src/student_journal/application/models/student.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import timezone 3 | 4 | from student_journal.domain.value_object.student_id import StudentId 5 | 6 | 7 | @dataclass(slots=True, frozen=True) 8 | class StudentReadModel: 9 | student_id: StudentId 10 | age: int | None 11 | avatar: str | None 12 | name: str 13 | home_address: str | None 14 | student_overall_avg_mark: float 15 | time_zone: timezone 16 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/schema/load_schema.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import as_file, files 2 | from sqlite3 import Cursor 3 | 4 | import student_journal.adapters.db.schema 5 | 6 | 7 | def load_and_execute(cursor: Cursor) -> None: 8 | source = files(student_journal.adapters.db.schema).joinpath("schema.sql") 9 | 10 | with as_file(source) as sql_path, sql_path.open() as sql_file: 11 | sql_script = sql_file.read() 12 | cursor.executescript(sql_script) 13 | -------------------------------------------------------------------------------- /src/student_journal/application/converters/student.py: -------------------------------------------------------------------------------- 1 | from datetime import timezone 2 | 3 | from adaptix.conversion import impl_converter 4 | 5 | from student_journal.application.models.student import StudentReadModel 6 | from student_journal.domain.student import Student 7 | 8 | 9 | @impl_converter() 10 | def convert_student_to_read_model( # type: ignore 11 | student: Student, 12 | student_overall_avg_mark: float, 13 | time_zone: timezone, 14 | ) -> StudentReadModel: ... 15 | -------------------------------------------------------------------------------- /src/student_journal/application/teacher/__init__.py: -------------------------------------------------------------------------------- 1 | from .create_teacher import CreateTeacher, NewTeacher 2 | from .delete_teacher import DeleteTeacher 3 | from .read_teacher import ReadTeacher 4 | from .read_teachers import ReadTeachers 5 | from .update_teacher import UpdatedTeacher, UpdateTeacher 6 | 7 | __all__ = [ 8 | "CreateTeacher", 9 | "DeleteTeacher", 10 | "NewTeacher", 11 | "ReadTeacher", 12 | "ReadTeachers", 13 | "UpdateTeacher", 14 | "UpdatedTeacher", 15 | ] 16 | -------------------------------------------------------------------------------- /src/student_journal/adapters/converter/home_task.py: -------------------------------------------------------------------------------- 1 | from adaptix import Retort, loader, name_mapping 2 | 3 | from student_journal.domain.home_task import HomeTask 4 | 5 | home_task_retort = Retort(strict_coercion=False) 6 | 7 | home_task_to_list_retort = Retort( 8 | recipe=[ 9 | loader( 10 | bool, 11 | lambda x: 1 if x is True else 0, 12 | ), 13 | name_mapping( 14 | HomeTask, 15 | as_list=True, 16 | ), 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /src/student_journal/application/invariants/home_task.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.exceptions.home_task import ( 2 | HomeTaskDescriptionError, 3 | ) 4 | 5 | DESCRIPTION_MAX_LENGTH = 65535 6 | DESCRIPTION_MIN_LENGTH = 1 7 | 8 | 9 | def validate_home_task_invariants( 10 | description: str, 11 | ) -> None: 12 | if len(description) > DESCRIPTION_MAX_LENGTH: 13 | raise HomeTaskDescriptionError 14 | 15 | if len(description) < DESCRIPTION_MIN_LENGTH: 16 | raise HomeTaskDescriptionError 17 | -------------------------------------------------------------------------------- /src/student_journal/domain/student.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import timedelta, timezone 3 | 4 | from student_journal.domain.value_object.student_id import StudentId 5 | 6 | 7 | @dataclass(slots=True) 8 | class Student: 9 | student_id: StudentId 10 | age: int | None 11 | avatar: str | None 12 | name: str 13 | home_address: str | None 14 | utc_offset: int = 3 15 | 16 | def get_timezone(self) -> timezone: 17 | return timezone(timedelta(hours=self.utc_offset)) 18 | -------------------------------------------------------------------------------- /tests/gateway/subject/conftest.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | 5 | from student_journal.adapters.db.gateway.subject_gateway import SQLiteSubjectGateway 6 | from student_journal.adapters.db.gateway.teacher_gateway import SQLiteTeacherGateway 7 | 8 | 9 | @pytest.fixture 10 | def subject_gateway(cursor: Cursor) -> SQLiteSubjectGateway: 11 | return SQLiteSubjectGateway(cursor) 12 | 13 | 14 | @pytest.fixture 15 | def teacher_gateway(cursor: Cursor) -> SQLiteTeacherGateway: 16 | return SQLiteTeacherGateway(cursor) 17 | -------------------------------------------------------------------------------- /tests/unit/subject/conftest.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from student_journal.domain.subject import Subject 4 | from student_journal.domain.value_object.subject_id import SubjectId 5 | from unit.teacher.conftest import TEACHER2_ID, TEACHER_ID 6 | 7 | SUBJECT_ID = SubjectId(uuid4()) 8 | SUBJECT2_ID = SubjectId(uuid4()) 9 | 10 | SUBJECT = Subject( 11 | subject_id=SUBJECT_ID, 12 | title="abracadabra", 13 | teacher_id=TEACHER_ID, 14 | ) 15 | 16 | SUBJECT2 = Subject( 17 | subject_id=SUBJECT2_ID, 18 | title="abracadabra", 19 | teacher_id=TEACHER2_ID, 20 | ) 21 | -------------------------------------------------------------------------------- /docs/edit_lesson.md: -------------------------------------------------------------------------------- 1 | ### Документация к виджету EditLesson 2 | 3 | Виджет редактирования и создания урока 4 | 5 | Элементы: 6 | - ``main_label`` - заголовок формы (QLabel) 7 | - ``subject_combo`` - выбор предмета (QComboBox) 8 | - ``datetime_edit`` - выбор даты (QDateTmeEdit) 9 | - ``note_input`` - ввод записки к уроку (QLineEdit) 10 | - ``mark_spinbox`` - ввод оценки за урок (QSpinBox) 11 | - ``room_spinbox`` - ввод оценки за урок (QSpinBox) 12 | - ``index_number_spinbox`` - ввод номера урока (QSpinBox) 13 | - ``submit_btn`` - кнопка отправки формы (QPushButton) 14 | - ``delete_btn`` - удалить обьект (QPushButton) -------------------------------------------------------------------------------- /src/student_journal/adapters/converter/__init__.py: -------------------------------------------------------------------------------- 1 | from .home_task import home_task_retort, home_task_to_list_retort 2 | from .lesson import lesson_retort, lesson_to_list_retort, lessons_to_list_retort 3 | from .student import student_retort, student_to_list_retort 4 | from .teacher import teacher_retort, teacher_to_list_retort 5 | 6 | __all__ = [ 7 | "home_task_retort", 8 | "home_task_to_list_retort", 9 | "lesson_retort", 10 | "lesson_to_list_retort", 11 | "lessons_to_list_retort", 12 | "student_retort", 13 | "student_to_list_retort", 14 | "teacher_retort", 15 | "teacher_to_list_retort", 16 | ] 17 | -------------------------------------------------------------------------------- /src/student_journal/application/exceptions/student.py: -------------------------------------------------------------------------------- 1 | from .base import ApplicationError 2 | 3 | 4 | class StudentNameError(ApplicationError): ... 5 | 6 | 7 | class StudentAgeError(ApplicationError): ... 8 | 9 | 10 | class StudentHomeAddressError(ApplicationError): ... 11 | 12 | 13 | class StudentAvatarDoesNotExistsError(ApplicationError): ... 14 | 15 | 16 | class StudentNotFoundError(ApplicationError): ... 17 | 18 | 19 | class StudentAlreadyExistError(ApplicationError): ... 20 | 21 | 22 | class StudentTimezoneError(ApplicationError): ... 23 | 24 | 25 | class StudentIsNotAuthenticatedError(ApplicationError): ... 26 | -------------------------------------------------------------------------------- /src/student_journal/application/common/student_gateway.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Protocol 3 | 4 | from student_journal.domain.student import Student 5 | from student_journal.domain.value_object.student_id import StudentId 6 | 7 | 8 | class StudentGateway(Protocol): 9 | def read_student(self, student_id: StudentId) -> Student: ... 10 | 11 | @abstractmethod 12 | def write_student(self, student: Student) -> None: ... 13 | 14 | @abstractmethod 15 | def get_overall_avg_mark(self) -> float: ... 16 | 17 | @abstractmethod 18 | def update_student(self, student: Student) -> None: ... 19 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from student_journal.bootstrap.entrypoint.qt import main as qt_main 4 | 5 | 6 | def main() -> None: 7 | argv = sys.argv[1:] 8 | 9 | if not argv: 10 | return 11 | 12 | try: 13 | module = argv[0] 14 | option = argv[1] 15 | args = argv[2:] 16 | except IndexError: 17 | return 18 | 19 | modules = { 20 | "run": { 21 | "gui": qt_main, 22 | }, 23 | } 24 | 25 | if module not in modules: 26 | return 27 | 28 | if option not in modules[module]: 29 | return 30 | 31 | modules[module][option](args) 32 | -------------------------------------------------------------------------------- /src/student_journal/application/teacher/read_teachers.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.teacher_gateway import TeacherGateway 5 | from student_journal.application.models.teacher import TeachersReadModel 6 | 7 | 8 | @dataclass(slots=True) 9 | class ReadTeachers: 10 | gateway: TeacherGateway 11 | idp: IdProvider 12 | 13 | def execute(self) -> TeachersReadModel: 14 | student_id = self.idp.get_id() 15 | teachers = self.gateway.read_teachers() 16 | 17 | return TeachersReadModel(student_id=student_id, teachers=teachers) 18 | -------------------------------------------------------------------------------- /src/student_journal/application/models/home_task.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.domain.lesson import Lesson 4 | from student_journal.domain.subject import Subject 5 | from student_journal.domain.value_object.student_id import StudentId 6 | from student_journal.domain.value_object.task_id import HomeTaskId 7 | 8 | 9 | @dataclass(slots=True) 10 | class HomeTaskReadModel: 11 | task_id: HomeTaskId 12 | lesson: Lesson 13 | subject: Subject 14 | description: str 15 | is_done: bool = False 16 | 17 | 18 | @dataclass(slots=True) 19 | class HomeTasksReadModel: 20 | student_id: StudentId 21 | home_tasks: list[HomeTaskReadModel] 22 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/transaction_manager.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | from contextlib import contextmanager 3 | from dataclasses import dataclass 4 | from sqlite3 import Connection 5 | 6 | from student_journal.application.common.transaction_manager import TransactionManager 7 | 8 | 9 | @dataclass(slots=True, frozen=True) 10 | class SQLiteTransactionManager(TransactionManager): 11 | connection: Connection 12 | 13 | @contextmanager 14 | def begin(self) -> Iterator[None]: 15 | yield None 16 | 17 | def commit(self) -> None: 18 | self.connection.commit() 19 | 20 | def rollback(self) -> None: 21 | self.connection.rollback() 22 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/di/config_provider.py: -------------------------------------------------------------------------------- 1 | from dishka import Provider, Scope, from_context, provide 2 | 3 | from student_journal.adapters.config import Config 4 | from student_journal.adapters.db.connection_maker import DBConfig 5 | from student_journal.adapters.id_provider import CredentialsConfig 6 | 7 | 8 | class ConfigProvider(Provider): 9 | scope = Scope.APP 10 | config = from_context(provides=Config, scope=Scope.APP) 11 | 12 | @provide() 13 | def db_config(self, config: Config) -> DBConfig: 14 | return config.db 15 | 16 | @provide() 17 | def credentials_config(self, config: Config) -> CredentialsConfig: 18 | return config.credentials 19 | -------------------------------------------------------------------------------- /src/student_journal/application/subject/read_subject.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.subject_gateway import SubjectGateway 5 | from student_journal.domain.subject import Subject 6 | from student_journal.domain.value_object.subject_id import SubjectId 7 | 8 | 9 | @dataclass(slots=True) 10 | class ReadSubject: 11 | gateway: SubjectGateway 12 | idp: IdProvider 13 | 14 | def execute(self, subject_id: SubjectId) -> Subject: 15 | self.idp.ensure_authenticated() 16 | 17 | subject = self.gateway.read_subject(subject_id) 18 | return subject 19 | -------------------------------------------------------------------------------- /src/student_journal/application/teacher/read_teacher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.teacher_gateway import TeacherGateway 5 | from student_journal.domain.teacher import Teacher 6 | from student_journal.domain.value_object.teacher_id import TeacherId 7 | 8 | 9 | @dataclass(slots=True) 10 | class ReadTeacher: 11 | gateway: TeacherGateway 12 | idp: IdProvider 13 | 14 | def execute(self, teacher_id: TeacherId) -> Teacher: 15 | self.idp.ensure_authenticated() 16 | teacher = self.gateway.read_teacher(teacher_id) 17 | 18 | return teacher 19 | -------------------------------------------------------------------------------- /src/student_journal/application/hometask/read_home_task.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.home_task_gateway import HomeTaskGateway 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.domain.home_task import HomeTask 6 | from student_journal.domain.value_object.task_id import HomeTaskId 7 | 8 | 9 | @dataclass(slots=True) 10 | class ReadHomeTask: 11 | gateway: HomeTaskGateway 12 | idp: IdProvider 13 | 14 | def execute(self, task_id: HomeTaskId) -> HomeTask: 15 | self.idp.ensure_authenticated() 16 | 17 | home_task = self.gateway.read_home_task(task_id) 18 | return home_task 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: 'https://github.com/pre-commit/pre-commit-hooks' 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-merge-conflict 6 | - id: detect-private-key 7 | - id: trailing-whitespace 8 | - id: check-added-large-files 9 | args: ['--maxkb=100'] 10 | - repo: https://github.com/astral-sh/ruff-pre-commit 11 | rev: v0.8.0 12 | hooks: 13 | - id: ruff 14 | args: [ --fix ] 15 | - id: ruff-format 16 | - repo: local 17 | hooks: 18 | - id: mypy-check 19 | types: [python] 20 | name: mypy-check 21 | entry: mypy 22 | language: python 23 | pass_filenames: false 24 | always_run: true 25 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/delete_all_lessons.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.lesson_gateway import LessonGateway 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | 7 | 8 | @dataclass(frozen=True, slots=True) 9 | class DeleteAllLessons: 10 | idp: IdProvider 11 | gateway: LessonGateway 12 | transaction_manager: TransactionManager 13 | 14 | def execute(self) -> None: 15 | self.idp.ensure_authenticated() 16 | with self.transaction_manager.begin(): 17 | self.gateway.delete_all_lessons() 18 | self.transaction_manager.commit() 19 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/connection_factory.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from collections.abc import Iterator 3 | from contextlib import closing, contextmanager 4 | from dataclasses import dataclass 5 | from sqlite3 import Connection 6 | 7 | from student_journal.adapters.db.connection_maker import SQLiteConnectionMaker 8 | 9 | 10 | @dataclass(slots=True, frozen=True) 11 | class SQLiteConnectionFactory: 12 | connection_maker: SQLiteConnectionMaker 13 | 14 | @contextmanager 15 | def connection(self) -> Iterator[Connection]: 16 | conn = self.connection_maker.create_connection() 17 | conn.row_factory = sqlite3.Row 18 | 19 | with closing(conn) as c: 20 | c.execute("PRAGMA foreign_keys = ON;") 21 | yield c 22 | -------------------------------------------------------------------------------- /src/student_journal/application/common/teacher_gateway.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Protocol 3 | 4 | from student_journal.domain.teacher import Teacher 5 | from student_journal.domain.value_object.teacher_id import TeacherId 6 | 7 | 8 | class TeacherGateway(Protocol): 9 | @abstractmethod 10 | def read_teacher(self, teacher_id: TeacherId) -> Teacher: ... 11 | 12 | @abstractmethod 13 | def write_teacher(self, teacher: Teacher) -> None: ... 14 | 15 | @abstractmethod 16 | def read_teachers(self) -> list[Teacher]: ... 17 | 18 | @abstractmethod 19 | def update_teacher(self, teacher: Teacher) -> None: ... 20 | 21 | @abstractmethod 22 | def delete_teacher(self, teacher_id: TeacherId) -> None: ... 23 | -------------------------------------------------------------------------------- /src/student_journal/application/hometask/read_home_tasks.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.home_task_gateway import HomeTaskGateway 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.models.home_task import HomeTasksReadModel 6 | 7 | 8 | @dataclass(slots=True) 9 | class ReadHomeTasks: 10 | gateway: HomeTaskGateway 11 | idp: IdProvider 12 | 13 | def execute(self, *, show_done: bool = False) -> HomeTasksReadModel: 14 | student_id = self.idp.get_id() 15 | home_tasks = self.gateway.read_home_tasks(show_done=show_done) 16 | 17 | return HomeTasksReadModel( 18 | student_id=student_id, 19 | home_tasks=home_tasks, 20 | ) 21 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/di/adapter_provider.py: -------------------------------------------------------------------------------- 1 | from dishka import Provider, Scope, provide 2 | 3 | from student_journal.adapters.error_locator import ErrorLocator, SimpleErrorLocator 4 | from student_journal.adapters.id_provider import FileIdProvider 5 | from student_journal.adapters.load_test_data import TestDataLoader 6 | from student_journal.application.common.id_provider import IdProvider 7 | 8 | 9 | class AdapterProvider(Provider): 10 | scope = Scope.REQUEST 11 | 12 | id_provider = provide(source=FileIdProvider, provides=IdProvider) 13 | file_id_provider = provide(FileIdProvider) 14 | error_locator = provide( 15 | source=SimpleErrorLocator, 16 | provides=ErrorLocator, 17 | scope=Scope.APP, 18 | ) 19 | data_loader = provide(TestDataLoader) 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | on: [push, pull_request] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - name: Set up Python 9 | uses: actions/setup-python@v4 10 | with: 11 | python-version: '3.11.9' 12 | - name: Install dependencies 13 | run: | 14 | python -m pip install uv==0.4.18 15 | uv pip install -e ".[ci]" --system 16 | - name: Run ruff for tests 17 | uses: astral-sh/ruff-action@v1 18 | with: 19 | src: "./tests" 20 | - name: Run mypy 21 | run: mypy 22 | - name: Test 23 | run: pytest 24 | - name: Ruff 25 | run: ruff check --fix 26 | - name: Format 27 | run: ruff format 28 | -------------------------------------------------------------------------------- /src/student_journal/application/invariants/lesson.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.exceptions.lesson import ( 2 | LessonMarkError, 3 | LessonNoteError, 4 | LessonRoomError, 5 | ) 6 | 7 | MIN_MARK = 1 8 | MAX_MARK = 5 9 | MARK_RANGE = range(MIN_MARK, MAX_MARK + 1) 10 | NOTE_MAX_LENGTH = 65535 11 | NOTE_MIN_LENGTH = 1 12 | MIN_ROOM = 1 13 | 14 | 15 | def validate_lesson_invariants( 16 | mark: int | None, 17 | note: str | None, 18 | room: int, 19 | ) -> None: 20 | if (mark is not None) and mark not in MARK_RANGE: 21 | raise LessonMarkError 22 | 23 | if (note is not None) and ( 24 | len(note) > NOTE_MAX_LENGTH or len(note) < NOTE_MIN_LENGTH 25 | ): 26 | raise LessonNoteError 27 | 28 | if room < MIN_ROOM: 29 | raise LessonRoomError 30 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/register.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.invariants.student import ( 2 | HOME_ADDRESS_MAX_LENGTH, 3 | MAX_AGE, 4 | MIN_AGE, 5 | NAME_MAX_LENGTH, 6 | ) 7 | from student_journal.presentation.ui.raw.register_ui import Ui_Register 8 | 9 | 10 | class RegisterUI(Ui_Register): 11 | def setupUi(self, *args, **kwargs): 12 | super().setupUi(*args, **kwargs) 13 | self.set_invariants() 14 | 15 | def set_invariants(self): 16 | self.name_input.setMaxLength(NAME_MAX_LENGTH) 17 | self.age_input.setMinimum(MIN_AGE) 18 | self.age_input.setValue(5) 19 | self.age_input.setSpecialValueText("Не выбран") 20 | self.age_input.setMaximum(MAX_AGE) 21 | self.address_input.setMaxLength(HOME_ADDRESS_MAX_LENGTH) 22 | -------------------------------------------------------------------------------- /src/student_journal/application/student/read_student.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.student_gateway import StudentGateway 4 | from student_journal.application.converters.student import convert_student_to_read_model 5 | from student_journal.application.models.student import StudentReadModel 6 | from student_journal.domain.value_object.student_id import StudentId 7 | 8 | 9 | @dataclass(slots=True) 10 | class ReadStudent: 11 | gateway: StudentGateway 12 | 13 | def execute(self, student_id: StudentId) -> StudentReadModel: 14 | student = self.gateway.read_student( 15 | student_id, 16 | ) 17 | avg = self.gateway.get_overall_avg_mark() 18 | return convert_student_to_read_model(student, avg, student.get_timezone()) 19 | -------------------------------------------------------------------------------- /src/student_journal/adapters/converter/lesson.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from adaptix import Retort, dumper, loader, name_mapping 4 | 5 | from student_journal.application.models.lesson import WeekLessons 6 | from student_journal.domain.lesson import Lesson 7 | 8 | lesson_retort = Retort() 9 | lesson_to_list_retort = Retort( 10 | recipe=[ 11 | loader(datetime, lambda x: x), 12 | dumper(datetime, lambda x: x), 13 | name_mapping( 14 | Lesson, 15 | as_list=True, 16 | ), 17 | ], 18 | ) 19 | lessons_to_list_retort = Retort( 20 | recipe=[ 21 | loader(datetime, lambda x: x), 22 | dumper(datetime, lambda x: x), 23 | name_mapping( 24 | WeekLessons, 25 | as_list=True, 26 | ), 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/edit_student.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.invariants.student import ( 2 | HOME_ADDRESS_MAX_LENGTH, 3 | MAX_AGE, 4 | MIN_AGE, 5 | NAME_MAX_LENGTH, 6 | ) 7 | from student_journal.presentation.ui.raw.edit_student_ui import Ui_EditStudent 8 | 9 | 10 | class EditStudentUI(Ui_EditStudent): 11 | def setupUi(self, *args, **kwargs): 12 | super().setupUi(*args, **kwargs) 13 | self.set_invariants() 14 | 15 | def set_invariants(self): 16 | self.name_input.setMaxLength(NAME_MAX_LENGTH) 17 | self.age_input.setMinimum(MIN_AGE) 18 | self.age_input.setValue(5) 19 | self.age_input.setSpecialValueText("Не выбран") 20 | self.age_input.setMaximum(MAX_AGE) 21 | self.address_input.setMaxLength(HOME_ADDRESS_MAX_LENGTH) 22 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/delete_lesson.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.lesson_gateway import LessonGateway 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.domain.value_object.lesson_id import LessonId 7 | 8 | 9 | @dataclass(slots=True) 10 | class DeleteLesson: 11 | transaction_manager: TransactionManager 12 | gateway: LessonGateway 13 | idp: IdProvider 14 | 15 | def execute(self, lesson_id: LessonId) -> None: 16 | self.idp.ensure_authenticated() 17 | 18 | with self.transaction_manager.begin(): 19 | self.gateway.delete_lesson(lesson_id) 20 | self.transaction_manager.commit() 21 | -------------------------------------------------------------------------------- /src/student_journal/application/hometask/delete_home_task.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.home_task_gateway import HomeTaskGateway 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.domain.value_object.task_id import HomeTaskId 7 | 8 | 9 | @dataclass(slots=True) 10 | class DeleteHomeTask: 11 | transaction_manager: TransactionManager 12 | gateway: HomeTaskGateway 13 | idp: IdProvider 14 | 15 | def execute(self, task_id: HomeTaskId) -> None: 16 | self.idp.ensure_authenticated() 17 | 18 | with self.transaction_manager.begin(): 19 | self.gateway.delete_home_task(task_id) 20 | self.transaction_manager.commit() 21 | -------------------------------------------------------------------------------- /src/student_journal/application/subject/delete_subject.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.subject_gateway import SubjectGateway 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.domain.value_object.subject_id import SubjectId 7 | 8 | 9 | @dataclass(slots=True) 10 | class DeleteSubject: 11 | transaction_manager: TransactionManager 12 | gateway: SubjectGateway 13 | idp: IdProvider 14 | 15 | def execute(self, subject_id: SubjectId) -> None: 16 | self.idp.ensure_authenticated() 17 | 18 | with self.transaction_manager.begin(): 19 | self.gateway.delete_subject(subject_id) 20 | self.transaction_manager.commit() 21 | -------------------------------------------------------------------------------- /src/student_journal/application/teacher/delete_teacher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.teacher_gateway import TeacherGateway 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.domain.value_object.teacher_id import TeacherId 7 | 8 | 9 | @dataclass(slots=True) 10 | class DeleteTeacher: 11 | transaction_manager: TransactionManager 12 | gateway: TeacherGateway 13 | idp: IdProvider 14 | 15 | def execute(self, teacher_id: TeacherId) -> None: 16 | self.idp.ensure_authenticated() 17 | 18 | with self.transaction_manager.begin(): 19 | self.gateway.delete_teacher(teacher_id) 20 | self.transaction_manager.commit() 21 | -------------------------------------------------------------------------------- /src/student_journal/adapters/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | 4 | from student_journal.adapters.db.connection_maker import DBConfig 5 | from student_journal.adapters.id_provider import CredentialsConfig 6 | 7 | BASE_PATH = Path(Path.expanduser(Path("~/student_journal/"))) 8 | CREDENTIALS_PATH = Path(BASE_PATH / "auth.toml") 9 | 10 | 11 | @dataclass(slots=True, frozen=True) 12 | class Config: 13 | db: DBConfig 14 | credentials: CredentialsConfig 15 | 16 | 17 | def load_from_file() -> Config: 18 | if not BASE_PATH.exists(): 19 | BASE_PATH.mkdir(parents=True) 20 | 21 | return Config( 22 | db=DBConfig( 23 | db_path=str(BASE_PATH / "db.sqlite3"), 24 | ), 25 | credentials=CredentialsConfig( 26 | path=CREDENTIALS_PATH, 27 | ), 28 | ) 29 | -------------------------------------------------------------------------------- /src/student_journal/application/student/read_current_student.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.student_gateway import StudentGateway 5 | from student_journal.application.converters.student import convert_student_to_read_model 6 | from student_journal.application.models.student import StudentReadModel 7 | 8 | 9 | @dataclass(slots=True) 10 | class ReadCurrentStudent: 11 | gateway: StudentGateway 12 | idp: IdProvider 13 | 14 | def execute(self) -> StudentReadModel: 15 | current_student_id = self.idp.get_id() 16 | student = self.gateway.read_student( 17 | current_student_id, 18 | ) 19 | 20 | avg = self.gateway.get_overall_avg_mark() 21 | return convert_student_to_read_model(student, avg, student.get_timezone()) 22 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/read_lesson.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.lesson_gateway import LessonGateway 5 | from student_journal.application.common.student_gateway import StudentGateway 6 | from student_journal.domain.lesson import Lesson 7 | from student_journal.domain.value_object.lesson_id import LessonId 8 | 9 | 10 | @dataclass(slots=True) 11 | class ReadLesson: 12 | gateway: LessonGateway 13 | student_gateway: StudentGateway 14 | idp: IdProvider 15 | 16 | def execute(self, lesson_id: LessonId) -> Lesson: 17 | self.idp.ensure_authenticated() 18 | student = self.student_gateway.read_student(self.idp.get_id()) 19 | 20 | lesson = self.gateway.read_lesson(lesson_id, student.get_timezone()) 21 | 22 | return lesson 23 | -------------------------------------------------------------------------------- /src/student_journal/application/common/home_task_gateway.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Protocol 3 | 4 | from student_journal.application.models.home_task import HomeTaskReadModel 5 | from student_journal.domain.home_task import HomeTask 6 | from student_journal.domain.value_object.task_id import HomeTaskId 7 | 8 | 9 | class HomeTaskGateway(Protocol): 10 | @abstractmethod 11 | def read_home_task(self, task_id: HomeTaskId) -> HomeTask: ... 12 | 13 | @abstractmethod 14 | def read_home_tasks( 15 | self, 16 | *, 17 | show_done: bool = False, 18 | ) -> list[HomeTaskReadModel]: ... 19 | 20 | @abstractmethod 21 | def write_home_task(self, home_task: HomeTask) -> None: ... 22 | 23 | @abstractmethod 24 | def update_home_task(self, home_task: HomeTask) -> None: ... 25 | 26 | @abstractmethod 27 | def delete_home_task(self, task_id: HomeTaskId) -> None: ... 28 | -------------------------------------------------------------------------------- /src/student_journal/application/subject/read_subjects.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.subject_gateway import SubjectGateway 5 | from student_journal.application.models.subject import SubjectReadModel 6 | 7 | 8 | @dataclass(slots=True) 9 | class ReadSubjects: 10 | gateway: SubjectGateway 11 | idp: IdProvider 12 | 13 | def execute( 14 | self, 15 | *, 16 | sort_by_title: bool = False, 17 | sort_by_avg_mark: bool = False, 18 | show_empty: bool = True, 19 | ) -> list[SubjectReadModel]: 20 | self.idp.ensure_authenticated() 21 | 22 | subjects = self.gateway.read_subjects( 23 | sort_by_title=sort_by_title, 24 | sort_by_avg_mark=sort_by_avg_mark, 25 | show_empty=show_empty, 26 | ) 27 | 28 | return subjects 29 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/read_first_lessons_of_weeks.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.lesson_gateway import LessonGateway 5 | from student_journal.application.common.student_gateway import StudentGateway 6 | from student_journal.application.models.lesson import LessonsByDate 7 | 8 | 9 | @dataclass(slots=True, frozen=True) 10 | class ReadFirstLessonsOfWeeks: 11 | gateway: LessonGateway 12 | student_gateway: StudentGateway 13 | idp: IdProvider 14 | 15 | def execute(self, month: int, year: int) -> LessonsByDate: 16 | self.idp.ensure_authenticated() 17 | 18 | student = self.student_gateway.read_student(self.idp.get_id()) 19 | 20 | return self.gateway.read_first_lessons_of_weeks( 21 | month, 22 | year, 23 | as_tz=student.get_timezone(), 24 | ) 25 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/di/container.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from dishka import Container, make_container 4 | 5 | from student_journal.adapters.config import Config, load_from_file 6 | from student_journal.bootstrap.di.adapter_provider import AdapterProvider 7 | from student_journal.bootstrap.di.command_provider import CommandProvider 8 | from student_journal.bootstrap.di.config_provider import ConfigProvider 9 | from student_journal.bootstrap.di.db_provider import DbProvider 10 | from student_journal.bootstrap.di.gateway_provider import GatewayProvider 11 | 12 | 13 | def get_container_for_gui() -> Container: 14 | config = load_from_file() 15 | 16 | return make_container( 17 | ConfigProvider(), 18 | CommandProvider(), 19 | AdapterProvider(), 20 | DbProvider(), 21 | GatewayProvider(), 22 | context={ 23 | Config: config, 24 | }, 25 | lock_factory=threading.Lock, 26 | ) 27 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/edit_lesson.py: -------------------------------------------------------------------------------- 1 | from datetime import time 2 | 3 | from student_journal.application.invariants.lesson import ( 4 | MIN_ROOM, 5 | MAX_MARK, 6 | ) 7 | from student_journal.presentation.ui.raw.edit_lesson_ui import Ui_EditLesson 8 | 9 | 10 | class EditLessonUI(Ui_EditLesson): 11 | def setupUi(self, *args, **kwargs): 12 | super().setupUi(*args, **kwargs) 13 | self.set_invariants() 14 | 15 | def set_invariants(self): 16 | self.room_spinbox.setMinimum(MIN_ROOM) 17 | self.room_spinbox.setMaximum(1_000_000) 18 | self.mark_spinbox.setMinimum(0) 19 | 20 | self.mark_spinbox.setValue(0) 21 | self.mark_spinbox.setSpecialValueText("Не выбрано") 22 | self.mark_spinbox.setMaximum(MAX_MARK) 23 | 24 | default_time = time(hour=8, minute=0, second=0) 25 | self.time_edit.setMinimumTime(default_time) 26 | self.datetime_preview.setTime(default_time) 27 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/read_lessons_for_week.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import date 3 | 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.common.lesson_gateway import LessonGateway 6 | from student_journal.application.common.student_gateway import StudentGateway 7 | from student_journal.application.models.lesson import WeekLessons 8 | 9 | 10 | @dataclass(slots=True, frozen=True) 11 | class ReadLessonsForWeek: 12 | gateway: LessonGateway 13 | student_gateway: StudentGateway 14 | idp: IdProvider 15 | 16 | def execute(self, week_start: date) -> WeekLessons: 17 | self.idp.ensure_authenticated() 18 | student = self.student_gateway.read_student(self.idp.get_id()) 19 | 20 | lessons_by_date = self.gateway.read_lessons_for_week( 21 | week_start, 22 | as_tz=student.get_timezone(), 23 | ) 24 | 25 | return lessons_by_date 26 | -------------------------------------------------------------------------------- /src/student_journal/application/common/subject_gateway.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Protocol 3 | 4 | from student_journal.application.models.subject import SubjectReadModel 5 | from student_journal.domain.subject import Subject 6 | from student_journal.domain.value_object.subject_id import SubjectId 7 | 8 | 9 | class SubjectGateway(Protocol): 10 | @abstractmethod 11 | def read_subject(self, subject_id: SubjectId) -> Subject: ... 12 | 13 | @abstractmethod 14 | def write_subject(self, subject: Subject) -> None: ... 15 | 16 | @abstractmethod 17 | def read_subjects( 18 | self, 19 | *, 20 | sort_by_title: bool = False, 21 | sort_by_avg_mark: bool = False, 22 | show_empty: bool = True, 23 | ) -> list[SubjectReadModel]: ... 24 | 25 | @abstractmethod 26 | def update_subject(self, subject: Subject) -> None: ... 27 | 28 | @abstractmethod 29 | def delete_subject(self, subject_id: SubjectId) -> None: ... 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 LYUBAVSKY ILYA 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tests/gateway/conftest.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from sqlite3 import Connection, Cursor 3 | 4 | import pytest 5 | 6 | from student_journal.adapters.db.connection_factory import SQLiteConnectionFactory 7 | from student_journal.adapters.db.connection_maker import DBConfig, SQLiteConnectionMaker 8 | from student_journal.adapters.db.schema.load_schema import load_and_execute 9 | from student_journal.adapters.db.transaction_manager import SQLiteTransactionManager 10 | 11 | 12 | @pytest.fixture 13 | def connection() -> Iterable[Connection]: 14 | maker = SQLiteConnectionMaker(DBConfig(":memory:")) 15 | factory = SQLiteConnectionFactory(maker) 16 | 17 | with factory.connection() as conn: 18 | load_and_execute(conn.cursor()) 19 | conn.execute("PRAGMA foreign_keys = OFF;") 20 | yield conn 21 | 22 | 23 | @pytest.fixture 24 | def cursor(connection: Connection) -> Cursor: 25 | return connection.cursor() 26 | 27 | 28 | @pytest.fixture 29 | def transaction_manager(connection: Connection) -> SQLiteTransactionManager: 30 | return SQLiteTransactionManager(connection) 31 | -------------------------------------------------------------------------------- /tests/unit/student/mock/student_gateway.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.common.student_gateway import StudentGateway 2 | from student_journal.application.exceptions.student import StudentNotFoundError 3 | from student_journal.domain.student import Student 4 | from student_journal.domain.value_object.student_id import StudentId 5 | 6 | 7 | class MockedStudentGateway(StudentGateway): 8 | AVG_MARK = 4.5 9 | 10 | def __init__(self) -> None: 11 | self._students = {} 12 | self.is_updated = False 13 | self.is_wrote = False 14 | 15 | def read_student(self, student_id: StudentId) -> Student: 16 | if not (student := self._students.get(student_id)): 17 | raise StudentNotFoundError 18 | 19 | return student 20 | 21 | def write_student(self, student: Student) -> None: 22 | self.is_wrote = True 23 | self._students[student.student_id] = student 24 | 25 | def get_overall_avg_mark(self) -> float: 26 | return self.AVG_MARK 27 | 28 | def update_student(self, student: Student) -> None: 29 | self.is_updated = True 30 | self.write_student(student) 31 | -------------------------------------------------------------------------------- /student-journal.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['src/student_journal/bootstrap/entrypoint/qt.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[ 9 | ('src/student_journal/adapters/db/schema/schema.sql', 'student_journal/adapters/db/schema/'), 10 | ('src/student_journal/presentation/resource/', 'student_journal/presentation/resource/'), 11 | ], 12 | hiddenimports=[], 13 | hookspath=[], 14 | hooksconfig={}, 15 | runtime_hooks=[], 16 | excludes=[], 17 | noarchive=False, 18 | optimize=1, 19 | ) 20 | pyz = PYZ(a.pure) 21 | 22 | exe = EXE( 23 | pyz, 24 | a.scripts, 25 | a.binaries, 26 | a.datas, 27 | [], 28 | name='ДневникШкольника', 29 | debug=False, 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=True, 33 | upx_exclude=[], 34 | runtime_tmpdir=None, 35 | console=False, 36 | disable_windowed_traceback=False, 37 | argv_emulation=False, 38 | target_arch=None, 39 | codesign_identity=None, 40 | entitlements_file=None, 41 | icon="src/student_journal/presentation/resource/favicon.ico", 42 | ) 43 | -------------------------------------------------------------------------------- /src/student_journal/application/invariants/student.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from student_journal.application.exceptions.student import ( 4 | StudentAgeError, 5 | StudentAvatarDoesNotExistsError, 6 | StudentHomeAddressError, 7 | StudentNameError, 8 | ) 9 | 10 | MIN_AGE = 6 11 | MAX_AGE = 100 12 | AGE_RANGE = range(MIN_AGE, MAX_AGE + 1) 13 | NAME_MAX_LENGTH = 60 14 | NAME_MIN_LENGTH = 2 15 | HOME_ADDRESS_MAX_LENGTH = 255 16 | HOME_ADDRESS_MIN_LENGTH = 2 17 | 18 | 19 | def validate_student_invariants( 20 | age: int | None, 21 | name: str, 22 | home_address: str | None, 23 | avatar: str | None, 24 | ) -> None: 25 | if (age is not None) and age not in AGE_RANGE: 26 | raise StudentAgeError 27 | 28 | if len(name) > NAME_MAX_LENGTH or len(name) < NAME_MIN_LENGTH: 29 | raise StudentNameError 30 | 31 | if home_address is not None and len(home_address) not in range( 32 | HOME_ADDRESS_MIN_LENGTH, 33 | HOME_ADDRESS_MAX_LENGTH, 34 | ): 35 | raise StudentHomeAddressError 36 | 37 | if (avatar is not None) and not Path(avatar).exists(): 38 | raise StudentAvatarDoesNotExistsError 39 | -------------------------------------------------------------------------------- /tests/common/mock/transaction_manager.py: -------------------------------------------------------------------------------- 1 | from contextlib import AbstractContextManager, contextmanager 2 | 3 | from student_journal.application.common.transaction_manager import TransactionManager 4 | 5 | 6 | class MockedTransactionManager(TransactionManager): 7 | def __init__(self) -> None: 8 | self.is_commited = False 9 | self.is_rolled_back = False 10 | self.is_begin = False 11 | 12 | @contextmanager 13 | def begin(self) -> AbstractContextManager[None]: 14 | if self.is_begin: 15 | raise ValueError("Transaction is trying to begin twice!") 16 | self.is_begin = True 17 | yield 18 | 19 | def commit(self) -> None: 20 | if self.is_commited: 21 | raise ValueError("Transaction is trying to commit twice!") 22 | 23 | if not self.is_begin: 24 | raise ValueError("Transaction has not begun") 25 | 26 | self.is_commited = True 27 | 28 | def rollback(self) -> None: 29 | if not self.is_begin: 30 | raise ValueError("Transaction has not begun") 31 | 32 | if self.is_rolled_back: 33 | raise ValueError("Transaction is trying to rollback twice!") 34 | 35 | self.is_rolled_back = True 36 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/delete_lessons_for_week.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import date 3 | 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.common.lesson_gateway import LessonGateway 6 | from student_journal.application.common.student_gateway import StudentGateway 7 | from student_journal.application.common.transaction_manager import TransactionManager 8 | 9 | 10 | @dataclass(frozen=True, slots=True) 11 | class DeleteLessonsForWeek: 12 | idp: IdProvider 13 | gateway: LessonGateway 14 | student_gateway: StudentGateway 15 | transaction_manager: TransactionManager 16 | 17 | def execute(self, week_start: date) -> None: 18 | self.idp.ensure_authenticated() 19 | student = self.student_gateway.read_student(self.idp.get_id()) 20 | 21 | dates = self.gateway.read_lessons_for_week( 22 | week_start, 23 | as_tz=student.get_timezone(), 24 | ) 25 | 26 | ids = [] 27 | 28 | for each in dates.lessons.values(): 29 | ids.extend([lesson.lesson_id for lesson in each]) 30 | 31 | with self.transaction_manager.begin(): 32 | self.gateway.delete_lessons(ids) 33 | self.transaction_manager.commit() 34 | -------------------------------------------------------------------------------- /tests/unit/teacher/mock/teacher_gateway.py: -------------------------------------------------------------------------------- 1 | from student_journal.application.common.teacher_gateway import TeacherGateway 2 | from student_journal.application.exceptions.teacher import TeacherNotFoundError 3 | from student_journal.domain.teacher import Teacher 4 | from student_journal.domain.value_object.teacher_id import TeacherId 5 | 6 | 7 | class MockedTeacherGateway(TeacherGateway): 8 | def __init__(self) -> None: 9 | self._teachers = {} 10 | self.is_updated = False 11 | self.is_wrote = False 12 | self.is_deleted = False 13 | 14 | def read_teacher(self, teacher_id: TeacherId) -> Teacher: 15 | if not (teacher := self._teachers.get(teacher_id)): 16 | raise TeacherNotFoundError 17 | 18 | return teacher 19 | 20 | def write_teacher(self, teacher: Teacher) -> None: 21 | self.is_wrote = True 22 | self._teachers[teacher.teacher_id] = teacher 23 | 24 | def read_teachers(self) -> list[Teacher]: 25 | return list(self._teachers.values()) 26 | 27 | def update_teacher(self, teacher: Teacher) -> None: 28 | self.is_updated = True 29 | self.write_teacher(teacher) 30 | 31 | def delete_teacher(self, teacher_id: TeacherId) -> None: 32 | del self._teachers[teacher_id] 33 | self.is_deleted = True 34 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/di/gateway_provider.py: -------------------------------------------------------------------------------- 1 | from dishka import Provider, Scope, provide 2 | 3 | from student_journal.adapters.db.gateway.home_task_gateway import SQLiteHomeTaskGateway 4 | from student_journal.adapters.db.gateway.lesson_gateway import SQLiteLessonGateway 5 | from student_journal.adapters.db.gateway.student_gateway import SQLiteStudentGateway 6 | from student_journal.adapters.db.gateway.subject_gateway import SQLiteSubjectGateway 7 | from student_journal.adapters.db.gateway.teacher_gateway import SQLiteTeacherGateway 8 | from student_journal.application.common.home_task_gateway import HomeTaskGateway 9 | from student_journal.application.common.lesson_gateway import LessonGateway 10 | from student_journal.application.common.student_gateway import StudentGateway 11 | from student_journal.application.common.subject_gateway import SubjectGateway 12 | from student_journal.application.common.teacher_gateway import TeacherGateway 13 | 14 | 15 | class GatewayProvider(Provider): 16 | scope = Scope.REQUEST 17 | 18 | student_gateway = provide(SQLiteStudentGateway, provides=StudentGateway) 19 | teacher_gateway = provide(SQLiteTeacherGateway, provides=TeacherGateway) 20 | subject_gateway = provide(SQLiteSubjectGateway, provides=SubjectGateway) 21 | lesson_gateway = provide(SQLiteLessonGateway, provides=LessonGateway) 22 | home_task_gateway = provide(SQLiteHomeTaskGateway, provides=HomeTaskGateway) 23 | -------------------------------------------------------------------------------- /src/student_journal/application/teacher/update_teacher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.teacher_gateway import TeacherGateway 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.application.invariants.teacher import validate_teacher_invariants 7 | from student_journal.domain.teacher import Teacher 8 | from student_journal.domain.value_object.teacher_id import TeacherId 9 | 10 | 11 | @dataclass(slots=True, frozen=True) 12 | class UpdatedTeacher: 13 | teacher_id: TeacherId 14 | full_name: str 15 | avatar: str | None 16 | 17 | 18 | @dataclass(slots=True) 19 | class UpdateTeacher: 20 | gateway: TeacherGateway 21 | transaction_manager: TransactionManager 22 | idp: IdProvider 23 | 24 | def execute(self, data: UpdatedTeacher) -> TeacherId: 25 | self.idp.ensure_authenticated() 26 | 27 | validate_teacher_invariants(full_name=data.full_name) 28 | 29 | teacher = Teacher( 30 | teacher_id=data.teacher_id, 31 | full_name=data.full_name, 32 | avatar=data.avatar, 33 | ) 34 | 35 | with self.transaction_manager.begin(): 36 | self.gateway.update_teacher(teacher) 37 | self.transaction_manager.commit() 38 | 39 | return data.teacher_id 40 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/schema/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "Student" ( 2 | "student_id" TEXT NOT NULL UNIQUE, 3 | "age" INTEGER, 4 | "avatar" TEXT, 5 | "name" VARCHAR NOT NULL, 6 | "home_address" VARCHAR, 7 | PRIMARY KEY("student_id") 8 | ); 9 | 10 | CREATE TABLE IF NOT EXISTS "Teacher" ( 11 | "teacher_id" TEXT NOT NULL UNIQUE, 12 | "full_name" VARCHAR NOT NULL, 13 | "avatar" TEXT, 14 | PRIMARY KEY("teacher_id") 15 | ); 16 | 17 | CREATE TABLE IF NOT EXISTS "Subject" ( 18 | "subject_id" TEXT NOT NULL UNIQUE, 19 | "title" VARCHAR NOT NULL, 20 | "teacher_id" TEXT NOT NULL, 21 | PRIMARY KEY("subject_id"), 22 | FOREIGN KEY ("teacher_id") REFERENCES "Teacher"("teacher_id") 23 | ON UPDATE NO ACTION ON DELETE CASCADE 24 | ); 25 | 26 | CREATE TABLE IF NOT EXISTS "Lesson" ( 27 | "lesson_id" TEXT NOT NULL UNIQUE, 28 | "subject_id" TEXT NOT NULL, 29 | "at" DATETIME NOT NULL, 30 | "mark" INTEGER, 31 | "note" TEXT, 32 | "room" INTEGER NOT NULL, 33 | PRIMARY KEY("lesson_id"), 34 | FOREIGN KEY ("subject_id") REFERENCES "Subject"("subject_id") 35 | ON UPDATE NO ACTION ON DELETE CASCADE 36 | ); 37 | 38 | CREATE TABLE IF NOT EXISTS "Hometask" ( 39 | "task_id" TEXT NOT NULL UNIQUE, 40 | "description" TEXT NOT NULL, 41 | "is_done" BOOLEAN NOT NULL DEFAULT 0, 42 | "lesson_id" TEXT NOT NULL, 43 | PRIMARY KEY("task_id"), 44 | FOREIGN KEY ("lesson_id") REFERENCES "Lesson"("lesson_id") 45 | ON UPDATE NO ACTION ON DELETE CASCADE 46 | ); 47 | -------------------------------------------------------------------------------- /src/student_journal/application/teacher/create_teacher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from uuid import uuid4 3 | 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.common.teacher_gateway import TeacherGateway 6 | from student_journal.application.common.transaction_manager import TransactionManager 7 | from student_journal.application.invariants.teacher import validate_teacher_invariants 8 | from student_journal.domain.teacher import Teacher 9 | from student_journal.domain.value_object.teacher_id import TeacherId 10 | 11 | 12 | @dataclass(slots=True, frozen=True) 13 | class NewTeacher: 14 | full_name: str 15 | avatar: str | None 16 | 17 | 18 | @dataclass(slots=True) 19 | class CreateTeacher: 20 | gateway: TeacherGateway 21 | transaction_manager: TransactionManager 22 | idp: IdProvider 23 | 24 | def execute(self, data: NewTeacher) -> TeacherId: 25 | self.idp.ensure_authenticated() 26 | 27 | validate_teacher_invariants(full_name=data.full_name) 28 | 29 | teacher_id = TeacherId(uuid4()) 30 | teacher = Teacher( 31 | teacher_id=teacher_id, 32 | full_name=data.full_name, 33 | avatar=data.avatar, 34 | ) 35 | 36 | with self.transaction_manager.begin(): 37 | self.gateway.write_teacher(teacher) 38 | self.transaction_manager.commit() 39 | 40 | return teacher_id 41 | -------------------------------------------------------------------------------- /src/student_journal/application/subject/update_subject.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.subject_gateway import SubjectGateway 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.application.invariants.subject import validate_subject_invariants 7 | from student_journal.domain.subject import Subject 8 | from student_journal.domain.value_object.subject_id import SubjectId 9 | from student_journal.domain.value_object.teacher_id import TeacherId 10 | 11 | 12 | @dataclass(slots=True, frozen=True) 13 | class UpdatedSubject: 14 | subject_id: SubjectId 15 | teacher_id: TeacherId 16 | title: str 17 | 18 | 19 | @dataclass(slots=True) 20 | class UpdateSubject: 21 | gateway: SubjectGateway 22 | transaction_manager: TransactionManager 23 | idp: IdProvider 24 | 25 | def execute(self, data: UpdatedSubject) -> SubjectId: 26 | self.idp.ensure_authenticated() 27 | 28 | validate_subject_invariants(data.title) 29 | 30 | subject = Subject( 31 | subject_id=data.subject_id, 32 | title=data.title, 33 | teacher_id=data.teacher_id, 34 | ) 35 | 36 | with self.transaction_manager.begin(): 37 | self.gateway.update_subject(subject) 38 | self.transaction_manager.commit() 39 | 40 | return data.subject_id 41 | -------------------------------------------------------------------------------- /src/student_journal/application/subject/create_subject.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from uuid import uuid4 3 | 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.common.subject_gateway import SubjectGateway 6 | from student_journal.application.common.transaction_manager import TransactionManager 7 | from student_journal.application.invariants.subject import validate_subject_invariants 8 | from student_journal.domain.subject import Subject 9 | from student_journal.domain.value_object.subject_id import SubjectId 10 | from student_journal.domain.value_object.teacher_id import TeacherId 11 | 12 | 13 | @dataclass(slots=True, frozen=True) 14 | class NewSubject: 15 | teacher_id: TeacherId 16 | title: str 17 | 18 | 19 | @dataclass(slots=True) 20 | class CreateSubject: 21 | gateway: SubjectGateway 22 | transaction_manager: TransactionManager 23 | idp: IdProvider 24 | 25 | def execute(self, data: NewSubject) -> SubjectId: 26 | self.idp.ensure_authenticated() 27 | validate_subject_invariants(data.title) 28 | 29 | subject_id = SubjectId(uuid4()) 30 | subject = Subject( 31 | subject_id=subject_id, 32 | title=data.title, 33 | teacher_id=data.teacher_id, 34 | ) 35 | 36 | with self.transaction_manager.begin(): 37 | self.gateway.write_subject(subject) 38 | self.transaction_manager.commit() 39 | 40 | return subject_id 41 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/di/db_provider.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from sqlite3 import Connection, Cursor 3 | 4 | from dishka import Provider, Scope, provide 5 | 6 | from student_journal.adapters.db.connection_factory import SQLiteConnectionFactory 7 | from student_journal.adapters.db.connection_maker import SQLiteConnectionMaker 8 | from student_journal.adapters.db.schema.load_schema import load_and_execute 9 | from student_journal.adapters.db.transaction_manager import SQLiteTransactionManager 10 | from student_journal.application.common.transaction_manager import TransactionManager 11 | 12 | 13 | class DbProvider(Provider): 14 | scope = Scope.REQUEST 15 | 16 | connection_maker = provide(SQLiteConnectionMaker, scope=Scope.APP) 17 | transaction_manager = provide(SQLiteTransactionManager, provides=TransactionManager) 18 | 19 | @provide(scope=Scope.APP) 20 | def connection_factory( 21 | self, 22 | maker: SQLiteConnectionMaker, 23 | ) -> SQLiteConnectionFactory: 24 | factory = SQLiteConnectionFactory(connection_maker=maker) 25 | 26 | with factory.connection() as conn: 27 | load_and_execute(conn.cursor()) 28 | 29 | return factory 30 | 31 | @provide() 32 | def connection(self, factory: SQLiteConnectionFactory) -> Iterable[Connection]: 33 | with factory.connection() as conn: 34 | yield conn 35 | 36 | @provide() 37 | def cursor(self, conn: Connection) -> Cursor: 38 | return conn.cursor() 39 | -------------------------------------------------------------------------------- /src/student_journal/application/common/lesson_gateway.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from datetime import date, timezone 3 | from typing import Protocol 4 | 5 | from student_journal.application.models.lesson import LessonsByDate, WeekLessons 6 | from student_journal.domain.lesson import Lesson 7 | from student_journal.domain.subject import Subject 8 | from student_journal.domain.value_object.lesson_id import LessonId 9 | 10 | 11 | class LessonGateway(Protocol): 12 | @abstractmethod 13 | def read_lesson(self, lesson_id: LessonId, as_tz: timezone) -> Lesson: ... 14 | 15 | @abstractmethod 16 | def write_lesson(self, lesson: Lesson) -> None: ... 17 | 18 | @abstractmethod 19 | def update_lesson(self, lesson: Lesson) -> None: ... 20 | 21 | @abstractmethod 22 | def delete_lesson(self, lesson_id: LessonId) -> None: ... 23 | 24 | @abstractmethod 25 | def read_lessons_for_week( 26 | self, 27 | week_start: date, 28 | as_tz: timezone, 29 | ) -> WeekLessons: ... 30 | 31 | @abstractmethod 32 | def read_first_lessons_of_weeks( 33 | self, 34 | month: int, 35 | year: int, 36 | as_tz: timezone, 37 | ) -> LessonsByDate: ... 38 | 39 | @abstractmethod 40 | def read_subjects_for_lessons( 41 | self, 42 | lessons: list[LessonId], 43 | ) -> dict[LessonId, Subject]: ... 44 | 45 | @abstractmethod 46 | def delete_lessons(self, lessons: list[LessonId]) -> None: ... 47 | 48 | @abstractmethod 49 | def delete_all_lessons(self) -> None: ... 50 | -------------------------------------------------------------------------------- /tests/unit/student/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.student_gateway import StudentGateway 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.application.student.create_student import CreateStudent 7 | from student_journal.application.student.read_current_student import ReadCurrentStudent 8 | from student_journal.application.student.update_student import UpdateStudent 9 | from unit.student.mock.student_gateway import MockedStudentGateway 10 | 11 | 12 | @pytest.fixture 13 | def student_gateway() -> MockedStudentGateway: 14 | return MockedStudentGateway() 15 | 16 | 17 | @pytest.fixture 18 | def create_student( 19 | transaction_manager: TransactionManager, 20 | student_gateway: StudentGateway, 21 | ) -> CreateStudent: 22 | return CreateStudent( 23 | transaction_manager=transaction_manager, 24 | gateway=student_gateway, 25 | ) 26 | 27 | 28 | @pytest.fixture 29 | def read_student( 30 | student_gateway: StudentGateway, 31 | idp: IdProvider, 32 | ) -> ReadCurrentStudent: 33 | return ReadCurrentStudent( 34 | gateway=student_gateway, 35 | idp=idp, 36 | ) 37 | 38 | 39 | @pytest.fixture 40 | def update_student( 41 | student_gateway: StudentGateway, 42 | idp: IdProvider, 43 | transaction_manager: TransactionManager, 44 | ) -> UpdateStudent: 45 | return UpdateStudent( 46 | gateway=student_gateway, 47 | idp=idp, 48 | transaction_manager=transaction_manager, 49 | ) 50 | -------------------------------------------------------------------------------- /src/student_journal/application/hometask/update_home_task.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.home_task_gateway import HomeTaskGateway 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.application.invariants.home_task import ( 7 | validate_home_task_invariants, 8 | ) 9 | from student_journal.domain.home_task import HomeTask 10 | from student_journal.domain.value_object.lesson_id import LessonId 11 | from student_journal.domain.value_object.task_id import HomeTaskId 12 | 13 | 14 | @dataclass(slots=True, frozen=True) 15 | class UpdatedHomeTask: 16 | task_id: HomeTaskId 17 | lesson_id: LessonId 18 | description: str 19 | is_done: bool = False 20 | 21 | 22 | @dataclass(slots=True) 23 | class UpdateHomeTask: 24 | gateway: HomeTaskGateway 25 | transaction_manager: TransactionManager 26 | idp: IdProvider 27 | 28 | def execute(self, data: UpdatedHomeTask) -> HomeTaskId: 29 | self.idp.ensure_authenticated() 30 | 31 | validate_home_task_invariants( 32 | description=data.description, 33 | ) 34 | 35 | home_task = HomeTask( 36 | task_id=data.task_id, 37 | lesson_id=data.lesson_id, 38 | description=data.description, 39 | is_done=data.is_done, 40 | ) 41 | 42 | with self.transaction_manager.begin(): 43 | self.gateway.update_home_task(home_task) 44 | self.transaction_manager.commit() 45 | 46 | return data.task_id 47 | -------------------------------------------------------------------------------- /src/student_journal/application/hometask/create_home_task.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from uuid import uuid4 3 | 4 | from student_journal.application.common.home_task_gateway import HomeTaskGateway 5 | from student_journal.application.common.id_provider import IdProvider 6 | from student_journal.application.common.transaction_manager import TransactionManager 7 | from student_journal.application.invariants.home_task import ( 8 | validate_home_task_invariants, 9 | ) 10 | from student_journal.domain.home_task import HomeTask 11 | from student_journal.domain.value_object.lesson_id import LessonId 12 | from student_journal.domain.value_object.task_id import HomeTaskId 13 | 14 | 15 | @dataclass(slots=True, frozen=True) 16 | class NewHomeTask: 17 | lesson_id: LessonId 18 | description: str 19 | is_done: bool = False 20 | 21 | 22 | @dataclass(slots=True) 23 | class CreateHomeTask: 24 | gateway: HomeTaskGateway 25 | transaction_manager: TransactionManager 26 | idp: IdProvider 27 | 28 | def execute(self, data: NewHomeTask) -> HomeTaskId: 29 | self.idp.ensure_authenticated() 30 | 31 | validate_home_task_invariants( 32 | description=data.description, 33 | ) 34 | task_id = HomeTaskId(uuid4()) 35 | home_task = HomeTask( 36 | task_id=task_id, 37 | lesson_id=data.lesson_id, 38 | description=data.description, 39 | is_done=data.is_done, 40 | ) 41 | 42 | with self.transaction_manager.begin(): 43 | self.gateway.write_home_task(home_task) 44 | self.transaction_manager.commit() 45 | 46 | return task_id 47 | -------------------------------------------------------------------------------- /src/student_journal/application/student/update_student.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from student_journal.application.common.id_provider import IdProvider 4 | from student_journal.application.common.student_gateway import StudentGateway 5 | from student_journal.application.common.transaction_manager import TransactionManager 6 | from student_journal.application.invariants.student import validate_student_invariants 7 | from student_journal.domain.student import Student 8 | from student_journal.domain.value_object.student_id import StudentId 9 | 10 | 11 | @dataclass(slots=True, frozen=True) 12 | class UpdatedStudent: 13 | age: int | None 14 | avatar: str | None 15 | name: str 16 | home_address: str | None 17 | 18 | 19 | @dataclass(slots=True) 20 | class UpdateStudent: 21 | gateway: StudentGateway 22 | transaction_manager: TransactionManager 23 | idp: IdProvider 24 | 25 | def execute(self, data: UpdatedStudent) -> StudentId: 26 | student = self.gateway.read_student(self.idp.get_id()) 27 | 28 | validate_student_invariants( 29 | age=data.age, 30 | name=data.name, 31 | home_address=data.home_address, 32 | avatar=data.avatar, 33 | ) 34 | 35 | student = Student( 36 | student_id=self.idp.get_id(), 37 | avatar=data.avatar, 38 | age=data.age, 39 | name=data.name, 40 | home_address=data.home_address, 41 | utc_offset=student.utc_offset, 42 | ) 43 | 44 | with self.transaction_manager.begin(): 45 | self.gateway.update_student(student) 46 | self.transaction_manager.commit() 47 | 48 | return student.student_id 49 | -------------------------------------------------------------------------------- /src/student_journal/application/student/create_student.py: -------------------------------------------------------------------------------- 1 | import time 2 | from dataclasses import dataclass 3 | from uuid import uuid4 4 | 5 | from student_journal.application.common.student_gateway import StudentGateway 6 | from student_journal.application.common.transaction_manager import TransactionManager 7 | from student_journal.application.invariants.student import validate_student_invariants 8 | from student_journal.domain.student import Student 9 | from student_journal.domain.value_object.student_id import StudentId 10 | 11 | 12 | @dataclass(slots=True, frozen=True) 13 | class NewStudent: 14 | age: int | None 15 | avatar: str | None 16 | name: str 17 | home_address: str | None 18 | 19 | 20 | @dataclass(slots=True) 21 | class CreateStudent: 22 | gateway: StudentGateway 23 | transaction_manager: TransactionManager 24 | 25 | def execute(self, data: NewStudent) -> StudentId: 26 | utc_offset = ( 27 | -time.timezone if not time.localtime().tm_isdst else -time.altzone 28 | ) # get system utc offset 29 | 30 | utc_offset //= 3600 31 | 32 | validate_student_invariants( 33 | age=data.age, 34 | name=data.name, 35 | home_address=data.home_address, 36 | avatar=data.avatar, 37 | ) 38 | 39 | student_id = StudentId(uuid4()) 40 | student = Student( 41 | student_id=student_id, 42 | avatar=data.avatar, 43 | age=data.age, 44 | name=data.name, 45 | home_address=data.home_address, 46 | utc_offset=utc_offset, 47 | ) 48 | 49 | with self.transaction_manager.begin(): 50 | self.gateway.write_student(student) 51 | self.transaction_manager.commit() 52 | 53 | return student_id 54 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/main_window.py: -------------------------------------------------------------------------------- 1 | from dishka import Container 2 | from PyQt6.QtWidgets import QMainWindow, QStackedWidget 3 | 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.exceptions.student import ( 6 | StudentIsNotAuthenticatedError, 7 | ) 8 | from student_journal.presentation.widget.dashboard import Dashboard 9 | from student_journal.presentation.widget.student.register import Register 10 | 11 | 12 | class MainWindow(QMainWindow): 13 | def __init__(self, container: Container) -> None: 14 | super().__init__() 15 | 16 | self.container = container 17 | 18 | with self.container() as r_container: 19 | self.idp: IdProvider = r_container.get(IdProvider) 20 | self.dashboard: None | Dashboard = None 21 | 22 | self.register_form = Register(container) 23 | self.register_form.finish.connect(self.finish_register) 24 | 25 | self.stacked_widget = QStackedWidget() 26 | self.stacked_widget.addWidget(self.register_form) 27 | 28 | self.setCentralWidget(self.stacked_widget) 29 | 30 | try: 31 | self.idp.ensure_authenticated() 32 | except StudentIsNotAuthenticatedError: 33 | self.display_register() 34 | return 35 | else: 36 | self.display_dashboard() 37 | 38 | def display_register(self) -> None: 39 | self.stacked_widget.setCurrentWidget(self.register_form) 40 | 41 | def display_dashboard(self) -> None: 42 | self.dashboard = Dashboard(self.container) 43 | self.stacked_widget.addWidget(self.dashboard) 44 | self.stacked_widget.setCurrentWidget(self.dashboard) 45 | 46 | def finish_register(self) -> None: 47 | self.display_dashboard() 48 | -------------------------------------------------------------------------------- /resources/forms/subject_list.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SubjectList 4 | 5 | 6 | 7 | 0 8 | 0 9 | 580 10 | 290 11 | 12 | 13 | 14 | Список предметов 15 | 16 | 17 | 18 | 19 | 20 | false 21 | 22 | 23 | false 24 | 25 | 26 | 27 | 28 | 29 | 30 | Добавить новые 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 0 39 | 0 40 | 41 | 42 | 43 | 44 | 18 45 | true 46 | 47 | 48 | 49 | Список предметов 50 | 51 | 52 | 53 | 54 | 55 | 56 | Обновить 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /resources/forms/teacher_list.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TeacherList 4 | 5 | 6 | 7 | 0 8 | 0 9 | 580 10 | 290 11 | 12 | 13 | 14 | Список учителей 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | 27 | 18 28 | true 29 | 30 | 31 | 32 | Список учителей 33 | 34 | 35 | 36 | 37 | 38 | 39 | Добавить новых 40 | 41 | 42 | 43 | 44 | 45 | 46 | false 47 | 48 | 49 | false 50 | 51 | 52 | 53 | 54 | 55 | 56 | Обновить 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/update_lesson.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | 4 | from student_journal.application.common.id_provider import IdProvider 5 | from student_journal.application.common.lesson_gateway import LessonGateway 6 | from student_journal.application.common.student_gateway import StudentGateway 7 | from student_journal.application.common.transaction_manager import TransactionManager 8 | from student_journal.application.invariants.lesson import validate_lesson_invariants 9 | from student_journal.domain.lesson import Lesson 10 | from student_journal.domain.value_object.lesson_id import LessonId 11 | from student_journal.domain.value_object.subject_id import SubjectId 12 | 13 | 14 | @dataclass(slots=True, frozen=True) 15 | class UpdatedLesson: 16 | lesson_id: LessonId 17 | subject_id: SubjectId 18 | at: datetime 19 | mark: int | None 20 | note: str | None 21 | room: int 22 | 23 | 24 | @dataclass(slots=True) 25 | class UpdateLesson: 26 | gateway: LessonGateway 27 | student_gateway: StudentGateway 28 | transaction_manager: TransactionManager 29 | idp: IdProvider 30 | 31 | def execute(self, data: UpdatedLesson) -> LessonId: 32 | student = self.student_gateway.read_student(self.idp.get_id()) 33 | local_at = data.at.replace(tzinfo=student.get_timezone()) 34 | 35 | validate_lesson_invariants( 36 | mark=data.mark, 37 | note=data.note, 38 | room=data.room, 39 | ) 40 | 41 | lesson = Lesson( 42 | lesson_id=data.lesson_id, 43 | subject_id=data.subject_id, 44 | at=local_at, 45 | mark=data.mark, 46 | note=data.note, 47 | room=data.room, 48 | ) 49 | 50 | with self.transaction_manager.begin(): 51 | self.gateway.update_lesson(lesson) 52 | self.transaction_manager.commit() 53 | 54 | return data.lesson_id 55 | -------------------------------------------------------------------------------- /tests/gateway/student/test_student_gateway.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | from unit.conftest import STUDENT, STUDENT_ID 5 | 6 | from student_journal.adapters.converter.student import student_retort 7 | from student_journal.application.common.student_gateway import StudentGateway 8 | from student_journal.application.exceptions.student import StudentNotFoundError 9 | from student_journal.domain.student import Student 10 | 11 | READ_STUDENT_SQL = "SELECT * FROM Student" 12 | 13 | 14 | def test_write( 15 | student_gateway: StudentGateway, 16 | cursor: Cursor, 17 | ) -> None: 18 | student_gateway.write_student(STUDENT) 19 | db_student = student_retort.load( 20 | dict(cursor.execute(READ_STUDENT_SQL).fetchone()), 21 | Student, 22 | ) 23 | 24 | assert db_student == STUDENT 25 | 26 | 27 | def test_read( 28 | student_gateway: StudentGateway, 29 | cursor: Cursor, 30 | ) -> None: 31 | student_gateway.write_student(STUDENT) 32 | db_student = student_retort.load( 33 | dict(cursor.execute(READ_STUDENT_SQL).fetchone()), 34 | Student, 35 | ) 36 | 37 | assert db_student == student_gateway.read_student(STUDENT_ID) 38 | 39 | 40 | def test_read_not_exist( 41 | student_gateway: StudentGateway, 42 | ) -> None: 43 | with pytest.raises(StudentNotFoundError): 44 | student_gateway.read_student(STUDENT_ID) 45 | 46 | 47 | def test_update( 48 | student_gateway: StudentGateway, 49 | cursor: Cursor, 50 | ) -> None: 51 | student_gateway.write_student(STUDENT) 52 | updated_student = Student( 53 | name="abracadabra", 54 | age=75, 55 | home_address=None, 56 | avatar=None, 57 | student_id=STUDENT_ID, 58 | ) 59 | 60 | student_gateway.update_student(updated_student) 61 | db_student = student_retort.load( 62 | dict(cursor.execute(READ_STUDENT_SQL).fetchone()), 63 | Student, 64 | ) 65 | 66 | assert db_student == updated_student 67 | -------------------------------------------------------------------------------- /src/student_journal/application/lesson/create_lesson.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from uuid import uuid4 4 | 5 | from student_journal.application.common.id_provider import IdProvider 6 | from student_journal.application.common.lesson_gateway import LessonGateway 7 | from student_journal.application.common.student_gateway import StudentGateway 8 | from student_journal.application.common.transaction_manager import TransactionManager 9 | from student_journal.application.invariants.lesson import validate_lesson_invariants 10 | from student_journal.domain.lesson import Lesson 11 | from student_journal.domain.value_object.lesson_id import LessonId 12 | from student_journal.domain.value_object.subject_id import SubjectId 13 | 14 | 15 | @dataclass(slots=True, frozen=True) 16 | class NewLesson: 17 | subject_id: SubjectId 18 | at: datetime 19 | mark: int | None 20 | note: str | None 21 | room: int = 1 22 | 23 | 24 | @dataclass(slots=True) 25 | class CreateLesson: 26 | gateway: LessonGateway 27 | student_gateway: StudentGateway 28 | transaction_manager: TransactionManager 29 | idp: IdProvider 30 | 31 | def execute(self, data: NewLesson) -> LessonId: 32 | student = self.student_gateway.read_student(self.idp.get_id()) 33 | 34 | local_at = data.at.replace(tzinfo=student.get_timezone()) 35 | 36 | validate_lesson_invariants( 37 | mark=data.mark, 38 | note=data.note, 39 | room=data.room, 40 | ) 41 | 42 | lesson_id = LessonId(uuid4()) 43 | lesson = Lesson( 44 | lesson_id=lesson_id, 45 | subject_id=data.subject_id, 46 | at=local_at, 47 | mark=data.mark, 48 | note=data.note, 49 | room=data.room, 50 | ) 51 | 52 | with self.transaction_manager.begin(): 53 | self.gateway.write_lesson(lesson) 54 | self.transaction_manager.commit() 55 | 56 | return lesson_id 57 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/gateway/student_gateway.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from sqlite3 import Cursor 3 | 4 | from student_journal.adapters.converter.student import ( 5 | student_retort, 6 | student_to_list_retort, 7 | ) 8 | from student_journal.application.common.student_gateway import StudentGateway 9 | from student_journal.application.exceptions.student import StudentNotFoundError 10 | from student_journal.domain.student import Student 11 | from student_journal.domain.value_object.student_id import StudentId 12 | 13 | 14 | @dataclass(slots=True, frozen=True) 15 | class SQLiteStudentGateway(StudentGateway): 16 | cursor: Cursor 17 | 18 | def read_student(self, student_id: StudentId) -> Student: 19 | query = ( 20 | "SELECT student_id, age, avatar, name, home_address " 21 | "FROM Student WHERE student_id = ?" 22 | ) 23 | res = self.cursor.execute(query, (str(student_id),)).fetchone() 24 | 25 | if res is None: 26 | raise StudentNotFoundError 27 | 28 | student = student_retort.load(dict(res), Student) 29 | 30 | return student 31 | 32 | def write_student(self, student: Student) -> None: 33 | query = ( 34 | "INSERT INTO Student " 35 | "(student_id, age, avatar, name, home_address) " 36 | "VALUES " 37 | "(?, ?, ?, ?, ?)" 38 | ) 39 | params = student_to_list_retort.dump(student) 40 | self.cursor.execute(query, params) 41 | 42 | def update_student(self, student: Student) -> None: 43 | query = ( 44 | "UPDATE Student " 45 | "SET age = ?, avatar = ?, name = ?, home_address = ? " 46 | "WHERE student_id = ?" 47 | ) 48 | params = student_to_list_retort.dump(student) 49 | params.append(params.pop(0)) 50 | 51 | self.cursor.execute(query, params) 52 | 53 | def get_overall_avg_mark(self) -> float: 54 | query = "SELECT avg(mark) FROM Lesson" 55 | res = self.cursor.execute(query).fetchone() 56 | 57 | if not res or not res[0]: 58 | return 0.0 59 | 60 | return res[0] # type: ignore 61 | -------------------------------------------------------------------------------- /tests/gateway/home_task/test_home_task_gateway.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | from unit.conftest import HOME_TASK, LESSON_ID, TASK_ID 5 | 6 | from student_journal.adapters.converter.home_task import ( 7 | home_task_retort, 8 | ) 9 | from student_journal.application.common.home_task_gateway import HomeTaskGateway 10 | from student_journal.application.exceptions.home_task import HomeTaskNotFoundError 11 | from student_journal.domain.home_task import HomeTask 12 | 13 | READ_HOME_TASK_SQL = "SELECT * FROM Hometask" 14 | 15 | 16 | def test_write( 17 | home_task_gateway: HomeTaskGateway, 18 | cursor: Cursor, 19 | ) -> None: 20 | home_task_gateway.write_home_task(HOME_TASK) 21 | db_home_task = home_task_retort.load( 22 | dict(cursor.execute(READ_HOME_TASK_SQL).fetchone()), 23 | HomeTask, 24 | ) 25 | assert db_home_task == HOME_TASK 26 | 27 | 28 | def test_read( 29 | home_task_gateway: HomeTaskGateway, 30 | cursor: Cursor, 31 | ) -> None: 32 | home_task_gateway.write_home_task(HOME_TASK) 33 | db_home_task = home_task_retort.load( 34 | dict(cursor.execute(READ_HOME_TASK_SQL).fetchone()), 35 | HomeTask, 36 | ) 37 | 38 | assert db_home_task == home_task_gateway.read_home_task(TASK_ID) 39 | 40 | 41 | def test_read_not_exist( 42 | home_task_gateway: HomeTaskGateway, 43 | ) -> None: 44 | with pytest.raises(HomeTaskNotFoundError): 45 | home_task_gateway.read_home_task(TASK_ID) 46 | 47 | 48 | def test_update( 49 | home_task_gateway: HomeTaskGateway, 50 | cursor: Cursor, 51 | ) -> None: 52 | home_task_gateway.write_home_task(HOME_TASK) 53 | updated_home_task = HomeTask( 54 | task_id=TASK_ID, 55 | lesson_id=LESSON_ID, 56 | description="testtest22222", 57 | is_done=True, 58 | ) 59 | 60 | home_task_gateway.update_home_task(updated_home_task) 61 | home_task_dict = dict(cursor.execute(READ_HOME_TASK_SQL).fetchone()) 62 | home_task_dict["is_done"] = bool(home_task_dict["is_done"]) 63 | db_home_task = home_task_retort.load( 64 | home_task_dict, 65 | HomeTask, 66 | ) 67 | assert db_home_task == updated_home_task 68 | -------------------------------------------------------------------------------- /resources/forms/hometask_list.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | HometaskList 4 | 5 | 6 | 7 | 0 8 | 0 9 | 580 10 | 290 11 | 12 | 13 | 14 | Список ДЗ 15 | 16 | 17 | 18 | 19 | 20 | Обновить 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Показать выполненные 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 0 47 | 0 48 | 49 | 50 | 51 | 52 | 18 53 | true 54 | 55 | 56 | 57 | Предстоящие задания 58 | 59 | 60 | 61 | 62 | 63 | 64 | false 65 | 66 | 67 | false 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/student_journal/adapters/id_provider.py: -------------------------------------------------------------------------------- 1 | import tomllib 2 | from dataclasses import dataclass 3 | from pathlib import Path 4 | from uuid import UUID 5 | 6 | import tomli_w 7 | 8 | from student_journal.application.common.id_provider import IdProvider 9 | from student_journal.application.common.student_gateway import StudentGateway 10 | from student_journal.application.exceptions.student import ( 11 | StudentIsNotAuthenticatedError, 12 | StudentNotFoundError, 13 | ) 14 | from student_journal.domain.value_object.student_id import StudentId 15 | 16 | 17 | @dataclass(slots=True, frozen=True) 18 | class CredentialsConfig: 19 | path: Path 20 | 21 | 22 | @dataclass(slots=True, frozen=True) 23 | class SimpleIdProvider(IdProvider): 24 | student_id: StudentId 25 | 26 | def get_id(self) -> StudentId: 27 | return self.student_id 28 | 29 | def ensure_authenticated(self) -> None: ... 30 | 31 | 32 | @dataclass(slots=True, frozen=True) 33 | class FileIdProvider(IdProvider): 34 | config: CredentialsConfig 35 | gateway: StudentGateway 36 | 37 | def get_id(self) -> StudentId: 38 | if not self.config.path.exists() or not self.config.path.is_file(): 39 | raise StudentIsNotAuthenticatedError from FileNotFoundError 40 | 41 | with self.config.path.open("rb") as f: 42 | try: 43 | data = tomllib.load(f) 44 | student_id = StudentId(UUID(data["auth"]["student_id"])) 45 | self.gateway.read_student(student_id) 46 | except ( 47 | ValueError, 48 | tomllib.TOMLDecodeError, 49 | KeyError, 50 | StudentNotFoundError, 51 | ) as e: 52 | raise StudentIsNotAuthenticatedError from e 53 | else: 54 | return student_id 55 | 56 | def save(self, student_id: StudentId) -> None: 57 | with self.config.path.open("wb") as f: 58 | tomli_w.dump( 59 | { 60 | "auth": { 61 | "student_id": student_id.hex, 62 | }, 63 | }, 64 | f, 65 | ) 66 | 67 | def ensure_authenticated(self) -> None: 68 | self.get_id() 69 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/subject/subject_list.py: -------------------------------------------------------------------------------- 1 | from dishka import Container 2 | from PyQt6.QtWidgets import QListWidgetItem, QWidget 3 | 4 | from student_journal.adapters.error_locator import ErrorLocator 5 | from student_journal.application.subject.read_subject import ReadSubject 6 | from student_journal.application.subject.read_subjects import ReadSubjects 7 | from student_journal.presentation.ui.subject_list_ui import Ui_SubjectList 8 | from student_journal.presentation.widget.subject.edit_subject import EditSubject 9 | 10 | 11 | class SubjectList(QWidget): 12 | def __init__(self, container: Container) -> None: 13 | super().__init__() 14 | 15 | self.container = container 16 | self.error_locator = container.get(ErrorLocator) 17 | self.current_widget: None | EditSubject = None 18 | 19 | self.ui = Ui_SubjectList() 20 | self.ui.setupUi(self) 21 | 22 | self.ui.refresh.clicked.connect(self.on_refresh) 23 | self.ui.add_more.clicked.connect(self.on_add_more) 24 | self.ui.list_subject.itemDoubleClicked.connect(self.on_item_double_clicked) 25 | 26 | self.load_subjects() 27 | 28 | def load_subjects(self) -> None: 29 | self.ui.list_subject.clear() 30 | with self.container() as r_container: 31 | command = r_container.get(ReadSubjects) 32 | subjects = command.execute() 33 | 34 | for subject in subjects: 35 | item = QListWidgetItem(subject.title) 36 | item.setData(0x100, subject.subject_id) 37 | self.ui.list_subject.addItem(item) 38 | 39 | def on_refresh(self) -> None: 40 | self.load_subjects() 41 | 42 | def on_add_more(self) -> None: 43 | form = EditSubject(self.container, subject_id=None) 44 | self.current_widget = form 45 | form.show() 46 | 47 | def on_item_double_clicked(self, item: QListWidgetItem) -> None: 48 | subject_id = item.data(0x100) 49 | 50 | with self.container() as r_container: 51 | command = r_container.get(ReadSubject) 52 | subject = command.execute(subject_id) 53 | 54 | form = EditSubject(self.container, subject_id=subject.subject_id) 55 | self.current_widget = form 56 | form.show() 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | 'setuptools==68.1.2', 4 | ] 5 | build-backend = 'setuptools.build_meta' 6 | 7 | [project] 8 | name = 'student_journal' 9 | version = '1.0.0' 10 | description = 'Student journal project' 11 | readme = 'README.md' 12 | requires-python = '>=3.11' 13 | dependencies = [ 14 | 'adaptix==3.0.0b8', 15 | 'PyQt6==6.7.1', 16 | 'dishka==1.4.0', 17 | 'tomli-w==1.1.0', 18 | 'faker==33.1.0', 19 | ] 20 | 21 | [project.optional-dependencies] 22 | test = [ 23 | 'pytest==8.3.3', 24 | ] 25 | lint = [ 26 | 'pre-commit==3.8.0', 27 | 'ruff==0.8.0', 28 | 'mypy==1.13.0', 29 | ] 30 | ci = [ 31 | 'mypy==1.13.0', 32 | 'pytest==8.3.3', 33 | 'ruff==0.8.0', 34 | ] 35 | build = [ 36 | 'pyinstaller==6.11.1', 37 | ] 38 | 39 | [tool.setuptools.packages.find] 40 | where = ["src"] 41 | 42 | [tool.setuptools] 43 | include-package-data = true 44 | 45 | [tool.pytest.ini_options] 46 | testpaths = ["tests"] 47 | filterwarnings = "ignore::DeprecationWarning" 48 | 49 | [tool.mypy] 50 | strict = true 51 | warn_unreachable = true 52 | show_column_numbers = true 53 | show_error_context = true 54 | check_untyped_defs = true 55 | ignore_missing_imports = false 56 | warn_no_return = true 57 | disallow_untyped_calls = false 58 | 59 | files = ["src/"] 60 | exclude = ["src/student_journal/presentation/ui/.*\\.py"] 61 | 62 | [tool.ruff] 63 | line-length = 88 64 | include = ["pyproject.toml", "src/**/*.py", "tests/**/*.py"] 65 | exclude = ["src/student_journal/presentation/ui/**/*.py"] 66 | 67 | [tool.ruff.lint] 68 | select = ['ALL'] 69 | 70 | ignore = [ 71 | # Strange and obscure 72 | 'D100', 73 | 'D104', 74 | 'D101', 75 | 'D102', 76 | 'RET504', 77 | 'D103', 78 | 'PLR0913', 79 | 'S101', 80 | 'EM101', 81 | 'TRY003', 82 | 'D107', 83 | 'ARG002', 84 | 'RUF001', 85 | 'TC003', 86 | 'PLR0912', 87 | 'C901', 88 | # Not applicable for now 89 | 'N802', 90 | 'PGH003', 91 | # Does not work correctly 92 | 'TC002', 93 | 'TC001', 94 | ] 95 | 96 | [[project.authors]] 97 | name = 'lubaskinc0de' 98 | email = 'lubaskincorporation@gmail.com' 99 | 100 | [project.scripts] 101 | student_journal = "student_journal.bootstrap.cli:main" -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/teacher/teacher_list.py: -------------------------------------------------------------------------------- 1 | from dishka import Container 2 | from PyQt6.QtWidgets import QListWidgetItem, QWidget 3 | 4 | from student_journal.adapters.error_locator import ErrorLocator 5 | from student_journal.application.teacher.read_teacher import ReadTeacher 6 | from student_journal.application.teacher.read_teachers import ReadTeachers 7 | from student_journal.presentation.ui.teacher_list_ui import Ui_TeacherList 8 | from student_journal.presentation.widget.teacher.edit_teacher import EditTeacher 9 | 10 | 11 | class TeacherList(QWidget): 12 | def __init__(self, container: Container) -> None: 13 | super().__init__() 14 | 15 | self.container = container 16 | self.error_locator = container.get(ErrorLocator) 17 | self.current_widget: None | EditTeacher = None 18 | 19 | self.ui = Ui_TeacherList() 20 | self.ui.setupUi(self) 21 | 22 | self.ui.refresh.clicked.connect(self.on_refresh) 23 | self.ui.add_more.clicked.connect(self.on_add_more) 24 | self.ui.list_teacher.itemDoubleClicked.connect(self.on_item_double_clicked) 25 | 26 | self.load_teachers() 27 | 28 | def load_teachers(self) -> None: 29 | self.ui.list_teacher.clear() 30 | with self.container() as r_container: 31 | command = r_container.get(ReadTeachers) 32 | teachers = command.execute().teachers 33 | 34 | for teacher in teachers: 35 | item = QListWidgetItem(f"{teacher.full_name}") 36 | item.setData(0x100, teacher.teacher_id) 37 | self.ui.list_teacher.addItem(item) 38 | 39 | def on_refresh(self) -> None: 40 | self.load_teachers() 41 | 42 | def on_add_more(self) -> None: 43 | form = EditTeacher(self.container, teacher_id=None) 44 | self.current_widget = form 45 | self.current_widget.show() 46 | 47 | def on_item_double_clicked(self, item: QListWidgetItem) -> None: 48 | teacher_id = item.data(0x100) 49 | 50 | with self.container() as r_container: 51 | command = r_container.get(ReadTeacher) 52 | teacher = command.execute(teacher_id) 53 | 54 | form = EditTeacher(self.container, teacher_id=teacher.teacher_id) 55 | self.current_widget = form 56 | self.current_widget.show() 57 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/gateway/teacher_gateway.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from sqlite3 import Cursor 3 | 4 | from student_journal.adapters.converter import ( 5 | teacher_retort, 6 | teacher_to_list_retort, 7 | ) 8 | from student_journal.application.common.teacher_gateway import TeacherGateway 9 | from student_journal.application.exceptions.teacher import TeacherNotFoundError 10 | from student_journal.domain.teacher import Teacher 11 | from student_journal.domain.value_object.teacher_id import TeacherId 12 | 13 | 14 | @dataclass(slots=True, frozen=True) 15 | class SQLiteTeacherGateway(TeacherGateway): 16 | cursor: Cursor 17 | 18 | def read_teacher(self, teacher_id: TeacherId) -> Teacher: 19 | query = """ 20 | SELECT teacher_id, full_name, avatar 21 | FROM Teacher WHERE teacher_id = ? 22 | """ 23 | res = self.cursor.execute(query, (str(teacher_id),)).fetchone() 24 | 25 | if res is None: 26 | raise TeacherNotFoundError 27 | 28 | teacher = teacher_retort.load(dict(res), Teacher) 29 | 30 | return teacher 31 | 32 | def write_teacher(self, teacher: Teacher) -> None: 33 | query = """ 34 | INSERT INTO Teacher 35 | (teacher_id, full_name, avatar) 36 | VALUES (?, ?, ?) 37 | """ 38 | params = teacher_to_list_retort.dump(teacher) 39 | self.cursor.execute(query, params) 40 | 41 | def read_teachers(self) -> list[Teacher]: 42 | query = """ 43 | SELECT teacher_id, full_name, avatar 44 | FROM Teacher 45 | """ 46 | res = self.cursor.execute(query).fetchall() 47 | 48 | teachers = teacher_retort.load([dict(row) for row in res], list[Teacher]) 49 | 50 | return teachers 51 | 52 | def update_teacher(self, teacher: Teacher) -> None: 53 | query = """ 54 | UPDATE Teacher 55 | SET full_name = ?, avatar = ? 56 | WHERE teacher_id = ? 57 | """ 58 | params = teacher_to_list_retort.dump(teacher) 59 | params.append(params.pop(0)) 60 | 61 | self.cursor.execute(query, params) 62 | 63 | def delete_teacher(self, teacher_id: TeacherId) -> None: 64 | query = """ 65 | DELETE FROM Teacher 66 | WHERE teacher_id = ? 67 | """ 68 | 69 | self.cursor.execute(query, (str(teacher_id),)) 70 | -------------------------------------------------------------------------------- /tests/gateway/teacher/test_teacher_gateway.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | from unit.teacher.conftest import TEACHER, TEACHER2, TEACHER_ID 5 | 6 | from student_journal.adapters.converter.teacher import teacher_retort 7 | from student_journal.application.common.teacher_gateway import TeacherGateway 8 | from student_journal.application.exceptions.teacher import TeacherNotFoundError 9 | from student_journal.domain.teacher import Teacher 10 | 11 | READ_TEACHER_SQL = "SELECT * FROM Teacher" 12 | 13 | 14 | def test_write( 15 | teacher_gateway: TeacherGateway, 16 | cursor: Cursor, 17 | ) -> None: 18 | teacher_gateway.write_teacher(TEACHER) 19 | db_teacher = teacher_retort.load( 20 | dict(cursor.execute(READ_TEACHER_SQL).fetchone()), 21 | Teacher, 22 | ) 23 | 24 | assert db_teacher == TEACHER 25 | 26 | 27 | def test_read( 28 | teacher_gateway: TeacherGateway, 29 | cursor: Cursor, 30 | ) -> None: 31 | teacher_gateway.write_teacher(TEACHER) 32 | db_teacher = teacher_retort.load( 33 | dict(cursor.execute(READ_TEACHER_SQL).fetchone()), 34 | Teacher, 35 | ) 36 | 37 | assert db_teacher == teacher_gateway.read_teacher(TEACHER_ID) 38 | 39 | 40 | def test_read_not_exist( 41 | teacher_gateway: TeacherGateway, 42 | ) -> None: 43 | with pytest.raises(TeacherNotFoundError): 44 | teacher_gateway.read_teacher(TEACHER_ID) 45 | 46 | 47 | def test_read_teachers( 48 | teacher_gateway: TeacherGateway, 49 | cursor: Cursor, 50 | ) -> None: 51 | teacher_gateway.write_teacher(TEACHER) 52 | teacher_gateway.write_teacher(TEACHER2) 53 | 54 | teachers_list = [ 55 | dict(teacher) for teacher in cursor.execute(READ_TEACHER_SQL).fetchall() 56 | ] 57 | 58 | db_teachers = teacher_retort.load( 59 | teachers_list, 60 | list[Teacher], 61 | ) 62 | 63 | assert db_teachers == teacher_gateway.read_teachers() 64 | 65 | 66 | def test_update( 67 | teacher_gateway: TeacherGateway, 68 | cursor: Cursor, 69 | ) -> None: 70 | teacher_gateway.write_teacher(TEACHER) 71 | updated_teacher = Teacher( 72 | teacher_id=TEACHER_ID, 73 | full_name="testtest", 74 | avatar=None, 75 | ) 76 | 77 | teacher_gateway.update_teacher(updated_teacher) 78 | db_teacher = teacher_retort.load( 79 | dict(cursor.execute(READ_TEACHER_SQL).fetchone()), 80 | Teacher, 81 | ) 82 | 83 | assert db_teacher == updated_teacher 84 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/subject_list_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'subject_list.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.4.2 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_SubjectList(object): 13 | def setupUi(self, SubjectList): 14 | SubjectList.setObjectName("SubjectList") 15 | self.gridLayout = QtWidgets.QGridLayout(SubjectList) 16 | self.gridLayout.setObjectName("gridLayout") 17 | self.list_subject = QtWidgets.QListWidget(parent=SubjectList) 18 | self.list_subject.setSelectionRectVisible(False) 19 | self.list_subject.setObjectName("list_subject") 20 | self.gridLayout.addWidget(self.list_subject, 2, 0, 1, 1) 21 | self.add_more = QtWidgets.QPushButton(parent=SubjectList) 22 | self.add_more.setObjectName("add_more") 23 | self.gridLayout.addWidget(self.add_more, 3, 0, 1, 1) 24 | self.label = QtWidgets.QLabel(parent=SubjectList) 25 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 26 | sizePolicy.setHorizontalStretch(0) 27 | sizePolicy.setVerticalStretch(0) 28 | sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) 29 | self.label.setSizePolicy(sizePolicy) 30 | font = QtGui.QFont() 31 | font.setPointSize(18) 32 | font.setBold(True) 33 | self.label.setFont(font) 34 | self.label.setObjectName("label") 35 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 36 | self.refresh = QtWidgets.QPushButton(parent=SubjectList) 37 | self.refresh.setObjectName("refresh") 38 | self.gridLayout.addWidget(self.refresh, 1, 0, 1, 1) 39 | 40 | self.retranslateUi(SubjectList) 41 | QtCore.QMetaObject.connectSlotsByName(SubjectList) 42 | 43 | def retranslateUi(self, SubjectList): 44 | _translate = QtCore.QCoreApplication.translate 45 | SubjectList.setWindowTitle(_translate("SubjectList", "Список предметов")) 46 | self.list_subject.setSortingEnabled(False) 47 | self.add_more.setText(_translate("SubjectList", "Добавить новые")) 48 | self.label.setText(_translate("SubjectList", "Список предметов")) 49 | self.refresh.setText(_translate("SubjectList", "Обновить")) 50 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/teacher_list_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'teacher_list.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.4.2 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_TeacherList(object): 13 | def setupUi(self, TeacherList): 14 | TeacherList.setObjectName("TeacherList") 15 | self.gridLayout = QtWidgets.QGridLayout(TeacherList) 16 | self.gridLayout.setObjectName("gridLayout") 17 | self.label = QtWidgets.QLabel(parent=TeacherList) 18 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 19 | sizePolicy.setHorizontalStretch(0) 20 | sizePolicy.setVerticalStretch(0) 21 | sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) 22 | self.label.setSizePolicy(sizePolicy) 23 | font = QtGui.QFont() 24 | font.setPointSize(18) 25 | font.setBold(True) 26 | self.label.setFont(font) 27 | self.label.setObjectName("label") 28 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 29 | self.add_more = QtWidgets.QPushButton(parent=TeacherList) 30 | self.add_more.setObjectName("add_more") 31 | self.gridLayout.addWidget(self.add_more, 3, 0, 1, 1) 32 | self.list_teacher = QtWidgets.QListWidget(parent=TeacherList) 33 | self.list_teacher.setSelectionRectVisible(False) 34 | self.list_teacher.setObjectName("list_teacher") 35 | self.gridLayout.addWidget(self.list_teacher, 2, 0, 1, 1) 36 | self.refresh = QtWidgets.QPushButton(parent=TeacherList) 37 | self.refresh.setObjectName("refresh") 38 | self.gridLayout.addWidget(self.refresh, 1, 0, 1, 1) 39 | 40 | self.retranslateUi(TeacherList) 41 | QtCore.QMetaObject.connectSlotsByName(TeacherList) 42 | 43 | def retranslateUi(self, TeacherList): 44 | _translate = QtCore.QCoreApplication.translate 45 | TeacherList.setWindowTitle(_translate("TeacherList", "Список учителей")) 46 | self.label.setText(_translate("TeacherList", "Список учителей")) 47 | self.add_more.setText(_translate("TeacherList", "Добавить новых")) 48 | self.list_teacher.setSortingEnabled(False) 49 | self.refresh.setText(_translate("TeacherList", "Обновить")) 50 | -------------------------------------------------------------------------------- /tests/unit/teacher/conftest.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | import pytest 4 | 5 | from student_journal.application.common.id_provider import IdProvider 6 | from student_journal.application.teacher import ( 7 | CreateTeacher, 8 | DeleteTeacher, 9 | ReadTeacher, 10 | ReadTeachers, 11 | UpdateTeacher, 12 | ) 13 | from student_journal.domain.teacher import Teacher 14 | from student_journal.domain.value_object.teacher_id import TeacherId 15 | from unit.student.mock import MockedTeacherGateway, MockedTransactionManager 16 | 17 | TEACHER_ID = TeacherId(uuid4()) 18 | TEACHER = Teacher( 19 | teacher_id=TEACHER_ID, 20 | full_name="John Doe", 21 | avatar=None, 22 | ) 23 | TEACHER2_ID = TeacherId(uuid4()) 24 | TEACHER2 = Teacher( 25 | teacher_id=TEACHER2_ID, 26 | full_name="John Not Doe", 27 | avatar=None, 28 | ) 29 | 30 | 31 | @pytest.fixture 32 | def teacher_gateway() -> MockedTeacherGateway: 33 | return MockedTeacherGateway() 34 | 35 | 36 | @pytest.fixture 37 | def create_teacher( 38 | transaction_manager: MockedTransactionManager, 39 | teacher_gateway: MockedTeacherGateway, 40 | idp: IdProvider, 41 | ) -> CreateTeacher: 42 | return CreateTeacher( 43 | transaction_manager=transaction_manager, 44 | gateway=teacher_gateway, 45 | idp=idp, 46 | ) 47 | 48 | 49 | @pytest.fixture 50 | def read_teacher( 51 | teacher_gateway: MockedTeacherGateway, 52 | idp: IdProvider, 53 | ) -> ReadTeacher: 54 | return ReadTeacher( 55 | gateway=teacher_gateway, 56 | idp=idp, 57 | ) 58 | 59 | 60 | @pytest.fixture 61 | def update_teacher( 62 | teacher_gateway: MockedTeacherGateway, 63 | transaction_manager: MockedTransactionManager, 64 | idp: IdProvider, 65 | ) -> UpdateTeacher: 66 | return UpdateTeacher( 67 | gateway=teacher_gateway, 68 | transaction_manager=transaction_manager, 69 | idp=idp, 70 | ) 71 | 72 | 73 | @pytest.fixture 74 | def read_teachers( 75 | teacher_gateway: MockedTeacherGateway, 76 | idp: IdProvider, 77 | ) -> ReadTeachers: 78 | return ReadTeachers( 79 | idp=idp, 80 | gateway=teacher_gateway, 81 | ) 82 | 83 | 84 | @pytest.fixture 85 | def delete_teacher( 86 | teacher_gateway: MockedTeacherGateway, 87 | transaction_manager: MockedTransactionManager, 88 | idp: IdProvider, 89 | ) -> DeleteTeacher: 90 | return DeleteTeacher( 91 | transaction_manager=transaction_manager, 92 | gateway=teacher_gateway, 93 | idp=idp, 94 | ) 95 | -------------------------------------------------------------------------------- /resources/forms/edit_teacher.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EditTeacher 4 | 5 | 6 | Qt::WindowModality::WindowModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 500 13 | 300 14 | 15 | 16 | 17 | 18 | 500 19 | 300 20 | 21 | 22 | 23 | Редактирование учителя 24 | 25 | 26 | 27 | 28 | 29 | Сохранить 30 | 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 38 | 39 | 10 40 | 41 | 42 | 10 43 | 44 | 45 | 46 | 47 | Полное имя 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 0 61 | 0 62 | 63 | 64 | 65 | 66 | 18 67 | true 68 | 69 | 70 | 71 | Редактирование учителя 72 | 73 | 74 | 75 | 76 | 77 | 78 | true 79 | 80 | 81 | Удалить 82 | 83 | 84 | false 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | from uuid import uuid4 3 | 4 | import pytest 5 | from common.mock.transaction_manager import MockedTransactionManager 6 | 7 | from student_journal.adapters.id_provider import SimpleIdProvider 8 | from student_journal.application.common.id_provider import IdProvider 9 | from student_journal.domain.home_task import HomeTask 10 | from student_journal.domain.lesson import Lesson 11 | from student_journal.domain.student import Student 12 | from student_journal.domain.value_object.lesson_id import LessonId 13 | from student_journal.domain.value_object.student_id import StudentId 14 | from student_journal.domain.value_object.subject_id import SubjectId 15 | from student_journal.domain.value_object.task_id import HomeTaskId 16 | 17 | student_timezone = timezone(timedelta(hours=3)) 18 | STUDENT_ID = StudentId(uuid4()) 19 | STUDENT = Student( 20 | student_id=STUDENT_ID, 21 | age=14, 22 | avatar=None, 23 | name="Ilya", 24 | home_address=None, 25 | ) 26 | 27 | SUBJECT_ID = SubjectId(uuid4()) 28 | LESSON_ID = LessonId(uuid4()) 29 | LESSON = Lesson( 30 | lesson_id=LESSON_ID, 31 | subject_id=SUBJECT_ID, 32 | at=datetime(2024, 11, 15, tzinfo=student_timezone), 33 | mark=None, 34 | note=None, 35 | room=5, 36 | ) 37 | 38 | LESSON_MONDAY_ID = LessonId(uuid4()) 39 | LESSON_MONDAY = Lesson( 40 | lesson_id=LESSON_MONDAY_ID, 41 | subject_id=SUBJECT_ID, 42 | at=datetime(2024, 11, 11, hour=8, minute=0, tzinfo=student_timezone), 43 | mark=None, 44 | note=None, 45 | room=5, 46 | ) 47 | 48 | LESSON_WEDNESDAY_ID = LessonId(uuid4()) 49 | LESSON_WEDNESDAY = Lesson( 50 | lesson_id=LESSON_WEDNESDAY_ID, 51 | subject_id=SUBJECT_ID, 52 | at=datetime(2024, 11, 13, hour=8, minute=0, tzinfo=student_timezone), 53 | mark=None, 54 | note=None, 55 | room=5, 56 | ) 57 | 58 | LESSON_MONDAY_2_ID = LessonId(uuid4()) 59 | LESSON_MONDAY_2 = Lesson( 60 | lesson_id=LESSON_MONDAY_2_ID, 61 | subject_id=SUBJECT_ID, 62 | at=datetime(2024, 11, 19, tzinfo=student_timezone), 63 | mark=None, 64 | note=None, 65 | room=5, 66 | ) 67 | 68 | TASK_ID = HomeTaskId(uuid4()) 69 | HOME_TASK = HomeTask( 70 | task_id=TASK_ID, 71 | lesson_id=LESSON_ID, 72 | description="§13 упр 13", 73 | is_done=False, 74 | ) 75 | 76 | TASK_ID_2 = HomeTaskId(uuid4()) 77 | HOME_TASK_2 = HomeTask( 78 | task_id=TASK_ID_2, 79 | lesson_id=LESSON_MONDAY_2_ID, 80 | description="§12 упр 12", 81 | is_done=True, 82 | ) 83 | 84 | 85 | @pytest.fixture 86 | def idp() -> IdProvider: 87 | return SimpleIdProvider(STUDENT_ID) 88 | 89 | 90 | @pytest.fixture 91 | def transaction_manager() -> MockedTransactionManager: 92 | return MockedTransactionManager() 93 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/utils/month_year_picker.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QDate 2 | from PyQt6.QtWidgets import ( 3 | QComboBox, 4 | QDialog, 5 | QLabel, 6 | QPushButton, 7 | QVBoxLayout, 8 | QWidget, 9 | ) 10 | 11 | 12 | class MonthYearPickerDialog(QDialog): 13 | def __init__(self, parent: QWidget | None = None) -> None: 14 | super().__init__(parent) 15 | self.setWindowTitle("Выбор месяца и года") 16 | self.setStyleSheet(""" 17 | QLabel { 18 | font-size: 16px; 19 | } 20 | QComboBox { 21 | font-size: 14px; 22 | padding: 5px; 23 | margin: 5px 0; 24 | } 25 | QPushButton { 26 | font-size: 14px; 27 | background-color: #4CAF50; 28 | color: white; 29 | padding: 5px; 30 | border-radius: 5px; 31 | } 32 | QPushButton:hover { 33 | background-color: #45a049; 34 | } 35 | QPushButton:pressed { 36 | background-color: #39843c; 37 | } 38 | """) 39 | 40 | layout = QVBoxLayout(self) 41 | self.label = QLabel("Выберите месяц и год:", self) 42 | layout.addWidget(self.label) 43 | 44 | self.month_combo = QComboBox(self) 45 | self.month_combo.addItems( 46 | [ 47 | "Январь", 48 | "Февраль", 49 | "Март", 50 | "Апрель", 51 | "Май", 52 | "Июнь", 53 | "Июль", 54 | "Август", 55 | "Сентябрь", 56 | "Октябрь", 57 | "Ноябрь", 58 | "Декабрь", 59 | ], 60 | ) 61 | layout.addWidget(self.month_combo) 62 | 63 | self.year_combo = QComboBox(self) 64 | current_year = QDate.currentDate().year() 65 | self.year_combo.addItems( 66 | [str(year) for year in tuple(range(current_year - 50, current_year + 51))], 67 | ) 68 | self.year_combo.setCurrentText(str(current_year)) 69 | layout.addWidget(self.year_combo) 70 | 71 | self.confirm_button = QPushButton("Выбрать", self) 72 | self.confirm_button.clicked.connect(self.accept_selection) 73 | layout.addWidget(self.confirm_button) 74 | 75 | self.selected_month: int | None = None 76 | self.selected_year: int | None = None 77 | 78 | def accept_selection(self) -> None: 79 | self.selected_month = self.month_combo.currentIndex() + 1 80 | self.selected_year = int(self.year_combo.currentText()) 81 | self.accept() 82 | 83 | def set_initial_selection(self, month: int, year: int) -> None: 84 | self.month_combo.setCurrentIndex(month - 1) 85 | self.year_combo.setCurrentText(str(year)) 86 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/hometask_list_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'hometask_list.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.4.2 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_HometaskList(object): 13 | def setupUi(self, HometaskList): 14 | HometaskList.setObjectName("HometaskList") 15 | self.gridLayout = QtWidgets.QGridLayout(HometaskList) 16 | self.gridLayout.setObjectName("gridLayout") 17 | self.refresh = QtWidgets.QPushButton(parent=HometaskList) 18 | self.refresh.setObjectName("refresh") 19 | self.gridLayout.addWidget(self.refresh, 2, 0, 1, 1) 20 | self.formLayout = QtWidgets.QFormLayout() 21 | self.formLayout.setObjectName("formLayout") 22 | self.label_2 = QtWidgets.QLabel(parent=HometaskList) 23 | self.label_2.setObjectName("label_2") 24 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) 25 | self.show_done = QtWidgets.QCheckBox(parent=HometaskList) 26 | self.show_done.setText("") 27 | self.show_done.setObjectName("show_done") 28 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.show_done) 29 | self.gridLayout.addLayout(self.formLayout, 1, 0, 1, 1) 30 | self.label = QtWidgets.QLabel(parent=HometaskList) 31 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 32 | sizePolicy.setHorizontalStretch(0) 33 | sizePolicy.setVerticalStretch(0) 34 | sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) 35 | self.label.setSizePolicy(sizePolicy) 36 | font = QtGui.QFont() 37 | font.setPointSize(18) 38 | font.setBold(True) 39 | self.label.setFont(font) 40 | self.label.setObjectName("label") 41 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 42 | self.list_hometask = QtWidgets.QListWidget(parent=HometaskList) 43 | self.list_hometask.setSelectionRectVisible(False) 44 | self.list_hometask.setObjectName("list_hometask") 45 | self.gridLayout.addWidget(self.list_hometask, 3, 0, 1, 1) 46 | 47 | self.retranslateUi(HometaskList) 48 | QtCore.QMetaObject.connectSlotsByName(HometaskList) 49 | 50 | def retranslateUi(self, HometaskList): 51 | _translate = QtCore.QCoreApplication.translate 52 | HometaskList.setWindowTitle(_translate("HometaskList", "Список ДЗ")) 53 | self.refresh.setText(_translate("HometaskList", "Обновить")) 54 | self.label_2.setText(_translate("HometaskList", "Показать выполненные")) 55 | self.label.setText(_translate("HometaskList", "Предстоящие задания")) 56 | self.list_hometask.setSortingEnabled(False) 57 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/di/command_provider.py: -------------------------------------------------------------------------------- 1 | from dishka import Provider, Scope, provide_all 2 | 3 | from student_journal.application.hometask.create_home_task import CreateHomeTask 4 | from student_journal.application.hometask.delete_home_task import DeleteHomeTask 5 | from student_journal.application.hometask.read_home_task import ReadHomeTask 6 | from student_journal.application.hometask.read_home_tasks import ReadHomeTasks 7 | from student_journal.application.hometask.update_home_task import UpdateHomeTask 8 | from student_journal.application.lesson.create_lesson import CreateLesson 9 | from student_journal.application.lesson.delete_all_lessons import DeleteAllLessons 10 | from student_journal.application.lesson.delete_lesson import DeleteLesson 11 | from student_journal.application.lesson.delete_lessons_for_week import ( 12 | DeleteLessonsForWeek, 13 | ) 14 | from student_journal.application.lesson.read_first_lessons_of_weeks import ( 15 | ReadFirstLessonsOfWeeks, 16 | ) 17 | from student_journal.application.lesson.read_lesson import ReadLesson 18 | from student_journal.application.lesson.read_lessons_for_week import ReadLessonsForWeek 19 | from student_journal.application.lesson.update_lesson import UpdateLesson 20 | from student_journal.application.student.create_student import CreateStudent 21 | from student_journal.application.student.read_current_student import ReadCurrentStudent 22 | from student_journal.application.student.read_student import ReadStudent 23 | from student_journal.application.student.update_student import UpdateStudent 24 | from student_journal.application.subject.create_subject import CreateSubject 25 | from student_journal.application.subject.delete_subject import DeleteSubject 26 | from student_journal.application.subject.read_subject import ReadSubject 27 | from student_journal.application.subject.read_subjects import ReadSubjects 28 | from student_journal.application.subject.update_subject import UpdateSubject 29 | from student_journal.application.teacher import ( 30 | CreateTeacher, 31 | DeleteTeacher, 32 | ReadTeacher, 33 | ReadTeachers, 34 | UpdateTeacher, 35 | ) 36 | 37 | 38 | class CommandProvider(Provider): 39 | scope = Scope.REQUEST 40 | 41 | commands = provide_all( 42 | CreateStudent, 43 | ReadCurrentStudent, 44 | ReadStudent, 45 | UpdateStudent, 46 | CreateTeacher, 47 | DeleteTeacher, 48 | ReadTeacher, 49 | ReadTeachers, 50 | UpdateTeacher, 51 | CreateSubject, 52 | DeleteSubject, 53 | ReadSubject, 54 | ReadSubjects, 55 | UpdateSubject, 56 | CreateLesson, 57 | DeleteLesson, 58 | ReadFirstLessonsOfWeeks, 59 | ReadLesson, 60 | ReadLessonsForWeek, 61 | UpdateLesson, 62 | CreateHomeTask, 63 | DeleteHomeTask, 64 | ReadHomeTask, 65 | ReadHomeTasks, 66 | UpdateHomeTask, 67 | DeleteLessonsForWeek, 68 | DeleteAllLessons, 69 | ) 70 | -------------------------------------------------------------------------------- /resources/forms/edit_hometask.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EditHometask 4 | 5 | 6 | 7 | 0 8 | 0 9 | 510 10 | 293 11 | 12 | 13 | 14 | 15 | 800 16 | 800 17 | 18 | 19 | 20 | Редактирование ДЗ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 0 28 | 0 29 | 30 | 31 | 32 | 33 | 18 34 | false 35 | true 36 | 37 | 38 | 39 | Редактирование ДЗ 40 | 41 | 42 | 43 | 44 | 45 | 46 | Сохранить 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Выполнил? 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Урок 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | Описание 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Удалить 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/teacher/edit_teacher.py: -------------------------------------------------------------------------------- 1 | from dishka import Container 2 | from PyQt6.QtWidgets import QWidget 3 | 4 | from student_journal.adapters.exceptions.ui.teacher import ( 5 | FullNameNotSpecifiedError, 6 | TeacherIsNotSpecifiedError, 7 | ) 8 | from student_journal.application.teacher import ( 9 | CreateTeacher, 10 | DeleteTeacher, 11 | NewTeacher, 12 | ReadTeacher, 13 | UpdatedTeacher, 14 | UpdateTeacher, 15 | ) 16 | from student_journal.domain.value_object.teacher_id import TeacherId 17 | from student_journal.presentation.ui.edit_teacher_ui import Ui_EditTeacher 18 | 19 | 20 | class EditTeacher(QWidget): 21 | def __init__( 22 | self, 23 | container: Container, 24 | teacher_id: TeacherId | None, 25 | ) -> None: 26 | super().__init__() 27 | 28 | self.container = container 29 | self.teacher_id = teacher_id 30 | 31 | self.ui = Ui_EditTeacher() 32 | self.ui.setupUi(self) 33 | 34 | self.full_name: str | None = None 35 | 36 | self.ui.submit_btn.clicked.connect(self.on_submit_btn) 37 | self.ui.delete_btn.clicked.connect(self.on_delete_btn) 38 | self.ui.full_name_input.textChanged.connect(self.on_full_name_input) 39 | 40 | if not self.teacher_id: 41 | self.ui.delete_btn.hide() 42 | self.ui.main_label.setText("Добавить учителя") 43 | else: 44 | with self.container() as r_container: 45 | command = r_container.get(ReadTeacher) 46 | teacher = command.execute(self.teacher_id) 47 | self.ui.full_name_input.setText(teacher.full_name) 48 | self.full_name = teacher.full_name 49 | 50 | self.ui.main_label.setText("Редактировать учителя") 51 | 52 | def on_submit_btn(self) -> None: 53 | if not self.full_name: 54 | raise FullNameNotSpecifiedError 55 | 56 | with self.container() as r_container: 57 | if not self.teacher_id: 58 | data = NewTeacher( 59 | full_name=self.full_name, 60 | avatar=None, 61 | ) 62 | command = r_container.get(CreateTeacher) 63 | command.execute(data) 64 | else: 65 | data_update = UpdatedTeacher( 66 | teacher_id=self.teacher_id, 67 | full_name=self.full_name, 68 | avatar=None, 69 | ) 70 | command_update = r_container.get(UpdateTeacher) 71 | command_update.execute(data_update) 72 | self.close() 73 | 74 | def on_delete_btn(self) -> None: 75 | if not self.teacher_id: 76 | raise TeacherIsNotSpecifiedError 77 | 78 | with self.container() as r_container: 79 | command = r_container.get(DeleteTeacher) 80 | command.execute(self.teacher_id) 81 | self.close() 82 | 83 | def on_full_name_input(self) -> None: 84 | self.full_name = self.ui.full_name_input.text() 85 | -------------------------------------------------------------------------------- /src/student_journal/bootstrap/entrypoint/qt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import signal 3 | import sys 4 | from functools import partial 5 | from importlib.resources import as_file, files 6 | from types import TracebackType 7 | 8 | from PyQt6 import QtWidgets 9 | from PyQt6.QtCore import Qt 10 | from PyQt6.QtWidgets import QApplication, QMessageBox 11 | 12 | import student_journal 13 | import student_journal.presentation.resource 14 | from student_journal.adapters.error_locator import ErrorLocator 15 | from student_journal.application.exceptions.base import ApplicationError 16 | from student_journal.application.exceptions.student import ( 17 | StudentIsNotAuthenticatedError, 18 | ) 19 | from student_journal.bootstrap.di.container import get_container_for_gui 20 | from student_journal.presentation.widget.main_window import MainWindow 21 | 22 | logging.basicConfig( 23 | level=logging.DEBUG, 24 | format="%(asctime)s %(levelname)s %(name)s %(message)s", 25 | datefmt="%Y-%m-%d %H:%M:%S", 26 | ) 27 | 28 | 29 | def display_error_text(wnd: MainWindow, text: str) -> None: 30 | msg = QMessageBox(wnd) 31 | msg.setIcon(QMessageBox.Icon.Critical) 32 | msg.setWindowTitle("Ошибка приложения") 33 | msg.setText("Произошла ошибка!") 34 | msg.setInformativeText(text) 35 | msg.setStandardButtons(QMessageBox.StandardButton.Ok) 36 | msg.exec() 37 | 38 | 39 | def except_hook( 40 | app: QApplication, 41 | error_locator: ErrorLocator, 42 | wnd: MainWindow, 43 | _exc_type: type[Exception], 44 | exc_value: BaseException, 45 | _exc_traceback: TracebackType, 46 | ) -> None: 47 | match exc_value: 48 | case StudentIsNotAuthenticatedError() as e: 49 | text = error_locator.get_text(e) 50 | display_error_text(wnd, text) 51 | app.closeAllWindows() 52 | wnd.show() 53 | wnd.display_register() 54 | 55 | case ApplicationError() as e: 56 | text = error_locator.get_text(e) 57 | display_error_text(wnd, text) 58 | 59 | case BaseException() as e: 60 | logging.critical("Unhandled exception", exc_info=e) 61 | sys.exit() 62 | 63 | 64 | def main(_argv: list[str]) -> None: 65 | if hasattr(Qt, "AA_EnableHighDpiScaling"): 66 | QtWidgets.QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) # noqa: FBT003 67 | 68 | if hasattr(Qt, "AA_UseHighDpiPixmaps"): 69 | QtWidgets.QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) # noqa: FBT003 70 | 71 | container = get_container_for_gui() 72 | resources = files(student_journal.presentation.resource) 73 | 74 | app = QApplication(_argv) 75 | 76 | main_wnd = MainWindow(container) 77 | main_wnd.setWindowTitle("Дневник Школьника") 78 | 79 | with as_file(resources.joinpath("styles.qss")) as qss_path: 80 | app.setStyleSheet(qss_path.read_text("utf-8")) 81 | 82 | main_wnd.show() 83 | 84 | locator = container.get(ErrorLocator) 85 | sys.excepthook = partial(except_hook, app, locator, main_wnd) 86 | 87 | signal.signal(signal.SIGINT, signal.SIG_DFL) 88 | sys.exit(app.exec()) 89 | 90 | 91 | if __name__ == "__main__": 92 | main(sys.argv) 93 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/edit_teacher_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'edit_teacher.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.4.2 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_EditTeacher(object): 13 | def setupUi(self, EditTeacher): 14 | EditTeacher.setObjectName("EditTeacher") 15 | EditTeacher.setWindowModality(QtCore.Qt.WindowModality.WindowModal) 16 | 17 | self.gridLayout = QtWidgets.QGridLayout(EditTeacher) 18 | self.gridLayout.setObjectName("gridLayout") 19 | self.submit_btn = QtWidgets.QPushButton(parent=EditTeacher) 20 | self.submit_btn.setObjectName("submit_btn") 21 | self.gridLayout.addWidget(self.submit_btn, 2, 0, 1, 1) 22 | self.formLayout = QtWidgets.QFormLayout() 23 | self.formLayout.setContentsMargins(0, 10, 10, -1) 24 | self.formLayout.setObjectName("formLayout") 25 | self.label = QtWidgets.QLabel(parent=EditTeacher) 26 | self.label.setObjectName("label") 27 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label) 28 | self.full_name_input = QtWidgets.QLineEdit(parent=EditTeacher) 29 | self.full_name_input.setObjectName("full_name_input") 30 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.full_name_input) 31 | self.gridLayout.addLayout(self.formLayout, 1, 0, 1, 1) 32 | self.main_label = QtWidgets.QLabel(parent=EditTeacher) 33 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 34 | sizePolicy.setHorizontalStretch(0) 35 | sizePolicy.setVerticalStretch(0) 36 | sizePolicy.setHeightForWidth(self.main_label.sizePolicy().hasHeightForWidth()) 37 | self.main_label.setSizePolicy(sizePolicy) 38 | font = QtGui.QFont() 39 | font.setPointSize(18) 40 | font.setBold(True) 41 | self.main_label.setFont(font) 42 | self.main_label.setObjectName("main_label") 43 | self.gridLayout.addWidget(self.main_label, 0, 0, 1, 1) 44 | self.delete_btn = QtWidgets.QPushButton(parent=EditTeacher) 45 | self.delete_btn.setEnabled(True) 46 | self.delete_btn.setCheckable(False) 47 | self.delete_btn.setObjectName("delete_btn") 48 | self.gridLayout.addWidget(self.delete_btn, 3, 0, 1, 1) 49 | 50 | self.retranslateUi(EditTeacher) 51 | QtCore.QMetaObject.connectSlotsByName(EditTeacher) 52 | 53 | def retranslateUi(self, EditTeacher): 54 | _translate = QtCore.QCoreApplication.translate 55 | EditTeacher.setWindowTitle(_translate("EditTeacher", "Редактирование учителя")) 56 | self.submit_btn.setText(_translate("EditTeacher", "Сохранить")) 57 | self.label.setText(_translate("EditTeacher", "Полное имя")) 58 | self.main_label.setText(_translate("EditTeacher", "Редактирование учителя")) 59 | self.delete_btn.setText(_translate("EditTeacher", "Удалить")) 60 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/subject/progress.py: -------------------------------------------------------------------------------- 1 | from dishka import Container 2 | from PyQt6.QtCore import Qt 3 | from PyQt6.QtWidgets import QHeaderView, QTableWidgetItem, QWidget 4 | 5 | from student_journal.application.models.subject import SubjectReadModel 6 | from student_journal.application.subject.read_subjects import ReadSubjects 7 | from student_journal.presentation.ui.progress_ui import Ui_Progress 8 | 9 | 10 | class Progress(QWidget): 11 | def __init__(self, container: Container) -> None: 12 | super().__init__() 13 | self.container = container 14 | self.ui = Ui_Progress() 15 | self.ui.setupUi(self) 16 | 17 | self.ui.refresh.clicked.connect(self.on_refresh) 18 | self.ui.sorting.currentIndexChanged.connect(self.on_sort_changed) 19 | self.ui.show_without_mark.stateChanged.connect(self.on_toggle_without_mark) 20 | 21 | if (header := self.ui.table.horizontalHeader()) is not None: 22 | header.setSectionResizeMode( 23 | QHeaderView.ResizeMode.Stretch, 24 | ) 25 | 26 | self.load_subjects() 27 | 28 | def load_subjects(self) -> None: 29 | show_without_mark = not self.ui.show_without_mark.isChecked() 30 | sort_by_title = False 31 | sort_by_avg_mark = False 32 | sorting_index = self.ui.sorting.currentIndex() 33 | 34 | if sorting_index == 0: 35 | sort_by_title = True 36 | elif sorting_index == 1: 37 | sort_by_avg_mark = True 38 | 39 | with self.container() as r_container: 40 | command = r_container.get(ReadSubjects) 41 | subjects = command.execute( 42 | sort_by_title=sort_by_title, 43 | sort_by_avg_mark=sort_by_avg_mark, 44 | show_empty=show_without_mark, 45 | ) 46 | self.populate_table(subjects) 47 | 48 | def populate_table(self, subjects: list[SubjectReadModel]) -> None: 49 | self.ui.table.setRowCount(len(subjects)) 50 | for row, subject in enumerate(subjects): 51 | subject_item = QTableWidgetItem(subject.title) 52 | subject_item.setFlags(Qt.ItemFlag.ItemIsEditable) 53 | self.ui.table.setItem(row, 0, subject_item) 54 | 55 | avg_mark_item = QTableWidgetItem( 56 | f"{subject.avg_mark:.2f}" if subject.avg_mark != 0.0 else "—", 57 | ) 58 | avg_mark_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) 59 | avg_mark_item.setFlags(Qt.ItemFlag.ItemIsEditable) 60 | self.ui.table.setItem(row, 1, avg_mark_item) 61 | 62 | marks_list_item = QTableWidgetItem( 63 | f"{' '.join(map(str, subject.marks_list))}" 64 | if subject.marks_list 65 | else "—", 66 | ) 67 | marks_list_item.setFlags(Qt.ItemFlag.ItemIsEditable) 68 | self.ui.table.setItem(row, 2, marks_list_item) 69 | 70 | def on_refresh(self) -> None: 71 | self.load_subjects() 72 | 73 | def on_sort_changed(self) -> None: 74 | self.load_subjects() 75 | 76 | def on_toggle_without_mark(self) -> None: 77 | self.load_subjects() 78 | -------------------------------------------------------------------------------- /src/student_journal/presentation/resource/styles.qss: -------------------------------------------------------------------------------- 1 | /* Общий стиль для приложения */ 2 | QWidget { 3 | background-color: #2B2B3D; 4 | color: #C1CEFE; 5 | font-family: "Segoe UI", Arial, sans-serif; 6 | } 7 | 8 | /* Главные окна */ 9 | QMainWindow { 10 | background-color: #2B2B3D; 11 | border: 1px solid #624CAB; 12 | border-radius: 8px; 13 | } 14 | 15 | /* Кнопки */ 16 | QPushButton { 17 | background-color: #624CAB; 18 | color: #FFFFFF; 19 | border: 1px solid #758ECD; 20 | border-radius: 5px; 21 | padding: 5px 10px; 22 | } 23 | 24 | QPushButton:hover { 25 | background-color: #7189FF; 26 | } 27 | 28 | QPushButton:pressed { 29 | background-color: #3A3A4F; 30 | } 31 | 32 | /* Меню */ 33 | QMenuBar { 34 | background-color: #3A3A4F; 35 | color: #C1CEFE; 36 | border: 1px solid #624CAB; 37 | } 38 | 39 | QMenuBar::item { 40 | background-color: transparent; 41 | padding: 5px 10px; 42 | } 43 | 44 | QMenuBar::item:selected { 45 | background-color: #624CAB; 46 | color: #FFFFFF; 47 | } 48 | 49 | QMenu { 50 | background-color: #3A3A4F; 51 | color: #C1CEFE; 52 | border: 1px solid #758ECD; 53 | } 54 | 55 | QMenu::item { 56 | background-color: transparent; 57 | padding: 5px 10px; 58 | } 59 | 60 | QMenu::item:selected { 61 | background-color: #7189FF; 62 | color: #FFFFFF; 63 | } 64 | 65 | /* Таблицы и списки */ 66 | QTableWidget, QListWidget, QTreeWidget { 67 | background-color: #3A3A4F; 68 | color: #C1CEFE; 69 | border: 1px solid #624CAB; 70 | border-radius: 5px; 71 | } 72 | 73 | QHeaderView::section { 74 | background-color: #758ECD; 75 | color: #FFFFFF; 76 | padding: 5px; 77 | border: 1px solid #624CAB; 78 | } 79 | 80 | /* Вкладки */ 81 | QTabWidget::pane { 82 | border: 1px solid #624CAB; 83 | background-color: #3A3A4F; 84 | } 85 | 86 | QTabBar::tab { 87 | background-color: #3A3A4F; 88 | color: #C1CEFE; 89 | padding: 5px 10px; 90 | border: 1px solid #624CAB; 91 | border-top-left-radius: 5px; 92 | border-top-right-radius: 5px; 93 | } 94 | 95 | QTabBar::tab:selected { 96 | background-color: #624CAB; 97 | color: #FFFFFF; 98 | } 99 | 100 | /* Поля ввода текста */ 101 | QLineEdit, QTextEdit { 102 | background-color: #3A3A4F; 103 | color: #C1CEFE; 104 | border: 1px solid #758ECD; 105 | border-radius: 5px; 106 | padding: 5px; 107 | } 108 | 109 | /* Слайдеры */ 110 | QSlider::groove:horizontal { 111 | height: 5px; 112 | background-color: #624CAB; 113 | } 114 | 115 | QSlider::handle:horizontal { 116 | width: 15px; 117 | height: 15px; 118 | background-color: #7189FF; 119 | border: 1px solid #C1CEFE; 120 | border-radius: 7px; 121 | } 122 | 123 | QSlider::handle:horizontal:hover { 124 | background-color: #C1CEFE; 125 | } 126 | 127 | /* ScrollBar */ 128 | QScrollBar:vertical, QScrollBar:horizontal { 129 | background-color: #3A3A4F; 130 | border: 1px solid #624CAB; 131 | width: 10px; 132 | } 133 | 134 | QScrollBar::handle { 135 | background-color: #7189FF; 136 | border-radius: 5px; 137 | } 138 | 139 | QScrollBar::handle:hover { 140 | background-color: #C1CEFE; 141 | } 142 | -------------------------------------------------------------------------------- /resources/forms/edit_subject.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EditSubject 4 | 5 | 6 | Qt::WindowModality::WindowModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 444 13 | 300 14 | 15 | 16 | 17 | 18 | 0 19 | 0 20 | 21 | 22 | 23 | 24 | 500 25 | 300 26 | 27 | 28 | 29 | Редактирование предмета 30 | 31 | 32 | 33 | 34 | 35 | Сохранить 36 | 37 | 38 | 39 | 40 | 41 | 42 | 10 43 | 44 | 45 | 9 46 | 47 | 48 | 10 49 | 50 | 51 | 52 | 53 | Название 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Учитель 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 0 87 | 0 88 | 89 | 90 | 91 | 92 | 18 93 | true 94 | 95 | 96 | 97 | Редактирование предмета 98 | 99 | 100 | -1 101 | 102 | 103 | 104 | 105 | 106 | 107 | Удалить 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /tests/gateway/subject/test_subject_gateway.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Cursor 2 | 3 | import pytest 4 | from unit.subject.conftest import SUBJECT, SUBJECT2, SUBJECT_ID 5 | from unit.teacher.conftest import TEACHER, TEACHER2 6 | 7 | from student_journal.adapters.converter.subject import subject_retort 8 | from student_journal.application.common.subject_gateway import SubjectGateway 9 | from student_journal.application.common.teacher_gateway import TeacherGateway 10 | from student_journal.application.exceptions.subject import SubjectNotFoundError 11 | from student_journal.domain.subject import Subject 12 | 13 | READ_SUBJECT_SQL = """ 14 | SELECT 15 | s.subject_id, 16 | s.title, 17 | s.teacher_id, 18 | COALESCE(AVG(l.mark), 0.0) AS avg_mark 19 | FROM Subject s 20 | LEFT JOIN Lesson l ON s.subject_id = l.subject_id 21 | GROUP BY s.subject_id 22 | ORDER BY avg_mark DESC 23 | """ 24 | 25 | 26 | def test_write_subject( 27 | subject_gateway: SubjectGateway, 28 | cursor: Cursor, 29 | ) -> None: 30 | subject_gateway.write_subject(SUBJECT) 31 | db_subject = subject_retort.load( 32 | dict(cursor.execute(READ_SUBJECT_SQL).fetchone()), 33 | Subject, 34 | ) 35 | 36 | assert db_subject == SUBJECT 37 | 38 | 39 | def test_read_subject( 40 | subject_gateway: SubjectGateway, 41 | cursor: Cursor, 42 | ) -> None: 43 | subject_gateway.write_subject(SUBJECT) 44 | db_subject = subject_retort.load( 45 | dict(cursor.execute(READ_SUBJECT_SQL).fetchone()), 46 | Subject, 47 | ) 48 | 49 | assert db_subject == subject_gateway.read_subject(SUBJECT_ID) 50 | 51 | 52 | def test_read_subject_not_exist( 53 | subject_gateway: SubjectGateway, 54 | ) -> None: 55 | with pytest.raises(SubjectNotFoundError): 56 | subject_gateway.read_subject(SUBJECT_ID) 57 | 58 | 59 | def test_read_subjects( 60 | teacher_gateway: TeacherGateway, 61 | subject_gateway: SubjectGateway, 62 | cursor: Cursor, 63 | ) -> None: 64 | teacher_gateway.write_teacher(TEACHER) 65 | teacher_gateway.write_teacher(TEACHER2) 66 | subject_gateway.write_subject(SUBJECT) 67 | subject_gateway.write_subject(SUBJECT2) 68 | subjects = subject_gateway.read_subjects() 69 | 70 | subjects = [ 71 | Subject( 72 | subject_id=x.subject_id, 73 | teacher_id=x.teacher.teacher_id, 74 | title=x.title, 75 | ) 76 | for x in subjects 77 | ] 78 | 79 | subjects_list = [dict(s) for s in cursor.execute(READ_SUBJECT_SQL).fetchall()] 80 | db_subjects = subject_retort.load( 81 | subjects_list, 82 | list[Subject], 83 | ) 84 | 85 | assert db_subjects == subjects 86 | 87 | 88 | def test_update_subject( 89 | subject_gateway: SubjectGateway, 90 | cursor: Cursor, 91 | ) -> None: 92 | subject_gateway.write_subject(SUBJECT) 93 | updated_subject = Subject( 94 | subject_id=SUBJECT_ID, 95 | title="Updated Title", 96 | teacher_id=SUBJECT.teacher_id, 97 | ) 98 | 99 | subject_gateway.update_subject(updated_subject) 100 | db_entry = dict(cursor.execute(READ_SUBJECT_SQL).fetchone()) 101 | 102 | db_subject = subject_retort.load( 103 | db_entry, 104 | Subject, 105 | ) 106 | 107 | assert db_subject == updated_subject 108 | 109 | 110 | def test_delete_subject( 111 | subject_gateway: SubjectGateway, 112 | cursor: Cursor, 113 | ) -> None: 114 | subject_gateway.write_subject(SUBJECT) 115 | subject_gateway.delete_subject(SUBJECT_ID) 116 | 117 | cursor.execute(READ_SUBJECT_SQL) 118 | assert cursor.fetchone() is None 119 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/gateway/subject_gateway.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from sqlite3 import Cursor 3 | 4 | from student_journal.adapters.converter.subject import ( 5 | subject_retort, 6 | subject_to_list_retort, 7 | ) 8 | from student_journal.application.common.subject_gateway import SubjectGateway 9 | from student_journal.application.exceptions.subject import SubjectNotFoundError 10 | from student_journal.application.models.subject import SubjectReadModel 11 | from student_journal.domain.subject import Subject 12 | from student_journal.domain.value_object.subject_id import SubjectId 13 | 14 | 15 | @dataclass(slots=True, frozen=True) 16 | class SQLiteSubjectGateway(SubjectGateway): 17 | cursor: Cursor 18 | 19 | def read_subject(self, subject_id: SubjectId) -> Subject: 20 | query = "SELECT subject_id, title, teacher_id FROM Subject WHERE subject_id = ?" 21 | res = self.cursor.execute(query, (str(subject_id),)).fetchone() 22 | 23 | if res is None: 24 | raise SubjectNotFoundError 25 | 26 | subject = subject_retort.load(dict(res), Subject) 27 | 28 | return subject 29 | 30 | def write_subject(self, subject: Subject) -> None: 31 | query = ( 32 | "INSERT INTO Subject " 33 | "(subject_id, teacher_id, title) " 34 | "VALUES " 35 | "(?, ?, ?)" 36 | ) 37 | params = subject_to_list_retort.dump(subject) 38 | self.cursor.execute(query, params) 39 | 40 | def update_subject(self, subject: Subject) -> None: 41 | query = "UPDATE Subject SET teacher_id = ?, title = ? WHERE subject_id = ?" 42 | params = subject_to_list_retort.dump(subject) 43 | params.append(params.pop(0)) 44 | 45 | self.cursor.execute(query, params) 46 | 47 | def delete_subject(self, subject_id: SubjectId) -> None: 48 | query = """DELETE FROM Subject WHERE subject_id = ?""" 49 | self.cursor.execute(query, (str(subject_id),)) 50 | 51 | def read_subjects( 52 | self, 53 | *, 54 | sort_by_title: bool = False, 55 | sort_by_avg_mark: bool = False, 56 | show_empty: bool = True, 57 | ) -> list[SubjectReadModel]: 58 | query = """ 59 | SELECT 60 | s.subject_id, 61 | s.title, 62 | t.teacher_id, 63 | t.full_name as teacher_full_name, 64 | t.avatar as teacher_avatar, 65 | COALESCE(AVG(l.mark), 0.0) AS avg_mark, 66 | GROUP_CONCAT(l.mark, '|') AS marks_list 67 | FROM Subject s 68 | JOIN Teacher t ON s.teacher_id = t.teacher_id 69 | LEFT JOIN Lesson l ON s.subject_id = l.subject_id 70 | GROUP BY s.subject_id, s.title, t.teacher_id, t.full_name, t.avatar 71 | """ 72 | 73 | if not show_empty: 74 | query += "HAVING COALESCE(AVG(l.mark), 0.0) != 0.0\n" 75 | 76 | if sort_by_avg_mark: 77 | query += "ORDER BY avg_mark DESC" 78 | elif sort_by_title: 79 | query += "ORDER BY title" 80 | else: 81 | query += "ORDER BY s.subject_id" 82 | 83 | res = self.cursor.execute(query).fetchall() 84 | entries = [dict(x) for x in res] 85 | 86 | for each in entries: 87 | each["teacher"] = { 88 | "teacher_id": each["teacher_id"], 89 | "full_name": each["teacher_full_name"], 90 | "avatar": each["teacher_avatar"], 91 | } 92 | 93 | if each["marks_list"] is None: 94 | each["marks_list"] = [] 95 | else: 96 | each["marks_list"] = list(map(int, each["marks_list"].split("|"))) 97 | 98 | result = subject_retort.load(entries, list[SubjectReadModel]) 99 | return result 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | .ruff_cache/ 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Secrets 123 | .env* 124 | .env 125 | 126 | # Environments 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | venv*/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | .idea/ -------------------------------------------------------------------------------- /resources/forms/progress.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Progress 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Моя успеваемость 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Без ср. балла 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Сортировать по 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Названию предмета 45 | 46 | 47 | 48 | 49 | Среднему баллу 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Обновить 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Qt::Orientation::Horizontal 69 | 70 | 71 | 72 | 70 73 | 20 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 0 83 | 0 84 | 85 | 86 | 87 | 88 | 18 89 | true 90 | 91 | 92 | 93 | Моя успеваемость 94 | 95 | 96 | 97 | 98 | 99 | 100 | Qt::Orientation::Horizontal 101 | 102 | 103 | 104 | 40 105 | 20 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Предмет 117 | 118 | 119 | 120 | 121 | Средний балл 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/edit_hometask_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'edit_hometask.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.4.2 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_EditHometask(object): 13 | def setupUi(self, EditHometask): 14 | EditHometask.setObjectName("EditHometask") 15 | self.gridLayout = QtWidgets.QGridLayout(EditHometask) 16 | self.gridLayout.setObjectName("gridLayout") 17 | self.main_label = QtWidgets.QLabel(parent=EditHometask) 18 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 19 | sizePolicy.setHorizontalStretch(0) 20 | sizePolicy.setVerticalStretch(0) 21 | sizePolicy.setHeightForWidth(self.main_label.sizePolicy().hasHeightForWidth()) 22 | self.main_label.setSizePolicy(sizePolicy) 23 | font = QtGui.QFont() 24 | font.setPointSize(18) 25 | font.setBold(True) 26 | font.setItalic(False) 27 | self.main_label.setFont(font) 28 | self.main_label.setObjectName("main_label") 29 | self.gridLayout.addWidget(self.main_label, 0, 0, 1, 1) 30 | self.submit_btn = QtWidgets.QPushButton(parent=EditHometask) 31 | self.submit_btn.setObjectName("submit_btn") 32 | self.gridLayout.addWidget(self.submit_btn, 2, 0, 1, 1) 33 | self.formLayout = QtWidgets.QFormLayout() 34 | self.formLayout.setObjectName("formLayout") 35 | self.lesson = QtWidgets.QComboBox(parent=EditHometask) 36 | self.lesson.setObjectName("lesson") 37 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.lesson) 38 | self.label = QtWidgets.QLabel(parent=EditHometask) 39 | self.label.setObjectName("label") 40 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label) 41 | self.is_done = QtWidgets.QCheckBox(parent=EditHometask) 42 | self.is_done.setText("") 43 | self.is_done.setObjectName("is_done") 44 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.is_done) 45 | self.label_2 = QtWidgets.QLabel(parent=EditHometask) 46 | self.label_2.setObjectName("label_2") 47 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) 48 | self.description = QtWidgets.QTextEdit(parent=EditHometask) 49 | self.description.setObjectName("description") 50 | self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.FieldRole, self.description) 51 | self.label_3 = QtWidgets.QLabel(parent=EditHometask) 52 | self.label_3.setObjectName("label_3") 53 | self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3) 54 | self.gridLayout.addLayout(self.formLayout, 1, 0, 1, 1) 55 | self.delete_btn = QtWidgets.QPushButton(parent=EditHometask) 56 | self.delete_btn.setObjectName("delete_btn") 57 | self.gridLayout.addWidget(self.delete_btn, 3, 0, 1, 1) 58 | 59 | self.retranslateUi(EditHometask) 60 | QtCore.QMetaObject.connectSlotsByName(EditHometask) 61 | 62 | def retranslateUi(self, EditHometask): 63 | _translate = QtCore.QCoreApplication.translate 64 | EditHometask.setWindowTitle(_translate("EditHometask", "Редактирование ДЗ")) 65 | self.main_label.setText(_translate("EditHometask", "Редактирование ДЗ")) 66 | self.submit_btn.setText(_translate("EditHometask", "Сохранить")) 67 | self.label.setText(_translate("EditHometask", "Выполнил?")) 68 | self.label_2.setText(_translate("EditHometask", "Урок")) 69 | self.label_3.setText(_translate("EditHometask", "Описание")) 70 | self.delete_btn.setText(_translate("EditHometask", "Удалить")) 71 | -------------------------------------------------------------------------------- /resources/forms/schedule.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Schedule 4 | 5 | 6 | 7 | 0 8 | 0 9 | 646 10 | 400 11 | 12 | 13 | 14 | Расписание 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | Очистить уроки недели 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 0 35 | 0 36 | 37 | 38 | 39 | Обновить все 40 | 41 | 42 | 43 | 44 | 45 | 46 | Выбрать месяц и год недель 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 14 58 | true 59 | 60 | 61 | 62 | Расписание на неделю 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 0 72 | 73 | 74 | 75 | false 76 | 77 | 78 | false 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 0 87 | 0 88 | 89 | 90 | 91 | Копировать расписание на след. неделю 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 14 103 | true 104 | 105 | 106 | 107 | Список недель 108 | 109 | 110 | 111 | 112 | 113 | 114 | Очистить все уроки всех недель 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/student_journal/adapters/db/gateway/home_task_gateway.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from sqlite3 import Cursor 3 | 4 | from student_journal.adapters.converter import ( 5 | home_task_retort, 6 | home_task_to_list_retort, 7 | ) 8 | from student_journal.application.common.home_task_gateway import HomeTaskGateway 9 | from student_journal.application.exceptions.home_task import HomeTaskNotFoundError 10 | from student_journal.application.models.home_task import HomeTaskReadModel 11 | from student_journal.domain.home_task import HomeTask 12 | from student_journal.domain.value_object.task_id import HomeTaskId 13 | 14 | 15 | @dataclass(slots=True, frozen=True) 16 | class SQLiteHomeTaskGateway(HomeTaskGateway): 17 | cursor: Cursor 18 | 19 | def read_home_task(self, task_id: HomeTaskId) -> HomeTask: 20 | query = """ 21 | SELECT task_id, lesson_id, description, is_done 22 | FROM Hometask WHERE task_id = ? 23 | """ 24 | res = self.cursor.execute(query, (str(task_id),)).fetchone() 25 | 26 | if res is None: 27 | raise HomeTaskNotFoundError 28 | 29 | home_task = home_task_retort.load(dict(res), HomeTask) 30 | return home_task 31 | 32 | def write_home_task(self, home_task: HomeTask) -> None: 33 | query = """ 34 | INSERT INTO Hometask 35 | (task_id, lesson_id, description, is_done) 36 | VALUES (?, ?, ?, ?) 37 | """ 38 | params = home_task_to_list_retort.dump(home_task) 39 | self.cursor.execute(query, params) 40 | 41 | def read_home_tasks(self, *, show_done: bool = False) -> list[HomeTaskReadModel]: 42 | query = """ 43 | SELECT Hometask.task_id, Hometask.description, Hometask.is_done, 44 | Lesson.lesson_id as lesson_lesson_id, 45 | Lesson.subject_id as lesson_subject_id, 46 | Lesson.at as lesson_at, 47 | Lesson.mark as lesson_mark, 48 | Lesson.note as lesson_note, 49 | Lesson.room as lesson_room, 50 | Subject.subject_id as subject_subject_id, 51 | Subject.title as subject_title, 52 | Subject.teacher_id as subject_teacher_id 53 | FROM Hometask 54 | JOIN Lesson ON Hometask.lesson_id = Lesson.lesson_id 55 | JOIN Subject ON Lesson.subject_id = Subject.subject_id 56 | """ 57 | if show_done is False: 58 | query += " WHERE is_done = ?" 59 | res = self.cursor.execute(query, (show_done,)).fetchall() 60 | else: 61 | res = self.cursor.execute(query).fetchall() 62 | 63 | rows = [dict(row) for row in res] 64 | result = [] 65 | 66 | for row in rows: 67 | lesson_fields = [x for x in row if x.startswith("lesson_")] 68 | subject_fields = [x for x in row if x.startswith("subject_")] 69 | own_fields = [ 70 | x for x in row if (x not in lesson_fields) and (x not in subject_fields) 71 | ] 72 | new_row = {key: row[key] for key in own_fields} 73 | new_row["lesson"] = {key[7:]: row[key] for key in lesson_fields} 74 | new_row["subject"] = {key[8:]: row[key] for key in subject_fields} 75 | 76 | result.append(new_row) 77 | 78 | home_tasks = home_task_retort.load(result, list[HomeTaskReadModel]) 79 | 80 | return home_tasks 81 | 82 | def update_home_task(self, home_task: HomeTask) -> None: 83 | query = """ 84 | UPDATE Hometask 85 | SET lesson_id = ?, description = ?, is_done = ? 86 | WHERE task_id = ? 87 | """ 88 | params = home_task_to_list_retort.dump(home_task) 89 | 90 | params.append(params.pop(0)) 91 | 92 | self.cursor.execute(query, params) 93 | 94 | def delete_home_task(self, task_id: HomeTaskId) -> None: 95 | query = """ 96 | DELETE FROM Hometask 97 | WHERE task_id = ? 98 | """ 99 | 100 | self.cursor.execute(query, (str(task_id),)) 101 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/edit_subject_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'edit_subject.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.4.2 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_EditSubject(object): 13 | def setupUi(self, EditSubject): 14 | EditSubject.setObjectName("EditSubject") 15 | EditSubject.setWindowModality(QtCore.Qt.WindowModality.WindowModal) 16 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) 17 | sizePolicy.setHorizontalStretch(0) 18 | sizePolicy.setVerticalStretch(0) 19 | sizePolicy.setHeightForWidth(EditSubject.sizePolicy().hasHeightForWidth()) 20 | EditSubject.setSizePolicy(sizePolicy) 21 | self.gridLayout = QtWidgets.QGridLayout(EditSubject) 22 | self.gridLayout.setObjectName("gridLayout") 23 | self.submit_btn = QtWidgets.QPushButton(parent=EditSubject) 24 | self.submit_btn.setObjectName("submit_btn") 25 | self.gridLayout.addWidget(self.submit_btn, 2, 0, 1, 1) 26 | self.formLayout = QtWidgets.QFormLayout() 27 | self.formLayout.setContentsMargins(10, 9, 10, -1) 28 | self.formLayout.setObjectName("formLayout") 29 | self.label_2 = QtWidgets.QLabel(parent=EditSubject) 30 | self.label_2.setObjectName("label_2") 31 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) 32 | self.title_input = QtWidgets.QLineEdit(parent=EditSubject) 33 | self.title_input.setObjectName("title_input") 34 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.title_input) 35 | self.label_3 = QtWidgets.QLabel(parent=EditSubject) 36 | self.label_3.setObjectName("label_3") 37 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3) 38 | self.teacher_combo = QtWidgets.QComboBox(parent=EditSubject) 39 | self.teacher_combo.setObjectName("teacher_combo") 40 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.teacher_combo) 41 | self.refresh = QtWidgets.QPushButton(parent=EditSubject) 42 | self.refresh.setText("Обновить") 43 | self.refresh.setObjectName("refresh") 44 | self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.FieldRole, self.refresh) 45 | self.gridLayout.addLayout(self.formLayout, 1, 0, 1, 1) 46 | self.main_label = QtWidgets.QLabel(parent=EditSubject) 47 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 48 | sizePolicy.setHorizontalStretch(0) 49 | sizePolicy.setVerticalStretch(0) 50 | sizePolicy.setHeightForWidth(self.main_label.sizePolicy().hasHeightForWidth()) 51 | self.main_label.setSizePolicy(sizePolicy) 52 | font = QtGui.QFont() 53 | font.setPointSize(18) 54 | font.setBold(True) 55 | self.main_label.setFont(font) 56 | self.main_label.setIndent(-1) 57 | self.main_label.setObjectName("main_label") 58 | self.gridLayout.addWidget(self.main_label, 0, 0, 1, 1) 59 | self.delete_btn = QtWidgets.QPushButton(parent=EditSubject) 60 | self.delete_btn.setObjectName("delete_btn") 61 | self.gridLayout.addWidget(self.delete_btn, 3, 0, 1, 1) 62 | 63 | self.retranslateUi(EditSubject) 64 | QtCore.QMetaObject.connectSlotsByName(EditSubject) 65 | 66 | def retranslateUi(self, EditSubject): 67 | _translate = QtCore.QCoreApplication.translate 68 | EditSubject.setWindowTitle(_translate("EditSubject", "Редактирование предмета")) 69 | self.submit_btn.setText(_translate("EditSubject", "Сохранить")) 70 | self.label_2.setText(_translate("EditSubject", "Название")) 71 | self.label_3.setText(_translate("EditSubject", "Учитель")) 72 | self.main_label.setText(_translate("EditSubject", "Редактирование предмета")) 73 | self.delete_btn.setText(_translate("EditSubject", "Удалить")) 74 | -------------------------------------------------------------------------------- /src/student_journal/presentation/widget/student/edit_student.py: -------------------------------------------------------------------------------- 1 | from dishka import Container 2 | from PyQt6.QtGui import QPixmap 3 | from PyQt6.QtWidgets import QFileDialog, QWidget 4 | 5 | from student_journal.adapters.error_locator import ErrorLocator 6 | from student_journal.application.student.read_current_student import ReadCurrentStudent 7 | from student_journal.application.student.update_student import ( 8 | UpdatedStudent, 9 | UpdateStudent, 10 | ) 11 | from student_journal.presentation.ui.edit_student import EditStudentUI 12 | 13 | 14 | class EditStudent(QWidget): 15 | def __init__(self, container: Container) -> None: 16 | super().__init__() 17 | 18 | self.container = container 19 | self.error_locator = container.get(ErrorLocator) 20 | 21 | self.ui = EditStudentUI() 22 | self.ui.setupUi(self) 23 | 24 | self.name = "" 25 | self.age: int | None = None 26 | self.home_address: str | None = None 27 | self.avatar: str | None = None 28 | 29 | self.ui.submit_btn.clicked.connect(self.on_submit_btn) 30 | self.ui.cancel_btn.clicked.connect(self.on_cancel_btn) 31 | self.ui.name_input.textChanged.connect(self.on_name_input) 32 | self.ui.age_input.valueChanged.connect(self.on_age_input) 33 | self.ui.address_input.textChanged.connect(self.on_address_input) 34 | self.ui.avatar_upload_btn.clicked.connect(self.on_avatar_upload_btn) 35 | self.ui.refresh.clicked.connect(self.refresh_avg_mark) 36 | 37 | self.load_student() 38 | 39 | def load_student(self) -> None: 40 | with self.container() as r_container: 41 | command = r_container.get(ReadCurrentStudent) 42 | student = command.execute() 43 | 44 | self.ui.name_input.setText(student.name) 45 | self.name = student.name 46 | 47 | if student.age: 48 | self.age = student.age 49 | self.ui.age_input.setValue(student.age) 50 | if student.home_address: 51 | self.home_address = student.home_address 52 | self.ui.address_input.setText(student.home_address) 53 | 54 | self.ui.avg_mark.setValue(student.student_overall_avg_mark) 55 | 56 | self.avatar = student.avatar 57 | self.update_avatar_preview() 58 | 59 | def refresh_avg_mark(self) -> None: 60 | with self.container() as r_container: 61 | command = r_container.get(ReadCurrentStudent) 62 | student = command.execute() 63 | self.ui.avg_mark.setValue(student.student_overall_avg_mark) 64 | 65 | def update_avatar_preview(self) -> None: 66 | if self.avatar: 67 | pixmap = QPixmap(self.avatar) 68 | self.ui.avatar_preview.setPixmap( 69 | pixmap.scaled(100, 100), 70 | ) 71 | else: 72 | self.ui.avatar_preview.setText("Аватар не выбран") 73 | 74 | def on_submit_btn(self) -> None: 75 | with self.container() as r_container: 76 | data = UpdatedStudent( 77 | name=self.name, 78 | age=self.age, 79 | home_address=self.home_address, 80 | avatar=self.avatar, 81 | ) 82 | command = r_container.get(UpdateStudent) 83 | command.execute(data) 84 | self.close() 85 | 86 | def on_name_input(self) -> None: 87 | self.name = self.ui.name_input.text() 88 | 89 | def on_age_input(self) -> None: 90 | self.age = self.ui.age_input.value() 91 | 92 | def on_address_input(self) -> None: 93 | self.home_address = self.ui.address_input.text() 94 | 95 | def on_avatar_upload_btn(self) -> None: 96 | file_dialog = QFileDialog(self) 97 | file_path, _ = file_dialog.getOpenFileName( 98 | self, 99 | "Выберите файл аватара", 100 | "", 101 | "Изображения (*.png *.jpg *.jpeg *.bmp)", 102 | ) 103 | if file_path: 104 | self.avatar = file_path 105 | self.update_avatar_preview() 106 | 107 | def on_cancel_btn(self) -> None: 108 | self.load_student() 109 | -------------------------------------------------------------------------------- /tests/unit/teacher/test_teacher.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from student_journal.application.exceptions.base import ApplicationError 4 | from student_journal.application.exceptions.teacher import ( 5 | TeacherFullNameError, 6 | ) 7 | from student_journal.application.invariants.teacher import FULL_NAME_MAX_LENGTH 8 | from student_journal.application.models.teacher import TeachersReadModel 9 | from student_journal.application.teacher import ( 10 | CreateTeacher, 11 | DeleteTeacher, 12 | NewTeacher, 13 | ReadTeacher, 14 | ReadTeachers, 15 | UpdatedTeacher, 16 | UpdateTeacher, 17 | ) 18 | from unit.student.mock import MockedTeacherGateway, MockedTransactionManager 19 | from unit.teacher.conftest import TEACHER, TEACHER2, TEACHER_ID 20 | 21 | BAD_INVARIANTS = ( 22 | [ 23 | ((FULL_NAME_MAX_LENGTH + 1) * "a", None, TeacherFullNameError), 24 | ], 25 | ) 26 | 27 | 28 | def test_create_teacher( 29 | create_teacher: CreateTeacher, 30 | transaction_manager: MockedTransactionManager, 31 | teacher_gateway: MockedTeacherGateway, 32 | ) -> None: 33 | data = NewTeacher( 34 | full_name="John Doe", 35 | avatar=None, 36 | ) 37 | 38 | create_teacher.execute(data) 39 | 40 | assert transaction_manager.is_begin 41 | assert transaction_manager.is_commited 42 | assert teacher_gateway.is_wrote 43 | 44 | 45 | @pytest.mark.parametrize( 46 | ("full_name", "avatar", "exc_class"), 47 | *BAD_INVARIANTS, 48 | ) 49 | def test_create_teacher_bad_invariants( 50 | create_teacher: CreateTeacher, 51 | full_name: str, 52 | avatar: str | None, 53 | exc_class: type[ApplicationError], 54 | ) -> None: 55 | data = NewTeacher( 56 | avatar=avatar, 57 | full_name=full_name, 58 | ) 59 | 60 | with pytest.raises(exc_class): 61 | create_teacher.execute(data) 62 | 63 | 64 | def test_read_teacher( 65 | read_teacher: ReadTeacher, 66 | teacher_gateway: MockedTeacherGateway, 67 | ) -> None: 68 | teacher_gateway.write_teacher(TEACHER) 69 | teacher = read_teacher.execute(TEACHER_ID) 70 | 71 | assert teacher.teacher_id == TEACHER_ID 72 | 73 | 74 | def test_read_teachers( 75 | read_teachers: ReadTeachers, 76 | teacher_gateway: MockedTeacherGateway, 77 | ) -> None: 78 | teacher_gateway.write_teacher(TEACHER) 79 | teacher_gateway.write_teacher(TEACHER2) 80 | teachers: TeachersReadModel = read_teachers.execute() 81 | 82 | assert TEACHER in teachers.teachers 83 | assert TEACHER2 in teachers.teachers 84 | 85 | 86 | def test_update_teacher( 87 | update_teacher: UpdateTeacher, 88 | teacher_gateway: MockedTeacherGateway, 89 | transaction_manager: MockedTransactionManager, 90 | ) -> None: 91 | teacher_gateway.write_teacher(TEACHER) 92 | 93 | updated_id = update_teacher.execute( 94 | UpdatedTeacher( 95 | teacher_id=TEACHER_ID, 96 | avatar=TEACHER.avatar, 97 | full_name=TEACHER.full_name, 98 | ), 99 | ) 100 | 101 | assert teacher_gateway.is_updated 102 | assert transaction_manager.is_begin 103 | assert transaction_manager.is_commited 104 | assert updated_id == TEACHER_ID 105 | 106 | 107 | @pytest.mark.parametrize( 108 | ("full_name", "avatar", "exc_class"), 109 | *BAD_INVARIANTS, 110 | ) 111 | def test_update_teacher_bad_invariants( 112 | teacher_gateway: MockedTeacherGateway, 113 | update_teacher: UpdateTeacher, 114 | full_name: str, 115 | avatar: str | None, 116 | exc_class: type[ApplicationError], 117 | ) -> None: 118 | teacher_gateway.write_teacher(TEACHER) 119 | 120 | with pytest.raises(exc_class): 121 | update_teacher.execute( 122 | UpdatedTeacher( 123 | teacher_id=TEACHER_ID, 124 | avatar=avatar, 125 | full_name=full_name, 126 | ), 127 | ) 128 | 129 | 130 | def test_delete_teacher( 131 | delete_teacher: DeleteTeacher, 132 | teacher_gateway: MockedTeacherGateway, 133 | transaction_manager: MockedTransactionManager, 134 | ) -> None: 135 | teacher_gateway.write_teacher(TEACHER) 136 | delete_teacher.execute(TEACHER_ID) 137 | 138 | assert transaction_manager.is_begin 139 | assert transaction_manager.is_commited 140 | assert teacher_gateway.is_deleted 141 | -------------------------------------------------------------------------------- /src/student_journal/presentation/ui/about_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'about.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.4.2 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_About(object): 13 | def setupUi(self, About): 14 | About.setObjectName("About") 15 | About.setWhatsThis("") 16 | self.gridLayout = QtWidgets.QGridLayout(About) 17 | self.gridLayout.setObjectName("gridLayout") 18 | self.label = QtWidgets.QLabel(parent=About) 19 | font = QtGui.QFont() 20 | font.setPointSize(18) 21 | font.setBold(True) 22 | self.label.setFont(font) 23 | self.label.setObjectName("label") 24 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 25 | self.textBrowser = QtWidgets.QTextBrowser(parent=About) 26 | self.textBrowser.setObjectName("textBrowser") 27 | self.gridLayout.addWidget(self.textBrowser, 1, 0, 1, 1) 28 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) 29 | sizePolicy.setHorizontalStretch(0) 30 | sizePolicy.setVerticalStretch(0) 31 | 32 | self.retranslateUi(About) 33 | QtCore.QMetaObject.connectSlotsByName(About) 34 | 35 | def retranslateUi(self, About): 36 | _translate = QtCore.QCoreApplication.translate 37 | About.setWindowTitle(_translate("About", "О программе")) 38 | self.label.setText(_translate("About", "О программе")) 39 | self.textBrowser.setHtml(_translate("About", "\n" 40 | "\n" 46 | "

ДневникШкольника!

\n" 47 | "


\n" 48 | "

Специализированное приложение для ведения школьного дневника: календарь и заметки с функцией записи домашнего задания.

\n" 49 | "


\n" 50 | "

Разработано как проект для Я. Лицея.

\n" 51 | "


\n" 52 | "

Разработано этими замечательными людьми и многообещающими мужчинами:

\n" 53 | "

- Любавский Илья

\n" 54 | "

- Роман Мельниченко

")) 55 | --------------------------------------------------------------------------------