Chalmers Advanced Python

Lab 3: A web application for tram networks

Advanced Python Course
Chalmers DAT690 / DIT516 / DAT516
2026

by Aarne Ranta & John J. Camilleri

The Gothenburg tram network image is based on Västtrafik’s map of trams, trunk buses and boats.

Purpose

The purpose of this lab is to build a web application replicating some functionalities of apps such as Västtrafik’s Travel Planner. Your application will display a map of tram lines and highlight the shortest paths between two stops, in terms of both time and geographical distance, taking changes into account.

Here is an example screenshot showing the tram map with the quickest and shortest routes highlighted from stops Chalmers to Komettorget:

Gothenburg tram map showing shortest paths between Chalmers and Komettorget

Unlike the official app, ours will not have access to the actual timetables, but just to the distances and times as defined in Labs 1 and 2. This is of course a severe simplification, but on the other hand, our app will be usable for any transport system that can be represented by the class TramNetwork.

The learning outcomes include:

Requirements

R1: core functionality

For this lab you must develop a web application with the following functionality:

  1. display the complete map of all tram lines
  2. allow the user to input a source and destination tram stop, with basic error handling
  3. highlight on the map the shortest path in terms of time, taking changes into account
  4. highlight on the map the shortest path in terms of geographical distance, taking changes into account

This document covers step-by-step instructions for how to build a web application from scratch using Django which fulfills requirements 1-4 above. Note that using Django is not a requirement. You are allowed to use other Python frameworks, as long as your application meets these functional requirements.

R2: additional features

In addition to the core functionality described above, you are also required to add at least one additional feature to your application. What this additional feature will be is completely up to you. See Task 3 below for some suggestions.

AI Policy

For this lab only, there are no restrictions on AI usage. You are allowed to use any tools you wish, as long as:

  1. Your web application works correctly and implements all the core requirements above (R1). Note that your backend must be implemented in Python.
  2. Your application contains at least one additional feature (R2).
  3. You can navigate your application’s source code and explain how it works.
  4. You document your choices, including which tools/models were used, examples of prompts, integrations used, estimated costs, and can reflect on the process afterwards.

Using AI is not a requirement, and it is entirely possible to complete this lab, together with additional features, without any AI at all. Whether to use AI or not is up to you, and whatever you choose to do, you should be ready to reflect on it during the peer review.

Submission & peer review

  1. The source code for your entire application should be submitted by pushing it to GitLab, being careful to avoid committing unnecessary files (see more details at the end of this document).
  2. In addition, every group must participate in an obligatory peer review session, where you will:
    • demonstrate your application in use
    • show what additional features were implemented, and describe how they were implemented
    • describe and reflect on your development process, in particular AI usage
    • answer questions about your application/process from peers and TAs
    • ask questions about other groups’ projects

Task 0: Getting started

We will follow the standard workflow for the Django framework. There are several tutorials available, for instance:

You can look at these for more information, but this document is aimed to be self-contained and sufficient.

Directory structure

We will start from a blank slate and build up the project’s directory structure step-by-step. The final structure is shown below. The files are obtained in various ways:

lab3
├── .gitignore 🟠
├── db.sqlite3
├── manage.py
├── mysite
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py 🟡
│   └── wsgi.py
├── venv
│   └── ...
├── static
│   └── tramnetwork.json ⚪️
└── tram
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── forms.py 🟠
    ├── migrations
    │   └── __init__.py
    ├── models.py 🟠
    ├── templates
    │   └── tram
    │      ├── find_route.html 🔵
    │      ├── home.html 🔵
    │      ├── images
    │      │   ├── tramlines_map.svg 🔵
    │      │   └── generated
    │      │       └── ... ⚪️
    │      └── show_route.html 🔵
    ├── tests.py
    ├── urls.py 🟠
    ├── utils
    │   ├── __init__.py 🔴
    │   ├── color_tramlines_svg.py 🔵
    │   ├── graphs.py 🔴
    │   ├── trams.py 🔴
    │   └── tramviz.py 🔴
    └── views.py 🔵

Create Django project

