Django 1.3 en «cero coma» II

La primera entrega de este tutorial sirvió para introducir en 10 pasos algunos aspectos interesantes de Django 1.3. en esta segunda parte vamos a tratar de ser más caseros, programando un poco más pero utilizando correctamente el framework. Aquí tenéis un resumen:

  1. Añadiremos un propietario al modelo
  2. Mejoraremos la administración de posts
  3. Implementaremos un sistema de comentarios
  4. Retocaremos el administrador para gestionar los post y comentarios
  5. Implementaremos un formulario de búsqueda muy sencillo

0.- Estilizando la página

Asumiremos que hemos seguido la primera parte del tutorial rápido de Django. Estamos creando un blog de funcionalidad básica y vamos a estilizarlo un poco añadiendo una hoja de estilo CSS3 para mejorar el aspecto gráfico del mismo. Para ello, edita un archivo style.css dentro de la carpeta staticfiles del proyecto y copia el siguiente contenido:

@font-face { font-family: myScript; src: url('SCRIPTIN.ttf'); }
body, p, td, th { font-family:"Times New Roman", times, serif; }
body { background-color:#f4e0b4; margin:4em; margin-top:0; }
body > header { font-family:myScript; font-size:20pt; border-bottom:2px solid black; }
body > header > h1 { margin-top:0; }
body > header > h1 a { margin:0; color:#333; text-decoration:none; }
.post p { text-indent:4ex; text-align:justify; }
.post aside p { font-style:italic; text-align:right; }
.post header a { font-family:sans; color:#333; text-decoration:none; }
.post header a:hover { color:#f4e0b4; text-shadow:0 1px 1px #333, 1px 0 1px #333, -1px 0 1px #333, 0 -1px 1px #333, -1px 1px 1px #333, -1px 1px 1px #333, -1px -1px 1px #333; font-size:1.1em; }
#comments { color:#333; }
#comments article { border:1px solid black; border-radius:5px 5px; box-shadow:5px 5px 5px #888; margin:1em; padding:1em; }
#comments article p.owner { text-align:center; }
#comments article aside p { text-align:right; font-style:italic; }
.errorlist { background-color:#f44; color:white; padding:0.25em; font-weight:bold; font-style:italic; font-family:sans; list-style:none;}
label { display:block; margin-top:1em;}

Necesitareis situar la fuente Scriptina en la carpeta staticfiles también.

1.- Modificando el modelo

La modificación de modelos es normal en proyectos grandes. En este caso, por ejemplo, los posts podrían tener un propietario lo que permitiría en un futuro identificar al autor de un artículo además de asegurar que los mismos sólo fueran editados por sus legítimos propietarios.

Editaremos el archivo models.py de la aplicación posts y añadiremos la siguiente línea debajo de publication_date:

owner = models.ForeignKey(auth.models.User)

Comprobamos cómo User no es más que otro modelo de la aplicación auth que proporciona el framework.

Ahora tenemos que actualizar la base de datos para lo que debemos indicar a south que realice otro snapshot y luego migrar a él. Para ello, escribimos:

$ ./manage schemamigration posts --auto

El parámetro auto indica que queremos que sea south quien se de cuenta de los cambios realizados. Al ejecutar el comando se nos explicará que el campo es no nulo (por defecto) y nuevo por lo que necesita un valor por defecto. Seleccionamos la opción 2 para especificarlo manualmente y luego 1 (el administrador por defecto tiene clave 1).

Ahora aplicamos el snapshot mediante:

$ ./manage migrate posts

Podemos comprobar que todos los posts tienen ahora propietario ejecutando el servidor y desde el panel de administración.

2.- Mejorando la administración de posts

Podemos mejorar la forma en la que se editan los posts en el administrador registrando, junto al modelo, un modelo de administración. Para ello, editaremos el archivo admin.py de la aplicación posts y lo dejaremos de la siguiente manera:

from posts.models import Post
from django.contrib import admin

class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'briefing', 'publication_date', 'owner')

    list_filter = ['publication_date']

    date_hierarchy = 'publication_date'

    search_fields = ['title', 'content', 'owner__username', 'owner__first_name', 'owner__last_name']

    prepopulated_fields = { 'machine_name' : ('title', ) }

admin.site.register(Post, PostAdmin)

Los distintos campos merecen una explicación:

  • list_display establece qué campos veremos en la lista de posts
  • list_filter añade a la derecha un filtro por fecha
  • date_hierarchy permite establecer la jerarquía de fechas que aparece sobre el cuadro de búsqueda
  • search_fields indica dónde se buscará el texto introducido en el cuadro de búsqueda (no podemos especificar funciones, sólo campos)
  • prepopulated_fileds se utiliza para poblar algunos campos en función de otros

La notación ‘__’ se utiliza para alcanzar las propiedades de otros objetos. Así ‘owner__firstname‘ es el equivalente de owner.first_name y ‘owner__last_name‘, de owner.last_name

Fijémonos por último que la línea que registra Post en el administrador incluye ahora el modelo de administración también.

Podemos probar el nuevo administrador arracando el servidor.

3.- Un sistema de comentarios

Resulta que Django ya tiene una aplicación de comentarios. No obstante, con el fin de ilustrar más aspectos del framework, vamos a implementar un pequeño sistema de comentarios sin necesidad de registro por nosotros mismos.

Lo primero será crear el modelo, para lo cual añadimos a models.py:

class Commentary(models.Model):

    post = models.ForeignKey(Post)

    content = models.TextField()

    publication_date = models.DateTimeField(auto_now_add=True)

    owner = models.CharField(max_length=50, default='El hombre invisible')

    def __unicode__(self):
        return self.owner + '@' + unicode(self.post)

    class Meta:
        verbose_name_plural = 'commentaries'
        ordering = ['-publication_date']

Los comentarios pertenecen a un post, y podemos reflejar este hecho mediante un campo ForeignKey. El valor del parámetro default en owner indica qué valor deberá grabarse en la base de datos en caso que el usuario no especifique ninguno. El campo verbose_name_plural, dentro de la clase interna Meta, indica el nombre «humanizado», en plural, de la clase.

Antes de continuar, debemos crear una nueva instantanea y aplicarla mediante south:

$ ./manage.py schemamigration posts --auto
$ ./manage.py migrate posts

El formulario para agregar comentarios

Ahora definiremos el formulario para agregar comentarios. Los formularios son colecciones de campos. Como precisamente el modelo Commentary que acabamos de definir ya especifica los campos de un objeto de clase Commentary, utilizaremos la clase ModelForm para evitar redefinir dichos campos. Para ello crearemos un nuevo archivo forms.py dentro de la aplicacion posts con el siguiente contenido:

from django import forms
from posts.models import Commentary

class AddCommentaryForm(forms.ModelForm):
    class Meta:
        model = Commentary
        fields = ('owner', 'content')

La propiedad fields permite indicar el orden de los campos que serán mostrados en el formulario. Faltarían publication_date (que se añade automáticamente) y post pero no necesitamos que el usuario especifique a qué post pertenecerá el comentario. Este será el que en ese momento estemos mostrando.

La vista del post

Vamos a crear una vista para presentar un sólo post y sus comentarios. Para ello modificaremos el archivo views.py de la aplicación posts. Esta vista estará muy personalizada. Comenzaremos importando TemplateView al comienzo del archivo:

from django.views.generic import ListView, TemplateView

Y añadiremos la nueva vista tal y como sigue:

class PostDetails(TemplateView):
    template_name='postdetails.html'

    def get_post(self, slug):
        return Post.objects.get(pk=slug)

    def get_form(self, post):
        if self.request.method == 'POST':
            form = AddCommentaryForm(self.request.POST)
            if form.is_valid():
                post.commentary_set.add(form.save(commit=False))
            form = AddCommentaryForm()

        else:
            form = AddCommentaryForm()

        return form

    def get_context_data(self, **kwargs):
        context = super(PostDetails, self).get_context_data(**kwargs)

        post = self.get_post(kwargs['slug'])
        form = self.get_form(post)

        context.update({'form':form, 'post':post})

        return context

     def post(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
         return self.render_to_response(context)

Esta vista ya tiene más chicha. Primero, el método get_post() recupera un único objeto (el método get sólo recupera objetos únicos, si hubiese una ambigüedad se lanzaría una excepción), aquel cuya public key (parámetro pk) sea igual al parámetro slug.

La función get_form() es más elaborada. Primero se comprueba si existen datos enviados como POST. Si no es así, se crea un formulario vacío. En caso contrario crearemos un formulario con los datos pasados por POST. El objeto formulario es inmutable, una vez construido no puede modificarse. Cuando se construye un formulario, se valida automáticamente y la información se normaliza, crandose campos internos para albergar los problemas que hubiesen podido detectarse-

A continuación comprobamos si es válido. El método save devuelve el objeto Commentary asociado al formulario y lo introduce en la base de datos. El problema es que, al no especificar el campo post, el modelo está incompleto y Django no nos permitirá grabarlo en la base de datos. Tendremos que completarlo primero.

El parámetro commit=false evita, precisamente, que el objeto se grabe en la base de datos aunque el método save seguirá devolviendo el objeto asociado. La propiedad commentary_set, disponible en la clase Post por el mero hecho de tener una relación desde Commentary, es una relación inversa (backward relation). Las relaciones inversas se crean automáticamente en la clase referenciada por una ForeignKey. Al añadir un objeto a commentary_set, la propiedad de la clase Commentary que establece la relación (en este caso, post) se establece automáticamente al objeto propietario del conjunto.

Tras salvarlo devolveremos un formulario vacío.

El método get_context_data() nos permite definir los objetos del contexto. Para ello, dejaremos que la superclase TemplateView realice su trabajo y luego actualizaremos el objeto diccionario para incluir el post y el formulario. Cabe destacar el uso de kwargs para obtener el parámetro de URL ‘slug‘. Veremos cómo pasar parámetros a las URLs en breves momentos.

Por último, el método post() se debe definir para que la vista sea capaz de procesar datos envíados en POST puesto que, por alguna razón, la vista TemplateView sólo maneja datos enviados mediante GET.

El penúltimo paso será añadir esta vista al archivo de URLs. Así que editamos urls.py para que quede de la siguiente manera:

from posts.views import PostList, PostDetails
from django.conf.urls.defaults import patterns, include, url

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
# Examples:
# url(r'^$', 'djangoblog.views.home', name='home'),
# url(r'^djangoblog/', include('djangoblog.foo.urls')),

# Uncomment the admin/doc line below to enable admin documentation:
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

# Uncomment the next line to enable the admin:
url(r'^admin/', include(admin.site.urls), name='adminpage'),

url(r'^$', PostList.as_view(), name='mainpage'),
url(r'^posts/', PostList.as_view(), name='postlist'),
url(r'^post/(?P<slug>[a-zA-Z0-9_-]+)/', PostDetails.as_view(), name='postdetails'),
)

El fragmento de la expresion regular (?P<slug>[a-zA-Z0-9_]+) se denomina grupo con nombre. La expresión regular del grupo comienza tras el símbolo > y el nombre es precisamente lo que se encuentra entre < y >. En este caso, slug. Es así como se proporcionan parámetros a las vistas. Como decíamos, el método get_contex_data() utiliza el parámetros slug que es justamente lo que proporcionamos.

El template del post

Vamos ahora a escribir el template. Para ello, crea un nuevo archivo postdetails.html dentro de la carpeta templates del proyecto, con el siguiente contenido:

{% extends "base.html" %}

{% block title %}{{ post.title|title }}{% endblock %}

{% block content %}
    <a id="top"></a>

    <!-- Post -->
   {% include "post.html" %}
   <hr>

    <!-- Añadir comentario -->
    <h2>Añadir comentario:</h2>
    <a id="add_commentary"></a>

    <form action="{% url postdetails post.machine_name  %}#add_commentary" method="post">
        {{ form.as_p }}
        <p><input type="submit" value="Nuevo comentario"></p>
    </form>

    <!-- Comentarios  -->
    <h2>Comentarios:</h2>
    <a id="commentaries"></a>

    <section id="comments">
        {% if post.commentary_set.all %}
            {% for commentary in post.commentary_set.all %}
                <article>
                    <p>{{ commentary.owner  }} dijo</p>
                    <cite>{{ commentary.content }}</cite>
                    <aside>
                    <p>A {{commentary.publication_date|date:"r"}} </p>
                    </aside>
                </article>
             {% endfor %}
         {% else %}
             <p>Todavía no hay comentarios...</p>
         {% endif %}
      </section>
{% endblock %}

Este nuevo template merece algunas explicaciones rápidas:

  • La etiqueta {% include %} se utiliza para incluir un documento externo. A este documento se le pasa el contexto actual y se interpreta como si fuera otra plantilla.
  • El método as_p() del formulario se utiliza para volcar el formulario en párrafos
  • El método all() del conjunto commentary_set se utiliza para seleccionar todos sus elementos
  • La etiqueta {% if %} permite tomar decisiones. En caso de que haya comentarios, los mostramos y, si no, indicamos que aun no hay comentarios.

Modificaremos el archivo postlist.html y copiaremos el contenido del bucle for en otro archivo al que llamaremos post.html. La plantilla postlist.html quedará así:

{% extends "base.html" %}

{% block title %}Post list{% endblock %}

{% block content %}
<section id="postlist">

{% for post in object_list %}
{% include "post.html" %}
{% endfor %}

</section>
{% endblock %}

Y, por último, el fichero post.html contendrá sólo la parte que muestra el post y algunas modificaciones para incluir los comentarios:

<article>
    <header>
        <h2><a href="{% url postdetails post.machine_name %}">{{ post.title }}</a></h2>
    </header>
    <section>
    {{ post.content|safe }}
    </section>
    <aside>
    {% with comcount=post.commentary_set.all.count %}
        <p><a href="{% url postdetails post.machine_name %}#commentaries">{{ comcount }} comentario{{comcount|pluralize}}</a></p>
    {% endwith %}
    <p>Fecha de publicación: {{ post.publication_date|date:"r" }}</p>
    </aside>
</article>

Como últimas explicaciones:

  • La etiqueta {% with %} permite definir nombres alternativos (variables nuevas) como en el caso de comcount para almacenar el número de comentarios de un post.
  • El filtro pluralize añade una ‘s’ si la variable filtrada es distinta de 1.

4.- Gestión de posts y comentarios

Registraremos el modelo Commentary en la aplicación de administrador editando admin.py de la siguiente manera:

from posts.models import Post, Commentary
from django.contrib import admin

class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'briefing', 'publication_date', 'owner')

    list_filter = ['publication_date']

    date_hierarchy = 'publication_date'

    search_fields = ['title', 'content', 'owner__username', 'owner__first_name', 'owner__last_name']

    prepopulated_fields = { 'machine_name' : ('title', ) }

class CommentaryAdmin(admin.ModelAdmin):
    list_display = ('owner', 'post', 'publication_date')

    list_filter = ['publication_date', 'owner']

    search_fields = ['owner', 'content', 'post__title']

admin.site.register(Post, PostAdmin)
admin.site.register(Commentary, CommentaryAdmin)

Comprobarás que editar los comentarios es muy incómodo porque primero hay que localizar el post al que pertenece. Sería mucho más sencillo si pudiéramos editar el los comentarios de un post al mismo tiempo que el post en sí.

Para ello volveremos a editar admin.py y crearemos un modelo de administración «en línea» y le diremos al modelo de administración de post, PostAdmin, que lo incluya:

from posts.models import Post, Commentary
from django.contrib import admin

class CommentaryInline(admin.StackedInline):
    model = Commentary

class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'briefing', 'publication_date', 'owner')

    list_filter = ['publication_date']

    date_hierarchy = 'publication_date'

    search_fields = ['title', 'content', 'owner__username', 'owner__first_name', 'owner__last_name']

    prepopulated_fields = { 'machine_name' : ('title', ) }

    inlines = [ CommentaryInline ]

class CommentaryAdmin(admin.ModelAdmin):
    list_display = ('owner', 'post', 'publication_date')

    list_filter = ['publication_date', 'owner']

    search_fields = ['owner', 'content', 'post__title']

admin.site.register(Post, PostAdmin)
admin.site.register(Commentary, CommentaryAdmin)

Hemos añadido la propiedad inlines a PostAdmin indicando un modelo de administración en línea para los comentarios a través de la clase CommentaryInline.

5.- Añadiendo la posibilidad de buscar

Para terminar, vamos a añadir un formulario de búsqueda. Para ello aplicaremos todo lo aprendido anteriormente e introduciremos el concepto de context processor o procesador de contexto. Un procesador de contexto no es más que una función que acepta un objeto request y devuelve un diccionario que se añadirá al contexto.

Utilizamos el procesador de contexto para añadir el formulario de búsqueda a nuestros templates y no tener que reescribir el código de cada vista. Los procesadores de contexto son muy útiles para fragmentos de funcionalidad comunes a todas las páginas.

El formulario de búsqueda y el procesador de contexto

Editaremos el archivo forms.py de la siguiente manera:

from django import forms
from posts.models import Commentary

class AddCommentaryForm(forms.ModelForm):
    class Meta:
        model = Commentary
        fields = ('owner', 'content')

class SearchForm(forms.Form):
    query = forms.CharField(min_length=3, required=False)

Y ahora crearemos un nuevo archivo llamado context.py con el siguiente contenido:

from posts.forms import SearchForm

def searchform(request):
    if request.method == 'GET':
        form = SearchForm(request.GET)

    else:
        form = SearchForm()

    return { 'searchform' : form }

Debemos editar el archivo de configuración, settings.py, para incluir la variable TEMPLATE_CONTEXT_PROCESSORS. Obtendremos los valores por defecto de la documentación de Django y añadiremos el nuestro:

TEMPLATE_CONTEXT_PROCESSORS = (
    "django.contrib.auth.context_processors.auth",
    "django.core.context_processors.debug",
    "django.core.context_processors.i18n",
    "django.core.context_processors.media",
    "django.core.context_processors.static",
    "django.contrib.messages.context_processors.messages",
    "posts.context.searchform",
)

Para terminar con la integración del formulario de búsqueda, modificaremos el template base.html para que quede así:

{% load static %}
{% get_static_prefix as STATIC_URL %}
<!DOCTYPE HTML>
<html>
    <head>
        <title>Mi blog | {% block title %}Un título genérico{% endblock %}</title>
        <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}style.css">
    </head>

    <body>
        <form action="{% url searchresults %}" method="GET">
            <p>{{ searchform.query.errors }}</p>
            <label>Buscar: {{ searchform.query }}<input type="submit" value="Buscar"></label>
        </form>
        <header>
            <h1><a href="{% url mainpage %}">Mi blog</a></h1>
        </header>
        {% block content %}Un contenido genérico{% endblock %}
    </body>
</html>

La vista de resultados de búsqueda

De nuevo tendremos que crear crearemos un template, searchresults.html, con, por ejemplo, la siguiente estructura:

{% extends "base.html" %}

{% block title %}Search results{% endblock %}

{% block content %}

{% if object_list %}
<h2>Resultados:</h2>
{% for post in object_list %}
{% include "post.html" %}
{% endfor %}
{% else %}
<p>No hay resultados para la búsqueda indicada</p>
{% endif %}

{% endblock %}

También crearemos la vista en views.py como sigue:

from posts.forms import AddCommentaryForm, SearchForm
...
class SearchResults(ListView):
    template_name = "searchresults.html"

    def get_queryset(self):
        if self.request.method == 'GET':
            form = SearchForm(self.request.GET)
            if form.is_valid():
                query = form.cleaned_data['query']
                results = Post.objects.filter(content__icontains=query)
                return results

        return Post.objects.none()

Esta vista hereda de ListView como la vista PostList pero redefine el método get_queryset() para obtener el conjunto de resultados de la búsqueda. En este caso tratamos de construir un formulario de búsqueda válido. Si el formulario valida, accedemos a la información normalizada del formulario a través de la propiedad cleaned_data. Las validaciones y normalizaciones de los campos del formulario vienen dadas por los tipos de los campos definidos en la clase del formulario.

El resultado es un filtro de los objetos post que devolverá aquellos que contengan, en su contenido, la cadena buscada. La propiedad icontains indica que se comparará sin atender a mayúsculas o minúsculas.

Por último, en urls.py, enlazaremos la vista con la URL como sigue:

from posts.views import PostList, PostDetails, SearchResults
from django.conf.urls.defaults import patterns, include, url

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
# Examples:
# url(r'^$', 'djangoblog.views.home', name='home'),
# url(r'^djangoblog/', include('djangoblog.foo.urls')),

# Uncomment the admin/doc line below to enable admin documentation:
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

# Uncomment the next line to enable the admin:
url(r'^admin/', include(admin.site.urls), name='adminpage'),

url(r'^$', PostList.as_view(), name='mainpage'),
url(r'^posts/', PostList.as_view(), name='postlist'),
url(r'^post/(?P<slug>[a-zA-Z0-9_-]+)/', PostDetails.as_view(), name='postdetails'),
url(r'^search/', SearchResults.as_view(), name='searchresults'),
)

Hemos terminado… o no.

Una pequeña mejora con objetos Q

Nuestro buscador tiene un problema y es que en el momento que introduzacmos más de una palabra, sólo mostrará los resultados que contengan la cadena de búsqueda tal cual. Podríamos hacer que buscara el post que contuviese todas las palabras bien en su contenido, bien en el título.

Para ello utilizaremos objetos Q. Importa el objeto y modifica el archivo views.py:

from django.db.models import Q
...
class SearchResults(ListView):
template_name = "searchresults.html"

def get_queryset(self):
if self.request.method == 'GET':
form = SearchForm(self.request.GET)
if form.is_valid():
query = form.cleaned_data['query']
words = query.split(' ')
qobjects = [Q(content__icontains=w) | Q(title__icontains=w) for w in words]
condition = reduce(lambda x,y:x&y, qobjects)
results = Post.objects.filter(condition)
return results

return Post.objects.none()

En pocas palabras, los objetos Q se usan para construir las clausulas WHERE de SQL. Dos objetos Q pueden combinarse mediante | y & para combinar condiciones mediante OR y AND respectivamente.

Lo que hemos hecho puede resumirse en:

  1. Dividir la cadena de búsqueda en palabras
  2. Crear un objeto Q por palabra con el significado «contenido contiene palabra O título contiene palabra»
  3. Combinar todos los objetos Q en uno sólo para que busque los post que contengan TODAS las palabras
  4. Filtrar con este objeto Q

