Building a Smart To-Do List Manager with Python, MongoDB & Kivy (Part 4: Filter & Search)
Implement search and filter capabilities in your Kivy application.
Hello everybody and welcome (back) to my blog!
This is the part 4 (and the last article) of our series about building a smart To-Do list Manager with Python, MongoDB and Kivy! xD
The search & filter features are crucial functionalities of an application and today, we are going to look at how it could be integrated into our application.
This article could serve as a base even for someone that has not been building this application along since the first article. You just need to grasp the notions and make them your own.
Without further ado, let’s get into it.
Search Feature
1- The search related variables
In a previous article, while building the main screen, we created a research bar as a
TextInput
variable and a simple string variable to hold the value typed by the user.self.search_query = "" self.search_input = TextInput(hint_text="Search tasks", size_hint_x=0.5, multiline=False)
2- The binding operation
The
TextInput
variable has been bound to a function that will ensure to display the tasks according to what the user has written in the research bar. The bind operation will use thetext
property, ensuring that the research function is triggered whenever the text inside the input changes.self.search_input.bind(text=self.on_search_text)
When the bind operation is performed, Kivy automatically monitors the
text
property ofself.search_input
and stores the text entered by the user in a variable that we will callvalue
. In our implementation, we use thisvalue
to update ourself.search
_query
variable and refresh the display for each task tab. Each time the user types or edits the text, theon_search_text
function is called. This function receives two parameters:instance
: TheTextInput
widget that triggered the event.value
: The current text entered by the user.
def on_search_text(self, instance, value):
# Trigger a task refresh for each tab
self.search_query = value
self.all_tab.filter_tasks()
self.pending_tab.filter_tasks()
self.completed_tab.filter_tasks()
3- The logic behind the search operation?
As mentioned in earlier articles, the task tabs are represented by the
TaskTab
class. Each tab displays tasks of a specific category (e.g., all tasks, pending tasks, completed tasks). Thefilter_tasks
method in theTaskTab
class is responsible for filtering and displaying tasks based on the search query.The
TaskTab
class is initialised with an instance ofTaskListScreen
, which represents the main screen. This allows theTaskTab
class to access thesearch_query
stored inTaskListScreen
.Here’s how everything fits together:
The user types in the search bar (
TextInput
).The
text
property changes, triggeringon_search_text
.on_search_text
updatessearch_query
and callsfilter_tasks
for each tab.Each tab filters tasks using the search query and updates its display.
Here’s how the filtering logic works:
def filter_tasks(self):
# Filter tasks based on the search query
filtered_tasks = [task for task in self.tasks if
self.task_list_screen.search_query.lower() in task['name'].lower() or
self.task_list_screen.search_query.lower() in task['description'].lower()]
self.task_list_layout.clear_widgets()
# Add tasks to the layout
for task in filtered_tasks:
task_frame = TaskFrame(task, self.task_list_screen)
self.task_list_layout.add_widget(task_frame)
For reference (and context), the
TaskTab
class is created with an instance of theTaskListScreen
(which is the class representing the main screen).self.task
is a dictionary that contains the tasks retrieved from the database.We iterate over
self.tasks
and we filter out the task by simply keeping those whose name or description contains the searched word.After clearing out the layout, we create the tasks objects to display using the TaskFrame class.
While our example filters tasks based on the task's name and description, you can customize the filtering logic to suit your needs. For instance, you could filter based on additional fields or use advanced search criteria.
Additionally, the task display uses the
TaskFrame
class to create the visual representation of each task. For more details on this class, refer to Part 2 of the tutorial.
You should be able to perform a search!
Now, something a little bit more complex: the filter feature.
Filter Feature
The FilterPopup
class is a user interface component designed to facilitate task filtering based on multiple criteria such as date, status, category, and priority. It is built using the Popup
widget from Kivy, providing a modal window where users can refine their task searches. This class ensures seamless interaction between the popup and the main task management screen by leveraging a callback function to pass filtered data.
Let’s begin by defining the layout and the basic functioning of the items displayed on the screen.
Filter Popup Layout
1.Initialisation
The constructor accepts two main parameters:
on_filter_applied
: A callback function provided by the main screen to handle and display the filtered data.dbname
: A reference to the database for tasks.
The popup is titled "Filter Tasks" and uses a size hint of 80% of the screen for width and height.
A dictionary,
selected_filters
, stores the user's selected criteria, initialised with default values.
class FilterPopup(Popup):
def __init__(self, on_filter_applied, dbname, **kwargs):
super(FilterPopup, self).__init__(**kwargs)
#self.task_list_screen = task_list_screen
# on_filter_applied is actually a Callback function to handle filtered data and display them
self.on_filter_applied = on_filter_applied
self.dbname = dbname
self.title = 'Filter Tasks'
self.size_hint = (0.8, 0.8)
self.selected_filters = {
'date': None,
'status': "",
'category': "",
'priority': ""
}
2. Layout Design
The popup uses a structured layout to present filtering options clearly:
Date Filter:
Includes a label and a button to open a calendar for date selection.
The selected date is displayed on the label once chosen.
Status Filter:
- A
Spinner
widget allows the user to choose between statuses such as "to-do," "in progress," and "completed."
- A
Category Filter:
- Another
Spinner
provides categories like "Work/Professional," "Personal," and "Health/Fitness."
- Another
Priority Filter:
- A final
Spinner
offers priority levels like "high," "medium," and "low."
- A final
Filter Button:
- A button triggers the filtering process and invokes the callback function.
The layout ensures a clean, user-friendly interface with proper spacing and alignment.
# Create the form layout
form_layout = GridLayout(cols=2, padding=10, spacing=10)
# Date Filter
date_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=50)
# Add a label for the date on the left
self.date_label = Label(text="Select Date:", size_hint=(0.4, None), height=50) # Adjust size_hint if needed
date_layout.add_widget(self.date_label)
date_picker_btn = Button(text="Open Calendar", size_hint=(0.6, None), height=50)
date_picker_btn.bind(on_release=self.open_calendar)
date_layout.add_widget(date_picker_btn)
form_layout.add_widget(date_layout)
# Status Filter
status_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=50)
status_layout.add_widget(Label(text="Status:",size_hint=(0.4, None), height=50))
self.status_spinner = Spinner(
text="All",
values=("all", "to-do", "in progress", "completed"),
size_hint=(0.6, None),
height=50
)
status_layout.add_widget(self.status_spinner)
form_layout.add_widget(status_layout)
# Category Filter
category_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=50)
category_layout.add_widget(Label(text="Category:",size_hint=(0.4, None), height=50))
#form_layout.add_widget(Label(text="Category:",size_hint_x=0.3))
self.category_spinner = Spinner(
text="All",
values=("All", "Work/Professional", "Personal", "Home", "Education/Learning", "Health/Fitness", "Social", "Travel", "Creativity/Projects", "Goals and Long-Term Plans"),
size_hint=(0.6, None),
height=50
)
category_layout.add_widget(self.category_spinner)
form_layout.add_widget(category_layout)
# Priority Filter
priority_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=50)
priority_layout.add_widget(Label(text="Priority:",size_hint=(0.4, None), height=50))
self.priority_spinner = Spinner(
text="All",
values=("All", "high", "medium", "low"),
size_hint=(0.6, None),
height=50
)
priority_layout.add_widget(self.priority_spinner)
form_layout.add_widget(priority_layout)
3. Calendar Integration
The open_calendar
method displays a date picker (MDDatePicker
) for selecting a specific date. Once the user selects a date, the set_date
method updates the corresponding label with the chosen value in the format YYYY-MM-DD
.
def open_calendar(self, instance):
date_dialog = MDDatePicker()
date_dialog.bind(on_save=self.set_date)
date_dialog.open()
def set_date(self, instance, value, *args):
self.date_label.text = f"{value.strftime('%Y-%m-%d')}"
Filtering Logic
When the user clicks the "Apply Filter" button, the apply_filter
method is triggered:
It gathers the selected filters from the form.
Passes the data to the
on_filter_applied
callback for processing and display.Closes the popup.
Focus on Callback functions
Callback functions can seem tricky at first, but they’re a powerful concept in programming. By the end of this section, you’ll understand what they are, how they work, and how we use them in the Filter Popup Logic.
What are Callback functions?
A callback function is a function that is passed as an argument to another function. The receiving function can then call (or "callback") the function at the right time.
Callbacks are especially useful when you need to:
Perform an action after something else is completed.
Decouple logic (separate responsibilities in code).
Make code reusable and flexible.
Basic Example of a callback function
Here’s a simple example to illustrate:
def greet(name):
print(f"Hello, {name}!")
def run_callback(callback_function, value):
# The callback function is called here
callback_function(value)
# Call run_callback and pass 'greet' as the callback
run_callback(greet, "Prunella")
What happens:
run_callback
accepts a function (callback_function
) and a value (value
).Inside
run_callback
, it callsgreet("Prunella")
.Output:
Hello, Prunella!
Real-World Use Case: The Filter Popup
In our Filter Popup, we use a callback function to apply a filter and display the filtered results. Let’s break it down step by step.
1. Why Do We Need a Callback?
The FilterPopup
class handles the user’s input (e.g., filter criteria). However, the actual task filtering logic belongs to the TaskListScreen
class. Instead of tightly coupling the two classes, we pass a callback function from TaskListScreen
to FilterPopup
.
This way:
The
FilterPopup
doesn’t need to know anything about the task filtering logic.The
TaskListScreen
can handle the filtered results directly.
2. How It Works
Here’s how the callback logic connects the Filter Popup and TaskListScreen:
Step1: Create the Callback in TaskListScreen
class
In the TaskListScreen
class, define the callback function that processes filtered data:
def display_filtered_data(self, filtered_data):
# This function is called when the FilterPopup applies filters
self.all_tab.filter_feature(filtered_data)
self.pending_tab.filter_feature(filtered_data)
self.completed_tab.filter_feature(filtered_data)
This function:
Accepts
filtered_data
as a parameter (passed from the popup).Updates the display for all tabs.
The filter_feature method is a method in TaskTab class which like the filter_tasks
will display the tasks according to the filters imposed by the user.
This function should look like this:
def filter_feature(self, filtered_data):
#filtered_data is a dictionary that represents the task that the user will see
#according to the filters applied
filtered_tasks = [task for task in self.tasks if task in filtered_data]
self.task_list_layout.clear_widgets()
# Add tasks to the layout
for task in filtered_tasks:
task_frame = TaskFrame(task, self.task_list_screen)
self.task_list_layout.add_widget(task_frame)
Step 2: Pass the Callback to FilterPopup
When creating the FilterPopup
instance in TaskListScreen
class, pass display_filtered_data
as the callback:
def show_filter_popup(self):
popup = FilterPopup(self.display_filtered_data, self.dbname)
popup.open()
Here, self.display_filtered_data
is passed to FilterPopup
.
Also, this function is triggered when the user clicks on the filter button on the main screen, as written in the code below:
filter_button = Button(text="Filter Tasks", size_hint_x=0.2, height=50)
filter_button.bind(on_press=self.show_filter_popup)
#don't forget to actually add the button to your screen layout
Step 3: Trigger the Callback in FilterPopup
Inside the FilterPopup
, a button is created for the user to apply the chosen filters:
# Add Filter Button
filter_btn = Button(text="Apply Filter", size_hint=(1, None), background_color=(0, 0, 1, 1), height=50)
filter_btn.bind(on_press=self.apply_filter)
As stated above, a callback function provided by the main screen to handle and display the filtered data.We call the callback when the user applies the filters using the apply_filter
method :
#Note: gcommands is just a file imported and used as a module
def apply_filter(self, instance):
# We retrieve the values from the form
try:
self.selected_filters['date'] = gcommands.datetime.strptime(self.date_label.text, '%Y-%m-%d')
except:
self.selected_filters['date'] = None
self.selected_filters['status'] = self.status_spinner.text
self.selected_filters['category'] = self.category_spinner.text
self.selected_filters['priority'] = self.priority_spinner.text
#We retrieve the data from the database
filtered_data = gcommands.filter_func(self.selected_filters['date'], self.selected_filters['status'], self.selected_filters['category'], self.selected_filters['priority'], self.dbname)
# Trigger the callback with the selected filter values to display them
self.on_filter_applied(filtered_data)
# Dismiss the popup
self.dismiss()
First, we have to retrieve the values selected by the user in the form and pass them down to our variable
selected_filters
:#Note: gcommands is just a file imported and used as a module try: self.selected_filters['date'] = gcommands.datetime.strptime(self.date_label.text, '%Y-%m-%d') except: self.selected_filters['date'] = None self.selected_filters['status'] = self.status_spinner.text self.selected_filters['category'] = self.category_spinner.text self.selected_filters['priority'] = self.priority_spinner.text
Then, using
selected_filters
, we call the database management functionfilter_func
to get the right tasks to display. Let’s go through this function using the comments in the code (it will be less confusing I think ;) ).#those are the variables selected by the user and an instance of the database def filter_func(selected_date, selected_status, selected_category, selected_priority, dbname): #Let's get the right collection to work with in the database tasks_collection = dbname["tasks"] #We create a dictionary to keep the conditions aka the values to look for in the collection query_conditions = dict() #Connected_user is used as a session variable to identify the user query_conditions["creator_name"] = connected_user #If the user selected a date, # it filters documents where the deadline field falls within a specific day # (starting at selected_date and ending just before the next day). if selected_date != None: query_conditions["deadline"] = { "$gte": selected_date, "$lt": selected_date + timedelta(days=1) - timedelta(microseconds=1) } #We check if the other fields/filter values are not defaut (All) before #adding them to query_conditions if selected_status.lower() != "all": query_conditions["status"] = selected_status if selected_priority.lower() != "all": query_conditions["priority"] = selected_priority if selected_category.lower() != "all": query_conditions["category"] = selected_category #We verify that the conditions are met if query_conditions: # Execute the query documents = list(tasks_collection.find(query_conditions, {"_id": 0})) #print(documents) return documents
Finally, we can trigger the callback function (
on_filter_applied
) with the values retrieved the usingfilter_func
method
3. Callback Flow Visualisation
Just to make sure you understood, let’s recap the callback flow.
The user interacts with the popup and selects filters.
The
TaskListScreen
creates an instance ofFilterPopup
, passing its own filtering method as theon_filter_applied
callback.The user opens the popup, selects filter criteria, and clicks "Apply Filter."
The popup calls
apply_filter
, which processes the filters and generatesfiltered_data
The popup gathers the criteria, calls the callback, and sends the filtered data back to the main screen: it triggers the callback (
self.on_filter_applied
), passingfiltered_data
back toTaskListScreen
.The main screen (
TaskListScreen
) refreshes the task list to display only the tasks matching the selected filters.
Aaaaaaand, we are all set! Your task management app is now not only functional but also user-friendly and dynamic. The filter & search features ensure that users can efficiently find and manage their tasks based on specific needs.
Conclusion
Over the course of this series, we’ve journeyed through building a feature-rich task management application. From designing the main screen and handling database operations to implementing advanced features like filtering and searching, we've covered a comprehensive set of tools and techniques. While this series concludes here, the journey doesn’t have to stop. You could add notifications, task-sharing capabilities, or even integrate the app with external calendars like Google Calendar. These additions would make your app even more robust and versatile.
I want to thank you for joining me in this adventure.I hope this series has been as rewarding for you to follow as it was for me to create. Your dedication to building this app speaks volumes about your commitment to learning and growth.
You can find the complete code for this project here. I would love to hear your feedbacks and see how you’ve expanded on the project!
To really conclude, let’s all remember that every great app starts with a single line of code. Let’s keep experimenting, building, and most importantly, keep learning. Cheers to more successful projects!🥂🥂