Now we will prepare our environment, install the Django library, and initialize our Django project.

  1. Clone the repository this project from GitLab onto your computer (replace group-0 with your own group number):

     $ git clone git@git.chalmers.se:courses/advanced-python/2026/lab-3/lab3-group-0.git
    
  2. Move inside it:

     $ cd lab3-group-0
    
  3. Create a Python virtual environment:

     $ python3 -m venv venv
    

    This will create the directory venv with lots of things in it, but you don’t need to concern yourselves with its contents.

  4. Activate the virtual environment:
    • on Linux/Mac:

      $ source venv/bin/activate
      
    • on Windows:

      $ venv\Scripts\activate.bat
      

      or

      $ venv\Scripts\activate.ps1
      

      Which of these two commands will work depends on what shell you are using. If unsure, try both.

    You should now see the string (venv) prefixed to your command line prompt something like this:

     (venv) $
    

    This will be assumed for the rest of this document.

  5. Install the necessary Python libraries (specified in the file requirements.txt) into the virtual environment:

     $ pip install -r requirements.txt
    
  6. Create a new Django project with:

     $ django-admin startproject mysite .
    

    The last dot . is necessary: it refers to your working directory, where it creates a directory named mysite and the file manage.py.

The next time you resume working on the project in a new terminal, you only need the activate step (4).

Initialize database

This step is needed to initialize the database:

$ python manage.py migrate

It creates a database (in the file db.sqlite3), which is a standard part of any Django project, even if we don’t use it in this lab.

Run web server

Now we can start the web server locally with:

$ python manage.py runserver
...
Starting development server at http://127.0.0.1:8000/
...

127.0.0.1 is the IP address associated with your own computer, also called localhost. Open the URL above in a web browser to check if the installation succeeded. You should see a generic Django-generated page, which tells you one important thing: that your server is up and running.

Task 1: Building the Django “app”

Now that we have a basic Django server set up, it’s time to start customising it for our task. For this we will create a Django “app” with the name tram (not to be confused with the entire web application itself):

$ python manage.py startapp tram

This creates the directory tram, with a lot of predefined contents, but also many things that you will have to complete with your own code.

To recognize this file in your Django website, add the line:

'tram.apps.TramConfig',

to the end of the INSTALLED_APPS list in mysite/settings.py.

Create model

Create a data model for route searches in tram/models.py:

from django.db import models

class Route(models.Model):
    dep = models.CharField(max_length=200)
    dest = models.CharField(max_length=200)

    def __str__(self):
        return self.dep + '-' + self.dest

Each instance of our model will contain a departure and destination stop, stored as strings. Then migrate the model to the database:

$ python manage.py makemigrations tram
$ python manage.py migrate tram

You will see that your db.sqlite3 file has contents now, but they are in a binary format that you cannot read.

Note: In this lab, the application does not add any data to the database, but its schema (the Route class) is used to structure the queries made via the route search form. This is known as the MVT Design Pattern (Model-View-Template):

The database could also be manipulated in the SQL language, but one of the things Django does is generate SQL queries from Python code, so that you usually don’t need to use SQL directly. This is known as ORM, Object-Relational Mapping.

Update URL patterns

Edit the generated mysite/urls.py so that it contains the following:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('tram.urls')),
]

The admin URL is used for managing the website and requires a login. You can try and create users and passwords, but this is not needed in this lab.

The second path includes the URLs given in our tram app. For this purpose, you have to create the file tram/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.tram_net),
    path('route/', views.find_route),
]

Each path has two required arguments:

Create views

In order for tram/urls.py to work, you have to edit tram/views.py, so that it contains the following:

from django.shortcuts import render
from .forms import RouteForm

def tram_net(request):
    return render(request, 'tram/home.html', {})

def find_route(request):
    form = RouteForm()
    return render(request, 'tram/find_route.html', {'form': form})

The former function is all we need to render the start page. The latter function creates a web form, but does not yet do anything with it; we will return to this later.

Of course, we also need to define the RouteForm class and the HTML files. This is the next topic.

Create form

In order for the find_route view to work, we need to create a corresponding form in tram/forms.py:

from django import forms
from .models import Route

class RouteForm(forms.ModelForm):
    class Meta:
        model = Route
        fields = ('dep', 'dest',)

This new form RouteForm is linked to our data model Route. We specifying which fields of the model should be present in the form, and Django will automatically create a web form for us.

Create templates

The HTML files to be created are actually templates, since they contain slots where dynamic data is inserted by the server. The templates reside in a sub-sub-sub-directory, which has to be created first:

$ mkdir tram/templates
$ mkdir tram/templates/tram

Copy the HTML template files from the files folder to the newly created tram/templates/tram, so that:

$ ls tram/templates/tram/
find_route.html    home.html   show_route.html

Create template images

Also create the images subdirectory:

$ mkdir tram/templates/tram/images

and copy the tram network image tramlines_map.svg into it.

