Skip to content

MPV 2 Done, Getting ready for deploy - #74

Merged
ZolotarevAlexandr merged 101 commits into
mainfrom
dev
Apr 5, 2026
Merged

MPV 2 Done, Getting ready for deploy#74
ZolotarevAlexandr merged 101 commits into
mainfrom
dev

Conversation

@upconett

@upconett upconett commented Apr 5, 2026

Copy link
Copy Markdown
Member

Description of changes

Added core functionality for the MVP 2 of Academic Tutorship Bot
Getting ready for the deploy

upconett added 24 commits March 22, 2026 18:35
TODO:
- create new notification for room & date change
- create the admin approval mechanism
- Implemented `clear_aiogram_redis.py` to remove specific keys from Redis related to aiogram and aiogram_dialog.
- Added functionality for dry-run mode to preview keys without deletion.

fix: Improve meeting info text rendering

- Updated `MeetingInfoText` to return "Meeting" if rendered text is empty.

fix: Prevent multiple pending updates for meetings

- Added check in `on_date_room_btn` to ensure no pending updates exist before allowing changes.

refactor: Consolidate meeting update logic

- Removed redundant methods for updating meeting date and room.
- Introduced `approve_meeting_update` to handle applying updates and notifying participants.

feat: Enhance notification system for meeting updates

- Added new notification texts and methods for sending update requests and approvals.
- Updated notification manager to handle meeting update notifications.

fix: Update i18n files to include new notification texts

- Added new keys for meeting update notifications in `notifications.ftl`.

refactor: Clean up meeting repository methods

- Renamed `del_update` to `remove_update` for clarity.
- Changed project name in pyproject.toml from "aiogram-template" to "academic-tutorship-bot".
- Added proxy_url field to settings.schema.yaml and updated config_schema.py.
- Implemented proxy support in app.py for the bot session.
- Updated i18n.py to use the new locales directory path.
- Enhanced logging configuration to handle permission errors for file handlers.
- Updated dependencies in uv.lock and pyproject.toml, including beanie and aiohttp-socks.
Copilot AI review requested due to automatic review settings April 5, 2026 17:30
@upconett upconett self-assigned this Apr 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements MVP2 core functionality for the Academic Tutorship Bot, adding scheduling, notification-bot support, DB repositories/schema, authentication flow, and a new aiogram_dialog-based UI with i18n.

Changes:

  • Added scheduling infrastructure (APScheduler + meeting reminder/conduct/finish jobs) and notification bot wiring.
  • Introduced new DB schema/repositories/models for students/admins/tutors/meetings/updates/attendance + Alembic migrations.
  • Replaced old routers with a multi-dialog UI (root/settings/guide/meetings/tutors/etc.) plus fluent-based localization and new tests/scripts.

Reviewed changes

