Как сделать доступ в личный кабинет с помощью Flet

от автора

В этой статье мы рассмотрим, как использовать 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 для создания личных кабинетов пользователей и административных панелей, чтобы удобно и быстро осуществить реализацию интерфейса. Этот пример может послужить отправной точкой для разработки более сложных кроссплатформенных приложений.


ссылка на оригинал статьи https://habr.com/ru/articles/831142/