Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Test-Driven Development with Django
PyMeister
PyMeister

Posted on

     

Test-Driven Development with Django

Initializing the Test Project

Open your terminal and navigate to the directory that contains your Django projects. On my computer, that folder is called Projects.

cdProjects
Enter fullscreen modeExit fullscreen mode

Create a new Django project,test-driven, using thedjango-admin.py utility, and change directories to the newly generated folder.

django-admin.py startproject test-driven&&cdtest-driven
Enter fullscreen modeExit fullscreen mode

Create a new virtual environment using thevirtualenvwrapper utility.

mkvirtualenv testdriven
Enter fullscreen modeExit fullscreen mode

or Create a new virtual environment using thevenv utility.

python-m venvenv
Enter fullscreen modeExit fullscreen mode

Start PyCharm and click File > Open in the menu bar. Find and select thetest-driven project folder. When prompted, choose to open the project in a new window. Wait for the project to open and then click PyCharm > Preferences > Project Interpreter in the menu bar to open the Project Interpreter panel. Click the gear icon in the upper right corner of the panel; then, select the "Add local..." option. In file explorer, navigate to your virtual environments folder, find thetest-driven directory, and select thepython2.7 file in thebin folder. On my machine, this file is located at~/Virtualenvs/test-driven/bin. In PyCharm, close the Preferences window. The virtual environment you just added is now being used by PyCharm for thetest-driven project.

Now that we have installed our virtual environment, we must add some dependencies to our project. First, install Django.

pipinstalldjango
Enter fullscreen modeExit fullscreen mode

Return to PyCharm and run the Django project by selecting Run from the menu bar. PyCharm should start the development server and log the progress in a terminal window. Click the link that PyCharm provides and your browser should open to a default Django page that reads "It worked!"

Next, install the Psycopg Python database adapter, so that we can use PostgreSQL as our database.

pipinstallpsycopg2
Enter fullscreen modeExit fullscreen mode

In PyCharm, open thesettings.py file in thetest-driven app directory. Change theDATABASES attribute to use PostgreSQL instead of SQLite.

# test-driven/settings.pyDATABASES={'default':{'ENGINE':'django.db.backends.postgresql_psycopg2','NAME':'test-driven','USER':'admin','PASSWORD':'password1','HOST':'localhost','PORT':'5432'}}
Enter fullscreen modeExit fullscreen mode

Start the pgAdmin III program, and create a new database calledtestdriven. In the window that appears, entertest-driven in the name field andadmin in the owner field. Close the window and then find the newly created database. Notice it does not have any tables yet.

Return to PyCharm and delete thedb.sqlite3 file that was created when you ran the Django project initially. Migrate the database to create the default Django tables.

python manage.py migrate
Enter fullscreen modeExit fullscreen mode

If you examine your database in pgAdmin III, you should see 10 new tables. At this point, we are ready to start tracking our file changes using Git. Initialize a Git repository and commit all of the file changes that we have done up to this point.

git initgit add.git commit-m"Initial commit."
Enter fullscreen modeExit fullscreen mode

Beginning the Test-Driven Development Process

In general, the test-driven development workflow follows these steps:

  1. Write a short story that captures what a user needs to complete a requirement.
  2. Wireframe a simple model of an application or feature that meets the requirement.
  3. Write a use case that narrates a scenario based on the wireframe.
  4. Write a functional test that follows the actions outlined in the use case.
  5. Write unit tests to evaluate the operability of low-level code.
  6. Write functional code that satisfies the tests.

These are the critical tenets of test-driven development:

  • Always write a failing test before you program any functional code.
  • Write theminimum amount of code necessary to make a test pass.
  • When a test passes, restart the process or refactor the code if necessary.

Writing a User Story

Conventionally, the user story should be short enough to be written on a notecard. It is written in the language of the client and avoids the use of technical vocabulary. Here is our user story:

John wants an application that allows him to manage a list of organizations.

Wireframing an Application

