<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>Nicole Harris</title>
    <link href="http://www.whoisnicoleharris.com/atom.xml" rel="self"/>
    <link href="http://www.whoisnicoleharris.com"/>
    <updated>2024-04-19T20:59:27+01:00</updated>
    <id>http://www.whoisnicoleharris.com</id>
    <author>
        <name>Nicole Harris</name>
        <email>n.harris@kabucreative.com</email>
    </author>
    
        <entry>
            <title>Talking about PyPI on podcast.__init__</title>
            <link href="http://www.whoisnicoleharris.com/2019/08/21/pypi-on-podcast-init.html"/>
            <updated>2019-08-21T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2019/08/21/pypi-on-podcast-init</id>
            <content type="html">&lt;p&gt;A big thank you to &lt;a href=&quot;https://twitter.com/TobiasMacey&quot;&gt;Tobias Macey&lt;/a&gt; for hosting us.&lt;/p&gt;

&lt;p&gt;You can listen to the episode below, or by visiting &lt;a href=&quot;https://www.pythonpodcast.com/pypi-improvements-episode-225/&quot;&gt;the podcast.__init__ website&lt;/a&gt;, where you will also find the show notes.&lt;/p&gt;

&lt;iframe title=&quot;Podlove Web Player: The Python Podcast.__init__ - Security, UX, and Sustainability For The Python Package Index&quot; width=&quot;auto&quot; height=&quot;180&quot; src=&quot;https://cdn.podlove.org/web-player/share.html?episode=https%3A%2F%2Fwww.pythonpodcast.com%2F%3Fpodlove_player4%3D583&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; tabindex=&quot;0&quot; id=&quot;podcast&quot;&gt;&lt;/iframe&gt;
</content>
        </entry>
    
        <entry>
            <title>EuroPython Keynote - PyPI: Past, Present and Future</title>
            <link href="http://www.whoisnicoleharris.com/2018/07/26/pypi-past-present-future.html"/>
            <updated>2018-07-26T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2018/07/26/pypi-past-present-future</id>
            <content type="html">&lt;p&gt;In July 2018, I was honoured to keynote at &lt;a href=&quot;https://europython.eu/&quot;&gt;EuroPython&lt;/a&gt; - the largest European conference for the Python programming language, with over 1,100 attendees.&lt;/p&gt;

&lt;p&gt;My talk explored the past, present and future of the &lt;a href=&quot;http://pypi.org&quot;&gt;Python Package Index&lt;/a&gt; (PyPI), a project &lt;a href=&quot;https://whoisnicoleharris.com/warehouse/&quot;&gt;I have been working on&lt;/a&gt; for approximately 3 years. This post documents the video, slides, links, resources and statistics resulting from that presentation.&lt;/p&gt;

&lt;p&gt;I would like to thank the organisers for the opportunity to present, and the audience for their warm response.&lt;/p&gt;

&lt;h2 id=&quot;video&quot;&gt;Video&lt;/h2&gt;

&lt;figure class=&quot;img-figure centered&quot;&gt;
    &lt;div class=&quot;embed-container&quot; style=&quot;padding-bottom: 56.25%&quot;&gt;
      &lt;iframe src=&quot;https://www.youtube.com/embed/scum5a_mqBc?rel=0&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
    &lt;/div&gt;
    &lt;figcaption&gt;Or watch on &lt;a href=&quot;https://www.youtube.com/watch?v=scum5a_mqBc&quot;&gt;YouTube&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;slides&quot;&gt;Slides&lt;/h2&gt;

&lt;div class=&quot;embed-container&quot; style=&quot;padding-bottom: 66.4%;&quot;&gt;
  &lt;iframe src=&quot;https://docs.google.com/presentation/d/e/2PACX-1vRw88JQz20Um5wOZhhHVWSSK-LhHUmwm6Ux2IiqFMwg98pZye3NNr8Y62eSsXAmn-EWOYi6pF2gKjQC/embed?start=false&amp;amp;loop=false&amp;amp;delayms=60000&quot; frameborder=&quot;0&quot; width=&quot;750&quot; height=&quot;591&quot; allowfullscreen=&quot;true&quot; mozallowfullscreen=&quot;true&quot; webkitallowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;links-and-resources&quot;&gt;Links and Resources&lt;/h2&gt;

&lt;h3 id=&quot;on-python-packaging&quot;&gt;On Python Packaging&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://wiki.python.org/psf/PackagingWG/Charter&quot;&gt;Python Packaging Working Group&lt;/a&gt;: a sub-body of the PSF dedicated to raising funds to advance the state of Python packaging. You can &lt;a href=&quot;https://donate.pypi.org&quot;&gt;donate to this group here&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pypa.io/&quot;&gt;Python Packaging Authority&lt;/a&gt;: a group of developers who maintain major Python packaging projects (including PyPI). This group also maintains the &lt;a href=&quot;https://packaging.python.org/&quot;&gt;Python Packaging User Guide&lt;/a&gt; which is the authoritative guide to Python Packaging. PyPA projects &lt;a href=&quot;https://github.com/pypa/&quot;&gt;can be found on GitHub&lt;/a&gt;. There is also a really interesting &lt;a href=&quot;https://www.pypa.io/en/latest/history/&quot;&gt;history of Python Packaging&lt;/a&gt; in the PyPA documentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;on-pypi--warehouse&quot;&gt;On PyPI / Warehouse&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://pypi.org&quot;&gt;pypi.org&lt;/a&gt;: the web interface for the Python Package Index&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/pypa/warehouse&quot;&gt;Warehouse&lt;/a&gt;: the codebase currently powering PyPI. If you’d like to contribute, please visit our &lt;a href=&quot;https://github.com/pypa/warehouse/issues&quot;&gt;issue tracker&lt;/a&gt;. Issues set aside for new contributors &lt;a href=&quot;https://github.com/pypa/warehouse/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22&quot;&gt;are found here&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://pyfound.blogspot.com/2017/11/the-psf-awarded-moss-grant-pypi.html&quot;&gt;PSF announcement of MOSS award&lt;/a&gt;: Details of the award from Mozilla that funded Warehouse development between Dec 2017 and May 2018.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://caremad.io/posts/2016/05/powering-pypi/&quot;&gt;Powering the Python Package Index&lt;/a&gt;: an article from Donald Stufft (PyPI / Warehouse maintainer) that discusses what it takes to power PyPI. This article is now slightly out of date (as the figures have grown over the past two years), but is still a very interesting read.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://lwn.net/Articles/751458/&quot;&gt;“A new package index for Python”&lt;/a&gt;: LWN.net article from Sumana Harihareswara (Warehouse project manager) covering the past, present and future of PyPI.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://dustingram.com/articles/2018/03/16/markdown-descriptions-on-pypi&quot;&gt;“Markdown Descriptions on PyPI”&lt;/a&gt;: article from Dustin Ingram (PyPI / Warehouse maintainer) on the steps required to enable Markdown on your PyPI project descriptions&lt;/li&gt;
  &lt;li&gt;I have also published a number of articles about the Warehouse project, covering &lt;a href=&quot;https://whoisnicoleharris.com/2015/12/31/designing-warehouse-an-overview.html&quot;&gt;design objectives&lt;/a&gt;, &lt;a href=&quot;https://whoisnicoleharris.com/2018/03/13/user-testing-warehouse.html&quot;&gt;user testing&lt;/a&gt;, &lt;a href=&quot;https://whoisnicoleharris.com/2018/05/17/warehouse-accessibility.html&quot;&gt;accessibility&lt;/a&gt; and &lt;a href=&quot;https://whoisnicoleharris.com/2018/07/22/pypi-user-research.html&quot;&gt;user research&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;statistics&quot;&gt;Statistics&lt;/h2&gt;

&lt;p&gt;I used &lt;a href=&quot;https://infogram.com&quot;&gt;infogram&lt;/a&gt; to generate the graphs for my presentation. Happily, these can also be embedded:&lt;/p&gt;

&lt;div class=&quot;infogram-embed&quot; data-id=&quot;4e7b0b39-4b7c-4cdf-81c6-6c425be6f1a1&quot; data-type=&quot;interactive&quot; data-title=&quot;Bar Chart&quot;&gt;&lt;/div&gt;
&lt;script&gt;!function(e,t,n,s){var i=&quot;InfogramEmbeds&quot;,o=e.getElementsByTagName(t)[0],d=/^http:/.test(e.location)?&quot;http:&quot;:&quot;https:&quot;;if(/^\/{2}/.test(s)&amp;&amp;(s=d+s),window[i]&amp;&amp;window[i].initialized)window[i].process&amp;&amp;window[i].process();else if(!e.getElementById(n)){var a=e.createElement(t);a.async=1,a.id=n,a.src=s,o.parentNode.insertBefore(a,o)}}(document,&quot;script&quot;,&quot;infogram-async&quot;,&quot;https://e.infogram.com/js/dist/embed-loader-min.js&quot;);&lt;/script&gt;
&lt;div style=&quot;padding:8px 0;font-family:Arial!important;font-size:13px!important;line-height:15px!important;text-align:center;border-top:1px solid #dadada;margin:0 30px&quot;&gt;&lt;a href=&quot;https://infogram.com/4e7b0b39-4b7c-4cdf-81c6-6c425be6f1a1&quot; style=&quot;color:#989898!important;text-decoration:none!important;&quot; target=&quot;_blank&quot;&gt;Bar Chart&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://infogram.com&quot; style=&quot;color:#989898!important;text-decoration:none!important;&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Infogram&lt;/a&gt;&lt;/div&gt;

&lt;h2 id=&quot;images&quot;&gt;Images&lt;/h2&gt;

&lt;p&gt;Thanks to &lt;a href=&quot;http://alinacristea.daportfolio.com/&quot;&gt;Alina Cristea&lt;/a&gt;, the official conference photographer, for taking the following images:&lt;/p&gt;

&lt;figure class=&quot;img-figure&quot;&gt;
    &lt;img src=&quot;/assets/img/europython1.jpg&quot; alt=&quot;On stage, presenting&quot; /&gt;
    &lt;figcaption&gt;Image by Alina Cristea. &lt;a href=&quot;https://www.flickr.com/photos/alina-cristea/42945163604/in/album-72157697856140041/&quot;&gt;View on Flickr&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure class=&quot;img-figure&quot;&gt;
    &lt;img src=&quot;/assets/img/europython2.jpg&quot; alt=&quot;On stage, presenting&quot; /&gt;
    &lt;figcaption&gt;Image by Alina Cristea.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;thanks&quot;&gt;Thanks!&lt;/h2&gt;

&lt;p&gt;I’d like to extend a big thank you to the following people:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The EuroPython organising team for giving me the opportunity to present&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://twitter.com/EWDurbin&quot;&gt;Ee W. Durbin III&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/di_codes&quot;&gt;Dustin Ingram&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/dstufft&quot;&gt;Donald Stufft&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/brainwane&quot;&gt;Sumana Harihareswara&lt;/a&gt; for digging up facts and figures for my presentation and for being great co-contributors on the Warehouse project&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://twitter.com/ainarela&quot;&gt;Aina Requena Lafuente&lt;/a&gt; for the lovely illustrations&lt;/li&gt;
  &lt;li&gt;My colleagues at PeopleDoc for reviewing my presentation&lt;/li&gt;
  &lt;li&gt;All packaging contributors - past, present and future :)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;addendum-6th-aug-2018&quot;&gt;Addendum (6th Aug, 2018)&lt;/h2&gt;

&lt;p&gt;Since presenting my keynote, &lt;a href=&quot;https://twitter.com/EWDurbin&quot;&gt;Ee&lt;/a&gt; has dug up some more information on PyPI’s history, so I thought I’d share it here:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The original Python Package Index was born on Friday, the 1st of November 2002, with both the original commit and first package uploaded on that day. The first upload was for the &lt;a href=&quot;https://pypi.org/project/roundup/&quot;&gt;roundup&lt;/a&gt; project - the codebase that still powers &lt;a href=&quot;http://bugs.python.org/&quot;&gt;bugs.python.org&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The Index was moved from the “Dinsdale” server in Holland to the Oregon State University Open Source Lab on the 14th of August, 2012&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks Ee!&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>PyPI User Research</title>
            <link href="http://www.whoisnicoleharris.com/2018/07/22/pypi-user-research.html"/>
            <updated>2018-07-22T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2018/07/22/pypi-user-research</id>
            <content type="html">&lt;p&gt;Along with &lt;a href=&quot;https://whoisnicoleharris.com/2018/03/13/user-testing-warehouse.html&quot;&gt;user testing&lt;/a&gt;, user research is an important part of developing robust user experiences. Truly understanding your users - what motivates them, what they want, and how they use your product or website is essential for making better design decisions.&lt;/p&gt;

&lt;h2 id=&quot;conducting-user-research-for-pypi&quot;&gt;Conducting User Research for PyPI&lt;/h2&gt;

&lt;p&gt;As PyPI is an open-source project we don’t have the time (or money) to conduct individual user interviews, develop user personas, deeply analyse our Google Analytics data, or run discovery workshops with PyPI users. In the short-term, it’s necessary to find a way of collecting information about what the community wants, using a method that is fast, low-cost and engaging.&lt;/p&gt;

