Skip to content

StrongMind/cohort_template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cohorts Template

Session 1

Prerequisites

  • System with git installed (Tested with Mac and Windows. Should work happily on Linux).
  • A GitHub user account.
  • Editor or IDE (PyCharm recommended, either professional or community) capable of working with Python files. I recommend you use the professional trial version of PyCharm, and our instructions will assume that. You will need to self-adapt the instructions if you use something different.
  • Pop in order to pair with other members if not local.

Setup

In GitHub, go to the template repository for this class, and clone into your personal GitHub by selecting "Use This Template".

In a terminal window:

  • git clone <your repo url here>

    • where "your repo url here" is the URL of the cloned repository in your account change directory to where you cloned it.
  • Open the project in PyCharm by opening the project folder inside of it.

  • go to your project

  • go to "Python interpreter"

  • click the settings icon next to your interpreter

  • click add

  • select "Docker Compose" in the left of the dialog

  • select the new button next to server

    • choose Docker for Mac/Windows
  • choose "web" in service and then click "OK"

In WSL2 or a terminal:

  • To get a bash prompt inside your docker environment:
    • docker-compose run web bash
  • To start a new django project:
    • django-admin startproject cohort

Those two steps should have created a new project inside your repository named "cohort". In the future, when we talk about "from a docker bash prompt", you will use docker-compose run web bash to get there.

In PyCharm, right click on the cohort folder, then Mark directory as sources root.

Finally, right click on docker-compose.yml and click "run docker-compose.yml". (If you decided not to use PyCharm, you would run docker-compose up here.)

In a browser, go to http://localhost:8000. You should now see a welcome page. You now have a django app successfully running.

Create an app

A django project consists of applications. These can either be applications you create yourself, or applications installed from Python packages available on PyPI (the python package repository). Reusable applications are a quick way to get significant chunks of functionality very quickly, as we will show you later.

For now, though, let's start our first application!

From a docker bash prompt:

  • cd cohort
  • python manage.py startapp forum this command creates an application in the project named "forum".

In cohort/settings.py, you will see an INSTALLED_APPS section. This controls which applications a django project has installed. You can see several applications already installed that come as a standard part of django. Let's install our newly created application into this list, by adding "forum" to this list.

Set Up Tests

This project comes set up in advance to use pytest-describe. From a docker bash prompt in the cohort project directory, run pytest. You should see similar output to this.

root@f861c41270fe:/code/cohort# pytest
================================================================================ test session starts ================================================================================
platform linux -- Python 3.10.3, pytest-7.1.1, pluggy-1.0.0
rootdir: /code/cohort
plugins: Faker-13.3.2, django-4.5.2, describe-2.0.1
collected 0 items                                                                                                                                                                   

=============================================================================== no tests ran in 0.06s ===============================================================================
root@f861c41270fe:/code/cohort# 

Our first model

We're going to create a discussion system. When creating a system like this, it helps to start "model out"; this way, we can already know what the APIs of our dependencies look like. Here's what our first model will look like. It will have an established relationship with a model that already exists by default in django, the user model.

