Skip to main content

Command Palette

Search for a command to run...

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

Updated
25 min read
Building a Smart To-Do List Manager with Python, MongoDB & Kivy (Part 3: Main Screen & Task Management Screens)
M

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.

  1. 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 = dbname
    
  2. We 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])
    
  3. 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)
    
  4. 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 class TaskTab, 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 TabbedPanel in 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.

    1. 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)
      
    2. 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 Scrollview object 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_tasks
        

        Your TaskTab display method 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)
        
    3. The TaskFrame class

      The 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_detail method of the TaskListScreen.

          view_button.bind(on_press=partial(self.task_list_screen.open_task_details, task))
        
      • The open_task_detail method opens a screen displaying all the selected tasks details.
        I wrote this function in the TaskListScreen class 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 the TaskListScreen class). 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_details method in TaskListScreen takes a task parameter, which is a dictionary holding all the details about the selected task. When called, open_task_details passes this data to the TaskDetailScreen class via set_task_data, so the appropriate values can be assigned to the widgets in the detail view.

        1. How task Flows Through Classes:

          • display_tasks in TaskTab retrieves tasks from the database, each represented as a dictionary (task).

          • For each task, display_tasks creates a TaskFrame instance, passing task and a reference to TaskListScreen.

          • Inside TaskFrame, the 'view' button is linked to open_task_details. When clicked, it provides the task, allowing the details view to be filled in accurately.

        2. Responsibilities Recap:

          • TaskListScreen: Manages screen navigation and the open_task_details method.

          • TaskTab: Retrieves tasks by category and organises them in frames.

          • TaskFrame: Represents each task visually and calls open_task_details with the appropriate task data when needed.

  5. 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:

  1. Class Setup: We created a TaskListScreen class inheriting from Screen, which structures the main screen into two sections in a vertical BoxLayout: the "header" (logo, date, search, and filter options) and the "tasks" section (using a TabbedPanel for task states: All, Pending, and Completed).

  2. Task Tab Creation: A custom TaskTab class (inheriting from TabbedPanelItem) 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).

  3. Database Interaction: A get_tasks function was defined for fetching tasks from MongoDB, filtering based on the selected task category.

  4. Task Display: Within TaskTab, the display_tasks method fetches tasks and clears/repopulates the layout with task information. Each task is displayed in a TaskFrame, which includes the title, status, and deadline, and a "view" button to open detailed task information.

  5. TaskFrame Setup: This class (inheriting BoxLayout) is used to present individual task info within a flexible frame, allowing for customisation.

  6. Task Detail Navigation: The open_task_details method in TaskListScreen enables transitioning to a detail screen with in-depth task information when the "view" button is clicked.

  7. 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 category should 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 name

  • project_description: the description of the task

  • deadline: the project’s deadline

  • category: the category to which the task belongs

  • priority: the level of priority of the task

  • status: 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:

  1. set_task_data(self, task)

    This function assigns the values of the widgets to the corresponding values in the task dictionary.

     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)
    
  2. 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_data method 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'
    
  3. 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 behavior
      
    • delete method

        def 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_deletion method

      This function uses the task name and the connected_user variable to find the task to delete among the tasks of the connected user. It then deletes the document using MongoDB's delete_one method.

        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
      
  4. The go_back function

    The 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:

  1. 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.

  2. edit_task(self, instance)

    • Pre-fills the Edit screen with the current task’s details for editing.
  3. 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_deletion method.

  4. 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_confirm and on_dismiss methods to manage the user’s decision.

  5. task_deletion(task_name, dbname)

    • Searches for the task by name and creator (connected user) and deletes it from the database.
  6. 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

  1. The Task Details screen uses the task dictionary to hold task data.

  2. The set_task_data method assigns task values to the UI widgets, including handling late tasks.

  3. The edit_task method allows the user to transfer the current task's data to the Edit screen.

  4. Deletion is handled through a confirmation popup, where the user’s decision triggers the task_deletion method 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 Spinner class. 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_save event to to set_date function.

      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
    

    When a date is chosen, a variable containing the value chosen is created and stored in memory: the selected date (value) is formatted to YYYY-MM-DD and set as the text for self.deadline_input in the set_date function, updating the relevant field in your UI with the chosen date.

  • Quick overview of the set_date method

      def set_date(self, instance, value, *args):
          self.deadline_input.text = f"{value.strftime('%Y-%m-%d')}"
    

    The set_date function is a callback that handles the date selected by the user from the MDDatePicker widget. 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 as self.deadline_input.

    • instance: The MDDatePicker instance 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 a datetime.date object. This is the date chosen by the user in the MDDatePicker dialog. We’re formatting this value to the "YYYY-MM-DD" format and setting it as the text for self.deadline_input.

    • *args: This is a flexible argument placeholder allowing additional positional arguments to be passed to set_date. It's common to include *args in 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

  1. set_task_data(self, task)

    While writing the TaskDetailsScreen class, we wrote a method named edit_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_data method, 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']
    
  2. 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 dbname
      
      • 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 from 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_task field 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 $set operator 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 the updated_task dictionary are modified, leaving other fields in the document unchanged.

      • The update operation uses the _id field 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.

  3. 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:

  1. show_error(self, message)

     def show_error(self, message):
         self.error_message.text = message
    
  2. go_back

     def go_back(self, instance):
         # Navigate back to the task list
         self.manager.current = 'task_list'
    
  3. 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_text is set using the set_date method in the ScreenBase class. Read above for further details.

    • We are using try/except to 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_one method:

        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!