DEV Community

Cover image for Seven URL Shortener
Rithvik
Rithvik

Posted on • Edited on

Seven URL Shortener

Introduction

Flask is a framework for building web applications utilizing Python and SQLite [here with sqlalchemy ] is a database engine that can be used to store application data.

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.

Setting Up Dependencies

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
Enter fullscreen mode Exit fullscreen mode

Once you have activated your programming environment, install Flask and the sqlalchemy library using the following command:

pip install Flask
Enter fullscreen mode Exit fullscreen mode
pip install Flask-SQLAlchemy
Enter fullscreen mode Exit fullscreen mode
pip install python-dotenv
Enter fullscreen mode Exit fullscreen mode

Environment variables

.flaskenv

We will be adding a file .flaskenv to mention the directory of our app

FLASK_APP=url_shortener
Enter fullscreen mode Exit fullscreen mode

.env

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
Enter fullscreen mode Exit fullscreen mode

Creating APP files

Now, We will create a new directory to store all our flask files

mkdir url_shortener
Enter fullscreen mode Exit fullscreen mode

First we will be creating Dunder init file to write create_app() function and have the settings file.

url_shortener/__init__.py

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
Enter fullscreen mode Exit fullscreen mode

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.

url_shortener/settings.py

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')
Enter fullscreen mode Exit fullscreen mode

To import db objects we will create extensions.py file.

url_shortener/extensions.py

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
Enter fullscreen mode Exit fullscreen mode

We will init database in Dunder init file too.

To shorten links we will be making a model.py file.

url_shortener/extensions.py

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
Enter fullscreen mode Exit fullscreen mode

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.

url_shortener/auth.py

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
Enter fullscreen mode Exit fullscreen mode

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 using POST 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
Enter fullscreen mode Exit fullscreen mode

url_shortener/templates/index.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>
        <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>
Enter fullscreen mode Exit fullscreen mode

url_shortener/templates/link_added.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>
Enter fullscreen mode Exit fullscreen mode

url_shortener/templates/stats.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>
Enter fullscreen mode Exit fullscreen mode

url_shortener/templates/404.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>
Enter fullscreen mode Exit fullscreen mode

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.

url_shortener/routes.py

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
Enter fullscreen mode Exit fullscreen mode

We will then add register_blueprint() to __init__.py

Creating the database

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')
Enter fullscreen mode Exit fullscreen mode
python3
Enter fullscreen mode Exit fullscreen mode
> from url shortener import create_ app
> from url shortener.extensions import db
> from url shortener.models import Link
> db.create_all (app=create_app())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
sqlite> .tables                                                                                                                                             
link
sqlite> .exit
Enter fullscreen mode Exit fullscreen mode

Source

Full Source Code: Github

Deployment

Blog - Due to insufficient credits, currently down - will try to rehost some-other way.

Deploy Locally

flask run
Enter fullscreen mode Exit fullscreen mode

We can find the app locally deployed at http://127.0.0.1:5000/.

username: admin
password: password
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
abhishekbharadwaj profile image
Info Comment hidden by post author - thread only accessible via permalink
abhishek bharadwaj

why all these xD

Some comments have been hidden by the post's author - find out more