Our application will consist of two pages, ahome page that lists organizations and acreate page that generates new organizations. Sketch a simple layout of the application and make sure to include components and page states. See the attached document for an example of an application wireframe created with MockFlow.

Writing a Use Case

Use our wireframe to imagine how the typical user will interact with our application. Follow along with this outline of an expected user experience:

John goes to the home page. He sees a data table with a single cell that says "No organizations". He also sees a button labelled "Create organization". He clicks the create button. The page refreshes and John sees a form with a single text input control and a submit button. John enters an organization name and clicks the submit button. The page refreshes and John notices that the table now has a single row containing the details of the organization that he added.

Writing a Functional Test

A functional test (acceptance test) follows the scenario laid out in a user case. It evaluates the system from the user's point of view. Functional tests are helpful because they allow us to mimic the behavior of a real user, and they can be repeated consistently over time. Do not write exhaustive functional tests that touch every possible interaction and cover every single outcome that can occur within a system. Instead, focus on the important aspects of a user experience, and write tests that encompass the most popular and direct set of actions that a user is expected to follow. In practice, a separate quality assurance (QA) team should evaluate the application for bugs.

Leverage software like Selenium to drive the functional tests in Django. Selenium provides functionality that allows automated tests to interact with a browser in an authentic way. Examples include opening and closing browser windows, navigating to URLs, and interacting with on-screen components using mouse and keyboard events like a human user.

Implement functional tests by creating a new Python directory calledfunctional_tests in the test-driven project folder. Open the project'ssettings.py file and change theINSTALLED_APPS attribute as shown below.

# test-driven/settings.pyDEFAULT_APPS=('django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles',)LOCAL_APPS=('functional_tests',)INSTALLED_APPS=DEFAULT_APPS+LOCAL_APPS
Enter fullscreen modeExit fullscreen mode

Install the Selenium package in the terminal.

pipinstallselenium
Enter fullscreen modeExit fullscreen mode

Create a new Python file in thefunctional_tests folder and call ittest_organizations. Set up a simple test case.

# functional_tests/test_organizations.pyfromselenium.webdriver.firefox.webdriverimportWebDriverfromdjango.testimportLiveServerTestCaseclassOrganizationsTest(LiveServerTestCase):defsetUp(self):self.browser=WebDriver()self.browser.implicitly_wait(5)deftearDown(self):self.browser.quit()
Enter fullscreen modeExit fullscreen mode

In Django, functional tests extend thedjango.test.LiveServerTestCase class. Unlike the standardTestCase, theLiveServerTestCase starts a real development server that allows an automated test client like Selenium to operate within a browser. ThesetUp() method is run immediately before a test starts and thetearDown() method is run directly after the test completes. When run, each test will open a web browser, execute some behaviors, and then close the browser. Run the functional tests and pay attention to the output.

python manage.pytestfunctional_tests
Enter fullscreen modeExit fullscreen mode

The functional tests run successfully with no failures. In fact, we have not actually programmed any tests yet. Create a functional test that follows that actions narrated by the use case. In the example below, the use case is broken into digestible instructions that guide our development.

# functional_tests/test_organizations.pydeftest_new_organizations_are_shown_in_list(self):# John goes to the home page.self.browser.get(self.live_server_url)# He sees a data table with a single cell that says "No organizations".# He also sees a button labelled 'Create organization'. He clicks the create button.cell=self.browser.find_element_by_xpath('//table/tbody/tr/td')self.assertEqual(cell.text,'No organizations')create_button=self.browser.find_element_by_id('create-button')self.assertEqual(create_button.text,'Create organization')create_button.click()# The page refreshes and John sees a form with a single text input control and a submit button.name_input=self.browser.find_element_by_name('name')self.assertEqual(name_input.get_attribute('placeholder'),'Organization name')# John enters an organization name and clicks the submit button.name_input.send_keys('TDD Organization')submit_button=self.browser.find_element_by_id('submit')self.assertEqual(submit_button.text,'Submit')submit_button.click()# The page refreshes and John notices that the table now has a single row containing# the details of the organization that he added.row=self.browser.find_element_by_xpath('//table/tbody/tr')self.assertIn('TDD Organization',row.text)
Enter fullscreen modeExit fullscreen mode

