Photo by Onur Binay on Unsplash
Building a Smart To-Do List Manager with Python, MongoDB & Kivy (Part 2: Screen Creation with Kivy & User Authentication)
Hello Tribe! Welcome to Part 2 of our To-Do app series.
Last time, we went over the project workflow, the setup and we created our database! Today, I will walk you through the creation of screens and user authentication (registration & log in).
Without further ado, let’s get into it.
1 - Screen Creation
Our first topic for today is how to build a screen using Kivy.
What is Kivy?
Kivy is an open-source Python framework for developing GUI apps that work cross-platform, including desktop, mobile and embedded platforms.
The aim is to allow for quick and easy interaction design and rapid prototyping whilst making your code reusable and deployable: Innovative user interfaces made easy.
Okay, well…How do you use it?
Quite simple actually. We are going to create the Welcome screen of our project. This is the first screen that can be seen when the project is launched. These are the generic steps to follow when creating a Kivy Window:
Initialise the Layout
Create a layout to hold the screen’s elements, such as
GridLayout
orBoxLayout
.Set the layout properties like
cols
,size_hint
,pos_hint
,padding
, andspacing
.
Add Widgets to the Layout
Add various widgets like
Image
,Label
,Button
, etc., to the layout.Customise the widgets using parameters like
size_hint
,text
,color
, and more.
Bind Buttons to Functions for specific actions
- Attach event listeners to buttons using the
bind()
method to define actions when the buttons are pressed.
- Attach event listeners to buttons using the
Add the Layout to the Screen
- Add the layout (which contains the widgets) to the screen using a syntax like
self.add_widget()
.
- Add the layout (which contains the widgets) to the screen using a syntax like
Define Functions for Button Actions
- Write functions for navigation (e.g.,
go_to_login
,go_to_register
) or other logic when buttons are clicked.
- Write functions for navigation (e.g.,
For our project, we will create a class name WelcomeScreen that will inherits from Kivy Screen class.
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.core.window import Window
from kivymd.app import MDApp
from kivy.uix.image import Image
# First screen to choose between Login and Register
class WelcomeScreen(Screen):
def __init__(self, **kwargs):
super(WelcomeScreen, self).__init__(**kwargs)
#Initialise the Layout
self.window = GridLayout()
self.window.cols = 1
self.window.size_hint = (0.4, 0.7)
self.window.pos_hint = {"center_x":0.5, "center_y":0.5}
# Add padding and spacing to the GridLayout
self.window.padding = [20, 20, 20, 20] # Padding around the grid
self.window.spacing = [10, 10] # Spacing between widgets
#we can add our application's logo
logo_image = Image(source='mylogo.png', size_hint=(0.4, None), size=(500, 500), allow_stretch=True)
# Center the image horizontally
logo_image.pos_hint = {"center_x": 0.5}
#we add the image to the window/layout
self.window.add_widget(logo_image)
#creating and adding widgets/elements to the window/layout
welcome_label = Label(text="Welcome to your Smart To-Do Manager", color=(0, 0, 0, 1), halign="center",valign="middle")
self.window.add_widget(welcome_label)
login_button = Button(text="Login", size_hint=(0.5, 0.4), bold=True, background_color="#1E88E5")
self.window.add_widget(login_button)
register_button = Button(text="Register", size_hint=(0.5, 0.4), bold=True, background_color="#1E88E5")
self.window.add_widget(register_button)
#here, we bind the buttons to specific functions for them to trigger an action
login_button.bind(on_press=self.go_to_login)
register_button.bind(on_press=self.go_to_register)
# Add the complete layout to the screen
self.add_widget(self.window)
def go_to_login(self, instance):
# Navigate to the login screen
self.manager.current = 'login'
def go_to_register(self, instance):
# Navigate to the registration screen
self.manager.current = 'register'
Explanation:
A- Imports
from kivy.uix.screenmanager import Screen
Purpose: This imports the
Screen
class, which is used to define individual screens in a Kivy application that uses the screen manager. Each screen represents a different view or interface that the user can interact with.Use in the code: The
WelcomeScreen
class inherits fromScreen
, making it a screen in the app.
from kivy.uix.gridlayout import GridLayout
Purpose: This imports the
GridLayout
class, which is a layout that arranges its children (widgets) in a grid with rows and columns.Use in the code: A
GridLayout
is used as the main layout to arrange the widgets (like labels, buttons, and images) in the welcome screen.
from kivy.uix.label import Label
Purpose: This imports the
Label
class, which is a widget used to display text in the app.Use in the code: The
Label
is used to show a welcome message to the user in theWelcomeScreen
.
from kivy.uix.button import Button
Purpose: This imports the
Button
class, which is a widget that the user can click to trigger an action (such as navigating to another screen or performing an action).Use in the code:
Button
widgets are used for the "Login" and "Register" buttons on the screen.
from kivy.uix.image import Image
Purpose: This imports the
Image
class, which is a widget used to display images in the app.Use in the code: The
Image
widget is used to display a logo or graphic on the welcome screen (in this case,mylogo.png
).
B- Code
We define the Screen Class:
The class inherits from
Screen
and represents the entire welcome screen. The__init__
method sets up the screen components.We initialise the layout
Here, to initialise the layout, we used the GridLayout method as no specific orientation is needed.
We set the number of columns (
cols=1
) and adjust the size and position of the layout usingsize_hint
andpos_hint
.We also add some padding to add extra pixels around the edges, keeping the size consistent during operations.
The spacing is used to ensure a certain distance between the elements
#Initialise the Layout self.window = GridLayout() self.window.cols = 1 self.window.size_hint = (0.4, 0.7) self.window.pos_hint = {"center_x":0.5, "center_y":0.5} # Add padding and spacing to the GridLayout self.window.padding = [20, 20, 20, 20] # Padding around the grid self.window.spacing = [10, 10] # Spacing between widgets
We add Widgets to the Layout
Here, we add the widgets representing all the elements you want to display on our screen. On our Welcome screen, we are going to display our logo, a welcome message, the login and registration buttons.
We use the Image widget to create the logo and we will change its properties to make it centred.
We use the Label widget to display the text and the Button widget for the buttons. The key properties we play around here are
size_hint: (width_proportion, height_proportion). It is used to determine how a widget should be sized relative to its parent layout/container. Instead of specifying an absolute size for a widget (in pixels, for instance),
size_hint
allows you to set its size as a proportion of the available space in the parent container (like aBoxLayout
orGridLayout
).color: is used to determine the color of the font used.
background_color is used to set a specific color for the background of a widget.
bold which obviously is to set a text in bold. When used in a Label widget, you have to add the property
markup=True
for the text to be set in bold.
#we can add our application's logo
logo_image = Image(source='mylogo.png', size_hint=(0.4, None), size=(500, 500), allow_stretch=True)
# Center the image horizontally
logo_image.pos_hint = {"center_x": 0.5}
#we add the image to the window/layout
self.window.add_widget(logo_image)
#creating and adding widgets/elements to the window/layout
welcome_label = Label(text="Welcome to your Smart To-Do Manager", color=(0, 0, 0, 1), halign="center",valign="middle")
self.window.add_widget(welcome_label)
login_button = Button(text="Login", size_hint=(0.5, 0.4), bold=True, background_color="#1E88E5")
self.window.add_widget(login_button)
register_button = Button(text="Register", size_hint=(0.5, 0.4), bold=True, background_color="#1E88E5")
self.window.add_widget(register_button)
We bind the buttons to the action functions
Here, we use the
bind()
function to create a “triggering link“ between the buttons and the action functions. When pressed (on_press
), the buttons will call the right functions.#defining some actions login_button.bind(on_press=self.login_action) back_button.bind(on_press=self.go_to_welcome)
The action functions
In the code, I have written two functions that will help us to go either on the login or the registration page.
Simply put, since the class inherits from
Screen
, we have a screen manager that will allow us to go from a screen to another.def go_to_login(self, instance): # Navigate to the login screen self.manager.current = 'login' def go_to_register(self, instance): # Navigate to the registration screen self.manager.current = 'register'
Now, we can initialise our app and visualise our screen.
We create a ToDoApp class that inherits from MDApp.
I choose MDApp (from kivymd.app import MDApp
) instead of Kivy App (from kivy.app import App
) because to create the tasks later, we will need a date picker, a feature that the simple Kivy does not have.
class ToDoApp(MDApp):
def __init__(self, dbname, **kwargs):
super(ToDoApp, self).__init__(**kwargs)
self.dbname = dbname
def build(self):
sm = ScreenManager()
# Add WelcomeScreen, LoginScreen, and RegisterScreen to ScreenManager
sm.add_widget(WelcomeScreen(name='welcome'))
return sm
Now, in my main function, I run this code:
if __name__ == "__main__":
# Get the database
dbname = mongo.get_database()
#code pour lancer le projet en interface graphique
graphic.ToDoApp(dbname).run()
We have this cute window
2- User Authentication
Now that we have created our first page, let’s create the logic for user registration and login.
A- User Registration
First, we will write the code not considering the intervention of Kivy, just as if we were implementing a command line application. This will be a foundation to re-write the functions to serve the purposes of the Kivy classes.
Let’s say, to register, the user has to type in the following command: register xxxx
. We will implement a logic to parse the command entered by the user so that we have for this case an array where x[0] = ‘register‘
and x[1] = ‘xxxx‘
. So, our register function will take the username typed and the database variable as parameters.
First, let’s see how the parsing should go. For practicality, we will use a dictionary to map each command to its action function.
PS: All the functions you see are here because obviously, they were all implemented. They are not all needed just yet.
command_map = {
"login": commands.login,
"register": commands.register,
"add_task": commands.add_task,
"edit_task": commands.edit_task,
"delete_task": commands.delete_task,
"view_task": commands.view_task,
"search": commands.search
}
single_arg_command_map = {
"ongoing_tasks": commands.ongoing_tasks,
"view_completed_tasks": commands.view_completed_tasks
}
def read_cmd(user_input, dbname):
split_cmd = shlex.split(user_input)
if len(split_cmd) == 1:
if split_cmd[0] == "help":
print("help")
if split_cmd[0] == "exit":
#84 is the exit code
return(84)
if len(split_cmd) == 1 and split_cmd[0] in single_arg_command_map:
single_arg_command_map[split_cmd[0]](dbname)
if len(split_cmd) == 2 and split_cmd[0] in command_map:
dbname = command_map[split_cmd[0]](split_cmd[1], dbname)
return 0
In our registration function, we ask the user for a mail and password. For the password, we use the
getpass
function so that the password is not visible. Here’s what it looks like:mail = input("Enter a valid mail adress:\n") #add a verification with regex password = getpass.getpass("Set a password. It has to be at least 7 characters long:\n") while len(password) < 7: password = getpass.getpass("Not strong enough. Try again:\n") if len(password) >= 7: break
For security purpose, the password needs to be hashed before storage. Here is a function that can do just that using the
bcrypt
module:You can read more about how to hash passwords in Python here.
def hash_password(plain_password): # Generate a salt and hash the password salt = bcrypt.gensalt() hashed_password = bcrypt.hashpw(plain_password.encode('utf-8'), salt) return hashed_password
Now, let’s check if the username already exists:
while dbname["users"].find_one({"name":username}): username = input("Username exits! Login or Enter new name:\n")
If everything is correct, we create a dictionary with all user’s information and insert it into our users collection.
new_user_infos = {"name":username, "mail":mail, "password":hpwd} #we create our first collection inside the database users_collection = dbname["users"] if users_collection.insert_one(new_user_infos): print("Registration successful! Proceed.\n") connected_user = username
You might have noticed
connected_user = username
. Good: I useconnected_user
as session management variable. Of course, there are different ways to do so but I chose this simple direction and it works for me (and this little app). I store everything with the name of the connected user making it simple to display user’s specific informations.Here is the whole function:
def register(username,dbname): global connected_user mail = input("Enter a valid mail adress:\n") #add a verification with regex password = getpass.getpass("Set a password. It has to be at least 7 characters long:\n") while len(password) < 7: password = getpass.getpass("Not strong enough. Try again:\n") if len(password) >= 7: break hpwd = hash_password(password) while dbname["users"].find_one({"name":username}): username = input("Username exits! Login or Enter new name:\n") #new_user_infos is a dictionary containing all the information about the user that is trying to register new_user_infos = {"name":username, "mail":mail, "password":hpwd} #we create the "users" collection inside the database users_collection = dbname["users"] if users_collection.insert_one(new_user_infos): print("Registration successful! Proceed.\n") connected_user = username return dbname
To create a version of this function fit to be integrated into our Kivy class, we will apply some little changes.
def register_user(username, mail, password, dbname):
global connected_user
#check if the user already exits
if dbname["users"].find_one({"name":username}):
return None, "User already exists"
if len(password) < 7:
#verify that the password's length is above the required length
return None, "Not strong enough. Try again:"
if len(password) >= 7:
hpwd = hash_password(password)
#new_user_infos is a dictionary containing all the information about the user that is trying to register
new_user_infos = {"name":username, "mail":mail, "password":hpwd}
users_collection = dbname["users"]
if users_collection.insert_one(new_user_infos):
#we stored the username as the connected user
connected_user = username
return dbname, "Registration succesful!"
return None, ""
This function will take as parameters the username, the mail, the password and the database variable. The username, the mail and the password here will be attributes of our
RegistrationScreen
class and used in our function.The function returns:
the database variable and a message when the operation is successful.
None
and a message when there is an issue. This will help us know in the class what kind of message needs to be printed on the screen.
Following the logic of our Welcome screen, the definition of the screen should look approximately the same. But here, our class has to be created with a database variable as arguments so that we can access the database variable created at the beginning of the project. As matter of fact, all the screens from now on will be taking dbname
as parameter of their constructor.
Of course, you can modify this code to fit your specific needs. In the code, I used comments to help you through each step.
def __init__(self, dbname, **kwargs):
super(RegisterScreen, self).__init__(**kwargs)
#this helps us ensure the page is completely blank, white background
Window.clearcolor = (1, 1, 1, 1)
#initialising the layout of the window/screen using GridLayout
self.window = GridLayout()
self.window.cols = 1
self.window.size_hint = (0.4, 0.8)
self.window.pos_hint = {"center_x":0.5, "center_y":0.5}
# Add padding and spacing to the GridLayout
self.window.padding = [15,15,15,15] # Padding around the grid
self.window.spacing = [10, 10] # Spacing between widgets
#creating elements or widgets
logo_image = Image(source='mylogo.png', size_hint=(0.4, None), size=(500, 500), allow_stretch=True)
logo_image.pos_hint = {"center_x": 0.5} # Center the image horizontally
self.window.add_widget(logo_image)
#creating the attributes of the class
self.label = Label(text="[b]Register[/b]", font_size=40, color=(0, 0, 0, 1), halign="center",valign="middle", markup=True)
self.mail = TextInput(hint_text='Mail address', multiline=False, hint_text_color=(0, 0, 0, 1))
self.username = TextInput(hint_text='Username', multiline=False, hint_text_color=(0, 0, 0, 1))
self.password = TextInput(hint_text='Input a strong password: at least seven characters', multiline=False, password=True, password_mask='*', hint_text_color=(0, 0, 0, 1))
self.dbname = dbname
register_button = Button(text='Register', size_hint=(1, 0.6), bold=True, background_color="#004aad")
back_button = Button(text='Back', size_hint=(1, 0.6), bold=True, background_color="#004aad")
#this variable is supposed to hold the error message to be displayed when there is an issue
self.error_message = Label(text="", color=(1, 0, 0, 1)) # Empty error message, red color
#defining some actions and binding
register_button.bind(on_press=self.create_user) # Bind to registration function
back_button.bind(on_press=self.go_to_welcome) #goes back to the welcome page
#adding elements/widgets to the layout/window
self.window.add_widget(self.label)
self.window.add_widget(self.mail)
self.window.add_widget(self.username)
self.window.add_widget(self.password)
self.window.add_widget(Label(text="Already have an account? Go back", font_size=18))
self.window.add_widget(register_button)
self.window.add_widget(back_button)
self.window.add_widget(self.error_message)
self.add_widget(self.window)
I suggest you play around with the properties of each widget so that you have a better understanding of how they work.
Check out the documentation of Kivy here.
The create_user
method takes as parameters self
that is a reference to the current instance of the class and instance
which is a reference to the object or widget that triggered a specific event.
We store the values of our attributes (username, password and mail) in local variables to use in the function for a safer management.
We call the
register_user
function and proceed to some verifications. According to the message, and the content of the data variable, we display a message (in green or in red) and when there is an error, we empty the fields for the user to enter new ones.#Note: gcommands is just a file imported and used as a module def create_user(self, instance): username = self.username.text password = self.password.text mail = self.mail.text dbname = self.dbname #we call the register_user function data, msg = manage_tasks.gcommands.register_user(username, mail, password, dbname) if data != None and msg == "Registration succesful": self.error_message.text = "Registration succesful!" self.error_message.color = (0, 1, 0, 1) # Green color for success #this method will be used to go from the Registration screen to the main screen with the tasks self.go_to_manager(None) if data == None and msg == "User already exists": self.error_message.text = "User already exists" self.error_message.color = (1, 0, 0, 1) # Red color for error # Optionally, clear the fields or leave them for correction self.username.text = "" self.password.text = "" if data == None and msg=="Not strong enough. Try again:": self.error_message.text = "Not strong enough. Try again:" self.error_message.color = (1, 0, 0, 1) self.username.text = "" self.password.text = "" else: self.error_message.text = "Something went wrong" self.error_message.color = (1, 0, 0, 1) self.username.text = "" self.password.text = ""
Let’s take a look at what our class looks like now.
class RegisterScreen(Screen):
def __init__(self, dbname, **kwargs):
super(RegisterScreen, self).__init__(**kwargs)
Window.clearcolor = (1, 1, 1, 1)
self.window = GridLayout()
self.window.cols = 1
self.window.size_hint = (0.4, 0.8)
self.window.pos_hint = {"center_x":0.5, "center_y":0.5}
self.window.padding = [15,15,15,15] # Padding around the grid
self.window.spacing = [10, 10] # Spacing between widgets
logo_image = Image(source='mylogo.png', size_hint=(0.4, None), size=(500, 500), allow_stretch=True)
logo_image.pos_hint = {"center_x": 0.5} # Center the image horizontally
self.window.add_widget(logo_image)
self.label = Label(text="[b]Register[/b]", font_size=40, color=(0, 0, 0, 1), halign="center",valign="middle", markup=True)
self.mail = TextInput(hint_text='Mail address', multiline=False, hint_text_color=(0, 0, 0, 1))
self.username = TextInput(hint_text='Username', multiline=False, hint_text_color=(0, 0, 0, 1))
self.password = TextInput(hint_text='Input a strong password: at least seven characters', multiline=False, password=True, password_mask='*', hint_text_color=(0, 0, 0, 1))
self.dbname = dbname
register_button = Button(text='Register', size_hint=(1, 0.6), bold=True, background_color="#004aad")
back_button = Button(text='Back', size_hint=(1, 0.6), bold=True, background_color="#004aad")
self.error_message = Label(text="", color=(1, 0, 0, 1)) # Empty error message, red color
register_button.bind(on_press=self.create_user) # Bind to registration function
back_button.bind(on_press=self.go_to_welcome) #goes back to the welcome page
#adding elements/widgets to the window
self.window.add_widget(self.label)
self.window.add_widget(self.mail)
self.window.add_widget(self.username)
self.window.add_widget(self.password)
self.window.add_widget(Label(text="Already have an account? Go back", font_size=18))
self.window.add_widget(register_button)
self.window.add_widget(back_button)
self.window.add_widget(self.error_message)
self.add_widget(self.window)
def create_user(self, instance):
username = self.username.text
password = self.password.text
mail = self.mail.text
dbname = self.dbname
data, msg = manage_tasks.gcommands.register_user(username, mail, password, dbname)
if data != None and msg == "Registration succesful":
self.error_message.text = "Registration succesful!"
self.error_message.color = (0, 1, 0, 1) # Green color for success
self.go_to_manager(None)
if data == None and msg == "User already exists":
self.error_message.text = "User already exists"
self.error_message.color = (1, 0, 0, 1) # Red color for error
# Optionally, clear the fields or leave them for correction
self.username.text = ""
self.password.text = ""
if data == None and msg=="Not strong enough. Try again:":
self.error_message.text = "Not strong enough. Try again:"
self.error_message.color = (1, 0, 0, 1)
self.username.text = ""
self.password.text = ""
else:
self.error_message.text = "Something went wrong"
self.error_message.color = (1, 0, 0, 1)
self.username.text = ""
self.password.text = ""
def go_to_welcome(self, instance):
# Navigate to the login screen
self.manager.current = 'welcome'
def go_to_manager(self, instance):
# Navigate to the main screen
self.manager.current = 'task_list'
B- User Login
Here again, we will first take a look at what the login function should look like if our application were a command line application instead of a Kivy one.
With the same considerations as previously, we know that to login, the user has to type in
login xxxx
. This command is parsed so thatx[0] = ‘login‘
andx[1] = ‘xxxx‘
. Our function will take the username typed and the database variable as parameters.Since a username has been provided, we will directly use the find_one method of MongoDB to check if the user is registered.
If he/she is registered, we now ask for the password. We use the
bcrypt.checkpw
method to check if the given password (hashed) is equal to the stored password for this user (hashed of course =) !).As long as the password is incorrect, we keep prompting the user to enter a correct password. Our function should look like this:
def login(username, dbname): global connected_user users_collection = dbname["users"] #the user has entered his/her name, let's see if it exists logging_user = users_collection.find_one({"name":username}) #if found if logging_user: #we check if the password is correct password = getpass.getpass("What's your password? ") pwd_check = bcrypt.checkpw(password.encode('utf-8'), looging_user["password"]) while not pwd_check: password = getpass.getpass("Password incorrect:\n") pwd_check = bcrypt.checkpw(password.encode('utf-8'), looging_user["password"]) print("Login successful!\n") connected_user = username return dbname return dbname
Considering how our current function is implemented, I think you might have guessed how the “Kivy conscious“ version of it should be written.
The function takes the username, the password (stored as attributes in our class) and the database variable.
Using the find_one function, we check if the user is registered. It yes, we check if the password is correct. If the password is incorrect, we return None and an error message and if everything is correct, we return the database variable and the success message.
You might have noticed that I didn't specify anything in case the user is not found. The function will return the good values in very specific cases and if something went wrong (anything that I haven’t thought of), this line
return None, ""
will take care of it. But feel free to specify all events for safety!
def login_user(username, password, dbname):
global connected_user
users_collection = dbname["users"]
#l'utilisateur met son nom
logging_user = users_collection.find_one({"name":username})
#rechercher l'utilisateur
if logging_user:
pwd_check = bcrypt.checkpw(password.encode('utf-8'), looging_user["password"])
if not pwd_check:
return None, "Incorrect password"
else:
connected_user = username
return dbname, "Login successful!"
return None, ""
Let’s integrate it in our class, shall we? We will use the same logic as for the registration, using the function we just wrote in a method of the class.
def login_action(self, instance):
username = self.username.text
password = self.password.text
dbname = self.dbname
data, msg = manage_tasks.gcommands.login_user(username, password, dbname)
if data != None and msg == "Login successful!":
self.error_message.text = "Login successful!"
self.error_message.color = (0, 1, 0, 1) # Green color for success
self.go_to_manager(None)
else:
self.error_message.text = "Invalid username or password!"
self.error_message.color = (1, 0, 0, 1) # Red color for error
# Optionally, clear the fields or leave them for correction
self.username.text = ""
self.password.text = ""
Now, this time around, I am not going to show you what the class for the Login screen looks like, I think you have a pretty good idea of that by now. But anyways, here’s how the screen should be integrated to the app.
class ToDoApp(MDApp):
def __init__(self, dbname, **kwargs):
super(ToDoApp, self).__init__(**kwargs)
self.dbname = dbname
def build(self):
sm = ScreenManager()
# Add WelcomeScreen, LoginScreen, and RegisterScreen to ScreenManager
sm.add_widget(WelcomeScreen(name='welcome'))
sm.add_widget(LoginScreen(self.dbname, name='login'))
sm.add_widget(RegisterScreen(self.dbname, name='register'))
return sm
After execution, you should have nice screens like these:
Tadaaaaaaaaa!!! We now have three pages and we can finally work on those tasks! But that will be the main focus of the next article.
Thank you for working with me today and stay tuned for part 3 where we are going to create the main screen with tasks and implement some task management (add, edit and delete) features!
Toodaloo! 👋🏾