&lt;p&gt;Currently, the page that we’d like to improve the most is the project detail page (&lt;a href=&quot;https://pypi.org/project/black/&quot;&gt;view example&lt;/a&gt;). This page contains an invidual project’s README, history, files and other data. It is the most visited page type on the site and the interface we receive the most design feedback about.&lt;/p&gt;

&lt;p&gt;Depending on the project, there can be a lot of information on this page. To maximise the potential of our redesign, it is important to understand how much our users value each piece of information in relation to the rest of the content.&lt;/p&gt;

&lt;h2 id=&quot;approach&quot;&gt;Approach&lt;/h2&gt;

&lt;p&gt;To discover what the community thinks, I set up a “Buy a Feature” form. This form was based off the &lt;a href=&quot;http://www.uxforthemasses.com/buy-the-feature/&quot;&gt;“Buy a Feature” game&lt;/a&gt; that is typically run as a workshop.&lt;/p&gt;

&lt;p&gt;The idea was to price each feature on the current page according to the space it takes up on the screen and then allocate a limited budget to each participant. Participants then use that budget to buy the features they want to see. I also added a wildcard item, to allow community members to request new features. The resulting data should indicate the overall priorities of the research participants.&lt;/p&gt;

&lt;figure class=&quot;img-figure white-bg&quot;&gt;
    &lt;img src=&quot;/assets/img/buy-a-feature.png&quot; alt=&quot;Buy a feature form, showing instructions and prices of individual features&quot; /&gt;
    &lt;figcaption&gt;The beginning of the &quot;Buy a Feature&quot; form filled by research participants. In total, there were 18 features available to buy, each priced relative to their size.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;limitations&quot;&gt;Limitations&lt;/h3&gt;

&lt;p&gt;No research method is perfect, and there were certainly limitations to this exercise:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Both the pricing and the budget were subjective.&lt;/li&gt;
  &lt;li&gt;Participants may have chosen a lower priced item to use all of their allocated budget, even if this item was not their ideal choice.&lt;/li&gt;
  &lt;li&gt;As the Google Form contained no budget validation, we relied on trust (and good mathematics!) to ensure participants didn’t overspend their budget.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;results&quot;&gt;Results&lt;/h3&gt;

&lt;p&gt;In total, 1,926 people participated in the exercise. Here are the results:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Participants = Total number of participants who selected this feature&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;Percentage = Percentage of participants who selected this feature&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Participants&lt;/th&gt;
      &lt;th&gt;Percentage&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Project description&lt;/td&gt;
      &lt;td&gt;1466&lt;/td&gt;
      &lt;td&gt;76.19%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Required Python version&lt;/td&gt;
      &lt;td&gt;1349&lt;/td&gt;
      &lt;td&gt;70%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Links to project online&lt;/td&gt;
      &lt;td&gt;1337&lt;/td&gt;
      &lt;td&gt;69.4%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Date project last updated&lt;/td&gt;
      &lt;td&gt;1235&lt;/td&gt;
      &lt;td&gt;64.1%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Licence&lt;/td&gt;
      &lt;td&gt;1059&lt;/td&gt;
      &lt;td&gt;55%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Project name and release no.&lt;/td&gt;
      &lt;td&gt;1001&lt;/td&gt;
      &lt;td&gt;52%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Version status&lt;/td&gt;
      &lt;td&gt;854&lt;/td&gt;
      &lt;td&gt;44.3%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Release history&lt;/td&gt;
      &lt;td&gt;813&lt;/td&gt;
      &lt;td&gt;42.2%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GitHub statistics&lt;/td&gt;
      &lt;td&gt;780&lt;/td&gt;
      &lt;td&gt;40.5%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Project summary&lt;/td&gt;
      &lt;td&gt;752&lt;/td&gt;
      &lt;td&gt;39%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Requires distribution&lt;/td&gt;
      &lt;td&gt;647&lt;/td&gt;
      &lt;td&gt;33.6%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Download files&lt;/td&gt;
      &lt;td&gt;638&lt;/td&gt;
      &lt;td&gt;33.1%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Author&lt;/td&gt;
      &lt;td&gt;619&lt;/td&gt;
      &lt;td&gt;32.1%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Tags&lt;/td&gt;
      &lt;td&gt;461&lt;/td&gt;
      &lt;td&gt;23.9%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Trove classifiers&lt;/td&gt;
      &lt;td&gt;441&lt;/td&gt;
      &lt;td&gt;22.9%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Maintainers&lt;/td&gt;
      &lt;td&gt;424&lt;/td&gt;
      &lt;td&gt;22%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Statistics instructions&lt;/td&gt;
      &lt;td&gt;266&lt;/td&gt;
      &lt;td&gt;13.8%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Wildcard (new feature)&lt;/td&gt;
      &lt;td&gt;157&lt;/td&gt;
      &lt;td&gt;8.2%&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;div class=&quot;infogram-embed&quot; data-id=&quot;aafb06e8-4bc6-4e49-8bbe-9e2525b18ddf&quot; data-type=&quot;interactive&quot; data-title=&quot;&amp;amp;quot;Buy a Feature&amp;amp;quot; - PyPI research&quot;&gt;&lt;/div&gt;
&lt;script&gt;!function(e,t,n,s){var i=&quot;InfogramEmbeds&quot;,o=e.getElementsByTagName(t)[0],d=/^http:/.test(e.location)?&quot;http:&quot;:&quot;https:&quot;;if(/^\/{2}/.test(s)&amp;&amp;(s=d+s),window[i]&amp;&amp;window[i].initialized)window[i].process&amp;&amp;window[i].process();else if(!e.getElementById(n)){var a=e.createElement(t);a.async=1,a.id=n,a.src=s,o.parentNode.insertBefore(a,o)}}(document,&quot;script&quot;,&quot;infogram-async&quot;,&quot;https://e.infogram.com/js/dist/embed-loader-min.js&quot;);&lt;/script&gt;
&lt;div style=&quot;padding:8px 0;font-family:Arial!important;font-size:13px!important;line-height:15px!important;text-align:center;border-top:1px solid #dadada;margin:0 30px&quot;&gt;&lt;a href=&quot;https://infogram.com/aafb06e8-4bc6-4e49-8bbe-9e2525b18ddf&quot; style=&quot;color:#989898!important;text-decoration:none!important;&quot; target=&quot;_blank&quot;&gt;&quot;Buy a Feature&quot; - PyPI research&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://infogram.com&quot; style=&quot;color:#989898!important;text-decoration:none!important;&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Infogram&lt;/a&gt;&lt;/div&gt;

&lt;p&gt;157 people chose to purchase a wildcard item - meaning that they forfeited some of their budget to request something different. I am in the process of moving these suggestions into our issue tracker. The full list can be &lt;a href=&quot;https://github.com/pypa/warehouse/issues/4335&quot;&gt;viewed here&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h2&gt;

&lt;p&gt;In the coming months, we will continue to &lt;a href=&quot;https://whoisnicoleharris.com/2018/03/13/user-testing-warehouse.html&quot;&gt;conduct user tests&lt;/a&gt; to qualify the results of this research, and identify flaws in the current interface.&lt;/p&gt;

&lt;p&gt;After this, I will work to propose a new interface that responds to our discoveries, in an effort to better serve the Python community.&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>Accessibility on Warehouse (PyPI)</title>
            <link href="http://www.whoisnicoleharris.com/2018/05/17/warehouse-accessibility.html"/>
            <updated>2018-05-17T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2018/05/17/warehouse-accessibility</id>
            <content type="html">&lt;p&gt;This year &lt;a href=&quot;http://globalaccessibilityawarenessday.org/&quot;&gt;Global Accessibility Awareness Day&lt;/a&gt; is being marked on May 17th. The purpose of this day is to raise awareness about digital inclusion - and how people who work in the digital space can ensure their software works for the largest possible group of users.&lt;/p&gt;

&lt;p&gt;Accessibility is an important subject; as a digital designer, it is my responsibility to do everything that I can to design solutions that include, rather than exclude, users.&lt;/p&gt;

&lt;p&gt;On the Python Package Index, accessibility has been baked into the design from the beginning: from the selection of high-contrast colors, to the large default text size, to the way that our HTML markup is structured. However, like everything, meeting accessibility standards and including all users regardless of their impairments is an &lt;em&gt;ongoing&lt;/em&gt; task that will never be complete.&lt;/p&gt;

&lt;p&gt;To that end, earlier this year, I contacted &lt;a href=&quot;http://axesslab.com/&quot;&gt;Axcess Lab&lt;/a&gt; to ask if one of their team members would be interested in dedicating some of their awesome &lt;a href=&quot;https://axesslab.com/paying-open-source/&quot;&gt;open-source project time&lt;/a&gt; to the Python Package Index. Lucky for us, &lt;a href=&quot;https://github.com/JazzBrotha&quot;&gt;Mattias&lt;/a&gt; volunteered to assist, and has since completed an accessibility audit on &lt;a href=&quot;https://pypi.org&quot;&gt;pypi.org&lt;/a&gt; (thanks Mattias!!!).&lt;/p&gt;

&lt;p&gt;You can &lt;a href=&quot;https://drive.google.com/file/d/0B523GRwFhk7BMDNZUnJVQzJXZ1RpdjFfb1l0a3NMRnprX0Jr/view?usp=sharing&quot;&gt;read the full audit here&lt;/a&gt;, a few highlights:&lt;/p&gt;

&lt;h2 id=&quot;what-were-doing-well&quot;&gt;What we’re doing well&lt;/h2&gt;

&lt;p&gt;Mattias noted font size, color contrast, outline design and the uncluttered UI as positive accessibility attributes.&lt;/p&gt;

&lt;p&gt;Our form design is helpful for users using screen readers, while visually impaired users are able to zoom the interface without loss of function.&lt;/p&gt;

&lt;p&gt;On mobile, the whitespace we provide between elements helps users with motor impairments scroll the page.&lt;/p&gt;

&lt;h2 id=&quot;improvements&quot;&gt;Improvements&lt;/h2&gt;

&lt;p&gt;There are still a few things we can do to make the user experience better for all:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Redesigning the line length of our text, for &lt;a href=&quot;https://baymard.com/blog/line-length-readability&quot;&gt;optimal readability&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Removing italic styles, as these can be difficult to read for people with reading impairments&lt;/li&gt;
  &lt;li&gt;Reconsider our use of disabled buttons, &lt;a href=&quot;https://axesslab.com/disabled-buttons-suck/&quot;&gt;because they suck&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Underlining links&lt;/li&gt;
  &lt;li&gt;Increasing the clickable areas of some elements&lt;/li&gt;
  &lt;li&gt;Adding &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA&quot;&gt;ARIA&lt;/a&gt; where needed&lt;/li&gt;
  &lt;li&gt;Improving the way some of our forms work, particularly for people using screen readers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;next-steps&quot;&gt;Next steps&lt;/h2&gt;

&lt;p&gt;I have started opening tickets on the &lt;a href=&quot;https://github.com/pypa/warehouse/issues?q=is%3Aopen+is%3Aissue+label%3Aaccessibility&quot;&gt;project issue tracker&lt;/a&gt; to address each of these issues, and will begin working through these in the coming months.&lt;/p&gt;

&lt;p&gt;I will also be running a PyPI sprint at &lt;a href=&quot;https://ep2018.europython.eu/en/&quot;&gt;EuroPython&lt;/a&gt; this year, so hope that any remaining issues can be addressed at this time.&lt;/p&gt;

&lt;h2 id=&quot;how-you-can-help&quot;&gt;How you can help&lt;/h2&gt;

&lt;p&gt;If you are a &lt;a href=&quot;https://pypi.org&quot;&gt;PyPI&lt;/a&gt; user and notice an accessibility issue on the site, please report it &lt;a href=&quot;https://github.com/pypa/warehouse/issues/new&quot;&gt;via our issue tracker&lt;/a&gt;. We take these issues seriously, so will try our best to respond as soon as possible.&lt;/p&gt;

&lt;p&gt;If you are a developer, and would like to help make the Python ecosystem a more friendly and welcoming place, we’d love you to make a PyPI PR addressing an &lt;a href=&quot;https://github.com/pypa/warehouse/issues?q=is%3Aopen+is%3Aissue+label%3Aaccessibility&quot;&gt;accessibility issue&lt;/a&gt;. We do our best to support new contributors - just comment on the issue you’d like to work on, and let us know if you need any help&lt;/p&gt;

&lt;h2 id=&quot;addendum---21st-july-2018&quot;&gt;Addendum - 21st July 2018&lt;/h2&gt;

&lt;p&gt;The &lt;a href=&quot;https://wiki.python.org/psf/PackagingWG/Charter&quot;&gt;Python Packaging Working Group&lt;/a&gt; continues to seek funding to improve PyPI. With more funding, the team aims to make the accessibility improvements highlighted by this audit, as well as identify and address other accessibility issues. To donate to the working group, visit &lt;a href=&quot;http://donate.pypi.org/&quot;&gt;donate.pypi.org&lt;/a&gt; - donations can be made on a single, or recurring basis. Every donation is appreciated!&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>My guest appearance on the Talk Python to Me podcast</title>
            <link href="http://www.whoisnicoleharris.com/2018/04/27/talk-python-to-me.html"/>
            <updated>2018-04-27T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2018/04/27/talk-python-to-me</id>
            <content type="html">&lt;p&gt;A big thank you to &lt;a href=&quot;https://twitter.com/mkennedy&quot;&gt;Michael Kennedy&lt;/a&gt; for hosting us!&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://talkpython.fm/episodes/show/159/inside-the-new-pypi-launch&quot;&gt;Check out the recording here&lt;/a&gt; or &lt;a href=&quot;Transcript: https://talkpython.fm/episodes/transcript/159/inside-the-new-pypi-launch&quot;&gt;view the transcript&lt;/a&gt;.&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>User Testing Warehouse</title>
            <link href="http://www.whoisnicoleharris.com/2018/03/13/user-testing-warehouse.html"/>
            <updated>2018-03-13T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2018/03/13/user-testing-warehouse</id>
            <content type="html">&lt;p&gt;User testing is a user experience methodology that places the user at the center of design decisions. UX professionals conduct user tests to challenge their design concepts and validate that the UI works as they intended.&lt;/p&gt;

&lt;p&gt;User tests typically involve asking a target user to carry out a series of real world tasks on the UI. Testers can then identify problems in the UI - where the user gets lost or confused, or can’t work out how to complete the assigned task.&lt;/p&gt;

&lt;p&gt;Any identified problems can then be raised as design issues for the team to fix.&lt;/p&gt;

&lt;h2 id=&quot;user-testing-warehouse&quot;&gt;User Testing Warehouse&lt;/h2&gt;

&lt;p&gt;It &lt;a href=&quot;http://whoisnicoleharris.com/2015/12/31/designing-warehouse-an-overview.html&quot;&gt;had always been my intention&lt;/a&gt; to run user tests on the &lt;a href=&quot;https://pypi.org/&quot;&gt;Warehouse project&lt;/a&gt;, but it wasn’t until the project &lt;a href=&quot;http://pyfound.blogspot.co.uk/2017/11/the-psf-awarded-moss-grant-pypi.html&quot;&gt;received MOSS funding&lt;/a&gt; that I was able to dedicate the amount of time required to set up and conduct these tests.&lt;/p&gt;

&lt;p&gt;Having recently completed the pages for logged in users (account settings, manage projects, etc.) these pages seemed like the best candidates for our initial round of testing.&lt;/p&gt;

&lt;p&gt;The test I constructed was simple: asking users about their current use of PyPI, then requesting them to complete a series of tasks on the new management pages. For the test, I recruited users who were package owners or maintainers - i.e. people who had experienced the old user interface and who would understand my instructions.&lt;/p&gt;

&lt;p&gt;Over three weeks, I ran tests with six different users, with each test taking between 25 minutes and one hour, the duration depending on how much the test subject had to say.&lt;/p&gt;

&lt;p&gt;As expected, the tests highlighted a number of problems in the UI, which in turn prompted me to open or tag &lt;a href=&quot;https://github.com/pypa/warehouse/issues?q=is%3Aissue+label%3A%22user+testing%22&quot;&gt;20 issues&lt;/a&gt; in our issue tracker. The vast majority of these tickets are simple changes that have been tagged as &lt;a href=&quot;https://github.com/pypa/warehouse/issues?q=is%3Aissue+label%3A%22good+first+issue%22&quot;&gt;good first issues&lt;/a&gt; - which should in turn help with our efforts to reach out to new contributors.&lt;/p&gt;

&lt;p&gt;Several of the test participants also raised new ideas, for which I will open new tickets soon.&lt;/p&gt;

&lt;p&gt;In total, this round of testing took 15 hours - encompassing writing the testing script, recruiting users, running the tests, and opening related tickets.&lt;/p&gt;

&lt;p&gt;A big thanks to Ee W. Durbin III, Nicolas Dubois, Łukasz Langa, Russell Keith-Magee, Alex Chan and Eliot Berriot for participating in these tests.&lt;/p&gt;

&lt;h2 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h2&gt;

&lt;p&gt;From this intial program it is clear that user testing can bring value to the Warehouse project. The challenge is to continue to conduct such tests once our MOSS funding is depleted.&lt;/p&gt;

&lt;p&gt;Fortunately, the Python community is engaged with the project, and, so far, we’ve had no problems recruiting test participants. Usability experts &lt;a href=&quot;https://www.nngroup.com/articles/how-many-test-users/&quot;&gt;recommend a minumum of 5 users per test&lt;/a&gt;, so as long as this goodwill continues, I don’t anticipate running out of test subjects.&lt;/p&gt;

&lt;p&gt;On the other hand, setting up and running user tests takes time; and it would be nice to have some help with this to lighten the workload (and increase our testing bus factor). For this reason, today I’ve put out a call to &lt;a href=&quot;https://gist.github.com/nlhkabu/a0b1ae0016a2641f6b79d9ace9110403&quot;&gt;train up to three people on how to conduct and run user tests&lt;/a&gt;. My hope is that this opportunity will be taken by students (or UX enthusiasts) as a way to build their experience by working on a live, widely used application.&lt;/p&gt;

&lt;p&gt;Should this program be successful, I’d like to start group testing the &lt;a href=&quot;https://pypi.org/project/Flask/&quot;&gt;project detail page&lt;/a&gt; in the coming weeks. This page is the most important on the site, and coincidentally, the page we’ve also received the most feedback about.&lt;/p&gt;

&lt;p&gt;With more funding, it would be possible to extend our testing further to encompass testing the UI for accessibility and internationalisation.&lt;/p&gt;

&lt;h2 id=&quot;get-involved&quot;&gt;Get Involved&lt;/h2&gt;

&lt;p&gt;So, how can you help?&lt;/p&gt;

&lt;p&gt;Firstly, if you are a developer, please consider contributing to &lt;a href=&quot;https://github.com/pypa/warehouse&quot;&gt;Warehouse&lt;/a&gt;. Right now, we have lots of tickets marked as &lt;a href=&quot;https://github.com/pypa/warehouse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22&quot;&gt;good first issues&lt;/a&gt; in our &lt;a href=&quot;https://github.com/pypa/warehouse/issues&quot;&gt;issue tracker&lt;/a&gt;. Because of our MOSS funding, we are also able to be more responsive to your questions and provide you with more support to &lt;a href=&quot;https://warehouse.readthedocs.io/development/getting-started/&quot;&gt;get started&lt;/a&gt;. If you’re planning on attending PyCon North America, or EuroPython, our team is planning on running &lt;a href=&quot;https://wiki.python.org/psf/PackagingSprints&quot;&gt;conference sprints&lt;/a&gt; - this is a great opportunity to hack with members of the team and work with others on the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are interested in learning how to &lt;em&gt;conduct&lt;/em&gt; user tests&lt;/strong&gt;, please &lt;a href=&quot;https://goo.gl/forms/vdqr14IzHwIQLfAc2&quot;&gt;fill out this form&lt;/a&gt;. If you have any questions about what’s involved, you can also email me directly at n.harris[at]kabucreative.com.&lt;/p&gt;

&lt;p&gt;Finally, &lt;strong&gt;if you’d like to volunteer to be a test &lt;em&gt;participant&lt;/em&gt;&lt;/strong&gt;, please &lt;a href=&quot;https://goo.gl/forms/KsgCZKWRHPS572gp2&quot;&gt;fill out this form&lt;/a&gt;.&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>Interview Series - UX & UI at PeopleDoc</title>
            <link href="http://www.whoisnicoleharris.com/2017/08/10/peopledoc-user-experience.html"/>
            <updated>2017-08-10T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2017/08/10/peopledoc-user-experience</id>
            <content type="html">&lt;p&gt;As part of my work at &lt;a href=&quot;http://www.people-doc.com/&quot;&gt;PeopleDoc&lt;/a&gt;, I was recently interviewed by &lt;a href=&quot;https://twitter.com/NicolePeopleDoc&quot;&gt;Nicole Lindenbaum&lt;/a&gt; on our approach to UX and the development of PeopleDoc UI - our internal design system.&lt;/p&gt;

&lt;p&gt;The interview was split into three parts:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://www.people-doc.com/blog/improving-your-user-experience&quot;&gt;Introducing PeopleDoc UI - what and why&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://www.people-doc.com/blog/user-experience-principles-in-action&quot;&gt;Building and rolling out PeopleDoc UI&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://www.people-doc.com/blog/user-experience-principles&quot;&gt;Defining great UX and how we measure success&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A big thanks to Nicole for organising and publishing this content!&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>Presenting Warehouse at PyCon France</title>
            <link href="http://www.whoisnicoleharris.com/2016/10/16/warehouse-at-pycon-france.html"/>
            <updated>2016-10-16T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2016/10/16/warehouse-at-pycon-france</id>
            <content type="html">&lt;p&gt;The talk explored the organisational state of Python packaging, the current state of the &lt;a href=&quot;https://pypi.python.org&quot;&gt;Python Package Index&lt;/a&gt;, and introduced the &lt;a href=&quot;https://pypi.io&quot;&gt;Warehouse project&lt;/a&gt; - the &lt;a href=&quot;https://github.com/pypa/warehouse/&quot;&gt;codebase&lt;/a&gt; that will soon power PyPI.&lt;/p&gt;

&lt;p&gt;I’d like to thank the organisers of PyCon France for the opportunity to speak to such an engaged audience and for delivering an excellent conference overall. Special mention to all those French speakers who, during the conference, patiently corrected my stumbling French and helped me build my vocabulary.&lt;/p&gt;

&lt;p&gt;The organisers of PyCon France have put together &lt;a href=&quot;http://www.demo.openveo.com/publish/video/SJ6ZOSHyx?fullscreen&quot;&gt;a fabulous video&lt;/a&gt; that combines the video of the talk with the slide display. If you’d like to view the talk, this is the best format.&lt;/p&gt;

&lt;p&gt;I’ve also embedded the raw video and slide presentation here for posterity. Enjoy!&lt;/p&gt;

&lt;figure class=&quot;img-figure centered&quot;&gt;
    &lt;div class=&quot;embed-container&quot; style=&quot;padding-bottom: 56.25%&quot;&gt;
      &lt;iframe src=&quot;https://www.youtube.com/embed/v_wFF2wEG_A&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
    &lt;/div&gt;
    &lt;figcaption&gt;Or watch on &lt;a href=&quot;https://www.youtube.com/watch?v=v_wFF2wEG_A?|&quot;&gt;YouTube&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div class=&quot;embed-container&quot; style=&quot;top: 15px; padding-bottom: 78.9%&quot;&gt;
  &lt;iframe src=&quot;https://docs.google.com/presentation/d/1PEcnSNTrIFKTbgGn1nSueB4dwOMCd1xyHmO0IVEHVec/embed?start=false&amp;amp;loop=false&amp;amp;delayms=3000&quot; frameborder=&quot;0&quot; width=&quot;750&quot; height=&quot;591&quot; allowfullscreen=&quot;true&quot; mozallowfullscreen=&quot;true&quot; webkitallowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;
</content>
        </entry>
    
        <entry>
            <title>Presenting at DjangoCon Europe</title>
            <link href="http://www.whoisnicoleharris.com/2016/04/16/djangocon-eu-2016.html"/>
            <updated>2016-04-16T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2016/04/16/djangocon-eu-2016</id>
            <content type="html">&lt;p&gt;Imagine this you’re on stage and you have a captive audience of 300+ people. That’s the view I faced just a few weeks ago, as I presented ‘Learning Django, Learning French’ (an extension of my &lt;a href=&quot;http://whoisnicoleharris.com/2015/12/14/my-talk-at-pycon-fr.html&quot;&gt;‘Learning Django, Learning French’ talk&lt;/a&gt;) to the audience at DjangoCon Europe 2016.&lt;/p&gt;

&lt;p&gt;You can judge for yourself the success of my talk - in any case, I’d like to thank the organisers of DjangoCon Europe for having me on stage. I’d also like to thank those community members who gave me so much positive feedback and made my conference experience great!&lt;/p&gt;

&lt;figure class=&quot;img-figure centered&quot;&gt;
    &lt;div class=&quot;embed-container&quot; style=&quot;padding-bottom: 56.25%&quot;&gt;
      &lt;iframe class=&quot;wistia_embed&quot; name=&quot;wistia_embed&quot; src=&quot;http://fast.wistia.net/embed/iframe/tzu957oa60?canonicalUrl=https%3A%2F%2Fopbeat.com%2Fevents%2Fdjangocon-eu-2016%2F&amp;amp;canonicalTitle=DjangoCon%20Europe%202016%2C%20Budapest%20%7C%20Opbeat&quot; allowtransparency=&quot;true&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt;&lt;/iframe&gt;
      &lt;br /&gt;
    &lt;/div&gt;
    &lt;figcaption&gt;Or watch on &lt;a href=&quot;https://opbeat.com/events/djangocon-eu-2016/&quot;&gt; Opbeat&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;All of the videos for the conference can be found &lt;a href=&quot;https://opbeat.com/events/djangocon-eu-2016/&quot;&gt;on Opbeat’s website&lt;/a&gt;. In particular, I’d like to highlight the talk by Erik Romijn and Mikey Ariel entitled “Healthy Minds in a Healthy Community” as an important message for everyone trying to find a healthy balance working in open source.&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>Designing Warehouse - An Overview</title>
            <link href="http://www.whoisnicoleharris.com/2015/12/31/designing-warehouse-an-overview.html"/>
            <updated>2015-12-31T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2015/12/31/designing-warehouse-an-overview</id>
            <content type="html">&lt;p&gt;For the last six months, I have been working with the Python Packaging Authority (PyPA) to design &lt;a href=&quot;https://warehouse.python.org/&quot;&gt;Warehouse&lt;/a&gt; - the new &lt;a href=&quot;https://github.com/pypa/warehouse&quot;&gt;codebase&lt;/a&gt; that will soon replace the &lt;a href=&quot;https://pypi.python.org/pypi&quot;&gt;Python Package Index&lt;/a&gt; (PyPI).&lt;/p&gt;

&lt;p&gt;This article is an attempt to explore our objectives and challenges, provide an overview of our activities so far, and engage the wider Python community to help us move forward.&lt;/p&gt;

&lt;h2 id=&quot;our-goals&quot;&gt;Our Goals&lt;/h2&gt;

&lt;p&gt;When &lt;a href=&quot;https://github.com/dstufft&quot;&gt;Donald&lt;/a&gt; (our lead developer) and I first talked about the project, he had some very clear objectives for the redesign:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;To update the visual identity&lt;/li&gt;
  &lt;li&gt;To make packages more discoverable; and&lt;/li&gt;
  &lt;li&gt;To look after &lt;em&gt;both&lt;/em&gt; kinds of users, namely, users &lt;em&gt;and&lt;/em&gt; package maintainers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To this list, I added a two of my own objectives:&lt;/p&gt;

&lt;p&gt;First; to give the project the same level of professionalism as a commercial project of the same scale.&lt;/p&gt;

&lt;p&gt;Second; to ensure that the user experience reflect the Python community that I know - a community that is welcoming, helpful and inclusive.&lt;/p&gt;

&lt;h2 id=&quot;what-weve-done&quot;&gt;What We’ve Done&lt;/h2&gt;

&lt;p&gt;So now you see our goals, let’s take a look at the solution we’ve come up with so far.&lt;/p&gt;

&lt;h3 id=&quot;making-the-ui-welcoming&quot;&gt;Making the UI Welcoming&lt;/h3&gt;

&lt;p&gt;Python is used by many community and educational organisations to introduce newcomers to programming. By extension, PyPI might be the first package manager that some of our users have ever seen. This is a big responsibility, but also an opportunity to make a small part of the learning journey easier.&lt;/p&gt;

&lt;p&gt;In designing the user interface, I tried to make no assumptions about prior knowledge. Don’t know what pip is? That’s ok - we’ll link you to the appropriate help section. Not sure what to do with an MD5 hash? No worries - there’s a help popover for that.&lt;/p&gt;

&lt;p&gt;The colours, fonts, text sizes and spacing were also designed to be large, friendly and accessible (we’ll talk more about accessibility later) - all in an attempt to make new coders comfortable with the tool at hand.&lt;/p&gt;

&lt;p&gt;Now - you might be thinking - “but most PyPI users are &lt;em&gt;not&lt;/em&gt; beginners, so why design for them?” The answer is simple: a site designed for beginners can be used by everyone; a site designed for experts may alienate and confuse beginners. Of course, getting this right is a challenge, so we’ll be usability testing the design to find a balance that works for users of all experience levels.&lt;/p&gt;

&lt;h3 id=&quot;helping-users-overcome-obstacles&quot;&gt;Helping Users Overcome Obstacles&lt;/h3&gt;

&lt;p&gt;To ensure that &lt;em&gt;all&lt;/em&gt; of our users are getting the most out of PyPI, it’s not enough just to design an interface - I needed to look at the whole user experience. This meant thinking about an individual’s experience moving through the site, and asking myself:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Where could our users be getting ‘stuck’?&lt;/li&gt;
  &lt;li&gt;How can I help them on their way?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not only did this mode of thinking influence a lot of my &lt;em&gt;interface&lt;/em&gt; design decisions, it also led me to explore the documentation resources that are currently available.&lt;/p&gt;

&lt;p&gt;Currently, the &lt;a href=&quot;https://packaging.python.org/en/latest/&quot;&gt;Python Packaging User Guide&lt;/a&gt; is the authoritative resource on Python packaging. Unfortunately, it is not linked to from PyPI itself; and while the content is of very high quality, the tutorials are written through the lens of a more experienced Pythonista. I wanted something with a much lower threshold… something that would really hold the user’s hand when they got stuck.&lt;/p&gt;

&lt;p&gt;In October I put out a call for help writing two new tutorials to help beginners; one for users who have never installed a package, and one for users who want to upload their first package to PyPI. My hope was that I’d attract a couple of technical writers to the project, but after an incredible response from the Python community (and consulting with &lt;a href=&quot;https://github.com/qwcode&quot;&gt;Marcus&lt;/a&gt; and the PyPA team) we’ve settled on something more ambitious: to edit the current packaging guide to include new tutorials, while improving and extending the documentation in general.&lt;/p&gt;

&lt;p&gt;Our goal is to complete this work by early 2016 to coincide with the launch of the new Warehouse codebase. This will facilitate extensive linking between the two resources, and dramatically improve the overall user experience.&lt;/p&gt;

&lt;h2 id=&quot;ensuring-the-site-is-inclusive&quot;&gt;Ensuring the Site Is Inclusive&lt;/h2&gt;

&lt;p&gt;The final piece of the puzzle is ensuring that the site is usable and accessible for &lt;em&gt;all&lt;/em&gt; of our community members, whether they are using a phone, screen reader or an old, unpopular browser.&lt;/p&gt;

&lt;p&gt;This has resulted in a number of technical decisions; the first (and most obvious) is that the design supports a range of devices via responsive media queries. We’re also going to (at least in the short term) continue support for Internet Explorer &lt;strike&gt;8&lt;/strike&gt; 9, as we have a number of users, primarily in China, still using this browser. See &lt;a href=&quot;https://github.com/pypa/warehouse/issues/979&quot;&gt;this issue&lt;/a&gt; for more information.&lt;/p&gt;

&lt;p&gt;On the accessibility side, the project has already benefited from one audit from &lt;a href=&quot;http://xavier.dutreilh.com/&quot;&gt;Xavier Dutreilh&lt;/a&gt; and we are looking forward to another before the site launches. Planned improvements include the addition of ARIA roles to the HTML, a ‘skip to content’ link, as well as better labeling of form fields for screen readers.&lt;/p&gt;

&lt;p&gt;Lastly, Donald is introducing both localisation and internationalisation to the codebase. Currently PyPI is very US centric - supporting US English and US formats only. We’re very excited to be updating the user experience to better support Python users from all over the world, and will be calling for volunteers in early 2016 to assist us in this activity.&lt;/p&gt;

&lt;h2 id=&quot;how-you-can-help&quot;&gt;How You Can Help&lt;/h2&gt;

&lt;p&gt;So what’s next? And importantly - how can you help?&lt;/p&gt;

&lt;h3 id=&quot;contributing-code&quot;&gt;Contributing Code&lt;/h3&gt;

&lt;p&gt;We have a huge amount of work to complete before launching Warehouse, and would love to see some new faces working on the project. If you have skills in Python, HTML, SCSS, or JavaScript, then please check out the &lt;a href=&quot;https://github.com/pypa/warehouse/issues&quot;&gt;open issues&lt;/a&gt; on the issue tracker. Thanks to Donald’s hard work, the project itself is &lt;a href=&quot;https://warehouse.readthedocs.org/development/getting-started/&quot;&gt;easy to set up&lt;/a&gt; and &lt;a href=&quot;https://warehouse.readthedocs.org/&quot;&gt;extensively documented&lt;/a&gt;. If you’ve never contributed to an open-source project before, or are new to web development, we’d love to accept your first pull request! We have labelled a number of issues on the issue tracker as &lt;a href=&quot;https://github.com/pypa/warehouse/issues?q=is%3Aopen+is%3Aissue+label%3Aeasy&quot;&gt;‘easy’&lt;/a&gt; - start here!&lt;/p&gt;

&lt;h3 id=&quot;participating-in-usability-testing&quot;&gt;Participating in Usability Testing&lt;/h3&gt;

&lt;p&gt;As already mentioned, I am planning on running a number of usability testing sessions. The objectives of these sessions is to ask:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Are the design decisions &lt;em&gt;really&lt;/em&gt; helping new learners?&lt;/li&gt;
  &lt;li&gt;Have I got the balance right for more experienced Pythonistas?&lt;/li&gt;
  &lt;li&gt;Is it clear what each page does?&lt;/li&gt;
  &lt;li&gt;Can users find help when they need it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since making a call for volunteers, over 150 Pythonistas have come forward to participate. From these, I will ask a variety of users to participate in the tests - the objective being to test with users from different backgrounds and with different levels of Python experience. If you think that you have a new perspective to bring to this process, please &lt;a href=&quot;https://docs.google.com/forms/d/14YJvKOVIzFjXkhUq5y0GnD3FYq4xUuW-oIGypSuvJhQ/viewform&quot;&gt;volunteer here&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;helping-rewrite-pypug&quot;&gt;Helping Rewrite PyPUG&lt;/h3&gt;

&lt;p&gt;I’m very excited to report that the work on rewriting the Python Packaging User Guide has already begun, with &lt;a href=&quot;http://danieldbeck.com/&quot;&gt;Daniel Beck&lt;/a&gt; leading a team of volunteer writers. If you’d like to get involved in this project, please start by visiting &lt;a href=&quot;https://github.com/pypa/python-packaging-user-guide/issues/194&quot;&gt;this ticket&lt;/a&gt; on the &lt;a href=&quot;https://github.com/pypa/python-packaging-user-guide/issues&quot;&gt;issue tracker&lt;/a&gt; - Daniel and the team would be excited for your contribution!&lt;/p&gt;

&lt;h3 id=&quot;providing-feedback&quot;&gt;Providing Feedback&lt;/h3&gt;

&lt;p&gt;Finally, we’re planning on installing a widget on the site to gather community feedback. Perhaps there is something small that is bothering you, but it doesn’t justify opening an issue on the issue tracker. Perhaps there is something big, but you don’t like issue trackers, or find them difficult or intimidating to use. Perhaps you just want to put an opinion forward. In all of these cases, please use our widget - we can only better serve the community if we hear from the community!&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;

&lt;p&gt;Warehouse is an exciting, interesting and challenging project to work on, and I’m very pleased to have been able to make a large impact on such an important project for the Python community.&lt;/p&gt;

&lt;p&gt;There is still lots to do and many challenges ahead, but I am confident that the final site will deliver significant improvements to the user experience for Python users across the globe.&lt;/p&gt;

&lt;p&gt;Stay tuned for my next article which will explore the new design in more depth.&lt;/p&gt;

&lt;h2 id=&quot;update-27th-feb-2018&quot;&gt;Update (27th Feb, 2018)&lt;/h2&gt;

&lt;p&gt;My intention to write a series of posts on this subject was never realised - so instead, here an update on the project (and this article):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Last year, the PSF’s Packaging Working Group applied for and recived a Mozilla Open Source Support grant to focus on Warehouse (&lt;a href=&quot;https://pyfound.blogspot.com/2017/11/the-psf-awarded-moss-grant-pypi.html&quot;&gt;more detail here&lt;/a&gt;). I’m amongst the group of developers who can now dedicate paid time to focus on getting the new PyPI up to standard and fully replacing legacy PyPI.&lt;/li&gt;
  &lt;li&gt;Warehouse is now available to try at &lt;a href=&quot;https://pypi.org&quot;&gt;the pre-production site&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;There has been lots of great work completed on the &lt;a href=&quot;https://packaging.python.org/en/latest/&quot;&gt;Python Packaging User Guide&lt;/a&gt; - Warehouse is taking full advantage of this by liberally linking to relevant documentation.&lt;/li&gt;
  &lt;li&gt;Instead of installing a feedback widget on the site, we decided to go with prominent links to open new GitHub issues, at least for now.&lt;/li&gt;
  &lt;li&gt;There has been a big focus on making it easier to dive into the Warehouse codebase, with &lt;a href=&quot;https://github.com/pypa/warehouse/labels/good%20first%20issue&quot;&gt;several “good first issue” items&lt;/a&gt; and an &lt;a href=&quot;https://warehouse.readthedocs.io/development/getting-started/&quot;&gt;improved “Getting Started” guide&lt;/a&gt;. Maintainer Ernest W. Durbin III will even &lt;a href=&quot;https://twitter.com/EWDurbin/status/955415184339849217&quot;&gt;help you with a one-on-one session and give you stickers&lt;/a&gt;!&lt;/li&gt;
  &lt;li&gt;After &lt;a href=&quot;https://github.com/pypa/warehouse/issues/402&quot;&gt;much discussion&lt;/a&gt; we removed localisation and internationalisation functionality from the codebase, but &lt;a href=&quot;https://github.com/pypa/warehouse/issues/1453&quot;&gt;are working on bringing it back in a sustainable way&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;You can follow &lt;a href=&quot;https://wiki.python.org/psf/WarehouseRoadmap&quot;&gt;the Warehouse development roadmap&lt;/a&gt; or watch &lt;a href=&quot;https://github.com/pypa/warehouse/projects/1&quot;&gt;our rollout board on GitHub&lt;/a&gt; to see as we progress towards launch.&lt;/li&gt;
&lt;/ul&gt;
</content>
        </entry>
    
        <entry>
            <title>Learning Python, Learning French - My Talk from PyConFr</title>
            <link href="http://www.whoisnicoleharris.com/2015/12/14/my-talk-at-pycon-fr.html"/>
            <updated>2015-12-14T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2015/12/14/my-talk-at-pycon-fr</id>
            <content type="html">&lt;p&gt;In October 2015, I had the opportunity to present at &lt;a href=&quot;http://www.pycon.fr/2015/&quot;&gt;PyConFr&lt;/a&gt; in the beautiful city of &lt;a href=&quot;https://en.wikipedia.org/wiki/Pau,_Pyr%C3%A9n%C3%A9es-Atlantiques&quot;&gt;Pau&lt;/a&gt;. My talk explored my experiences of learning Python and French at the same time and looked at what the Python community could take from the language learning community to better help new learners.&lt;/p&gt;

&lt;p&gt;I’d like to thank the organisers of PyConFr for having me - it was a terrific conference, and I was so happy to present my ideas to such a warm and welcoming group of Pythonistas. Thanks also to all those attendees who were patient with my less than perfect French, or happy to switch to English when I got stuck!&lt;/p&gt;

&lt;p&gt;The talk is in English, with a brief introduction in French (for which I have provided subtitles). Enjoy!&lt;/p&gt;

&lt;figure class=&quot;img-figure centered&quot;&gt;
    &lt;div class=&quot;embed-container&quot; style=&quot;padding-bottom: 56.25%&quot;&gt;
      &lt;iframe src=&quot;https://www.youtube.com/embed/n2WjDQaowBo&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
    &lt;/div&gt;
    &lt;figcaption&gt;Or watch on &lt;a href=&quot;https://www.youtube.com/watch?v=n2WjDQaowBo&quot;&gt;YouTube&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;And here is a copy of my slides for the curious:&lt;/p&gt;

&lt;div class=&quot;embed-container&quot; style=&quot;padding-bottom: 66.4%; top: 15px;&quot;&gt;
  &lt;iframe src=&quot;https://docs.google.com/presentation/d/1OiFhksppWqe_YAwx0OwAkocDCFeHmm5mQhvsjav35kQ/embed?start=false&amp;amp;loop=false&amp;amp;delayms=60000&quot; frameborder=&quot;0&quot; width=&quot;750&quot; height=&quot;497&quot; allowfullscreen=&quot;true&quot; mozallowfullscreen=&quot;true&quot; webkitallowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;
</content>
        </entry>
    
        <entry>
            <title>My First DjangoCon</title>
            <link href="http://www.whoisnicoleharris.com/2015/06/15/my-first-djangocon.html"/>
            <updated>2015-06-15T00:00:00+01:00</updated>
            <id>http://www.whoisnicoleharris.com/2015/06/15/my-first-djangocon</id>
            <content type="html">&lt;p&gt;DjangoCon Europe was held for a week in Cardiff, Wales. It was my first conference and I was very excited to finally meet a lot of people within the community who I knew via twitter, email and IRC.&lt;/p&gt;

&lt;h2 id=&quot;talks&quot;&gt;Talks&lt;/h2&gt;

&lt;p&gt;I enjoyed the vast majority of the talks and managed to take something out of almost every one of them. Without a doubt the most memorable talk was a lightning talk by Russell Keith-Magee, where he talked about his feelings of doubt and inadequacy and disclosed that he had been going though a major depressive episode. In his talk, Russell used his own position within the community to urge others to seek help if they need it - including with the Cardiff University wellbeing team who the organisers had arranged to be on hand for three days of the conference. Thank-you Russell, and get well soon :)&lt;/p&gt;

&lt;p&gt;On a practical level two other talks stood out to me - Hanna Kollo’s &lt;em&gt;Avoiding monoliths&lt;/em&gt; and David Winterbottom’s &lt;em&gt;How to write a view&lt;/em&gt;. Both were really talks about application and code structure - topics that I’ve been considering more and more as &lt;a href=&quot;/connect&quot;&gt;Connect&lt;/a&gt; has grown.&lt;/p&gt;

&lt;p&gt;A few other things I learned:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Load testing is not that difficult! If you can unit test you can load test. This was quite a revelation for me as I had always assumed it was some kind of dark art that only the most senior software engineers are qualified to practice. Turns out, it’s not and there are libararies that can help you do it.&lt;/li&gt;
  &lt;li&gt;Pytest is awesome and I should totally be using it. And (apparently) it’s not at all difficult to make the switch.&lt;/li&gt;
  &lt;li&gt;Security is hard…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;… and web accessibility is important!&lt;/p&gt;

&lt;h2 id=&quot;sprints&quot;&gt;Sprints&lt;/h2&gt;

&lt;p&gt;After speaking to a few people about &lt;a href=&quot;/connect&quot;&gt;Connect&lt;/a&gt;, I decided to open source &lt;a href=&quot;https://github.com/nlhkabu/connect&quot;&gt;the code&lt;/a&gt; with the intention of launching a mentoring platform for the Django community.&lt;/p&gt;

&lt;p&gt;After sorting out some installation bugs, we ended up with seven people hacking on the code and making general improvements. There’s lots &lt;a href=&quot;http://django-mentor-connect.readthedocs.org/en/latest/roadmap.html&quot;&gt;planned&lt;/a&gt; and I’m looking forward to leading my first open source project!&lt;/p&gt;

&lt;h2 id=&quot;community&quot;&gt;Community&lt;/h2&gt;

&lt;p&gt;Finally, I wanted to make a note on community. I was overwhelmed by the friendliness, generosity and openness of the community. Never once did I feel as though I shouldn’t be there, participating and contributing.
To all of the people I met - thank-you! You made my first DjangoCon awesome!&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>Beginning BDD with Django - Part Two</title>
            <link href="http://www.whoisnicoleharris.com/2015/03/19/bdd-part-two.html"/>
            <updated>2015-03-19T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2015/03/19/bdd-part-two</id>
            <content type="html">&lt;p&gt;This is the second in a two part series attempting to answer the questions:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Why should I consider using BDD?&lt;/li&gt;
  &lt;li&gt;What are the key concepts?&lt;/li&gt;
  &lt;li&gt;How can I use it to test my Django project?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the &lt;a href=&quot;http://whoisnicoleharris.com/2015/03/16/bdd-part-one.html&quot;&gt;first half of this series&lt;/a&gt; we outlined the benefits of BDD and scoped and wrote a Gherkin feature file for our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filter Users&lt;/code&gt; feature. In this part we’ll use &lt;a href=&quot;http://pythonhosted.org/behave/&quot;&gt;Behave&lt;/a&gt; to hook up our feature file to an automated test suite.&lt;/p&gt;

&lt;p&gt;This guide has been tested to work with the following stack:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Python 3.3&lt;/li&gt;
  &lt;li&gt;Django 1.7&lt;/li&gt;
  &lt;li&gt;Factory Boy 2.4.1&lt;/li&gt;
  &lt;li&gt;Splinter 0.7.0&lt;/li&gt;
  &lt;li&gt;Selenium 2.44.0&lt;/li&gt;
  &lt;li&gt;Django Behave 0.1.2&lt;/li&gt;
  &lt;li&gt;Phantom JS 1.9.8&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;contents&quot;&gt;Contents&lt;/h2&gt;

&lt;ul class=&quot;no_toc&quot;&gt;
  &lt;li&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;revisiting-our-feature-file&quot;&gt;Revisiting Our Feature File&lt;/h2&gt;

&lt;p&gt;For reference, let’s take a quick look at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filter Users&lt;/code&gt; feature we wrote in &lt;a href=&quot;http://whoisnicoleharris.com/2015/03/16/bdd-part-one.html&quot;&gt;the first part&lt;/a&gt; of this series:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.feature&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot; data-lang=&quot;gherkin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users by interest
As a standard user
I want to filter users by their listed interests
So I can find users who have similar interests to my own

&lt;span class=&quot;kn&quot;&gt;Background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; There are interests and users in the system
&lt;span class=&quot;err&quot;&gt;Given there are a number of interests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;interest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Django&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Testing&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Speaking&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DevOps&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PHP&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;

    &lt;span class=&quot;err&quot;&gt;And there are many users, each with different interests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;           &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;nv&quot;&gt;interests&lt;/span&gt;                  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Billie&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Jean&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Django,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Testing&lt;/span&gt;            &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Rocky&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Raccoon&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Django,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Speaking&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Major&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Tom&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Testing,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Devops&lt;/span&gt;            &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Bobbie&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;McGee&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Speaking,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DevOps&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;Scenario Outline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users
&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;I am a logged in user
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I filter the list of users by &lt;span class=&quot;nv&quot;&gt;&amp;lt;filter&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I see &lt;span class=&quot;nv&quot;&gt;&amp;lt;num&amp;gt;&lt;/span&gt; users

    &lt;span class=&quot;nn&quot;&gt;Examples&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;nv&quot;&gt;filter&lt;/span&gt;             &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;nv&quot;&gt;num&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Django&lt;/span&gt;             &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;2&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Django,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Testing&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;3&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;PHP&lt;/span&gt;                &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;0&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Remember that? Great! Let’s get started.&lt;/p&gt;

&lt;h2 id=&quot;dependencies&quot;&gt;Dependencies&lt;/h2&gt;

&lt;p&gt;First off, we’ll need to install our dependencies:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://pythonhosted.org/behave/&quot;&gt;Behave&lt;/a&gt; will run our BDD tests.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/django-behave/django-behave&quot;&gt;Django Behave&lt;/a&gt; will let us run our Behave tests via the Django test runner.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://phantomjs.org/&quot;&gt;PhantomJS&lt;/a&gt; will drive our interactions with the browser.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/cobrateam/splinter&quot;&gt;Splinter&lt;/a&gt; sits on top of PhantomJS (and others) and will help us write simpler, more elegant test code.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://factoryboy.readthedocs.org/en/latest/&quot;&gt;Factory Boy&lt;/a&gt; will allow us to generate Users and Interests to use in our tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After installing all of the above, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;settings.py&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;django-behave&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSTALLED_APPS&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TEST_RUNNER = 'django_behave.runner.DjangoBehaveTestSuiteRunner'&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;note-header&quot;&gt;
    &lt;i class=&quot;fa fa-exclamation-circle&quot;&gt;&lt;/i&gt;
    Note
&lt;/div&gt;
&lt;div class=&quot;note&quot;&gt;
    &lt;p&gt;We'll be using Django's built in test runner throughout this tutorial.  But if you prefer to use PyTest, then you should check out &lt;a href=&quot;https://github.com/pytest-dev/pytest-django&quot;&gt;pytest-django&lt;/a&gt; and &lt;a href=&quot;https://github.com/pytest-dev/pytest-bdd&quot;&gt;pytest-bdd&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;h2 id=&quot;folder-structure&quot;&gt;Folder Structure&lt;/h2&gt;

&lt;p&gt;Next we’ll need to create a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bdd&lt;/code&gt; app where we can save our existing feature file as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter_users.feature&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;project_root/
bdd/
init.py
features/
filter_users.feature
environment.py
steps/
filter_users.py&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;note-header&quot;&gt;
    &lt;i class=&quot;fa fa-exclamation-circle&quot;&gt;&lt;/i&gt;
    Note
&lt;/div&gt;
&lt;div class=&quot;note&quot;&gt;
    &lt;p&gt;We &lt;em&gt;could&lt;/em&gt; instead include feature folders inside individual existing Django applications.  However, utilising one central bdd application allows us to share the same environment for all of our tests, whilst accounting for situations where individual tests cases span multiple Django applications.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Remember to add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bdd&lt;/code&gt; to your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSTALLED_APPS&lt;/code&gt; in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;settings.py&lt;/code&gt; file.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-our-test-environment&quot;&gt;Setting Up Our Test Environment&lt;/h2&gt;

&lt;h3 id=&quot;creating-factories&quot;&gt;Creating Factories&lt;/h3&gt;

&lt;p&gt;Because we’ve already written our feature file, we know that we’ll need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Users&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interests&lt;/code&gt; in the database to run our test scenarios. To create these, we’ll use &lt;a href=&quot;https://factoryboy.readthedocs.org/en/latest/&quot;&gt;Factory Boy&lt;/a&gt; - a features replacement tool.&lt;/p&gt;

&lt;p&gt;We’ll setup our factories in the same application that our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interest&lt;/code&gt; models are defined:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;project_root/
accounts/
models.py # Our User and Interest models live here
factories.py # This is where we'll create our Factory Boy factories&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p class=&quot;code-heading&quot;&gt;factories.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;factory&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.contrib.auth.hashers&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;make_password&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;.models&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Interest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;User&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;django&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DjangoModelFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
Creates a standard active user.
&quot;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;User&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;first_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'Standard'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;last_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'User'&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Emails must be unique - so use a sequence here:
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Sequence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'user.{}@test.test'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;make_password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'pass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;is_active&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;

    &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_generation&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;interests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extracted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
        Where 'interests' are defined, add them to this user.
        &quot;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extracted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interest&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extracted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InterestFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;django&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DjangoModelFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Interest&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Sequence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'interest{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Let’s go over whats going on here:&lt;/p&gt;

&lt;p&gt;First, we define the model we want to instantiate by setting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model&lt;/code&gt; inside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;class Meta&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;Next, we define defaults for the corresponding model fields. In our example, all of our users will have the first name of ‘Standard’ unless we specify otherwise.&lt;/p&gt;

&lt;p&gt;For the email field (in our UserFactory) and name field (in our InterestFactory), we can use a &lt;a href=&quot;https://factoryboy.readthedocs.org/en/latest/introduction.html?highlight=sequence#sequences&quot;&gt;factory sequence&lt;/a&gt;, so that each object in our factory is unique. If we now create two instances of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InterestFactory&lt;/code&gt;, they will each have a unique name - the first will be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interest1&lt;/code&gt;, the second &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interest2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, to define the many-to-many relationship between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interest&lt;/code&gt;, we need to setup our interests as a method using the &lt;a href=&quot;https://factoryboy.readthedocs.org/en/latest/reference.html#factory.post_generation&quot;&gt;post_generation&lt;/a&gt; hook.&lt;/p&gt;

&lt;p&gt;Voila! Now we’re all set to create objects in our tests. For example, we can:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# Create a User with the default settings
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Will generate a user with the name 'Standard User'
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Create a User with a custom name
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;major_tom&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'Major'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;last_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'Tom'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Create a User with Interests
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;django&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InterestFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'Django'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;public_speaking&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InterestFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'Public Speaking'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;lucy_diamond&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'Lucy'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;last_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'Diamond'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;interests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;django&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;public_speaking&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;configuring-environmentpy&quot;&gt;Configuring environment.py&lt;/h3&gt;

&lt;p&gt;We can use our environment.py file to define what should happen before and after certain points in our tests. There are &lt;a href=&quot;http://pythonhosted.org/behave/api.html#environment-file-functions&quot;&gt;several hooks&lt;/a&gt; we can utilise, but for our example, we’re going to focus on:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_all&lt;/code&gt; Code defined here will run before all of our tests begin. We’ll use this hook to set up our browser.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_all&lt;/code&gt; Code defined here will run after all of our tests finish. We’ll use this hook to quit our browser.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_scenario&lt;/code&gt; Code defined here runs before each individual scenario. We’ll use this to setup (and teardown) our database. This will help us keep our data clean between each scenario.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our example:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;environment.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;behave&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;splinter.browser&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Browser&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.core&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;management&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;before_all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Unless we tell our test runner otherwise, set our default browser to PhantomJS
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'phantomjs'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# When we're running with PhantomJS we need to specify the window size.
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# This is a workaround for an issue where PhantomJS cannot find elements
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# by text - see: https://github.com/angular/protractor/issues/585
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;driver_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'PhantomJS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set_window_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1280&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;before_scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Reset the database before each scenario # This means we can create, delete and edit objects within an # individual scenerio without these changes affecting our # other scenarios
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;management&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;call_command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'flush'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;verbosity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interactive&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# At this stage we can (optionally) generate additional data to setup in the database.
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# For example, if we know that all of our tests require a 'SiteConfig' object,
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# we could create it here.
&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;after_all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Quit our browser once we're done!
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;context&lt;/code&gt; variable is an instance of &lt;a href=&quot;http://pythonhosted.org/behave/api.html#behave.runner.Context&quot;&gt;behave.runner.Context&lt;/a&gt;.
This variable holds additional contextual information during the running of tests, so we could also pass it additional information and retreive that value later.&lt;/p&gt;

&lt;h2 id=&quot;running-our-tests&quot;&gt;Running Our Tests&lt;/h2&gt;

&lt;p&gt;Now, we’ve setup our environment, we’re ready to run our tests!
In your terminal run:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;python manage.py test bdd&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;You’ll see:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Failing scenarios:
bdd/features/filter_users.feature:22 Filter users
bdd/features/filter_users.feature:22 Filter users
bdd/features/filter_users.feature:22 Filter users

0 features passed, 1 failed, 0 skipped
0 scenarios passed, 3 failed, 0 skipped
0 steps passed, 0 failed, 0 skipped, 15 undefined
Took 0m0.000s&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Why? Because Behave can’t find any instructions (known as steps) for each of our scenarios. Conveniently, Behave provides us with some default snippets. Copy these from your terminal and paste them into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter_users.py&lt;/code&gt; file - grouping common steps together:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;behave&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; \&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# We'll need to import all from behave first
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Then we can copy the snippets into our file
&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;given&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'there are a number of interests'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;given&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'there are many users, each with different interests'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;given&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I am a logged in user'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I filter the list of users by Django'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I filter the list of users by Django, Testing'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I filter the list of users by PHP'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I see 3 users'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I see 2 users'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I see 0 users'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Step functions are defined using step decorators, here shown as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@given&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@then&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@when&lt;/code&gt;. These are universally imported when you import Behave; you do not need to import them individually.&lt;/p&gt;

&lt;p&gt;Step decorators use a string to match your Gherkin feature file step - this must be an exact match for the test to run correctly.&lt;/p&gt;

&lt;p&gt;The decorated function (in this case &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;def impl()&lt;/code&gt;) can be named anything - It doesn’t matter. The only thing you must do is pass it the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;context&lt;/code&gt; that we mentioned earlier.&lt;/p&gt;

&lt;h2 id=&quot;writing-test-code&quot;&gt;Writing Test Code&lt;/h2&gt;

&lt;p&gt;Let’s go through each of our steps and write our test code.&lt;/p&gt;

&lt;h3 id=&quot;1-given-there-are-a-number-of-interests&quot;&gt;1. Given there are a number of interests&lt;/h3&gt;

&lt;p&gt;For this step, we’ll need to use our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InterestFactory&lt;/code&gt; to create the interests listed in our feature file. We can access the name of our interests by looping over each row in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;context.table&lt;/code&gt; using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interest&lt;/code&gt; column heading as a key.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;behave&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;accounts.factories&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InterestFactory&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;given&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'there are a number of interests'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;interests&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;InterestFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'interest'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;2-and-there-are-many-users-each-with-different-interests&quot;&gt;2. And there are many users, each with different interests&lt;/h3&gt;

&lt;p&gt;In this step we create our users by:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Splitting the items listed under our ‘interest’ heading into list items&lt;/li&gt;
  &lt;li&gt;Fetching the interests (that we created in our last step) from the database&lt;/li&gt;
  &lt;li&gt;Creating a new user with our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UserFactory&lt;/code&gt;, passing in the the interest objects&lt;/li&gt;
&lt;/ol&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;accounts.factories&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;accounts.models&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Interest&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;given&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'there are many users, each with different interests'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;interest_names&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'interests'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;', '&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;interests&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Interest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;\&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;\&lt;span class=&quot;n&quot;&gt;_in&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interest_names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'email'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;3-given-i-am-a-logged-in-user&quot;&gt;3. Given I am a logged in user&lt;/h3&gt;

&lt;p&gt;To log in a user, we navigate to the login page and interact with the login form. Here we can start to appreciate the power of &lt;a href=&quot;https://splinter.readthedocs.org/en/latest/index.html&quot;&gt;Splinter&lt;/a&gt; for browsing, finding and filling in form fields.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;accounts.factories&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;given&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I am a logged in user'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# First we need to create the user to login.
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_to_login&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'log.me.in@test.test'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# All properties (other than email) will be inherited from our UserFactory. # Therefore our password for this user will be 'pass'.
&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# We visit the login page
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# context.config.server_url is by default set to http://localhost:8081
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# (Thanks to Cynthia Kiser for pointing this out.)
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# In this example we're visiting http://localhost:8081/accounts/login/
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;server_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'accounts/login/'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Next, we log in our user by interacting with the login form
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# Splinter has a handy fill function that helps us fill form fields based
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# on their name.  We'll use it to fill in the username and password fields.
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'username'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_to_login&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'password'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'pass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Finally we find the submit button (by its CSS attribute) and click on it!
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;find_by_css&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'form input[type=submit]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;At this point it might be helpful to see our tests running in a ‘real’ browser. To do this, we need to install &lt;a href=&quot;http://docs.seleniumhq.org/&quot;&gt;Selenium&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now we can tell Splinter to run our tests using Firefox (rather than the default PhantomJS):&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ python ./manage.py test bdd --behave_browser firefox&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;note-header&quot;&gt;
    &lt;i class=&quot;fa fa-exclamation-circle&quot;&gt;&lt;/i&gt;
    Note
&lt;/div&gt;
&lt;div class=&quot;note&quot;&gt;
    &lt;p&gt;Running with Firefox is significantly slower than with PhantomJS, so unless I'm debugging, I tend to stick with running PhantomJS locally and test with other, heavier browsers on my continuous integration server.&lt;/p&gt;
&lt;/div&gt;

&lt;h3 id=&quot;4-when-i-filter-the-list-of-users-by-&quot;&gt;4. When I filter the list of users by …&lt;/h3&gt;

&lt;p&gt;We can combine each of our filter steps into one single step by using Behave’s &lt;a href=&quot;http://pythonhosted.org/behave/tutorial.html?highlight=context#step-parameters&quot;&gt;step parameters&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, we need to change our feature file, wrapping our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; variable in string formatting:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.feature&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot; data-lang=&quot;gherkin&quot;&gt;&lt;span class=&quot;kn&quot;&gt;Scenario Outline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users
    ...
    &lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I filter the list of users by &lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;filter&amp;gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This allows us to write one (and only one) step for each filter step:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I filter the list of users by &quot;{checked}&quot;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;checked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# First we visit the page where we see all the users.
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# In our example, this happens to be the root domain.
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;server_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Then we get the list of interests
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;checked&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;checked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;', '&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;check&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;checked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# And click on each label.
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# This code assumes we have a form where each interest is listed as a
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# label containing a checkbox.
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;//label[contains(.,'{}')]/input&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;find_by_xpath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Finally, we submit the form
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;find_by_css&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'form input[type=submit]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;5-then-i-see--users&quot;&gt;5. Then I see … users&lt;/h3&gt;

&lt;p&gt;Finally, we can use the same pattern to count the number of users in our results.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.feature&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot; data-lang=&quot;gherkin&quot;&gt;&lt;span class=&quot;kn&quot;&gt;Scenario Outline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users
    ...
    ...
    &lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I see &lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;num&amp;gt;&quot;&lt;/span&gt; users&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And in our python file:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'I see &quot;{count}&quot; users'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;impl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Assuming there is a &amp;lt;div class=&quot;user-card&quot;&amp;gt;&amp;lt;/div&amp;gt; for each user
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;find_by_css&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'.user-card'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# We can now assert that the number of users on the page
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# is equal to the number we expect
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;

&lt;p&gt;That’s it for our test code! Now it’s over to you to write application code to make these failing scenarios pass.&lt;/p&gt;

&lt;p&gt;I hope you’ve enjoyed reading these articles as much as I’ve enjoyed writing them. If you have any questions or comments, don’t hesitate to leave them below.&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>Beginning BDD with Django - Part One</title>
            <link href="http://www.whoisnicoleharris.com/2015/03/16/bdd-part-one.html"/>
            <updated>2015-03-16T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2015/03/16/bdd-part-one</id>
            <content type="html">&lt;p&gt;For the last year I’ve been learning Django by building &lt;a href=&quot;/connect/&quot;&gt;Connect&lt;/a&gt;. Part of this journey has been to explore various automated testing methods and tools. Behaviour Driven Development (BDD) is one technique that really spoke to me and was surprisingly simple to integrate into my application.&lt;/p&gt;

&lt;p&gt;This article is the first of two parts that aim to pass on what I have learnt so far. It is not an in-depth exploration of the philosophy or best-practice application of BDD, but rather a highly practical example to get you started.&lt;/p&gt;

&lt;p&gt;By the end of both articles, I hope you will be able to answer the following questions:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Why should I consider using BDD?&lt;/li&gt;
  &lt;li&gt;What are the key concepts?&lt;/li&gt;
  &lt;li&gt;How can I use it to test my app?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s do it!&lt;/p&gt;

&lt;h2 id=&quot;contents&quot;&gt;Contents&lt;/h2&gt;

&lt;ul class=&quot;no_toc&quot;&gt;
  &lt;li&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;why-bdd&quot;&gt;Why BDD&lt;/h2&gt;

&lt;p&gt;I’m not going to go into why you should test your application. I assume that by reading this article you’re already sold on the benefits of testing. Instead I’m going to focus on why you should specifically consider &lt;em&gt;BDD&lt;/em&gt; for your next app.&lt;/p&gt;

&lt;p&gt;To do this, we need to step back and ask ourselves &lt;em&gt;why&lt;/em&gt; are we building software in the first place and &lt;em&gt;how&lt;/em&gt; we can assess the success of our application.&lt;/p&gt;

&lt;p&gt;In a great talk titled &lt;a href=&quot;http://businessofsoftware.org/2013/02/kathy-sierra-building-the-minimum-badass-user-business-of-software-a-masterclass-in-thinking-about-software-product-development/&quot; data-proofer-ignore=&quot;&quot;&gt;building the minimum badass user&lt;/a&gt;, Kathy Sierra argues that a successful application is one that people &lt;em&gt;want to use&lt;/em&gt; and &lt;em&gt;tell their friends about&lt;/em&gt;. The key to delivering this? Making our &lt;strong&gt;user&lt;/strong&gt; awesome:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“People aren’t using the app because they like the app or they like you. They’re doing it because they like themselves.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kathy argues that users will endure less than perfect design, or technical implementation &lt;em&gt;if the app makes them more awesome&lt;/em&gt; at the task at hand.&lt;/p&gt;

&lt;p&gt;For developers, the key take-away is that we need to be constantly thinking about how we can empower our users.&lt;/p&gt;

&lt;h2 id=&quot;how-bdd-can-help&quot;&gt;How BDD Can Help&lt;/h2&gt;

&lt;p&gt;BDD sits at the cusp of user experience and development, as it forces us to frame the software we’re developing in the context of the user.&lt;/p&gt;

&lt;p&gt;It helps us step back from the technical implementation and focus on these questions:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Who am I?&lt;/li&gt;
  &lt;li&gt;What can I do?&lt;/li&gt;
  &lt;li&gt;What benefits can I derive from my action?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A common template for defining this is:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;As a &amp;lt;role&amp;gt;
I want &amp;lt;feature&amp;gt;
So that &amp;lt;value&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This template also accounts for scenarios where the role is not a user, but rather a system or process (for example, an API).&lt;/p&gt;

&lt;h2 id=&quot;an-example&quot;&gt;An Example&lt;/h2&gt;

&lt;p&gt;To illustrate how we can transition from user-centered design thinking to writing test code and developing our application, we’re going to walk through an example from &lt;a href=&quot;/connect/&quot;&gt;Connect&lt;/a&gt;; a social application that helps users connect with each other based on common interests and location.&lt;/p&gt;

&lt;p&gt;Let’s imagine that we’ve built most of the app, but are yet to develop one of the most important features: filtering users by interests.&lt;/p&gt;

&lt;p&gt;Using the pattern defined earlier, we might describe this feature as:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;As a standard user
I want to filter users by their listed interests
So I can find users who have similar interests to my own&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;feature-files&quot;&gt;Feature Files&lt;/h3&gt;

&lt;p&gt;BDD can be segmented into two parts: the feature file, where we describe the behaviour we are going to build, and the code (in our case Python) where we test our application.&lt;/p&gt;

&lt;p&gt;First we’ll need to write our feature file. Feature files use the &lt;a href=&quot;http://pythonhosted.org/behave/gherkin.html#gherkin-feature-testing-language&quot;&gt;Gherkin syntax&lt;/a&gt; and are saved as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;featurename&amp;gt;.feature&lt;/code&gt;:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;example.feature&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot; data-lang=&quot;gherkin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; feature name
  description
  further description

&lt;span class=&quot;kn&quot;&gt;Background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; some requirement of this test
&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;some setup condition
&lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;some other setup action

&lt;span class=&quot;kn&quot;&gt;Scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; some scenario
&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;some condition
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;some action is taken
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;some result is expected&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;lines-1-3-defining-the-feature&quot;&gt;Lines 1-3: Defining the Feature&lt;/h4&gt;

&lt;p&gt;Technically, only the feature name is required here.
However, I also like to describe the feature using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;As a, I want, So that&lt;/code&gt; pattern established earlier.
This keeps our user’s needs front of mind.&lt;/p&gt;

&lt;h4 id=&quot;lines-5-7-setting-up-the-background&quot;&gt;Lines 5-7: &lt;a href=&quot;http://pythonhosted.org/behave/gherkin.html#background&quot;&gt;Setting Up the Background&lt;/a&gt;&lt;/h4&gt;

&lt;p&gt;A feature’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Background&lt;/code&gt; outlines the contextual information we need for the scenarios that follow.
For our example, we’re going to need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interests&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Users&lt;/code&gt; in the database so we can filter our users.&lt;/p&gt;

&lt;p&gt;Not all features will require a background, so this section is entirely optional.&lt;/p&gt;

&lt;h4 id=&quot;lines-9-12-writing-scenarios&quot;&gt;Lines 9-12: &lt;a href=&quot;http://pythonhosted.org/behave/gherkin.html#features&quot;&gt;Writing Scenarios&lt;/a&gt;&lt;/h4&gt;

&lt;p&gt;Finally, we outline the different scenarios our users might face.&lt;/p&gt;

&lt;p&gt;For our example, we need to consider what will happen when the user filters by one, or multiple interests. We also need to account for when there are no users that match the request.&lt;/p&gt;

&lt;p&gt;Each Scenario contains three types of steps:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Given&lt;/code&gt;: Defines the state of the system&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;When&lt;/code&gt;: Defines the action the user or system wants to take&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Then&lt;/code&gt;: Defines the outcome of the action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More complex scenarios can also include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;And&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;But&lt;/code&gt; keywords. &lt;em&gt;For example&lt;/em&gt;:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;activate_account.feature&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot; data-lang=&quot;gherkin&quot;&gt;&lt;span class=&quot;kn&quot;&gt;Scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Activate account
    &lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;I am a standard user
    &lt;span class=&quot;nf&quot;&gt;But &lt;/span&gt;I have not yet activated my account
    &lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I visit my account activation page
    &lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;I fill out the form
    &lt;span class=&quot;nf&quot;&gt;But &lt;/span&gt;I forget to confirm my password
    &lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;I submit the form
    &lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I see an error&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting It All Together&lt;/h3&gt;

&lt;p&gt;Here’s our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter users&lt;/code&gt; feature written as a Gherkin feature file:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.feature&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot; data-lang=&quot;gherkin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users by interest
As a standard user
I want to filter users by their listed interests
So that I can find users who share my interests

&lt;span class=&quot;kn&quot;&gt;Background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; There are interests and users in the system
&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;there are a number of interests in the database
&lt;span class=&quot;nf&quot;&gt;And &lt;/span&gt;there are many users in the database, each with different interests

&lt;span class=&quot;kn&quot;&gt;Scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users by one interest
&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;I am a logged in user
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I filter the list of users by a single interest
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I only see the users with that interest

&lt;span class=&quot;kn&quot;&gt;Scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users by multiple interests
&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;I am a logged in user
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I filter the list of users by multiple interests
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I see the users with those interests

&lt;span class=&quot;kn&quot;&gt;Scenario&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;No &lt;/span&gt;result
&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;I am a logged in user
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I filter the list of users by an interest that no-one has listed
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I see no users&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;refactoring&quot;&gt;Refactoring&lt;/h4&gt;

&lt;p&gt;You’ve probably noticed that our scenarios use a common pattern. They’re also a little vague.
To fix this we can include more context in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Background&lt;/code&gt; and refactor our three scenarios into one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Scenario Outline&lt;/code&gt;:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;filter_users.feature&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot; data-lang=&quot;gherkin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users by interest
As a standard user
I want to filter users by their listed interests
So I can find users who have similar interests to my own

&lt;span class=&quot;kn&quot;&gt;Background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; There are interests and users in the system
&lt;span class=&quot;err&quot;&gt;Given there are a number of interests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;interest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Django&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Testing&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Speaking&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DevOps&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PHP&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;

    &lt;span class=&quot;err&quot;&gt;And there are many users, each with different interests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;           &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;nv&quot;&gt;interests&lt;/span&gt;                  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Billie&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Jean&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Django,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Testing&lt;/span&gt;            &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Rocky&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Raccoon&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Django,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Speaking&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Major&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Tom&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Testing,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Devops&lt;/span&gt;            &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Bobbie&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;McGee&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Speaking,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DevOps&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;Scenario Outline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; Filter users
&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;I am a logged in user
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I filter the list of users by &lt;span class=&quot;nv&quot;&gt;&amp;lt;filter&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I see &lt;span class=&quot;nv&quot;&gt;&amp;lt;num&amp;gt;&lt;/span&gt; users

    &lt;span class=&quot;nn&quot;&gt;Examples&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;nv&quot;&gt;filter&lt;/span&gt;             &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;nv&quot;&gt;num&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Django&lt;/span&gt;             &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;2&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Django,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Testing&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;3&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;PHP&lt;/span&gt;                &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;0&lt;/span&gt;      &lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;First we define the interests and users in the background using tables. This makes our data easy to read, whilst providing hooks that we’ll use later in our Python code.&lt;/p&gt;

&lt;p&gt;Next, we compile our scenarios into one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Scenario Outline&lt;/code&gt;, specifying the skills and number of users based on the information defined in our new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Background&lt;/code&gt;.
When we run our tests, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Scenario&lt;/code&gt; will be created out of each (non heading) line in the example table. So, the first line would become…&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot; data-lang=&quot;gherkin&quot;&gt;&lt;span class=&quot;nf&quot;&gt;Given &lt;/span&gt;I am a logged in user
&lt;span class=&quot;nf&quot;&gt;When &lt;/span&gt;I filter the list of users by Django
&lt;span class=&quot;nf&quot;&gt;Then &lt;/span&gt;I see 2 users&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;… and so on.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;That’s it! We’ve defined our feature and written our feature file!&lt;/p&gt;

&lt;p&gt;In the &lt;a href=&quot;http://whoisnicoleharris.com/2015/03/19/bdd-part-two.html&quot;&gt;next post&lt;/a&gt; we’ll explore how we can write Python code to utilise our feature file and run it as an automated test.&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>Tutorial : Implementing Django Formsets</title>
            <link href="http://www.whoisnicoleharris.com/2015/01/06/implementing-django-formsets.html"/>
            <updated>2015-01-06T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2015/01/06/implementing-django-formsets</id>
            <content type="html">&lt;p&gt;I’ve noticed on #django IRC that many people need guidance on formsets, and as I’ve now used them in a couple of my projects — most recently, &lt;a href=&quot;/connect/&quot;&gt;Connect&lt;/a&gt; — I thought I could offer a short how-to based on my own experiences.&lt;/p&gt;

&lt;p&gt;Firstly, if you haven’t already, go and &lt;a href=&quot;https://docs.djangoproject.com/en/1.7/topics/forms/formsets/&quot;&gt;read the docs&lt;/a&gt;. If you’re still confused, or want an end-to-end tutorial, then read on. The code contained in this tutorial has been tested to work with Django 1.7.&lt;/p&gt;

&lt;h2 id=&quot;contents&quot;&gt;Contents&lt;/h2&gt;

&lt;ul class=&quot;no_toc&quot;&gt;
  &lt;li&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-does-a-formset-do&quot;&gt;What Does a Formset Do?&lt;/h2&gt;

&lt;p&gt;Formsets are for dealing with sets of identical data. For example in Connect, I have a form where the user can save multiple links to their public profile, with each link having both a URL and an anchor:&lt;/p&gt;

&lt;figure class=&quot;img-figure&quot;&gt;
    &lt;img src=&quot;/assets/img/formset-animation.gif&quot; alt=&quot;Animation of a formset in action&quot; /&gt;
    &lt;figcaption&gt;A Django formset in action.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I also want:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The formset to be nested &lt;em&gt;within&lt;/em&gt; the user’s profile form.&lt;/li&gt;
  &lt;li&gt;The user to add or remove as many links as they like.&lt;/li&gt;
  &lt;li&gt;Custom validation checking that no anchor or URL is entered more than once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Django comes with a number of ‘batteries included’ formsets. There are &lt;a href=&quot;https://docs.djangoproject.com/en/1.7/topics/forms/modelforms/#model-formsets&quot;&gt;formsets for models&lt;/a&gt; and &lt;a href=&quot;https://docs.djangoproject.com/en/1.7/topics/forms/modelforms/#model-formsets&quot;&gt;formsets for models related by a foreign key&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This how-to, however, is going to focus on creating a standard formset using custom forms&lt;/em&gt;.&lt;/p&gt;

&lt;h2 id=&quot;step-1-create-your-forms&quot;&gt;Step 1. Create Your Forms&lt;/h2&gt;

&lt;p&gt;First we need to set out our link form. This is just a standard Django form.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;forms.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LinkForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
Form for individual user links
&quot;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CharField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;max_length&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;widget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TextInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'placeholder'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'Link Name / Anchor Text'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;URLField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;widget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;URLInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'placeholder'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'URL'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As our formset will need to be nested inside a profile form, let’s go ahead and create that now:&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;forms.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProfileForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
    Form for user to update their own profile details
    (excluding links which are handled by a separate formset)
    &quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'user'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ProfileForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fields&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'first_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CharField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                                        &lt;span class=&quot;n&quot;&gt;max_length&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                        &lt;span class=&quot;n&quot;&gt;initial&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                        &lt;span class=&quot;n&quot;&gt;widget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TextInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                                            &lt;span class=&quot;s&quot;&gt;'placeholder'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'First Name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                        &lt;span class=&quot;p&quot;&gt;}))&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fields&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'last_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CharField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                                        &lt;span class=&quot;n&quot;&gt;max_length&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                        &lt;span class=&quot;n&quot;&gt;initial&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                        &lt;span class=&quot;n&quot;&gt;widget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TextInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                                            &lt;span class=&quot;s&quot;&gt;'placeholder'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'Last Name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                        &lt;span class=&quot;p&quot;&gt;}))&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;step-2-create-your-formset&quot;&gt;Step 2. Create Your Formset&lt;/h2&gt;