Our functional test is pretty simple: 14 lines of code and 5 assertions. Every test method begins with the prefixtest_. Selenium provides some pretty useful methods to interact with the elements on the page. Walk through what is happening in the test:

  • The home page opens in the browser.
  • The browser searches the HTML content for a<td> element and checks to make sure it says "No organizations".
  • The browser searches the HTML content for a create button, then click its.
  • At this point, the client should navigate to a new page. When it refreshes, the browser checks the HTML content for a text input control and a submit button. It also confirms that the text field has a placeholder that says "Organization name".
  • The browser enters an organization name into the text field and clicks the submit button.
  • At this point, the client should return to the home page and the browser checks to make sure the table contains a row with the name of the organization added on the previous page.

Notice how this test combines the use case and the wireframe. Run the functional test again and see what happens.

python manage.pytestfunctional_tests
Enter fullscreen modeExit fullscreen mode

A web browser opens, waits 5 seconds, and then closes with a failure. This is good! In fact, tests should always initially fail. Notice that the test failed with the message "Unable to locate element". It cannot find the<td> element in the rendered HTML. You might have also observed that the web page itself could not be found when it opened in the browser. In order to address these failures we need to write our first unit test.

Writing a Unit Test

While a functional test mimics a real user interacting with an application, a unit test ensures that the functionality of the bits of code themselves work as expected. Our web application failed to load because we don't have anything to load yet. We're missing the fundamental elements of a Django web application: URLs, views, and templates.

Let's create a new Django app calledorganizations. We're creating a new app instead of using the default project app in reaction to the guidelines laid out by the creators of Django. The idea is that Django apps should be autonomous from the project, so that they can be packaged and distributed easily as individual modules.

python manage.py startapp organizations
Enter fullscreen modeExit fullscreen mode

You'll notice that your Django project is updated in PyCharm. Open your projectsettings.py and addorganizations as an installed local app. This action configures and registers the module with Django, so that the URLs, views, forms, models, etc. are accessible throughout the project.

# test-driven/settings.pyLOCAL_APPS=('functional_tests','organizations',)
Enter fullscreen modeExit fullscreen mode

Inside theorganizations folder, delete thetests.py file and create a Python directory called tests. Create a new Python file calledtest_views and place it in thetests folder. This is where our unit tests will live. Add the code shown below to create a unit test class for our views.

# organizations/tests/test_views.pyfromdjango.testimportClientfromdjango.testimportTestCaseclassHomeViewTest(TestCase):defsetUp(self):self.client=Client()
Enter fullscreen modeExit fullscreen mode

This class creates a test case and sets up each test method to use a fake client that will interact with the framework as if it were real. Remember, this is the basic way that Django works:

  1. A web client sends an HTTP request to the server.
  2. The server passes the request to the Django web framework.
  3. Django parses the URL from the request and resolves it to an associated view method.
  4. The view processes some code that usually involves communicating with a database and then returns an HTTP response.
  5. The response typically contains a string of HTML text, which is usually rendered from a template.
  6. The web client receives the response and displays the content as a web page.

Let's test out our new unit test.

python manage.pytestorganizations
Enter fullscreen modeExit fullscreen mode

Our tests are working.

Let's add some actual logic to our unit test. At this point, we can reasonably expect that a user should be shown a web page when he or she enters a URL into the browser. Thus, our first unit test is very straightforward.

# organizations/tests/test_views.pydeftest_view_renders_template(self):response=self.client.get('/')self.assertTemplateUsed(response,'organizations/home.html')
Enter fullscreen modeExit fullscreen mode

