22
Seven URL Shortener
Here, We wil be buliding a URL shortener, a service that takes any URL and generates a short URL, similar to tinyurl or bit.ly.
We are going to generate random strings for the alias using python library random.
We will be using Flask, SQLite to build your URL shortener. Our application will allow users to enter a URL and generate a shorter version, in addition to a statistics page where users can view the number of times a URL has been clicked. We will be using the Bulma toolkit to style our application.
We need to activate environment and install Flask using the pip package installer. Then you’ll create the database you will use to store URLs.
$ source env/bin/activate
Once you have activated your programming environment, install Flask and the sqlalchemy library using the following command:
pip install Flask
pip install Flask-SQLAlchemy
pip install python-dotenv
We will be adding a file .flaskenv
to mention the directory of our app
FLASK_APP=url_shortener
We will add another file .env
to set admin username, password and database url as environment variables.
ADMIN_USERNAME=admin
ADMIN_PASSWORD=password
DATABASE_URL=sqlite:///db.sqlite3
Now, We will create a new directory to store all our flask files
mkdir url_shortener
First we will be creating Dunder init file to write create_app()
function and have the settings file.
from flask import Flask
from .extensions import db
from .routes import short
def create_app(config_file='settings.py'):
app = Flask(__name__)
app.config.from_pyfile(config_file)
db.init_app(app)
app.register_blueprint(short)
return app
settings file will be holding the settings for the app and will be related to .env
file.
we will be adding SQLAlchemy URI, ADMIN_USERNAME and ADMIN_PASSWORD to the file in order to make it ease to edit in future. We will turn off track modification for SQLAlchemy.
import os
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
SQLALCHEMY_TRACK_MODIFICATIONS = False
ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME')
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD')
To import db objects we will create extensions.py
file.
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
We will init database in Dunder init file too.
To shorten links we will be making a model.py
file.
import string
from datetime import datetime
from random import choices
from .extensions import db
class Link(db.Model):
id = db.Column(db.Integer, primary_key=True)
original_url = db.Column(db.String(512))
short_url = db.Column(db.String(3), unique=True)
visits = db.Column(db.Integer, default=0)
date_created = db.Column(db.DateTime, default=datetime.now)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.short_url = self.generate_short_link()
def generate_short_link(self):
characters = string.digits + string.ascii_letters
short_url = ''.join(choices(characters, k=7))
link = self.query.filter_by(short_url=short_url).first()
if link:
return self.generate_short_link()
return short_url
We have created a table Link
in which we will have the following columns:
- id - Integer, primary key
- original_url - String
- short_url - String, unique
- visits - Integer, default=0
- date_created - DateTime, default=datetime.now
We have extended Dunder init to call the methods with keyword arguments. Then, we will call generate_short_link
where we will get a random 7 letters string from Upper case, Lower case or Numbers. For this we have used choices()
from random
library. We will check if the generated string exists in our database and if it exits we will run again to get unique string.
Now we are done with our model. So, we need to redirect our generated string to the original url. We will create routes.py
file. Main motto of this app is for personal use. So, we will be using basic authentication. We will be having check_auth()
function which will be comparing the username and password with the ones given as environment variables. authenticate()
will be saying that authorization is required. In requires_auth()
it will ask for authorization if its not authorized else it will check for authorisation with check_auth()
then it will return the function keyword arguments.
from functools import wraps
from flask import request, Response, current_app
def check_auth(username, password):
#This function is called to check if a username password combination is valid.
return username == current_app.config['ADMIN_USERNAME'] \
and password == current_app.config['ADMIN_PASSWORD']
def authenticate():
#Sends a 401 response that enables basic auth
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
We will be adding the authorisation as decorator in routes.py
we will create a Blueprint
- short
. Now we need to create some endpoints.
-
/short_url
will be used to redirect the url. -
/
will be used for index. -
/add_link
will be used to add the url and will be usingPOST
method. -
/stats
will be used to display the statistics. -
errorhandler(404)
will be shown when the user goes out with a random link.
In order to work with routes we need to create html
files, which we will be trying to keep simple - have used Bulma to do it so.
We will create a directory for these files.
mkdir templates
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>URL Shortener</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
<link href='https://fonts.googleapis.com/css?family=Tangerine' rel='stylesheet'>
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<style>
.center {
padding: 30px;
border: 3px blue;
text-align: center;
}
h1 {
font-family: 'Tangerine', serif;
font-size: 50px;
text-align: center;
}
h2 {
font-size: 37px;
text-align: left;
}
</style>
</head>
<body>
<h1 style="color:black;" allign="center"><b>Seven URL Shortener</b></h1>
<div class="tabs is-centered" >
<ul>
<li class="is-active">
<a>
<span class="icon is-small"><i class="fas fa-home" aria-hidden="true"></i></span>
<span onclick="window.location='/'">Home</span>
</a>
</li>
<li>
<a>
<span class="icon is-small"><i class="far fa-file-alt" aria-hidden="true"></i></span>
<span onclick="window.location='/stats'">statistics</span>
</a>
</li>
</ul>
</div>
<div class="center">
<div>
<img src="https://github.com/rith-vik-7/trash/blob/main/short.jpg?raw=true" alt="short" width="700" height="433">
</div>
<section class="section">
<div class="container">
<form method="POST" action="{{ url_for('short.add_link') }}">
<div class="field">
<div class="control">
<input class="input" type="text" placeholder="Shorten your link" name="original_url">
</div>
</div>
<div class="control">
<button type="submit" style="float: right;" class="button is-info">Shorten</button>
</div>
</form>
</div>
</section>
</div>
</body>
<script>
document.addEventListener('keydown', function() {
if (event.keyCode == 123) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
} else if (event.ctrlKey && event.shiftKey && event.keyCode == 73) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
} else if (event.ctrlKey && event.keyCode == 85) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
}
}, false);
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
alert("This function has been disabled to prevent you from stealing my code!");
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
alert("This function has been disabled to prevent you from stealing my code!");
window.event.returnValue = false;
});
}
</script>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>URL Shortener</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
<link href='https://fonts.googleapis.com/css?family=Tangerine' rel='stylesheet'>
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<style>
.center {
padding: 30px;
border: 3px blue;
text-align: center;
}
h1 {
font-family: 'Tangerine', serif;
font-size: 50px;
text-align: center;
}
h2 {
font-size: 37px;
text-align: left;
}
</style>
</head>
<body>
<h1 style="color:black;" allign="center"><b>Seven URL Shortener</b></h1>
<div class="tabs is-centered">
<ul>
<li class="is-active">
<a>
<span class="icon is-small"><i class="fas fa-home" aria-hidden="true"></i></span>
<span onclick="window.location='/'">Home</span>
</a>
</li>
<li>
<a>
<span class="icon is-small"><i class="far fa-file-alt" aria-hidden="true"></i></span>
<span onclick="window.location='/stats'">statistics</span>
</a>
</li>
</ul>
</div>
<div class="center">
<div>
<img src="https://github.com/rith-vik-7/trash/blob/main/short.jpg?raw=true" alt="short" width="700" height="433">
</div>
<div class="container">
<div class="field">
<div class="control">
<input class="input" type="text" name="original_url" value="{{ original_url }}">
</div>
</div>
<div class="field">
<label class="label"><b>Seven</b> Shortened URL</label>
<div class="control">
<input class="input" type="text" name="original_url"
value="{{ url_for('short.redirect_to_url', short_url=new_link, _external=True) }}">
</div>
</div>
</div>
</section>
</div>
</body>
<script>
document.addEventListener('keydown', function() {
if (event.keyCode == 123) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
} else if (event.ctrlKey && event.shiftKey && event.keyCode == 73) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
} else if (event.ctrlKey && event.keyCode == 85) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
}
}, false);
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
alert("This function has been disabled to prevent you from stealing my code!");
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
alert("This function has been disabled to prevent you from stealing my code!");
window.event.returnValue = false;
});
}
</script>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>URL Shortener</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
<link href='https://fonts.googleapis.com/css?family=Tangerine' rel='stylesheet'>
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<style>
.center {
padding: 30px;
border: 3px blue;
text-align: center;
}
h1 {
font-family: 'Tangerine', serif;
font-size: 50px;
text-align: center;
}
h2 {
font-size: 37px;
text-align: left;
}
</style>
</head>
<body>
<h1 style="color:black;" allign="center"><b>Seven URL Shortener</b></h1>
<div class="tabs is-centered">
<ul>
<li>
<a>
<span class="icon is-small"><i class="fas fa-home" aria-hidden="true"></i></span>
<span onclick="window.location='/'">Home</span>
</a>
</li>
<li>
<a>
<span class="icon is-small"><i class="fas fa-link" aria-hidden="true"></i></span>
<span>Link</span>
</a>
</li>
<li class="is-active">
<a>
<span class="icon is-small"><i class="far fa-file-alt" aria-hidden="true"></i></span>
<span onclick="window.location='/stats'">statistics</span>
</a>
</li>
</ul>
</div>
<section class="section">
<div class="container">
<table class="table is-bordered is-striped is-narrow is-hoverable">
<thead>
<tr>
<th>Original URL</th>
<th>Short URL</th>
<th>Visits</th>
</tr>
</thead>
<tbody>
{% for link in links %}
<tr>
<td>{{ link.original_url }}</td>
<td>{{ url_for('short.redirect_to_url', short_url=link.short_url, _external=True) }}</td>
<td>{{ link.visits }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</body>
<script>
document.addEventListener('keydown', function() {
if (event.keyCode == 123) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
} else if (event.ctrlKey && event.shiftKey && event.keyCode == 73) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
} else if (event.ctrlKey && event.keyCode == 85) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
}
}, false);
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
alert("This function has been disabled to prevent you from stealing my code!");
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
alert("This function has been disabled to prevent you from stealing my code!");
window.event.returnValue = false;
});
}
</script>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 ERROR</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<body style="background-color:#192643;">
<section class="section">
<div class="container">
<img src="https://github.com/rith-vik-7/trash/blob/main/404.gif?raw=true" alt="Trulli" width="1000" height="900">
</div>
</section>
</body>
<script>
document.addEventListener('keydown', function() {
if (event.keyCode == 123) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
} else if (event.ctrlKey && event.shiftKey && event.keyCode == 73) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
} else if (event.ctrlKey && event.keyCode == 85) {
alert("This function has been disabled to prevent you from stealing my code!");
return false;
}
}, false);
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
alert("This function has been disabled to prevent you from stealing my code!");
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
alert("This function has been disabled to prevent you from stealing my code!");
window.event.returnValue = false;
});
}
</script>
</html>
We will be calling function in add_link()
method to get the short url. Then, we will call render template that link is added and we will display the generated url by passing the generated short url and original url.
In short_url()
we will be using redirect()
function from flask to redirect short url to original url and get the counts of visits by using a counter which increments whenever the short url is called to redirect the original url. If the short url is not found in the database it will be redirected to 404 error.
Then from stats
we will redirect the visit counts along with original and short url to the rendered template.
from flask import Blueprint, render_template, request, redirect
from .extensions import db
from .models import Link
from .auth import requires_auth
short = Blueprint('short', __name__)
@short.route('/<short_url>')
def redirect_to_url(short_url):
link = Link.query.filter_by(short_url=short_url).first_or_404()
link.visits = link.visits + 1
db.session.commit()
return redirect(link.original_url)
@short.route('/')
@requires_auth
def index():
return render_template('index.html')
@short.route('/add_link', methods=['POST'])
@requires_auth
def add_link():
original_url = request.form['original_url']
link = Link(original_url=original_url)
db.session.add(link)
db.session.commit()
return render_template('link_added.html',
new_link=link.short_url, original_url=link.original_url)
@short.route('/stats')
@requires_auth
def stats():
links = Link.query.all()
return render_template('stats.html', links=links)
@short.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
We will then add register_blueprint()
to __init__.py
We need to create the database now.
For instance in order to setup, We need to make some changes to url_shortener/settings.py
import os
SQLALCHEMY_DATABASE_URI = 'sqlite:///db.sqlite3' #os.environ.get('DATABASE_URL')
SQLALCHEMY_TRACK_MODIFICATIONS = False
ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME')
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD')
python3
> from url shortener import create_ app
> from url shortener.extensions import db
> from url shortener.models import Link
> db.create_all (app=create_app())
After noticing db.sqlite3
we can change the SQLALCHEMY_DATABASE_URI
back as before.
We can cross check the table if its created.
sqlite3 db.sqlite3
sqlite> .tables
link
sqlite> .exit
Full Source Code: Github
Deployment
Blog
flask run
We can find the app locally deployed at http://127.0.0.1:5000/.
username: admin
password: password
22