Escribiendo tu primer parche para Django

Introducción

Interesado en devolver algo a la comunidad? Quizás hayas encontrado un bug en Django que te gustaría ver arreglado, o quizás hay alguna funcionalidad que te gustaría ver incorporada.

Constribuir con Django es la mejor manera de ver nuestras preocupaciones tenidas en cuenta. Puede parece intimidante al principio, pero es realmente simple. Vamos a recorrer el proceso completo para aprender mediante un ejemplo.

Para quién es este tutorial?

Para este tutorial esperamos que tengas al menos un conocimiento básico sobre cómo funciona Django. Esto significa que deberías estar cómodo recorriendo los tutoriales existentes, Escribiendo tu primera Django app. Además, deberías tener una buena base de Python. Si no es el caso, Dive Into Python es un libro online fantástico (y gratis) para aquellos que comienzan con Python.

Para aquellos que no estén familiarizados con sistemas de control de versiones y Trac, van a encontrar en este tutorial y sus links la información suficiente para empezar. Sin embargo, probablemente quieras empezar a leer más sobre estas diferentes herramientas si tu plan es contribuir con Django regularmente.

En gran medida este tutorial trata de explicar tanto como es posible para que resulte útil para una amplia audiencia.

Dónde encontrar ayuda:

Si tuvieras algún problema durante este tutorial, por favor posteá un mensaje en django-developers o unite a #django-dev en irc.freenode.net para hablar con otros usuarios de Django que quizás puedan ayudar (ambos en inglés).

Qué cubre este tutorial?

Vamos a recorrer el camino para contribuir un patch a Django por primera vez. Al final de este tutorial, deberías tener un conocimiento básico sobre las herramientas y los procesos involucrados. Específicamente, cubriremos lo siguiente:

  • Instalar Git.

  • Bajar una copia de desarrollo de Django.

  • Correr la test suite de Django.

  • Escribir un test para el patch.

  • Escribir el código del patch.

  • Testear el patch.

  • Generar un archivo con los cambios del patch.

  • Dónde buscar más información.

Una vez que terminés este tutorial, podés ver el resto de la documentación sobre contribuir con Django. Contiene mucha más información y es algo que uno debe leer si quiere convertirse en un contribuidor regular de Django. Si tenés preguntas, probablemente tendrá las respuestas.

Instalar Git

Para este tutorial, necesitaremos tener Git instalado para bajar la versión de desarrollo de Django y generar los archivos con los cambios de nuestro patch.

Para chequear si tenemos Git instalado o no, escribimos git en la línea de comandos. Si obtenemos un mensaje diciendo que este comando no se encontró, tendremos que bajar e instalar Git, ver Git’s download page.

Si no estás familiarizado con Git, siempre se puede saber más sobre los comandos (una vez instalado) tipeando git help en la línea de comandos.

Obtener una copia de la versión de desarrollo de Django

El primer paso para contribuir con Django es obtener una copia del código fuente. En la línea de comandos, usamos el comando cd para navegar al directorio donde queremos que viva nuestra copia local de Django.

Bajamos el código fuente de Django usando el siguiente comando:

git clone https://github.com/django/django.git

Nota

Para usuarios que quieran usar virtualenv, se puede usar:

pip install -e /path/to/your/local/clone/django/

(donde django es el directorio del repositorio clonado que contiene el archivo setup.py) para linkear nuestra copia local en el entorno del virtualenv. Es una gran opción para aislar nuestra copia de desarrollo de Django del resto de nuestro sistema y evitar potenciales conflictos de paquetes.

Volviendo a una revisión anterior de Django

Para este tutorial vamos a usar el #17549 como caso de estudio, así que vamos a volver atrás la historia de versionado de Django en git hasta antes de que el patch se aplicó. Esto nos va a permitir recorrer todos los pasos requeridos al escribir un patch desde cero, incluyendo correr la test suite de Django.

Por más que usemos una revisión más vieja del trunk de Django para los propósitos de este tutorial, siempre se debe usar la revisión actual cuando se trabaja en un patch para un ticket!

Nota

El patch para este ticket fue escrito por Ulrich Petri, y fue aplicado a Django en el commit ac2052ebc84c45709ab5f0f25e685bf656ce79bc. Entonces vamos a usar la revisión anterior a este commit, commit 39f5bc7fc3a4bb43ed8a1358b17fe0521a1a63ac.