Copilot reviewed 151 out of 158 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/test_student_repo.py Adds StudentRepository create/admin-flag tests.
tests/test_scheduling.py Adds tests for meeting scheduling and reminder job behavior.
tests/test_auto_auth_middleware.py Adds tests for AutoAuthMiddleware status updates.
tests/test_admin_repo.py Adds AdminRepository sync-with-config tests.
tests/fixtures.py Adds async DB/repository fixtures (sqlite in-memory).
src/utils/colors.py Introduces ANSI color constants.
src/scheduling/scheduling.py Implements meeting scheduling + reminder/conduct/finish job handlers.
src/scheduling/scheduler.py Adds scheduler configuration with Redis/Memory jobstore selection.
src/prepare.py Switches pre-commit install command from poetry to uv.
src/notifications/texts.py Adds notification message templates (legacy-style).
src/notifications/bot.py Creates notification bot + dispatcher objects.
src/notifications/init.py Exposes NotificationManager instance and handler init hook.
src/domain/models.py Adds/updates domain models (Student/Tutor/Meeting/etc.) and statuses.
src/db/schema.py Defines SQLAlchemy tables for students/admins/tutors/meetings/updates/etc.
src/db/repositories/tutor.py Adds TutorRepository CRUD + profile/photo/discipline methods.
src/db/repositories/sql.py Adds SQLDatabase + base Repository.
src/db/repositories/discipline.py Adds DisciplineRepository query/create methods.
src/db/repositories/admin.py Adds AdminRepository and sync_with_config implementation.
src/db/repositories/init.py Instantiates shared DB + repository singletons.
src/config_schema.py Extends app settings (accounts integration, db_url, proxy, notification bot token, mock_auth).
src/bot/utils.py Expands bot utilities (dialog helpers, parsing, keyboards, attendance parsing).
src/bot/user_errors.py Adds user-facing exception types for dialog flows.
src/bot/routers/user.py Removes old placeholder dialog router.
src/bot/routers/registration.py Removes old registration flow router.
src/bot/routers/queries.py Adds callback handler router (e.g., delete warning).
src/bot/logging_.py Enhances logging setup and adds structured log helpers.
src/bot/i18n.py Adds fluent-based dialog i18n utilities + notification l10n loader.
src/bot/filters.py Reworks filters for authenticated/status/email-entered flows.
src/bot/exceptions.py Adds auth/authority exception types.
src/bot/dialogs/tutors/windows.py Adds admin tutor-control dialog windows.
src/bot/dialogs/tutors/states.py Adds tutor-control dialog states.
src/bot/dialogs/tutors/logic.py Adds tutor add/remove logic with notifications and safety checks.
src/bot/dialogs/tutors/handles.py Adds tutor-control handlers (select/add/remove).
src/bot/dialogs/tutors/getters.py Adds tutor list/info getters for tutor-control dialog.
src/bot/dialogs/tutors/dialog_buttons.py Adds scrolling/select widgets for tutor-control dialog.
src/bot/dialogs/tutors/init.py Wires tutor-control dialog and auth guard middleware.
src/bot/dialogs/tutors_profile/windows.py Adds tutor profile and profile-control windows.
src/bot/dialogs/tutors_profile/states.py Adds tutor profile dialog states.
src/bot/dialogs/tutors_profile/handles.py Adds tutor profile handlers (select, edit, disciplines).
src/bot/dialogs/tutors_profile/getters.py Adds tutor profile getters (self/other + selected disciplines).
src/bot/dialogs/tutors_profile/dialog_buttons.py Adds tutor profile list scrolling widgets.
src/bot/dialogs/tutors_profile/init.py Wires tutor profile dialog and auth guard middleware.
src/bot/dialogs/student_meetings/windows.py Adds student meeting list/info windows.
src/bot/dialogs/student_meetings/states.py Adds student meeting dialog states.
src/bot/dialogs/student_meetings/handles.py Adds student meeting selection handler.
src/bot/dialogs/student_meetings/getters.py Adds student meeting list/info getters and date filtering.
src/bot/dialogs/student_meetings/dialog_buttons.py Adds meeting list scrolling widgets for students.
src/bot/dialogs/student_meetings/init.py Wires student meeting dialog and auth guard middleware.
src/bot/dialogs/root/windows.py Adds root/start/settings windows and navigation wiring.
src/bot/dialogs/root/states.py Adds root dialog states.
src/bot/dialogs/root/handles.py Adds settings handlers (language toggle, notification toggle, disciplines).
src/bot/dialogs/root/getters.py Adds root start/settings getters.
src/bot/dialogs/root/init.py Wires root dialog and auth guard middleware.
src/bot/dialogs/meetings/states.py Adds tutor/admin meetings dialog states.
src/bot/dialogs/meetings/logic.py Adds meeting lifecycle logic (create/approve/announce/cancel/finish/update-approve).
src/bot/dialogs/meetings/getters.py Adds meetings type/list/info/create getters with role-based filtering.
src/bot/dialogs/meetings/dialog_buttons.py Adds meeting list scrolling widgets for tutor/admin flows.
src/bot/dialogs/meetings/init.py Wires meetings dialog and auth guard middleware.
src/bot/dialogs/guide/windows.py Adds onboarding/guide dialog windows (language/disciplines/notifications).
src/bot/dialogs/guide/states.py Adds guide dialog states.
src/bot/dialogs/guide/handles.py Adds guide handlers (language select, finish guide).
src/bot/dialogs/guide/getters.py Adds guide getters (notification bot activation state, etc.).
src/bot/dialogs/guide/init.py Wires guide dialog and auth guard middleware.
src/bot/dialogs/discipline_picker/windows.py Adds discipline picker windows (language/year/discipline + multi select).
src/bot/dialogs/discipline_picker/states.py Adds discipline picker states.
src/bot/dialogs/discipline_picker/handles.py Adds discipline picker selection handlers.
src/bot/dialogs/discipline_picker/getters.py Adds discipline picker getters and multi-select state building.
src/bot/dialogs/discipline_picker/dialog_buttons.py Adds picker scrolling widgets for language/year/discipline selection.
src/bot/dialogs/discipline_picker/init.py Wires discipline picker dialog and auth guard middleware.
src/bot/dialogs/change_meeting/windows.py Adds meeting edit dialog windows (title/discipline/date/room/tutor/etc.).
src/bot/dialogs/change_meeting/states.py Adds meeting edit dialog states.
src/bot/dialogs/change_meeting/logic.py Adds meeting edit business logic (assign tutor, duration, date combine, etc.).
src/bot/dialogs/change_meeting/keyboards.py Adds “choose user” reply keyboard for tutor assignment.
src/bot/dialogs/change_meeting/getters.py Adds getters for meeting edit UI and tutor lists.
src/bot/dialogs/change_meeting/dialog_buttons.py Adds meeting edit scrolling/select widgets and navigation buttons.
src/bot/dialogs/change_meeting/init.py Wires change-meeting dialog and auth guard middleware.
src/bot/dialogs/authentication/windows.py Adds authentication/bind window.
src/bot/dialogs/authentication/states.py Adds authentication dialog state(s).
src/bot/dialogs/authentication/handles.py Adds “check connected” handler to route into root upon auth.
src/bot/dialogs/authentication/getters.py Adds binding URL getter based on bot username and settings.
src/bot/dialogs/authentication/init.py Wires authentication dialog.
src/bot/dialogs/attendance/windows.py Adds attendance dialog windows (download/resend/add/close).
src/bot/dialogs/attendance/states.py Adds attendance dialog states.
src/bot/dialogs/attendance/logic.py Adds attendance file parsing/storage and close-meeting flow.
src/bot/dialogs/attendance/handles.py Adds attendance handlers (download/resend/close/add email).
src/bot/dialogs/attendance/init.py Wires attendance dialog and auth guard middleware.
src/bot/dialog_extension/extended_fsm_context.py Adds typed FSM state helpers and sync context managers.
src/bot/dialog_extension/extended_dialog_manager.py Adds extended dialog manager utilities (tracking/cleanup/messages/i18n).
src/bot/dialog_extension/dialog_wrapper.py Adds delegating wrapper for DialogManager.
src/bot/dialog_extension/init.py Exports dialog extension helpers.
src/bot/custom_widgets/unpacked_list.py Adds List widget variant that unpacks dict items for fluent formatting.
src/bot/custom_widgets/tutors_profile_text.py Adds custom widget for rendering tutor profile text with fluent formatting.
src/bot/custom_widgets/meeting_update_text.py Adds custom widget for rendering meeting update summaries.
src/bot/custom_widgets/meeting_info_text.py Adds custom widget for rendering meeting info views.
src/bot/custom_widgets/meeting_date_room_text.py Adds custom widget for rendering date/room quick view.
src/bot/custom_widgets/init.py Exports custom widgets.
src/bot/constants.py Adds constants used by middlewares/widgets (i18n format key).
src/bot/bot_container.py Adds a global Bot container to access Bot in lower layers.
src/bot/app.py Reworks app bootstrap: proxy support, auth middleware, dialogs, scheduler, notification polling, startup/shutdown routines.
src/accounts_sdk.py Adds InNoHassle Accounts client + token decoding + user lookup.
settings.schema.yaml Updates settings schema for new config fields (accounts/db/notification bot/etc.).
settings.example.yaml Updates example settings with db_url and notification bot token.
scripts/run_tests.sh Adds uv-based pytest runner script.
scripts/clear_aiogram_redis.py Adds Redis cleanup script for aiogram/aiogram_dialog keys.
scripts/alembic_auto_migrations.py Adds helper for autogenerating alembic migrations from commit message.
pyproject.toml Renames project and adds dependencies + ruff ignore for star imports.
logging.yaml Adds file handler and logger wiring for app logs.
locales/ru/LC_MESSAGES/notification_handles.ftl Adds RU fluent strings for notification-handlers UI.
locales/ru/LC_MESSAGES/handles.ftl Adds RU fluent strings for UI messages.
locales/en/LC_MESSAGES/notifications.ftl Adds EN fluent strings for notification content.
locales/en/LC_MESSAGES/notification_handles.ftl Adds EN fluent strings for notification-handlers UI.
locales/en/LC_MESSAGES/handles.ftl Adds EN fluent strings for UI messages.
docs/README.md Adds naming scheme notes.
docs/disciplines.sql Adds seed SQL for disciplines.
docker-compose.yaml Adds db dependency to app service.
alembic/versions/2026_04_05_15_57.py Migration: meeting.tutor_id FK adjustment.
alembic/versions/2026_04_05_15_54.py Migration: meeting.creator_id nullable change.
alembic/versions/2026_04_04_17_51.py Migration: meeting_update table creation.
alembic/versions/2026_03_26_15_26.py Migration: student.language column addition.
alembic/versions/2026_03_21_21_52.py Migration: student.saw_guide column addition.
alembic/versions/2026_03_21_13_46.py Migration: student.notification_bot_status column addition.
alembic/versions/_2026_03_19_16_10_add_meeting_approving_status.py Migration: inserts APPROVING status via status shift.
alembic/versions/_2026_03_09_12_25_email_not_null.py Migration: email.value NOT NULL.
alembic/versions/_2026_03_07_18_29_better_foreign_keys.py Migration: improves FK ondelete behaviors.
alembic/versions/_2026_03_07_15_40_initial.py Migration: initial schema.
alembic/script.py.mako Adds alembic revision template.
alembic/README.md Adds migration usage docs.
alembic/env.py Adds async alembic env + db_url loading from settings/env.
alembic.ini Adds alembic config and ruff post-write hook.
.vscode/launch.json Adds debug configuration for running bot module.
.pre-commit-config.yaml Adjusts alembic auto-migration hook file scope.
.gitignore Expands ignores (sqlite, logs, OS/editor artifacts).
.dockerignore Ensures locales are included in Docker build context.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +78 to +83
meeting = await meeting_repo.get(meeting_id)
if not meeting:
logger.warning("error in _job_meeting_conduct: no meeting")
try:
meeting.conduct()
await meeting_repo.update(meeting, ["status"])

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_job_meeting_conduct() logs a warning when the meeting is not found, but then continues into the try-block and will raise an AttributeError on meeting.conduct(). Return immediately after the not-found check to avoid noisy errors and unintended behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +110
async def _job_meeting_finish(meeting_id: int):
meeting = await meeting_repo.get(id=meeting_id)
if not meeting:

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_job_meeting_finish() calls meeting_repo.get(id=meeting_id), but MeetingRepository.get() takes a positional id argument (no keyword). This will raise a TypeError at runtime; call meeting_repo.get(meeting_id) instead.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +18
def _parse_redis_url(url: str) -> dict:
parsed = urlparse(url)
return {
"host": parsed.hostname,
"port": parsed.port,
"db": int(parsed.path.lstrip("/")) if parsed.path else None,
"password": parsed.password,
}

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_parse_redis_url() returns db=None when the Redis URL has no path. RedisJobStore expects an integer DB index; default to 0 (or omit the key) to avoid runtime errors for URLs like redis://host:6379.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +37
_executors = {"default": AsyncIOExecutor()}