Note that images in this folder can be used in templates (e.g. home.html) but are not served publicly by the web server; i.e. you cannot access it via a web browser using something like http://127.0.0.1:8000/images/tramlines_map.svg.

Now run the web server again. You will see a home screen with the gorgeous SVG image of Gothenburg tram network.

Dynamic images

Our application will dynamically generate a new image for each route that is requested. Create a subdirectory for these dynamically generated images:

$ mkdir tram/templates/tram/images/generated

To avoid adding any of these generated images to the repository, tell Git to ignore this folder by creating a .gitignore file in the top-level folder and add this line to it:

tram/templates/tram/images/generated/

Render dynamic content

The form find_route.html does not find any routes yet. You can submit queries, but when you press “Search”, the form just becomes empty without showing any result.

So now we want to add some basic functionality that actually shows the shortest path. The following things are needed:

  1. a “utility” function that actually calculates the shortest path (from Lab 2)
  2. an extended find_route() function in tram/views.py (to be copied from files/views.py)
  3. a template that show_route.html shows the route that is found (already copied above)

Add utility functions

It is a good practice to create a separate directory for “non-Django” utility functions and create an empty __init__.py file in it, so that it is recognized as a package.

$ mkdir tram/utils
$ touch tram/utils/__init__.py

Copy the following Python files from files into tram/utils:

Update views

Now that you have created the utility files, you can replace the simplified tram/views.py with the one given in files.

Now you can visit http://127.0.0.1:8000/route/, submit a query and get a response (although this is just using a dummy implementation for now).

Task 2: Implementing the shortest path functionality

It is now time to implement the core function in our app: calculating the shortest path between two stops.

Most of this work is to be done in the files in tram/utils. They contain TODO comments that instruct you what to do.

The main function in the file tram/utils/tramviz.py, imported in tram/views.py, is show_shortest. Its task is to:

Add colours

We expect three different colours to be used (see image at the top of this document):

You can also use some other colours if you prefer. Other stops should be left white. The default implementation copied from files/tramviz.py is a mock-up, which always shows the same colours and the same route.

You can of course also makes the HTML files look nicer if you have time!

Account for line changes

In the Lab 2 shortest path, we ignored the effect of changing from one line to the other. This effect is a major factor that can make the “shortest time” and “shortest distance” differ significantly. Its implementation requires that we recognize when a change must be made and add a suitable number of minutes or meters to the cost.

One way to do this with the existing algorithms is to build a new graph for the network, where:

Implement the specialize_stops_to_lines() function which builds such a graph. You can then run the Dijkstra algorithm with cost functions specialized_transition_time() and specialized_geo_distance() for finding the quickest and shortest paths respectively. You will need to figure out how to use stop names as individual strings with this new graph containing vertices as tuples.

Task 3: Adding data validation

You should now have a working application which returns the shortest routes between two stops. But what happens when you enter a stop name that doesn’t exist (e.g. you make a typo)? The Django server doesn’t crash, but you will see an ugly error page and the HTTP response from the server will have status code 500 Internal Server Error.

Instead, we should gracefully handle incorrect user input by returning a more appropriate HTTP status such as 400 Bad Request and showing the user a suitable message.

Edit the find_route view in your trams/views.py file to check for incorrect input and behave accordingly. You can use the following, but the details are up to you:

from django.http import HttpResponseBadRequest
...
return HttpResponseBadRequest(f"Unknown stop name: {stop}")

Task 4: Additional features

Finally, for R2 you should add at least one custom feature of your own to the application. This is up to you, but some ideas include:

  1. Improving the visual styles of the application, including making it work well at narrow screen sizes.
  2. Adding drop-downs or other user-friendly ways for selecting tram stops.
  3. Allowing the changeover time and distance to be adjusted by the user.
  4. Storing a history of past searches.
  5. Having a “favourite routes” functionality.
  6. Anything else you can think of! (within reason)

Whatever features you add, be ready to show them and explain how they work in the peer review session.

Submission

Submit all the files that are needed to run your application by committing them to your repository, but take care to avoid committing automatically generated files, in particular:

  1. the dynamic images in tram/templates/tram/images/generated/
  2. your local Python virtual environment venv/
  3. cache folders such as __pycache__/ which can be automatically created by your IDE

You should use a .gitignore file to avoid committing such paths to the repository. The .gitignore file itself should be committed to the repository.

Peer review

Apart from submitting your code, the examination of this lab also includes participation a mandatory peer review session. More details about peer reviewing