CMPUT 404

Web Applications and Architecture

Heroku Lab


Description

Big lab! This lab contains two phases. In Phase One, you will build a simple Django website. Understand the fundamentals of Django's MVC architecture using the built in models and views. In Phase Two, you will deploy the Django application to Heroku. Understand the reasoning behind Platform as a Service (PaaS) businesses like Heroku. You may follow the official documentation.

Warning!

The University's firewall (UWS) blocks new domains for 24 hours to prevent scam domains.

This might affect your Heroku domain.The University's firewall is also inconsistent so it doesn't always seem to do this. We have complained about this every semester since forever, but IST simply does not care.

Double check that your Heroku is not blocked before you demo. We will not give you an extension if you Heroku is blocked.

You have several options to make sure this doesn't happen:

Getting Started

Prepare your Repo

  1. Get the github classroom link from eClass, create your assignment, and clone it.
  2. Create an appropriate .gitignore file, to prevent unwanted files being commited to your repository.

Place this gitignore within the root of your project. You can combine this one and this one and this one for your django+node project. Double check you're not staging any unwanted files before you commit. The git status command can help with that.

Make sure your .gitignore contains *.sqlite3.

Installing venv with pip

Virtual environment is a CLI tool for managing python dependencies. Different projects have different dependencies, and version requirements. A virtual environment allows you to manage your dependencies specific to your project.

To install follow these steps (Note that you need pip installed and configured):

  1. python -m pip install --user virtualenv
  2. Check installation: python -m virtualenv --help

For more info check here. To create and use a virtual environment:

  1. virtualenv venv --python=python3
  2. source venv/bin/activate

The first command will create a directory named venv. Contained in the directory is your project dependecy installation and the activate script. Run deactivate to exit the virtual environment.

Lab Instructions

Phase One: Django Polls App

Creating a Django Project

Make sure to use a virtual environment for this lab!

If you're doing this on Windows please make sure to follow the Windows instructions for Lab 1 before starting this lab!

Follow Labsignments 2 to create a virtual environment and install Django. You can tell Django is installed and which version by running the following command in a shell prompt:

python -m django --version

Initialize a new Django project in your repo.

Now that the server’s running, visit http://127.0.0.1:8000/ with your web browser. You’ll see a “Congratulations!” page, with a rocket taking off. It worked!


Creating a Django App

Create a new application within your django project called polls.

python manage.py startapp polls

Modify the polls/views.py file to look like the following.

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")

Create a file at polls/urls.py with the following code.

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

Within urls.py (the outer one) include the following code.

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("polls/", include("polls.urls")),
    path("admin/", admin.site.urls),
]

Run the Django project with the runserver command.

python manage.py runserver

Go to http://localhost:8000/polls/ in your browser, and you should see the text “Hello, world. You’re at the polls index.”, which you defined in the index view.

If you get an error page here, check that you’re going to http://localhost:8000/polls/ and not http://localhost:8000/.


Working with Models

Time to create our first models. Open up settings.py and ensure that the default database is set to sqlite3.

# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

Within polls/models.py include the following code.

from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField("date published")

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

To activate our poll application in our project, add it to the installed apps within lab3/settings.py. This file already exists, you do not need to create it.