&lt;p&gt;For this particular example, we’re going to add some validation to our formset, as we want to ensure that there are no duplicate URLs or anchors.&lt;/p&gt;

&lt;p&gt;We also want to verify that all links have both an anchor and URL. We &lt;em&gt;could&lt;/em&gt; simply set the fields as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;required&lt;/code&gt; on the form itself, &lt;em&gt;however&lt;/em&gt; this will prevent our users from submitting empty forms, which is not the behaviour we’re looking for here. From a usability perspective, it would be better to simply &lt;em&gt;ignore&lt;/em&gt; forms that are completely empty, raising errors &lt;em&gt;only if&lt;/em&gt; a form is partially incomplete.&lt;/p&gt;

&lt;p&gt;If you don’t want any custom validation on your formset, you can skip this step entirely.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;forms.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.forms.formsets&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BaseFormSet&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BaseLinkFormSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BaseFormSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;clean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
Adds validation to check that no two links have the same anchor or URL
and that all links have both an anchor and URL.
&quot;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;anchors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;urls&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;duplicates&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cleaned_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cleaned_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'anchor'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cleaned_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'url'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

                &lt;span class=&quot;c1&quot;&gt;# Check that no two links have the same anchor or URL
&lt;/span&gt;                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;duplicates&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;anchors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

                    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;urls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;duplicates&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;urls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duplicates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;s&quot;&gt;'Links must have unique anchors and URLs.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'duplicate_links'&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

                &lt;span class=&quot;c1&quot;&gt;# Check that all links have both an anchor and URL
