### Environment Configuration Variables (.env)
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
This outlines the structure of the `.env` file used for environment configuration, including settings for the Telegram bot token, admin ID, database credentials, web converter login and password, logging level, and audio processing parameters like max file size and default bitrate. It shows an example `.env.example` file structure.
```bash
# .env file structure (copy from .env.example):
# Telegram Bot
BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ
ADMIN_TELEGRAM_ID=123456789
# Database
DB_NAME=audio_bot
DB_USER=postgres
DB_PASSWORD=secure_password
DB_HOST=db
DB_PORT=5432
# Web Converter
WEB_CONVERTER_LOGIN=admin
WEB_CONVERTER_PASSWORD=secure_converter_pass
WEB_CONVERTER_SECRET=random_secret_key
# Logging
LOG_LEVEL=INFO
# Audio Settings
MAX_FILE_SIZE_MB=50
DEFAULT_BITRATE_KBPS=64
```
--------------------------------
### Docker Compose Deployment and Management (YAML/Bash)
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
This section details Docker Compose configurations for deploying and managing the Telegram bot and its associated services (database, adminer, web converter). It includes commands for starting, viewing logs, restarting, executing commands within containers, performing database migrations, accessing PostgreSQL, stopping, and resetting services. It assumes a `docker-compose.yml` file is present.
```yaml
# docker-compose.yml structure:
services:
bot: # Main Telegram bot (aiogram 3.x)
db: # PostgreSQL 15 with Moscow timezone
adminer: # Database admin UI (port 8080)
web-converter: # Audio converter service (port 1992)
# Start all services:
docker-compose up -d --build
# View bot logs:
docker-compose logs bot --tail=50 --follow
# Restart bot after code changes:
docker-compose restart bot
# Execute commands in bot container:
docker-compose exec bot python init_data.py
# Database migrations:
docker-compose exec bot alembic revision --autogenerate -m "Add field"
docker-compose exec bot alembic upgrade head
# Make user admin:
docker-compose exec db psql -U postgres -d audio_bot -c \
"UPDATE users SET role_id = 1 WHERE telegram_id = 123456789;"
# Access PostgreSQL:
docker-compose exec db psql -U postgres -d audio_bot
# Stop all services:
docker-compose down
# Reset database (WARNING: destroys all data):
docker-compose down -v
docker-compose up -d
```
--------------------------------
### Lesson Management: Create, Update, Delete, and Get Lessons (Python)
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Demonstrates the core CRUD operations for lessons in the tg-islamic-bot. This includes creating new lessons linked to series, updating existing lesson details, deleting lessons, and fetching lessons with detailed filtering and eager loading of related objects like series, teachers, and books. It utilizes SQLAlchemy for asynchronous database interactions.
```python
new_lesson = await create_lesson(
series_id=45, # Required - links to lesson_series
book_id=12,
teacher_id=3,
theme_id=1,
title="Введение в науку хадис",
lesson_number=1,
description="Определение хадиса и его важность",
audio_path="bot/audio_files/teacher_name/book_name/2024_series/урок_1_введение.mp3",
duration_seconds=3600, # 1 hour
tags="хадис, мусталах, введение"
)
# Update lesson (modify object then call update)
lesson.title = "Новое название"
lesson.description = "Обновленное описание"
lesson.tags = "новый_тег, другой_тег"
await update_lesson(lesson)
# CRITICAL: Reload lesson after update to get fresh relationships
lesson = await get_lesson_by_id(lesson.id)
# Delete lesson (cascades to bookmarks, test_questions; sets test_attempts.lesson_id to NULL)
success = await delete_lesson(lesson_id=100)
# Get all lessons with filters
from bot.models import Lesson, async_session_maker
from sqlalchemy import select
from sqlalchemy.orm import joinedload
async with async_session_maker() as session:
result = await session.execute(
select(Lesson)
.options(
joinedload(Lesson.series),
joinedload(Lesson.teacher),
joinedload(Lesson.book).joinedload(Book.author)
)
.where(Lesson.is_active == True)
.where(Lesson.series_id == 45)
.order_by(Lesson.lesson_number)
)
lessons = result.scalars().unique().all()
```
--------------------------------
### User and Role Management (Python)
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Handles user authentication and role-based access control. This snippet shows how to get or create users based on Telegram ID, retrieve user roles, and update user roles. It interacts with the database service to manage user data and their associated roles.
```python
from bot.services.database_service import (
get_user_by_telegram_id,
get_or_create_user,
update_user_role,
get_all_users
)
# Get or create user from Telegram data
user = await get_or_create_user(
telegram_id=123456789,
username="john_doe",
first_name="John",
last_name="Doe"
)
# If user exists: returns existing user
# If new: creates with role_id=3 (User role)
# Check user role (relationship pre-loaded)
print(f"Role: {user.role.name}") # "admin", "moderator", or "user"
print(f"Is admin: {user.role.name == 'admin'}")
# Update user role
await update_user_role(telegram_id=123456789, role_id=1) # Make admin
await update_user_role(telegram_id=123456789, role_id=2) # Make moderator
await update_user_role(telegram_id=123456789, role_id=3) # Make user
# List all users (paginated)
users = await get_all_users(limit=50, offset=0)
for user in users:
print(f"{user.telegram_id} ({user.username}): {user.role.name}")
```
--------------------------------
### Get Feedback Counts by Status (Python)
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
This Python code snippet retrieves the counts of feedbacks based on their status (new, replied, closed) using an asynchronous function `get_feedbacks_by_status`. It then prints a formatted summary of these counts. It assumes the existence of the `get_feedbacks_by_status` function and a `print` statement for output.
```python
new_count = len(await get_feedbacks_by_status("new"))
replied_count = len(await get_feedbacks_by_status("replied"))
closed_count = len(await get_feedbacks_by_status("closed"))
print(f"Feedback stats: 🆕 {new_count} | ✅ {replied_count} | 🔒 {closed_count}")
```
--------------------------------
### Debug Handler Triggering with Regex Filter
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
Example of how to define a handler filter using regular expressions in Python with aiogram. This is useful for matching specific patterns in callback data, ensuring the correct handler is triggered.
```python
from aiogram.filters import F
# Example filter for callback data starting with 'edit_lesson_' followed by digits
# This helps in debugging handler order and filter syntax issues.
# F.data.regexp(r"^edit_lesson_\d+$")
```
--------------------------------
### Manage Test and Question Data
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Facilitates the management of tests and questions within the bot. It includes functions to retrieve test details based on a series ID, get questions associated with a lesson, create new tests and questions, submit test answers, and retrieve a user's test attempts. This functionality relies on the database service.
```python
from bot.services.database_service import (
get_test_by_series,
get_questions_by_lesson,
create_test,
create_test_question,
submit_test_answer,
get_user_test_attempts
)
# Get test for series
test = await get_test_by_series(series_id=45)
if test:
print(f"{test.display_title}") # "Экзамен (2024 - Основы Акыды)"
print(f"Questions: {test.questions_count}")
print(f"Passing score: {test.passing_score}% ({test.passing_points}/{test.max_score})")
print(f"Time: {test.formatted_time}") # "15 мин 0 сек"
print(f"Full info:\n{test.full_info}")
```
--------------------------------
### Moscow Time Operations (Python)
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
This Python code demonstrates how to handle Moscow time (UTC+3) using `bot.utils.timezone_utils`. It shows how to get the current Moscow time as a naive datetime object suitable for database storage and how to format it for display. It emphasizes the importance of using naive datetimes for database operations to avoid issues.
```python
from bot.utils.timezone_utils import get_moscow_now, MOSCOW_TZ
from datetime import datetime
# Get current Moscow time (naive datetime for DB)
now = get_moscow_now()
print(now) # datetime.datetime(2024, 1, 15, 14, 30, 0)
print(now.tzinfo) # None (naive)
# Store in database (all timestamps use get_moscow_now as default)
from bot.models import Lesson
lesson = Lesson(
title="New lesson",
created_at=get_moscow_now(), # Automatically set if not provided
updated_at=get_moscow_now() # Auto-updated on changes
)
# IMPORTANT: Never use timezone-aware datetimes
# ❌ WRONG:
# aware_time = datetime.now(MOSCOW_TZ)
# lesson.created_at = aware_time # Will cause issues
# ✅ CORRECT:
# naive_time = get_moscow_now()
# lesson.created_at = naive_time
# Display formatted timestamps
print(f"Created: {lesson.created_at.strftime('%d.%m.%Y %H:%M:%S')}")
# "Created: 15.01.2024 14:30:00"
```
--------------------------------
### Project Startup and Initialization Commands
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/AGENTS.md
These bash commands are used to build, run, and initialize the TG Islamic Bot project. They cover full deployment, initial data seeding, and assigning administrator roles.
```bash
# Полный запуск с инициализацией
docker-compose up -d --build
# Инициализация начальных данных (только после первого запуска)
docker-compose exec bot python init_data.py
# Назначение прав администратора
docker-compose exec db psql -U postgres -d audio_bot -c "UPDATE users SET role_id = 1 WHERE telegram_id = ВАШ_TELEGRAM_ID;"
```
--------------------------------
### Database Migration Commands with Alembic
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
This Bash snippet outlines the essential commands for managing database migrations using Alembic within a Dockerized environment. It covers creating new migrations with autogeneration, reviewing the generated scripts, and applying them to the database. It also includes notes on best practices for migration management.
```bash
# Create migration
docker-compose exec bot alembic revision --autogenerate -m "Add field to Lesson"
# Review in alembic/versions/ (autogenerate isn't perfect)
# Apply
docker-compose exec bot alembic upgrade head
```
--------------------------------
### Python: Series Model and Lesson Retrieval
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Illustrates how to fetch and display information about a series, including its metadata, lesson statistics, and a list of its associated lessons. The code demonstrates accessing properties like display name, teacher, book title, theme, completion status, and various lesson counts, as well as iterating through lessons within a series.
```python
from bot.models import LessonSeries
from bot.services.database_service import get_series_by_id, get_lessons_by_series
# Fetch series with relationships
series = await get_series_by_id(series_id=45)
# Series metadata
print(f"Display: {series.display_name}") # "2024 - Основы Акыды"
print(f"Teacher: {series.teacher_name}") # "Мухаммад Абу Мунира"
print(f"Book: {series.book_title}") # "4 правила" or None
print(f"Theme: {series.theme_name}") # "Акыда" (from book or direct)
print(f"Completed: {series.is_completed}") # True/False
# Lesson statistics
print(f"Total lessons: {series.total_lessons}") # 25
print(f"Active lessons: {series.active_lessons_count}") # 23
print(f"Total duration: {series.formatted_total_duration}") # "15в 30м"
# Fetch all lessons in series (sorted by lesson_number)
lesssons = await get_lessons_by_series(series_id=45)
for lesson in lessons:
print(f"Lesson {lesson.lesson_number}: {lesson.title}")
# Full info display (for admin/user views)
info_text = series.full_info
# Output:
# 📚 2024 - Основы Акыды
# 👤 Мухаммад Абу Мунира
# 📚 Книга: 4 правила
# 📋 Тема: Акыда
# 🎧 Уроков: 23/25
# 🕒 Длительность: 15в 30м
# ✅ Серия завершена
```
--------------------------------
### Audio Conversion and Analysis Utilities (Python)
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Provides a suite of tools for managing audio files, including conversion to MP3 with automatic bitrate detection, calculating audio duration, retrieving detailed audio information, and determining the optimal bitrate for a target file size. These utilities are crucial for processing and storing lesson audio content efficiently.
```python
from bot.utils.audio_converter import (
convert_to_mp3_auto,
get_audio_duration,
get_audio_info,
calculate_optimal_bitrate
)
# Convert audio file with automatic bitrate selection
# Tries 64 kbps first, calculates optimal if file > 50 MB
success, error, used_bitrate = await convert_to_mp3_auto(
input_path="/tmp/uploaded_lesson.wav",
output_path="bot/audio_files/teacher/book/series/урок_1.mp3",
preferred_bitrate=64, # kbps
channels=1, # mono
sample_rate=44100,
normalize=True, # Loudness normalization
max_attempts=2
)
if success:
print(f"Converted at {used_bitrate} kbps")
else:
print(f"Error: {error}")
# Get audio duration
duration_seconds = await get_audio_duration("path/to/audio.mp3")
print(f"Duration: {duration_seconds} seconds") # 3600
# Get full audio info
info = await get_audio_info("path/to/audio.mp3")
print(info)
# {
# 'duration': 3600,
# 'format': 'mp3',
# 'bitrate': 64000,
# 'sample_rate': 44100,
# 'channels': 1,
# 'size': 28800000
# }
# Calculate optimal bitrate for target size
optimal = await calculate_optimal_bitrate(
duration_seconds=7200, # 2 hours
target_size_mb=49 # Just under 50 MB limit
)
print(f"Optimal bitrate: {optimal} kbps") # 28 kbps (calculated)
```
--------------------------------
### Python: Manage Lesson Questions and Test Creation
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
This snippet demonstrates how to retrieve questions for a specific lesson, create a new test for a series, add questions to a test, and simulate a user taking and submitting a test. It uses asynchronous functions and database models for test and attempt management.
```python
questions = await get_questions_by_lesson(test_id=test.id, lesson_id=100)
for q in questions:
print(f"Q{q.order}: {q.question_text}")
print(f" Options: {q.options_list}") # ['A', 'B', 'C', 'D']
print(f" Correct: {q.correct_answer}") # 'B'
new_test = await create_test(
series_id=45,
teacher_id=3,
title="Экзамен по серии",
description="Тест на знание основ акыды",
passing_score=80, # 80% to pass
time_per_question_seconds=30
)
question = await create_test_question(
test_id=new_test.id,
lesson_id=100, # Optional: link to specific lesson
question_text="Что такое таухид?",
options="Единобожие;Многобожие;Неверие;Сомнение", # Semicolon-separated
correct_answer="Единобожие",
explanation="Таухид - это единобожие, основа ислама",
order=1
)
from bot.models.test_attempt import TestAttempt
attempt = TestAttempt(
user_id=42,
test_id=new_test.id,
lesson_id=100 # Optional
)
await session.add(attempt)
await session.commit()
score = await submit_test_answer(
attempt_id=attempt.id,
question_id=question.id,
user_answer="Единобожие"
)
attempts = await get_user_test_attempts(user_id=42, test_id=new_test.id)
for attempt in attempts:
print(f"Score: {attempt.score}/{attempt.test.max_score} ({attempt.percentage}%)")
print(f"Passed: {attempt.passed}")
print(f"Date: {attempt.completed_at.strftime('%d.%m.%Y %H:%M')}")
```
--------------------------------
### Handle User Lesson Playback with Audio
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Handles user requests to play lessons, including audio playback, displaying lesson details, checking for associated tests and bookmarks, and managing audio file caching. It uses aiogram and a database service to retrieve lesson information and user data.
```python
from aiogram import Router, F
from aiogram.types import CallbackQuery, FSInputFile
from bot.services.database_service import (
get_lesson_by_id,
get_test_by_series,
get_questions_by_lesson,
get_bookmark_by_user_and_lesson,
get_user_by_telegram_id,
update_lesson
)
from bot.keyboards.user import get_lesson_control_keyboard
from bot.utils.decorators import user_required_callback
from bot.utils.audio_utils import AudioUtils
router = Router()
@router.callback_query(F.data.startswith("lesson_"))
@user_required_callback
async def play_lesson(callback: CallbackQuery):
"""Play lesson with context-aware navigation"""
lesson_id = int(callback.data.split("_")[1])
lesson = await get_lesson_by_id(lesson_id)
if not lesson or not lesson.has_audio():
await callback.answer("Аудиофайл недоступен", show_alert=True)
return
# Build caption with lesson info
caption = (
f"🎧 Урок {lesson.lesson_number}\n\n"
f"📖 Книга: «{lesson.book_title}»\n"
f"✍️ Автор: {lesson.book.author_info if lesson.book else 'Не указан'}\n"
f"🎙️ Преподаватель: {lesson.teacher_name}\n"
f"⏱️ Длительность: {lesson.formatted_duration}\n"
)
if lesson.tags_list:
caption += f"🏷️ Теги: {', '.join(lesson.tags_list)}\n"
if lesson.description:
caption += f"\n📝 {lesson.description}"
# Check for test availability
has_test = False
if lesson.series_id:
test = await get_test_by_series(lesson.series_id)
if test and test.is_active:
questions = await get_questions_by_lesson(test.id, lesson.id)
has_test = len(questions) > 0
# Check for bookmark
user = await get_user_by_telegram_id(callback.from_user.id)
bookmark = await get_bookmark_by_user_and_lesson(user.id, lesson_id) if user else None
has_bookmark = bookmark is not None
# Build keyboard (Previous/Next, Test, Bookmark, Back)
keyboard = get_lesson_control_keyboard(lesson, has_test=has_test, has_bookmark=has_bookmark)
# Delete previous message (single-window pattern)
try:
await callback.message.delete()
except:
pass
# Send audio (with file_id caching)
if lesson.telegram_file_id:
# Fast path: cached file_id
await callback.message.answer_audio(
audio=lesson.telegram_file_id,
caption=caption,
reply_markup=keyboard
)
else:
# Slow path: first upload
audio_file = FSInputFile(lesson.audio_path)
sent = await callback.message.answer_audio(
audio=audio_file,
title=lesson.title,
caption=caption,
reply_markup=keyboard
)
# Cache file_id
if sent.audio:
lesson.telegram_file_id = sent.audio.file_id
await update_lesson(lesson)
await callback.answer()
```
--------------------------------
### Python: Feedback System Workflow
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
This snippet outlines a three-status feedback workflow using asynchronous functions from `database_service`. It covers creating feedback, retrieving feedbacks by status, replying to feedback, and closing feedback. The workflow transitions feedback from 'new' to 'replied' and finally to 'closed'.
```python
from bot.services.database_service import (
create_feedback,
get_feedback_by_id,
get_feedbacks_by_status,
update_feedback_status,
reply_to_feedback,
close_feedback
)
# User submits feedback
feedback = await create_feedback(
user_id=42,
message_text="Не могу найти урок по намазу"
)
# Status: "new"
# Admin views new feedbacks
new_feedbacks = await get_feedbacks_by_status(status="new")
for fb in new_feedbacks:
print(f"{fb.status_emoji} [{fb.created_at.strftime('%d.%m %H:%M')}]")
print(f"User: {fb.user.first_name} (@{fb.user.username})")
print(f"Message: {fb.message_text}\n")
# Admin replies to user
await reply_to_feedback(
feedback_id=feedback.id,
admin_reply="Уроки по намазу находятся в разделе Фикх → Намаз",
bot=bot # Bot instance to send message to user
)
# Status: "new" → "replied"
# replied_at: set to current Moscow time
# User receives reply via bot message
# Admin closes feedback
await close_feedback(feedback_id=feedback.id)
# Status: "replied" → "closed"
# closed_at: set to current Moscow time
```
--------------------------------
### Python: Database Service CRUD Operations
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Demonstrates essential Create, Read, Update, and Delete (CRUD) operations for lessons using the `database_service` layer. It specifically shows how to fetch a lesson with all its related objects (series, teacher, book, theme) eagerly loaded to prevent common ORM errors like `DetachedInstanceError`.
```python
from bot.services.database_service import (
get_lesson_by_id,
create_lesson,
update_lesson,
delete_lesson,
get_all_lessons
)
# Get lesson with all relationships loaded (prevents DetachedInstanceError)
lessson = await get_lesson_by_id(lesson_id=100)
# Relationships loaded: series, teacher, book, book.author, book.theme, theme
```
--------------------------------
### Python: FastAPI Audio Conversion API with aiohttp
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
This snippet defines asynchronous functions to interact with a FastAPI web converter service for audio file conversion. It includes functionalities for uploading and converting files, and downloading the converted output. Dependencies include `aiohttp`.
```python
import aiohttp
async def upload_and_convert(file_path: str, bitrate: str = "64") -> dict:
"""Upload file to web converter service"""
url = "http://web-converter:1992/convert"
async with aiohttp.ClientSession() as session:
with open(file_path, 'rb') as f:
form = aiohttp.FormData()
form.add_field('file', f, filename='lesson.wav')
form.add_field('bitrate', bitrate)
async with session.post(url, data=form) as resp:
if resp.status == 200:
result = await resp.json()
# {
# "filename": "lesson_1234567890.mp3",
# "duration": "1ч 30м 0с",
# "bitrate": 64,
# "mp3_size": "43.2 МБ",
# "original_size": "152.0 МБ"
# }
return result
else:
error = await resp.json()
raise Exception(error['detail'])
# Download converted file
async def download_converted(filename: str, save_path: str):
"""Download converted file from web converter"""
url = f"http://web-converter:1992/download/{filename}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status == 200:
with open(save_path, 'wb') as f:
f.write(await resp.read())
else:
raise Exception("File not found")
# Bitrate options:
# - "64" = 64 kbps (up to 40 min in 50 MB)
# - "48" = 48 kbps (up to 1 hour in 50 MB)
# - "32" = 32 kbps (up to 1.5 hours in 50 MB)
# - "auto" = automatically calculate optimal bitrate
# Web interface available at: http://localhost:1992
# - Login with credentials from .env (WEB_CONVERTER_LOGIN, WEB_CONVERTER_PASSWORD)
# - Drag & drop audio files up to 2 GB
# - PWA-enabled for mobile installation
```
--------------------------------
### Manage User Bookmarks
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Provides functionality to manage user bookmarks, including retrieving all bookmarks for a user, checking for existing bookmarks for a specific lesson, counting bookmarks to enforce a limit, creating new bookmarks, renaming them, and deleting them. It relies on a database service for these operations.
```python
from bot.services.database_service import (
get_bookmarks_by_user,
get_bookmark_by_user_and_lesson,
count_user_bookmarks,
create_bookmark,
update_bookmark_name,
delete_bookmark
)
# Get user's bookmarks (ordered by created_at desc)
user_id = 42
bookmarks = await get_bookmarks_by_user(user_id)
print(f"User has {len(bookmarks)} bookmarks")
for bookmark in bookmarks:
print(f"{bookmark.custom_name} | {bookmark.created_at.strftime('%d.%m.%Y')}")
print(f" Lesson: {bookmark.lesson.title}")
# Check if bookmark exists
existing = await get_bookmark_by_user_and_lesson(user_id=42, lesson_id=100)
if existing:
print(f"Already bookmarked as: {existing.custom_name}")
# Count bookmarks (enforces 20 limit)
count = await count_user_bookmarks(user_id=42)
if count >= 20:
print("❌ Bookmark limit reached (20 max)")
else:
# Create new bookmark
bookmark = await create_bookmark(
user_id=42,
lesson_id=100,
custom_name="Важный урок о таухиде"
)
print(f"✅ Bookmark created: {bookmark.id}")
# Rename bookmark
await update_bookmark_name(
bookmark_id=bookmark.id,
new_name="Переименованная закладка"
)
# Delete bookmark
await delete_bookmark(bookmark_id=bookmark.id)
```
--------------------------------
### Python: Lesson Model and Data Access
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Demonstrates fetching lesson data with its related series, teacher, and book information using the `get_lesson_by_id` service function. It highlights how to correctly access related fields through relationships, avoiding issues with detached instances, and displays various lesson properties like title, number, duration, and tags.
```python
from bot.models import Lesson
from bot.services.database_service import get_lesson_by_id
# Fetch lesson with eager-loaded relationships to avoid DetachedInstanceError
lesson = await get_lesson_by_id(lesson_id=123)
# Access series information through relationship (NOT through removed fields)
# ☠ WRONG - these fields were removed in migration:
# year = lesson.series_year
# name = lesson.series_name
# ✅ CORRECT - use relationship:
if lesson.series:
series_year = lesson.series.year # e.g., 2024
series_name = lesson.series.name # e.g., "Основы Акыды"
series_display = lesson.series.display_name # "2024 - Основы Акыды"
teacher_name = lesson.series.teacher.name # Through series relationship
# Lesson properties
print(f"Title: {lesson.title}")
print(f"Number: {lesson.lesson_number}")
print(f"Duration: {lesson.formatted_duration}") # "1в 23м 45с"
print(f"Display: {lesson.display_title}") # "Урок 5"
print(f"Full: {lesson.full_display_title}") # "2024 - Основы Акыды | Урок 5"
print(f"Has audio: {lesson.has_audio()}") # True/False
print(f"Tags: {lesson.tags_list}") # ['акыда', 'таухид']
```
--------------------------------
### Python: Theme and Book Hierarchy Operations
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Shows how to retrieve themes and books, demonstrating the hierarchical structure of content. It covers fetching all active themes, a specific theme with its associated books, and books filtered by theme (including orphaned books). The code also accesses details like theme names, book display names, author information, and lesson counts.
```python
from bot.services.database_service import (
get_all_active_themes,
get_theme_by_id,
get_books_by_theme,
get_book_by_id
)
# Fetch active themes (sorted by sort_order)
themes = await get_all_active_themes()
for theme in themes:
print(f"{theme.name}: {theme.active_books_count} books")
# "Акыда: 12 books"
# "Сира: 8 books"
# Fetch theme with books eagerly loaded
theme = await get_theme_by_id(theme_id=1)
print(f"Theme: {theme.name}")
print(f"Description: {theme.desc}")
active_books = [b for b in theme.books if b.is_active]
# Fetch books by theme (with authors eagerly loaded)
books = await get_books_by_theme(theme_id=1)
for book in books:
print(f"{book.display_name}") # "«4 правила» - Мухаммад ибн Абдуль-Ваххаб"
print(f"Author info: {book.author_info}") # "Мухаммад ибн Абдуль-Ваххаб (1703-1792)"
print(f"Lessons: {book.active_lessons_count}")
# Books without theme (orphaned books)
orphaned_books = await get_books_by_theme(theme_id=None)
```
--------------------------------
### Grouping Lessons by Series with SQLAlchemy
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
This Python snippet utilizes SQLAlchemy to fetch lessons and their associated series, then groups them into a dictionary keyed by series ID. It demonstrates fetching data with `joinedload` for efficient relationship loading and then programmatically organizing the results, which is useful for displaying series-based content.
```python
# Load lessons with series relationship
result = await session.execute(
select(Lesson)
.options(joinedload(Lesson.series))
.where(Lesson.teacher_id == teacher_id)
.order_by(Lesson.series_id)
)
lessons = result.scalars().unique().all()
# Group by series
series_map = {}
for lesson in lessons:
if lesson.series_id:
key = f"series_{lesson.series_id}"
if key not in series_map:
series_map[key] = {
"series_id": lesson.series_id,
"year": lesson.series.year, # From relationship
"name": lesson.series.name, # From relationship
"lessons": []
}
series_map[key]["lessons"].append(lesson)
```
--------------------------------
### Lesson Search and Filtering (Python)
Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt
Enables searching for lessons based on keywords found in their title, description, or tags. This function efficiently filters lessons, ensuring results are from active books and associated with active authors. The search is case-insensitive and uses pattern matching for flexibility.
```python
from bot.services.database_service import search_lessons
# Search lessons by title, description, or tags
# Only returns lessons from active books with active authors (or no author)
results = await search_lessons(query="хадис")
for lesson in results:
print(f"{lesson.title}")
print(f"Book: {lesson.book_title}")
print(f"Teacher: {lesson.teacher_name}")
print(f"Tags: {lesson.tags_list}")
# Search is case-insensitive and uses LIKE pattern matching
# Query "Хадис" finds: "Введение в хадис", "хадисоведение", etc.
```
--------------------------------
### Correct Series Sorting in SQL
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
This SQL code snippet shows the correct way to order lessons by series, specifically addressing an issue where the previous method was broken. It highlights the importance of ordering by `series_id` and `lesson_number` for accurate series-based sorting, contrasting it with the incorrect approach.
```sql
# ❌ OLD (broken)
# .order_by(Lesson.series_year.desc(), Lesson.series_name)
# ✅ NEW (correct)
.order_by(Lesson.series_id, Lesson.lesson_number)
```
--------------------------------
### View Docker Compose Logs for Bot
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
Commands to view logs for the bot service managed by docker-compose. Supports real-time following, limiting output to the last N lines, and searching for specific patterns like 'ERROR'.
```bash
# Real-time
docker-compose logs bot --follow
# Last N lines
docker-compose logs bot --tail=50
# Search
docker-compose logs bot | grep ERROR
```
--------------------------------
### Python: Edit Theme Name Handler
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
Handles the callback query to initiate the theme name editing process. It extracts the theme ID, stores message coordinates for later updates, sets the FSM state to 'name', and prompts the user for input.
```python
from aiogram import F
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
# Assuming 'router' and 'ThemeStates' are defined elsewhere
@router.callback_query(F.data.startswith("edit_theme_name_"))
async def edit_theme_name_start(callback: CallbackQuery, state: FSMContext):
theme_id = int(callback.data.split("_")[3])
# Save message coordinates for later update
await state.update_data(
theme_id=theme_id,
edit_message_id=callback.message.message_id,
edit_chat_id=callback.message.chat.id
)
await state.set_state(ThemeStates.name)
# ... prompt for input
```
--------------------------------
### Context-Aware Callbacks for Navigation
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
This Python code illustrates how to retrieve saved context from an FSM state and construct dynamic callback data for Telegram bot navigation. It allows the bot to determine the user's previous navigation point (e.g., teacher view vs. general view) and generate an appropriate callback, facilitating seamless user experience and context preservation.
```python
# Pattern: Context-aware callbacks
data = await state.get_data()
teacher_id = data.get("teacher_id")
if teacher_id:
# User came from teacher view - return there
callback_data = f"teacher_{teacher_id}_play_lesson_{lesson_id}"
else:
# User came from general view
callback_data = f"lesson_{lesson_id}"
```
--------------------------------
### Python: Eager Loading with SQLAlchemy
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
Demonstrates how to perform eager loading of related entities (like 'series', 'teacher', 'book') when querying a 'Lesson' object using SQLAlchemy's `joinedload`. This pattern helps prevent N+1 query issues and DetachedInstanceError by loading all necessary data in a single query.
```python
from sqlalchemy.orm import joinedload
from sqlalchemy.future import select
# Assuming 'session', 'Lesson', 'lesson_id' are defined elsewhere
result = await session.execute(
select(Lesson)
.options(
joinedload(Lesson.series), # Load series relationship
joinedload(Lesson.teacher),
joinedload(Lesson.book)
)
.where(Lesson.id == lesson_id)
)
lesson = result.scalar_one_or_none()
```
--------------------------------
### Python: Save Theme Name Handler
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
Handles saving the theme name, differentiating between editing an existing theme and creating a new one. For editing, it updates the theme in the database and refreshes the message view. For creation, it stores the name and proceeds to the next step in the creation flow.
```python
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardMarkup, InlineKeyboardButton
# Assuming 'router', 'ThemeStates', 'get_theme_by_id', 'update_theme', 'get_all_themes', 'create_theme', 'get_theme_by_name' are defined elsewhere
@router.message(ThemeStates.name)
async def save_theme_name(message: Message, state: FSMContext):
data = await state.get_data()
theme_id = data.get("theme_id")
if theme_id: # EDITING
# Delete user input message
try:
await message.delete()
except: # Handle potential errors if message is already deleted or inaccessible
pass
# Update entity
theme = await get_theme_by_id(theme_id)
theme.name = message.text
await update_theme(theme)
# Reload to get fresh data
theme = await get_theme_by_id(theme_id)
# Build complete edit menu
info = f"📚 {theme.name}\n..."
builder = InlineKeyboardBuilder()
# ... add all edit buttons
# Update ORIGINAL message
await message.bot.edit_message_text(
chat_id=data['edit_chat_id'],
message_id=data['edit_message_id'],
text=info,
reply_markup=builder.as_markup()
)
await state.clear()
else: # CREATING
# Continue creation flow
await state.update_data(name=message.text)
# ... next step
# Pattern 3: Creation completion (as a separate handler or part of the flow)
async def complete_creation(message: Message, state: FSMContext):
data = await state.get_data()
# Delete user input
try:
await message.delete()
except: # Handle potential errors if message is already deleted or inaccessible
pass
# Create entity
theme = await create_theme(name=data['name'], ...)
# Load all entities
themes = await get_all_themes()
# Build list view
builder = InlineKeyboardBuilder()
for theme in themes:
builder.add(...)
# Update ORIGINAL message to show list
await message.bot.edit_message_text(
chat_id=data['create_chat_id'],
message_id=data['create_message_id'],
text="📚 Управление темами",
reply_markup=builder.as_markup()
)
await state.clear()
# Validation Error Handler (Duplicate Name Check)
async def check_duplicate_theme_name(message: Message, state: FSMContext, theme_id=None):
data = await state.get_data()
new_name = message.text
existing = await get_theme_by_name(new_name)
if existing and existing.id != theme_id:
try:
await message.delete()
except: # Handle potential errors if message is already deleted or inaccessible
pass
# Show error in SAME window
await message.bot.edit_message_text(
chat_id=data['edit_chat_id'],
message_id=data['edit_message_id'],
text="❌ Ошибка!\n\nТема с таким названием уже существует!\n\nВведите другое название:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text="🔙 Отмена", callback_data=f"edit_theme_{theme_id}")
]])
)
# DON'T clear state - user can retry
return False # Indicate failure
return True # Indicate success
```
--------------------------------
### Python: Correct Series Attribute Access
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
Highlights the correct way to access attributes of a related 'Series' object from a 'Lesson' object. It contrasts the incorrect method of accessing non-existent direct attributes with the correct method of using the defined relationship attribute (e.g., `lesson.series.year`).
```python
# Assuming 'lesson' object is loaded with its 'series' relationship
# ❌ WRONG - these fields don't exist anymore on the lesson object directly
# year = lesson.series_year
# name = lesson.series_name
# ✅ CORRECT - use relationship
year = lesson.series.year
name = lesson.series.name
```
--------------------------------
### Python: Object Reloading After Updates
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
Illustrates the pattern of reloading an object from the database after it has been updated. This ensures that any changes made and relationships are reflected correctly, preventing stale data issues, especially when accessing related attributes.
```python
# Assuming 'update_lesson' and 'get_lesson_by_id' are defined elsewhere
await update_lesson(lesson)
lesson = await get_lesson_by_id(lesson_id) # Reload with fresh relationships
```
--------------------------------
### Save Context in FSM State
Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md
This Python snippet demonstrates how to save user context within a Finite State Machine (FSM) using a state object. It's crucial for preserving navigation information, such as lesson and teacher IDs, along with message identifiers, to enable proper back navigation in a Telegram bot. This pattern is widely used in user-facing handlers.
```python
# Pattern: Save context in FSM state
await state.update_data(
lesson_id=lesson_id,
teacher_id=teacher_id, # May be None for general navigation
bookmark_message_id=callback.message.message_id,
bookmark_chat_id=callback.message.chat.id
)
```
=== COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.