_job_defaults = {"misfire_grac"}

scheduler = AsyncIOScheduler(
jobstores=_jobstores,
executors=_executors,
)

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_job_defaults = {"misfire_grac"} is a set (not a dict), has a truncated key name, and is never passed into AsyncIOScheduler. Either remove it or define a proper dict (e.g., misfire_grace_time/coalesce/max_instances) and pass it via job_defaults=.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +50
photo_stmt = insert(photo).values(telegram_file_id=telegram_file_id, file_path=file_path).returning(photo.c.id)
async with self._db.engine.begin() as conn:
result = await conn.execute(photo_stmt)
photo_id = result.scalar_one()
tutor_stmt = update(tutor).values(photo_id=photo_id)
await conn.execute(tutor_stmt)

async def unset_photo(self, tutor_id: int):
stmt = update(tutor).values(photo_id=None)
async with self._db.engine.begin() as conn:
await conn.execute(stmt)

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TutorRepository.set_photo() updates the tutor table without a WHERE clause, so it will set photo_id for all tutors. Add where(tutor.c.id == tutor_id) (same issue applies to unset_photo()).

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +68
DISCIPLINE_YEAR_SCROLL = ScrollingGroup(
Select(
Format("{item[1]}"),
id="select_year",
item_id_getter=(lambda item: item[0]),
items="years",
on_click=on_year_select,
),
id="scroll_year",
width=1,
height=15,
hide_pager=True,
)