&lt;/span&gt;                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;s&quot;&gt;'All links must have an anchor.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'missing_anchor'&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;s&quot;&gt;'All links must have a URL.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'missing_URL'&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;step-3-hook-up-your-view&quot;&gt;Step 3. Hook Up Your View&lt;/h2&gt;

&lt;p&gt;Now we can use Django’s built in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formset_factory&lt;/code&gt; to generate our formset.
As the name suggests, this function takes a form and returns a formset. At its most basic, we &lt;em&gt;only&lt;/em&gt; need to pass it the form we want to repeat - in this case our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LinkForm&lt;/code&gt;. However, as we have created a custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BaseLinkFormSet&lt;/code&gt;, we &lt;em&gt;also&lt;/em&gt; need to tell our factory to use this instead of using Django’s default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BaseFormSet&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In our example, we also want our formset to display all of the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UserLinks&lt;/code&gt; for the logged in user. To do this, we need to build a dict of our user’s links and pass this as our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initial_data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To save our data we can build a list of UserLinks and save this to the user’s profile using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bulk_create&lt;/code&gt; method. Wrapping this code in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transaction&lt;/code&gt; will avoid a situation where the old links are deleted, but the connection to the database is lost before the new links are created.&lt;/p&gt;

&lt;p&gt;We are also going to use the &lt;a href=&quot;https://docs.djangoproject.com/en/1.7/ref/contrib/messages/&quot;&gt;messages framework&lt;/a&gt; to tell our users whether their profile was updated.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;views.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.contrib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.contrib.auth.decorators&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;login_required&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.core.urlresolvers&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reverse&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.db&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IntegrityError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transaction&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.forms.formsets&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formset_factory&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.shortcuts&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;myapp.forms&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LinkForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BaseLinkFormSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ProfileForm&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;myapp.models&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserLink&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;login_required&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_profile_settings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
Allows a user to update their own profile.
&quot;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Create the formset, specifying the form and formset we want to use.
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;LinkFormSet&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formset_factory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LinkForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BaseLinkFormSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Get our existing link data for this user.  This is used as initial data.
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;user_links&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserLink&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;order_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'anchor'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;link_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'anchor'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'url'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_links&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;method&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'POST'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;profile_form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ProfileForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;link_formset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LinkFormSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profile_form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;is_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_formset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;is_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# Save user info
&lt;/span&gt;            &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profile_form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cleaned_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'first_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profile_form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cleaned_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'last_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

            &lt;span class=&quot;c1&quot;&gt;# Now save the data for each form in the formset