erDiagram
	User ||--o{ Topic : creates
	Topic {
		string title
		datetime created_at
		User created_by
	}
	User {
	
	}
Loading

Tests

Rename the file tests.py to test_topic.py.

Create a file in your cohort project directory called pytest.ini that contains the following:

[pytest]
DJANGO_SETTINGS_MODULE=cohort.settings

If you are using PyCharm, you can automatically run tests whenever changes are made. This reduces feedback time significantly. To do this:

  • Go to Run -> Edit Configurations
  • Click the + in the top left of the dialog
  • Choose pytest within Python tests
  • Set the target to "script path" and in the path box below, select the cohort subdirectory in your project.
  • Save this by pressing okay, then selecting Run -> Run pytest in cohort
  • Test runs will appear at the bottom of you window; to turn on auto-test, select the icon to the left two below the green run icon.

Let's set up your first couple of tests working toward building this system. Copy and paste the following into test_topic.py

import factory.django
import pytest
from django.contrib.auth.models import User

from forum.models import Topic


class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    email = factory.Faker('email')
    username = factory.Faker('user_name')


class TopicFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Topic

    title = factory.Faker('sentence')
    created_by = factory.SubFactory(UserFactory)


@pytest.mark.django_db
def describe_topic():
    def exists():
        TopicFactory()

    def saves_its_fields():
        topic = TopicFactory()

        sut = Topic.objects.get(pk=topic.id)

        assert sut.title == topic.title
        assert sut.created_by == topic.created_by

Now run the tests; if you're in a taught session, this is where we'll start. Otherwise, follow the errors!

Answering the tests

In the last section, we set up a project and created a few failing tests. In this session, we're first going to answer those tests and set up a few pieces of functionality.

Set up models

Our model looked like the following:

erDiagram
	User ||--o{ Topic : creates
	Topic {
		string title
		datetime created_at
		User created_by
	}
	User {
	
	}
Loading

The tests that we have set up expect you to create a Topic model. A model is a representation of a business object as a class, which in the case of most MVC frameworks is mapped to a database table using an ORM.

Update your two respective files to match the following linked files:

  • test_topic.py
  • requirements.txt
    • This is to fix an oversight where we forgot to set a test to check for created_at. You may need to rebuild your dependencies using docker-compose build.

Take a look at the django models documentation and django migrations documentation.

Try to respond to the test failures using what you've read about in the documents above. Ask lots of questions!

Solution

You should end up with a models file that looks a bit like this and a few migrations files that pass your tests.

Authentication and Admin

Add the following test to your pre-existing topic tests:

    def is_registered_in_the_django_admin():
        from django.contrib.admin.sites import site as admin_site
        assert admin_site.is_registered(Topic)

This will be inside the describe_topic definition. How do you think we could make this test pass?

Solution

You should end up with a file that look like this. This will now mean that you have activated Django's admin interface for this model.

Add a super user

Now that you have an admin interface, you'll need an administrative user.

cohort_template % docker-compose run web bash
root@4731f4adc56f:/code# cd cohort/
root@4731f4adc56f:/code/cohort# python manage.py createsuperuser
Username (leave blank to use 'root'): admin
Email address: [email protected]
Password: 
Password (again): 
Superuser created successfully.

Use the admin interface

You may need to run python manage.py migrate in your web bash shell before this.

Now that you have your admin interface configured, if your web server is running you should be able to browse to your local administration interface. Log in and take a look at how much functionality you get by using reusable apps. The admin app is included with django, along with some other applications, and there are lots of other applications that you can trivially install that do things like revision control for your database records, add significant debugging capabilities, automatically create documented REST APIs, provide full featured registration for your application, provide full featured full text search for your content, make your application an LTI tool provider and many more things.

TDD

Let's expand the system so we can there are posts on a topic! You'll do this work in pairs, so open up pop and find a pair.

erDiagram
    User ||--o{ Post : creates
    Post ||--o{ Topic : "belongs to"
    Post {
        text comment
        datetime created_at
        User created_by
        Topic topic
    }
	User ||--o{ Topic : creates
	Topic {
		string title
		datetime created_at
		User created_by
	}
	User {
	
	}
Loading

Decomposition

In the diagram above, we describe the links between a Post, a Topic and a User. Normally, at the start of every pairing session, you would make sure to decompose your story or task into small chunks and design the system so that you have a plan. For this model, we're going to do this for you, and you can concentrate on the TDD workflow.

Ping Pong Pairing

We're going to use a pairing and TDD workflow often called Ping Pong Programming. This look a bit like this.

graph TD
	Red1[Alice Writes a test]
	Red1 --> Green1
	Green1[Bob Writes smallest increment possible to pass test]
	Green1 --> Refactor1
	Refactor1[Bob and Alice refactor if necessary]
	Refactor1 --> Red2
	Red2[Bob writes a test]
	Red2 --> Green2
	Green2[Alice Writes smallest increment possible to pass test]
	Green2 --> Refactor2
	Refactor2[Alice and Bob refactor if necessary]
	Refactor2 --> Red1
Loading

The TDD cycle is often talked about as:

  • Red (write a test, which in most test suites turns "Red" to reflect failure)
  • Green (respond to a test, which in most test suites turns "Green" to reflect success)
  • Refactor (improve current implementation to ensure the implementation can be understood, whilst adding no functionality)

If you look at your current test_topic.py file and models.py file, you'll see some steps emerging.

  • Ensure a model exists at all
  • Add fields to the model
  • Register the model in admin

Let's try using a ping pong TDD flow to create a Post model. Ask lots of questions along the way!

When you're done, ask these questions in your pair.

  • Did you always follow the red-green-refactor cycle?
    • If not, what made that difficult?
    • What could you do to make those things easier?
  • Are there ways you could change the code and that changes the material functionality that you just created without changing the tests?
    • How do you know how safe your tests are when you create them?
  • If you were an engineer looking at this code for the first time, how would you understand why the tests and the code exists?
  • If you were an engineer removing functionality from this application, how would you know how to safely do that?
  • If you were an engineer adding functionality to this application, how confident would you be that your new functionality did not break existing functionality?
  • What was good about this pairing style?
  • What was challenging about this pairing style?

Adding end user functionality

We now have an admin interface and models to represent our Topics and Posts within discussion board. Let's get users posting directly to them!

Note: Django uses a style of framework that they call MTV; which stands for "Models, Templates and Views". This is a slightly different interpretation of the MVC pattern. Roughly, a model is equivalent with MVC frameworks, a template is equivalent to a View and a VIew is equivalent to a controller, other than that some parts of the "controller" are automatically handled by the framework. For the sake of this document, MVC and MTV are close enough to use them interchangably.

We're going to create 4 views, tests and corresponding templates, with the following specification, where "1" is the primary key of the topic model.

Topic List View

You already have the required models to do that! Let's do the topic list view first. Add the following lines of code to test_topic_views.py in your forum folder.

@pytest.mark.django_db
def describe_a_topic_list_view():
    @pytest.fixture
    def view_class():
        return TopicListView()

    @pytest.fixture
    def view():
        return TopicListView.as_view()

    @pytest.fixture
    def http_request():
        return RequestFactory().get("/topics/")

    def it_exists():
        assert TopicListView()

    def describe_with_no_topics():
        def it_returns_a_404_response(http_request):
            with pytest.raises(Http404):
                TopicListView.as_view()(http_request)

        def it_responds_with_a_404_from_its_url(client):
            response = client.get("/topics/")
            assert response.status_code == 404

    def describe_with_topics():
        @pytest.fixture
        def topics():
            return TopicFactory.create_batch(5)

        def it_returns_a_200_response(http_request, topics):
            response = TopicListView.as_view()(http_request)
            assert response.status_code == 200

        def it_returns_topics_in_our_template_context(http_request, topics, view_class):
            view_class.setup(http_request)
            view_class.get(http_request)

            context = view_class.get_context_data()

            assert list(context['object_list']) == topics

        def it_responds_with_a_200_from_its_url(client, topics):
            response = client.get("/topics/")
            assert response.status_code == 200
            assert list(response.context_data['object_list']) == topics

Fulfill the above tests:

Topic Detail View

Now that we've done the topic list view, how would you make a similar set of tests and (optionally) TDD a topic detail view? Go ahead and do that, and let's talk about how that went afterwards.

Topic and Post creation views

Change your topic_view_test to the following. You can use the Django Edit Generic View documentation to find some ways to respond to these tests.

import pytest
from django.forms import CharField
from django.http import Http404
from django.test import RequestFactory

from forum.models import Topic
from forum.test_topic import TopicFactory, UserFactory
from forum.views import TopicListView, TopicCreateView


@pytest.mark.django_db
def describe_a_topic_list_view():
    @pytest.fixture
    def view_class():
        return TopicListView()

    @pytest.fixture
    def view():
        return TopicListView.as_view()

    @pytest.fixture
    def http_request():
        return RequestFactory().get("/topics/")

    def it_exists():
        assert TopicListView()

    def describe_with_no_topics():
        def it_returns_a_404_response(http_request):
            with pytest.raises(Http404):
                TopicListView.as_view()(http_request)

        def it_responds_with_a_404_from_its_url(client):
            response = client.get("/topics/")
            assert response.status_code == 404

    def describe_with_topics():
        @pytest.fixture
        def topics():
            return TopicFactory.create_batch(5)

        def it_returns_a_200_response(http_request, topics):
            response = TopicListView.as_view()(http_request)
            assert response.status_code == 200

        def it_returns_topics_in_our_template_context(http_request, topics, view_class):
            view_class.setup(http_request)
            view_class.get(http_request)

            context = view_class.get_context_data()

            assert list(context['object_list']) == topics

        def it_responds_with_a_200_from_its_url(client, topics):
            response = client.get("/topics/")
            assert response.status_code == 200
            assert list(response.context_data['object_list']) == topics


@pytest.mark.django_db
def describe_a_topic_creation_view():
    @pytest.fixture
    def user():
        return UserFactory.create()

    @pytest.fixture
    def view_class():
        return TopicCreateView()

    @pytest.fixture
    def view():
        return TopicCreateView.as_view()

    def describe_get():
        @pytest.fixture
        def http_request():
            return RequestFactory().get("/topics/create/")

        def it_returns_a_200_response(http_request):
            response = TopicCreateView.as_view()(http_request)
            assert response.status_code == 200

        def it_responds_with_a_200_from_its_url(client):
            response = client.get("/topics/create/")
            assert response.status_code == 200

        def it_responds_with_a_form_for_the_topic(client, view_class):
            response = client.get("/topics/create/")
            form = response.context_data['form']
            assert isinstance(form.fields['title'], CharField)

    def describe_post():
        def describe_with_a_valid_form():
            @pytest.fixture
            def http_request(user):
                unsaved_topic = TopicFactory.build()
                request = RequestFactory().post("/topics/create/", {
                    "title": unsaved_topic.title,
                })
                request.user = user
                return request

            def it_creates_a_topic_with_the_user_recorded(http_request, view, user):
                response = view(http_request)
                topic = Topic.objects.get(title=http_request.POST['title'])
                assert topic.created_by == user
                assert response.status_code == 302
                assert response.url == topic.get_absolute_url()

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published