In this test, we are simulating a call to the home page and are confirming that we are rendering the expected template. When we run the unit tests, they fail. Remember that's a good thing! Now, let's write the minimum amount of code necessary to make this test pass. The first thing we need is a template. Create atemplates/organizations directory in your organizations folder. Create a simple HTML file in the new directory and name ithome.html.

<!-- organizations/templates/organizations/home.html --><!DOCTYPE html><html><headlang="en"><metacharset="utf-8"><title>Test-Driven</title></head><body></body></html>
Enter fullscreen modeExit fullscreen mode

Next, open theviews.py file from theorganizations folder. Add the minimum amount of code necessary to render the template.

# organizations/views.pydefhome_view(request):returnrender(request,'organizations/home.html')
Enter fullscreen modeExit fullscreen mode

Lastly, open theurls.py file in thetest-driven folder and adjust the URL configuration as shown.

# test-driven/urls.pyfromdjango.conf.urlsimportpatternsfromdjango.conf.urlsimportre_pathurlpatterns=patterns('organizations.views',re_path(r'^$','home_view',name='home'),)
Enter fullscreen modeExit fullscreen mode

Run the unit tests again. They pass! Let's try the functional tests again. They fail with the same error as before, but at least we're actually rendering the expected web page. This is a good time for a commit. Look at the status of the project and then add and commit all of our new and modified files.

git statusgit add.git commit-m"Added functional tests and organizations."
Enter fullscreen modeExit fullscreen mode

Exploring the Test-Driven Development Process

At this point, we've gotten a taste of how the basic flow of TDD works. We've created our functional test, which is driving the entire workflow. We've created our very first unit test in order to push through our functional test failures. The process is iterative: run the functional test until it breaks, create a failing unit test, write a small amount of code to make it pass, and repeat until no more errors exist. At this point, our functional test is failing because it cannot find a table cell. Let's remedy that by adding it to the template.

<body><table><tbody><tr><td>No organizations</td></tr></tbody></table></body>
Enter fullscreen modeExit fullscreen mode

Run the functional tests. They fail again, but notice that we have a new error! We've moved a step forward. Now, the create button cannot be found. Let's return to the template.

<body><aid="create-button"href="#">Create organization</a><table><tbody><tr><td>No organizations</td></tr></tbody></table></body>
Enter fullscreen modeExit fullscreen mode

Run the functional tests. We've progressed a little more! Now, the test cannot find the text input control, but if we look at the user story, we realize the test fails because the page never changes. If we look at our wireframe, we can see that we need a second page, the create page. Let's follow the same steps as when we created the home page. First, create a unit test. Notice how we use a new test case for a new view.

# organizations/tests/test_views.pyclassHomeViewTest(TestCase):...classCreateViewTest(TestCase):defsetUp(self):self.client=Client()deftest_view_renders_template(self):response=self.client.get('/create/')self.assertTemplateUsed(response,'organizations/create.html')
Enter fullscreen modeExit fullscreen mode

Next, we follow the same steps for creating the URL, view, and template as we did for the home page. Create a new HTML file in thetemplates/organizations folder calledcreate.html.

<!-- organizations/templates/organizations/create.html --><!DOCTYPE html><html><headlang="en"><metacharset="utf-8"><title>Test-Driven</title></head><body></body></html>
Enter fullscreen modeExit fullscreen mode

Create the other Django files.

# organizations/views.pydefhome_view(request):defcreate_view(request):returnrender(request,'organizations/create.html')
Enter fullscreen modeExit fullscreen mode
# test-driven/urls.pyurlpatterns=patterns('organizations.views',re_path(r'^$','home_view',name='home'),re_path(r'^create/$','create_view',name='create'),)
Enter fullscreen modeExit fullscreen mode

Run the unit tests. They passed! Now that we have a working web page, we can link to it in thehome.html template.

<!-- organizations/templates/organizations/create.html --><aid="create-button"href="{% url 'create' %}">Create organization</a>
Enter fullscreen modeExit fullscreen mode