Navegamos al directorio raíz de Django (el que contiene django, docs, tests, AUTHORS, etc.). Chequeamos la revisión que vamos a usar para el tutorial:

git checkout 39f5bc7fc3a4bb43ed8a1358b17fe0521a1a63ac

Corriendo la test suite de Django por primera vez

Cuando uno contribuye con Django es muy importante que los cambios no introduzcan bugs en otras áreas de Django. Una manera de chequear que Django todavía funciona después de hacer cambios es correr la test suite. Si todos los tests todavía pasan, entonces uno puede estar razonablemente seguro de que los cambios no han roto Django. Si nunca corriste la test suite de Django antes, es una buena idea hacerlo una vez antes de comenzar para familiarizarse con la salida.

Podemos correr la test suite simplemente haciendo cd al directorio tests/ de Django y, si usamos GNU/Linux, Mac OS X o algún otro sabor de Unix, ejecutar:

PYTHONPATH=.. python runtests.py --settings=test_sqlite

Si estamos en Windows, lo de arriba debería funcionar si estamos usando “Git Bash” provisto por la instalación por defecto de Git. GitHub tiene un buen tutorial.

Nota

Si usamos virtualenv, podemos omitir PYTHONPATH=.. cuando corremos los tests. Esto le dice a Python que busque Django en el directorio padre de tests. virtualenv pone nuestra copia de Django en el PYTHONPATH automáticamente.

Ahora nos sentamos y esperamos. La test suite de Django tiene más de 4800 tests, así que puede tomar entre 5 y 15 minutos de correr, dependiendo de la velocidad de nuestra computadora.

Mientras la suite corre, veremos un flujo de caracteres representando el estado de cada test a medida que corre. E indica que hubo un error durante el test, y F indica que una aserción del test falló. Ambos casos se consideran fallas de test. Por otro lado, x y s indican fallas esperadas y tests que se saltean, respectivamente. Los puntos indican tests que pasan correctamente.

Los tests que se saltean son típicamente a causa de librerías externas que se requieren para el test pero que no están disponibles; ver Corriendo todos los tests para una lista de dependencias y asegurarse de instalarlas para los tests relacionados de acuerdo a los cambios que estemos haciendo (no será necesario para este tutorial).

Una vez que se completan los tests, deberíamos obtener un mensaje que nos informa si la test suite pasó o falló. Como no hemos hecho ningún cambio al código, debería haber pasado. Si tuviéramos algún fallo o error, deberíamos asegurarnos que seguimos los pasos previos correctamente. Ver Corriendo los unit tests para más información.

Notar que la última revisión del trunk de Django podría no siempre ser estable. Cuando se desarrolla contra trunk, se puede chequear Django’s continuous integration builds para determinar si los fallos son específicos de nuestra máquina o también están presentes en los builds oficiales de Django. Si uno cliquea para ver un build particular, puede ver la “Matriz de configuración” que muestra las fallas detalladas por versión de Python y backend de base de datos.

Nota

Para este tutorial y el ticket en el que vamos a trabajar, testear contra SQLite es suficiente, pero es posible (y a veces necesario) correr los tests usando una base de datos diferente.

Escribir tests para el ticket

En la mayoría de los casos para que un patch se acepte en Django tiene que incluir tests. Para patches correspondientes a arreglar un bug, esto significa escribir un test de regresión para asegurarse de que el bug no se reintroduce nuevamente. Un test de regresión se debe escribir de tal manera que falle cuando el bug todavía existe y pase cuando el bug se haya arreglado. Para patches de nuevas funcionalidades, es necesario incluir tests que aseguren que la nueva funcionalidad funciona correctamente. También deben fallar cuando la nueva funcionalidad no está presente, y pasar una vez que se haya implementado.

Una buena manera de hacer esto es escribir los tests primero, antes de hacer cualquier cambio al código. Este estilo de desarrollo se llama test-driven development y se puede aplicar tanto a proyectos enteros como a patches. Después de escribir los tests, se corren para estar seguros de que de hecho fallan (ya que no se ha escrito el código que arregla el bug o agrega la funcionalidad todavía). Si los tests no fallan, es necesario arreglarlos para que fallen. Después de todo un test de regresión que pasa independientemente de si el bug está presente o no no es muy útil previniendo que el bug reaparezca más tarde.

