Cuddley

project / August 24th, 2021

Cuddley Main Image

Cuddley LinkGitHub

Background

Cuddley is a web app that allows registered users to create, edit, and view their own collection of to-do lists. Tasks can be added to each list with a description and deadline and be edited, deleted, or marked as incomplete/complete at any time. The account username and the number of lists, tasks, and tasks completed will also be displayed above the collection of lists.

Essentially, this is a much-improved version of my original To-Do List project. Unlike the original project, Cuddley possesses user login and authentication features, allows for the creation of multiple lists, has editing and deleting features, and is a multi-page app that is made entirely in Python.

This is a professional portfolio project that was created after completing the 100 Days of Code Python Bootcamp.

Purpose

After completing the 100 Days of Code Python Bootcamp, this project was offered as a suggested project idea to apply my knowledge of web development with the Flask and SQLAlchemy Iibraries from the course to build full-stack web applications. Learning to use Bootstrap components to create my own pages for this project was another valuable experience for my web development skills. Just like other professional portfolio projects, developing skills in planning and executing a project from scratch without guidance was the most valuable lesson.

Additionally, I also wanted to build an improved version of my original, single-page To-do List app. By implementing user login, authentication, and more command options, Cuddley is a significantly more viable and usable app compared to the original.

Concepts and Tools Used

  • Python

  • Flask (with Login, Bootstrap, and WTForm extensions)

  • Jinja

  • SQLAlchemy

  • Bootstrap

  • HTML

  • CSS

  • Heroku

  • DB Browser

  • Werkzeug

Process

At the beginning of this project, I was on the fence between choosing to create a simple, single-page to-do list app or a complex, larger project that is slightly similar to Trello that would incorporate multiple lists and possess editing and deleting features. Because I had already completed a simple To-do List app in a previous bootcamp, I ultimately decided to create a complex app and gave myself one week to try and complete it.

Define Project Requirements

The planning stages of this project involved deciding which pages, database classes, and app functionalities I would need. For the pages, I decided that a home page, sign-up and login pages, a dashboard page, pages to add/edit a list, and pages to add/edit a task would be needed for a total of six page templates for eight different pages. The home page will list the features of Cuddley and encourage the user to sign up. The sign-up and login pages will use quick forms from the Flask-WTF extension to allow the user to enter their information in the form fields. Once logged in, all requests to the home route will redirect to the dashboard route and render the dashboard page. The dashboard will display the account username, their collection of lists and tasks, and buttons to add, edit, and delete lists and tasks. Finally, the pages for adding or editing lists and tasks will contain a Flask-WTF quick form. If an add command is clicked, the form fields will be blank and the header will contain the word "Add". If an edit command is clicked, the header will contain the word "Edit" and the form fields will be populated with information from the list or task.

Additionally, header and footer templates will be created to store the navigation bar, custom CSS styling, links for the Font Awesome and Bootstrap content delivery networks, and the rest of the HTML boilerplate code. These templates are imported to the other pages to provide the same structure and styling for each of them. If a user is signed in, the navigation bar will contain links to log out or go to the dashboard. If a user is not signed in, there will be links to sign up or login instead.

In terms of database data, I will have three classes: User, List, and Task. The User class will contain a username, email, and password. The List class will contain the list name. The Task class will contain the task name, task description, deadline, completion status, and the date the task was created. The Cuddley database will contain three tables, one for each of these classes. There will also be three relationships that link the tables: User-to-List, List-to-Task, and User-to-Task. Each of these relationships will be one-to-many, meaning that each parent will have many children. Each User will have a collection of lists and tasks and each List will have a collection of tasks. To facilitate this, an extra column will be added in each child table and will store the IDs from each parent in order to link the tables. So the List class will also contain the ID of the User that created the list and the Task class will also contain the ID of the User that created the task and the ID of its parent list.

Before starting work on the front-end, a Flask app is created in the main Python script and routes are set up to run empty placeholder functions (for now) with a pass statement. The following twelve routes are created: / (for home), /sign-up, /login, /new-list, /update-list, /delete-list, /new-task, /update-task, /delete-task, /update-task-progress, /dashboard, and /logout.

