diff --git a/icons/logo.png b/icons/logo.png new file mode 100644 index 0000000..6d3ca32 Binary files /dev/null and b/icons/logo.png differ 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 0000000..a72ff85 Binary files /dev/null and b/src/justgetit/images/help.png differ diff --git a/src/justgetit/images/key.png b/src/justgetit/images/key.png new file mode 100644 index 0000000..e0b28d6 Binary files /dev/null and b/src/justgetit/images/key.png differ diff --git a/src/justgetit/images/logout.png b/src/justgetit/images/logout.png new file mode 100644 index 0000000..e4c02ac Binary files /dev/null and b/src/justgetit/images/logout.png differ 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