Run the functional tests again. We can see the browser navigate to the create page, so we've passed one more hurdle. The tests fail because the text input field cannot be found. Let's add it.

<!-- organizations/templates/organizations/create.html --><inputtype="text"name="name"placeholder="Organization name">
Enter fullscreen modeExit fullscreen mode

Remember, we just want the minimum amount of code necessary to move forward in the test. Don't get ahead of yourself! The functional tests fail, but we've progressed one more step. We need the submit button.

<!-- organizations/templates/organizations/create.html --><buttonid="submit">Submit</button>
Enter fullscreen modeExit fullscreen mode

We've taken another step! Again the test is failing because it cannot find an element, but we know that the real reason is because the page hasn't navigated back home yet. Before we do anything else, let's commit our changes.

git add.git commit-m"Home and create pages rendering correctly."
Enter fullscreen modeExit fullscreen mode

We need our application to return to the home page after a post request. Let's create a new unit test.

# organizations/tests/test_views.pyclassCreateViewTest(TestCase):...deftest_view_redirects_home_on_post(self):response=self.client.post('/create/')self.assertRedirects(response,'/')
Enter fullscreen modeExit fullscreen mode

The unit test fails, so let's make it pass.

# organizations/views.pyfromdjango.core.urlresolversimportreversefromdjango.shortcutsimportredirect...defcreate_view(request):ifrequest.method=='POST':returnredirect(reverse('home'))returnrender(request,'organizations/create.html')
Enter fullscreen modeExit fullscreen mode

The unit test passes. The functional tests are still failing because we haven't actually added any posting behavior to our controls yet. Let's upgrade our template, so that it actually uses a form. Remember that all post calls need a CSRF token.

<!-- organizations/templates/organizations/create.html --><formaction="{% url 'create' %}"method="post">    {% csrf_token %}<inputtype="text"name="name"placeholder="Organization name"><buttonid="submit"type="submit">Submit</button></form>
Enter fullscreen modeExit fullscreen mode

That gets the functional test moving ahead to the next failure. The new organization we created should be displayed in the list, but its not. It's time for some more advanced testing. We need to employ the use of models if we want to save our organizations. We'll have to pull the name of an organization from the post data that comes in when the form is submitted. Next, we'll need to save the organization with the given name. Lastly, we'll have to supply the home page with a list of organizations. That's a tall order. Let's get started with some unit tests.

# organizations/tests/test_views.pyfrom..modelsimportOrganizationclassHomeViewTest(TestCase):...deftest_view_returns_organization_list(self):organization=Organization.objects.create(name='test')response=self.client.get('/')self.assertListEqual(response.context['organizations'],[organization])
Enter fullscreen modeExit fullscreen mode

PyCharm already warns us that it cannot find the model, but let's run the unit test anyway. Of course, we get an import error. Let's create the model. Open themodels.py file in theorganizations folder and add the following code.

# organization/models.pyfromdjango.dbimportmodelsclassOrganization(models.Model):name=models.CharField(max_length=250)
Enter fullscreen modeExit fullscreen mode

PyCharm stops complaining, but what happens when we run the unit test? We get a programming error! We need to add theOrganization model as a table to the database. Luckily, Django makes it really easy to do this via the terminal.

python manage.py makemigrations organizationspython manage.py migrate organizations
Enter fullscreen modeExit fullscreen mode

Our unit tests are still failing, but the programming error is taken care of. Let's make our test pass with minimal code.

# organizations/views.pyfrom.modelsimportOrganizationdefhome_view(request):returnrender(request,'organizations/home.html',{'organizations':list(Organization.objects.all())})
Enter fullscreen modeExit fullscreen mode

That gets our unit tests passing. Organizations are being passed to the home page, but they are not being saved yet. Let's write another unit test to our create view.

# organizations/tests/test_views.pyclassCreateViewTest(TestCase):...deftest_view_creates_organization_on_post(self):self.client.post('/create/',data={'name':'test'})self.assertEqual(Organization.objects.count(),1)organization=Organization.objects.last()self.assertEqual(organization.name,'test')
Enter fullscreen modeExit fullscreen mode

