From 362a83b2e3cdfdb25d86b9824b77d5a35178afea Mon Sep 17 00:00:00 2001 From: Ignace Date: Fri, 9 Jan 2026 13:36:21 +0100 Subject: [PATCH] base app with app-key, registration, login, logout --- icons/logo.png | Bin 0 -> 5073 bytes pyproject.toml | 3 +- src/justgetit/app.py | 65 +++++++++++--- src/justgetit/basepage.py | 101 ++++++++++++++++++++++ src/justgetit/configuration.py | 79 +++++++++++++++++ src/justgetit/constants.py | 18 +++- src/justgetit/images/help.png | Bin 0 -> 6387 bytes src/justgetit/images/key.png | Bin 0 -> 3884 bytes src/justgetit/images/logout.png | Bin 0 -> 6562 bytes src/justgetit/lists.py | 89 ++++++++++++++++++++ src/justgetit/login.py | 134 ++++++++++++++--------------- src/justgetit/providekey.py | 128 ++++++++++++++++++++++++++++ src/justgetit/register.py | 145 ++++++++++++++++---------------- 13 files changed, 602 insertions(+), 160 deletions(-) create mode 100644 icons/logo.png create mode 100644 src/justgetit/basepage.py create mode 100644 src/justgetit/configuration.py create mode 100644 src/justgetit/images/help.png create mode 100644 src/justgetit/images/key.png create mode 100644 src/justgetit/images/logout.png create mode 100644 src/justgetit/lists.py create mode 100644 src/justgetit/providekey.py diff --git a/icons/logo.png b/icons/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6d3ca3263f187280408749d62fb61a3e60ebfc93 GIT binary patch literal 5073 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 8420444..b172d18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ author_email = "ignace@suy.nl" [tool.briefcase.app.justgetit] formal_name = "Just Get It" description = "Just Get It App" +icon = "icons/logo" long_description = """More details about the app should go here. """ sources = [ @@ -22,7 +23,7 @@ test_sources = [ ] requires = [ - "requests", + "httpx", ] test_requires = [ "pytest", diff --git a/src/justgetit/app.py b/src/justgetit/app.py index d26c59b..8f6572b 100644 --- a/src/justgetit/app.py +++ b/src/justgetit/app.py @@ -1,40 +1,79 @@ import toga from toga.style.pack import COLUMN, ROW, CENTER, START, END from toga.style import Pack -import requests from justgetit.constants import * from justgetit.login import Login from justgetit.register import Register +from justgetit.configuration import Configuration +from justgetit.providekey import ProvideKey +from justgetit.lists import Lists class JustEatIt(toga.App): login_screen = toga.Box() register_screen = toga.Box() + providekey_screen = toga.Box() + lists_screen = toga.Box() + configuration = None + last_screen_width = SCREENWIDTH - def goto_next_page(self, widget): + def goto_next_page_by_name(self, destination): """ ask app to activate a next page, id of the page is in the id of the widget :param widget: the widget that called this function """ - print(f"action 6 {widget.id}") - if widget.id == 'register': - self.main_window.content = self.register_screen - elif widget.id =='login': - self.main_window.content = self.login_screen - + if destination == PAGE_REGISTER: + self.set_page(self.register_screen) + elif destination == PAGE_LOGIN: + self.set_page(self.login_screen) + elif destination == PAGE_LISTS: + self.set_page(self.lists_screen) def startup(self): - self.login_screen = Login(navigate_to=self.goto_next_page) - self.register_screen = Register(navigate_to=self.goto_next_page) + self.configuration = Configuration(self) - self.main_window = toga.MainWindow(title=self.formal_name, size=(SCREENWIDTH, SCREENHEIGHT)) - self.main_window.content = self.register_screen + self.login_screen = Login() + self.register_screen = Register() + self.providekey_screen = ProvideKey() + self.lists_screen = Lists() + + self.main_window = toga.MainWindow(title=self.formal_name) + + # here comes the startup logic. + # check if there is a valid APP-KEY + # check if there is va alid user and user has been logged in recently + if self.configuration.has_valid_app_key(): + if self.configuration.user_is_logged_in(): + self.set_page(self.lists_screen) + else: + self.set_page(self.login_screen) + else: + self.set_page(self.providekey_screen) self.main_window.show() + # Initial layout clamp + self.on_resize(self.main_window) + + def set_page(self, page): + page.set_screen_width(self.last_screen_width) + self.main_window.content = page + def on_resize(self, widget, **kwargs): + width = self.main_window.size.width + if width < SCREENWIDTH: + # Phone-like: fill available width + elf.last_screen_width = width + else: + self.last_screen_width = SCREENWIDTH + # set actie screen to the corre ct width + self.main_window.content.set_screen_width(self.last_screen_width) + + def on_exit(self): + self.configuration.save() + return super().on_exit() def main(): - return JustEatIt() \ No newline at end of file + return JustEatIt(icon='images/logo.png') \ No newline at end of file diff --git a/src/justgetit/basepage.py b/src/justgetit/basepage.py new file mode 100644 index 0000000..9d778a6 --- /dev/null +++ b/src/justgetit/basepage.py @@ -0,0 +1,101 @@ +import toga +from toga.style import Pack +from toga.style.pack import COLUMN, ROW, END, START, CENTER, HIDDEN, JUSTIFY +# from justgetit.constants import SCREENHEIGHT, SCREENWIDTH +import asyncio +from justgetit.constants import * +import httpx + + +class BasePage(toga.Box): + dialog_text = '' + first_menu_button_icon = '' + first_menu_button_callback = None + second_menu_button_icon = '' + second_menu_button_callback = None + page_pngimage = '' + + def __init__(self, *args, **kwargs): + """ + This is a skeleton page with the basic setup for all pages: + header with button + some services + + :param self: me + :param args: args for super + :param kwargs: kwars for super + """ + super().__init__(*args, **kwargs) + + # menu bar. button(s) to be added in subclasses + menu_bar_inner = toga.Box( + children = [], + style=Pack( + background_color="#efefef", + justify_content=END, + flex=1), + ) + + if self.first_menu_button_callback and self.first_menu_button_icon: + first_button = toga.Button( + icon=toga.Icon(f"images/{self.first_menu_button_icon}"), + style=Pack(margin=5), + on_press=self.first_menu_button_callback) + menu_bar_inner.add(first_button) + + if self.second_menu_button_callback and self.second_menu_button_icon: + second_button = toga.Button( + icon=toga.Icon(f"images/{self.second_menu_button_icon}"), + style=Pack(margin=5), + on_press=self.second_menu_button_callback) + menu_bar_inner.add(second_button) + + menu_bar_outer = toga.Row( + children = [menu_bar_inner] + ) + + # line ast the bottom of the menubar + liner=toga.Box(style=Pack(flex=1, height = 2, background_color="#ddd")) + + # create the main body + self.content_box = toga.Box( + style=Pack( + direction=COLUMN, + align_items=CENTER, + justify_content=END, + margin=24 + ) + ) + + # add image + if self.page_pngimage: + center_image = toga.Image(f"images/{self.page_pngimage}.png") + center_image_view = toga.ImageView(center_image, style=Pack( + width=50, + margin_bottom=20 + )) + self.content_box.add(center_image_view) + + # wrapping up the page elements header liner and body + self.add(menu_bar_outer) + self.add(liner) + self.add(self.content_box) + self.style=Pack( + direction=COLUMN, + flex=1, + align_items=CENTER, + ) + + def set_screen_width(self, w): + """ + mainscreen is calling this for setting proper screen width, upon start and on-resize + + :param self: this screen + :param w: width + """ + self.content_box.style.width = w + + async def dialog(self, widget, **kwargs): + ask_a_question = toga.InfoDialog( + "Help", + "some text") diff --git a/src/justgetit/configuration.py b/src/justgetit/configuration.py new file mode 100644 index 0000000..5f50efd --- /dev/null +++ b/src/justgetit/configuration.py @@ -0,0 +1,79 @@ +import shelve +import datetime + +DBFILE = "config.shelve" + +class Configuration(): + """ + This class holds persistent configuration: + - appkey: the app key, alowing traffic with the backend + - last_key_validation_date: last date the appkey was validated, should be no more than a week ago otherwise the app will not run + - username: the user name + - userpwd: the users pwd + - userlang: the users language + - lastlogin_date: date of last sucessful login + """ + loaded = False + data = {} + + def __init__(self, app): + self.app = app + self.load() + + def load(self): + path = self.app.paths.config / DBFILE + if path.exists(): + shelve_file = shelve.open(path) + self.data = shelve_file['data'] + shelve_file.close() + print("shelve opened") + print(self.data) + + def save(self): + shelve_file = shelve.open(self.app.paths.config / DBFILE) + shelve_file['data'] = self.data + shelve_file.close() + print("shelve stored") + + + def has_valid_app_key(self): + if 'appkey' in self.data: + print("found app key {self.data['appkey']}") + today = datetime.date.today() + last_month = today - datetime.timedelta(days=30) + if self.data['last_key_validation_date'] > last_month: + self.data['last_key_validation_date'] = today + return True + return False + + def store_app_key(self, new_key): + self.data['appkey'] = new_key + self.data['last_key_validation_date'] = datetime.date.today() + self.save() + + def user_is_logged_in(self): + if 'username' in self.data: + print("found user {self.data['username']}") + today = datetime.date.today() + last_week = today - datetime.timedelta(days=7) + if self.data['lastlogin_date'] > last_week: + self.update_last_activity_date() + return True + return False + + def store_login(self, user, pwd): + self.data['username'] = user + self.data['userpwd'] = pwd + self.data['lastlogin_date'] = datetime.date.today() + self.save() + + def update_last_activity_date(self): + today = datetime.date.today() + self.data['last_key_validation_date'] = today + self.data['lastlogin_date'] = today + + def logout_user(self): + self.data.pop('username', None) + self.data.pop('userpwd', None) + self.data.pop('lastlogin_date', None) + self.save() \ No newline at end of file diff --git a/src/justgetit/constants.py b/src/justgetit/constants.py index 5efa2b8..74e593a 100644 --- a/src/justgetit/constants.py +++ b/src/justgetit/constants.py @@ -1,2 +1,16 @@ -SCREENWIDTH = 380 -SCREENHEIGHT = 680 +SCREENWIDTH = 400 +# SCREENHEIGHT = 680 + +PAGE_LOGIN = 'login' +PAGE_REGISTER = 'register' +PAGE_LISTS = 'lists' +PAGE_EDITLISTS = 'editlists' +PAGE_ITEMS = 'items' +PAGE_ADDITEMS = 'additems' + +HELP_ON_KEY = """ +You should have received a application-key (a bunch of characters) from the person referring you to this app. + +If not, sorry, but this is not a free app. +""" +API = "http://127.0.0.1:5001/api" \ No newline at end of file diff --git a/src/justgetit/images/help.png b/src/justgetit/images/help.png new file mode 100644 index 0000000000000000000000000000000000000000..a72ff855eef1d68edc2a1c5043f42cd54da2ea02 GIT binary patch literal 6387 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 B16Tk6 literal 0 HcmV?d00001 diff --git a/src/justgetit/images/key.png b/src/justgetit/images/key.png new file mode 100644 index 0000000000000000000000000000000000000000..e0b28d6bf5de450b1cecabf74d1546c1996cd28f GIT binary patch literal 3884 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 UDo~vNAOHXW07*qoM6N<$g5{sJ5&!@I literal 0 HcmV?d00001 diff --git a/src/justgetit/lists.py b/src/justgetit/lists.py new file mode 100644 index 0000000..3e4f4b6 --- /dev/null +++ b/src/justgetit/lists.py @@ -0,0 +1,89 @@ +import toga +from toga.style import Pack +from toga.style.pack import COLUMN, ROW, END, START, CENTER, HIDDEN, JUSTIFY +# from justgetit.constants import SCREENHEIGHT, SCREENWIDTH +import asyncio +from justgetit.constants import * +import httpx + + +class Lists(toga.Box): + dialog_txt = "" + + def __init__(self, *args, **kwargs): + """ + Docstring for __init__ + + :param self: register-screen + :param args: args for super + :param kwargs: kwars for super + """ + super().__init__(*args, **kwargs) + + # menu bar with register button + register_button = toga.Button( + icon=toga.Icon("images/logout"), + id="login", + style=Pack(margin=5), + on_press=self.logout_button_pressed) + menu_bar_inner = toga.Box( + children = [register_button], + style=Pack( + background_color="#efefef", + justify_content=END, + flex=1), + ) + menu_bar = toga.Row( + children = [menu_bar_inner] + ) + + # line ast the bottom of the menubar + liner=toga.Box(style=Pack(flex=1, height = 2, background_color="#ddd")) + + # body of the screen with image 2 edit fielfds and ok button + # Title icon + my_image = toga.Image("images/key.png") + title = toga.ImageView(my_image, style=Pack( + width=50, + margin_bottom=20 + )) + + # wrapping up the windows elements + self.content_box = toga.Box( + children=[ + title, + + # self.helpbox + + ], + style=Pack( + direction=COLUMN, + align_items=CENTER, + justify_content=END, + margin=24 + ) + ) + + # bringing the 3 parts together + self.add(menu_bar) + self.add(liner) + self.add(self.content_box) + + self.style=Pack( + direction=COLUMN, + flex=1, + align_items=CENTER, + ) + + def set_screen_width(self, w): + """ + mainscreen is calling this for setting proper screen width, upon start and on-resize + + :param self: this screen + :param w: width + """ + self.content_box.style.width = w + + def logout_button_pressed(self, widget): + self.app.configuration.logout_user() + self.app.goto_next_page_by_name(PAGE_LOGIN) \ No newline at end of file diff --git a/src/justgetit/login.py b/src/justgetit/login.py index bb59c86..46beaa5 100644 --- a/src/justgetit/login.py +++ b/src/justgetit/login.py @@ -1,106 +1,98 @@ import toga from toga.style import Pack -from toga.style.pack import COLUMN, ROW, END, START, CENTER -from justgetit.constants import SCREENHEIGHT, SCREENWIDTH +from toga.style.pack import COLUMN, ROW, END, START, CENTER, HIDDEN, JUSTIFY +# from justgetit.constants import SCREENHEIGHT, SCREENWIDTH +import asyncio +from justgetit.constants import * +import httpx +from justgetit.basepage import BasePage -class Login(toga.Box): +class Login(BasePage): + + def __init__(self, *args, **kwargs): + """ + This page allows the user to login + Login details will be kept for a week after the user last uses this app + + :param self: login-screen + :param args: args for super + :param kwargs: kwars for super + """ + # set button and image + self.first_menu_button_icon = 'register' + self.first_menu_button_callback = self.goto_registration + self.page_pngimage = 'login' - def __init__(self, navigate_to, *args, **kwargs): super().__init__(*args, **kwargs) - self.navigate_to= navigate_to - register_button = toga.Button( - icon=toga.Icon("images/register"), - style=Pack( - width=50, - margin=5, - ), - id="register", - on_press=self.navigate_to) - - liner =toga.Box(style=Pack(width=SCREENWIDTH, height = 2, background_color="#ddd")) - - menu_bar= toga.Row( - children=[register_button, liner], - style=Pack( - justify_content=END, - width = SCREENWIDTH, - background_color="#eee" - ) - ) - - # Title icon - my_image = toga.Image("images/login.png") - title = toga.ImageView(my_image, style=Pack( - width=50, - margin_bottom=20 - )) - - # Email input - name_input = toga.TextInput( + # Name input + self.name_input = toga.TextInput( placeholder="Could be you here", style=Pack( width=250, margin_bottom=10 ) ) - # Password input - password_input = toga.PasswordInput( + self.password_input = toga.PasswordInput( placeholder="Feeling lucky?", style=Pack( width=250, margin_bottom=20 ) ) - - # Login button - login_button = toga.Button( + # Save button + save_button = toga.Button( icon=toga.Icon("images/save"), style=Pack( width=50, margin=10, - background_color="white" ), - on_press=self.login_button_press) + on_press=self.login_button_pressed) - login_area= toga.Box( - children=[login_button], + save_button_box= toga.Column( + children=[save_button], style=Pack( - width=250, - direction=ROW, + flex=1, align_items=START, margin=20 ) ) - # Main content box (centered) - content_box = toga.Column( - children=[ - title, - name_input, - password_input, - login_area - ], - style=Pack( - align_items=CENTER, - height=SCREENHEIGHT - 50, # iPhone height - margin=20, - ) - ) - # Outer box to simulate iPhone screen - screen = toga.Column( - children=[menu_bar, - liner, - content_box], - style=Pack( - width=SCREENWIDTH, # iPhone width - ) - ) + # wrapping up the windows elements + self.content_box.add(self.name_input) + self.content_box.add(self.password_input) + self.content_box.add(save_button_box) - self.add(screen) + async def login_button_pressed(self, widget): + self.result = 0 + self.message = '' + user = self.name_input.value + pwd = self.password_input.value - def login_button_press(self, widget): - print("pressesd") \ No newline at end of file + if len(user)>0 and len(pwd)>0: + data = {'user': user, 'password': pwd} + try: + async with httpx.AsyncClient() as client: + response = await client.post(API+'/login', data=data) + payload = response.json() + print(payload) + self.result = int(payload['status']) + if 'message' in payload: + self.message = payload['message'] + except httpx.RequestError as exc: + self.result = -2 + + if self.result == 1: + # store login etc in config + self.app.configuration.store_login(user, pwd) + # goto lists screen + self.app.goto_next_page_by_name(PAGE_LISTS) + else: + await self.app.main_window.dialog( + toga.InfoDialog("Login error", self.message) + ) + def goto_registration(self, dummy): + self.app.goto_next_page_by_name(PAGE_REGISTER) \ No newline at end of file diff --git a/src/justgetit/providekey.py b/src/justgetit/providekey.py new file mode 100644 index 0000000..32d52e9 --- /dev/null +++ b/src/justgetit/providekey.py @@ -0,0 +1,128 @@ +import toga +from toga.style import Pack +from toga.style.pack import COLUMN, ROW, END, START, CENTER, HIDDEN, JUSTIFY +# from justgetit.constants import SCREENHEIGHT, SCREENWIDTH +import asyncio +from justgetit.constants import * +import httpx +from justgetit.basepage import BasePage + + +class ProvideKey(BasePage): + + def __init__(self, *args, **kwargs): + """ + This page allows you to enter the aplication key + The aplication-key is kept until a month after the app is last used + + :param self: register-screen + :param navigate_to: function to call if we want to switch screens + :param args: args for super + :param kwargs: kwars for super + """ + # set button and image + self.first_menu_button_icon = 'help' + self.first_menu_button_callback = self.help_dialog + self.page_pngimage = 'key' + + super().__init__(*args, **kwargs) + + # Key input + self.app_key = toga.TextInput( + placeholder="your application key goes here", + style=Pack( + width=250, + margin_bottom=10 + ) + ) + # Save button + save_button = toga.Button( + icon=toga.Icon("images/save"), + style=Pack( + width=50, + margin=10, + ), + on_press=self.save_button_pressed) + + save_button_box= toga.Column( + children=[save_button], + style=Pack( + flex=1, + align_items=START, + margin=20 + ) + ) + + # # the help text box + # self.helptext = toga.Label(HELP_ON_KEY, + # style=Pack(margin=10, + # background_color="#eee", + # text_align=JUSTIFY) + # ) + + # self.helpbox_inner = toga.Box( + # children = [self.helptext], + # style=Pack( + # direction=COLUMN, + # justify_content=CENTER, + # margin=2, + # background_color="#eee" + # ) + # ) + + # self.helpbox = toga.Box( + # children = [self.helpbox_inner], + # style=Pack( + # direction=COLUMN, + # justify_content=CENTER, + # margin=24, + # background_color="#ccc", + # visibility=HIDDEN + # ) + # ) + + + # wrapping up the windows elements + self.content_box.add(self.app_key) + self.content_box.add(save_button_box) + + async def save_button_pressed(self, widget): + self.result = 0 + print(self.app_key.value) + if len(self.app_key.value)>1: + data = {'key': self.app_key.value} + try: + async with httpx.AsyncClient() as client: + response = await client.post(API+'/validate_key', data=data) + payload = response.json() + print(payload) + self.result = int(payload['status']) + except httpx.RequestError as exc: + self.result = -2 + + self.dialog_txt = "Key unknowm. \nTry again." + if self.result == 1: + # success + self.dialog_txt = "Key validated. \nProceed to login or registration." + + await self.app.main_window.dialog( + toga.InfoDialog("Key Validation", self.dialog_txt) + ) + self.app.configuration.store_app_key(self.app_key.value) + # goto login screen + self.app.goto_next_page_by_name(PAGE_LOGIN) + + else: + await self.app.main_window.dialog( + toga.InfoDialog("Key Validation", "Key unknowm. \nTry again.") + ) + + async def help_dialog(self, widget, **kwargs): + ask_a_question = toga.InfoDialog( + "Help", + HELP_ON_KEY) + + if await self.app.main_window.dialog(ask_a_question): + print("The user said yes!") + else: + print("The user said no.") \ No newline at end of file diff --git a/src/justgetit/register.py b/src/justgetit/register.py index cdf8ada..0c0cf2e 100644 --- a/src/justgetit/register.py +++ b/src/justgetit/register.py @@ -1,107 +1,106 @@ import toga from toga.style import Pack -from toga.style.pack import COLUMN, ROW, END, START, CENTER -from justgetit.constants import SCREENHEIGHT, SCREENWIDTH +from toga.style.pack import COLUMN, ROW, END, START, CENTER, HIDDEN, JUSTIFY +# from justgetit.constants import SCREENHEIGHT, SCREENWIDTH +import asyncio +from justgetit.constants import * +import httpx +from justgetit.basepage import BasePage -class Register(toga.Box): +class Register(BasePage): + + def __init__(self, *args, **kwargs): + """ + This page allows a new user to register + + :param self: registratioin-page + :param args: args for super + :param kwargs: kwars for super + """ + # set button and image + self.first_menu_button_icon = 'login' + self.first_menu_button_callback = self.goto_login + self.page_pngimage = 'register' - def __init__(self, navigate_to, *args, **kwargs): super().__init__(*args, **kwargs) - self.navigate_to= navigate_to - register_button = toga.Button( - icon=toga.Icon("images/login"), - style=Pack( - width=50, - margin=5, - ), - id="login", - on_press=self.navigate_to) - liner = toga.Box(style=Pack(width=SCREENWIDTH, height = 2, background_color="#ddd")) - - menu_bar= toga.Row( - children=[register_button, liner], - style=Pack( - justify_content=END, - width = SCREENWIDTH, - background_color="#eee" - ) - ) - - # Title icon - my_image = toga.Image("images/register.png") - title = toga.ImageView(my_image, style=Pack( - width=50, - margin_bottom=20 - )) - - # Email input - name_input = toga.TextInput( - placeholder="Gimme you wallet", + # Name input + self.name_input = toga.TextInput( + placeholder="Could be you here", style=Pack( width=250, margin_bottom=10 ) ) - # Password input - password_input = toga.PasswordInput( - placeholder="You have a secret?", + self.password_input = toga.PasswordInput( + placeholder="Feeling lucky?", style=Pack( width=250, margin_bottom=20 ) ) - - # Login button - login_button = toga.Button( + # Save button + save_button = toga.Button( icon=toga.Icon("images/save"), style=Pack( width=50, margin=10, - background_color="white" ), - - on_press=self.register_button_press) + on_press=self.register_button_pressed) - login_area= toga.Box( - children=[login_button], + save_button_box= toga.Column( + children=[save_button], style=Pack( - width=250, - direction=ROW, + flex=1, align_items=START, margin=20 ) ) - # Main content box (centered) - content_box = toga.Column( - children=[ - title, - name_input, - password_input, - login_area - ], - style=Pack( - align_items=CENTER, - height=SCREENHEIGHT - 50, # iPhone height - margin=20, - ) - ) - # Outer box to simulate iPhone screen - screen = toga.Column( - children=[menu_bar, - liner, - content_box], - style=Pack( - width=SCREENWIDTH, # iPhone width - ) - ) + # wrapping up the windows elements + self.content_box.add(self.name_input) + self.content_box.add(self.password_input) + self.content_box.add(save_button_box) - self.add(screen) - def register_button_press(self, widget): - print("presses register") \ No newline at end of file + async def register_button_pressed(self, widget): + self.result = 0 + self.message = '' + user = self.name_input.value + pwd = self.password_input.value + pwd2 = self.password_verify_input.value + + if len(user)>0 and len(pwd)>0 and len(pwd2)>0: + if pwd==pwd2: + data = {'user': user, 'password': pwd} + try: + async with httpx.AsyncClient() as client: + response = await client.post(API+'/register', data=data) + payload = response.json() + print(payload) + self.result = int(payload['status']) + if 'message' in payload: + self.message = payload['message'] + except httpx.RequestError as exc: + self.result = -2 + + if self.result == 1: + # store login etc in config + self.app.configuration.store_login(user, pwd) + # goto lists screen + self.app.goto_next_page_by_name(PAGE_LOGIN) + else: + await self.app.main_window.dialog( + toga.InfoDialog("Registration error", self.message) + ) + + else: + await self.app.main_window.dialog( + toga.InfoDialog("Error", "Passwords do not match") + ) + def goto_login(self, dummy): + self.app.goto_next_page_by_name(PAGE_LOGIN) \ No newline at end of file