DISCIPLINE_SCROLL = ScrollingGroup(
Select(
Format("{item[1][display]}"),
id="select_discipline",
item_id_getter=(lambda item: item[0]),
items="disciplines",
on_click=on_discipline_select,
),
id="scroll_year",
width=1,
height=30,
hide_pager=True,
)


DISCIPLINE_MULTI_SCROLL = ScrollingGroup(
Select(
Format("{item[2]} {item[1][display]}"),
id="select_discipline",
item_id_getter=(lambda item: item[0]),
items="disciplines_multi",
on_click=on_discipline_select_multi,
),
id="scroll_year",
width=1,
height=30,
hide_pager=True,
)

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dialog defines duplicate widget IDs: ScrollingGroup uses id="scroll_year" in three different widgets and Select uses id="select_discipline" twice. aiogram_dialog requires widget IDs to be unique within a dialog; this will raise at startup. Rename these IDs to be distinct (e.g., scroll_discipline, scroll_discipline_multi, select_discipline_multi).

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +93
async def on_toggle_notifications(query: CallbackQuery, _, manager: DialogManager):
manager = extend_dialog(manager)
_ = manager.tr
try:
self_student = await manager.state.get_self_student()
s = self_student.settings
log_info(
"student.notifications.toggle.requested",
user_id=query.from_user.id,
current_value=s.receive_notifications,
)
s.receive_notifications = not s.receive_notifications
await student_repo.update(self_student, ["receive_notifications"])
await notification_manager.send_receive_notification_toggled(self_student.id, s.receive_notifications)
self_student = await student_repo.get(self_student.telegram_id) # check if notification really sent