Ahora, manos al ejemplo.

Escribir tests para el ticket #17549

El :ticket:`17549`describe la siguiente funcionalidad a agregar:

Es útil para un URLField tener la opción de abrir la URL; de otra forma uno podría usar un CharField igualmente.

Para resolver este ticket vamos a agregar un método render al AdminURLFieldWidget para que se muestre un link cliqueable arriba del widget. Pero antes de hacer ese cambio, vamos a escribir un par de tests que verifiquen que nuestras modificaciones funcionarían correctamente, y lo continuarían haciendo en el futuro.

Navegamos al directorio tests/regressiontests/admin_widgets/ de Django y abrimos el archivo tests.py. Agregamos el siguiente código en la línea 269 justo antes de la clase AdminFileWidgetTest:

class AdminURLWidgetTest(DjangoTestCase):
    def test_render(self):
        w = widgets.AdminURLFieldWidget()
        self.assertHTMLEqual(
            conditional_escape(w.render('test', '')),
            '<input class="vURLField" name="test" type="text" />'
        )
        self.assertHTMLEqual(
            conditional_escape(w.render('test', 'http://example.com')),
            '<p class="url">Currently:<a href="http://example.com">http://example.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com" /></p>'
        )

    def test_render_idn(self):
        w = widgets.AdminURLFieldWidget()
        self.assertHTMLEqual(
            conditional_escape(w.render('test', 'http://example-äüö.com')),
            '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com">http://example-äüö.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com" /></p>'
        )

    def test_render_quoting(self):
        w = widgets.AdminURLFieldWidget()
        self.assertHTMLEqual(
            conditional_escape(w.render('test', 'http://example.com/<sometag>some text</sometag>')),
            '<p class="url">Currently:<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com/<sometag>some text</sometag>" /></p>'
        )
        self.assertHTMLEqual(
            conditional_escape(w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')),
            '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com/<sometag>some text</sometag>" /></p>'
        )

Los nuevos tests chequean que el método render que vamos a agregar funciona correctamente en un par de situaciones diferentes.

Pero esto parece un tanto difícil...

Si nunca tuviste que lidiar con tests antes, puede parece un poco difícil escribir los tests a primera vista. Afortunadamente, el testing es un tema importante en programación, así que hay mucha información:

Correr los nuevos tests

Recordemos que todavía no hemos hecho ninguna modificación a AdminURLFieldWidget, entonces nuestros tests van a fallar. Corramos los tests en el directorio model_forms_regress para asegurarnos de que eso realmente sucede. En la línea de comandos, cd al directorio tests/ de Django y corremos:

PYTHONPATH=.. python runtests.py --settings=test_sqlite admin_widgets

Si los tests corren correctamente, deberíamos ver tres fallas correspondientes a cada uno de los métodos de test que agregamos. Si todos los tests pasan, entonces deberíamos revisar que agregamos los tests en el directorio y clase apropiados.

Escribir el código para el ticket

A continuación vamos a agregar a Django la funcionalidad descripta en el #17549.

Escribir el código para el ticket #17549

Navegamos al directorio django/django/contrib/admin/ y abrimos el archivo widgets.py. Buscamos la clase AdminURLFieldWidget en la línea 302 y agregamos el siguiente método render después del método __init__ ya existente:

def render(self, name, value, attrs=None):
    html = super(AdminURLFieldWidget, self).render(name, value, attrs)
    if value:
        value = force_text(self._format_value(value))
        final_attrs = {'href': mark_safe(smart_urlquote(value))}
        html = format_html(
            '<p class="url">{} <a {}>{}</a><br />{} {}</p>',
            _('Currently:'), flatatt(final_attrs), value,
            _('Change:'), html
        )
    return html

Verificar que los tests pasan

Una vez que terminamos las modificaciones, necesitamos verificar que los tests que escribimos anteriormente pasan, para saber si el código que acabamos de escribir funciona correctamente. Para correr los tests en el directorio admin_widgets, hacemos cd al directorio tests/ de Django y corremos:

PYTHONPATH=.. python runtests.py --settings=test_sqlite admin_widgets

Oops, menos mal que escribimos esos tests! Todavía deberíamos ver las 3 fallas, con la siguiente excepción:

NameError: global name 'smart_urlquote' is not defined

Nos olvidamos de agregar el import para ese método. Agregamos el import de smart_urlquote al final de la línea 13 de django/contrib/admin/widgets.py para que se vea así:

from django.utils.html import escape, format_html, format_html_join, smart_urlquote

Volvemos a correr los tests y todos deberían pasar. Si no, asegurarse de que se modificó correctamente la clase AdminURLFieldWidget como se mostró arriba y que los tests también se copiaron correctamente.

Corriendo la test suite de Django por segunda vez

Una vez que verificamos que nuestro patch y nuestros tests funcionan correctamente, es una buena idea correr la test suite de Django entera para confirmar que nuestro cambio no introdujo ningún bug en otras áreas de Django. Si bien el que la test suite pase no garantiza que nuestro código esté libre de bugs, ayuda a identificar muchos bugs y regresiones que de otra manera podrían pasar desapercibidos.

Para correr la test suite de Django completa, cd al directorio tests/ y corremos:

PYTHONPATH=.. python runtests.py --settings=test_sqlite

Mientras que no veamos ninguna falla, estamos bien. Notemos que en el fix original también se hace un pequeño cambio de CSS para formatear el widget. Podemos incluir este cambio también, pero lo salteamos por ahora por cuestiones de brevedad.

Escribir documentación

Esta es una nueva funcionalidad, así que debería documentarse. Agregamos lo siguiente desde la línea 925 de django/docs/ref/models/fields.txt junto a la documentación existente de URLField:

.. versionadded:: 1.5

    The current value of the field will be displayed as a clickable link above the
    input widget.

Para mayor información sobre escribir documentación, incluyendo una explicación de qué se trata esto de versionadded, se puede consultar en la sección correspondiente. Esa página también incluye una explicación de cómo generar una copia de la documentación localmente, para poder prever el HTML que se va a producir.

Generar un patch con los cambios

Ahora es momento de generar un archivo de patch que se pueda subir al Trac o aplicarse a otra copia de Django. Para obtenere un vistazo del contenido de nuestro patch, corremos el siguiente comando:

git diff

Esto va a mostrar las diferencias entre nuestra copia actual de Django (con los cambios) y la revisión que teníamos inicialmente al principio del tutorial.

Apretamos q para volver a la línea de comandos. Si el contenido del patch se veía bien, podemos correr el siguiente comando para guardar el archivo del patch en nuestro directorio actual:

git diff > 17549.diff

Deberíamos tener un archivo en el directorio raíz de Django llamando 17549.diff. Este archivo contiene nuestros cambios y debería verse algo así:

diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 1e0bc2d..9e43a10 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -10,7 +10,7 @@ from django.contrib.admin.templatetags.admin_static import static
 from django.core.urlresolvers import reverse
 from django.forms.widgets import RadioFieldRenderer
 from django.forms.util import flatatt
-from django.utils.html import escape, format_html, format_html_join
+from django.utils.html import escape, format_html, format_html_join, smart_urlquote
 from django.utils.text import Truncator
 from django.utils.translation import ugettext as _
 from django.utils.safestring import mark_safe
@@ -306,6 +306,18 @@ class AdminURLFieldWidget(forms.TextInput):
             final_attrs.update(attrs)
         super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)

