Adding a field to the profile

= 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:

$ git log -1 # look at the most recent commit on the current branch commit d56a354403026a1c3ecd634f2731e7d98a9513ff Author: Asheesh Laroia  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:       web site mysite/static/sample-data/open-opensolaris-bug.html: 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 %} web site {{ person.homepage_url|break$ {% endif %}

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

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:

birthday January 1, 1980

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 @@          {% endif %} +       birthday +       January 1, 1980 +          {% if person.irc_nick %} irc nick 

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: person.irc_nick = edit_info_form['irc_nick'].data
 * 1) grab the irc nick

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. person.irc_nick = edit_info_form['irc_nick'].data
 * 1) grab the irc nick

person.birthday = edit_info_form['birthday'].data
 * 1) grab the birthday

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 %} birthday  {% endif %} {% if person.irc_nick %} irc nick  {% 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.

Birthday: Date must be entered in YYYY-MM-DD format. Example: 1980-01-01

IRC nick: Example: sufjan

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: <div class="example" $git status


 * 1) On branch tutorial
 * 2) Changes not staged for commit:
 * 3)   (use "git add ..." to update what will be committed)
 * 4)   (use "git checkout -- ..." to discard changes in working directory)
 * 5) 	modified:  mysite/profile/forms.py
 * 6) 	modified:  mysite/profile/models.py
 * 7) 	modified:  mysite/profile/templates/profile/base_profile.html
 * 8) 	modified:  mysite/profile/templates/profile/info_wrapper.html
 * 9) 	modified:  mysite/profile/views.py
 * 1) 	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.

$ ./bin/mysite 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. $ bin/mysite 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

$ bin/mysite 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 nothing added to commit but untracked files present (use "git add" to track)
 * 1) On branch tutorial
 * 2) Untracked files:
 * 3)   (use "git add ..." to include in what will be committed)
 * 4) 	mysite/indexes/
 * 5) 	mysite/profile/migrations/0090_auto__add_field_person_birthday.py
 * 1) 	mysite/profile/migrations/0090_auto__add_field_person_birthday.py

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! $ bin/mysite test profile.EditBirthday



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 no changes added to commit (use "git add" and/or "git commit -a")
 * 1) On branch tutorial
 * 2) Changes not staged for commit:
 * 3)   (use "git add ..." to update what will be committed)
 * 4)   (use "git checkout -- ..." to discard changes in working directory)
 * 5) 	modified:  mysite/profile/tests.py
 * 6) Untracked files:
 * 7)   (use "git add ..." to include in what will be committed)
 * 8) 	mysite/indexes/
 * 9) 	mysite/static/twisted-ping-dir/twisted-ping-file
 * 1) 	mysite/indexes/
 * 2) 	mysite/static/twisted-ping-dir/twisted-ping-file
 * 1) 	mysite/static/twisted-ping-dir/twisted-ping-file

$ 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.