def is_valid_url(url):
# Given a value for the parameter 'url'
# Check if it is a valid URL
# Return True if it is a valid URL
# Return False if it is not a valid URL
In the previous post of the data driven web applications basics, we built a basic Flask application that enabled users to register their favorite websites by inputting a URL and receive a confirmation that the URL had been successfully registered. While this was a useful starting point, the application had a significant limitation: it did not store the registered URLs. As a result, the inputted information was not preserved and could not be accessed or used later. Without a mechanism to save this data, the application lacked practical utility—after all, registering a URL is pointless if it can’t be retrieved and used in the future.
Without a system to store the registered URLs, we cannot, for instance, track which URLs users have submitted, determine which sites are most popular, or analyze user preferences. Therefore, to make our application more useful, we need a way to store information persistently. This requires a reliable storage solution that allows data to be saved, retrieved, and managed efficiently.
Initially, using Excel might seem like a convenient solution—especially for small-scale applications or for users familiar with spreadsheet software. For example, when a user registers a URL, the application could write the URL to a new row in an Excel file. However, this method has significant limitations. Excel is not designed to handle large volumes of data or support simultaneous access and modifications by multiple users. In addition, as the number of records grows, performance can degrade significantly, and Excel files have size limitations that can lead to corruption and potential data loss.
To address these limitations, we turn to databases, which are purpose-built for efficient data management. Databases offer faster read and write operations, support concurrent access by multiple users, and ensure data integrity.
Among the various types of databases, relational databases are the most common due to their intuitive data model. They use a well-defined schema organized in a tabular format with rows and columns, simplifying data organization, querying, and management. Relational databases also support the creation of multiple tables and the establishment of relationships between them. This ability to link data across tables is why they are called “relational.”
Relational databases are typically accessed and manipulated using SQL (Structured Query Language), a standardized language designed specifically for interacting with relational data. As a result, relational databases are often referred to as SQL databases.
To overcome the limitation of data persistence, we will enhance our Flask application by integrating a relational database to store the URLs submitted by users. This means that each time a user registers a URL, it will be saved in an SQL database, ensuring the data is preserved, as illustrated in Figure 1.
Integrating a relational database into our application will allow us to leverage the data submitted by users. With this addition, we will use the stored data to identify and showcase the top favorite websites, which we will also refer to as the most popular websites. To achieve this, we will add a new page to the application that retrieves and counts the different registered URLs, displaying the ones registered the most times. This process is schematically illustrated in Figure 2.
Moreover, since our application will now feature multiple pages, and as you may have already spot in the previous Figure, we will add a navigation bar to facilitate easy navigation between pages.
In summary, building on the previous post, we will add three additional features to our web application:
Incorporation of a database to store the URLs registered by our users.
A new page displaying the top favorite websites.
A navigation bar to facilitate easy navigation between pages.
Ensuring data validity and security
Given that we will be storing user-generated data, it is crucial to ensure that the data is both valid and secure before it is saved. This means that users should not be able to save arbitrary or incorrect data in our database. Ensuring data validity involves checking that the data conforms to the desired format and requirements. For example, if users are supposed to enter a URL, we need to validate that the input is indeed in a valid URL format before accepting it.
In the previous example, we did not implement any format validation for user input. While our goal was to have users submit URLs, there was no enforcement to ensure that users actually entered a proper URL. As a result, users could input any value or format they wished. For instance, if a user typed “hello” and clicked the “Register URL” button, the application would erroneously confirm that the URL was registered, as illustrated in Figure 3.
Creating a function to validate URLs
To address this issue, we need to ensure that only valid URLs are registered. We can achieve this by creating a function that verifies whether the user input is a valid URL. This function will return True
if the input is a valid URL and False
otherwise. We could name this function is_valid_url()
, and its behavior can be summarized as follows:
There are several ways to implement this function, including pattern matching or even pinging the website to verify its existence. For our purpose, we will use pattern matching with regular expressions to determine if the input is a valid URL.
Validating the URL using regex
To check if the input value is a valid URL, we can use regular expressions (regex). Typically, URLs start with http:// or https://, followed by www.. However, it’s also common for URLs to omit the http:// or https://, beginning instead with just www., or even without the www. prefix. We can specify all these variations in regex through the following steps:
Detecting the protocol (http or https): The initial part of a URL is often the protocol, which is either
http
orhttps
. In regex, we can usehttps?
to represent this. Thes?
means that thes
is optional, so the URL may start with eitherhttp
orhttps
. Essentially, the question mark (?
) signals that the character before it (in this case,s
) may or may not appear in the URL.Colon and Slashes: Following the protocol (http or https), a URL includes a colon and two slashes (
://
). In regex, the slash (/
) is a special, so to match it literally, we need to escape it with a backslash (\
) . This results in:\/\/
. Therefore, the regex expression to match the protocol and the following colon and slashes can be written in the following way:https?:\/\/
.Making the Protocol Optional: As mentioned, https:// or http:// are not mandatory; a URL could start directly with www.. Therefore, we enclose this part in parentheses and add a question mark (
?
) after it, making the entire segment optional. Our regex expression would now look in the following way:(https?:\/\/)?
.Detecting the www. prefix: After the https:// or http:// part of a URL, we often encounter the www. prefix. In regex, the dot (
.
) is a special character that represents any character, so to match a literal dot, we need to escape it with a backslash (\
). Thus, www. is written aswww\.
in regex. Adding this to our current expression, we get(https?:\/\/)?www\.
.Making the www. Prefix Optional: The www. part of a URL is not always present, so we need to make it optional. We achieve this by enclosing www\. in parentheses and adding a question mark (
?
) after it. This indicates that the www. prefix may or may not appear in the URL. Therefore, our regex pattern becomes(https?:\/\/)?(www\.)?
.
Following this, we expect the domain name, such as google, facebook, etc. We can specify this with the following steps:
Character Set Specification: A domain name can contain lowercase letters (a-z), uppercase letters (A-Z), and even digits (0-9). In regex, we can specify which characters are allowed by placing them inside brackets (
[]
). Inside these brackets, you can either list specific characters or define ranges with a hyphen (-
). For instance, to match any character that is a lowercase letter, an uppercase letter, or a digit, we can use the regex pattern[a-zA-Z0-9]
. This pattern covers all characters froma
toz
,A
toZ
, and0
to9
.Requiring Multiple Characters: While the expression
[a-zA-Z0-9]
matches only a single character, a URL often contains more than one such character. To handle this, we use a plus sign (+
) after the character set in regex. The plus sign (+
) indicates that one or more occurrences of the specified characters are allowed. Thus,[a-zA-Z0-9]+
matches sequences of one or more characters that are either lowercase letters, uppercase letters, or digits.
After the domain name, there is always a dot followed by the domain extension, such as .com or .net.
- Matching the Domain Extension: To match domain extensions such as .com or .org, we need a regex pattern that identifies a literal dot followed by one or more alphabetic characters. This can be expressed in regex as
\.[a-zA-Z]+
. Remember that, in regex, the dot is escaped with a backslash (\
) to ensure it is treated as a literal dot rather than a wildcard.
Putting it all together, the regular expression we will use is ^(https?:\/\/)?(www\.)?[a-zA-Z0-9]+\.[a-zA-Z]+$
.
Notice that we’ve added two additional symbols: ^
at the beginning and $
at the end. These symbols ensure that the entire string must match the regular expression from start to finish. Specifically, ^
indicates the start of the string, and $
indicates the end.
Keep in mind that the regular expression we have just written is not perfect. For example, some URLs may include hyphens in the domain name, subdomains, or other special characters that this basic pattern does not account for. Additionally, this regex does not ensure that the URL actually exists or is reachable; it only checks for a general structure. However, for the sake of this tutorial, this approach is sufficient and serves as a good starting point to highlight how data can be validated.
Integrating the regex pattern into a Python function
Now that we have our regex pattern for detecting URLs, we can put it to use in Python to validate user input.
To do so, we will leverage the re
library, which handles regular expression operations in Python. First, we’ll define our pattern and then use the fullmatch()
function from the re
library to check if the user-provided URL matches the pattern exactly. Note that fullmatch()
doesn’t return a simple boolean value; instead, it provides a Match
object if there’s a match, or None
if there isn’t. To convert this result into a boolean, we can use the bool()
function—bool()
will return True
for any value other than None
, and False
for None
.
Using this approach, we can write our function to validate whether user input follows a correct URL format as follows:
import re # We need to import the re library
def is_valid_url(url):
= r"^(https?:\/\/)?(www\.)?[a-zA-Z0-9]+\.[a-zA-Z]+$"
pattern return bool(re.fullmatch(pattern, url)) # Checks if the URL matches the pattern
When creating functions, it’s advisable to specify the types of the arguments and the expected return type. For example, in this case, the parameter url
should be a string (str
), and the output of this function should be a boolean (bool
). We can specify this as follows:
import re
def is_valid_url(url: str) -> bool:
= r"^(https?:\/\/)?(www\.)?[a-zA-Z0-9]+\.[a-zA-Z]+$"
pattern return bool(re.fullmatch(pattern, url))
By adding type hints, we introduce an additional layer of verification, making our code more robust and specific.
Modifying the application’s behavior to validate and confirm URLs
We have now incorporated a function to check if a user’s input is a valid URL. With this in place, we can adjust the part of the code found in app.py responsible for rendering the page that displays the URL confirmation.
To recap, here is the code we built for the main route (/
) in the previous post:
@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
= request.form.get('urlInput')
url return redirect(url_for('display_url', url=url))
else:
return render_template('index.html')
The current code captures the URL submitted by the user through the urlInput form element, which is intended for URL input. However, the code uses the submitted value regardless of its format. As a result, even when an invalid URL (such as “hello”) is entered, as demonstrated in Figure 3, the code still displays a confirmation message.
For this reason, we want to enhance this code by adding a validation step to ensure that the submitted URL is valid before doing anything with it. To accomplish this, we will use the is_valid_url()
function that we have just created.
If is_valid_url()
returns True
, it confirms that the submitted URL is valid, and we will then display a confirmation message to the user, indicating that the URL has been successfully registered. However, if is_valid_url()
returns False
, an error message will be displayed to inform the user that the URL is invalid.
To implement this, we can modify our code by adding an if-else
statement based on the result of is_valid_url(url)
. If the URL is valid, we will reuse the code we previously had, i.e., redirect the user to a confirmation page using redirect(url_for('display_url', url=url))
. Conversely, if the URL is invalid, we will add a return
statement with the following message: "The URL you entered is not valid. Please check the format and try again."
In Flask, returning a string generates a plain HTML page containing that message. Therefore, our code would now look as displayed below:
@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
= request.form.get('urlInput')
url if is_valid_url(url):
return redirect(url_for('display_url', url=url))
else:
return "The URL you entered is not valid. Please check the format and try again."
else:
return render_template('index.html')
In addition, Figure 4 shows the behavior of our application after implementing these changes and entering both an invalid URL (hello) and a valid URL (www.google.com).
This method is quite basic, as it simply shows a plain page with the message “The URL you entered is not valid. Please check the format and try again.” A more effective approach would be to display this message within the confirmation container.
If the URL is valid, a confirmation box will appear with the message: “You have successfully registered the following URL: [url].” If the URL is invalid, the confirmation box will instead display: “The URL you entered is not valid. Please check the format and try again.”
To implement this update, we need to modify the HTML template (index.html), specifically the section related to the confirmation container, which is identified by the ID confirmationContainer
.
Previously, we set up the confirmation container as follows:
{% if url %}<div id="confirmationContainer">
<p>You have registered the following URL: {{ url }}</p> <!-- Display URL registration confirmation -->
</div>
{% endif %}
In this code, we included a predefined message: “You have registered the following URL:”.
However, we can simplify this by removing the predefined message from the template and passing the entire confirmation text from the backend code, i.e., from our Python code. Therefore, the updated template would look like this:
{% if url %}<div id="confirmationContainer">
<p>{{ url }}</p> <!-- Display URL registration confirmation -->
</div>
</div>
{% endif %}
Now, there is no predefined text inside the container; the full message is provided by the backend.
In addition, we need to update the backend code (app.py) so that it not only sends the URL but also includes a message based on the URL’s validity. If the URL is valid, it should prepend the text “Has entrado la URL:” to the URL. If the URL is invalid, it should display the text “Error URL incorrecta!”:
@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
= request.form.get('urlInput')
url if is_valid_url(url):
= "You have successfully registered the following URL: " + url
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html')
Next, we need to make the same updates to the /display_url/<path:url>
route. This route, as you might remember, is where users are redirected to see the confirmation container after registering a URL. Since users can also register new URLs from this route, it’s important to apply these updates here as well. Below is the revised code for this route:
@app.route("/display_url/<path:url>", methods=['GET', 'POST'])
def display_url(url=None):
if url:
if request.method == 'POST':
= request.form.get('urlInput')
url2 if is_valid_url(url2):
= "You have successfully registered the following URL: " + url2
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html', url=url)
else:
return redirect(url_for('home'))
After implementing these changes, we can run our updated application and test it by entering an invalid URL, such as hello. When we do this, the confirmation container will display the message, “The URL you entered is not valid. Please check the format and try again.”
After seeing this message, we can directly proceed to enter a valid URL, such as www.google.com. This time, the confirmation container will display the message, “You have successfully registered the following URL: www.google.com,” as shown in Figure 5.
As our codebase grows, it becomes more complex and harder to read. This often leads to repetitive code in different parts of our application. Recognizing and addressing code repetition allows us to simplify our code by consolidating repetitive logic into functions. This approach, known as encapsulation, helps us avoid duplicating code and makes our application more maintainable. Instead of copying and pasting the same code repeatedly, we can create reusable functions.
For instance, the code we’ve added for both routes can be consolidated into a single function, which we might call process_url
. This function will handle the URL processing and return the appropriate confirmation text.
We can define the process_url
function as follows:
def process_url(url: str) -> str:
if is_valid_url(url):
= "You have successfully registered the following URL: " + url
confirmation_message else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return confirmation_message
By using this function, we can simplify the code for the home
and display_url
routes as shown below:
@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
= request.form.get('urlInput')
url = process_url(url)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html')
@app.route("/display_url/<path:url>", methods=['GET', 'POST'])
def display_url(url=None):
if url:
if request.method == 'POST':
= request.form.get('urlInput')
url2 = process_url(url2)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html', url=url)
else:
return redirect(url_for('home'))
Setting Up Our Database
When creating a database, you can choose to set it up on your own hardware locally or use a remote database hosted by a cloud service provider like Azure, AWS, or Google Cloud.
Setting up a local database involves installing and managing it on your own computer, a dedicated server, or on-premises infrastructure. This method gives you full control over the environment, allowing you to customize settings, manage security, and optimize performance according to your needs.
The advantages of a local database include complete customization and no ongoing subscription fees. However, you are responsible for running costs such as electricity, hardware maintenance, and software updates. Additionally, scaling a local database can be costly and challenging, often requiring additional hardware. If your local server experiences downtime, your database’s availability may also be compromised unless you implement high-availability solutions, which can increase complexity and expenses.
In contrast, a cloud-based database is hosted on a remote server managed by a cloud service provider. Providers offer a range of options, including SQL and NoSQL databases, allowing you to select the best fit for your needs. Cloud databases simplify management as the provider handles infrastructure, maintenance, and upgrades, letting you focus on utilizing the database.
Cloud databases offer excellent scalability, enabling you to easily adjust resources such as storage and computing power as your needs evolve. They also ensure high availability with built-in redundancy and failover options, and provide global access, which is ideal for distributed teams or global applications. However, cloud databases can be more expensive over time, particularly as your usage grows. They also offer less control over the database environment since the provider manages the infrastructure. Data security in the cloud depends on the provider’s security measures and compliance with relevant regulations. Proper security practices are essential regardless of where the database is hosted.
Configuring Our Database with Supabase
For our purpose, we will use a cloud service, specifically Supabase, https://supabase.com/, which allows us to create dedicated PostgreSQL databases and supporting a free tier. PostgreSQL, also known as Postgres, is an open-source, cross-platform, and highly scalable relational database management system designed to offer advanced features and strong performance in production environments.
If you prefer and are already knowledgeable with other databases or cloud services, you can use alternatives for this tutorial. Whether it’s another managed PostgreSQL provider, a different relational database system, or even a local database setup.
To get started, you need to create a free account on Supabase. Once your account is set up and you’re logged in, you can initiate a new project.
Navigate to the “Projects” section—this is usually the main screen you see after logging in—and click on “New Project,” as highlighted in red in the Figure 6. By following these steps, you will have access to a dedicated PostgreSQL database in the cloud, allowing you to efficiently manage and scale your database needs.
Once you click this button, a menu will appear, as shown in Figure 7. In this menu, you will need to define several parameters. First, enter a unique name for your project. Next, create and enter a password for your database; make sure to remember or securely store this password, as you will need it later. Finally, select a region that is geographically close to you to ensure optimal performance and reduced latency.
After creating a project, you’ll be redirected to the project view, as shown in Figure 8. You might notice a brief delay in being able to perform any actions; this is indicated by the status icon next to the project name, which will show that the project is being set up (highlighted in red in Figure 8). If you encounter any issues with redirection, you can alternatively access the project by going to the “Projects” section and clicking on the specific project.
Retrieving the database connection information
Once the project is set up, you will notice that the project view content updates and a green status indicator appears at the top right corner of the page. This green status means your database is available. Next to this status indicator, you’ll find a “Connect” button, which is highlighted in red in Figure 9.
Clicking this button will open a panel displaying the details required to connect to your database. Be sure to select the “Python” tab, as highlighted in red in Figure 10, to get the specific connection values needed for Python.
As shown in Figure 10, in this panel, we can see a series of connection parameters: user=[DB_USER]
, password=[DB_PASSWORD]
, host=[DB_HOST]
, port=[DB_PORT]
, and dbname=[DB_NAME]
. The placeholders inside the square brackets ([]
) represent where you should enter your specific account details. Be sure to replace these placeholders with your actual information. Remember, these values are sensitive and should be kept confidential to protect access to your database.
Please note that the password (DB_PASSWORD
) should be the one you set when creating the current project.
Connecting our application to our database
Now that we have the required connection parameters to connect to our database, it’s a good practice to keep these configuration values separate from the main code file. Instead of hardcoding them, you should use a .env file.
A .env file is a simple text file used to store environment variables and configuration settings outside of your main codebase. It helps keep sensitive information, like database credentials, secure and makes it easier to manage different settings for development, testing, and production environments.
You should create this .env file and place it in the root directory of your application. With this setup, the file structure for your application will look like this:
- my_flask_app/ (Main folder of the application)
|
|- app.py (Main file for the Flask application)
|
|- .env (File to store environmental variables)
|
|- templates/ (Folder to store HTML templates)
| |
| |- index.html (HTML template for the main page)
|
|- static/ (Folder to store static files such as CSS, JavaScript, images, etc.)
|
|- styles (Folder to store style sheets, such as CSS files)
|
|- home.css (CSS file to style the main page)
The content of this .env file will be as follows:
= 'your_username'
DB_USER = 'your_password'
DB_PASSWORD = 'database_host'
DB_HOST = 'port_number'
DB_PORT = 'database_name' DB_NAME
Since the database connection parameters are defined in a separate .env file, we need to import them into the backend of our application, specifically into app.py. To achieve this, we will use the os
module, which provides functionality for interacting with the operating system. One useful feature of the os
module is the os.getenv()
method. This method retrieves the value of an environment variable if it exists; otherwise, it returns None
or a default value if specified.
Moreover, to ensure that these environment variables are properly loaded from the .env
file, we will utilize the dotenv
library and its load_dotenv()
function. The load_dotenv()
function reads the .env
file and sets the environment variables in the current process.
Without load_dotenv()
, the Python code may not automatically recognize or load the .env
file, which could lead to missing or incorrect environment variable values. By explicitly calling load_dotenv()
, we make sure that these variables are available in the environment before we attempt to retrieve them with os.getenv()
.
Therefore, we will make three key updates to our app.py file: (1) we will import the os
module and the load_dotenv
function from the dotenv
library, (2) we will call load_dotenv()
to ensure the .env
file is loaded properly, and (3) we will use the os.getenv()
method to retrieve the database connection parameters. These additions will look like this in Python:
# Import the os module
import os
# Import load_dotenv from the dotenv library
from dotenv import load_dotenv
# Make clear that env variables are picked from .env file
load_dotenv()
# Retrieve database connection parameters
= os.getenv('DB_USER')
db_user = os.getenv('DB_PASSWORD')
db_password = os.getenv('DB_HOST')
db_host = os.getenv('DB_PORT')
db_port = os.getenv('DB_NAME') db_name
Now that we have the necessary values to connect to our database available within our app.py file, we can proceed with establishing the connection. To accomplish this, we’ll use the psycopg
library, which facilitates connecting to and managing Postgres databases through Python.
First, we need to import the psycopg
library. Our next step is to create a connection to the database using the connect
function provided by psycopg
. This function will use the connection parameters that we have loaded from the .env file. Here’s how you can set up the connection:
# import psycopg library
import psycopg
# Create a connection to our database and store it in a variable called conn
= psycopg.connect(
conn =db_name,
dbname=db_user,
user=db_password,
password=db_host,
host=db_port
port )
To make our code more modular and reusable, we can encapsulate this code within a function. Moreover, we can also add a try-catch statement to handle exceptions that might occur during the connection process and facilitating debugging. You can find the revised implementation below:
import psycopg
def connect_to_database():
try:
= psycopg.connect(
conn =db_name,
dbname=db_user,
user=db_password,
password=db_host,
host=db_port
port
)return conn
except Exception as e:
print(e)
Creating our table
Having established the connection to the database, we can proceed to create the table we will be working with. This table only needs to be set up once during the initial setup.
To accomplish this, we will add an additional function to the app.py file, which will create the table only if it does not already exist.
Remember, we are developing a basic application that allows users to register URLs, which helps us track the popularity of these URLs. To support this functionality, we will create a simple table with just two fields: an ID and the URL value.
Here’s how it will work: Each time a user submits a URL, a new entry will be added to the table, consisting of a unique ID and the URL value. This design keeps the table immutable because each URL is recorded as a separate row without modifying existing entries.
Maintaining an immutable table is crucial as it ensures the integrity of our data. By avoiding changes to existing records, we prevent data loss or corruption and ensure a reliable and accurate record of all submitted URLs.
In this tutorial, we are simplifying the scenario to make it easier to understand compared to more complex real-world situations. This approach helps us focus on how different parts or components of a data-driven application work together in a straightforward way.
For example, in this tutorial, our table will only include two columns: an identifier (ID) and a URL value. In a real-world application, however, the table would likely include additional attributes, such as a user ID. This would require implementing a user identification system to ensure that each user can only register a URL once. We might also include other attributes, such as the date, to provide more detailed information.
To create a table in our database, we will follow these four essential steps:
Establish a Connection to the Database: Begin by using the
connect_to_database()
function to establish a connection to the database.Create a Cursor: Once the connection is established, create a cursor to execute SQL commands. Cursors are temporary objects in SQL that allow us to interact with and manipulate data.
Execute SQL Commands and Commit Changes: Use the cursor to execute the necessary SQL commands to create and modify the table. Once these commands have been executed, use the
commit()
method to save the changes to the database permanently.Close the Database Connection: Finally, once all changes have been made and saved, close the connection to the database.
We can implement these steps in a function called create_table()
. The code for this function is provided below:
def create_table():
try:
# Connect to the PostgreSQL database using the connect_to_database function
= connect_to_database()
conn
# Create a cursor to execute SQL commands using the cursor method
= conn.cursor()
cur
# Execute the command to create the table if it does not already exist
"""
cur.execute( CREATE TABLE IF NOT EXISTS registered_urls (
id SERIAL PRIMARY KEY,
url TEXT
)
""")
# Commit the changes and close the connection
conn.commit()# Print statement confirming correct execution
print("create_table function executed correctly")
except Exception as e:
print(e)
finally:
conn.close()
As you can see, in the code above, the first step is to establish a connection to the database using the connect_to_database()
function. We store this connection in a variable called conn
, which has a method named cursor()
. This method allows us to create a cursor for the database, enabling us to execute SQL commands.
Using this method, we create a cursor and assign it to a variable named cur
. With this cursor, we can execute SQL code using the execute()
method, which we use to execute the following SQL command:
CREATE TABLE IF NOT EXISTS registered_urls (
id SERIAL PRIMARY KEY,
url TEXT )
This command creates a table in our database named registered_urls
, but only if it does not already exist. It specifies that the table will have two columns: id
, which will be the primary key and will have a serial value, meaning it will be numeric and automatically increment for each new row inserted; and url
, which will be of type text and will store the registered URLs.
As previously mentioned, with this table design, we ensure that the values are immutable. This means that once rows are added to the database, they cannot be changed. If a user registers a URL that has already been registered by others, a new row will be created with the same URL but with a different id
.
In the code above, we use a try-catch
statement with a finally
block. The try-catch statement helps manage situations where errors might arise while executing code, especially after opening a database connection.
If an error occurs within the try
block, the code in the catch
block will execute, but the connection might not be closed if the finally
block is not used.
To prevent this, we include a finally
block, which ensures that the code within it runs regardless of whether an error occurs. This guarantees that the database connection is always closed properly, even if an exception is thrown.
At this stage, it’s important to determine the most appropriate when and how to run our create_table()
function. This function is responsible for setting up a database table, but it’s not something that needs to be executed every time we run our Flask application. Running it repeatedly would be inefficient because the table only needs to be created once, or checked occasionally, not on every application start. To avoid unnecessary checks and improve the efficiency of our application, we can create a custom Flask command that allows us to run create_table()
only when we specifically want to.
To accomplish this, Flask provides a convenient way to define custom commands using the @app.cli.command
decorator. By attaching this decorator to a function, we can register a new command that can be executed through the Flask command-line interface (CLI). This means that instead of embedding the table creation logic directly into our application’s startup process, we can isolate it in a command that can be manually triggered whenever needed.
For instance, we could define a command called init-db-table
by using @app.cli.command("init-db-table")
to the create_table()
function as depicted below:
@app.cli.command("init-db-table")
def create_table():
try:
= connect_to_database()
conn = conn.cursor()
cur
"""
cur.execute( CREATE TABLE IF NOT EXISTS registered_urls (
id SERIAL PRIMARY KEY,
url TEXT
)
""")
conn.commit()print("create_table function executed correctly")
except Exception as e:
print(e)
finally:
conn.close()
With these adjustments, our app.py file will look in the following way:
from flask import Flask, request, render_template, redirect, url_for
import os
from dotenv import load_dotenv
import psycopg
import re
load_dotenv()
= os.getenv('DB_USER')
db_user = os.getenv('DB_PASSWORD')
db_password = os.getenv('DB_HOST')
db_host = os.getenv('DB_PORT')
db_port = os.getenv('DB_NAME')
db_name
def connect_to_database():
try:
= psycopg.connect(
conn =db_name,
dbname=db_user,
user=db_password,
password=db_host,
host=db_port
port
)return conn
except Exception as e:
print(e)
def is_valid_url(url: str) -> bool:
= r"^(https?:\/\/)?(www\.)?[a-zA-Z0-9]+\.[a-zA-Z]+$"
pattern return bool(re.fullmatch(pattern, url))
def process_url(url: str) -> str:
if is_valid_url(url):
= "You have successfully registered the following URL: " + url
confirmation_message else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return confirmation_message
= Flask(__name__)
app
@app.cli.command("init-db-table")
def create_table():
try:
= connect_to_database()
conn = conn.cursor()
cur
"""
cur.execute( CREATE TABLE IF NOT EXISTS registered_urls (
id SERIAL PRIMARY KEY,
url TEXT
)
""")
conn.commit()print("create_table function executed correctly")
except Exception as e:
print(e)
finally:
conn.close()
@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
= request.form.get('urlInput')
url = process_url(url)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html')
@app.route("/display_url/<path:url>", methods=['GET', 'POST'])
def display_url(url=None):
if url:
if request.method == 'POST':
= request.form.get('urlInput')
url2 = process_url(url2)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html', url=url)
else:
return redirect(url_for('home'))
if __name__ == "__main__":
=True) app.run(debug
Once we have updated our app.py file, we can move on to creating the registered_urls
table. To accomplish this, we need to use the new CLI command: flask init-db-table
.
To do this, we can open a terminal and navigate to the directory where the app.py file is located. Then, we can execute the command flask init-db-table
, as shown below in Figure 11.
Great! The command seems to have executed successfully. Now, we need to ensure that the table has been created correctly. To do this, we will go back to the Supabase project dashboard, where we will select the project we are working on. Once in our project, we will click on the “Table Editor” tab, found in the right-hand panel, as highlighted in red Figure 12.
After clicking on “Table Editor,” you’ll see all the tables created in your database. In this view, you should be able to find the table named registered_urls
, which is highlighted in red in Figure 13.
Additionally, we can click on the table registered_urls
to see its contents. Doing so will reveal that the table has two empty columns: id and url. Each column is also labeled with its data type—int4 for id and text for url—as shown in Figure 14.
Great! We have successfully created the table and are now ready to start adding values to it!
Storing submitted URLs in our database
Now that we have established a connection with our database and created a table called registered_urls
, we can store each URL that a user registers. To achieve this, we will create a new function that stores a URL in our database whenever a user registers one.
We can call this function insert_url_into_database()
. This function will operate similarly to the previously created function connect_to_database()
. Specifically, it will:
Establish a connection to the database.
Create a cursor.
Use the cursor to execute an SQL command to insert the URL into the
registered_urls
table. Here is how we can implement this using a simple SQLINSERT
statement:INSERT INTO registered_urls (url) VALUES (%s)", (url,)
This SQL query inserts a URL into the
registered_urls
table by specifying theurl
column and using a placeholder%s
to safely insert the value provided by theurl
variable.Notice that we do not need to specify the value for the
id
column. This is because we defined theid
column with theSERIAL
property when we created the table, which automatically generates a unique identifier for each new row.Finally, we need to confirm the changes using the
commit
method and close the connection.
Therefore, the implementation of the insert_url_into_database()
function in Python will look as follows:
def insert_url_into_database(url: str):
try:
= connect_to_database()
conn = conn.cursor()
cur "INSERT INTO registered_urls (url) VALUES (%s)", (url,))
cur.execute(
conn.commit()except Exception as e:
print("Error inserting URL into the database:", e)
finally:
conn.close()
The next step is to execute this function every time a valid URL has been submitted. To achieve this, we can modify the process_url()
function, which, as you may recall, validates whether a URL is valid and generates the confirmation message to be displayed. Currently, this function looks like this:
def process_url(url: str) -> str:
if is_valid_url(url):
= "You have successfully registered the following URL: " + url
confirmation_message else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return confirmation_message
All we need to do now is call our new insert_url_into_database()
function within the if
statement. In other words, if the URL is valid, we will store it in our database. This will modify the function as follows:
def process_url(url: str) -> str:
if is_valid_url(url):
insert_url_into_database(url)= "You have successfully registered the following URL: " + url
confirmation_message else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return confirmation_message
As a result, our code in app.py now looks as follows:
from flask import Flask, request, render_template, redirect, url_for
import os
from dotenv import load_dotenv
import psycopg
import re
load_dotenv()
= os.getenv('DB_USER')
db_user = os.getenv('DB_PASSWORD')
db_password = os.getenv('DB_HOST')
db_host = os.getenv('DB_PORT')
db_port = os.getenv('DB_NAME')
db_name
def connect_to_database():
try:
= psycopg.connect(
conn =db_name,
dbname=db_user,
user=db_password,
password=db_host,
host=db_port
port
)return conn
except Exception as e:
print(e)
def is_valid_url(url: str) -> bool:
= r"^(https?:\/\/)?(www\.)?[a-zA-Z0-9]+\.[a-zA-Z]+$"
pattern return bool(re.fullmatch(pattern, url))
def insert_url_into_database(url: str):
try:
= connect_to_database()
conn = conn.cursor()
cur "INSERT INTO registered_urls (url) VALUES (%s)", (url,))
cur.execute(
conn.commit()except Exception as e:
print("Error inserting URL into the database:", e)
finally:
conn.close()
def process_url(url: str) -> str:
if is_valid_url(url):
insert_url_into_database(url)= "You have successfully registered the following URL: " + url
confirmation_message else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return confirmation_message
= Flask(__name__)
app
@app.cli.command("init-db-table")
def create_table():
try:
= connect_to_database()
conn = conn.cursor()
cur
"""
cur.execute( CREATE TABLE IF NOT EXISTS registered_urls (
id SERIAL PRIMARY KEY,
url TEXT
)
""")
conn.commit()print("create_table function executed correctly")
except Exception as e:
print(e)
finally:
conn.close()
@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
= request.form.get('urlInput')
url = process_url(url)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html')
@app.route("/display_url/<path:url>", methods=['GET', 'POST'])
def display_url(url=None):
if url:
if request.method == 'POST':
= request.form.get('urlInput')
url2 = process_url(url2)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html', url=url)
else:
return redirect(url_for('home'))
if __name__ == "__main__":
=True) app.run(debug
With our code now updated, we can proceed to run our Flask application. At this point, we should verify that URLs registered through the application are correctly stored in the database.
To verify the successful addition of a URL to our dabatase, we can use the Table Editor view in Supabase. First, we should navigate to the Table Editor for our project and locate the registered_urls
table, as shown in Figure 13.
Next, open a separate tab with our running application and register a valid URL. Once you receive confirmation that the URL has been successfully registered, return to the Table Editor view to check that the new URL appears correctly in the registered_urls
table, as shown in Figure 15. In this way, we can confirm that our application is correctly registering URLs into the registered_urls
table.
Great! Our application is starting to become functional. Users can register URLs, and these URLs are stored in our database, which allows us to access and manage this information.
Next, we can move forward by creating a new feature: a page that displays the top favorite websites to our users. This page will present a list of websites ranked by their registration count, showing which websites have been registered the most.
Creating a new page: Top favorite websites
After updating our application to store submitted URLs and verifying their correct storage in our database, our goal is to leverage this data. We aim to identify the top favorite websites—those registered most frequently—and present them to our users on a dedicated page.
To achieve this, we will need to take two main steps:
Extracting URL counts: We will count the frequency of each unique URL in our database, allowing us to identify the most frequently submitted URLs.
Creating a page to display top favorite websites: We will then create a page that dynamically populates a table containing the most popular websites (URLs) and their counts.
Extracting URL counts
Let’s delve into the first step: determining the popularity of each URL by counting how often each URL appears in our database.
To achieve this, we will use an SQL query that groups identical URLs, counts their occurrences, and sorts the results in descending order of frequency. To focus on the most popular URLs, we will also limit the results to the top 10. Here’s the SQL query for this process:
SELECT url, COUNT(*) as count
FROM registered_urls
GROUP BY url
ORDER BY count DESC
LIMIT 10;
Having created this query, we want to execute it through our backend to enable our application to retrieve the relevant information from the database. To accomplish this, and in line with our previous database interactions, we will create a function, which we will call get_top_registered_urls()
. This function will connect to the database, execute the previous SQL query, and return the results. The code for this function is shown below:
def get_top_registered_urls():
try:
= connect_to_database()
conn = conn.cursor()
cur """
cur.execute( SELECT url, COUNT(*) as count
FROM registered_urls
GROUP BY url
ORDER BY count DESC
LIMIT 10;
""")
= cur.fetchall()
top_registered_urls return top_registered_urls
except Exception as e:
print(f"Error retrieving the URLs: {e}")
finally:
if conn:
conn.close()
In this function, you will notice that, compared to the other functions we have created to interact with our database, we are not using the commit()
method. This is because we are not modifying any data in the database; we are merely retrieving it. The commit()
method is only necessary when making changes to the database, such as inserting or updating records.
Instead, we use the fetchall()
method, which retrieves all rows resulting from the query. This method returns a list of tuples, with each tuple representing a row from the query result. Each tuple contains the URL and its count, showing how many times that URL appears in the database.
For example, the result from fetchall()
might look like this: [('www.facebook.com', 1), ('www.google.com', 1)]
. This list provides each URL along with its count of occurrences. In this case, the output indicates that there are two URLs in the table—www.facebook.com and www.google.com—each with a registration count of 1.
We are now ready to create a new page in our application to present this data to users in an appealing and user-friendly format.
Creating a page to display the top favorite websites
To display the information retrieved from the get_top_registered_urls()
function to users, we will need to set up a new page using an HTML template. This involves creating a new file named popular.html in the templates directory.
We will begin by copying the contents from index.html into this new file. However, unlike index.html, the popular.html template will not contain the form elements for users to submit URLs, such as the text field and button, nor will it have the confirmation container.
The primary focus of this new page will be to present a table of popular URLs along with their counts, without any additional functionalities. Therefore, the starting point for this file will look like this:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{{ url_for('static', filename='styles/popular.css') }}"><!-- Replaced home.css file to popular.css -->
</head>
<body>
<div class="content">
<div class="table-container"><!-- Replaced container class to table-container -->
<h1>Top Favorite Websites</h1> <!-- Updated the title -->
<!-- Removed all sections related to the form and the confirmation container -->
</div>
</div>
</body>
</html>
Note that in the previous code, two additional changes have been made: the link to the CSS file has been updated to a new file specifically for this page, named popular.css, and the class name of the div
containing the table has been changed to table-container
.
To present the information retrieved by get_top_registered_urls()
on this page, we need to consider the best way to display it. This function returns a list of tuples, with each tuple containing a URL and its registration count. To clearly present this data, the best approach is to use a table format. In this table, each row will represent a different URL, with the URL displayed in the first column and its registration count in the second.
Since this data is dynamically generated by our Python backend (app.py), we can use Flask’s templating system to render it. The frontend will receive a list of tuples from the backend, with each tuple containing information about a website. Each tuple can then be used to populate a row in an HTML table, using each entry to populate the columns.
Specifically, we can use a for
loop in the template to iterate over the information retrieved by get_top_registered_urls()
, which we will call top_registered_urls
. For each tuple in top_registered_urls
, we extract its values to populate the table columns: the first value renders the URL in the URL column, and the second value renders the count in the tracking count column.
Here is how we can implement this logic into our template:
<table>
<thead>
<tr>
<!-- Column headers -->
<th>URL</th> <!-- Column for the URL -->
<th>Tracking Count</th> <!-- Column for the tracking count -->
</tr>
</thead>
<tbody>
<!-- Loop through each item in the list -->
{% for top_url in top_registered_urls %}<tr>
<!-- Data cell for the URL -->
<td>{{ top_url[0] }}</td> <!-- Displays the URL of the current item in top_registered_urls, i.e., the first entry of the tuple -->
<!-- Data cell for the tracking count -->
<td>{{ top_url[1] }}</td> <!-- Displays the tracking count of the current item in top_registered_urls, i.e., the second entry of the tuple -->
</tr>
{% endfor %}</tbody>
</table>
We can integrate this code into our popular.html
file, which will now contain the following content:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{{ url_for('static', filename='styles/popular.css') }}">
</head>
<body>
<div class="content">
<div class="table-container">
<h1>Top Favorite Websites</h1>
<table>
<thead>
<tr>
<th>URL</th>
<th>Tracking Count</th>
</tr>
</thead>
<tbody>
{% for top_url in top_registered_urls %}<tr>
<td>{{ top_url[0] }}</td>
<td>{{ top_url[1] }}</td>
</tr>
{% endfor %}</tbody>
</table>
</div>
</div>
</body>
</html>
The next step is to create the popular.css file. As with the HTML file for this page, we will start with the existing CSS from the main page, home.css, and make the necessary adjustments to reflect the updates in popular.html.
Since the popular.html file no longer contains the container
class, form elements, or the confirmation container, we need to remove all associated classes and IDs from popular.css. Instead, we will introduce new styles specifically for the elements within the table-container
.
Our goal is to make the table within the table-container
both attractive and easy to read. To achieve this, we will center the table’s title to ensure the header information is well-organized. The table will be set to occupy 50% of the viewport width, which will provide a balanced and visually pleasing layout. We will also merge the table borders into a single continuous line and center the table horizontally for a cleaner look.
To enhance readability, we will add generous padding inside the table cells and use a subtle border to clearly define each cell. The text within the cells will be left-aligned for consistency, while header cells will feature a dark green background with white text to make them stand out.
Additionally, we will enhance user interaction by changing the background color of table rows to a slightly darker shade of gray when hovered over. This subtle color shift will provide a clear visual cue and make the table easier to navigate.
Here is the updated content of popular.css, reflecting all these changes:
body {font-family: Arial, sans-serif;
margin: 0;
padding: 0;
height: 100vh; /* Ensure the body takes up the full height of the viewport */
}
.content {
display: flex;
align-items: center;
padding-top: 5vh; /* Adds padding at the top to avoid overlap with the navbar we will create */
height: 90.5vh; /* Sets the height of the container to 90.5% of the viewport height, i.e., area remaining after navbar and top padding */
background-color: #f0f0f0; /* Light gray background color */
flex-direction: column; /* Arrange children elements in a column */
justify-content: center; /* Horizontally centers the content */
}
/* CSS styling for the table format */
.table-container h1 {
text-align: center; /* Center-aligns the table header */
}
.table-container table {
width: 50vw; /* Set table width to 50% of the viewport width */
border-collapse: collapse; /* Collapse borders for a cleaner look */
margin: auto; /* Center the table horizontally */
}
.table-container th,
.table-container td {
padding: 12px; /* Add padding for better readability */
border: 1px solid #ddd; /* Light gray border for table cells */
text-align: left; /* Left-align text in table cells */
}
.table-container th {
background-color: #4CAF50; /* Dark green background for header cells */
color: white; /* White text color for header cells */
}
.table-container tr:nth-child(even) {
background-color: #f2f2f2; /* Light gray background for even rows */
}
.table-container tr:hover {
background-color: #ddd; /* Change table row background color on hover for better visibility */
}
With all these changes, our file structure will now look like this:
- my_flask_app/ (Main folder of the application)
|
|- app.py (Main file for the Flask application)
|
|- .env (File to store environmental variables)
|
|- templates/ (Folder to store HTML templates)
| |
| |- index.html (HTML template for the main page)
| |
| |- popular.html (HTML template for the top favorite websites page)
|
|- static/ (Folder to store static files such as CSS, JavaScript, images, etc.)
|
|- styles (Folder to store style sheets, such as CSS files)
|
|- home.css (CSS file to style the main page)
|
|- popular.css (CSS file to style the top favorite websites page)
Almost everything is set up. We are now retrieving the most frequently registered websites from the database—essentially, the most popular sites. We have created a new page to display these websites along with their counts. All that remains is to pass the retrieved data from the backend to the frontend so it can be rendered on this page.
Populating the top favorite websites table
To define the behavior for the popular page in our application, we need to start by specifying its route. For instance, we can designate the URL path /popular
for this page. In our app.py
file, we will then outline the necessary steps for handling requests to this route. Specifically,we need to implement the following steps:
Retrieve the unique URLs and their counts by executing the
get_top_registered_urls()
function.Render the
popular.html
page using the data obtained fromget_top_registered_urls()
to populate the table.
To accomplish this, we will use similar approach to what we have employed for other routes in our application. In app.py, we will define a route handler function for the /popular
route. This function will first gather the necessary data and then render the popular.html template, incorporating the retrieved data into the page. Consequently, the code for this route handler will be as follows:
@app.route("/popular", methods=['GET'])
def tracked_page():
= get_top_registered_urls()
top_registered_urls return render_template('popular.html', top_registered_urls=top_registered_urls)
With all these changes implemented, the content of our app.py
is now as follows:
from flask import Flask, request, render_template, redirect, url_for
import os
from dotenv import load_dotenv
import psycopg
import re
load_dotenv()
= os.getenv('DB_USER')
db_user = os.getenv('DB_PASSWORD')
db_password = os.getenv('DB_HOST')
db_host = os.getenv('DB_PORT')
db_port = os.getenv('DB_NAME')
db_name
def connect_to_database():
try:
= psycopg.connect(
conn =db_name,
dbname=db_user,
user=db_password,
password=db_host,
host=db_port
port
)return conn
except Exception as e:
print(e)
def is_valid_url(url: str) -> bool:
= r"^(https?:\/\/)?(www\.)?[a-zA-Z0-9]+\.[a-zA-Z]+$"
pattern return bool(re.fullmatch(pattern, url))
def insert_url_into_database(url: str):
try:
= connect_to_database()
conn = conn.cursor()
cur "INSERT INTO registered_urls (url) VALUES (%s)", (url,))
cur.execute(
conn.commit()except Exception as e:
print("Error inserting URL into the database:", e)
finally:
conn.close()
def process_url(url: str) -> str:
if is_valid_url(url):
insert_url_into_database(url)= "You have successfully registered the following URL: " + url
confirmation_message else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return confirmation_message
def get_top_registered_urls():
try:
= connect_to_database()
conn = conn.cursor()
cur """
cur.execute( SELECT url, COUNT(*) as count
FROM registered_urls
GROUP BY url
ORDER BY count DESC
LIMIT 10;
""")
= cur.fetchall()
top_registered_urls return top_registered_urls
except Exception as e:
print(f"Error retrieving the URLs: {e}")
finally:
if conn:
conn.close()
= Flask(__name__)
app
@app.cli.command("init-db-table")
def create_table():
try:
= connect_to_database()
conn = conn.cursor()
cur
"""
cur.execute( CREATE TABLE IF NOT EXISTS registered_urls (
id SERIAL PRIMARY KEY,
url TEXT
)
""")
conn.commit()print("create_table function executed correctly")
except Exception as e:
print(e)
finally:
conn.close()
@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
= request.form.get('urlInput')
url = process_url(url)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html')
@app.route("/display_url/<path:url>", methods=['GET', 'POST'])
def display_url(url=None):
if url:
if request.method == 'POST':
= request.form.get('urlInput')
url2 = process_url(url2)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html', url=url)
else:
return redirect(url_for('home'))
@app.route("/popular", methods=['GET'])
def tracked_page():
= get_top_registered_urls()
top_registered_urls return render_template('popular.html', top_registered_urls=top_registered_urls)
if __name__ == "__main__":
=True) app.run(debug
After implementing the changes to retrieve and display the top favorite websites in the newly created route, let’s check how these modifications appear in our application.
To do so, first restart the application. Open a terminal or command prompt, navigate to the directory containing our Flask application, and execute the command flask run
. Alternatively, you can run the application in debug mode by executing the script directly with python app.py
or python3 app.py
, depending on your Python version.
Once the application is running, open a web browser and navigate to http://127.0.0.1:5000/. To view the new route, append /popular
to the end of the URL, so it becomes http://127.0.0.1:5000/popular. If your application is hosted on a different IP address or port, use the address displayed in your terminal or command prompt, followed by /popular
.
On this route, you will see the page we just created displaying a table with the most popular URLs, as shown in Figure 16.
Amazing! We can see that the table of the top favorite websites is displayed correctly. Currently, we only see two websites, www.google.com and www.facebook.com, each with a registration count of 1. This is because these are the only websites we’ve added so far.
If you have registered additional URLs or logged these URLs more times, you would see different values reflecting those changes.
However, you might have noticed a potential issue—accessing the popular URLs page is not very intuitive. We had to manually modify the URL by adding /popular
to reach this page, which can make it difficult for users to find. Additionally, users might not be aware that this page exists, so they may never visit it.
The simplest solution to this problem is to create a navigation bar. By incorporating a navigation bar into our application, we can make it much easier for users to access various pages, including the popular URLs page.
Final code
Final file structure
- my_flask_app/ (Main folder of the application)
|
|- app.py (Main file for the Flask application)
|
|- .env (File to store environmental variables)
|
|- templates/ (Folder to store HTML templates)
| |
| |- base_template.html (HTML template for the base template)
| |
| |- index.html (HTML template for the main page)
| |
| |- navbar.html (HTML template for the navigation bar)
| |
| |- popular.html (HTML template for the top favorite websites page)
|
|- static/ (Folder to store static files such as CSS, JavaScript, images, etc.)
|
|- styles (Folder to store style sheets, such as CSS files)
|
|- base_template.css (CSS file to style the base template elements)
|
|- home.css (CSS file to style the main page)
|
|- navbar.css (CSS file to style the navigation)
|
|- popular.css (CSS file to style the top favorite websites page)
Python - app.py
from flask import Flask, request, render_template, redirect, url_for
import os
from dotenv import load_dotenv
import psycopg
import re
load_dotenv()
= os.getenv('DB_USER')
db_user = os.getenv('DB_PASSWORD')
db_password = os.getenv('DB_HOST')
db_host = os.getenv('DB_PORT')
db_port = os.getenv('DB_NAME')
db_name
def connect_to_database():
try:
= psycopg.connect(
conn =db_name,
dbname=db_user,
user=db_password,
password=db_host,
host=db_port
port
)return conn
except Exception as e:
print(e)
def is_valid_url(url: str) -> bool:
= r"^(https?:\/\/)?(www\.)?[a-zA-Z0-9]+\.[a-zA-Z]+$"
pattern return bool(re.fullmatch(pattern, url))
def insert_url_into_database(url: str):
try:
= connect_to_database()
conn = conn.cursor()
cur "INSERT INTO registered_urls (url) VALUES (%s)", (url,))
cur.execute(
conn.commit()except Exception as e:
print("Error inserting URL into the database:", e)
finally:
conn.close()
def process_url(url: str) -> str:
if is_valid_url(url):
insert_url_into_database(url)= "You have successfully registered the following URL: " + url
confirmation_message else:
= "The URL you entered is not valid. Please check the format and try again."
confirmation_message return confirmation_message
def get_top_registered_urls():
try:
= connect_to_database()
conn = conn.cursor()
cur """
cur.execute( SELECT url, COUNT(*) as count
FROM registered_urls
GROUP BY url
ORDER BY count DESC
LIMIT 10;
""")
= cur.fetchall()
top_registered_urls return top_registered_urls
except Exception as e:
print(f"Error retrieving the URLs: {e}")
finally:
if conn:
conn.close()
= Flask(__name__)
app
@app.cli.command("init-db-table")
def create_table():
try:
= connect_to_database()
conn = conn.cursor()
cur
"""
cur.execute( CREATE TABLE IF NOT EXISTS registered_urls (
id SERIAL PRIMARY KEY,
url TEXT
)
""")
conn.commit()print("create_table function executed correctly")
except Exception as e:
print(e)
finally:
conn.close()
@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
= request.form.get('urlInput')
url = process_url(url)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html')
@app.route("/display_url/<path:url>", methods=['GET', 'POST'])
def display_url(url=None):
if url:
if request.method == 'POST':
= request.form.get('urlInput')
url2 = process_url(url2)
confirmation_message return redirect(url_for('display_url', url=confirmation_message))
else:
return render_template('index.html', url=url)
else:
return redirect(url_for('home'))
@app.route("/popular", methods=['GET'])
def tracked_page():
= get_top_registered_urls()
top_registered_urls return render_template('popular.html', top_registered_urls=top_registered_urls)
if __name__ == "__main__":
=True) app.run(debug
Environmental variables file - .env
= 'your_username'
DB_USER = 'your_password'
DB_PASSWORD = 'database_host'
DB_HOST = 'port_number'
DB_PORT = 'database_name' DB_NAME
Base Template
HTML - base_template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='/styles/base_template.css')}}">
{% block styles %}{% endblock %}</head>
<body>
{% include 'navbar.html' %}<div class="content">
{% block content %}{% endblock %}</div>
</body>
</html>
CSS - base_template.css
body {font-family: Arial, sans-serif;
margin: 0;
padding: 0;
height: 100vh;
}
.content {
display: flex;
align-items: center;
padding-top: 5vh; /* Adds padding at the top to avoid overlap with the navbar */
height: 90.5vh; /* Sets the height of the container to 90.5% of the viewport height, i.e., area remaining after navbar and top padding */
background-color: #f0f0f0;
flex-direction: column;
justify-content: center; /* Horizontally centers the content */
}
Home page
HTML - index.html
{% extends "base_template.html" %}
{% block title %}Home{% endblock %}
{% block content %}<div class="container">
<h1>What is your favorite website?</h1>
<form method="post"> <!-- Specify method="post" for form submission -->
<input type="text" name="urlInput" placeholder="Enter URL" class="form-text-input" required>
<br>
<button type="submit" class="submit-btn">Register URL</button>
</form>
{% if url %}<div id="confirmationContainer">
<p>{{ url }}</p> <!-- Display URL registration confirmation -->
</div>
{% endif %}</div>
{% endblock %}
{% block styles %}<link rel="stylesheet" href="{{ url_for('static', filename='/styles/home.css')}}">
{% endblock %}
CSS - home.css
.container {
text-align: center; /* Centers the text inside the container */
margin-bottom: 20px; /* Adds bottom margin to separate the form from the result */
}
.form-text-input {
width: 300px; /* Sets the width of the URL input field */
padding: 10px; /* Adds padding around the input field */
font-size: 16px; /* Sets the font size of the input field */
border: 1px solid #ccc; /* Sets a 1px solid border with a light gray color */
border-radius: 5px; /* Applies rounded corners to the input field */
margin-bottom: 20px; /* Adds bottom margin to separate the input field from the button */
}
.submit-btn {
background-color: #4CAF50; /* Sets the background color of the submit button */
color: white; /* Sets the text color of the submit button */
padding: 10px 20px; /* Adds padding around the button text */
font-size: 16px; /* Sets the font size of the button text */
border: none; /* Removes the border from the submit button */
border-radius: 5px; /* Applies rounded corners to the submit button */
cursor: pointer; /* Changes the cursor to a pointer when hovering over the button */
transition: background-color 0.3s; /* Adds a smooth transition to the button's background color */
}
.submit-btn:hover {
background-color: #3e8e41; /* Changes the button's background color when hovering over it */
}
#confirmationContainer {
padding: 20px; /* Adds padding around the content of the result container */
background-color: #f0eaea; /* Sets the background color of the result container */
border: 1px solid #ccc; /* Sets a 1px solid border with a light gray color */
border-radius: 5px; /* Applies rounded corners to the result container */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Adds a shadow effect to the result container */
margin-top: 20px; /* Adds top margin to visually separate the confirmation container from the form */
}
Top favorite Websites pages
HTML - popular.html
{% extends "base_template.html" %}
{% block title %}Popular Websites{% endblock %}
{% block content %}<div class="table-container">
<h1>Top Favorite Websites</h1>
<table>
<thead>
<tr>
<th>URL</th>
<th>Tracking Count</th>
</tr>
</thead>
<tbody>
{% for top_url in top_registered_urls %}<tr>
<td>{{ top_url[0] }}</td>
<td>{{ top_url[1] }}</td>
</tr>
{% endfor %}</tbody>
</table>
</div>
</div>
{% endblock %}
{% block styles %}<link rel="stylesheet" href="{{ url_for('static', filename='/styles/popular.css')}}">
{% endblock %}
CSS - popular.css
body {font-family: Arial, sans-serif;
margin: 0;
padding: 0;
height: 100vh; /* Ensure the body takes up the full height of the viewport */
}
/* CSS styling for the table format */
.table-container h1 {
text-align: center; /* Center-aligns the table header */
}
.table-container table {
width: 50vw; /* Set table width to 50% of the viewport width */
border-collapse: collapse; /* Collapse borders for a cleaner look */
margin: auto; /* Center the table horizontally */
}
.table-container th,
.table-container td {
padding: 12px; /* Add padding for better readability */
border: 1px solid #ddd; /* Light gray border for table cells */
text-align: left; /* Left-align text in table cells */
}
.table-container th {
background-color: #4CAF50; /* Dark green background for header cells */
color: white; /* White text color for header cells */
}
.table-container tr:nth-child(even) {
background-color: #f2f2f2; /* Light gray background for even rows */
}
.table-container tr:hover {
background-color: #ddd; /* Change table row background color on hover for better visibility */
}
Summary
In this second post of the data-driven web applications basics, we have expanded our application by integrating a database to store user-registered URLs. This enhancement allows us to track and identify the most popular URLs. As a result, we have added a new page to our application that displays the top favorite websites—those that have been registered the most frequently—in a table format.
We have also covered several best practices for developing such applications. For instance, we emphasized the importance of validating user data before processing or storing it to ensure data integrity and security. We used .env files for storing connection and setup details rather than hardcoding them into the main code, which helps in managing configurations more securely and flexibly.
Additionally, we discussed the benefits of encapsulating functions to improve code organization and maintenance, employing design patterns to enhance code structure, and considering the overall UX to ensure a positive interaction with the application.