- Clone the repo
git clone https://github.com/aerabi/link-shortener
- Bring up the app
docker-compose up -d --build
- Perform the migration
docker-compose exec web python manage.py migrate
Create virtualenv and activate it:
virtualenv -p python3.8 venv
source venv/bin/activate
Install the dependencies:
pip install -r requirements.txt
Create virtualenv and activate it:
virtualenv -p python3.8 venv
source venv/bin/activate
Install Django:
pip install Django
Create a src
directory and go there:
mkdir -p src && cd src
Create a Django project there:
django-admin startproject urlshortener
By creating the Django project, the tree structure of the repo would look like this:
src
└── urlshortener
├── manage.py
└── urlshortener
├── asgi.py
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
Now, let's create the Django app for shortening the URLs:
cd src/urlshortener
python manage.py startapp main
It will create directory under src/urlshortener
:
src
└── urlshortener
├── main
│  ├── admin.py
│  ├── apps.py
│  ├── __init__.py
│  ├── migrations
│  ├── models.py
│  ├── tests.py
│  └── views.py
├── manage.py
└── urlshortener
Install the package urlshorteners
:
pip install pyshorteners
Dump the pip freeze for the next generations:
pip freeze > requirements.txt
Head to main/views.py
and edit it accordingly:
from django.shortcuts import render
from django.http import HttpResponse
import pyshorteners
# Create your views here.
def shorten(request, url):
shortener = pyshorteners.Shortener()
shortened_url = shortener.chilpit.short(url)
return HttpResponse(f'Shortened URL: <a href="{shortened_url}">{shortened_url}</a>')
Now, we should assign a URL to this function. Create a urls.py
under main
:
touch main/urls.py
And fill it up:
from django.urls import path
from . import views
urlpatterns = [
path('shorten/<str:url>', views.shorten, name='shorten'),
]
Now head back to the urlshortener/urls.py
and include the newly created urls.py
file:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('', include('main.urls')),
path('admin/', admin.site.urls),
]
Now, run the development server:
python manage.py runserver
And open 127.0.0.1:8000/shorten/aerabi.com
in your browser.
Now let's create the landing page. Create a new HTML file:
mkdir -p main/templates/main
touch main/templates/main/index.html
Open the index.html
and fill it up the with following content:
<form action="{% url 'main:shorten' url %}" method="post">
{% csrf_token %}
<fieldset>
<input type="text" name="url">
</fieldset>
<input type="submit" value="Shorten">
</form>
Now head to main/views.py
and create two functions, namely index
and shorten_post
:
from django.shortcuts import render
from django.http import HttpResponse
import pyshorteners
def index(request):
return render(request, 'main/index.html')
def shorten_post(request):
return shorten(request, request.POST['url'])
. . .
Then to the main/urls.py
to bind the function to URLs:
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('shorten', views.shorten_post, name='shorten_post'),
path('shorten/<str:url>', views.shorten, name='shorten'),
]
The main difference between shorten
and shorten_post
is that the latter accepts
HTTP POST parameters instead of URL path parameters.
Now head to urlshortener/settings.py
and add 'main.apps.MainConfig'
to the beginning of the list INSTALLED_APPS
:
. . .
INSTALLED_APPS = [
'main.apps.MainConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
. . .
Now restart the development server:
python manage.py runserver
This time go to the root: 127.0.0.1:8000
Bingo!
To use Docker Compose with the current setup, first add Gunicorn to the list of dependencies:
gunicorn==20.1.0
psycopg2-binary==2.9.3
Now, create the following docker-compose.yml
file in the root of the repo:
services:
web:
build:
context: ./src/urlshortener/
dockerfile: Dockerfile
command: gunicorn urlshortener.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
Now, start the app using Docker Compose:
docker-compose build
docker-compose up -d
The server should run on port 8000 now: 127.0.0.1:8000
Now, to save the URLs and their short versions locally, we should create database models for them.
Head to main/models.py
and created the following model:
from django.db import models
# Create your models here.
class Question(models.Model):
original_url = models.CharField(max_length=256)
hash = models.CharField(max_length=10)
creation_date = models.DateTimeField('creation date')
We'll assume that the given URLs fit in 256 characters and the short version are less than 10 characters (usually 7 characters would suffice).
Now, create the database migrations:
python manage.py makemigrations
A new file will be created under main/migrations
. Commit this file.
Now to apply the database migrations to the default SQLite DB, run:
python manage.py migrate
Now that we have the database models, we would want to create a shortener service.
Create a Python file main/service.py
and add the following functionality:
import random
import string
from django.utils import timezone
from .models import LinkMapping
def shorten(url):
random_hash = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(7))
mapping = LinkMapping(original_url=url, hash=random_hash, creation_date=timezone.now())
mapping.save()
return random_hash
def load_url(url_hash):
return LinkMapping.objects.get(hash=url_hash)
Now, create a new function in the views for redirecting:
from django.shortcuts import render, redirect
from . import service
. . .
def redirect_hash(request, url_hash):
original_url = service.load_url(url_hash).original_url
return redirect(original_url)
Create a URL mapping for the redirect function:
urlpatterns = [
path('', views.index, name='index'),
path('shorten', views.shorten_post, name='shorten_post'),
path('shorten/<str:url>', views.shorten, name='shorten'),
path('<str:url_hash>', views.redirect_hash, name='redirect'),
]
And finally change the shorten view function to use the internal service:
from django.shortcuts import render, redirect
from django.http import HttpResponse
from django.urls import reverse
from . import service
. . .
def shorten(request, url):
shortened_url_hash = service.shorten(url)
shortened_url = request.build_absolute_uri(reverse('redirect', args=[shortened_url_hash]))
return HttpResponse(f'Shortened URL: <a href="{shortened_url}">{shortened_url}</a>')
We can also remove the third-party shortener library from requirements.txt
, as we don't use it anymore.
To use PostgreSQL instead of SQLite, we'll change the config in settings.py
:
import os
. . .
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
if os.environ.get('POSTGRES_NAME'):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('POSTGRES_NAME'),
'USER': os.environ.get('POSTGRES_USER'),
'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),
'HOST': 'db',
'PORT': 5432,
}
}
Now head to docker-compose.yml
and change it to the following:
version: '3.2'
services:
web:
build: ./src/urlshortener/
command: gunicorn urlshortener.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
environment:
- POSTGRES_NAME=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
depends_on:
- db
db:
image: postgres
volumes:
- ./data/db:/var/lib/postgresql/data
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
Now start the Docker Compose services:
docker-compose up --build -d
Now, to do the migrations, do:
docker-compose exec web python manage.py migrate
The web server is not ready. Go ahead and try it: 127.0.0.1:8000
Create a base.html
under main/templates/main
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Link Shortener</title>
<link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet">
<script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>
</head>
<style>
#main-card {
margin:0 auto;
display: flex;
width: 50em;
align-items: center;
}
</style>
<body class="mdc-typography">
<div id="main-card">
{% block content %}
{% endblock %}
</div>
</body>
Alter the index.html
to use material design:
{% extends 'main/base.html' %}
{% block content %}
<form action="{% url 'shorten_post' %}" method="post">
{% csrf_token %}
<label class="mdc-text-field mdc-text-field--outlined">
<span class="mdc-notched-outline">
<span class="mdc-notched-outline__leading"></span>
<span class="mdc-notched-outline__notch">
<span class="mdc-floating-label" id="my-label-id">URL</span>
</span>
<span class="mdc-notched-outline__trailing"></span>
</span>
<input type="text" name="url" class="mdc-text-field__input" aria-labelledby="my-label-id">
</label>
<button class="mdc-button mdc-button--outlined" type="submit">
<span class="mdc-button__ripple"></span>
<span class="mdc-button__label">Shorten</span>
</button>
</form>
{% endblock %}
Create another view for the response, namely link.html
:
{% extends 'main/base.html' %}
{% block content %}
<div class="mdc-card__content">
<p>Shortened URL: <a href="{{shortened_url}}">{{shortened_url}}</a></p>
</div>
{% endblock %}
Now, get back to views.py
and change the shorten
function to render instead of
returning a plain HTML:
. . .
def shorten(request, url):
shortened_url_hash = service.shorten(url)
shortened_url = request.build_absolute_uri(reverse('redirect', args=[shortened_url_hash]))
return render(request, 'main/link.html', {'shortened_url': shortened_url})
To apply the changes, do:
docker-compose up --build -d