Ticket #16779: contrib-tutorial-3.txt

File contrib-tutorial-3.txt, 24.0 KB (added by Taavi Taijala, 13 years ago)

Fixed some incorrect links

Line 
1===================================
2Writing your first patch for Django
3===================================
4
5Now that you've finished writing your first Django app, how about giving back to the community a little? Maybe you've found a bug in Django that you'd like to see fixed, or maybe there's a small feature you want added.
6
7Contributing back to Django itself is the best way to see your own concerns addressed. This may seem daunting at first, but it's actually pretty simple. We'll walk you through the entire process, so you can learn by example.
8
9In this tutorial, we'll walk through contributing a patch to Django for the first time. By the end of this tutorial, you should understand both the tools and the process involved. We'll be covering the following:
10
11 * The tools you'll need to get started.
12 * How to write your first patch.
13 * Where to find more information.
14
15Once you're done with the tutorial, you can look through the rest of :doc:`Django's documentation on contributing<internals/contributing/index>`. It contains lots of great information and is a must read for anyone who'd like to become a regular contributor to Django. If you've got questions, it's probably got the answers.
16
17.. admonition:: Where to get help:
18
19 If you're having trouble going through this tutorial, please post a message
20 to `django-users`__ or drop by `#django on irc.freenode.net`__ to chat
21 with other Django users who might be able to help.
22
23__ http://groups.google.com/group/django-users
24__ irc://irc.freenode.net/django
25
26What you'll need to get started
27===============================
28
29Subversion or Git
30-----------------
31
32For this tutorial, you'll need to have either Subversion or Git installed. These tools will be used to download the current development version of Django and to generate patch files for the changes you make.
33
34To check whether or not you have one of these tools already installed, enter either ``svn`` or ``git`` into the command line. If you get messages saying that neither of these commands could be found, you'll have to download and install one of them yourself.
35
36You can download and install Subversion from the `Apache's Subversion Binary Packages page`__.
37
38Alternatively, you can download and install Git from `Git's download page`__.
39
40__ http://subversion.apache.org/packages.html
41__ http://git-scm.com/download
42
43How to write your first patch
44=============================
45
46Checking out Django's development version
47-----------------------------------------
48
49The first step to contributing to Django is to check out the Django's current development revision using either Subversion or Git.
50
51.. note::
52
53 For this tutorial, we'll be using `ticket 15315`__ as a case study, so we'll be using an older version of Django from before this ticket's patch was applied. This will allow us to go through all of the steps involved in writing that patch from scratch, including running Django's test suite. Keep in mind that while we'll be checking out an **older** revision of Django's trunk below, you should always check out the **current** development revision of Django when working on your own patch for another ticket.
54
55 The patch for this ticket was generously written by SardarNL and Will Hardy, and it was applied to Django as `changeset 16659`__. Consequently, we'll be checking out the revision just prior to that, revision 16658.
56
57From the command line, use the ``cd`` command to navigate to the directory where you'll want your local copy of Django to live.
58
59__ https://code.djangoproject.com/ticket/15315
60__ https://code.djangoproject.com/changeset/16659
61
62Using Subversion
63~~~~~~~~~~~~~~~~
64
65If you have Subversion installed, you can run the following command to download revision 16658 of Django:
66
67.. code-block:: bash
68
69 svn checkout -r 16658 http://code.djangoproject.com/svn/django/trunk/
70
71Using Git
72~~~~~~~~~
73
74Alternatively, if you prefer using Git, you can clone the official Django Git mirror (which is updated from the Subversion repository every five minutes), and then switch to revision 16658 as shown below:
75
76.. code-block:: bash
77
78 git clone https://github.com/django/django.git
79 cd django
80 git checkout 90e8bd48f2
81
82.. note::
83
84 If you're not that familiar with Subversion or Git, you can always find out more about their various commands by typing either ``svn help`` or ``git help`` into the command line.
85
86Running Django's test suite
87---------------------------
88
89When contributing to Django it's very important that your code changes don't introduce bugs into other areas of Django. One way to check that Django stills works after you make your changes is by running Django's entire test suite. If all the tests still pass, then you can be reasonably sure that your changes haven't completely broken Django. If you've never run Django's test suite before, it's a good idea to run it once beforehand just to get familiar with what its output is supposed to look like.
90
91
92Setting Django up to run the test suite
93~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
94
95Before we can actually run the test suite though, we need to make sure that your new local copy of Django is on your ``PYTHONPATH``; otherwise, the test suite won't run properly. We also need to make sure that there aren't any **other** copies of Django installed somewhere else that are taking priority over your newer copy (this happens more often than you might think). To check for these problems, start up the Python interpreter and follow along with the code below:
96
97.. code-block:: python
98
99 >>> import django
100 >>> django
101 <module 'django' from '/.../django/__init__.pyc'>
102
103If you get an ``ImportError: No module named django`` after entering the first line, then you'll need to add your new copy of Django to your ``PYTHONPATH``. For more details on how to do this, read :ref:`pointing-python-at-the-new-django-version`.
104
105If you didn't get any errors, then look at the path found in the third line (abbreviated above as ``/.../django/__init__.pyc``). If that isn't the directory that you put Django into earlier in this tutorial, then there is **another** copy of Django on your ``PYTHONPATH`` that is taking priority over the newer copy. You'll either have to remove this older copy from your ``PYTHONPATH``, or add your new copy to the beginning of your ``PYTHONPATH`` so that it takes priority.
106
107.. note::
108
109 If you're a savvy Djangonaut you might be thinking that using ``virtualenv`` would work perfectly for this type of thing, and you'd be right (+100 bonus points). Using ``virtualenv`` with the ``--no-site-packages`` option isolates your local copy of Django from the rest of your system and avoids potential conflicts. Just make sure not to use ``pip`` to install Django, since that causes Django's setup.py script to be run. You'll still need to use Subversion or Git to check out a copy of Django, and then :ref:`manually add it to your ``PYTHONPATH``<pointing-python-at-the-new-django-version>`.
110
111Actually running the Django test suite
112~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
113
114Once Django is setup properly, we can actually run the test suite. Simply ``cd`` into the Django ``tests/`` directory and run:
115
116.. code-block:: bash
117
118 ./runtests.py --settings=test_sqlite
119
120If you get an ``ImportError: No module named django.contrib`` error, you still need to add your current copy of Django to your ``PYTHONPATH``. For more details on how to do this, read :ref:`pointing-python-at-the-new-django-version`.
121
122Otherwise, sit back and relax. Django's entire test suite has over 4000 different tests, so it can take anywhere from 5 to 15 minutes to run, depending on the speed of your computer.
123
124.. note::
125
126 While Django's test suite is running, you'll see a stream of characters representing the status of each test as it's run. ``E`` indicates that an error was raised during a test, and ``F`` indicates that a test's assertions failed. Both of these are considered to be test failures. Meanwhile, ``x`` and ``s`` indicated expected failures and skipped tests, respectively. Dots indicated passing tests.
127
128Once the process is done, you should be greeted with a message informing you whether the test suite passed or failed. Since you haven't yet made any changes to Django's code, the entire test suite **should** pass. If it doesn't, make sure you've followed all of the previous steps properly.
129
130More information on how to run Django's test suite can be found in the :doc:`online documentation</internals/contributing/writing-code/unit-tests>`.
131
132Writing a test for your ticket
133------------------------------
134
135In most cases, for a patch to be accepted into Django, it has to include tests. For bug fix patches, this means writing a regression test to ensure that the bug is never reintroduced into Django later on. A regression test should be written in such a way that it will fail while the bug still exists and pass once the bug has been fixed. For patches containing new features, you'll need to include tests which ensure that the new features are working correctly. They too should fail when the new feature is not present, and then pass once it has been implemented.
136
137A good way to do this is to write your new tests first before making any changes to the code, and then run them to ensure that they do indeed fail. If your new tests don't fail, you'll need to fix them so that they do. After all, a regression test that passes regardless of whether a bug is present is not very helpful for preventing that bug's reoccurrence.
138
139Now for our hands on example.
140
141Writing a test for ticket 15315
142~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
143
144`Ticket 15315`__ describes the following, small feature addition:
145
146 Since Django 1.2 model forms can override widgets by specifying a ``'widgets'`` attribute in ``Meta``, similar to ``'fields'`` or ``'exclude'``. The ``modelform_factory`` function doesn't accept widgets, so the only way to specify it is by defining a new parent ``ModelForm`` with ``Meta`` and giving it as the ``'form'`` argument. This is more complex than it needs to be.
147
148 The fix is to add a new keyword argument ``widgets=None`` to ``modelform_factory``.
149
150In order to resolve this ticket, we'll modify the ``modelform_factory`` function to accept a ``widgets`` keyword argument. Before we make those changes though, we're going to write a test to verify that our modification actually functions correctly and continues to function correctly in the future.
151
152Navigate to the Django's ``tests/regressiontests/model_forms_regress/`` folder and open the ``tests.py`` file. Find the :class:`FormFieldCallbackTests` class on line 264 and add the ``test_factory_with_widget_argument`` test to it as shown below:
153
154.. code-block:: python
155
156 class FormFieldCallbackTests(TestCase):
157
158 def test_factory_with_widget_argument(self):
159 """ Regression for #15315: modelform_factory should accept widgets
160 argument
161 """
162 widget = forms.Textarea()
163
164 # Without the widget, the form field should not have the widget set to a textarea
165 Form = modelform_factory(Person)
166 self.assertNotEqual(Form.base_fields['name'].widget.__class__, forms.Textarea)
167
168 # With a widget, the form field should have the widget set to a textarea
169 Form = modelform_factory(Person, widgets={'name':widget})
170 self.assertEqual(Form.base_fields['name'].widget.__class__, forms.Textarea)
171
172 def test_baseform_with_widgets_in_meta(self):
173 ...
174
175This test checks to see if the function ``modelform_factory`` accepts the new widgets argument specifying what widgets to use for each field. It also makes sure that those form fields actually use the specified widgets. Now we have the test for our patch written.
176
177__ https://code.djangoproject.com/ticket/15315
178
179Running your new test
180~~~~~~~~~~~~~~~~~~~~~
181
182Remember that we haven't actually made any modifications to ``modelform_factory`` yet, so our test is going to fail. Let's run all the tests in the ``model_forms_regress`` folder to make sure that's really what happens. From the command line, ``cd`` into the Django ``tests/`` directory and run:
183
184.. code-block:: bash
185
186 ./runtests.py --settings=test_sqlite model_forms_regress
187
188If tests ran correctly, you should see that one of the tests failed with an error. Verify that it was the new ``test_factory_with_widget_argument`` test we added above, and then go on to the next step.
189
190If all of the tests passed, then you'll want to make sure that you added the new test shown above to the appropriate folder and class. It's also possible that you have a second copy of Django on your ``PYTHONPATH`` that is taking priority over this copy, and thus it may actually be that copy of Django whose tests are being run. To check if this might be the problem, refer to the section `Setting Django up to run the test suite`_.
191
192Making your changes to Django
193-----------------------------
194
195Next we'll actually add the functionality described in `ticket 15315`__ to Django.
196
197Fixing ticket 1315
198~~~~~~~~~~~~~~~~~~
199
200Navigate to the ``trunk/django/forms/`` folder and open ``models.py``. Find the ``modelform_factory`` function on line 370 and modify it so that it reads as follows:
201
202.. code-block:: python
203
204 def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
205 formfield_callback=None, widgets=None):
206 # Create the inner Meta class. FIXME: ideally, we should be able to
207 # construct a ModelForm without creating and passing in a temporary
208 # inner class.
209
210 # Build up a list of attributes that the Meta object will have.
211 attrs = {'model': model}
212 if fields is not None:
213 attrs['fields'] = fields
214 if exclude is not None:
215 attrs['exclude'] = exclude
216 if widgets is not None:
217 attrs['widgets'] = widgets
218
219 # If parent form class already has an inner Meta, the Meta we're
220 # creating needs to inherit from the parent's inner meta.
221 parent = (object,)
222 if hasattr(form, 'Meta'):
223 parent = (form.Meta, object)
224 Meta = type('Meta', parent, attrs)
225
226 # Give this new form class a reasonable name.
227 class_name = model.__name__ + 'Form'
228
229 # Class attributes for the new form class.
230 form_class_attrs = {
231 'Meta': Meta,
232 'formfield_callback': formfield_callback
233 }
234
235 form_metaclass = ModelFormMetaclass
236
237 if issubclass(form, BaseModelForm) and hasattr(form, '__metaclass__'):
238 form_metaclass = form.__metaclass__
239
240 return form_metaclass(class_name, (form,), form_class_attrs)
241
242Notice that we're only actually adding three new lines of code. We've added a new ``widgets`` keyword argument to the function's signature, and then two more lines which set ``attrs['widgets']`` to the value of that argument whenever it's supplied.
243
244Verifying your test now passes
245~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
246
247Once you're done modifying ``modelform_factory``, we need to make sure that the test we wrote earlier passes now. To run the tests in the ``model_forms_regress`` folder, ``cd`` into the Django ``tests/`` directory and run:
248
249.. code-block:: bash
250
251 ./runtests.py --settings=test_sqlite model_forms_regress
252
253If everything passes, then we can move on. If it doesn't, make sure you correctly modified the ``modelform_factory`` function as shown above and copied the ``test_factory_with_widget_argument`` test correctly.
254
255__ https://code.djangoproject.com/ticket/15315
256
257Running Django's test suite (again)
258-----------------------------------
259
260Once you've verified that your patch and your test work correctly, it's a good idea to run the entire Django test suite to verify that your change hasn't introduced any bugs in other areas of Django. While successfully passing the entire test suite doesn't guarantee your code is bug free, it does help identify many bugs and regressions which otherwise would go unnoticed.
261
262To run the entire Django test suite, ``cd`` into the Django ``tests/`` directory and run:
263
264.. code-block:: bash
265
266 ./runtests.py --settings=test_sqlite
267
268As long as you don't see any failures, you're good to go, and you can move on to generating a patch file that can be uploaded to Trac. If you do have any failures, you'll need to examine them in order to determine whether or not the modifications you made caused them.
269
270Generating a patch file
271-----------------------
272
273Now it's time to actually generate a patch file that can be uploaded to Trac or applied to another copy of Django.
274
275First, you'll need to navigate to your root Django directory (that's the one that contains ``django``, ``docs``, ``tests``, ``AUTHORS``, etc.). This is where we'll generate the patch file from.
276
277Creating a patch file with Subversion
278~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
279
280If you used Subversion to checkout your copy of Django, you can get a look at what your patch file will look like by running the following command:
281
282.. code-block:: bash
283
284 svn diff
285
286This will display the differences between your current copy of Django (with your changes) and the revision that you initially checked out earlier in the tutorial. It should look similar to (but not necessarily the same as) the following:
287
288.. code-block:: diff
289
290 Index: django/forms/models.py
291 ===================================================================
292 --- django/forms/models.py (revision 16658)
293 +++ django/forms/models.py (working copy)
294 @@ -368,7 +368,7 @@
295 __metaclass__ = ModelFormMetaclass
296
297 def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
298 - formfield_callback=None):
299 + formfield_callback=None, widgets=None):
300 # Create the inner Meta class. FIXME: ideally, we should be able to
301 # construct a ModelForm without creating and passing in a temporary
302 # inner class.
303 @@ -379,7 +379,9 @@
304 attrs['fields'] = fields
305 if exclude is not None:
306 attrs['exclude'] = exclude
307 -
308 + if widgets is not None:
309 + attrs['widgets'] = widgets
310 +
311 # If parent form class already has an inner Meta, the Meta we're
312 # creating needs to inherit from the parent's inner meta.
313 parent = (object,)
314
315 Index: tests/regressiontests/model_forms_regress/tests.py
316 ===================================================================
317 --- tests/regressiontests/model_forms_regress/tests.py (revision 16658)
318 +++ tests/regressiontests/model_forms_regress/tests.py (working copy)
319 @@ -263,6 +263,20 @@
320
321 class FormFieldCallbackTests(TestCase):
322
323 + def test_factory_with_widget_argument(self):
324 + """ Regression for #15315: modelform_factory should accept widgets
325 + argument
326 + """
327 + widget = forms.Textarea()
328 +
329 + # Without a widget should not set the widget to textarea
330 + Form = modelform_factory(Person)
331 + self.assertNotEqual(Form.base_fields['name'].widget.__class__, forms.Textarea)
332 +
333 + # With a widget should not set the widget to textarea
334 + Form = modelform_factory(Person, widgets={'name':widget})
335 + self.assertEqual(Form.base_fields['name'].widget.__class__, forms.Textarea)
336 +
337 def test_baseform_with_widgets_in_meta(self):
338 """Regression for #13095: Using base forms with widgets defined in Meta should not raise errors."""
339 widget = forms.Textarea()
340
341If the patch's content looks okay, you can run the following command to save the patch file to your current working directory.
342
343.. code-block:: bash
344
345 svn diff > myfancynewpatch.diff
346
347You should now have a file in the root Django directory called ``myfancynewpatch.diff``. This patch file that contains all your changes.
348
349Creating a patch file with Git
350~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
351
352For those who prefer using Git, the process is much the same as with Subversion. To get a look at the content of your patch, run the following command instead:
353
354.. code-block:: bash
355
356 git diff
357
358This will display the differences between your current copy of Django (with your changes) and the revision that you initially checked out earlier in the tutorial. It should look similar to (but not necessarily the same as) the following:
359
360.. code-block:: diff
361
362 diff --git a/django/forms/models.py b/django/forms/models.py
363 index 527da5e..a93f21c 100644
364 --- a/django/forms/models.py
365 +++ b/django/forms/models.py
366 @@ -368,7 +368,7 @@ class ModelForm(BaseModelForm):
367 __metaclass__ = ModelFormMetaclass
368
369 def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
370 - formfield_callback=None):
371 + formfield_callback=None, widgets=None):
372 # Create the inner Meta class. FIXME: ideally, we should be able to
373 # construct a ModelForm without creating and passing in a temporary
374 # inner class.
375 @@ -379,6 +379,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
376 attrs['fields'] = fields
377 if exclude is not None:
378 attrs['exclude'] = exclude
379 + if widgets is not None:
380 + attrs['widgets'] = widgets
381
382 # If parent form class already has an inner Meta, the Meta we're
383 # creating needs to inherit from the parent's inner meta.
384
385 diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests
386 index 9860c2e..d2d9aa3 100644
387 --- a/tests/regressiontests/model_forms_regress/tests.py
388 +++ b/tests/regressiontests/model_forms_regress/tests.py
389 @@ -263,6 +263,20 @@ class URLFieldTests(TestCase):
390
391 class FormFieldCallbackTests(TestCase):
392
393 + def test_factory_with_widget_argument(self):
394 + """ Regression for #15315: modelform_factory should accept widgets
395 + argument
396 + """
397 + widget = forms.Textarea()
398 +
399 + # Without a widget should not set the widget to textarea
400 + Form = modelform_factory(Person)
401 + self.assertNotEqual(Form.base_fields['name'].widget.__class__, forms.Textarea)
402 +
403 + # With a widget should not set the widget to textarea
404 + Form = modelform_factory(Person, widgets={'name':widget})
405 + self.assertEqual(Form.base_fields['name'].widget.__class__, forms.Textarea)
406 +
407 def test_baseform_with_widgets_in_meta(self):
408 """Regression for #13095: Using base forms with widgets defined in Meta should not raise errors."""
409 widget = forms.Textarea()
410
411The Git patch format is slightly different than the Subversion format, but both are acceptable for patches. Once you're done looking at the patch, hit the ``Q`` key to exit back to the command line. If the patch's content looked okay, you can run the following command to save the patch file to your current working directory.
412
413.. code-block:: bash
414
415 git diff > myfancynewpatch.diff
416
417You should now have a file in the root Django directory called ``myfancynewpatch.diff``. This patch file that contains all your changes.
418
419Where to find more information
420==============================
421
422Now that you've generated your first patch, you're ready to go on and find a ticket of your own to write a patch for. Before you do that, make sure you read Django's documentation on :doc:`claiming tickets and submitting patches</internals/contributing/writing-code/submitting-patches>`. It covers how to claim tickets on Trac, expected coding style for patches, and many other important details.
423
424First time contributors should probably also read Django's :doc:`documentation for first time contributors</internals/contributing/new-contributors/>`. It has lots of good advice for those of us who are new to helping with Django.
425
426After those, if you're still hungering for more information about contributing, you can always browse through the rest of :doc:`Django's documentation on contributing<internals/contributing/index>`. It contains a ton of useful information and should be your first source for answering any questions you might have.
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
Back to Top