+    def render(self, name, value, attrs=None):
+        html = super(AdminURLFieldWidget, self).render(name, value, attrs)
+        if value:
+            value = force_text(self._format_value(value))
+            final_attrs = {'href': mark_safe(smart_urlquote(value))}
+            html = format_html(
+                '<p class="url">{} <a {}>{}</a><br />{} {}</p>',
+                _('Currently:'), flatatt(final_attrs), value,
+                _('Change:'), html
+            )
+        return html
+
 class AdminIntegerFieldWidget(forms.TextInput):
     class_name = 'vIntegerField'

diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index 809d56e..d44f85f 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -922,6 +922,10 @@ Like all :class:`CharField` subclasses, :class:`URLField` takes the optional
 :attr:`~CharField.max_length`argument. If you don't specify
 :attr:`~CharField.max_length`, a default of 200 is used.

+.. versionadded:: 1.5
+
+The current value of the field will be displayed as a clickable link above the
+input widget.

 Relationship fields
 ===================
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
index 4b11543..94acc6d 100644
--- a/tests/regressiontests/admin_widgets/tests.py
+++ b/tests/regressiontests/admin_widgets/tests.py

@@ -265,6 +265,35 @@ class AdminSplitDateTimeWidgetTest(DjangoTestCase):
                     '<p class="datetime">Datum: <input value="01.12.2007" type="text" class="vDateField" name="test_0" size="10" /><br />Zeit: <input value="09:30:00" type="text" class="vTimeField" name="test_1" size="8" /></p>',
                 )