if self_student.notification_bot_status != NotificationBotStatus.ACTIVATED:
# NOTE: we don't care if user deactivates notifications and blocks bot
if s.receive_notifications:
raise PermissionError("Notification bot is not activated")

await student_repo.update(self_student, ["receive_notifications"])
await query.answer(_("Q_SETTINGS_NOTIF_ON") if s.receive_notifications else _("Q_SETTINGS_NOTIF_OFF"))
await manager.state.set_self_student(self_student)
log_info(
"student.notifications.toggle.succeeded",
user_id=query.from_user.id,
new_value=s.receive_notifications,
)
except PermissionError:
log_warning(
"student.notifications.toggle.permission_denied",
user_id=query.from_user.id,
)
self_student.settings.receive_notifications = False
await student_repo.update(self_student, ["receive_notifications"])
await query.answer(_("Q_SETTINGS_NOTIF_BOT_NOT_ACTIVATED"))
except Exception as e:
log_error("student.notifications.toggle.failed", user_id=query.from_user.id, reason=str(e))
raise
await query.answer()
await manager.switch_to_current()

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on_toggle_notifications() answers the callback query inside the try/except and then unconditionally calls await query.answer() again at the end. Answering twice can raise TelegramBadRequest / cause unnecessary API calls; remove the final unconditional answer (or only answer once in a single place).

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +35
def _filter_meetings_by_date(meetings: list[Meeting]) -> list[Meeting]:
"""Filter meetings which finish date is within last 12 hours or later in future"""
now = datetime.now()
filtered = []
for m in meetings:
if m.datetime_ and (m.datetime_ + timedelta(seconds=m.duration)) > now:
filtered.append(m)
return filtered

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says it filters meetings whose finish date is within the last 12 hours, but the implementation only keeps meetings whose (datetime_ + duration) is after now (i.e., not finished yet). Either update the logic to match the 12h requirement or update the docstring to reflect the actual behavior.