This test sends a post request to the create view with some data. The test then checks to make sure an organization is created and that it has the same name as the data sent. The unit test fails as expected. Let's write some code to make it pass.

# organizations/views.pydefcreate_view(request):ifrequest.method=='POST':name=request.POST.get('name','')Organization.objects.create(name=name)returnredirect(reverse('home'))returnrender(request,'organizations/create.html')
Enter fullscreen modeExit fullscreen mode

The unit tests pass. Let's try the functional tests. We're close, I can feel it. The functional test cannot find our organization, so we just need to adjust our home template.

<!-- organizations/templates/organizations/home.html --><table><tbody>        {% for organization in organizations %}<tr><td>{{ organization.name }}</td></tr>        {% empty %}<tr><td>No organizations</td></tr>        {% endfor %}</tbody></table>
Enter fullscreen modeExit fullscreen mode

Our functional tests pass! Everything works! Let's commit our changes.

git add.git commit-m"Added organization model. All tests passing."
Enter fullscreen modeExit fullscreen mode

Refactoring Our Code

Let's take a look at our website. Wow, it's functional, but it's really ugly. Also, even though its functional, it's not functioning the most efficiently. We should be using forms to handle the data transfers and the template rendering. Let's make this site look pretty and let's add forms.

Adding Forms

We want to utilize Django forms in our application. On the front-end, we want the forms to render the controls that we expect (the input text field). We need to pass an instance of the form to the template. We also want to replace the clunky code that is extracting the post data and manually saving the model with something smoother. Let's handle the view first.

# organizations/tests/test_views.pyfrom..formsimportOrganizationFormclassCreateViewTest(models.Model):...deftest_view_returns_organization_form(self):response=self.client.get('/create/')self.assertIsInstance(response.context['form'],OrganizationForm)
Enter fullscreen modeExit fullscreen mode

As expected, the unit tests fail with an import error. Let's create the form. Add a new Python fileforms.py to theorganizations folder and add the following code.

# organizations/forms.pyfromdjangoimportformsclassOrganizationForm(forms.Form):pass
Enter fullscreen modeExit fullscreen mode

The unit tests fail, but we don't get the import error. Add code to make the test pass.

# organizations/views.pyfrom.formsimportOrganizationFormdefcreate_view(request):...returnrender(request,'organizations/create.html',{'form':OrganizationForm()})
Enter fullscreen modeExit fullscreen mode

The unit test passes. Let's test the form to make sure that it renders the controls we want. Create a new Python file in theorganizations/tests directory and call ittest_forms.py. Add a test that checks to see if the text input control is present.

# organizations/tests/test_forms.pyfromdjango.testimportTestCasefrom..formsimportOrganizationFormclassOrganizationFormTest(TestCase):deftest_form_has_required_fields(self):form=OrganizationForm()self.assertIn('id="id_name"',form.as_p())
Enter fullscreen modeExit fullscreen mode

Run the unit tests and see that one fails. We need to make the form use theOrganization model. Adjust theOrganizationForm like the following.

# organizations/forms.pyfrom.modelsimportOrganizationclassOrganizationForm(forms.ModelForm):classMeta:model=Organization
Enter fullscreen modeExit fullscreen mode

The unit test passes, however, you might see a warning regardingModelForms. Fix the form to get rid of that warning.

# organizations/forms.pyclassOrganizationForm(forms.ModelForm):classMeta:model=Organizationfields=('name',)
Enter fullscreen modeExit fullscreen mode

Return to our view unit tests. We need to replace the current logic, so that the form handles all of the data transfers. We need to add a couple of tests, but first let's install a new Python library.

pipinstallmock
Enter fullscreen modeExit fullscreen mode

Let's add a couple new unit tests for the create view.

