Не могу разобраться с `FSMContext`, `State` в `aiogram v3.x`. Пишу систему регистрации

Рейтинг: 1Ответов: 2Опубликовано: 25.08.2023

В результате моих "непониманий" функция show_summary в приведенном ниже коде выводит значения некорректно. Вот что выводится по итогу в (show_summary):

The following is a summary of the information you provided:
Student Name: (Parent) # Здесь по какой-то причине выводится/принимается первое значение (`Parent`) из первой в коде функции `command_start`, вместо `student_name`, следовательно, нарушается и остальная "последовательность".
Date of birth: (full name)
Parent's name: (YYYYY-MM-DD)
Parent's phone number: (full name of (parent))
Thank you for enrolling your child. You will receive further instructions.

То, что в скобках, я ввел вручную во время тестирования в Telegram боте (для демонстрации)

Сообщение/show_summary должно быть по идеи такое:

Here is the summary of the information you provided:
Student Name: (Student Name) # И так оно и должно быть по идеи.
Date of Birth: (Date of Birth)
Parent Name: (Parent Name)
Parent Phone: (Parent Phone)
Thank you for enrolling your child. You will receive further instructions.

Вот полный код:

class Form(StatesGroup):
    select = State()
    student_name = State()
    student_dob = State()
    parent_name = State()
    parent_phone = State()
    finish = State()

@router.message(CommandStart())
async def command_start(message: Message, state: FSMContext) -> None:
    await state.set_state(Form.select)
    await message.answer(
        "Welcome! We are happy to welcome you to our bot. "
        "By continuing, you automatically agree to the terms of use and privacy policy. "
        "Are you a parent or a student?",
    reply_markup=ReplyKeyboardMarkup(
        keyboard=[
            [
                KeyboardButton(text="Parent"),
                KeyboardButton(text="Student")
            ]
        ],
        resize_keyboard=True,
        ),
    )

@router.message(Form.select, F.text.casefold() == "student")
async def student(message: Message, state: FSMContext) -> None:
    await state.clear()
    await message.answer(
        "Not bad not terrible.\nSee you soon.",
        reply_markup=ReplyKeyboardRemove(),
    )

@router.message(Form.select, F.text.casefold() == "parent")
async def student_name(message: Message, state: FSMContext) -> None:
    await state.update_data(student_name=message.text)
    await state.set_state(Form.student_dob)
    await message.reply(
        "Great! To enroll your child, please enter the student's full name:",
        reply_markup=ReplyKeyboardRemove(),
    )

@router.message(Form.student_dob)
async def student_dob(message: Message, state: FSMContext) -> None:
    await state.update_data(student_dob=message.text)
    await state.set_state(Form.parent_name)
    await message.reply(
        "Thank you! Please enter the student's date of birth (YYYYY-MM-DD).",
        reply_markup=ReplyKeyboardRemove(),
    )

@router.message(Form.parent_name)
async def parent_name(message: Message, state: FSMContext) -> None:
    await state.update_data(parent_name=message.text)
    await state.set_state(Form.parent_phone)
    await message.reply(
        "Please enter your full name (parent):",
        reply_markup=ReplyKeyboardRemove(),
    )

@router.message(Form.parent_phone)
async def parent_phone(message: Message, state:FSMContext) -> None:
    await state.update_data(parent_phone=message.text)
    await state.set_state(Form.finish)
    await message.reply(
        "Last step - enter your phone number (parent).",
        reply_markup=ReplyKeyboardRemove(),
    )

@router.message(Form.finish)
async def process_register(message: Message, state: FSMContext) -> None:
    data = await state.get_data()
    await state.clear()
    await message.answer(
        "Congratulations! You have successfully enrolled your child.",
        reply_markup=ReplyKeyboardRemove(),
    )
    await show_summary(message=message, data=data)

