Building a Smart To-Do List Manager with Python, MongoDB & Kivy (Part 3: Main Screen & Task Management Screens)
Discover how to create the main screen for displaying tasks and important buttons, plus how to build the task creation feature

I’m Prunella DOUSSO, an IT student with a passion for data science and software engineering. This blog is my space to share insights, experiences, and opinions, making tech discoveries fun and accessible to everyone, no matter your skill level.
Hello tribe, welcome to part 3 of our Series about building a to-do application using Python, MongoDB and Kivy.
In the previous tutorials, we learnt how to:
setup the development environment
interact with MongoDB in Python
create a screen using Kivy
build the User Authentication feature
Today, I will walk you over how I built the main screen of the app and how I built the task details view, the creation & edition features.
DISCLAIMER! The article is quite long but those features strongly interact with each other and tackling them in different articles would be confusing (for you and me xD). Of course, this logic isn't mandatory, but it might give you some ideas or serve as a useful comparison (or guide).
I wish you good reading and coding!
Main Screen’s class
As per usual, we are working around classes here. We will build a class named TaskListScreen inheriting from Screen (obviously) that will represent our main screen. Here’s how that class will work:
The screen is divided into two sections:
The “header“ section that contains pretty much whatever you want: your logo, the date or a text displaying “Today“, the search widget and the filter button.
The “tasks“ section that contains a TabbedPanel holding the different states of our screen. The tasks will indeed be displayed by section: All, Pending, Completed. We will talk more about the TabbedPanel in a little bit.
Let’s dive into the TaskListScreen class.
We initialise our class with the database variable as parameter
class TaskListScreen(Screen): def __init__(self, dbname, **kwargs): super(TaskListScreen, self).__init__(**kwargs) self.dbname = dbnameWe then initialise the layout of the screen, creating a section for the header and a section for the tabs. I didn’t really create a particular section for the tabs because they are already in a TabbedPanel and I figured I didn’t need to specifically define a section for it.
self.layout = BoxLayout(orientation='vertical', padding=10, spacing=10) top_section = BoxLayout(orientation='horizontal', size_hint_y=0.1, padding=[10, 10, 10, 10])It is now time to create and add the widgets of the top section. I chose to put the current date on the left side, the filter and search buttons on the right.
Please don’t mind the binding functions on the search and filter buttons, we will discuss them in another tutorial.
current_date = gcommands.datetime.now().strftime(f"%B {get_day_with_suffix(gcommands.datetime.now().day)} %Y, %A") date = Label(text=current_date, font_size='20sp', size_hint=(0.5, None), height=50, halign='left', valign='middle', color=(0, 0, 0, 1)) date.bind(size=date.setter('text_size')) # Search field (on the right side) filter_button = Button(text="Filter Tasks", size_hint_x=0.2, height=50) filter_button.bind(on_press=self.show_filter_popup) self.search_input = TextInput(hint_text="Search tasks", size_hint_x=0.5, multiline=False) self.search_input.bind(text=self.on_search_text) # Add logo if present, date, and search to the top_section layout top_section.add_widget(date) top_section.add_widget(filter_button) top_section.add_widget(self.search_input) self.layout.add_widget(top_section)The next step is to create the entity that will hold our different tabs, the tabs themselves and all the entities surrounding them.
We do this by creating a TabbedPanel item as our “container“ and we create a classTaskTab, inheriting from TabbedPanelItem, to represent the tabs on the screen. I chose to proceed this way to ensure a DRY code and this approach is to be used solely as a reference.Before we look at the code, a little bit of theory.
What is a TabbedPanel?
The
TabbedPanelin Kivy is a widget that allows you to create a tabbed interface, where different sections or views of an application can be accessed through tabs. Each tab can contain different widgets, such as buttons, labels, text inputs, or even other layouts. This makes it easy to organise related content and provides a clean navigation mechanism within the app.I like to think of it as a sort of a table or container that is able to hold multiple screens.
TabbedPanel and tabs creation on the screen
Use the comments in the code as reference.
#we create our TabbedPanel self.task_tabs = TabbedPanel(size_hint_y=0.9, do_default_tab=False) #we create three tabs, one for each section on our screen. #Tasks will be displayed according to the names == status self.all_tab = TaskTab('All', self.dbname, self) self.pending_tab = TaskTab('Pending', self.dbname, self) self.completed_tab = TaskTab('Completed', self.dbname, self) #let's add the tabs to the TabbedPanel entity self.task_tabs.add_widget(self.all_tab) self.task_tabs.add_widget(self.pending_tab) self.task_tabs.add_widget(self.completed_tab) #we now add the TabbedPanel to the widgets of the window self.layout.add_widget(self.task_tabs)Let’s go over the TaskTab class
The TaskTab class is a DRY solution for representing and managing task lists. This class, derived from TabbedPanelItem, will be used to display different categories of tasks (all, pending, completed) in a user-friendly interface.
The structure of the class is the following:
The construction:
The class takes as parameters the category name, the database variable and an instance of the TaskListScreen class. All the parameters might be pretty obvious except from the instance of the TaskListScreen class.
Actually, it depends on your code flow. In my code, I decided to display each task in a nice frame. To ensure a modular code, I created a class TaskFrame which represents each task in a frame. In that frame, I put a view button that is supposed to open a details page about the task when pressed. Since all the tasks are meant to be displayed on the TaskListScreen’s window, the function that opens the ‘details‘ page/window is a method of
TaskListScreen, hence the importance of an instance of this class in nearly all the classes we will create today.The initialisation is pretty much the same as for our previous classes. Basically, we create a layout that will contain a
Scrollviewobject to allow scrolling through tasks.
The methods:
The main method here is the one that will help us get and display our tasks data.
To get the tasks information in MongoDB, we will use the find method of MongoDB like so:
def get_tasks(dbname, category): #we get the tasks collection from our database tasks_collection = dbname["tasks"] # Find tasks where the creator_name matches the connected user if category.lower() == 'all': user_tasks = list(tasks_collection.find({"creator_name": connected_user}, {"_id":0, "creator_name":1, "name": 1, "description": 1, "deadline":1, "priority":1, "status":1, "category":1, "creation_date": 1, "edited": 1})) elif category.lower() == 'pending': user_tasks = list(tasks_collection.find({"creator_name": connected_user, "status": {"$in": ["in progress", "to-do"]}}, {"_id":0, "creator_name":1, "name": 1, "description": 1, "deadline":1, "priority":1, "status":1, "category":1, "creation_date": 1,"edited": 1})) elif category.lower() == 'completed': user_tasks = list(tasks_collection.find({"creator_name": connected_user, "status": "completed"}, {"_id":0, "creator_name":1, "name": 1, "description": 1, "deadline":1, "priority":1, "status":1, "category":1, "creation_date": 1,"edited": 1})) return user_tasksYour TaskTab
displaymethod should therefore call this function like so:#that is the file where I write all MongoDB management functions import gcommands def display_tasks(self): # Retrieve tasks from the database tasks = [] #self.text is the attribute that holds the value of the category_name parameter tasks = gcommands.get_tasks(self.dbname, self.text) self.tasks = tasks # Clear the layout before adding tasks #After creating the ScrollView, I have created another box just to "contain" the tasks frames self.task_list_layout.clear_widgets() # Add tasks to the layout for task in tasks: # Create a framed box for each task task_frame = TaskFrame(task, self.task_list_screen) # Add the task frame to the layout self.task_list_layout.add_widget(task_frame)
The
TaskFrameclassThe TaskFrame class represent a horizontal box displaying just some information on the task like the title, the status and the deadline. Since it is a box, it will inherit from the BoxLayout object/class.
The parameters are a dictionary (tasks) that holds all the informations about a task and an instance of TaskListScreen class (explained earlier)
The class is configured with a vertical or horizontal orientation and specified dimensions, creating a framework for each task.
You can really freestyle everything now: make rounded borders, display the elements you want: I stuck to the title, status and deadline. On each frame, there is a view button that is bound to the
open_task_detailmethod of the TaskListScreen.view_button.bind(on_press=partial(self.task_list_screen.open_task_details, task))The
open_task_detailmethod opens a screen displaying all the selected tasks details.
I wrote this function in theTaskListScreenclass because it allows us to go from a screen to another. We therefore needed the instance of the class it belonged to be a Screen object so that we could access the screen manager.
Besides, visually, the view button is part of the main screen (built and held by theTaskListScreenclass). Here is the function and we will go over it just after:def open_task_details(self, task, instance): # Set task data in the TaskDetailScreen and navigate to it task_detail_screen = self.manager.get_screen('task_details') task_detail_screen.set_task_data(task) self.manager.current = 'task_details'The
open_task_detailsmethod inTaskListScreentakes ataskparameter, which is a dictionary holding all the details about the selected task. When called,open_task_detailspasses this data to theTaskDetailScreenclass viaset_task_data, so the appropriate values can be assigned to the widgets in the detail view.How
taskFlows Through Classes:display_tasksinTaskTabretrieves tasks from the database, each represented as a dictionary (task).For each
task,display_taskscreates aTaskFrameinstance, passingtaskand a reference toTaskListScreen.Inside
TaskFrame, the 'view' button is linked toopen_task_details. When clicked, it provides thetask, allowing the details view to be filled in accurately.
Responsibilities Recap:
TaskListScreen: Manages screen navigation and theopen_task_detailsmethod.TaskTab: Retrieves tasks by category and organises them in frames.TaskFrame: Represents each task visually and callsopen_task_detailswith the appropriatetaskdata when needed.
Now that we have gone over everything regarding our TabbedPanel, its direct and recursive components, we can add whatever we want to complete the design of the page. Here, I chose to add an add_button component and a method to access the screen dedicated to adding new tasks.
add_button = Button(text="Add task", background_color=(0, 0.5, 1, 1), size_hint=(None, None),size=(100, 50)) add_button.bind(on_press=self.go_add_screen) # Function to go to the tasks creation screen self.layout.add_widget(add_button) self.add_widget(self.layout)def go_add_screen(self, instance): self.manager.current = 'add_task'
🔄 A quick checkpoint and summary!
Here's a summary of what's been achieved so far:
Class Setup: We created a
TaskListScreenclass inheriting fromScreen, which structures the main screen into two sections in a verticalBoxLayout: the "header" (logo, date, search, and filter options) and the "tasks" section (using aTabbedPanelfor task states: All, Pending, and Completed).Task Tab Creation: A custom
TaskTabclass (inheriting fromTabbedPanelItem) was implemented to represent each task category. It initialises with task data from a MongoDB database, displaying tasks based on the tab’s category name (all, pending, or completed).Database Interaction: A
get_tasksfunction was defined for fetching tasks from MongoDB, filtering based on the selected task category.Task Display: Within
TaskTab, thedisplay_tasksmethod fetches tasks and clears/repopulates the layout with task information. Each task is displayed in aTaskFrame, which includes the title, status, and deadline, and a "view" button to open detailed task information.TaskFrame Setup: This class (inheriting
BoxLayout) is used to present individual task info within a flexible frame, allowing for customisation.Task Detail Navigation: The
open_task_detailsmethod inTaskListScreenenables transitioning to a detail screen with in-depth task information when the "view" button is clicked.Adding New Tasks: An "Add Task" button and a navigation method (
go_add_screen) were incorporated, enabling users to access a separate screen for creating new tasks.
Task Details Class
After running the code we wrote so far, we have a pretty cool and simple screen (feel free to change the design later) displaying all tasks in small frames. In each of these frames is a small button (view) that is linked to the open_task_details method in TaskListScreen. Let’s go over the class handling the task details screen.
We will not go over the layout and window initialisation together. Let me not hinder your creativity anymore. Just make sure that your class (or screen) contains and displays all of the informations the following attributes:
task: a dictionary to hold the tasks details/information. It represents a task in the database and should be set like this:task_infos = { #remember in the last tutorial, we stored the username of #the connected user to make sure we keep the information #and interactions user specific "creator_name":connected_user, #the name of the task "name":task_name, #the task's decription "description": description, #the deadline of the task "deadline":deadline, #the level of priority "priority":priority, #the status of completion of the task "status":status, #the category to which the task belongs "category":category, #the date of creation "creation_date": datetime.now(), #a list that should contain all the dates at which the task got edited "edited":[] }The
categoryshould be selected among those values ("Work/Professional", "Personal", "Home", "Education/Learning", "Health/Fitness", "Social", "Travel", "Creativity/Projects", "Goals and Long-Term Plans") and you can use a global variable to manage the values if you want.The status should be selected among those values:
"to-do","in progress","completed".The priority should be selected among those:
"high", "medium", "low".
project_name: the task nameproject_description: the description of the taskdeadline: the project’s deadlinecategory: the category to which the task belongspriority: the level of priority of the taskstatus: the current status of completion of the task
Another consideration for your screen is that we will add three buttons to the screen: edit, back and delete. Now, let’s focus on the methods of the class:
set_task_data(self, task)This function assigns the values of the widgets to the corresponding values in the
taskdictionary.def set_task_data(self, task): self.task=task self.project_name.text = task['name'] self.project_description.text = task['description'] self.deadline.text = task['deadline'].strftime('%Y-%m-%d') self.category.text = task['category'] self.priority.text = task['priority'] self.status.text = task['status'] #I decided to write a message depending of if the task is late or not #but it is totally optional if task['deadline'] < gcommands.datetime.today() and task["status"].lower() != 'completed': self.late_task_message.text = "This task is late" self.late_task_message.color = (1, 0, 0, 1) # Red color for error else: self.late_task_message.text = "You still have some time" self.late_task_message.color = (0, 0, 0, 1)edit_task(self, instance)Since the screen has an Edit button, we need to transfer the task information from the current screen to the Edit screen. This way, the user can see all the pre-filled information and have a clearer view of what to edit or modify.
The edit screen class also has a
set_task_datamethod that works almost the same as the function we just wrote:def edit_task(self, instance): edit_task_screen = self.manager.get_screen('edit_task') edit_task_screen.set_task_data({ 'name': self.project_name.text, 'description': self.project_description.text, 'deadline': gcommands.datetime.strptime(self.deadline.text, '%Y-%m-%d'), # datetime.strptime(key_value["deadline"], '%Y-%m-%d %H:%M:%S'), 'category': self.category.text, 'priority': self.priority.text, 'status': self.status.text }) self.manager.current = 'edit_task'delete(self, instance)To delete a task with the user's confirmation, we start by creating a small popup that prompts the user to confirm their decision. The popup offers two options: "Yes" to proceed with the deletion, and "No" to cancel.
If the user selects "Yes," the popup triggers a callback function that performs the deletion once the popup is closed. This ensures that the deletion only takes place after the user's choice has been confirmed and the popup has been dismissed, adding a layer of intentionality and preventing accidental deletions.
Confirmation Popup
class ConfirmationPopup(Popup): def __init__(self, title="Are you sure?", message="Do you want to proceed?", **kwargs): super(ConfirmationPopup, self).__init__(**kwargs) self.title = title self.size_hint = (0.6, 0.4) self.decision = "" # Layout for the popup layout = BoxLayout(orientation='vertical', spacing=10, padding=10) # Message label layout.add_widget(Label(text=message, halign='center', valign='middle', size_hint_y=0.7)) # Buttons layout button_layout = BoxLayout(size_hint_y=0.3, spacing=20) # Yes button yes_button = Button(text="Yes", background_color=(0, 1, 0, 1)) yes_button.bind(on_press=self.on_confirm) # No button no_button = Button(text="No", background_color=(1, 0, 0, 1)) no_button.bind(on_press=self.on_dismiss) # Add buttons to button layout button_layout.add_widget(yes_button) button_layout.add_widget(no_button) # Add button layout to main layout layout.add_widget(button_layout) self.content = layout def on_confirm(self, instance): self.decision = 'Yes' self.dismiss() # Trigger on_dismiss and call the callback def on_dismiss(self, instance=None): #This checks if the decision attribute already has a value (Yes) if not hasattr(self, 'decision'): self.decision = 'No' super().on_dismiss() # Ensure the default dismiss behaviordeletemethoddef delete(self, instance): task_name = self.project_name.text # Define a callback that will be called when the popup is dismissed def on_popup_dismiss(decision): if decision == 'Yes': self.dbname = gcommands.task_deletion(task_name, self.dbname) self.go_back(instance) # Create the popup and set the callback for dismissal popup = ConfirmationPopup() popup.bind(on_dismiss=lambda *args: on_popup_dismiss(popup.decision)) popup.open()The
task_deletionmethodThis function uses the task name and the
connected_uservariable to find the task to delete among the tasks of the connected user. It then deletes the document using MongoDB'sdelete_onemethod.def task_deletion(task_name, dbname): tasks_collection = dbname["tasks"] if tasks_collection.find_one({"name":task_name,"creator_name":connected_user}): #on recherche maintenant la tâche en fonction de son nom et du nom du créateur de la tâche if tasks_collection.delete_one({"name":task_name,"creator_name":connected_user}): print("Task deleted\n") else: print("Operation cancelled.\n") return dbname
The
go_backfunctionThe go_back method simply goes back to the task list screen
def go_back(self, instance): # Navigate back to the task list self.manager.current = 'task_list'
🔄 A quick checkpoint and summary!
Here’s a breakdown of the key components and methods:
Task Data Representation
task dictionary holds all task details such as:
Task name, description, deadline, priority, status, category, and creation date.
A list of edited dates to track when the task has been updated.
Categories:
"Work/Professional","Personal", etc., and statuses:"to-do","in progress","completed".Priority levels:
"high","medium","low".Key Methods:
set_task_data(self, task)
Assigns the task details to corresponding widgets on the Task Details screen.
Also checks if the task is late and updates the message accordingly.
edit_task(self, instance)
- Pre-fills the Edit screen with the current task’s details for editing.
delete(self, instance)
Prompts a confirmation popup asking if the user is sure about deleting the task.
If confirmed, the task is deleted via the
task_deletionmethod.ConfirmationPopup Class
A popup that asks the user to confirm or cancel the deletion action. It contains two buttons: "Yes" for confirming and "No" for cancelling.
Uses
on_confirmandon_dismissmethods to manage the user’s decision.task_deletion(task_name, dbname)
- Searches for the task by name and creator (connected user) and deletes it from the database.
go_back(self, instance)
- Navigates back to the Task List screen after an action is completed (such as after editing or deleting a task).
Key relationships
The Task Details screen uses the
taskdictionary to hold task data.The
set_task_datamethod assigns task values to the UI widgets, including handling late tasks.The
edit_taskmethod allows the user to transfer the current task's data to the Edit screen.Deletion is handled through a confirmation popup, where the user’s decision triggers the
task_deletionmethod to remove the task from the database.
Task Creation and Edition
Screen base class
For both these screens, I have used the same canva. It is a simple form containing all the fields that should constitute a task variable. Let’s quickly go over the code.
class ScreenBase(Screen):
def create_base_layout(self):
# Left column (Project Name, Project Description, Status)
left_layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
left_layout.bind(minimum_height=left_layout.setter('height'))
# Project Name
left_layout.add_widget(Label(text="[b]Project Name[/b]", size_hint=(1, None), height=50,color=(0, 0, 0, 1),markup=True))
self.project_name_input = TextInput(size_hint=(1, None), height=50)
left_layout.add_widget(self.project_name_input)
# Project Description
left_layout.add_widget(Label(text="[b]Project description[/b]", size_hint=(1, None), height=50,color=(0, 0, 0, 1),markup=True))
self.project_description_input = TextInput(size_hint=(1, None), height=100)
left_layout.add_widget(self.project_description_input)
# Status
left_layout.add_widget(Label(text="[b]Status[/b]", size_hint=(1, None), height=50,color=(0, 0, 0, 1),markup=True))
#A Spinner is a UI element that allows users to select from a
# list of options by scrolling through them
self.status_input = Spinner(text="Select",values=("to-do", "in progress", "completed"),size_hint=(1, None),height=50)
left_layout.add_widget(self.status_input)
# Right column (Deadline, Category, Priority)
right_layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
right_layout.bind(minimum_height=right_layout.setter('height'))
# Deadline
right_layout.add_widget(Label(text="[b]Deadline[/b]", size_hint=(1, None), height=50,color=(0, 0, 0, 1),markup=True))
date_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=50)
# Add a label for the date on the left
self.deadline_input = Label(text="Select Date:", size_hint=(0.6, None), height=50,color=(0, 0, 0, 1)) # Adjust size_hint if needed
date_layout.add_widget(self.deadline_input)
date_picker_btn = Button(text="Open Calendar", size_hint=(0.4, None), height=50)
date_picker_btn.bind(on_release=self.open_calendar)
date_layout.add_widget(date_picker_btn)
right_layout.add_widget(date_layout)
# Category
right_layout.add_widget(Label(text="[b]Category[/b]", size_hint=(1, None), height=50,color=(0, 0, 0, 1),markup=True))
#self.category_input = TextInput(size_hint=(1, None), height=50)
self.category_input = Spinner(text="Select", values=("Work/Professional", "Personal", "Home", "Education/Learning", "Health/Fitness", "Social", "Travel", "Creativity/Projects", "Goals and Long-Term Plans"),
size_hint=(1, None),height=50,)
right_layout.add_widget(self.category_input)
# Priority
right_layout.add_widget(Label(text="[b]Priority[/b]", size_hint=(1, None), height=50,color=(0, 0, 0, 1),markup=True))
#self.priority_input = TextInput(size_hint=(1, None), height=50)
self.priority_input = Spinner(text="Select",values=("high", "medium", "low"),size_hint=(1, None),height=50)
right_layout.add_widget(self.priority_input)
return left_layout, right_layout
#this function creates a calendar for the user to choose from
def open_calendar(self, instance):
date_dialog = MDDatePicker() # Create the date picker
date_dialog.bind(on_save=self.set_date) # Bind the on_save event to set_date
date_dialog.open() # Open the date picker
#retrieves the value created by MDDatePicker at the time of the user's choice and assigns it to date_label.text
def set_date(self, instance, value, *args):
self.deadline_input.text = f"{value.strftime('%Y-%m-%d')}"
Layout initialisation: I divided the screen into two vertical sections. This gives us two columns, each as a GridLayout with one column, and they will be placed into a GridLayout with two columns.
You can pretty much design it as you want but as you can see my left layout contains the Project Name, the Project Description, and the Status and my right layout contains the Deadline, the Category, and the Priority.
The Spinner: To create the widgets representing the Status, the Category and the Status, I used a spinner object using the
Spinnerclass. In forms or applications, a spinner (often called a dropdown or combo box) presents a list of options from which users can select one. When a user clicks on the spinner, it expands to show the list of options, and the user can scroll through the items and select one.The DatePicker: If you have been following this series from the beginning, you probably remember me talking about using kivymd.app instead of just using kivy.app. That is to use the Date Picker feature of KivyMD. Now is the time to use it.
First, we create a date_picker button. When pressed, it will open a calendar to allow the user to pick a date.
#this will contain the value of the date chosen later self.deadline_input = Label(text="Select Date:", size_hint=(0.6, None), height=50,color=(0, 0, 0, 1)) # Adjust size_hint if needed date_layout.add_widget(self.deadline_input) #creation of our date_picker button date_picker_btn = Button(text="Open Calendar", size_hint=(0.4, None), height=50) date_picker_btn.bind(on_release=self.open_calendar)Overview of the open_calendar method:
Then, we write the function open_calendar.
In this function, we create a date picker object using the function MDDatePicker(). The date picker object naturally has a save button and we bind the
on_saveevent to toset_datefunction.def open_calendar(self, instance): date_dialog = MDDatePicker() # Create the date picker date_dialog.bind(on_save=self.set_date) # Bind the on_save event to set_date date_dialog.open() # Open the date pickerWhen a date is chosen, a variable containing the value chosen is created and stored in memory: the selected date (
value) is formatted toYYYY-MM-DDand set as the text forself.deadline_inputin theset_datefunction, updating the relevant field in your UI with the chosen date.Quick overview of the
set_datemethoddef set_date(self, instance, value, *args): self.deadline_input.text = f"{value.strftime('%Y-%m-%d')}"The
set_datefunction is a callback that handles the date selected by the user from theMDDatePickerwidget. Here’s a breakdown of its parameters:self: Refers to the instance of the class where this function is defined. It allows access to other class attributes and methods, such asself.deadline_input.instance: TheMDDatePickerinstance that called this callback. This parameter gives you access to the properties or state of the date picker if needed (although it’s not used in this function).value: The selected date, provided as adatetime.dateobject. This is the date chosen by the user in theMDDatePickerdialog. We’re formatting this value to the"YYYY-MM-DD"format and setting it as the text forself.deadline_input.*args: This is a flexible argument placeholder allowing additional positional arguments to be passed toset_date. It's common to include*argsin callback functions to ensure compatibility with additional arguments, but it’s not used here specifically.
Layout of the TaskEdition & TaskCreation Screen
As I stated before, we keep things simple here and adopt the same layout for both the classes. The code is self-explanatory but there are comments to help you understand:
#Let's say we are working on the TaskEditionScreen
#the class inherits from the ScreenBase class that we created earlier
class TaskEditionScreen(ScreenBase):
def __init__(self, dbname, **kwargs):
super(TaskEditionScreen, self).__init__(**kwargs)
# We create an edition layout for the screen
# There, we will place the fields to edit
edit_layout = GridLayout(cols=2, padding=10, spacing=10, size_hint=(1, 1))
self.dbname = dbname
#This variable is specific to the TaskEditionScreen
self.initial_task = ""
# We create the left and right layouts using the create_base_layout() method
#from the ScreenBase class
left_layout, right_layout = self.create_base_layout()
# Let's add those layouts to the main/upper layout
edit_layout.add_widget(left_layout)
edit_layout.add_widget(right_layout)
# Buttons layout (Edit, Delete, Back)
buttons_layout = BoxLayout(orientation='horizontal', size_hint=(1, None), height=70, spacing=20)
# Back Button (Placed on the left side, styled to match the others)
back_button = Button(text="Back", background_color=(0, 0.5, 1, 1), size_hint=(0.4, 1))
# Function to go back to the previous screen
back_button.bind(on_press=self.go_back)
buttons_layout.add_widget(back_button)
# We create Save Button
save_button = Button(text="Save", background_color=(0.035, 0.416, 0.035, 1), size_hint=(0.4, 1))
save_button.bind(on_press=self.save_task)
buttons_layout.add_widget(save_button)
# Add main and buttons layout to the screen
final_layout = BoxLayout(orientation='vertical', spacing=10)
# We Add buttons layout at the top for easier navigation
# We Add the task details below
final_layout.add_widget(edit_layout)
final_layout.add_widget(buttons_layout)
self.add_widget(final_layout)
Note:
I created a variable for the TaskEditionScreen named initial_task. Its purpose is to hold the task previous/initial name in case it is updated by the user. This way, it will be easy to find the task to edit even (and especially) if the name has been changed. You could do the same thing with an id field if you want.
Task Edition Class Methods
set_task_data(self, task)While writing the
TaskDetailsScreenclass, we wrote a method namededit_task.The purpose there was to ensure that the fields to edit when someone attempts to edit a task where pre-filled. In this method, we created an instance of the edit_screen using the Screen Manager like so:
edit_task_screen = self.manager.get_screen('edit_task')Using this instance, we called the
set_task_data(self, task)to set the data fields to what they should be.The task variable is supposed to be a dictionary containing all the viewed task informations, hence the code we wrote:
def edit_task(self, instance): edit_task_screen = self.manager.get_screen('edit_task') edit_task_screen.set_task_data({ 'name': self.project_name.text, 'description': self.project_description.text, 'deadline': gcommands.datetime.strptime(self.deadline.text, '%Y-%m-%d'), # datetime.strptime(key_value["deadline"], '%Y-%m-%d %H:%M:%S'), 'category': self.category.text, 'priority': self.priority.text, 'status': self.status.text }) self.manager.current = 'edit_task'Now that we are refreshed on the purpose of the
set_task_datamethod, let’s write it up:def set_task_data(self, task): # Thanks to this function, all fields are pre-filled # Those attributes are created in the ScreenBase class self.project_name_input.text = task['name'] # This is where we initialise the initial_task variable and give it # the potential previous value of the name self.initial_task = task["name"] self.project_description_input.text = task['description'] self.deadline_input.text = task['deadline'].strftime('%Y-%m-%d') self.category_input.text = task['category'] self.priority_input.text = task['priority'] self.status_input.text = task['status']save_task(self, instance)Here is the function:
# Reminder: gcommands is a file where all functions interacting with # MongDB are written. It is imported as a module def save_task(self, instance): #We create a new updated_task = { 'name': self.project_name_input.text, 'description': self.project_description_input.text, 'deadline': gcommands.datetime.strptime(self.deadline_input.text, '%Y-%m-%d'), 'category': self.category_input.text, 'priority': self.priority_input.text, 'status': self.status_input.text } # We call the database edition function here self.dbname = gcommands.task_edition(updated_task,self.dbname) self.go_back(instance)Now, let’s dive in:
First, we create a dictionary containing the fields (normally, some should be updated. Even if none are, all the fields were pre-filled ;) )
updated_task = { 'initial_name': self.initial_task, 'name': self.project_name_input.text, 'description': self.project_description_input.text, 'deadline': gcommands.datetime.strptime(self.deadline_input.text, '%Y-%m-%d'), 'category': self.category_input.text, 'priority': self.priority_input.text, 'status': self.status_input.text }Then, we update the task in the database. I do so using this function. Let’s follow along using the comments and the explanations after.
# updated_task is the dictionary representing the task def task_edition(updated_task, dbname): tasks_collection = dbname["tasks"] # Using the find_one method, we try to find a document where # the field name has the same value as initial_name task_to_edit = tasks_collection.find_one({"name":updated_task["initial_name"],"creator_name":connected_user}) # edition_list is a list keeping track of the dates where the task was edited # It is optional but still good. # Using the get method, we retrieve the field edited #[] (an empty list) is the default value returned if the "edited" key does not exist in the dictionary. edition_list = task_to_edit.get("edited", []) # This updates the list in place edition_list.append(datetime.now()) updated_task["edited"] = edition_list # We can delete the initial_task field since it is no longer needed del updated_task["initial_task"] # The set operation new_fields = { "$set": updated_task } edited = tasks_collection.update_one({"_id": task_to_edit["_id"]}, new_fields) # This condition verifies the task has been found if edited.matched_count > 0: # This condition verifies the task has been successfully modified if edited.modified_count > 0: print("Task updated.\n") else: print("No changes were made to the task.\n") return dbnameedition_listis a list keeping track of the dates where the task was edited. It is optional but still good.Using the
getmethod, we retrieve the fieldeditedfrom the dictionary.[](an empty list) is the default value returned if the "edited" key does not exist in the dictionary.To streamline the task update process, we can safely remove the
initial_taskfield as it is no longer necessary. Additionally, when passing the task dictionary directly as the variable for setting up the task, it's important to avoid potential errors.The
$setoperator is a MongoDB update operator used to update specific fields in a document. If the fields specified do not exist, MongoDB will create them. It ensures only the fields in theupdated_taskdictionary are modified, leaving other fields in the document unchanged.The update operation uses the
_idfield to identify the task. This is because the name or other attributes may have been changed during the editing process, and since we already retrieved the task to edit at the start, the only reliable way to access it now is by its ID since this is constant.
go_back(self, instance)This function just goes back to the TaskListScreen
def go_back(self, instance): # Navigate back to the task list self.manager.current = 'task_list'
Task Creation Class Methods
Before diving into the methods, let’s go over some particularity in the layout and the attributes.
I have added an error_message attribute to the class just in case a user creates an error by failing to fill the fields properly.
Since it is an error message, it only displays itself when there is an actual issue hence the function:
show_error(self, message)def show_error(self, message): self.error_message.text = messagego_back
def go_back(self, instance): # Navigate back to the task list self.manager.current = 'task_list'save_task(self, instance)Here is a view of the method:
# Reminder: gcommands is used as a module here import gcommands def save_task(self, instance): if not self.project_name_input.text.strip(): self.show_error("Project name is required.") return if not self.project_description_input.text.strip(): self.show_error("Project description is required.") return if self.deadline_input.text == "Select Date:": self.show_error("Please select a valid deadline.") return if self.category_input.text == "Select": self.show_error("Category is required.") return if self.priority_input.text == "Select": self.show_error("Priority is required.") return if self.status_input.text == "Select": self.show_error("Status is required.") return # If all fields are valid, create the task try: created_task = { 'name': self.project_name_input.text, 'description': self.project_description_input.text, 'deadline': gcommands.datetime.strptime(self.deadline_input.text, '%Y-%m-%d'), 'category': self.category_input.text, 'priority': self.priority_input.text, 'status': self.status_input.text } self.dbname = gcommands.task_creation(created_task, self.dbname) self.go_back(instance) except ValueError: self.show_error("Invalid deadline format. Please select a date.")We begin by verifying if the user has properly filled all the fields.
The actions here ensure that the user will only see a message displaying in case of error; the application won’t break.
As a reminder, the deadline field has been handled using a DatePicker and the
deadline_input_textis set using theset_datemethod in the ScreenBase class. Read above for further details.We are using
try/exceptto handle potential errors related to date formatting (which is crucial for preventing crashes). This is a good practice, as it ensures that even if the user enters an invalid date, the app won't break.The task creation process in MongoDB is very simple. We just use the
insert_onemethod:def task_creation(task, dbname): task_infos = { "creator_name": connected_user, "name": task["name"], "description": task["description"], "deadline": task["deadline"], "priority": task["priority"], "status": task["status"], "category": task["category"], "creation_date": datetime.now(), "edited":[] } tasks_collection = dbname["tasks"] if tasks_collection.insert_one(task_infos): print("Task saved!\n") else: print("Operation cancelled!\n") return dbname
And that’s it!
We went over a lot today! I hope it wasn’t too difficult to keep up with and that I have been able to help you in some way.
In the next article, we will go over the filter and search features!