# organizations/tests/test_views.pyfrommockimportpatchfromdjango.testimportRequestFactoryfrom..viewsimportcreate_viewclassCreateViewTest(TestCase):defsetUp(self):self.factory=RequestFactory()...@patch('organizations.views.OrganizationForm')deftest_passes_post_data_to_form(self,mock_organization_form):request=self.factory.post('/create/',data={'name':'test'})create_view(request)mock_organization_form.assert_any_call(data=request.POST)@patch('organizations.views.OrganizationForm')deftest_saves_organization_for_valid_data(self,mock_organization_form):mock_form=mock_organization_form.return_valuemock_form.is_valid.return_value=Truemock_form.save.return_value=Nonerequest=self.factory.post('/create/',data={'name':'test'})create_view(request)self.assertTrue(mock_form.save.called)
Enter fullscreen modeExit fullscreen mode

Let's break down our changes. Notice that we are using a different strategy for these tests. Instead of using the Django client, we are using theRequestFactory to manufacture a DjangoHttpRequest and passing it to the view itself. In these new tests, our goal is not to test the functionality of the forms. We only want to test that the view is interacting with the form in the way it should. Our focus is on the view.

Our first test is confirming that the request data is being passed to the form. Before, we handled the data ourselves, so now we want to make sure that the form is actually being given the chance to handle it instead. We do this by temporarily overwriting the real for with a fake form. The patch function does this and passes the fake form object to unit test to use. The only thing we need to know is that the form in our view is being called with the post parameters.

Our second test requires a little more work. Again, we are mocking the form with a fake object, but this time, we have to add some fake functions to reflect the structure of the real form. When a form is used in a view, it has to validate the input and then save, in order to successfully create a new model object. In this unit test, we are mocking theis_valid() andsave() methods, so that they operate the way we expect them to. We are then checking to make sure that the save function is called on the form.

The unit tests fail as expected. Let's add the code to make them pass.

# organizations/views.pydefcreate_view(request):form=OrganizationForm()ifrequest.method=='POST':form=OrganizationForm(data=request.POST)ifform.is_valid():form.save()returnredirect(reverse('home'))returnrender(request,'organizations/create.html',{'form':form})
Enter fullscreen modeExit fullscreen mode

We adjust thecreate_view() so that an empty form is created on every request, and the post data is passed to the form on a post request. We also add functionality to check that the form is valid and then to save it. The unit tests pass. Our last step is to adjust the create template, so that is uses the Django form to render the controls. We replace the hard-coded HTML with the form context.

<!-- organizations/templates/organizations/create.html --><formaction="{% url 'create' %}"method="post">    {% csrf_token %}    {{ form.as_p }}<buttonid="submit"type="submit">Submit</button></form>
Enter fullscreen modeExit fullscreen mode

When we run the functional tests, we see that they fail. We need to add a placeholder attribute to the name field. Let's adjust our form.

# organizations/forms.pyclassOrganizationForm(forms.ModelForm):classMeta:model=Organizationfields=('name',)widgets={'name':forms.fields.TextInput(attrs={'placeholder':'Organization name'})}
Enter fullscreen modeExit fullscreen mode

Both the functional tests and the unit tests are passing. We've successfully implemented forms in our project! Let's commit our code.

git add.git commit-m"Replaced template and view code with forms."
Enter fullscreen modeExit fullscreen mode

Making the Application Look Pretty

Our code is now more efficient. Using forms allows us to avoid having to edit the template and the view every time we add more fields to the form. The code is solid, but the application still looks ugly. Let's spruce it up using Bootstrap components and styles. While we're at it, let's make a base template that the home and create templates can inherit from, so we avoid duplicate code. Create a new HTML file in thetemplates/organizations folder and name itbase.html.

<!-- organizations/templates/organizations/base.html --><!DOCTYPE html><html><headlang="en"><metacharset="utf-8"><title>Test-Driven</title><metaname="viewport"content="user-scalable=no, width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"><linkrel="stylesheet"href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"><linkrel="stylesheet"href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.2/yeti/bootstrap.min.css"></head><body><divclass="container">        {% block page-content %}{% endblock page-content %}</div><scriptsrc="//code.jquery.com/jquery-2.1.3.min.js"></script><scriptsrc="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script></body></html>
Enter fullscreen modeExit fullscreen mode