+class AdminURLWidgetTest(DjangoTestCase):
+    def test_render(self):
+        w = widgets.AdminURLFieldWidget()
+        self.assertHTMLEqual(
+            conditional_escape(w.render('test', '')),
+            '<input class="vURLField" name="test" type="text" />'
+        )
+        self.assertHTMLEqual(
+            conditional_escape(w.render('test', 'http://example.com')),
+            '<p class="url">Currently:<a href="http://example.com">http://example.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com" /></p>'
+        )
+
+    def test_render_idn(self):
+        w = widgets.AdminURLFieldWidget()
+        self.assertHTMLEqual(
+            conditional_escape(w.render('test', 'http://example-äüö.com')),
+            '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com">http://example-äüö.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com" /></p>'
+        )
+
+    def test_render_quoting(self):
+        w = widgets.AdminURLFieldWidget()
+        self.assertHTMLEqual(
+            conditional_escape(w.render('test', 'http://example.com/<sometag>some text</sometag>')),
+            '<p class="url">Currently:<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com/<sometag>some text</sometag>" /></p>'
+        )
+        self.assertHTMLEqual(
+            conditional_escape(w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')),
+            '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com/<sometag>some text</sometag>" /></p>'
+        )

 class AdminFileWidgetTest(DjangoTestCase):
     def test_render(self):

Qué hacemos ahora?

Felicitaciones, hemos generado nuestro primer patch para Django! Ahora podemos usar estas habilidades para ayudar a mejorar el código de Django. Generar patches y adjuntarlos a tickets en el Trac es útil, pero, como ahora estamos usando git - se recomiendo adoptar un workflow orientado a git.

Como nunca commiteamos nuestros cambios localmente, hacemos lo siguiente para devolver nuestro branch a un buen punto de comienzo (reseteamos nuestros cambios):

git reset --hard HEAD
git checkout master

Más información para nuevos contribuidores

Antes de ponerse de lleno a escribir patches para Django, hay más información sobre el tema que probablemente deberías leer:

  • Deberías asegurarte de leer la documentación de Django sobre reclamando tickets y enviando patches. Cubre la etiqueta en Trac, cómo reclamar tickets, el estilo de código esperado para un patch, y otros detalles importantes.

  • Aquellos que contribuyen por primera vez deberían leer también la documentación para contribuidores por primera vez. Tiene muchos consejos para quienes son nuevos en esto de ayudar con Django.

  • Y si todavía buscás más información sobre contribuir, siempre se puede revisar el resto de la documentación de Django sobre contribuir. Contiene mucha información útil y debería ser el primer recurso en busca de respuestas a cualquier pregunta que te pudiera surgir.

Encontrar nuestro primer ticket de verdad

Después de recorrer la información de arriba, estaremos listos para salir a buscar un ticket para el que podamos escribir un patch. Hay que prestar especial atención a los tickets marcados como “easy pickings”. Estos tickets suelen ser más simples y son una gran oportunidad para aquellos que quieren contribuir por primera vez. Una vez que estemos familiarizados con el contribuir con Django, podemos pasar a escribir patches para tickets más complicados.

Si queremos empezar ya, se puede pegar una mirada a la lista de tickets simples que necesitan patches y la lista de tickets simples que tienen patches que necesitan mejoras. Si estamos familiarizados con escribir tests, también podemos ver la lista de tickets simples que necesitan tests. Recordar seguir las instrucciones sobre reclamar tickets mencionadas en el link a la documentación en reclamando tickets y enviando patches.

Qué sigue?

Después que un ticket tiene un patch, se necesita que un segundo par de ojos lo revisen. Después de subir un patch o enviar un pull request, hay que asegurarse de actualizar la metadata del ticket, seteando los flags del ticket para que diga “has patch”, “doesn’t need tests”, etc. para que otros lo puedan encontrar para revisarlo. Contribuir no necesariamente significa escribir un patch desde cero, revisar patches existentes es también una forma útil de ayudar. Para más detalles, ver /internals/contributing/triaging-tickets.