Как я пагинацию на telebot делал
Введение
Напишу сразу, о том что данную статью я изначально выложил на habr — с целью узнать популярное мнение к такому подходу, методы улучшения и просто разные мнения. Однако получил всего пять комментариев, из которых только один информативный, но не по теме. Проанализировал немного статистику и увидев, что за последний год посты новых акаунтов не читаются и не поднимаются в рейтинге. Я решил на Хабре быть ReadOnly пользователем, а здесь буду писать. Я буду рад получить критику и советы по улучшению данного метода.
Разберемся с начала, что это за статья зачем она и для кого. Пришлось мне в рамках хакатона «Поколение ИТ» писать бота для telegram. (Хотел оставить сайт организаторов, но он не работает. Хакатон организовывало министерство образования Москвы, совместно с тремя колледжами)
Но готового решения для пагинации, которое бы нам подходило мы не нашли. Поэтому было принято решение изобретать велосипед. Решение моих товарищей было максимально странным, брать количество записей и перебирать их в цикле от 1 до N (конца, записей), но данная идея сразу была отброшена. Поэтому предоставляю вашему вниманию наше творчество, которое мы изобрели.
Демонстрация результата
Покажу для начала, что мы вообще сделали и о чем пойдет речь. Времени было мало (6 часов на все про все), и нужен был алгоритм, который позволит быстро и просто реализовать пагинацию, с возможностью отправлять условия на выборку — под разные таблицы.
Создание «шаблонного» бота
Для упрощения описания алгоритма, я буду описывать его на шаблоном боте. Который будет отвечать на любое отправленное ему сообщение — «Привет»ю
Создание inline кнопок
Сначало прописываем зависимость в начале кода:
Реализуем кнопку скрыть
Перед тем, как отправить сообщение пользователю создадим markup с inline кнопкой «скрыть» после чего отправим пользователю сообщение с данной кнопкой.
Чтобы кнопка работала, нам для нее нужен отдельный обработчик он выглядит так:
Необходимо учитывать, что здесь может возникнуть эксепшн, т. к у телеграмма есть ограничения на удаление. Например старые сообщения бот не сможет удалить. Поэтому нужно писать отдельное исключение. Но цель моей статьи заключается в том, что бы показать мой подход к демонстрации алгоритма пагинации.
Проверяем:
При нажатии на кнопку скрыть сообщение удаляется, а значит не будем зацикливаться больше на этом.
Кнопки навигации
Теперь перейдем непосредственно к кнопкам «Вперед« и «Назад» для перехода по страницам? которые будут располагаться под кнопкой »скрыть».
К уже существующему markup добавим еще две новые кнопки. Т. к при первом сообщение от бота приходит первая страница, добавим пока что только кнопку «Вперед».
Кнопка вперед будет в callback_data отправлять строку «next-page«, а в обработчике мы будем прибавлять к page 1. После чего пересоздадим markap уже с кнопкой назад и новым сообщением. Аналогичным образом для кнопки назад в callback_data будем отправлять строку »back-page»
Теперь наш код выглядит вот так:
На данном этапе для простоты демонстрации я делаю count и page глобальными переменными, позже я заменю их.
Теперь мы можем перемещаться по страницам:
Да, но перед нами встает две проблемы:
- Нужно писать отдельные исключения, чтобы при возврате кнопкой назад на первую страницу — кнопка назад больше не отображалась. А при переходе на последнюю не было кнопки вперед.
- Проблема передачи локальной переменной (page и count) от главной функции в обработчик нажатия кнопки. Telebot в отличие от например aiogramm не может передать другие параметры вместе с callback_data. А callback_data – это строка. Решение этой проблемы, я увидел в передаче в callback_data склеенного через разделитель json в который и запишу count и page. Мой товарищ вышел из данной ситуации более странным на мой взгляд решением – он записывал во временную таблицу бд id узера, id сообщение и страницу, которую он смотрит и потом удалял их. Достаточно радикальный способ по ряду многих причин (что если не удалиться запись из БД; БД вообще не для этого сделана; нам нужно будет столько таблиц, во скольких местах будет пагинация), но переубедить я его не смог) .Как вариант еще можно создать публичный словарь, но ради двух переменных это странно + способ с передачей json с двумя параметрами, как строку в callback_data на мой взгляд кажется самым универсальным и адекватным решением при данной проблеме.
Для начала решим 2 проблему:
- Удалим глобальные переменный page и count и создадим их внутри функции start
В callback будем отправлять такую строку:
{'method': 'page', 'NumberPage': 1, 'CountPage': 10}
Для кнопки вперед, это выглядит вот так:
Для кнопки назад, это выглядит, так:
На данном этапе видно, что я начал отдавать номер строки, на которую перехожу. Если нажму "Назад" со второй строки в обработчик уйдет 1. Это избавляет меня от необходимости использовать теперь разные обработчик для кнопки назад (back-page) и вперед (next-page) (их можно просто удалить)
Теперь за пагинацию будет отвечать новый обработчик, который увидит в полученной строке вхождение 'pagination':
После чего строка полученная в callback_data будет распаршена в json. И уже из JSON мы получим необходимые нам Count и Page.
Пока пишу новый обработчик, сразу решу проблему 1. Сделаю три условия вывода кнопок.
Вывод кнопки "Назад" для последний страницы
Вывод "Вперед" и "Назад" для всех страниц между первой и последней
- Вывод кнопки "Назад" для последний страницы
А вот и полученный код:
Желательно в обработчик добавить исключение, что page > 0 и page <=count, а так же строку c bot.edit_message_text занести в блок try. На случай, если телеграмм зависнет и пользователь, сможет сделать двойной клик по одной кнопке. Но статья направлена на описания подхода реализации. Поэтому на этом я не буду останавливаться.
Получения нужных строк по страницам из БД
Вернемся к "главной" функции start, которая принимает сообщения от пользователя. Я оставил count = 10 и page = 1; Пора это исправить!
count - необходимо будет получать из БД, а в сообщение пользователю отдавать необходимые строки.
Для этого создадим новый класс database, который будет отвечать за подключение к БД и вывод нужных строк из БД.
Что бы выводить построчно записи из БД я буду использовать конструкцию SQL: OFFSET-FETCH. Она предназначена, как раз для разбиения результирующего набора на части (страницы)
У меня с хакатона осталась таблица учебных организаций Москвы. На ней и покажу, как это будет выглядеть:
Делаем сортировку по id и отображаем 15 записей от 0 (т.е самой первой). Это будет наша первая страница, что бы показать 2 страницу нужно будет пропустить столько записей, сколько было на первой странице.
Таким образом число после NEXT – является всегда статичным это будет переменная SkipSize. А число после OFFSET - то после которой надо забрать следующие (SkipSize) строк. Записать это можно, как (Номер страницы – 1)*SkipSize
На псевдо-коде это выглядит вот так:
Теперь перейдем к написанию самого класса. Он вышел вот таким:
Теперь в main нам надо объявить наш новый класс:
А вот так к нему можно обратиться:
Или вот так
Вот такой вывод получаем по итогу:
И тут я понял, что общие количество записей так и не получил (тот самый count). В return к функции (listColledjeForPage) вывода записей добавлю еще вывод общего количества записей в запросе.
Я еще раз убедился, что все работает и пошел дописывать main
Сделаем вывод уже полученной строки из нашего нового класса. В "главной функции" переменную page оставляем равной 1, а count получаем из функции класса database, которая выполняет нужный нам запрос.
Вот, что получили:
Осталось только вывести этот текст в отформатированном виде и сделать такой же вывод в обработчики, который отвечает за перелистывание страниц вперед, назад.
Форматируем тест сообщения с помощью HTML
Теперь такое же форматирование вставляем в обработчик при редактировании сообщения после нажатия кнопок "Впред"/"Назад".
Итог
Код
main.py
database.py
ай да человек, спасибо)
почти узнал то что хотел, нужно днем еще разок почитать
Как раз на днях придумал похожую штуку для себя, а вот как переключать и сохранять страницы, над этим задумался. были варианты как на сайтах
<<8, 9, 10, 11, 12...55 >> вроде красиво, привычно, но ваше, минимализм и простота, наверное сделаю так-же.
Пасиб )
Спасибо!
Узнал для себя новое: формат сообщения в HTML, а также OFFSET-FETCH
Доброго времени суток.
Понравилась мне эта замутка - данные JSON через callback отправлять.
Дешево и сердито. Как Беломорканал.
Токо я вот себе еще под стрелочки две стрелочки добавил - чтобы "выборки" - страницы найденых страниц просматривать.
Т.е. в словарь ещё пару ключей и значений добавилось.
Сразу же полетели ошибки BUTTON_DATA_INVALID
Сделал названия ключей по две буквы (NC вместо NumberCount например)- заработало.
В callback_data можно очень ограниченую строку добавить.
Т.е. если нужно более менее большие данные перебрасывать, хотя бы переменных 10, обработчику, лучше всё-таки словарь глобальный завести.
Отличная статья, спасибо.
Сижу изучаю кнопки. Начал с ReplyKeyboard, но не нравится дизайн, смотрю сейчас, как просто реализовать Inline
У меня вопрос, а как сделать подобное меня, как на Рис. 1. Демонстрация результата вначале
это?