&lt;/span&gt;            &lt;span class=&quot;n&quot;&gt;new_links&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_form&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_formset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cleaned_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'anchor'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cleaned_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'url'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;new_links&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UserLink&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;atomic&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
                    &lt;span class=&quot;c1&quot;&gt;#Replace the old with the new
&lt;/span&gt;                    &lt;span class=&quot;n&quot;&gt;UserLink&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;UserLink&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bulk_create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_links&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

                    &lt;span class=&quot;c1&quot;&gt;# And notify our users that it worked
&lt;/span&gt;                    &lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'You have updated your profile.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IntegrityError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;#If the transaction failed
&lt;/span&gt;                &lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'There was an error saving your profile.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reverse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'profile-settings'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;profile_form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ProfileForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;link_formset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LinkFormSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;initial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;link_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'profile_form'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profile_form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'link_formset'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_formset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'our_template.html'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;step-4-html--js&quot;&gt;Step 4. HTML / JS&lt;/h2&gt;

&lt;p&gt;Now that we have passed our formset to our template, we can use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forloop&lt;/code&gt; to
display each of our forms.&lt;/p&gt;

&lt;p&gt;An additional (but not necessarily obvious) step here is to include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{ link_formset.management_form }}&lt;/code&gt;. This is used by Django to manage the forms within the formset.&lt;/p&gt;

