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

ba63314223e07c5966d22c8c847d5b9e.png

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

© Habrahabr.ru