Copilot uses AI. Check for mistakes.
Comment thread src/accounts_sdk.py
Comment on lines +167 to +174
if settings.accounts:
inh_accounts: InNoHassleAccounts = InNoHassleAccounts(
api_url=settings.accounts.api_url,
api_jwt_token=settings.accounts.api_jwt_token.get_secret_value(),
verify=False,
)
else:
raise ImportError("InNoHassle Accounts is not configured in ./settings.yaml")

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accounts_sdk raises ImportError at import time when settings.accounts is not configured. This makes it impossible to run the app in modes like mock_auth (and breaks any module importing inh_accounts). Prefer deferring the error to first use, or set inh_accounts to None/a stub and have middleware/routers handle the missing configuration gracefully.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +45
format_text = manager.middleware_data.get(
I18N_FORMAT_KEY,
_default_format_text,
)

meeting_update = await manager.state.get_value("meeting_updated", default={})
meeting = await manager.state.get_meeting()

update_data = self.__filter_valid_update_data(meeting_update, meeting)

lines = []
lines.append(format_text("MEETING_UPDATE_HEADER"))
for key, value in update_data.items():
if key in ["datetime", "datetime_"]:
lines.append(format_text("MEETING_UPDATE_DATETIME_LINE", datetime=normalize_l10n_value(value)))
elif key in ["room"]:
lines.append(format_text("MEETING_UPDATE_ROOM_LINE", room=value))
else:

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MeetingUpdateText uses the i18n formatter incorrectly: format_text is set to FluentLocalization.format_value, which expects (message_id, args_dict). The code calls it with keyword args (e.g., format_text("MEETING_UPDATE_ROOM_LINE", room=value)) which will raise TypeError. Pass a dict as the second positional argument (or use manager.tr), and consider aligning the state key name (meeting_updated vs meeting_update).

Copilot uses AI. Check for mistakes.
@ZolotarevAlexandr
ZolotarevAlexandr merged commit 265a159 into main Apr 5, 2026
13 checks passed
@ZolotarevAlexandr
ZolotarevAlexandr deleted the dev branch April 5, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants