Escribiendo su primera aplicación en Django, parte 2

Este tutorial comienza donde quedó el Tutorial 1. Vamos a configurar la base de datos, crear su primer modelo y recibir una introducción rápida al sitio administrativo generado automáticamente de Django.

Configuración de la base de datos

Ahora, abra el archivo mysite/settings.py. Es un módulo normal de Python con variables de nivel de módulo que representan la configuración de Django.

Por defecto la configuración utiliza SQLite. Si tiene poca experiencia con bases de datos o su interés es solo probar Django esta es la opción más fácil. SQLite está incluido en Python por lo que no tendrá que instalar nada más para soportar su base de datos. Sin embargo, al iniciar su primer proyecto real, es posible que desee utilizar una base de datos más potente como PostgreSQL para evitar dolores de cabeza en el futuro al tener que cambiar entre base de datos.

Si desea utilizar otra base de datos, instale los conectores de base de datos apropiados, y cambie las siguientes claves en el ítem DATABASES 'default' para que se ajusten a la configuración de conexión de la base de datos:

  • ENGINE – bien sea 'django.db.backends.sqlite3', 'django.db.backends.postgresql_psycopg2', 'django.db.backends.mysql', o 'django.db.backends.oracle'. Otros backends también están disponibles.
  • NAME –el nombre de su base de datos. Si está utilizando SQLite, la base de datos será un archivo en su computadora; en ese caso NAME debe ser la ruta absoluta completa, incluyendo el respectivo nombre del archivo. El valor predeterminado, os.path.join(BASE_DIR, 'db.sqlite3'), guardará el archivo en el directorio de su proyecto.

Si no está utilizando SQLite como su base de datos, se deben añadir ajustes adicionales tales como USER, PASSWORD, y HOST se deben añadir. Para más información, vea la documentación de referencia para DATABASES.

Para bases de datos distintas a SQLite

Si está utilizando una base de datos SQLite, asegúrese de que ha creado ya la base de datos en este punto. Hágalo con el comando «CREATE DATABASE database_name;» en la consola interactiva de la base de datos.

Del mismo modo asegúrese de que la base de datos proporcionada en el archivo mysite/settings.py tiene permisos de tipo «create database». Esto permitirá la creación automática de test database que será necesaria en un tutorial posterior.

Si estás usando SQLite, no es necesario crear nada de antemano - el archivo de base de datos se creará automáticamente cuando sea necesario.

Mientras que usted está editando el archivo mysite/settings.py, configure TIME_ZONE a su zona horaria.

Además, observe que la configuración de INSTALLED_APPS se encuentra en la parte superior del archivo. Esta contiene los nombres de todas las aplicaciones Django que están activadas en esta instancia de Django. Las aplicaciones se pueden usar en diversos proyectos y usted puede empaquetar y distribuirlas para que otras personas las puedan utilizar en sus proyectos.

Por defecto, INSTALLED_APPS contiene las siguientes aplicaciones y Django viene equipado con todas ellas:

Estas aplicaciones se incluyen de forma predeterminada como una conveniencia para el caso común.

Algunas de estas aplicaciones utilizan al menos una tabla de base de datos, por lo que necesitamos crear las tablas en la base de datos antes de poder utilizarlas. Para ello, ejecute el siguiente comando:

$ python manage.py migrate
...\> py manage.py migrate

El comando migrate analiza la configuración INSTALLED_APPS y crea las tablas de base de datos necesarias según la configuración de base de datos en su archivo mysite/settings.py y las migraciones de base de datos distribuidas con la aplicación (trataremos este tema más tarde). Verá un mensaje para cada migración que aplique. Si está interesado, ejecute el cliente de línea de comandos para su base de datos y teclee \dt (PostgreSQL), SHOW TABLES; (MySQL), .schema (SQLite), o SELECT TABLE_NAME FROM USER_TABLES; (Oracle) para desplegar las tablas que creó Django.

Para los minimalistas

Como dijimos anteriormente, las aplicaciones predeterminadas se incluyen para el caso común, pero no todos las necesitan. Si usted no necesita alguna o ninguna de ellas, no dude en dejar fuera o borrar las línea(s) correspondientes desde INSTALLED_APPS antes de ejecutar el comando migrate. El comando migrate solo ejecutará migraciones para aplicaciones en INSTALLED_APPS.

Creando modelos

A continuación definiremos sus modelos, sobre todo su estructura de base de datos, con metadatos adicionales.

Filosofía

Un modelo es la fuente única y definitiva de información sobre sus datos. Contiene los campos esenciales y los comportamientos de los datos que usted guarda. Django sigue el Principio DRY. El objetivo es definir el modelo de datos en un solo lugar y derivar cosas de este automáticamente.

Este incluye las migraciones, a diferencia de Ruby On Rails, por ejemplo, las migraciones se derivan totalmente de su archivo de modelos y son esencialmente sólo un historial a través del cual Django puede pasar para actualizar su esquema de base de datos para que coincida con sus modelos actuales.

En nuestra sencilla aplicación encuesta, vamos a crear dos modelos: Question y Choice . Una Question tiene una pregunta y una fecha de publicación. Una Choice tiene dos campos: el texto de la elección y un conteo de votos. Cada Choice se asocia a una Question.

Estos conceptos se representan mediante clases sencillas de Python. Edite el archivo polls/models.py de modo que se vea de la siguiente manera:

polls/models.py
from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

El código es sencillo. Cada modelo está representado por una clase que subclasifica django.db.models.Model. Cada modelo tiene una serie de variables de clase, cada uno de los cuales representa un campo de la base de datos en el modelo.

Cada campo está representado por una instancia de una clase Field, por ejemplo, CharField para campos de caracteres y DateTimeField para variables de tiempo y fecha. Esto le dice a Django qué tipo de datos cada campo contiene.