Y ahora sí. Hemos terminado.

Conclusión

A lo largo de estos cinco puntos he tratado de exponer Django como herramienta al mismo tiempo que he terminado de presentar los aspectos más importantes del framework. Mi idea era que vierais cómo programar de verdad con Django haciendo cosas un pelín fuera de lo común.

Espero que os haya sido entretenido y si queréis saber más cosas o ver cómo se puede codificar todo esto en una hora, os espero el día 5 de Mayo a las 18.00 de la tarde en el aula 13 de la Facultad de Informática de la UCM.

Si teneis cualquier problema, a los comentarios y si os quedais con ganas de que enseñe algo más, a los comentarios también.

Hasta la próxima.

26 comentarios en “Django 1.3 en «cero coma» II

  1. Después de seguir todos los pasos que comentas me sale este error cuando accedo a la URL:

    type object ‘PostDetails’ has no attribute ‘_as_view’

  2. Ahora me encuentro con otro error:
    In template /home/jose/djangoblog/templates/post.html, error at line 3
    Caught NoReverseMatch while rendering: Reverse for ‘postdetails’ with arguments ‘(u’-‘,)’ and keyword arguments ‘{}’ not found.

    Si quito las dos líneas que contienen la cadena:
    {% url postdetails post.machine_name %}
    Funciona.

    Le he dado vueltas y no encuentro el error.

      1. La expresión regular de la vista PostDetails en el archivo urls.py era incorrecta porque, como apunta Josué, sólo admitía guiones bajos. Ahora ya admite guiones también (tan sólo hay que añadir el guión a la lista de caracteres soportados).

    1. Ok, entonces lo que hay que poner en urls.py es:
      url(r’^post/(?P[a-zA-Z0-9_-]+)/’, PostDetails.as_view(), name=’postdetails’),

      Al final he añadido el guión medio y con esto ya funciona, pero…
      ¿me pueden explicar por qué? ¿por qué faltaba el guión medio si no lo estamos utilizando en ningún nombre de campo? El campo machine_name lleva un guión bajo, no un guión medio.
      No entiendo por qué faltaba este caracter.

      1. Ojo, te explico cómo funciona:
        url(r’^post/(?P[a-zA-Z0-9_-]+)/’, PostDetails.as_view(), name=’postdetails’)

        url() hace coincidir una determinada forma de URL con una vista. La forma de la URL se especifica como una expresión regular (en breve le dedicaré un post a expresiones regulares). Est expresión regular ‘^post/(?P[a-zA-Z0-9_-]+)/’ se explica más o menos así:

        – Debe comenzar con post (de ahí el ^) seguido de una barra
        – A continuación quiero que agrupes lo que encuentres en un conjunto llamado ‘slug’ (de ahí el ?P)
        – Este conjunto está formado al menos un símbolo (de ahí el +) que puede ser una letra, números, guión bajo y guión.
        – Al grupo debe seguirle otra barra

        La función url() mandará entonces algo de la forma ‘/posts/mi-primer-post/’ a la vista indicada. Y agrupará ‘mi-primer-post’ (esto es lo que lleva guiones) y lo mandará a la clase de vista como un parámetro llamado ‘slug’.

        Así, la expresión regular no se compara con el nombre del campo, sino con su contenido. Y su contenido es el que puede llevar los guiones.

        ¿Comprendido?

  3. lodr :
    ¿Comprendido?

    Perfectamente, muchas gracias por tan clara explicación.
    Me confundía entre el nombre del campo y el contenido. Lo más curioso es que de los posts que me he creado sólo en uno le puse un guión medio, sino llega a ser por esto me hubiera funcionado, no os hubiera molestado y no hubiera aprendido, jeje.

    Gracias.

  4. fenomenal! el tutorial, Felicidades y gracias por el aporte!.
    estoy haciendo un nuevo trabajo con django, algo sencillo, a modo de aprendisaje, es algo así como un libro de visitas.

    Pero he tenido problemas para crear el formulario… lodr crees que podrias echarme una mano.?

    1. Hola Barceló, si me explicas tu problema quizá pueda echarte una mano. Si no quieres exponerlo en público puedes mandarme un mail pero si lo cuelgas aquí, ¡aprendemos todos!

  5. ok lodr mejor se pega aqui para que aprendamos todos.
    bien, pues he enviado al grupo django en google mail exponiendo mi problema, pero pegare aqui el ultimo mensaje que he enviado al grupo:

    ya he logrado hacer el formulario pero bueno aún faltan unos detallitos.
    cuando lleno el formulario y presiono en el boton guardar, me sale un
    error 404 (adjunto una imagen)

    Las imagenes de la firma siguen sin verse(adjunto imagen), deberian
    verce en el marco, a la izquierda del comentario.

    En esta direción podran ver las imagenes: http://webftp.ssp.co.cu/django/

    En el modelo tengo esto:
    —————————————————————————————————-
    from django.db import models
    En la clase:
    photo = models.ImageField(upload_to = ‘staticfiles/photos_people/’,
    blank=True , verbose_name=’Foto’)
    ————————————————————————————————-
    y en la plantillas lo tengo así:
    ————————————————————————————————-
    Para las fotos:

    Para el formulario:


    Introdusca sus datos:

    {% csrf_token %}

    {{ formset }}

    ——————————————————————————-
    El server de produccion me devuelbe estos mensajes:
    ——————————————————————————-
    /usr/lib/pymodules/python2.6/django/template/defaulttags.py:101:
    UserWarning: A {% csrf_token %} was used in a template, but the
    context did not provide the value. This is usually caused by not
    using RequestContext.
    warnings.warn(«A {% csrf_token %} was used in a template, but the
    context did not provide the value. This is usually caused by not
    using RequestContext.»)
    [18/Jul/2011 09:51:40] «GET /firmar/ HTTP/1.1» 200 2520
    [18/Jul/2011 09:53:18] «POST /firmar/ HTTP/1.1» 403 2332
    [18/Jul/2011 09:53:47] «GET / HTTP/1.1» 200 2949
    [18/Jul/2011 09:53:47] «GET /staticfiles/photos_people/Irandy.jpg
    HTTP/1.1» 404 2468
    [18/Jul/2011 09:53:47] «GET /staticfiles/photos_people/photo_yun_4.jpg
    HTTP/1.1» 404 2483
    ——————————————————————————————————-
    una ultima preguntilla, xD ¿cómo le hago para que solo se
    muestren 10 firmas por página?

  6. lodr, creo que mejor pongo los ficheros del projecto aqui, esto es lo que tengo hasta ahora, bueno solo pongo los .py cuando lo termine, quiero hacer tambien una tutorial de como se hizo, para los que como yo tambien desean aprender django.

    models.py:

    from django.db import models
    from django.forms import ModelForm
    # Create your models here.
    class Firma(models.Model):
    firts_name = models.CharField(max_length=30, verbose_name='Nombre')
    last_name = models.CharField(max_length=30, verbose_name='Apellido')
    country = models.CharField(max_length=40, verbose_name='Pais')
    city = models.CharField(max_length=40, verbose_name='Ciudad')
    email = models.EmailField(blank=True, verbose_name='E-mail')
    photo = models.ImageField(upload_to = 'staticfiles/photos_people/', blank=True , verbose_name='Foto') #[, height_field=None, wigth_field=None])
    content = models.TextField()
    firm_date = models.DateTimeField(auto_now_add=True, verbose_name='Fecha de Firma')

    def __unicode__(self):
    return self.firts_name

    class Meta:
    ordering = [ '-firm_date' ]

    class FirmaForm(ModelForm):
    class Meta:
    model = Firma
    fields= ('firts_name', 'last_name', 'country', 'city', 'email', 'photo', 'content')

    views.py:

    # Create your views here.
    from django.shortcuts import render_to_response
    from django.views.generic import ListView
    from firmas.models import Firma
    from django.forms.models import modelformset_factory

    class FirmaList(ListView):
    template_name="friends_list.html"
    model = Firma

    def add_firma(request):
    FirmaFormSet = modelformset_factory(Firma)
    if request.method == 'POST':
    formset = FirmaFormSet(request.POST, request.FILES, queryset=Firma.objects.filter(firts_name__startswith= '0'))
    if formset.is_valid():
    formset.save()
    # do something.
    else:
    formset = FirmaFormSet(queryset=Firma.objects.filter(firts_name__startswith='0'))
    return render_to_response("add_firma.html", { "formset": formset, })

    urls.py:

    url(r'^admin/', include(admin.site.urls), name='adminpage'),
    url(r'^$', FirmaList.as_view(), name='mainpage'),
    url(r'^friends/', FirmaList.as_view(), name='friendslist'),
    url(r'^firmar/', add_firma, name='firmar'),

  7. Hola que tal tengo un problemita con el código,

    Exception Value:
    Reverse for » with arguments ‘()’ and keyword arguments ‘{}’ not found.

    y me sale este error

    11

    no se si habría la manera de ayudarme

      1. Hola Miguel

        Tienes que darme alguna pista más si quieres qué te ayuda. ¿Cuándo te ocurre eso?

Replica a Miguel de la Rosa Cancelar la respuesta