Creating the Front-end

Building the front-end starts with creating two folders: static and templates. An empty stylesheet for the custom CSS styles I will soon implement is created inside static. In templates, I start by creating the header and footer templates, since they will used in every other page template. For the header, the Bootstrap CSS is loaded by pasting their CDN link in the header. The Font Awesome CDN link is also pasted in order to make their icons accessible for use throughout the website. Links for the Google Fonts I will use and the custom stylesheet from static are also included. The header is completed by using the fixed Navbar Bootstrap component, which means a navigation bar will appear in every page that contains the header and will always be fixed to the top. The footer simply contains my name and the current year in a container that is fixed to the bottom, as well as scripts for jQuery, Popper.js, and Bootstrap JavaScript animations.

The first page template I worked on was the home page. In between the imported header and footer, the slogan and basic features of Cuddley are displayed. There is also a Sign Up button that will take the user to the sign up page. For sign up and login pages, a container with header text is inserted between the header and footer. Because these pages first require a FlaskForm to be created in the back-end before it can be inserted below the header text, I move on to working on the dashboard page for the time being. Since I cannot dynamically render the username, lists, and tasks from a database yet, the focus during this step was to create a hard-coded example of how I want a list and its tasks to look like. Once I am satisfied with the look of one list and the tasks inside it, I copy the code and paste it multiple times inside a Bootstrap grid to ensure that the page will be responsive when the screen size changes. Here, I am also experimenting with the list and task sizes to see if I want a fixed list size (which would contain a scroll) or an adjustable list size (each list would be displayed as different sizes). Ultimately, I chose a fixed size for each list because I thought it would look more organized across the page. Below is a screenshot from my experimenting, with the first list in the top left being my chosen list template:

Cuddley Dashboard Page - In Progress

Next, a page template for adding or editing lists is created. A header (which will be dynamic and show either "Add" or "Edit" depending on which command was clicked) is placed inside a container. Just like the sign up and login pages, a FlaskForm needs to be created in the back-end before inserting it into the page. The same approach is used for the page template for adding or editing tasks.

As I complete the initial makeup of each page, the Flask routes in the main Python script are updated to render them. As I view and test each page on my browser, I continually experiment and write additional CSS code to customize the appearance of each component the way I see fit. Whenever possible, the Bootstrap grid system is used to make each page responsive for all viewport sizes.

Creating the Back-End: Create Database and Establish Relationships

