Skip to main content

Creating the Project from Scratch

Create project directory

Create a directory for your project, call it Devii_Python_Tutorial; if you are using VS Code open a new window (Cntl+Shift+N) then open a terminal (Cntl+`). If you are not using VS Code, on Windows, you can use Command Prompt or PowerShell, while on Linux or macOS, you can use the terminal, then navigate to where you would like to create the project directory

cd path/to/parent/directory

Create a new directory for your project:

mkdir project_name

Navigate to the directory you just created:

cd project_name

To open this directory in VS Code, use:

code -r .

Create Python Virtual Environment

Next, create a python virtual environment, this will isolate project dependencies and avoid conflicts with other projects.

MacOS and Linux

Run the following command to create a virtual environment named "venv":

python3 -m venv venv

Run this command to activate your venv:

source venv/bin/activate

Windows

Run the following command to create a virtual environment named "venv":

python -m venv venv

Run this command to activate your venv:

venv\Scripts\activate

Create Requirements File

A requirements file will be used to install all the dependencies (modules or packages) for this project. If using VS Code, open the Explorer on the left side of the screen and create a new file using the new file icon and name the file 'requirements.txt'

VS Code New File

inside this file add:

flask
requests
jinja2

Run the command to install dependencies:

pip install -r requirements.txt

Create Python Files

auth.py file

Create 'auth.py' to handle Devii authorization. This file manages the retrieval of an access token:

"""This module is for handling authentication and retrieving access and refresh tokens from Devii."""

import requests

AUTH_URL = "https://api.devii.io/auth"

Creates a JSON file for storing and loading tokens. Storing the tokens in a JSON file without encryption is done only as an example, and is not best practice: other options include using an encrypted JSON file, in-memory keystore, browser local storage (for browser apps), an encrypted SQLite database file, and so on.

TOKEN_FILE = 'token.json'

def load_token():
'''Load the tokens from the token.json file'''
if os.path.exists(TOKEN_FILE):
with open(TOKEN_FILE, 'r') as file:
return json.load(file)
else:
return None

def save_token(token):
'''Save the tokens to the token.json file'''
with open(TOKEN_FILE, 'w') as file:
json.dump(token, file)

Next, create a function to login and retrieve your access token from Devii. For this function you will need to have your tenant id, if you need to find it you can find the instructions in Project and Tenant ID.

def login():
"""Retrieve tokens for the application"""

# Create a dictionary to store the form data
data = {
"login": "<your Devii user name>",
"password": "<your Devii Root role password>",
"tenantid": "<your tenant ID>",
}

# Make the POST request to the Devii authentication endpoint with the provided data.
response = requests.post(AUTH_URL, data=data)

# Check for a successful response, if status code is 200 parse the JSON response
if response.status_code == 200:
json_response = response.json()

# Extract the access token
access_token = json_response.get("access_token")
refresh_token = json_response.get("refresh_token")
roleid = json_response.get("roleid")
# Save the refresh token to a file
save_token({"access_token": access_token, "refresh_token": refresh_token, "roleid": roleid})

else:
print("error in login: ", response.status_code)
print(response.text)

The following code provides a mechanism for managing authentication tokens. The refresh_access_token function refreshes the access token using a refresh token, ensuring continued access to resources. If the token file does not exist, it initiates a login process. The is_token_expired function checks whether a token has expired by decoding it and comparing the expiration time with the current time. The ensure_token_exists function ensures valid tokens are present, refreshing or renewing them as necessary. If either the access or refresh token is expired or missing, it either refreshes the tokens or logs in again to obtain new ones.

def refresh_access_token():
"""Refresh the access token using the refresh token"""

#ensure the token file exists
if not os.path.exists(TOKEN_FILE):
print("Token file not found, logging in...")
login()
return

#load the refresh token from the token file
refresh_token = load_token().get('refresh_token')

#header to send to the Devii authentication endpoint for new tokens using the refresh token
refresh_headers = {
"Authorization": f"Bearer {refresh_token}",
"Content-Type": "application/json",
}

# Make the POST request to the Devii authentication endpoint with the provided data.
response = requests.get(AUTH_URL, headers=refresh_headers)

# Check for a successful response, if status code is 200 parse the JSON response and extract tokens then save them to the token file
if response.status_code == 200:
json_response = response.json()
access_token = json_response.get("access_token")
refresh_token = json_response.get("refresh_token")
roleid = json_response.get("roleid")
save_token({"access_token": access_token, "refresh_token": refresh_token, "roleid": roleid})
return access_token

else:
print("error in refresh access token: ", response.status_code)
print(response.text)



def is_token_expired(token):
'''Check if refresh token has expired'''

# Decode the token without verifying the signature
decoded_token = jwt.decode(token, options={"verify_signature": False})
expiration_timestamp = decoded_token['exp']

# Extract expiration time from decoded token
if expiration_timestamp:
expiration_datetime = datetime.fromtimestamp(expiration_timestamp, tz=timezone.utc)
print("Expiration Time:", expiration_datetime)
return expiration_datetime < datetime.now(timezone.utc)
return False

def ensure_token_exists():
'''checking to see if the token exists'''

if not os.path.exists(TOKEN_FILE):
print("Token file not found, logging in...")
login()
return

access_token = load_token().get('access_token')
refresh_token = load_token().get('refresh_token')

if access_token is None or is_token_expired(access_token):
if refresh_token is None or is_token_expired(refresh_token):
print("Refresh token expired or not found, logging in...")
login()
else:
print("Access token expired, refreshing token...")
refresh_access_token()
ensure_token_exists()

Load the refresh token from the token.json file to be used in the headers.

access_token = load_token().get('access_token')

Creates a dictionary headers that includes the obtained access token in the "Authorization" header. The "Content-Type" header is also set to indicate that the content being sent is in JSON format. This will be used in subsequent HTTP requests to Devii API endpoints, ensuring that the requests are authenticated with the obtained access token.

headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}

graphql_helper.py file

Now, create 'graphql_helper.py' to manage GraphQL queries and mutations:

"""Helper functions for GraphQL queries and mutations"""
import requests
import auth

NOTE: the imported auth is the auth.py created in the previous step.

Define Devii query endpoint as a constant. NOTE: this is the same endpoint for mutations.

QUERY_URL = "https://api.devii.io/query"

Implement a general helper function for executing GraphQL queries and mutations. It takes a GraphQL query, and optionally, variables to be used in the query.

def execute_graphql_query(query, variables=None):
"""helper function for all queries and mutations"""
# This will load the query or mutation and variable, if any, into the GraphQL query or mutation
payload = {"query": query, "variables": variables}

# the query will always recieve a return response of data in the same shape as the query
response = requests.post(QUERY_URL, headers=auth.headers, json=payload)

# the response is returned in json form
return response.json()

Add two functions, one to retrieve all the list and item data and the other to retrieve status data from our PostgreSQL database via Devii API using a GraphQL query

def get_list_data():
# query that will be sent to Devii to retrieve all the data from the list and item tables
list_name_query = """
{
list {
listid
listname
statusid
item_collection {
itemid
itemname
statusid
}
}
}
"""
# creates the payload that will be used by Devii to return the data
list_name_payload = {"query": list_name_query, "variables": {}}

# sends the query payload and authorization token to devii
list_name_response = requests.post(
QUERY_URL, headers=auth.headers, json=list_name_payload
)

# returns the response from GraphQL in a json nested dictionary, it retrieves the values from the keys, data and list
return list_name_response.json()["data"]["list"]


def get_status_name():
query_status_name="""
{
status_value{
statusid
statusname
}
}
"""

# creates the payload that will be used by Devii to return the data
status_name_payload = {"query": query_status_name, "variables": {}}

# sends the query payload and authorization token to devii
status_name_response = requests.post(
QUERY_URL, headers=auth.headers, json=status_name_payload
)

# returns the response from GraphQL in a json nested dictionary, it retrieves the values from the keys, data and status
return status_name_response.json()["data"]["status_value"]

Next, several mutation functions need to be created: add, edit and delete for both list(s) and item(s), each corresponding to a specific GraphQL mutation. These functions encapsulate the logic needed to interact with the Devii GraphQL API, making it easier to perform common operations in a structured and reusable manner. They utilize the requests library to send HTTP requests and the execute_graphql_query function to handle the specifics of sending GraphQL queries and mutations.

def add_item(item_name, list_id, status):
# to add an item to the item table with and a listid FK
add_item_mutation = """
mutation ($i: itemInput){
create_item(input: $i){
itemid
itemname
status_value {
statusname
}
list {
listname
}
}
}
"""
# the variables will be retrieved from a form the user will submit
variables = {"i": {"itemname": item_name, "listid": int(list_id), "statusid": int(status_id)}}

# the GraphQL mutation run by the helper function
return execute_graphql_query(add_item_mutation, variables)


# Each one of the following add functions has the same format as the add_item

def add_list(listname, status):
add_list_mutation = """
mutation($i:listInput){
create_list(input:$i){
listid
listname
status_value{
statusid
statusname
}
}
}
"""
variables = {"i": {"listname": listname, "statusid":int(status_id)}}
return execute_graphql_query(add_list_mutation, variables)

# Editing items requires identifying the Primary Key of the item you want to edit/change
# In this case the PK is the itemid that will be the varible $j
# The varible $i will be the changes to the item

def edit_item(itemid, new_name, list_id, status):
edit_item_mutation = """
mutation ($i: itemInput, $j: ID!) {
update_item(input: $i, itemid: $j) {
itemid
itemname
status_value{
statusid
statusname
}
list {
listid
listname
}
}
}
"""

variables = {
"j": itemid, # the Primary key for items
"i": {"itemname": new_name, "listid": int(list_id), "statusid": int(status_id)},
}
return execute_graphql_query(edit_item_mutation, variables)

# Each one of the following edit functions has the same format as the edit_item

def edit_list(listid, new_list_name, status):
edit_list_mutation = """
mutation($i:listInput, $j:ID!){
update_list(input:$i, listid: $j){
listid
listname
status_value{
statusname
statusid
}
}
}
"""

variables = {"j": int(listid), "i": {"listname": new_list_name, "statusid": int(status_id)}}
return execute_graphql_query(edit_list_mutation, variables)


# Deleting objects will only require the Primary Key of the object to be deleted

def delete_item(itemid):
delete_item_mutation = """
mutation($i:ID!){
delete_item(itemid:$i){
itemid
itemname
}
}
"""
variables = {"i": itemid}
return execute_graphql_query(delete_item_mutation, variables)

# Each one of the following delete functions has the same format as the delete_item

def delete_list(listid):
delete_list_mutation = """
mutation($i:ID!){
delete_list(listid:$i){
listid
listname
}
}
"""
variables = {"i": listid}
return execute_graphql_query(delete_list_mutation, variables)

app.py

Create a new file called app.py, this will be a flask application.

If you would like more information about Flask please refer to documentation at Flask.

Add the following packages:

from flask import Flask, render_template, request, redirect, url_for
import graphql_helper
import json
app = Flask(__name__)


@app.route("/")
#The index page acting as the home page for the app
def index():
#this will get all the data from your database via Devii
list_data = graphql_helper.get_list_data()
status_data = graphql_helper.get_status_name()

# sorts list by list id then item id so it will render the same way
list_data.sort(key=lambda x: x["listid"])
for item in list_data:
item["item_collection"].sort(key=lambda x: x["itemid"])

# this returns the index.html template with the list data
return render_template("index.html", list_data=list_data, status_data=status_data)


@app.route("/add_item", methods=["POST"])
# this will be the Add Item route
def add_item():
# items will be added via a form made in the index.html
item_name = request.form["itemname"]
list_id = request.form["listid"]
status_id = request.form["statusid"]

# The response will add the item to your database and add the list_id and status_id as the FK for that item
response = graphql_helper.add_item(item_name, list_id, status_id)

# each GraphQL query or mutation will send a nested json response back, the first key
# will be "data", if the response detects that key it will redirect to the index page and
# refresh the list_data
if response.get("data"):
return redirect(url_for("index"))
else:
return "Error adding item."


@app.route("/add_list", methods=["POST"])
def add_list():
list_name = request.form["listname"]
status_id = request.form["statusid"]

response = graphql_helper.add_list(list_name, status_id)

if response.get("data"):
return redirect(url_for("index"))
else:
return "Error adding catagory."


@app.route("/delete_item", methods=["POST"])
def delete_item():
itemid = request.form["itemid"]

response = graphql_helper.delete_item(itemid)

if response.get("data"):
return redirect(url_for("index"))
else:
return "Error deleting item."


@app.route("/edit_item", methods=["POST"])
def edit_item(): # rename edit_item
item_id = request.form["itemid"]
item_name = request.form["itemname"]
list_id = request.form["listid"]
status_id = request.form["statusid"]

response = graphql_helper.edit_item(item_id, item_name, list_id, status_id)

if response.get("data"):
return redirect(url_for("index"))
else:
return "Error editing item."


@app.route("/edit_list", methods=["POST"])
def edit_list():
listid = request.form["listid"]
new_list_name = request.form["listname"]
statusid = request.form["statusid"]

response = graphql_helper.edit_list(listid, new_list_name, statusid)

if response.get("data"):
return redirect(url_for("index"))
else:
return "Error editing catagory."


@app.route("/delete_list", methods=["POST"])
def delete_list():
list_id = request.form["listid"]

response = graphql_helper.delete_list(list_id)

if response.get("data"):
return redirect(url_for("index"))
else:
return "Error editing catagory."

@app.route("/get_status", methods=["GET", "POST"])
def get_status():
status_data = graphql_helper.get_status_name()

return status_data


if __name__ == "__main__":
app.run(debug=True)

Create Template and Static Files

Create a folder called 'templates', then a file named index.html inside the template folder. Create another folder named 'static', then create 3 files: css_reset.css, script.js and style.css inside the static folder. Your file structure should look like this:

  |--templates
| |--index.html
|--static
| |--css_reset.css
| |--script.js
| |--style.css

Template file

Open the index.html file. You will need to have a HTML preamble; if you are using VS Code you can type '!' and hit enter and it will be automatically added for you, else copy the code below:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>

</body>
</html>

Change the title to ToDo App, this will be shown on your browser's title bar.

After the </title> and up to the </head> we will need to add a few statements:

    <script type="text/javascript"
src="{{ url_for('static', filename='script.js') }}">
</script>
<script type="text/javascript">
let listData = JSON.parse('{{ list_data | tojson | safe }}');
let statusData = JSON.parse('{{ status_data | tojson | safe }}');
</script>
<link rel="stylesheet" type="text/css" href="../static/css_reset.css" />
<link rel="stylesheet" type="text/css" href="../static/style.css" />

The rest of the code will be between <body> </body>.

    <h1>ToDo App</h1></br>
<!-- Button to trigger the add item modal -->
<div class="add-item-button">
<button id="openAddItemModalBtn" class="openAddItemModalBtn btn">Add Item</button>
</div>
<!-- Add New Item Modal -->
<div id="addItemModal" class="modal">
<div class="modal-content">
<a href="#close" class="close">&times;</a>
<h2 class="modal-heading">Add New Item</h2>
<form action="/add_item" method="post">
<label for="itemname">Item Name:</label>
<input type="text" id="itemname" name="itemname" required /><br />
<label for="listid">Select List:</label>
<select id="listid" name="listid" required>
{% for list_item in list_data %}
<option value="{{ list_item.listid }}">
{{ list_item.listname }}
</option>
{% endfor %}</select><br />
<label for="statusid">Select Status:</label>
<select id="statusid" name="statusid" required>
{% for status_item in status_data %}
<option value="{{status_item.statusid}}">
{{ status_item.statusname}}
</option>
{%endfor %}
</select></br>
<input type="submit" class="btn" value="Save Item" />
<button type="button" class="cancel-btn btn">Cancel</button>
</form>
</div>
</div>

<!-- Button to trigger add new list modal -->
<div class="add-list-button">
<button id="openNewListModalBtn" class="openAddListModalBtn btn">Add List</button>
</div>
<!-- Add New List Modal -->
<div id="newListModal" class="modal">
<div class="modal-content">
<a href="#close" class="close">&times;</a>
<h2 class="modal-heading">Add New List</h2>
<form action="/add_list" method="post">
<label for="listname">List Name:</label>
<input type="text" id="listname" name="listname" required /><br />
<label for="statusid">Select Status:</label>
<select id="statusid" name="statusid" required>
{% for item in status_data %}
<option value="{{item.statusid}}">
{{ item.statusname}}
</option>
{%endfor %}
</select></br>
<input type="submit" class="btn" value="Save List" />
<button type="button" class="cancel-btn btn">Cancel</button>
</form>
</div>
</div>
<div class="lists-wrapper">
{% for item in list_data %}
<div class="list-container">
<ul class="items-collection-header">
<li class="list-header">
<div class="list-id"> {{ item.listid }} </div>
<div class="list-name"> {{ item.listname }} </div>
<div class="list-status">
{% for status_item in status_data %}
{% if status_item.statusid|int == item.statusid|int %}
{{ status_item.statusname }}
{% endif %}
{% endfor %}
</div>
<div class="list-buttons">
<div class="list-button">
<button class="openEditListModalBtn btn"
data-listid="{{ item.listid }}"
data-listname="{{ item.listname }}"
data-statusid="{{ item.statusid }}" >Edit List</button>
</div>
<div class="list-button">
<form action="/delete_list" method="post">
<input type="hidden" name="listid" value="{{ item.listid }}">
<button type="submit" class="list-delete-btn btn">Delete List</button>
</form>
</div>
</div>
</li>
</ul>
<ul class="items-collection">
{% for list_item in item.item_collection %}
<li class="item">
<div class="item-id">{{ list_item.itemid }}</div>
<div class="item-name">{{ list_item.itemname }}</div>
<div class="item-status">
{% for status_item in status_data %}
{% if status_item.statusid|int == list_item.statusid|int %}
{{ status_item.statusname }}
{% endif %}
{% endfor %}
</div>
<div class="item-buttons">
<div class="item-button">
<button class="openEditItemModalBtn btn"
data-itemid="{{ list_item.itemid }}"
data-itemname="{{ list_item.itemname }}"
data-listid="{{ item.listid }}"
data-listname="{{ item.listname }}"
data-itemstatusid="{{ list_item.statusid }}"
data-statusname="{{ item.statusname }}">Edit Item</button>
</div>
<div class="item-button">
<form action="/delete_item" method="post">
<input type="hidden" name="itemid" value="{{ list_item.itemid }}">
<button type="submit" class="item-delete-btn btn">Delete Item</button>
</form>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>

Static Files

css_reset.css

This code removes all of the default styling of your browser's CSS engine, so you can start fresh making your own. If you would like more information, please visit the website in the code.

/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/

html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6,
p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn,
em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var,
b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas,
details, embed, figure, figcaption, footer, header, hgroup, menu, nav,
output, ruby, section, summary, time, mark, audio, video
{
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, footer, header, hgroup,
menu, nav, section
{
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

style.css

This file stores all of the CSS styles applied to the app interface.

body {
font-family: Arial, Helvetica, sans-serif;
background-color: #daebf2;
}

h1 {
text-align: center;
font-size: 40px;
font-weight: bold;
}

.add-item-button,
.add-list-button {
display: flex;
flex-direction: row;
justify-content: center;
}

/* Modal styles */

.modal-heading {
font-size: 30px;
font-weight: bold;
}

.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
}

.modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
text-align: center;
}

/* Close button styles */
.close {
position: absolute;
top: 10px;
right: 20px;
font-size: 24px;
text-decoration: none;
color: #000;
}

/* List styles */

.list-container {
display: flex;
flex-direction: column;
background: #63adca;
max-width: 560px;
margin: 20px auto;
}

.list-header {
border-bottom: 1px solid black;
font-weight: bold;
}

.list-header,
.item {
display: flex;
flex-direction: row;
justify-content: space-between;
}

.list-id,
.item-id {
width: 60px;
border-right: 1px solid black;
}

.list-name,
.item-name,
.item-status,
.list-status {
width: calc(100% - 260px);
}

.list-buttons,
.item-buttons {
display: flex;
flex-direction: row;
width: 200px;
border-left: 1px solid black;
}

.btn {
background: #327893;
border: none;
color: white;
padding: 5px 10px;
text-align: center;
font-size: 14px;
margin: 2px;
cursor: pointer;
border-radius: 8px;
}

script.js

Our last file will be the JavaScript file.

document.addEventListener("DOMContentLoaded", () => {
const openAddItemModalBtn = document.getElementById("openAddItemModalBtn");
const openNewListModalBtn = document.getElementById("openNewListModalBtn");
const openEditItemModalBtns = document.querySelectorAll(".openEditItemModalBtn");
const openEditListModalBtns = document.querySelectorAll(".openEditListModalBtn");
const addItemModal = document.getElementById("addItemModal");
const newListModal = document.getElementById("newListModal");
const closeModalBtns = document.querySelectorAll(".modal .close");
const cancelBtns = document.querySelectorAll(".modal .cancel-btn");
const itemDeleteButton = document.querySelectorAll(".item-delete-btn");
const listDeleteButton = document.querySelectorAll(".list-delete-btn");
const introspectionBtn = document.getElementById("introspectionBtn");

openAddItemModalBtn.addEventListener("click", () => {
addItemModal.style.display = "block";
});

openNewListModalBtn.addEventListener("click", () => {
newListModal.style.display = "block";
});

openEditItemModalBtns.forEach((btn) => {
btn.addEventListener("click", () => {
let itemId = btn.getAttribute("data-itemid");
let itemName = btn.getAttribute("data-itemname");
let currentListId = btn.getAttribute("data-listid");
let currentStatusId = btn.getAttribute("data-itemstatusid");

let listDropdown = '<select id="editListId" name="listid" required>'; //resets list
let statusDropdown = '<select id="editStatusID" name="statusid" required>';


listData.forEach( (list_item) => {
listDropdown += '<option value="' + list_item.listid + '"';
if (list_item.listid === currentListId) {
listDropdown += " selected";
}
listDropdown += ">" + list_item.listname + "</option>";
});

listDropdown += "</select>";



statusData.forEach( ( status_item) => {
statusDropdown += '<option value="' + status_item.statusid + '"';
if (status_item.statusid === currentStatusId) {
statusDropdown += " selected";
}
statusDropdown += ">" + status_item.statusname + "</option>"
});

statusDropdown += "</select>";

let editItemModalHtml = `
<div id="editItemModal-${itemId}" class="method modal">
<div class="modal-content">
<a href="#close" class="close">&times;</a>
<h2 class="modal-heading">Edit Item</h2>
<form action="/edit_item" method="post">
<input type="hidden" name="itemid" value="${itemId}">
<label for="itemname">New Item Name:</label>
<input type="text" id="itemname" name="itemname" value="${itemName}" required /><br />
<label for="listid">Select List:</label>
${listDropdown}<br />
<label for="statusid">Select Status:</label>
${statusDropdown}<br />
<input type="submit" class="btn" value="Save Changes" />
<button type="button" class="cancel-btn btn">Cancel</button>
</form>
</div>
</div>
`;

let editItemModalContainer = document.createElement("div");
editItemModalContainer.innerHTML = editItemModalHtml;
document.body.appendChild(editItemModalContainer);

// Get the dynamically created edit item modal
let editItemModal = document.getElementById("editItemModal-" + itemId);

// Close modal functionality
let closeModalBtn = editItemModal.querySelector(".close");
let cancelBtn = editItemModal.querySelector(".cancel-btn");

closeModalBtn.addEventListener("click", (event) => {
event.preventDefault();
editItemModal.style.display = "none";
editItemModal.remove(); // Remove the modal from the DOM
});

cancelBtn.addEventListener("click", (event) => {
event.preventDefault();
editItemModal.style.display = "none";
editItemModal.remove(); // Remove the modal from the DOM
});

// Display the dynamically created edit item modal
editItemModal.style.display = "block";
});
});

openEditListModalBtns.forEach((btn) => {
btn.addEventListener("click", () => {
let listId = btn.getAttribute("data-listid");
let listName = btn.getAttribute("data-listname");
let currentStatusId = btn.getAttribute("data-statusid");

let statusDropdown = '<select id="editStatusID" name="statusid" required>';

statusData.forEach( (status_item) => {
statusDropdown += '<option value="' + status_item.statusid + '"';
if (status_item.statusid === currentStatusId) {
statusDropdown += " selected";
}
statusDropdown += ">" + status_item.statusname + "</option>"
});

statusDropdown += "</select>";

let editListModalHTML = `
<div id="editListModal-${listId}" class="method modal">
<div class="modal-content">
<a href="#close" class="close">&times;</a>
<h2 class="modal-heading">Edit List</h2>
<form action="/edit_list" method="post">
<input type="hidden" name="listid" value="${listId}">
<label for="listname">New List Name:</label>
<input type="text" id="listname" name="listname" value="${listName}" required /><br />
<!-- Add hidden input for listId and updated listName -->
<input type="hidden" name="original_listid" value="${listId}">
<input type="hidden" name="updated_listname" value="${listName}">
<label for="statusid">Select Status:</label>
${statusDropdown}<br />
<input type="submit" class="btn" value="Save Changes" />
<button type="button" class="cancel-btn btn">Cancel</button>
</form>
</div>
</div>
`;

let listEditModalContainer = document.createElement("div");
listEditModalContainer.innerHTML = editListModalHTML;
document.body.appendChild(listEditModalContainer);

let editListModal = document.getElementById("editListModal-" + listId);

let listNameInput = editListModal.querySelector("#listname");
listNameInput.value = listName;

let closeModalBtn = editListModal.querySelector(".close");
let cancelBtn = editListModal.querySelector(".cancel-btn");

closeModalBtn.addEventListener("click", (event) => {
event.preventDefault();
editListModal.style.display = "none";
editListModal.remove(); // Remove the modal from the DOM
});

cancelBtn.addEventListener("click", (event) => {
event.preventDefault();
editListModal.style.display = "none";
editListModal.remove(); // Remove the modal from the DOM
});

editListModal.style.display = "block";
});
});

closeModalBtns.forEach( (btn) => {
btn.addEventListener("click", (event) => {
event.preventDefault(); // Prevent the default anchor behavior
btn.closest(".modal").style.display = "none";
});
});

cancelBtns.forEach((btn) => {
btn.addEventListener("click", () => {
btn.closest(".modal").style.display = "none";
});
});

itemDeleteButton.forEach( (button) => {
button.addEventListener("click", (event) => {
event.preventDefault(); // Prevent the form submission

let confirmDelete = confirm("Are you sure you want to delete this item?");
if (confirmDelete) {
// If user confirms, submit the form
button.closest("form").submit();
}
});
});

listDeleteButton.forEach((button) => {
button.addEventListener("click", (event) => {
event.preventDefault(); // Prevent the form submission

let confirmDelete = confirm(
"Are you sure you want to delete this list? \n All items associated with this list will also be deleted!"
);
if (confirmDelete) {
// If user confirms, submit the form
button.closest("form").submit();
}
});
});
});

Finally, we can run the project.

For windows:

python app.py 

For Mac/Linux:

flask run

The app should be running on your local server http://127.0.0.1:5000

If you close your code base you will need to reactivate your virtual environment.