INSTALLED_APPS = [
    "polls.apps.PollsConfig",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

Make the database migrations.

python manage.py makemigrations polls

You should see something similar to the following:

Migrations for 'polls':
  polls/migrations/0001_initial.py
    - Create model Question
    - Create model Choice

Run the migration command to create the tables in your database.

python manage.py migrate

Make sure your .gitignore contains *.sqlite3, and that you don't have db.sqlite3 in your git repo.

Using Django Admin

Create a user that can log into the admin site.

python manage.py createsuperuser

You will be asked to enter your username, email, and password (twice for confirmation).

Make the polls app modifiable in the admin by editing the polls/admin.py file to be the following:

from django.contrib import admin

from .models import Choice, Question

admin.site.register(Choice)
admin.site.register(Question)

Start the development server again and go to /admin on your local domain – e.g., http://127.0.0.1:8000/admin/. You should see the admin’s login screen and can login with your admin account.

python manage.py runserver
Working with Views

Add some additional views to the polls/views.py file. Include the following methods:

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

With the above views added, add them to polls/urls.py.

from django.urls import path

from . import views

urlpatterns = [
    # ex: /polls/
    path("", views.index, name="index"),
    # ex: /polls/5/
    path("<int:question_id>/", views.detail, name="detail"),
    # ex: /polls/5/results/
    path("<int:question_id>/results/", views.results, name="results"),
    # ex: /polls/5/vote/
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

Take a look in your browser, at /polls/34/. It’ll run the detail() function and display whatever ID you provide in the URL. Try /polls/34/results/ and /polls/34/vote/ too – these will display the placeholder results and voting pages.


Making views render model data

Update the polls/views.py index method so the questions are returned.

from django.http import HttpResponse

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
    output = ", ".join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

# Leave the rest of the views (detail, results, vote) unchanged

Create an empty directory named templates within polls. Then create another directory named polls within the templates directory. Lastly create a file called index.html within the second polls directory.

mkdir -p polls/templates/polls
touch polls/templates/polls/index.html

Within the newly created empty polls/templates/polls/index.html file, write the following.

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Update the index view in polls/views.py to use the new template.

from django.shortcuts import render

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
    context = {"latest_question_list": latest_question_list}
    return render(request, "polls/index.html", context)

Add a new template file for the poll details view.

touch polls/templates/polls/detail.html

For the newly created template in polls/templates/polls/detail.html, update the content with the template tag for our question:

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

Update the detail view in polls/views.py to use the new template.

from django.shortcuts import get_object_or_404, render

from .models import Question

# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/detail.html", {"question": question})

Remove the hardcoded urls that we specified in the polls/templates/polls/index.html file and replace it with at emplate tag referencing our url.

<!-- old -->
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

<!-- new -->
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

Namespacing URL names

Add an app_name in the polls/urls.py file to set the application namespace.

from django.urls import path

from . import views

app_name = "polls"
urlpatterns = [
    path("", views.index, name="index"),
    path("<int:question_id>/", views.detail, name="detail"),
    path("<int:question_id>/results/", views.results, name="results"),
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

Change your polls/index.html template to point at the namespaced detail view.

<!-- old -->
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

<!-- new -->
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

Writing a simple form

Update the polls/templates/polls/detail.html file to match the following:

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

Remember, in Part 3, we created a url for the polls application in polls/urls.py that includes this line:

path('<int:question_id>/vote/', views.vote, name='vote'),

We also created a dummy implementation of the vote() function in polls/views.py. Let’s update the vote view in polls/views.py to handle the new template.

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question

# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST["choice"])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(
            request,
            "polls/detail.html",
            {
                "question": question,
                "error_message": "You didn't select a choice.",
            },
        )
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))

After voting, the application should redirect to a view displaying the results. Update the results view in polls/views.py

from django.shortcuts import get_object_or_404, render


def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/results.html", {"question": question})

Create a template for the results in polls/templates/polls/results.html

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

Run your application. Use the admin interface to create aquestion, then create multiple choices for your question. Navigate back to polls/ and attempt to use your application.


Refactoring: Generic Views

To convert the poll application to use generic views, we will:

  1. Convert the old url conf.
  2. Delete some of the old, unnecessary views.
  3. Introduce new views based on Django's generic views.

Amend the polls/urls.py url configuration. Note that the name of the matched pattern in the path strings of the second and third patterns has changed from <question_id> to <pk>.

from django.urls import path

from . import views

