├── 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 |
ДневникШкольника!
\n" 47 | "Специализированное приложение для ведения школьного дневника: календарь и заметки с функцией записи домашнего задания.
\n" 49 | "Разработано как проект для Я. Лицея.
\n" 51 | "Разработано этими замечательными людьми и многообещающими мужчинами:
\n" 53 | "- Любавский Илья
\n" 54 | "- Роман Мельниченко
")) 55 | --------------------------------------------------------------------------------