To store all the Flask-WTF forms that will be used in the sign up, login, add/edit list, and add/edit task pages, a separate file called forms.py is created. The Flask-WTF extension is installed and imported to forms.py and four different forms are created. The sign up form will contain fields for the username, email, password, and confirmed password. The login form will contain fields for the email and password. The list form will simply contain a field for the list name. Finally, the task form will contain fields for the task name, task description, deadline, and a choice of which parent list the user wants to add the task to. For the deadline field, I experimented with DateFields or DateTimeFields, but found it to be too restrictive since the user always had to enter the date and time in the exact required format for it to be submitted. Ultimately, I chose to make it a StringField to give the user maximum flexibility (eg. they can write 'Soon' or 'TBD' if they don't have a deadline in mind). With the forms created, they are imported to main.py and rendered in their respective HTML pages using Jinja tags.

Back in the main.py script, Flask-SQLAlchemy is installed and imported in order to map database records as objects. Tables can also be defined as separate classes and fields act as object properties, thanks to SQLAlchemy. A new database called cuddley.db is created and connected to the Flask app. User, List, and Task classes are created by inheriting from the database Model class and the fields that were discussed in the planning stage are created inside each class. The relationship method is used inside each class to create the one-to-many parent and child relationships that each class has, and each child will have a foreign key field(s) inserted inside their table that links them to their parent(s).

Completing the Back-End: Routes, CRUD Operations, and Authentication

In main.py, the Flask app is configured to use Flask-Login by creating a login manager using the Login Manager class. This contains code that allows my app and Flask-Login to work together to load users from IDs, send logged in users to a specific route, etc. By default, Flask-Login uses sessions for authentication, which stores user-specific info for the logged-in user. A secret key is set on the Flask app in order for sessions to be used. Next, a user_loader function is written to reload the User object from the user ID stored in the current session. To make implementing a User class easier, the User class will also inherit from the UserMixin class from Flask-Login. This provides default Flask-Login user implementations for the User class and Flask-Login methods like is_authenticated, which can be used to check if a user is logged in or not.

For all routes, the current_user object is always passed to every page template that is rendered. Since each page template has the header template imported to it, and the navigation bar in the header template will render different buttons depending on whether the user is logged in or not, using the is_authenticated method from current_user will assist in deciding which buttons are rendered in the navigation bar at any given time. The current year from the datetime module is also passed to every page template, in order to render the current year in the footer. Routes that require a user to be logged in and authenticated to access the route are secured with the @login_required decorator. The button destinations inside each template are also updated during this time to ensure they access the correct routes.

For the root route '/' (home route), if the current user is already logged in and authenticated, they will be redirected to the dashboard route. Otherwise, they will see the home page and be encouraged to sign up.

Cuddley Home Page - User not logged in

For the /sign-up route, a GET request will render the sign up form.

Cuddley Sign Up Page

When the form's submit button is clicked and a POST request is made, the Users table is queried to check if the submitted email already exists in the database. If the email doesn't exist in the database, the submitted username, email, and password is used to create a new user that is added to the users table in the database. The method login_user is then used to log in the newly created user and redirect them to the dashboard. If the submitted email already exists in the database, the user will be redirected to the login page and a red flash message will appear to inform the user that the email is already registered and to log in instead.

Cuddley Login Page - Redirected from Sign Up Page

A GET request to the login page will render the login form. If a POST request is made to /login, the users table is queried to see if the submitted email exists in the database. If a user is found, the check_password_hash method from the Werkzeug library is used to compare the entered password to the hash inside the database to determine if login_user is used. If the passwords match, login_user is called to sign in the user and they are redirected to the dashboard. If the passwords don't match, the user is redirected back to the login page and a flash message appears to inform the user that an incorrect password was entered. If the submitted email is not found in the users table, the user is redirected back to the login page and a flash message will inform the user that the submitted email does not exist in their database.

For all renders and redirects to /dashboard, the username of the current user is always passed to the page template to be displayed at the top of the dashboard. In /dashboard, the ID of the current user is used to query all tasks and lists that possess a creator ID of the same value. A list of lists and a list of tasks from these queries are passed to the template and FOR loops are used in the dashboard template to render each list and their tasks. The list lengths for both lists also accessed to display the number of lists, tasks, and tasks completed for the user.

Cuddley Empty Dashboard

Since the user has not added any lists, the dashboard is currently empty. Clicking the "Add New List" button will make a GET request to the /new-list route and render the new list form.

Cuddley New Page Form

When a POST request is made to /new-list, the lists table is queried to see if the submitted list name already exists for the current user ID. If it does, the user is redirected back to the new list page and a flash message appears to inform them that they already have a list with the same name. If the list name doesn't exist for the current user ID, the submitted list name and current user ID are used to form a new list. After the new list is added to the lists table, a new default task with a parent list ID equal to the ID of the newly created list is also added. The default task is added to the tasks table and the user is redirected to the dashboard with the new list and default task displayed.

Cuddley Dashboard - List Added

Clicking the edit task button will redirect the user to the /update-task route and pass the ID for the task to be edited. A GET request will render the new task form (which is adjusted to show "Edit Task" as the header), and the task ID is used to query the tasks table for the task to edit. The data from the queried task is used to populate the fields of the task form. If a POST request is made to /update-task, the data in the form fields will replace the field values for that task in the database and the user is redirected to the dashboard.

Cuddley Edit Task PageCuddley Dashboard - Task Edited

Adding a task is similar to editing a task. A GET request to the /new-task route will render the new task form (which is adjusted to show "Add Task" as the header). The ID of the task's designated parent list is also passed to the route and is used to ensure the new task will be added to the list it should belong to. A POST request will use the form field data to create a new task and add it to the database. The user is redirected to the dashboard after the commit is made.

Cuddley Add Task PageCuddley Dashboard - Task Added

Before the next example, two more tasks are added to the Work list and another list called Chores with two tasks is created.

Cuddley Dashboard - Two Lists

Using the edit list button to edit the name of the Chores list follows an extremely similar process to the add list button. The only difference is that the list ID is passed to the /update-list route and is used to populate the list name field with the queried name of the list to be edited. If the current user already has a list with a name that matches the submitted list name, the user will be redirected back to the /update-list route and a flash message is used to inform the user to enter a different list name.

Cuddley Edit List PageCuddley Dashboard - Edited List

Clicking the checkmark inside a task container will make a GET request and pass the task ID to the /update-task-progress route. The task's progress field will be changed from 0 to 1 and the user is redirected back to the dashboard. The task is marked as complete and will have a strikethrough line through the task name and the checkmark button will contain a circle instead.

Cuddley Dashboard - Task Completed

Clicking the circle will make the same GET request to /update-task-progress and change the task progress value from 1 to 0 to mark the task as incomplete. The strikethrough line is removed from the task name and the circle button is changed back to a checkmark.

Clicking the delete button for a task will pass the task ID to the /delete-task route. The selected task is queried using the ID and deleted from the database. The user is redirected to the dashboard afterwards.

Cuddley Dashboard - Task Deleted

Clicking the delete list button will pass the list ID to the /delete-list route. The selected list is queried using the list ID. Before deleting the list, all the tasks inside the list should be deleted first. The current user ID is used to query all the tasks that belong to the user. A FOR loop is then used to loop through each task and if the task's parent list ID matches the ID of the selected list for deletion, the task is deleted and committed. After the FOR loop is completed and all the tasks inside the selected list have been deleted, the selected list itself is finally deleted and the user is redirected to the dashboard.

Cuddley Dashboard - List Deleted

The final route to complete is the /logout route, which simply uses the logout_user method to sign the user out of their current session. The user is redirected back to the home page.

Finishing touches are done by adding a favicon to the header template and a media query breakpoint is added in my custom stylesheet to further improve the appearance of the dashboard layout on a smaller screen.

For deployment, the same steps to deploy the Coffee Connect app on Heroku are followed.

Outcome and Takeaways

Creating Cuddley was the hardest and most complex project I have done so far. Although I wasn't sure if I would be able to complete this project, I now feel the reward was definitely worth the risk of investing a week of time into building this project. Using Bootstrap components to build my pages from scratch and incorporating user login and authentication were the two most valuable experiences from this project. Establishing relationships between the database tables was another useful challenge that required me to put more thought behind my database classes and structure choices. Overall, this was an excellent project that really tested my Python skills and my confidence rose upon completion.

Possible Improvements Going Forward

Given more time, there are a variety of tiny details that can be improved upon. This includes displaying the number of tasks on each list and adding extra fields for the most recent date/time that a list or task was modified and displaying them. It would also be helpful to find a way to implement a calendar date picker and a time selector for the task deadline field, so the user can simply choose the date and time instead of having to type it which can be highly restrictive and error-prone. Having a proper date/time type for the deadline field would have less flexibility than the current string type, but would also be beneficial in that the tasks can be sorted by date. Another potential idea from having a proper date/time field for the deadline is that the deadline could be displayed in different colours depending on how close the current date/time is to the deadline (eg. Yellow if it's currently the week or day of the deadline, red if the deadline has passed). The benefits and drawbacks of date/time fields compared to string fields would have to be further explored before fully committing to a change.

Another idea that would require further research is implementing a many-to-many relationship between User and List, which would allow multiple users to access and make changes to a list. However, this would open up another long set of brand new challenges and would require a massive time commitment. More interfaces, pages, and database changes would be required for a user to interact with and be aware of other users as they use Cuddley. Usernames would have to be unique in this scenario and more features to uniquely identify a user (eg. pictures, profile pages, etc.) would have to be added.

It would also be helpful if the list names, task names, descriptions, and deadlines can be edited on the spot (in the dashboard) instead of taking the user to a separate page, possibly by using React.


External Links