async def show_summary(message: Message, data: Dict[str, Any], positive: bool = True) -> None:
    student_name = data.get("student_name", "Unknown")
    student_dob = data.get("student_dob", "Unknown")
    parent_name = data.get("parent_name", "Unknown")
    parent_phone = data.get("parent_phone", "Unknown")
    
    summary_text = "Here is the summary of the information you provided:\n"
    summary_text += f"Student Name: {student_name}\n"
    summary_text += f"Date of Birth: {student_dob}\n"
    summary_text += f"Parent Name: {parent_name}\n"
    summary_text += f"Parent Phone: {parent_phone}\n"
    
    if positive:
        summary_text += "Thank you for enrolling your child. You will receive further instructions."
    else:
        summary_text += "We're sorry to hear that you encountered issues during the registration process."
    
    await message.answer(text=summary_text, reply_markup=ReplyKeyboardRemove())

Ответы

▲ 0Принят

Для "понимания" мне потребовалось упростить свой код, и вот каких результов я добился, когда по концу в Telegram боте можно наблюдать сообщение:

{
'student_name': 'мое первое написанное/принимаемое сообщение',
'student_dob': 'мое второе написанное/принимаемое сообщение'
} 

Далее собираюсь на основе этого писать другую последовательно-выстроенную архитектуру/логику FSMContext с регистрацией.

Вот full-code для вашего собственного тестирования. Мне помогла статья: https://mastergroosha.github.io/aiogram-3-guide/fsm/, а так же спасибо всем кто пытался объяснить словами, на словах я не очень понимаю (начинающий)

import asyncio
import logging
import sys
from typing import Any, Dict

from aiogram import Bot, Dispatcher, F, Router, html
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage

from config import BOT_TOKEN

router = Router()

bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()

# Класс "Form" для хранения состояний и так далее.
class Form(StatesGroup):
    student_name = State()
    student_dob = State()

# Обработчик "1-го шага" и так далее.
@router.message(CommandStart())
async def command_start(message: Message, state: FSMContext):
    await message.answer(
        text="Напишите ваше ФИО:",
        reply_markup=ReplyKeyboardRemove()
    )
    # Устанавливаем пользователю состояние "пишет своё ФИО" 
    # и/или
    # Переводим пользователя в состояние `Form.student_name`.
    await state.set_state(Form.student_name)

# Следующий обработчик и так далее.
@router.message(Form.student_name)
async def student_name(message: Message, state: FSMContext):
    await state.update_data(student_name=message.text)
    await message.answer(
        text="Спасибо. Теперь, пожалуйста, напишите ваш возраст:", 
        reply_markup=ReplyKeyboardRemove()
    )
    # Переводим пользователя в состояние `Form.student_dob`.
    await state.set_state(Form.student_dob)

# Cледующий обработчик и так далее.
@router.message(Form.student_dob)
async def student_dob(message: Message, state: FSMContext):
    await state.update_data(student_dob=message.text)
    user_data = await state.get_data()
    await message.answer(
        text=f"{user_data}",
        reply_markup=ReplyKeyboardRemove()
    )
    await state.clear()

async def main():
    bot = Bot(token=BOT_TOKEN, parse_mode=ParseMode.HTML)
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
    try:
        asyncio.run(main())
    except (KeyboardInterrupt, SystemExit):
        logging.info("Bot stopped!")
▲ 1

функция student_name() у вас вызывается когда пользователь нажимает кнопку KeyboardButton(text="Parent"), так как на нее назначен хендлер соответствующий: F.text.casefold() == "parent". в этой же функции вы в FSMContext записываете значение student_name=message.text, которое очевидно будет "Parent". далее в этой функции переводите стейт в Form.student_dob, но пользователю пишете "enter the student's full name:", и путаница продолжает быть.

вам нужно в этой же функции перевести стейт в Form.student_name, и что бы такой стейт был обьявлен у класса Form, убрать из этой функции сохранение значений в стейт. и добавить еще одну функцию, которая будет ловить текст при состоянии Form.student_name