app_name = "polls"
urlpatterns = [
    path("", views.IndexView.as_view(), name="index"),
    path("<int:pk>/", views.DetailView.as_view(), name="detail"),
    path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

Amend the polls/views.py file.

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by("-pub_date")[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = "polls/detail.html"


class ResultsView(generic.DetailView):
    model = Question
    template_name = "polls/results.html"


def vote(request, question_id):
    ...  # same as above, no changes needed.

Based from the DRF (Django Rest Framework) tutorial here

To convert your queries to or from a JSON object you can use Django's serializers to serialize or deserialize Django QuerySets to or from JSON objects.

First install the Django Rest Framework library using pip

pip install djangorestframework

Then add the rest_framework app to INSTALLED_APPS in our settings.py file

INSTALLED_APPS = [
    ...
    'rest_framework'
]
Creating a Serializer Class

Create a file in the polls directory named serializers.py and add the following

from rest_framework import serializers
from .models import Question

class QuestionSerializer(serializers.Serializer):
    question_text = serializers.CharField()
    pub_date = serializers.DateTimeField()

    def create(self, validated_data):
        """
        Create and return a new `Question` instance, given the validated data
        """
        return Question.object.create(**validated_data)

    def update(self, instance, validated_data):
        """
        Update and return an existing `Question` instance, given the validated data
        """
        instance.question_text = validated_data.get('question_text', instance.question_text)
        instance.pub_date = validated_data.get('pub_date', instance.pub_date)
        instance.save()
        return instance
Update our views using our Serializer

Once you have the serializers you now need to write some API views using the new Serializer class

Edit the polls/views.py file, and add the following

from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import QuestionSerializer

...


@api_view(['GET'])
def get_questions(request):
    """
    Get the list of questions on our website
    """
    questions = Question.objects.all()
    serializer = QuestionSerializer(questions, many=True)
    return Response(serializer.data)

The @api_view decorator will wrap the view so that only HTTP methods that are listed in the decorator will get executed.

Updating the our URLs for the new views

Because we want our API responses to have JSON objects we will have to add another set of urls with a api/ prefix to our polls/urls.py file.

from django.urls import path
from . import views

urlpatterns = [
    ...
    path('api/questions/', views.get_questions, name='get_questions'),
]

Now run the project again with the runserver command and go to polls/api/questions/

python manage.py runserver

You should see a list of question in a json format.

Updating a Question Using our Serializer

We can use the serializer to update the question_text field of our question entries.

@api_view(['GET','POST'])
def update_question(request, pk):
    """
    Get the list of questions on our website
    """
    questions = Question.objects.get(id=pk)
    serializer = QuestionSerializer(questions, data=request.data, partial=True)
    if serializer.is_valid():
        serializer.save()
        return Response(serializer.data)
    return Response(status=400, data=serializer.errors)

and update the polls/urls.py file.

from django.urls import path
from . import views

urlpatterns = [
    ...
    path('api/question/<int:pk>', views.update_question, name='update_question'),
]

Run the project using runserver and go to this link polls/api/question/1 and POST the following information below.

{
    "question_text": "Updated question text"
}

After clicking the POST button you should see the updated value in the json structure above. The new value should also be reflected in the model admin page as well.

Other cool things to know
More information about DRF

Her is the API Guide for Serializers

Here is the Tutorial guide

Optional/Outside of Lab

It is in your best interest to Work through the rest of Django's First Steps Tutorials:

Phase Two: Heroku

Setting up the Heroku CLI

Sign up for a Heroku account at https://signup.heroku.com/. Be sure to enable multi-factor authentication (two-factor authentication) or Heroku will not allow you to sign in.

You can apply for free Heroku credits for 12 months at https://www.heroku.com/github-students with an eligible GitHub student account https://education.github.com/pack. We cannot gaurantee that you will get free Heroku credits. As stated in the Course Outline, you may have to pay a small amount for Heroku.

Note: Remember to clean up all Heroku resources after this course to avoid unexpected charges after exceeding the credit limit or the offer expires.

Download and install the Heroku CLI tools.

wget https://cli-assets.heroku.com/channels/stable/heroku-linux-x64.tar.gz
tar -xvf heroku-linux-x64.tar.gz
export PATH="$PATH:$HOME/heroku/bin"

Ensure the heroku tool works, login to your account.

heroku --version
# heroku/8.7.1 linux-x64 node-v16.19.0
heroku login
Preparing our Django Application for Heroku

Ensure the Django application created in Phase One is working locally.

Activate the virtualenv for the Django application.

Pip install gunicorn and django-on-heroku.

pip install gunicorn django-on-heroku

Save the new python requirements into the requirements.txt file.

pip freeze > requirements.txt

requirements.txt must be in the root of your repo for Heroku to detect your project as a Python/Django project!

Within settings.py, add the following statements:

import django_on_heroku # top of the file

# ...

django_on_heroku.settings(locals()) # bottom of the file
Deploying our Django Application to Heroku

First, create an app on your Heroku dashboard. Keep in mind that a Heroku app is different from a Django app.

Commit your files and deploy the application using a the heroku command line tool. See their article on how to do this. Follow the instructions for an existing app, not a new app: use heroku git:remote, not heroku create. If you used heroku create, please see this stackoverflow question about how to return to a single repository.

You should have a heroku app. You should see it if you run the heroku list command. In the following, APPNAME refers to this heroku app's name.

Make a Postgres Database on Heroku
heroku addons:create heroku-postgresql:mini

You can manage your mini postgres on your heroku dashboard under the app addons.
access panel

Check that heroku is configuring the database:

heroku run env

You should get an output like that contains a line that starts with DATABASE_URL=postgres:// followed by a username and a password.

Check that django is now using your heroku postgres database:

The output should contain a line like this that says 'default' and has 'ENGINE': 'django.db.backends.postgresql'.

DATABASES = {'default': {'NAME': 'random letters', 'USER': 'random letters', 'PASSWORD': 'big hex number', 'HOST': 'something.amazonaws.com', 'PORT': 5432, 'CONN_MAX_AGE': 600, 'CONN_HEALTH_CHECKS': False, 'ENGINE': 'django.db.backends.postgresql', 'OPTIONS': {'sslmode': 'require'}, 'ATOMIC_REQUESTS': False, 'AUTOCOMMIT': True, 'TIME_ZONE': None, 'TEST': {'CHARSET': None, 'COLLATION': None, 'MIGRATE': True, 'MIRROR': None, 'NAME': None}}}

If it contains sqlite3, something is wrong. Please check that you followed the steps starting with adding django-on-heroku correctly.

Run your migrations, create a Superuser, and ensure your application functionality works.

After this if you select your postgres database in the Heroku dataclips interface, you should see your polls_question and poll_choice tables.

Go to /polls on your Heroku deployed site, you should be able to use the Polls app from Heroku.

Note: Please make sure that the Heroku app uses Postgres as the backend database. If you created the Heroku app through Git integration, this should be a default setting.

You can verify the backend in use by login into the dashboard of the Heroku app: https://dashboard.heroku.com/apps/APP_NAME, then click the Resources tab, you should see Heroku Postgres under the Add-ons Section.

If a different Heroku backend is used (e.g., SQLite), or if you try to create the Heroku app through the Heroku webpage, you can follow the below instructions to enable Postgres. https://www.geeksforgeeks.org/deploying-django-app-on-heroku-with-postgres-as-backend/

Checking your heroku app

You can use the heroku open command to open your heroku app in a web browser.

Restrictions

Violation of the restrictions will result in a mark of zero.

Requirements

Submission Instructions

Make sure you push to github classroom BEFORE 4PM on the day of your lab section! You will not be able to push after that!

Submit a link to your repo in the form https://github.com/uofa-cmput404/w24-h0x-labsignment-heroku-yourgithubname on eClass. Do not submit a link to a branch, a file, or the clone url. If you do not do this we will not know which github submission is yours.

After you receive your grade, you can delete your Heroku app to save credits/money.

Collaboration

Tips

Django is a complex framework and maybe overwhelming at times. You should consult the documentation should you run into any issues with the framework.

If you're unable to load a static file or resource, it maybe because you're not referencing it correctly. It may be in a different directory or you have a typo when you are referencing that particular resource using its path.

Another common problem is not being able to render the templates even though you're directory structure is correct. Make sure your app is registered in settings.py otherwise it may not render.