В этой статье мы рассмотрим, как использовать Flet для создания панели входа в личный кабинет, где пользователь сможет просматривать данные о своих тратах по счёту.
Для простоты и полноты объяснения будем считать, что номер банковского счета совпадает с номером мобильного телефона, как в недавно ушедшем с рынка Qiwi.
Flet — это фреймворк для разработки кроссплатформенных приложений на языке Python, который предоставляет удобные инструменты и функциональность для создания панелей управления и интерфейсов.
Flet использует Flutter для создания графического интерфейса приложений. Он предоставляет набор виджетов, которые можно использовать для создания различных элементов пользовательского интерфейса, таких как кнопки, текстовые поля, контейнеры и другое. Виджеты в Flet готовы к использованию, их можно комбинировать и настраивать для создания нужного внешнего вида и функциональности приложения.
В следующих разделах рассмотрим пошаговый процесс создания панели входа в личный кабинет с использованием Flet, начиная с настройки окружения и установки фреймворка и заканчивая созданием контроллеров и шаблонов для отображения данных.
Создание приложения
Покажу пример реализации входа в личный кабинет и получения данных о тратах со счета через SMS-код. Для отправки SMS-кода воспользуемся SMS API от платформы MTC Exolve. Путём взаимодействия с API покажем пример, как без труда отправить одноразовый код на указанный номер телефона.
Проект будет иметь следующую структуру:
fletsms/
/venv
/example_db
__init__.py
dbinfo.py
/mtt
__init__.py
client.py
config.py
main.py
.env
В файле .env хранятся переменные окружения: API-ключ и номер телефона, с которого будут отправляться SMS.
В файле config.py получим данные из переменных окружения.
from dotenv import dotenv_values
info_env = dotenv_values('.env')
API_KEY = info_env.get('API_KEY')
PHONE_SEND = info_env.get('PHONE_SEND')
Модуль генерации одноразового пароля
Перейдём в модуль client.py и создадим функцию для генерации и отправки одноразового кода на телефон пользователя
Импортируем необходимые модули:
import requests
import random
import string
from config import API_KEY, PHONE_SEND
Создадим функцию send_sms, которая будет отправлять SMS-сообщения:
def send_sms(number):
# Генерируем случайную последовательность из 5 латинских букв
code = ''.join(random.choice(string.ascii_letters) for _ in range(5))
# Отправляем SMS сгенерированным кодом
sms_data = {
"number": PHONE_SEND,
"destination": number,
"text": code
}
headers = {'Authorization': f'Bearer {API_KEY}'}
response = requests.post(url="https://api.exolve.ru/messaging/v1/SendSMS",
json=sms_data,
headers=headers)
if response.status_code == 200:
return code
else:
return f"Ошибка при отправке SMS: {response.status_code}"
В функции send_sms генерируется случайный код из 5 латинских букв с помощью функции random.choice и string.ascii_letters. Затем создаётся словарь sms_data, содержащий номер отправителя, номер получателя и текст сообщения. Заголовки запроса устанавливаются с помощью ключа авторизации Authorization и значения API_KEY. Далее выполняется POST-запрос к API для отправки SMS-сообщения.
Если сервер ответил статусом 200, то возвращается сгенерированный код. В противном случае возвращается сообщение об ошибке с указанием статус кода.
Приложение Flet
Если ваша OC — Linux Ubuntu 20.04 LTS, рекомендую использовать flet==0.18.0.
Установить его можно командой: pip install flet==0.18.0
Произведем в main.py все необходимые импорты и создадим основной класс приложения:
import time
import flet as ft
from mtt import client
from example_db.dbinfo import bank_list
class App(ft.Page):
__instance = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super(App, cls).__new__(cls)
return cls.__instance
def __init__(self, page):
self.p = page
self.p.on_resize = self.page_resize
self.info_w = self.p.window_width
self.info_h = self.p.window_height
def page_resize(self, e):
App.__new__(App).p.info_w = self.p.window_width
App.__new__(App).p.info_h = self.p.window_height
self.p.update()
Класс App представляет собой экземпляр страницы и корневое представление, которые автоматически создаются при запуске нового сеанса.
При создании этого класса необходимо использовать паттерн Singleton, так как это позволяет создать только один экземпляр класса App и обеспечить доступ к нему из разных частей приложения. Это гарантирует единообразие данных и согласованность состояния объекта App во всём приложении.
Для реализации паттерна проектирования Singleton я использовал механизм метакласса и переопределил метод __new__. Внутри метода __new__ я проверяю, существует ли уже экземпляр класса App. Если экземпляр не существует, то создаю его с помощью метода super().__new__(cls) и сохраняю в переменной __instance. В результате при последующих вызовах конструктора класса App всегда будет возвращаться один и тот же экземпляр.
В конструкторе класса App я инициализирую атрибуты p, on_resize, info_w и info_h. Атрибут p ссылается на объект страницы, а on_resize используется для отслеживания изменений размера окна приложения. Атрибуты info_w и info_h содержат информацию о ширине и высоте окна соответственно.
Метод page_resize служит для отслеживания события изменения размера окна приложения. Внутри метода я обновляю значения атрибутов info_w и info_h класса App с помощью App.__new__(App).p.info_w = self.p.window_width и App.__new__(App).p.info_h = self.p.window_height. Затем вызывается метод update, который обновляет страницу с учётом нового размера окна.
Далее приступим к созданию страницы входа в личный кабинет. Создадим класс Singup, который представляет собой компонент входа в личный кабинет. Давайте подробнее рассмотрим его реализацию.
class Singup(ft.Container):
__instance = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super(Singup, cls).__new__(cls)
return cls.__instance
def __init__(self):
super().__init__()
self.input_info = ft.TextField(label="phone", width=300, on_change=self.validate)
self.btn_sing = ft.TextButton(text="Singup", width=300, disabled=True, on_click=self.generate_code)
self.border = ft.border.all(5, ft.colors.BLUE)
self.alignment = ft.alignment.center
self.content = ft.Row(controls=[ft.Column([self.input_info, self.btn_sing],
alignment=ft.MainAxisAlignment.CENTER,)],
alignment=ft.MainAxisAlignment.CENTER,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
width=App.__new__(App).p.window_width -15,
height=App.__new__(App).p.window_height -15
)
App.__new__(App).p.add(self)
App.__new__(App).p.update()
def generate_code(self, e):
response = client.send_sms(self.input_info.value)
if len(response) == 5 and response.isalpha():
self.phone = self.input_info
self.input_info = ft.TextField(label="sms code", width=300, on_change=self.validate)
self.sms_code = response
self.btn_sing = ft.TextButton(text="enter", width=300, disabled=False, on_click=self.check_table)
self.content = ft.Row(controls=[ft.Column([self.input_info, self.btn_sing],
alignment=ft.MainAxisAlignment.CENTER, )],
alignment=ft.MainAxisAlignment.CENTER,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
width=App.__new__(App).p.window_width -15,
height=App.__new__(App).p.window_height-15)
App.__new__(App).p.add(self)
App.__new__(App).p.update()
else:
self.page.clean()
App.__new__(App).p.add(
ft.Text(f"Произошла ошибка при отправке SMS.\n Попробуйте снова через 10 сек.",
size=30,
color=ft.colors.RED))
App.__new__(App).p.update()
time.sleep(10)
self.page.clean()
App.__new__(App).p.add(self)
App.__new__(App).p.update()
def check_table(self, e):
if self.input_info.value == self.sms_code:
self.page.clean()
TableCont.__new__(TableCont).__init__(self.phone.value)
else:
self.page.clean()
App.__new__(App).p.add(ft.Text(f"Неверный код. Попробуйте снова через 10 сек.", size=30, color=ft.colors.RED))
App.__new__(App).p.update()
time.sleep(10)
self.page.clean()
App.__new__(App).p.add(self)
App.__new__(App).p.update()
def validate(self, e):
if all([self.input_info.value]):
self.btn_sing.disabled = False
else:
self.btn_sing.disabled = True
self.btn_sing.update()
Класс Singup наследуется от класса ft.Container, что позволяет поместить в него содержимое. При инициализации класса мы определяем его атрибуты, такие как поле ввода номера телефона self.input_info и кнопку self.btn_sing, которая будет менять своё состояние и генерировать одноразовый пароль для SMS через Exolve.
Метод generate_code отвечает за генерацию кода и отправку SMS. После получения ответа от функции client.send_sms мы проверяем длину полученного кода и его состав. Если код состоит из 5 буквенных символов, то мы сохраняем номер телефона в атрибуте self.phone, меняем поле ввода на поле для ввода кода SMS, сохраняем полученный код в атрибуте self.sms_code и меняем кнопку на кнопку «enter». Затем мы обновляем содержимое класса и графический интерфейс.
Чтобы обновить содержимое страницы, используем магические методы:
App.__new__(App).p.add(self)
App.__new__(App).p.update()
После создания нового содержимого страницы в переменной self.content добавляем его к объекту страницы App.__new__(App).p с помощью метода add. Затем вызывается метод update, который обновляет графический интерфейс страницы с учётом нового содержимого.
Это позволяет нам строить классы и изменять содержимое страницы Flet в методах класса, не перегружая функцию main, как во многих других примерах.
Метод check_table проверяет введённый код SMS. Если код совпадает с сохранённым кодом, то мы очищаем страницу self.page.clean().
И инициализируем новый класс TableCont. В противном случае мы выводим сообщение об ошибке и ждём 10 секунд перед очисткой страницы и обновлением интерфейса.
Так как в функции main у нас нет инициализации класса TableCont, нам необходимо инициализировать его внутри метода check_table командой:
TableCont.__new__(TableCont).__init__(self.phone.value)
Метод validate отслеживает заполнение поля self.input_info и активирует или деактивирует кнопку self.btn_sing, в зависимости от наличия значения в поле.
В результате класс Singup представляет собой компонент входа в личный кабинет, который обеспечивает ввод номера телефона, генерацию и проверку одноразового пароля SMS. Этот класс можно использовать в вашем проекте на Flet для создания удобного и безопасного механизма входа в личный кабинет.
Теперь создадим класс TableCont, который представляет собой компонент для отображения данных о тратах со счёта. Давайте рассмотрим его реализацию и функциональность.
class TableCont(ft.Container):
__instance = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super(TableCont, cls).__new__(cls)
return cls.__instance
def __init__(self, phone):
super().__init__()
self.phone = phone
self.data_info = DataUser(phone).__new__(DataUser)
self.btn_next = ft.TextButton(text="next", width=150, disabled=False, on_click=self.next_go_page)
self.btn_back = ft.TextButton(text="back", width=150, disabled=False, on_click=self.back_go_page)
self.page_number = ft.Text(f"{DataUser(phone).__new__(DataUser).page_number}", size=15)
self.btn_list = ft.Row(controls=[self.btn_back, self.page_number, self.btn_next],
alignment=ft.MainAxisAlignment.CENTER,
vertical_alignment=ft.CrossAxisAlignment.CENTER
)
self.border = ft.border.all(5, ft.colors.RED)
self.content = ft.Row(controls=[ft.Column([self.data_info, self.btn_list],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER)],
alignment=ft.MainAxisAlignment.CENTER,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
width=App.__new__(App).p.window_width -20,
height=App.__new__(App).p.window_height -20
)
App.__new__(App).p.add(self)
App.__new__(App).p.update()
def next_go_page(self, e):
start_page = DataUser.__new__(DataUser).start_page + 5
end_page = DataUser.__new__(DataUser).end_page + 5
self.data_info = DataUser(self.phone).__new__(DataUser).move_page(self.phone, start_page, end_page, int(end_page // 5))
self.page_number = ft.Text(f"{DataUser.__new__(DataUser).page_number}", size=15)
self.btn_next = ft.TextButton(text="next", width=150, disabled=False, on_click=self.next_go_page)
self.btn_back = ft.TextButton(text="back", width=150, disabled=False, on_click=self.back_go_page)
self.btn_list = ft.Row(controls=[self.btn_back, self.page_number, self.btn_next])
self.content = ft.Row(controls=[ft.Column([self.data_info, self.btn_list],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER)],
alignment=ft.MainAxisAlignment.CENTER,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
width=App.__new__(App).p.window_width - 20,
height=App.__new__(App).p.window_height - 20
)
App.__new__(App).p.add(self)
App.__new__(App).p.update()
def back_go_page(self, e):
start_page = DataUser.__new__(DataUser).start_page - 5
end_page = DataUser.__new__(DataUser).end_page - 5
page_number = DataUser.__new__(DataUser).page_number if DataUser.__new__(DataUser).page_number <=1 else DataUser.__new__(DataUser).page_number - 1
self.page_number = ft.Text(f"{page_number}", size=15)
self.data_info = DataUser(self.phone).__new__(DataUser).move_page(self.phone, start_page, end_page,int(end_page // 5))
self.btn_next = ft.TextButton(text="next", width=150, disabled=False, on_click=self.next_go_page)
self.btn_back = ft.TextButton(text="back", width=150, disabled=False, on_click=self.back_go_page)
self.btn_list = ft.Row(controls=[self.btn_back, self.page_number, self.btn_next])
self.content = ft.Row(controls=[ft.Column([self.data_info, self.btn_list],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER)],
alignment=ft.MainAxisAlignment.CENTER,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
width=App.__new__(App).p.window_width - 20,
height=App.__new__(App).p.window_height - 20
)
App.__new__(App).p.add(self)
App.__new__(App).p.update()
Класс TableCont наследуется от класса ft.Container, что предоставляет возможность генерировать и размещать виджеты и данные. При инициализации класса мы определяем его атрибуты, такие как номер телефона self.phone и self.data_info, который представляет собой класс таблицы с данными и номер счёта. Мы также определяем кнопки пагинации и номер страницы, а затем создаём содержимое контейнера.
Методы next_go_page и back_go_page отвечают за переход на следующую или предыдущую страницу с данными. Они обновляют данные, кнопки пагинации, номер страницы и содержимое контейнера, а затем добавляют контейнер к странице и обновляют интерфейс.
Благодаря командам App.__new__(App).p.window_width - 20 и App.__new__(App).p.window_height - 20 мы можем получать размер основного окна приложения и регулировать размер виджетов непосредственно в методах класса.
Перейдём непосредственно к данным. Рассмотрим реализацию класса хранения данных и его функциональность.
В папке /example_db модуля dbinfo.py поместим данные в словарь:
bank_list = {'79801110001': {'time': ["2024-01-01: 00-00",……..., "2024-02-20: 02-02"], 'sum': ["3721", …..., "121"]}}
Полную версию словаря можно посмотреть здесь.
Создадим класс DataUser в main.py:
class DataUser(ft.DataTable):
__instance = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super(DataUser, cls).__new__(cls)
return cls.__instance
def __init__(self, phone, start_page=0, end_page=5, page_number=1):
super().__init__()
self.columns = [ft.DataColumn(ft.Text("Date Time")),
ft.DataColumn(ft.Text("Sum"), numeric=True),
]
self.start_page = start_page
self.end_page = end_page
self.page_number = page_number
self.border = ft.border.all(5, ft.colors.BLUE)
self.width = App.__new__(App).p.window_width -50
self.height = App.__new__(App).p.window_height -50
self.rows = [ft.DataRow(cells=[ft.DataCell(ft.Text(bank_list[phone]["time"][indx])), ft.DataCell(ft.Text(bank_list[phone]["sum"][indx]))]) for indx in range(len(bank_list[phone]["time"]))][self.start_page:self.end_page]
def move_page(self, phone, start_page ,end_page, page_number):
if page_number > 1:
data_list = [ft.DataRow(cells=[ft.DataCell(ft.Text(bank_list[phone]["time"][indx])), ft.DataCell(ft.Text(bank_list[phone]["sum"][indx]))]) for indx in range(len(bank_list[phone]["time"]))][start_page:end_page]
if data_list:
self.rows = data_list
self.start_page = start_page
self.end_page = end_page
self.page_number = page_number
else:
self.page_number = 1
return self
Класс DataUser представляет собой таблицу с двумя колонками: «Date Time» и «Sum». При инициализации объекта этого класса мы определяем его атрибуты, такие как названия колонок таблицы, диапазон отображаемых страниц, а также ширину и высоту таблицы. Мы заполняем таблицу данными из словаря bank_list, используя номер телефона в качестве ключа, и делаем срез данных для отображения на странице.
Метод move_page отвечает за перемещение по страницам данных. Если страница не первая, он обновляет данные для отображения на основе переданного диапазона страниц и номера страницы. Если список данных пуст, происходит возврат на первую страницу.
Этот класс позволяет эффективно отображать данные о тратах со счёта и обеспечивает удобную навигацию по страницам с данными. Полученную таблицу передаём в контейнер класса TableCont, который уже непосредственно отображается на странице приложения.
На данным этапе реализованы все классы. Можем осуществить запуск приложения в main.py:
def main(page: ft.Page):
App(page)
sing_page = Singup()
if __name__ == "__main__":
ft.app(target=main)
Заключение
Этот проект — пример того, как можно использовать flet==0.18.0 для создания личных кабинетов пользователей и административных панелей, чтобы удобно и быстро осуществить реализацию интерфейса. Этот пример может послужить отправной точкой для разработки более сложных кроссплатформенных приложений.