El nombre de cada instancia Field (por ejemplo, question_text o pub_date)` es el nombre del campo, en formato adaptado al lenguaje de la máquina. Va a usar este valor en el código Python y su base de datos va a usarlo como el nombre de la columna.

Puede emplear un primer argumento posicional opcional a una Field para designar un nombre legible por humanos. Ese se utiliza en varias partes introspectivas de Django y sirve al mismo tiempo como documentación. Si no se proporciona este campo, Django usará el nombre legible por la máquina. En este ejemplo, sólo hemos definido un nombre legible para Question.pub_date. Para el resto de los campos en este modelo, el nombre del campo legible por la máquina servirá como el nombre legible por humanos.

Algunas clases Field precisan argumentos. La clase CharField, por ejemplo, requiere que usted le asigne un max_length. Esta se utiliza no sólo en el esquema de la base de datos, sino también en la validación como veremos dentro de poco.

Una clase Field también puede tener varios argumentos opcionales; en este caso, le hemos fijado al default el valor de votes en 0.

Por último, tenga en cuenta que una relación se define usando ForeignKey. Eso le indica a Django que cada Choice se relaciona con un sola Question. Django es compatible con todas las relaciones de bases de datos comunes: varias a una, varias a varias y una a una.

Activando los modelos

Ese pequeño fragmento de código de modelo le proporciona a Django mucha información. Con él Django es capaz de:

  • Crear un esquema de base de datos para esta aplicación (oraciones CREATE TABLE).
  • Crear una API de acceso a la base datos Python para acceder a los objetos Question y Choice.

Pero primero tenemos que indicarle a nuestro proyecto que la aplicación polls está instalada.

Filosofía

Las aplicaciones Django son «conectables»: Usted puede utilizar una aplicación en diversos proyectos y puede distribuir aplicaciones porque ellas no necesitan estar atadas a una determinada instalación de Django.

Para incluir la aplicación en nuestro proyecto necesitamos agregar una referencia a su clase de configuración en la configuración INSTALLED_APPS. La clase PollsConfig está en el archivo polls/apps.py por lo que su ruta punteada es 'polls.apps.PollsConfig'. Edite el archivo mysite/settings.py y agregue la ruta punteada a la configuración INSTALLED_APPS. Se verá así:

mysite/settings.py
INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Ahora Django sabe incluir la aplicación polls. Vamos a ejecutar otro comando:

$ python manage.py makemigrations polls
...\> py manage.py makemigrations polls

Usted debe ver algo similar a lo siguiente:

Migrations for 'polls':
  polls/migrations/0001_initial.py:
    - Create model Choice
    - Create model Question
    - Add field question to choice

Al ejecutar makemigrations, usted le indica a Django que ha realizado algunos cambios a sus modelos (en este caso, ha realizado cambios nuevos) y que le gustaría que los guarde como una migración.

Django guarda los cambios en sus modelos como migraciones (y por lo tanto en su esquema de base de datos); son solo archivos en el disco. Usted puede leer la migración para su nuevo modelo si lo desea, es el archivo polls/migrations/0001_initial.py. No se preocupe, no se espera que usted las lea cada vez que Django hace una, sino que están diseñadas para que sean editables en caso de que usted desee modificar manualmente como Django cambia las cosas.

Hay un comando que ejecutará las migraciones para usted y gestionará el esquema de base de datos automáticamente; este se denomina migrate, y hablaremos de ello en un momento, pero primero, vamos a ver cuál SQL esa migración ejecutaría . El comando sqlmigrate recibe nombres de migración y devuelve su SQL:

$ python manage.py sqlmigrate polls 0001
...\> py manage.py sqlmigrate polls 0001

Debería ver algo similar a lo siguiente (lo hemos reformateado para facilitar la lectura):

BEGIN;
--
-- Create model Choice
--
CREATE TABLE "polls_choice" (
    "id" serial NOT NULL PRIMARY KEY,
    "choice_text" varchar(200) NOT NULL,
    "votes" integer NOT NULL
);
--
-- Create model Question
--
CREATE TABLE "polls_question" (
    "id" serial NOT NULL PRIMARY KEY,
    "question_text" varchar(200) NOT NULL,
    "pub_date" timestamp with time zone NOT NULL
);
--
-- Add field question to choice
--
ALTER TABLE "polls_choice" ADD COLUMN "question_id" integer NOT NULL;
ALTER TABLE "polls_choice" ALTER COLUMN "question_id" DROP DEFAULT;
CREATE INDEX "polls_choice_7aa0f6ee" ON "polls_choice" ("question_id");
ALTER TABLE "polls_choice"
  ADD CONSTRAINT "polls_choice_question_id_246c99a640fbbd72_fk_polls_question_id"
    FOREIGN KEY ("question_id")
    REFERENCES "polls_question" ("id")
    DEFERRABLE INITIALLY DEFERRED;

COMMIT;

Tenga en cuenta lo siguiente:

  • La salida exacta variará dependiendo de la base de datos que esté utilizando. El ejemplo anterior se genera para PostgreSQL.
  • Los nombres de las tablas se generan automáticamente combinando el nombre de la aplicación (polls) y el nombre del modelo en minúscula; question y choice. (Usted puede anular este comportamiento)
  • Las claves primarias (IDs) se agregan automáticamente. (Usted también puede anular esto)
  • Convencionalmente, Django añade "_id" al nombre del campo de la clave externa (sí, usted también puede anular esto)
  • La relación de la clave externa se hace explícita por una restricción``FOREIGN KEY``. No se preocupe por las partes DEFERRABLE; eso solo le indica a PostgreSQL que no aplique la clave externa hasta el final de la transacción.
  • Se adapta a la base de datos que está utilizando, así que los tipos de campos específicos de la bases de datos como auto_increment (MySQL), serial (PostgreSQL) o integer primary key autoincrement (SQLite) se gestionan de forma automática. Lo mismo se aplica para la cita de nombres de campos, por ejemplo, el uso de comillas dobles o comillas simples.
  • El comando sqlmigrate en realidad no ejecuta la migración en su base de datos, sólo la imprime en la pantalla para que pueda ver lo que SQL Django piensa que se requiere. Es útil para comprobar lo que Django va a hacer o si usted tiene administradores de bases de datos que requieran scripts SQL para introducir cambios.

Si le interesa, usted también puede ejecutar el comando python manage.py check; este revisa cualquier problema en su proyecto sin hacer migraciones o modificar la base de datos.

A continuación, ejecute de nuevo el comando migrate para crear esas tablas modelos en su base de datos:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Rendering model states... DONE
  Applying polls.0001_initial... OK
...\> py manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Rendering model states... DONE
  Applying polls.0001_initial... OK

El comando migrate toma todas las migraciones que no han sido aplicadas (Django rastrea cuales se aplican utilizando una tabla especial en su base de datos llamada django_migrations) y las ejecuta contra su base de datos; básicamente, sincronizando los cambios que usted ha realizado a sus modelos con el esquema en la base de datos.

Las migraciones son muy potentes y le permiten modificar sus modelos con el tiempo, a medida que desarrolla su proyecto, sin necesidad de eliminar su base de datos o las tablas y hacer otras nuevas. Este se especializa en la actualización de su base de datos en vivo, sin perder datos. Vamos a hablar de ellas en mayor profundidad más tarde en el tutorial, pero por ahora, recuerde la guía de tres pasos para hacer cambios de modelo:

The reason that there are separate commands to make and apply migrations is because you’ll commit migrations to your version control system and ship them with your app; they not only make your development easier, they’re also usable by other developers and in production.

Lea la documentación del django-admin para obtener información detallada sobre lo que puede hacer la utilidad manage.py.

Jugando con la API

Ahora vayamos al shell interactivo de Python y juguemos con la API gratuita que Django le proporciona. Para llamar el shell de Python, utilice este comando:

$ python manage.py shell
...\> py manage.py shell

Estamos usando esto en vez de simplemente escribir «python», porque el archivo manage.py establece la variable de entorno DJANGO_SETTINGS_MODULE, que le suministra a Django la ruta de importación de Python para su archivo mysite/settings.py.

Una vez que esté en el shell, explore la API de base de datos:

>>> from polls.models import Choice, Question  # Import the model classes we just wrote.

# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>

# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# Save the object into the database. You have to call save() explicitly.
>>> q.save()

# Now it has an ID.
>>> q.id
1

# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

Un segundo. <Question: Question object (1)> no es una representación útil de este objeto. Arreglemos esto modificando el modelo Question (en el archivo polls/models.py) y agregando un metodo __str__() a los dos modelos, Question y Choice:

polls/models.py
from django.db import models

class Question(models.Model):
    # ...
    def __str__(self):
        return self.question_text

class Choice(models.Model):
    # ...
    def __str__(self):
        return self.choice_text

Es importante añadir los métodos __str__() a sus modelos, no solo para su conveniencia al lidiar con la línea de comandos interactiva, sino también porque las representaciones de objetos se usan en todo el sitio administrativo generado automáticamente de Django.

Let’s also add a custom method to this model:

polls/models.py
import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

Tenga en cuenta la adición de import datetime y from django.utils import timezone, para hacer referencia el módulo estándar de Python datetime y las herramientas relacionadas con el huso horario de Django django.utils.timezone respectivamente. Si usted no está familiarizado con el manejo de la zona horaria en Python, usted puede aprender más en la documentación de soporte de zona horaria.

Guarde estos cambios e inicie un nuevo shell interactivo Python ejecutando de nuevo python manage.py shell:

>>> from polls.models import Choice, Question

# Make sure our __str__() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>

# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.

# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>

# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. Django creates
# a set to hold the "other side" of a ForeignKey relation
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)

# Display any choices from the related object set -- none so far.
>>> q.choice_set.all()
<QuerySet []>

# Create three choices.
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)

# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>

# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3

# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()

Para obtener más información sobre las relaciones de modelos, consulte Accediendo a objetos relacionados. Para más información sobre cómo utilizar guiones bajos para realizar búsquedas de campo a través de la API, consulte :ref: Búsquedas de campos <field-lookups-intro>. Para más detalles sobre la API de base de datos, consulte nuestra Referencia de API de base de datos.

Presentando el sitio administrativo de Django

Filosofía

La generación de sitios administrativos para su personal o clientes para agregar, modificar y eliminar contenido es un trabajo aburrido que no requiere mucha creatividad. Por esa razón, Django automatiza completamente la creación de interfaces de sitios administrativos para los modelos.

Django fue escrito en un entorno de sala de redacción, con una separación muy clara entre «los editores de contenido» y el sitio «público». Los administradores del sitio utilizan el sistema para agregar noticias, eventos, resultados deportivos, etc., y ese contenido se muestra en el sitio público. Django resuelve el problema de crear una interfaz unificada para los administradores del sitio para editar el contenido.

El sitio admin no está destinado a ser utilizado por los visitantes del sitio. Es para los administradores del sitio.

Creando un usuario del admin

Primero tendremos que crear un usuario que pueda iniciar sesión en el sitio administrativo. Ejecute el siguiente comando:

$ python manage.py createsuperuser
...\> py manage.py createsuperuser

Introduzca su nombre de usuario deseado y pulse enter.

Username: admin

A continuación se le solicitará su dirección de correo electrónico deseada:

Email address: admin@example.com

El paso final es introducir su contraseña. Se le pedirá que introduzca su contraseña dos veces, la segunda vez como confirmación de la primera.

Password: **********
Password (again): *********
Superuser created successfully.

Inicie el servidor de desarrollo

El sitio administrativo de Django se activa de forma predeterminada. Vamos a iniciar el servidor de desarrollo y a explorarlo.

Si el servidor no está en marcha, inícielo de la siguiente forma:

$ python manage.py runserver
...\> py manage.py runserver

A continuación, abra un navegador Web y vaya a «/admin/» en su dominio local, por ejemplo, http://127.0.0.1:8000/admin/. Usted debe ver la pantalla de inicio de sesión del sitio administrativo:

Django admin login screen

Dado que la traducción se activa de forma predeterminada, la pantalla de inicio de sesión puede aparecer en su propio idioma, dependiendo de la configuración del navegador y de si Django tiene una traducción para este idioma.

Acceda al sitio administrativo

Ahora, intente iniciar sesión con la cuenta de superusuario que creó en el paso anterior. Debería ver la página de índice del sitio administrativo de Django:

Django admin index page

Usted debería ver algunos tipos de contenidos editables: grupos y usuarios. Ellos son proporcionados por django.contrib.auth, el framework de autenticación enviado por Django.

Haga que la aplicación encuesta se pueda modificar en el sitio administrativo

Pero ¿Dónde está nuestra aplicación encuesta? No se muestra en la página de índice del sitio administrativo.

Sólo hay una cosa por hacer: tenemos que indicarle al sitio administrativo que los objetos Question tienen una interfaz del sitio administrativo. Para ello, abra el archivo polls/admin.py y edítelo para que se vea de la siguiente manera:

polls/admin.py
from django.contrib import admin

from .models import Question

admin.site.register(Question)

Explore la funcionalidad gratuita del sitio administrativo

Ahora que hemos registrado el modelo Question, Django sabe que se debe desplegar en la página de índice del sitio administrativo:

Django admin index page, now with polls displayed

Haga clic en «Questions». Ahora usted está en la página «lista de cambios» para las preguntas. Esta página muestra todas las preguntas de la base de datos y le permite seleccionar una para modificarla. Ahí está la pregunta «¿Qué pasa?» que creamos anteriormente:

Polls change list page

Haga clic en la pregunta «¿Qué pasa?» para editarla:

Editing form for question object

Cosas a tener en cuenta aquí:

  • El formulario se genera automáticamente a partir del modelo Question.
  • Los diferentes tipos de campos del modelo (DateTimeField, CharField) corresponden al widget de entrada HTML adecuado. Cada tipo de campo sabe cómo mostrarse en el sitio administrativo de Django.
  • Cada DateTimeField tiene atajos de JavaScript sin restricciónes. Las fechas tienen un atajo «Hoy» y una ventana emergente del calendario, mientras que las horas tienen un atajo «Ahora» y una ventana emergente conveniente que enumera las horas que se registran comúnmente.

La parte inferior de la página le da un par de opciones:

  • Guardar – Guarda los cambios y retorna a la página de la lista de cambios para este tipo de objeto.
  • Guardar y continuar editando – Guarda los cambios y recarga la página del sitio administrativo para este objeto.
  • Guardar y añadir otro – Guarda los cambios y carga un nuevo formulario vacio para este tipo de objeto.
  • Eliminar – Muestra una página de confirmación de eliminación.

Si el valor de «Date published» no coincide con la hora en la que creó la pregunta en el Tutorial 1, probablemente significa que se olvidó de establecer el valor correcto para la configuración de TIME_ZONE. Modifíquelo, recargue la página y compruebe que aparezca el valor correcto.

Modifique la «Date published» haciendo clic en los atajos «Hoy» y «Ahora». Después, haga clic en «Guardar y continuar editando». Luego haga clic en «Historial» en la parte superior derecha. Usted verá una página que enumera todos los cambios realizados a este objeto a través del sitio administrativo de Django, con el registro de tiempo y el nombre de usuario de la persona que realizó el cambio:

History page for question object

Cuando se sienta cómodo con la API de modelos y se haya familiarizado con el sitio administrativo, lea la parte 3 de este tutorial para aprender sobre cómo agregar más vistas a nuestra aplicación encuestas.

Back to Top