Adding a field to the profile

From OpenHatch wiki

This is a page about improving or modifying OpenHatch.

We call that "Hacking OpenHatch," and there is a whole category of pages about that.


The set-up


Today, on July 4, 2011, my OpenHatch profile doesn't have a special field for my birthday. When I go to edit my profile, there's no place to put it in!

This tutorial walks you through adding that feature to the July 4, 2011 version of the OpenHatch code.

Together, we will change the OpenHatch source code, which builds on top of Django and Python. You'll see how to go all the way from a new feature idea to making and testing the change. On the way, you'll see how to search, study, and test your code.

The goal of this exercise is to show you the thought process we use when changing the website, give you confidence to try similar things on your own, and expose you to some helpful techniques that you might not know about.

Skills you need

In order to do this tutorial, you need to be willing to use a terminal and willing to learn some programming. The instructions are written for GNU/Linux systems, but Mac OS users will probably find the instructions work okay. (Windows users, I'm not sure.)

Getting help

If you try this and you get stuck, come come chat with us on IRC. Explain what you're doing, and what's not working, and we'll try to get you un-stuck. The same goes for if you can do the exercises but don't understand what they mean.


Getting the source

All the sample code in this walkthrough refers to version d56a354 of the OpenHatch source. To make sure the code is in line with what you expect, follow the instructions on the getting started with the OpenHatch code page. But right after you do the "git clone", do these commands:

$ git checkout d56a354 # Switch to that revision
$ git branch tutorial # Create a branch called tutorial that points at that revision
$ git checkout tutorial # Switch into that branch.

Here's how you can check that it worked. Run these commands in your new oh-mainline directory, and check that they have the same output as I've recorded:

TODO: before continuing need to setup the DB for older version of code that you are trying to run

 The command is: python manage.py migrate #####

How to determine what ##### should be? (bug paulproteus on IRC)

$ git log -1 # look at the most recent commit on the current branch
  commit d56a354403026a1c3ecd634f2731e7d98a9513ff
  Author: Asheesh Laroia <asheesh@asheesh.org>
  Date:   Wed Jun 29 11:38:31 2011 -0400
   Add a config option that enables cookie sharing for Vanilla ProxyRequest-bas
$ git branch # see what the current branch is named.
  master
* tutorial


So far, so good? Let's dive in. (If not, chat with us on IRC!)

So that you and I see the same things, modify my actual profile! Import a data snapshot from July 4, 2011 (or the most recent one before this date)!

The 'What': What are we doing, again?

On your own development instance, open up Mark's OpenHatch profile in a browser tab. Inside the "Info" box, there are sections like my "bio" and "web site". It should look like the screenshot at the top of this page.

Let's add a new one, "birthday".

The 'Where': finding the right spot to make changes

Now we know what we want to see changed, but we still have to figure out what files to modify.

One way to find that out is to be methodical: we can open the urls.py file in a text editor. Since it maps web URLs into Python code that gets run, it is the starting point of how requests get dispatched in the OpenHatch source. Then you open up the appropriate view, find the template file it loads, and then you'll know what to edit.

Honestly, that sounds like a lot of work. I usually just search instead.

I'll warn you, though, that OpenHatch code can seem to be a sprawling mess. We'll use the "git grep" command to search it.

First, a word about git

Before you start using it, tell it who you are. When you do a commit, git will store this information in the repository.

 $ git config --global user.name "Your name"
 $ git config --global user.email "your.email.address@example.com"

Some tips on "git grep" before we begin:

  • It's like grep but accelerates the search by using the git repository you download.
  • It can color the matches.
  • To learn more, type "git grep --help" into your terminal. It will probably open a full-screen window; you can move up and down with the arrow keys. Quit by typing q.

Search 1: Find the template file that generates the info box

Let's search the code on your computer for the string 'web site'.

 $ git grep --color 'web site'
 mysite/expect-deploy:status "\r\n*** Reloading the web site... ***\r\n"
 mysite/profile/templates/profile/base_profile.html:        <h4>web site</h4>
 mysite/static/sample-data/open-opensolaris-bug.html: <a href="http://hub.opensolaris.org/bin/view/Main/help">Hel

These all start with mysite. That's where the OpenHatch code all lives.

One of these looks like a template file: base_profile.html. Open it up in a text editor, and you'll see this snippet:

        {% if person.homepage_url %}
        <h4>web site</h4>
        <p>
            <a rel="me" href="{{ person.homepage_url|prepend_http_if_necessary }}">{{ person.homepage_url|break$
        </p>
        {% endif %}

That seems to be what generates the person homepage link. It's in the context of a

<div id='info' class='module'>

Edit 1: Let's give everyone the same birthday

Okay, so let's edit the template so that every profile page says their birthday is January 1, 1980.

Just jam this right below the homepage section:

        <h4>birthday</h4>
        <p>January 1, 1980</p>

You'll get a box that looks like the picture. Note the birthday now shows in the profile block!

(You can read more about Django templates in the online Django book.)

Commit 1: Save our work

You have made some changes! Let's ask git to show them to us:

 $ git diff --color
  diff --git a/mysite/profile/templates/profile/base_profile.html b/mysite/profile/templates/
  index 1503658..864b3a7 100644
  --- a/mysite/profile/templates/profile/base_profile.html
  +++ b/mysite/profile/templates/profile/base_profile.html
  @@ -176,6 +176,9 @@
           </p>
           {% endif %}
   
  +        <h4>birthday</h4>
  +        <p>January 1, 1980</p>
  +
           {% if person.irc_nick %}
           <h4>irc nick</h4>
           <p id="ircnick" style='clear: both;'>

Let's save that change, and give it a name:

 $ git commit -a -m "base_profile.html: Now everyone's profile says they were born on January 1, 1980."

Making the new field dynamic

Edit 2: Adding a field to the Person model

If you take a look at template you just edited, you will notice that the person's website was referred to as "person.homepage_url". Since you also know that the code we want to edit is most likely in the "profile" module, you should start by looking in models.py, located in the "mysite/profile/" folder.

Open models.py and search for "homepage_url". The results of this search should leave you just inside of the Person class. If you look at the surrounding code, you should also notice other familiar fields (take a look back at the template changes you just made), such as irc_nick. Now you should feel confident that this is exactly where you need to add a new field for birthday.

At the end of the field definitions, just following the line that adds irc_nick, add a new field for birthday. According to the Django documentation, you want to use a DateField for storing dates. You will also want to tell the model that it is OK for someone to leave the field blank. You can do that by appending (blank=True) onto the new field.

  birthday = models.DateField(blank=True, null=True)


Edit 3: Adding the new field to the View

Adding a field to the model will ensure that we have a place to store the new field. You now need a way to access and update the data in this field. In Django, this is the responsibility of the view.

Find the view.py file located in the "mysite/profile" folder. Since you know that another field, 'irc_nick' is handled the same way as you want the new birthday field handled, you can easily find the right places to make changes to the code by searching for 'irc_nick'. Do that now and you will find the field added in the following functions.

In the edit_person_info_do(request) function, you will find the following line:

 # grab the irc nick
 person.irc_nick = edit_info_form['irc_nick'].data

Now edit that to add in a similar line for our new birthday field. Note: We will soon add the new field to the form.

 # grab the irc nick
 person.irc_nick = edit_info_form['irc_nick'].data

 # grab the birthday
 person.birthday = edit_info_form['birthday'].data

Continuing your search, you will notice that the 'irc_nick' field also appears in the edit_info function. Continue to make a change so that our birthday field appears there as well. Note: Don't forget the comma at the end of the line!

 'homepage_url': person.homepage_url,
 'irc_nick': person.irc_nick,
 'birthday': person.birthday,
 'understands': data['tags_flat'].get('understands', ''),

Edit 4: Adding the Form

As you saw while adding the birthday field to the view, a form is being used by thew view to capture the data entered by the user. If you look in the edit_person_info_do(request) function, you should notice this line:

 edit_info_form = mysite.profile.forms.EditInfoForm(request.POST, prefix='edit-tags')

This tells us that in the forms.py file (mysite.profile.forms.EditInfoForm), there is a class, EditInfoForm, which may need to be changed.

Open up the 'mysite/forms.py' file and search for EditInfoForm. You should notice a line added for 'irc_nick', so go ahead and edit the class to add a new line for the 'birthday' field.

 irc_nick = django.forms.CharField(required=False, widget=django.forms.TextInput())
 birthday = django.forms.DateField(required=False, widget=django.forms.DateInput())
 understands = django.forms.CharField(required=False, widget=django.forms.Textarea())


Edit 5: Adding the template changes

Earlier you added a birthday to the base_profile.py file. This changed allowed you to see the birthday when viewing a profile. Now let's take it a step further and change it so the birthday is retrieved from the person's profile rather than being hard coded to January 1, 1980!

Open up '/mysite/profile/templates/profile/base_profile.html' and find the spot where you made your changes earlier. Remember, you want to mimic the way that the existing homepage_url and irc_nick fields work. Go ahead and do that now.

Did you end up with something like this?

{%if person.birthday %}
  <h4>birthday</h4>
  <p id="birthday" style='clear: both;'>
    {{ person.birthday }}
  </p>
{% endif %}
   
{% if person.irc_nick %}
 <h4>irc nick</h4>
 <p id="ircnick" style='clear: both;'>
   {{ person.irc_nick }}
 </p>
{% endif %}

Now let's see if there is anywhere else this field may need to be displayed to the user. Remember the 'git grep' command from before? Let's run that again and see if we find anywhere else we need to make our changes.

$ git grep --color 'homepage_url'

You should see some entries to 'mysite/profile/templates/profile/info_wrapper.html'. This html file is the page a user visits to update their information. You definitely need to add something here! Find the section where homepage_url and irc_nick have been added and you will again mimic the way this is set up and add some new code for the user to enter their birthday.

<div class='form-row'>
     <label>Birthday:</label>
     {{ form.birthday.errors }}
     {{ form.birthday }}
     <p class='example'>Date must be entered in YYYY-MM-DD format. Example: 1980-01-01</p>
</div>

<div class='form-row'>
    <label>IRC nick:</label>
    {{ form.irc_nick.errors }}
    {{ form.irc_nick }}
    <p class='example'>Example: sufjan</p>
</div>

Commit 2: Saving our work before making database changes

Next we need to sync the database and create the migration file. This is a good time for you to save your work. So, let's do that now.

Run 'git status' and you should see all of the files you have edited up to now:

$git status

# On branch tutorial
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	modified:   mysite/profile/forms.py
#	modified:   mysite/profile/models.py
#	modified:   mysite/profile/templates/profile/base_profile.html
#	modified:   mysite/profile/templates/profile/info_wrapper.html
#	modified:   mysite/profile/views.py

Now we can tell git to commit our changes:

$git commit -a -m "Added the birthday field to the profile: forms.py, models.pyviews.py and the templates: base_profile.html and info_wrapper.html."

[tutorial f38a12b] Added the birthday field to the profile: forms.py, models.pyviews.py and the templates: base_profile.html and info_wrapper.html.
 5 files changed, 18 insertions(+), 1 deletions(-)

Managing the Database

Edit 6: Syncing the database and creating a migration

Django has the ability to automatically save model changes to the database, however will only do this for new models. Once a model has been created, you must either manually make the change, or use a tool such as 'South' to make the change for you. South is what we refer to when speaking of migrations. Go ahead and try to sync the database and you will see the mysite.profiles does not get updated, even though you have added a new field to the model.

$ python manage.py syncdb
Syncing...
No fixtures found.

Synced:
 > ghettoq
 > django.contrib.auth
 > django.contrib.contenttypes
 > django.contrib.sessions
 > django.contrib.sites
 > django.contrib.webdesign
 > django.contrib.admin
 > registration
 > django_authopenid
 > django_extensions
 > south
 > django_assets
 > celery
 > invitation
 > haystack
 > voting
 > reversion
 > debug_toolbar
 > sessionprofile
 > model_utils

Not synced (use migrations):
 - mysite.search
 - mysite.profile
 - mysite.customs
 - mysite.account
 - mysite.base
 - mysite.project
 - mysite.missions
(use ./manage.py migrate to migrate these)

Now run the migration as described here.

$ python manage.py schemamigration profile --auto
 + Added field birthday on profile.Person
Created 0090_auto__add_field_person_birthday.py. You can now apply this migration with: ./manage.py migrate profile

$ python manage.py migrate profile
Running migrations for profile:
 - Migrating forwards to 0090_auto__add_field_person_birthday.
 > profile:0090_auto__add_field_person_birthday
2011-07-06 20:16:02,282 execute:209 DEBUG    south execute "ALTER TABLE `profile_person` ADD COLUMN `birthday` date NULL;" with params "[]"
2011-07-06 20:16:02,283 execute:209 DEBUG    south execute "SET FOREIGN_KEY_CHECKS=1;" with params "[]"
2011-07-06 20:16:02,286 execute:209 DEBUG    south execute "ALTER TABLE `profile_person` ADD COLUMN `birthday` date NULL;" with params "[]"
2011-07-06 20:16:02,701 execute:209 DEBUG    south execute "ALTER TABLE `profile_person` ;" with params "[]"
2011-07-06 20:16:02,703 execute:209 DEBUG    south execute "ALTER TABLE `profile_person` MODIFY `birthday` date NULL;;" with params "[]"
2011-07-06 20:16:02,714 execute:209 DEBUG    south execute "ALTER TABLE `profile_person` ALTER COLUMN `birthday` DROP DEFAULT;" with params "[]"
 - Loading initial data for profile.
No fixtures found.

Commit 3: Save the migration

Running 'git status' will show you that a couple of changes have taken place.

$ git status
# On branch tutorial
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	mysite/indexes/
#	mysite/profile/migrations/0090_auto__add_field_person_birthday.py
nothing added to commit but untracked files present (use "git add" to track)

Next you will want to add the migration to the files to be committed, however ignore the index changes. Those should occur automatically once the patch is applied and migrations are run against the openhatch core codebase.

$ git add mysite/profile/migrations/0090_auto__add_field_person_birthday.py

$ git commit -m "Migration for adding birthday to Person model."
[tutorial 1cfc527] Migration for adding birthday to Person model.
 1 files changed, 218 insertions(+), 0 deletions(-)
 create mode 100644 mysite/profile/migrations/0090_auto__add_field_person_birthday.py


Testing

Through all this, we've just been hacking and slashing until things work the way we want. But the OpenHatch code has all these automated tests, and we won't deploy new code that doesn't have automated tests written for it. Each module contains a tests.py, which is provided for just this purpose.

You are going to be writing a Twill test, which is a way of automating the steps a user would take when entering and saving data through the web site. Go ahead and edit mysite/profile/tests.py and search for 'irc_nick' and you will find an existing Twill test for the 'irc_nick' field. Use that as your template for writing one for the birthday field.

This test will do the following:

  • Set up a data fixture to be used with the test
  • Go to paulproteus's profile
  • Check that a birthday doesn't already exist
  • Click edit on the birthday form
  • Enter the birthday as a string
  • Save the changes
  • bring up the profile and verify the birthday is still there.
    • Note: Django will format the date as May 26, 1977
class EditBirthday(TwillTests):
    fixtures = ['user-paulproteus', 'person-paulproteus']

    def test(self):
        '''  
        * Goes to paulproteus's profile
        * checks that they don't already have a birthday that says "1977-05-26"
        * clicks edit on the birthday area
        * enters a string as birthday
        * checks that his birthday now contains string
        '''
        self.login_with_twill()
        tc.go(make_twill_url('http://openhatch.org/people/paulproteus/'))
        tc.notfind('May 26, 1977')
        tc.go(make_twill_url('http://openhatch.org/profile/views/edit_info'))
        # make sure our birthday is not already on the form
        tc.notfind('1977-05-26')
        # set the birthday in the form
        tc.fv("edit-tags", 'edit-tags-birthday', '1977-05-26')
        tc.submit()
        self.assertEqual(Person.get_by_username('paulproteus').birthday,
                datetime.date(1977, 5, 26)) 
        # now we should see our birthday in the edit form
        tc.go(make_twill_url('http://openhatch.org/profile/views/edit_info'))
        tc.find('1977-05-26')

Now run the test and let's make sure it passes!

$ python manage.py test profile.EditBirthday

   <test information will print here...>

Installed 12 object(s) from 1 fixture(s)
sh: /usr/sbin/postmap: not found
.
----------------------------------------------------------------------
Ran 1 test in 8.578s

OK
Destroying test database 'default'...


Commit 4: Save the tests

Go ahead and commit the tests.py file.

$ git status
# On branch tutorial
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	modified:   mysite/profile/tests.py
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	mysite/indexes/
#	mysite/static/twisted-ping-dir/twisted-ping-file
no changes added to commit (use "git add" and/or "git commit -a")

$ git add mysite/profile/tests.py

$ git commit -m "Added a test for the Person.birthday field."
[tutorial 9b13221] Added a test for the Person.birthday field.
 1 files changed, 26 insertions(+), 0 deletions(-)

Submitting a patch

At OpenHatch, code changes are accepted through patches. These should be uploaded and attached to the particular issue tracker item you are working on.

First, you should generate the patch file which includes all of your changes. Since you are making all of your changes as part of a branch, this process is easy! First, make sure you are in the tutorial branch and then create the patch file as seen below.

$ git branch
  master
* tutorial

$ git format-patch master --stdout > tutorial.patch

This will create the patch file for you to upload to your tracker item.

That it! For more information on using git and creating a patch, check out our git mission.

Wrapping it up

Welcome to the OpenHatch community. We are very glad to have you here! be sure to chat with us on IRC and say hello or ask any questions you may have.