&lt;p&gt;My personal preference is to individually specify each form field so I can wrap additional HTML around it, but you can also use the standard shortcuts, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{ form.as_p }}&lt;/code&gt; within a formset.&lt;/p&gt;

&lt;p&gt;We also want to use &lt;a href=&quot;https://github.com/elo80ka/django-dynamic-formset&quot;&gt;this jQuery plugin&lt;/a&gt; for dynamically adding and removing forms. Full documentation can be found &lt;a href=&quot;https://github.com/elo80ka/django-dynamic-formset/blob/master/docs/usage.rst&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;edit_profile.html&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;{% load staticfiles %}

{% if messages %}
{% for message in messages %}
&lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ message }}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
{% endfor %}
{% endif %}

&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;method=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;post&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    {% csrf_token %}

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;label&amp;gt;&lt;/span&gt;First Name&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    {{ profile_form.first_name }}
    {% if profile_form.first_name.errors %}
        {% for error in profile_form.first_name.errors %}
            {{ error|escape }}
        {% endfor %}
    {% endif %}

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Last Name&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    {{ profile_form.last_name }}
    {% if profile_form.last_name.errors %}
        {% for error in profile_form.last_name.errors %}
            {{ error|escape }}
        {% endfor %}
    {% endif %}

    {{ link_formset.management_form }}

    {% for link_form in link_formset %}
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;link-formset&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
            {{ link_form.anchor }}
            {% if link_form.anchor.errors %}
                {% for error in link_form.anchor.errors %}
                    {{ error|escape }}
                {% endfor %}
            {% endif %}

            {{ link_form.url }}
            {% if link_form.url.errors %}
                {% for error in link_form.url.errors %}
                    {{ error|escape }}
                {% endfor %}
            {% endif %}
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    {% endfor %}

    {% if link_formset.non_form_errors %}
        {% for error in link_formset.non_form_errors %}
            {{ error|escape }}
        {% endfor %}
    {% endif %}

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;submit&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Update Profile&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Include formset plugin - including jQuery dependency --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;{% static 'path_to/jquery.formset.js' %}&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.link-formset&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;formset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;addText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;add link&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;deleteText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;unit-testing&quot;&gt;Unit Testing&lt;/h2&gt;

&lt;p&gt;Let’s set up some basic unit tests to make sure everything is working correctly.&lt;/p&gt;

&lt;p&gt;As the profile form is available only to authenticated users, we’ll use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup&lt;/code&gt; method to create and login a user. In the examples below I’ve used &lt;a href=&quot;https://github.com/rbarrois/factory_boy&quot;&gt;factory boy&lt;/a&gt; to generate a dummy user.&lt;/p&gt;

&lt;p&gt;Most of the examples below are &lt;em&gt;variations&lt;/em&gt; on posting the same data either to the view or the form directly. For this reason, much of this functionality has been split into separate helper functions.&lt;/p&gt;

&lt;h4 id=&quot;test-the-profile-form&quot;&gt;Test the Profile Form&lt;/h4&gt;

&lt;p&gt;We can test the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProfileForm&lt;/code&gt; by passing data variations to the object and checking for validation errors.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;tests/test_forms.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.test&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TestCase&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;myapp.factories&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;myapp.forms&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ProfileForm&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProfileFormTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setUp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'pass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;form_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ProfileForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'first_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'last_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_valid_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'First'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'Last'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertTrue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;is_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_missing_first_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'Last'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'first_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;as_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'required'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_missing_last_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'First'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'last_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;as_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'required'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;test-the-formset&quot;&gt;Test the Formset&lt;/h4&gt;

&lt;p&gt;We can test our formset by either:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Passing data to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProfileForm&lt;/code&gt; (for this to work we &lt;strong&gt;must&lt;/strong&gt; include the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TOTAL_FORMS&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INITIAL_FORMS&lt;/code&gt; settings that are generated by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;management_form&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Posting data directly to the view. This allows us to check for specific errors using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assertFormsetError&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p class=&quot;code-heading&quot;&gt;tests/test_forms.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.core.urlresolvers&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reverse&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.test&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TestCase&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;myapp.factories&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;myapp.forms&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ProfileForm&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LinkFormsetTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setUp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'pass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;form_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ProfileForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'first_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'First'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'last_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'Last'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-TOTAL_FORMS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-INITIAL_FORMS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-0-anchor'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-0-url'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;post_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;reverse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'test:profile-settings'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-TOTAL_FORMS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-INITIAL_FORMS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-0-anchor'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-0-url'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-1-anchor'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;anchor2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;'form-1-url'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;raise_formset_error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertFormsetError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;formset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'link_formset'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;form_index&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_valid_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'My Link'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'http://mylink.com'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertTrue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;is_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_empty_fields&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
        Test validation passes when no data is provided
        (data is not required).
        &quot;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertTrue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;is_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_duplicate_anchors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
        Test validation fails when an anchor is submitted more than once.
        &quot;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'My Link'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'http://mylink.com'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                  &lt;span class=&quot;s&quot;&gt;'My Link'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'http://mylink2.com'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;raise_formset_error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                 &lt;span class=&quot;s&quot;&gt;'Links must have unique anchors and URLs.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_duplicate_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
        Test validation fails when a URL is submitted more than once.
        &quot;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'My Link'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'http://mylink.com'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                  &lt;span class=&quot;s&quot;&gt;'My Link2'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'http://mylink.com'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;raise_formset_error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                 &lt;span class=&quot;s&quot;&gt;'Links must have unique anchors and URLs.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_anchor_without_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
        Test validation fails when a link is submitted without a URL.
        &quot;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'My Link'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;raise_formset_error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'All links must have a URL.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_url_without_anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
        Test validation fails when a link is submitted without an anchor.
        &quot;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'http://mylink.com'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;raise_formset_error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'All links must have an anchor.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;testing-our-view&quot;&gt;Testing Our View&lt;/h4&gt;

&lt;p&gt;Finally, we need to check that when we do submit valid data, that data is saved to our user’s profile.&lt;/p&gt;

&lt;p class=&quot;code-heading&quot;&gt;tests/test_views.py&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.core.urlresolvers&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reverse&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.test&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TestCase&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;myapp.factories&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;myapp.models&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserLink&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProfileSettingsTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_can_update_profile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'pass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;reverse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'test:profile-settings'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'first_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'New First Name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'last_name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'New Last Name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'form-TOTAL_FORMS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'form-INITIAL_FORMS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'form-0-anchor'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'My Link'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'form-0-url'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'http://mylink.com'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Get the user again
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;user_link&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UserLink&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'New First Name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'New Last Name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'My Link'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'http://mylink.com/'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;That’s it! We have a working &lt;em&gt;tested&lt;/em&gt; formset saving our user’s links. If you found this article useful, please share it. If you have a comment or question, please get in touch!&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>2014&#58; My Year of Python</title>
            <link href="http://www.whoisnicoleharris.com/2014/12/29/2014-my-year-of-python.html"/>
            <updated>2014-12-29T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2014/12/29/2014-my-year-of-python</id>
            <content type="html">&lt;p&gt;At the end of 2013 (and after much convincing from &lt;a href=&quot;https://github.com/ojh&quot;&gt;ojh&lt;/a&gt;), I decided to make 2014 my ‘year of Python’. I chose a year because I felt that would be enough time to get involved to see if I liked the work and the community - and make a decision as to whether Python (and web development in general) is for me.&lt;/p&gt;

&lt;p&gt;Here’s what I did, what I’ve learnt and where I’m going:&lt;/p&gt;

&lt;h2 id=&quot;community&quot;&gt;Community&lt;/h2&gt;

&lt;h3 id=&quot;pyladies&quot;&gt;PyLadies&lt;/h3&gt;

&lt;p&gt;I was disappointed that there wasn’t an active Pyladies Australia - so (with some encouragement from the Pyladies mothership) I decided to form one myself. I set up a &lt;a href=&quot;http://australia.pyladies.com/&quot;&gt;website&lt;/a&gt; and mailing list to which 30+ women have registered, which is a great starting point. I had hoped that I would find a whole heap of other women interested in helping out (running workshops, etc.), but people are busy (!) so at this stage things are fairly dormant. Being in Bendigo, I can’t personally run local workshops (I just don’t think there would be any interest here), so that’s been a bit of a sticking point. Circumstances also meant that I couldn’t attend PyCon Australia, which was a missed opportunity to meet people and get stuff happening.&lt;/p&gt;

&lt;p&gt;At this stage I feel that the only way PyLadies Australia is going to move forward is if I put in a huge amount of personal time and energy - something I have to weigh up the benefits of.&lt;/p&gt;

&lt;h3 id=&quot;pycon-australia&quot;&gt;Pycon Australia&lt;/h3&gt;

&lt;p&gt;This year I also volunteered to design the PyCon Australia 2014 logo and website. This was to be my first significant contribution what I thought would be an open-source project - though, unfortunately, the software the site is built upon is private.&lt;/p&gt;

&lt;h4 id=&quot;what-worked&quot;&gt;What Worked&lt;/h4&gt;

&lt;p&gt;The process of putting together the logo was fairly straightforward as I had total creative freedom in this respect. The website itself was first designed in HTML, CSS &amp;amp; JS and then integrated into the CMS - a system that worked really well.&lt;/p&gt;

&lt;h4 id=&quot;what-id-do-differently&quot;&gt;What I’d Do Differently&lt;/h4&gt;

&lt;p&gt;In the end the branding was a bit all-over-the-place, as I didn’t have oversight on all visual materials. It would have been nice to have a list of &lt;em&gt;all&lt;/em&gt; of the places the logo would be used to make sure everything was visually coherant. For example the logo on the slide templates really should have sat on a white background.&lt;/p&gt;

&lt;p&gt;New pages to the website were published without being checked first, which sometimes meant we had non-responsive elements thrown into the page content. Some kind of design-review-before-publish mechanism may have been helpful.&lt;/p&gt;

&lt;p&gt;I would have also liked some more time to put together a better layout for the conference schedule as this didn’t work on mobile at all.&lt;/p&gt;

&lt;p&gt;Finally, as mentioned earlier, personal circumstances meant that I didn’t actually get to attend PyCon, which was a real let down - next time I’d definitely like to be there!&lt;/p&gt;

&lt;p&gt;Overall, I’d say that my interactions with the Python community have been positive and I am keen to continue to meet people and share my skills.&lt;/p&gt;

&lt;h2 id=&quot;code&quot;&gt;Code&lt;/h2&gt;

&lt;p&gt;My main project for the year was &lt;a href=&quot;/connect&quot;&gt;Connect&lt;/a&gt; - a web application written in Django that helps people connect with each other based on interests and location.&lt;/p&gt;

&lt;p&gt;My inspiration for the project was PyLadies Australia - as the women who expressed an interest in participating were located all across the country. Quickly though, I realised that Connect could be used internationally and for a number of different community groups and organisations.&lt;/p&gt;

&lt;p&gt;On a professional front, our business also completed two projects for &lt;a href=&quot;/bpcletool&quot;&gt;BPCLEtool&lt;/a&gt;, for which I was able to contribute small amounts of Python code.&lt;/p&gt;

&lt;p&gt;Working with Django has been an absolute delight and I am really starting to appreciate the ‘Pythonic’ way of doing things. Having a community consensus on what constitutes good code is &lt;em&gt;immensely&lt;/em&gt; helpful. Much of the syntax seems naturally logical and I like the way that Django encourages modular, structured code.&lt;/p&gt;

&lt;h2 id=&quot;learning-how-i-learn&quot;&gt;Learning How I Learn&lt;/h2&gt;

&lt;p&gt;I am well aware that learning Python via Django is a bad idea - but I’ve learnt that coding exercises like Katas don’t really do it for me. Fundamentally, my reason for learning Python is not to just learn Python - it’s to build stuff! For myself, or for other people.&lt;/p&gt;

&lt;p&gt;I think I’ve always known this, but this year has really put that into focus for me. I learn best by trying things - breaking things - then fixing things. On several occasions I tried &lt;em&gt;really&lt;/em&gt; hard to do tutorials, read books, or follow videos - but unless they were really engaging, well written (I’m looking at you official Django tutorial), or REALLY pertinent to the issue at hand, I often petered out. Fundamentally, my motivation wanes if I don’t have some kind of creative input into what I’m doing. Also - I &lt;em&gt;like&lt;/em&gt; problem solving. I &lt;em&gt;like&lt;/em&gt; searching for the answers. And I &lt;em&gt;like&lt;/em&gt; reading about different perspectives on the same problem.&lt;/p&gt;

&lt;p&gt;So, in searching for answers, here are some of the resources I’ve found most valuable:&lt;/p&gt;

&lt;h3 id=&quot;the-official-django-tutorial&quot;&gt;&lt;a href=&quot;https://docs.djangoproject.com/en/1.7/intro/tutorial01/&quot;&gt;The Official Django Tutorial&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;This seems pretty obvious, right? Apparently not. The number of people on IRC asking basic questions in #django without having done the official tutorial is pretty surprising. I suppose that might be because other frameworks and resources are not nearly as well documented as Django. I think the tutorial is great - it finds a good balance between not too complex and not too simple. Also - (unlike many other tutorials) I found once I had finished, I could actually go and BUILD something. In fact, when I was first starting out, I’d often refer to the tutorial instead of the docs, because I knew where the information I was looking for was located.&lt;/p&gt;

&lt;h3 id=&quot;test-driven-development-with-python&quot;&gt;&lt;a href=&quot;http://chimera.labs.oreilly.com/books/1234000000754&quot;&gt;Test-Driven Development with Python&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;Even though I’ve found that tutorial-books aren’t my ideal learning tool, there is enough helpful content here to make following along worthwhile. There are lots of code examples (which I love) and the author does a really good job of selling the benefits of TDD.&lt;/p&gt;

&lt;h3 id=&quot;two-scoops-of-django&quot;&gt;Two Scoops of Django&lt;/h3&gt;

&lt;p&gt;We’ve had this floating around the house for a while and though it was a bit much to take in when I first started, it is a great resource to come back to. I’m quite keen to get into good habits, so am constantly trying to learn about best-practices. It’s hard to find this kind of information online (because everybody has their own opinion), so it’s nice to have it bundled up and presented from well respected members of the Django community.&lt;/p&gt;

&lt;h3 id=&quot;secrets-of-the-testing-masters&quot;&gt;&lt;a href=&quot;http://pyvideo.org/video/2244/secrets-of-the-testing-masters&quot;&gt;Secrets of the Testing Masters&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;The first half of this talk was a real revelation for me - as I was stuck on how to test &lt;a href=&quot;/connect&quot;&gt;Connect&lt;/a&gt; without getting lost in fixtures. Hello &lt;a href=&quot;https://factoryboy.readthedocs.org/&quot;&gt;FactoryBoy&lt;/a&gt;! :D&lt;/p&gt;

&lt;p&gt;This is just my favourite of &lt;em&gt;many&lt;/em&gt; videos I have watched on &lt;a href=&quot;http://pyvideo.org/&quot;&gt;pyvideo&lt;/a&gt; which is an AMAZING resource.&lt;/p&gt;

&lt;h2 id=&quot;the-future&quot;&gt;The Future&lt;/h2&gt;

&lt;p&gt;I’m certainly keen to keep learning and engaging with the Python community. I’d like to see more mentoring going on, which I hope &lt;a href=&quot;/connect&quot;&gt;Connect&lt;/a&gt; can help with. Personally, I’d like to offer my skills and knowledge to help others - not only because I feel this is a great way of giving back, but also because this will help me to cement my knowledge.&lt;/p&gt;

&lt;p&gt;On a technical front, I’m currently exploring all manners of techniques and strategies for testing - at the moment that means BDD with &lt;a href=&quot;http://pythonhosted.org/behave/&quot;&gt;behave&lt;/a&gt;. I’m excited and optimistic about my future with Python.&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>You Are Not Your Code</title>
            <link href="http://www.whoisnicoleharris.com/2014/12/18/you-are-not-your-code.html"/>
            <updated>2014-12-18T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2014/12/18/you-are-not-your-code</id>
            <content type="html">&lt;p&gt;Programming is hard. And learning to program is even harder.&lt;/p&gt;

&lt;p&gt;And it seems to me that the more experienced you become, the harder the problems get.&lt;/p&gt;

&lt;p&gt;Since I started to learn to program, I’ve found it hard to separate professional successes and failures from my own self worth. When something goes right I feel fabulous. When I get stuck on something for days or weeks (hello timezones), then I begin to question whether or not I am ‘cut out’ for it, or if I’m really ‘smart’ enough.&lt;/p&gt;

&lt;h2 id=&quot;the-myth-of-the-rockstar-developer&quot;&gt;The Myth of the ‘Rockstar’ Developer&lt;/h2&gt;

&lt;p&gt;Sometimes it’s easy to assume that there is an elite group of developers - the job ads call them rockstars or ninjas - that have been coding since birth. They’ve mastered 15 languages and can code in in their sleep.&lt;/p&gt;

&lt;p&gt;But the more I read about this - the more I’ve realised this is a myth. Geniuses are few and far between and most successful people get to where they are with a combination of hard work and a lot of good luck.&lt;/p&gt;

&lt;h2 id=&quot;redefining-success---a-reminder-to-myself&quot;&gt;Redefining Success - A Reminder to Myself&lt;/h2&gt;

&lt;p&gt;Comparing yourself to this myth is damaging. Demanding perfection in your code is even worse. Learning to be a great software developer is a journey and there are many things you can bring to the table that don’t require being a ‘rockstar’:&lt;/p&gt;

&lt;h3 id=&quot;your-attitude&quot;&gt;Your Attitude&lt;/h3&gt;

&lt;p&gt;If you are truly enthusiastic about being the best you can be, you’re already half way there. Curiosity, professionalism, courtesy, honesty, and integrity are essential.&lt;/p&gt;

&lt;h3 id=&quot;your-soft-skills&quot;&gt;Your ‘Soft Skills’&lt;/h3&gt;

&lt;p&gt;I really dislike this phrase. What’s ‘soft’ about being able to communicate your ideas effectively? What’s ‘soft’ about being able to resolve conflict? What’s ‘soft’ about being a great leader? As technology and projects change, chances are your soft skills will continue to be of value.&lt;/p&gt;

&lt;h3 id=&quot;your-capacity-and-flexibility-to-learn-new-things&quot;&gt;Your Capacity (and Flexibility) to Learn New Things&lt;/h3&gt;

&lt;p&gt;Technologies change. Requirements change. You are going to face many, many problems that you have little to no idea how to solve. Having a ‘can do’ attitude is essential. So is the humility to know that you don’t have the best answer but are willing to look for it.&lt;/p&gt;

&lt;h3 id=&quot;your-delivery&quot;&gt;Your Delivery&lt;/h3&gt;

&lt;p&gt;Have you delivered something that is important to yourself, others, society? This is infinitely more valuable than perfection in your code or anything else.&lt;/p&gt;

&lt;p&gt;That’s not to say you shouldn’t strive to &lt;a href=&quot;https://www.youtube.com/watch?v=p0O1VVqRSK0&quot; data-proofer-ignore=&quot;&quot;&gt;be a professional&lt;/a&gt;. To deliver clean, maintainable code. You should.
But remember that you are not your code. You are so much more.&lt;/p&gt;
</content>
        </entry>
    
        <entry>
            <title>On Creativity</title>
            <link href="http://www.whoisnicoleharris.com/2014/11/18/on-creativity.html"/>
            <updated>2014-11-18T00:00:00+00:00</updated>
            <id>http://www.whoisnicoleharris.com/2014/11/18/on-creativity</id>
            <content type="html">&lt;p&gt;I have always considered myself to be creative. One of my earliest memories was drawing in my family lounge room at a child-size portable desk - it was bright yellow and had compartments on the side where I kept my pencils, crayons and textas.&lt;/p&gt;

&lt;p&gt;My parents encouraged my creativity - I always had supplies and tried lots of things - sewing, cooking, cake decorating, painting, beading, pottery, paper making. I likened myself to my Grandma or Aunty Julie who between them taught me that being creative was fun and fulfilling.&lt;/p&gt;

&lt;h2 id=&quot;stem--creativity&quot;&gt;STEM != Creativity&lt;/h2&gt;

&lt;p&gt;At school we had one lesson on HTML in IT. Other lessons focused on touch typing, Excel, Word and a tiny bit of hardware.&lt;/p&gt;

&lt;p&gt;Growing up, ‘computing’ was the opposite of creative. In fact, like all science and technology, computing was boring (this deeply incorrect perception is a story for another day). To me, a career working with computers meant systems administration. This was not surprising as in my hometown, the only people I knew who worked in ‘computers’ were the IT teacher, the school systems administrator and the people who sold my parents our first computer. They were all men. This probably didn’t help.&lt;/p&gt;

&lt;h2 id=&quot;seeing-the-light&quot;&gt;Seeing the Light&lt;/h2&gt;

&lt;p&gt;I got into web by accident - I wanted to create an online portfolio of my university work animations (I did a degree in film/photography/digital media) and having asked my then boyfriend for Dreamweaver, was presented with an Ubuntu install and book on HTML. My first portfolio was a disaster and I nearly gave up when I saw it in ie6. I cried when someone on a forum told me I had 350 errors in my HTML. But after a few more sites and a great deal of confusion, I had a great portfolio of online work to snag my first job in the industry.&lt;/p&gt;

&lt;p&gt;I absolutely adore the creativity of designing websites. I love that I can craft something from nothing. I love mixing with colours, typography, shapes, and images. I love thinking about how the site is going to solve a problem, or communicate with the user. I love the satisfaction of knowing that people are actually using the thing that I built.&lt;/p&gt;

&lt;p&gt;Even so, for a long time I avoided learning back-end development. I still had it in the ‘dry’, ‘boring’ and ‘difficult’ box. And that’s the thing - all the tutorials and books &lt;em&gt;were&lt;/em&gt; boring (because they built things I had no interest in making) or poorly written, or difficult. They all lacked creativity.&lt;/p&gt;

&lt;p&gt;After a lot of encouragement from my then-boyfriend-now-husband I finally decided to give it a go. I named 2014 my ‘&lt;a href=&quot;/2014/12/29/2014-my-year-of-python.html&quot;&gt;year of python&lt;/a&gt;’ and set about building an application to teach myself Django.&lt;/p&gt;

&lt;h2 id=&quot;down-the-rabbit-hole&quot;&gt;Down the Rabbit Hole&lt;/h2&gt;

&lt;p&gt;I didn’t have many expectations about learning Django. I knew it was a good framework and that Python is a great first language to learn. I hoped that I would like it, but had no idea how much personal and professional satisfaction I would gain by hacking away at &lt;a href=&quot;/connect&quot;&gt;my application&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To my surprise I quickly learnt that web development is innately CREATIVE. And - by extension - that web developers are creative people. After all - what is programming if not coming up with creative solutions to problems?&lt;/p&gt;

&lt;p&gt;The binary of creative vs technical that was so ingrained in my education (where most students are funneled into either an ‘arts’ or ‘STEM’ stream) fell apart. I realised that I &lt;em&gt;could&lt;/em&gt; be a web developer, because I didn’t need to identify as a technical person - I could just be me.&lt;/p&gt;
</content>
        </entry>
    
</feed>