We've added a<meta> element to control for the scaling that happens when a mobile device attempts to render a full page in the viewport. We've imported Bootstrap CSS and JS files and a jQuery dependency, and we've also opted to use a free Bootstrap theme to make our page look less generic. Within the body of the HTML, we've added a block template tag that can be overridden by templates that extend this base. Let's adjust the other templates.

<!-- organizations/templates/organizations/home.html -->{% extends 'organizations/base.html' %}{% block page-content %}<divclass="row"><divclass="col-md-offset-3 col-md-6"><divclass="panel panel-default"><divclass="panel-heading"><h4class="panel-title">Organizations</h4></div><divclass="panel-body"><aid="create-button"class="btn btn-default"href="{% url 'create' %}">                        Create organization</a></div><tableclass="table table-striped"><thead><tr><td><strong>Name</strong></td></tr></thead><tbody>                        {% for organization in organizations %}<tr><td>{{ organization.name }}</td></tr>                        {% empty %}<tr><td>No organizations</td></tr>                        {% endfor %}</tbody></table></div></div></div>{% endblock page-content %}
Enter fullscreen modeExit fullscreen mode

We spruce up the home page so that everything fits in a nice panel in the center of the screen. The table has a more interesting look with the striped style.

<!-- organizations/templates/organizations/create.html -->{% extends 'organizations/base.html' %}{% block page-content %}<divclass="row"><divclass="col-md-offset-3 col-md-6"><divclass="panel panel-default"><divclass="panel-heading"><h4class="panel-title">Create an organization</h4></div><divclass="panel-body"><formaction="{% url 'create' %}"method="post">                        {% csrf_token %}                        {% for field in form %}<divclass="form-group">                                {{ field.label_tag }}                                {{ field }}</div>                        {% endfor %}<buttonid="submit"class="btn btn-default"type="submit">Submit</button></form></div></div></div></div>{% endblock page-content %}
Enter fullscreen modeExit fullscreen mode

We've given a similar treatment to the create template, putting the form into a panel. In order to get our form to render with Bootstrap styling, we have a couple options. Once choice is to use a third-party library like Crispy Forms. I've chosen to implement it manually, by adding a mixin to the form class.

# organizations/forms.pyclassBootstrapMixin(object):def__init__(self,*args,**kwargs):super(BootstrapMixin,self).__init__(*args,**kwargs)forkeyinself.fields:self.fields[key].widget.attrs.update({'class':'form-control'})classOrganizationForm(BootstrapMixin,forms.ModelForm):...
Enter fullscreen modeExit fullscreen mode

Let's run all of our functional and unit tests one last time. They pass as expected. Let's visit our page and take a look. It's a lot prettier. Commit the visual changes to Git.

git add.git commit-m"Made the templates prettier with Bootstrap."
Enter fullscreen modeExit fullscreen mode

Visit your website and try out the functionality. If you want to deploy your site or share it with others, make sure to add a remote Git repository and push your code. Also, freeze your requirements and include them in a document to make duplication of your virtual environment easier.

pip freeze> requirements.txtgit add.git commit-m"Added requirements document."git remote add origin git@your_git_repositorygit push-u origin master
Enter fullscreen modeExit fullscreen mode

Top comments(1)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
osahenru profile image
Osahenru
Edo boy | Web developer | Build cool projects using django | Autodidact | B3 | Cool dude

Good write up, but I don't see the attached mock-up wire frame

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Hi, I'm a passionate and results-driven marketing engineer with a technical SEO background. With over 10 years of experience in the industry.
  • Location
    Germany
  • Education
    Southern Methodist University
  • Work
    Entrepreneur
  • Joined

Trending onDEV CommunityHot

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp