Para iniciar un proyecto con Wagtail, se requiere un entorno virtual de Python 3 y la insatlación del paquete a través de pip. La estructura inicial del proyecto se genera mediante el comando wagtail start, seguido de la ejecución de migraciones y creación de un usuario administrador.
# Crear entorno aislado con Python 3
python -m venv entorno_wagtail
# Instalar el framework
pip install wagtail
# Inicializar estructura del proyecto
wagtail start mi_portal
cd mi_portal
pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
La localización al idioma chino simplificado se configura en el archivo settings/base.py, estableciendo la zona horaria de Shanghái. Se recomienda desactivar el uso de zonas horarias en base de datos (USE_TZ = False) si no se requiere soporte para múltiples zonas.
# Configuración regional en settings/base.py
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
Para el sistema de publicaciones, se definen modelos jerárquicos que heredan de Page. El modelo principal del índice incluye un campo de introducción enriquecido y un método personalizado de contexto para ordenar las publicaciones descendientemente por fecha.
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index
class SectionIndex(Page):
description = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('description', classname="full")
]
class Meta:
verbose_name = "Sección principal"
verbose_name_plural = "Secciones principales"
def get_context(self, request):
ctx = super().get_context(request)
articles = self.get_children().live().order_by('-first_published_at')
ctx['published_articles'] = articles
return ctx
class ArticlePage(Page):
publication_date = models.DateField("Fecha de publicación")
source = models.CharField("Fuente", max_length=250)
content = RichTextField("Contenido principal", blank=True)
attachment = models.FileField("Adjunto multimedia", blank=True, upload_to="uploads")
search_fields = Page.search_fields + [
index.SearchField('source'),
index.SearchField('content'),
]
class Meta:
verbose_name = "Artículo"
verbose_name_plural = "Artículos"
content_panels = Page.content_panels + [
FieldPanel('publication_date'),
FieldPanel('source'),
InlinePanel('media_gallery', label="Galería de medios"),
FieldPanel('attachment'),
FieldPanel('content', classname="full"),
]
def primary_image(self):
first_item = self.media_gallery.first()
return first_item.media_file if first_item else None
class MediaItem(Orderable):
parent_page = ParentalKey(ArticlePage, on_delete=models.CASCADE, related_name='media_gallery')
media_file = models.ForeignKey(
'wagtailimages.Image', on_delete=models.CASCADE, related_name='+'
)
description = models.CharField('Descripción', blank=True, max_length=250)
panels = [
ImageChooserPanel('media_file'),
FieldPanel('description'),
]
Las plantillas se estructuran heredando de un template base. En la página principal, se muestra un mapa interactivo incrustado y listas de categorías de contenido. Los artículos se presentan mediante tarjetas que incluyen imagen principal, extracto y enlace a la publicación completa.
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block content %}
<div class="container-fluid">
<div class="map-container text-center my-4">
<iframe src="https://example.com/map/"
width="90%" height="600" frameborder="0">
</iframe>
</div>
<div class="row mt-4">
{% for article in published_articles %}
<div class="col-md-4 mb-4">
<div class="card h-100">
{% with article.primary_image as main_img %}
{% if main_img %}
{% image main_img fill-400x300 as thumb %}
<img src="{{ thumb.url }}" class="card-img-top" alt="">
{% endif %}
{% endwith %}
<div class="card-body">
<h5 class="card-title">
<a href="{% pageurl article %}">{{ article.title }}</a>
</h5>
<p class="card-text">
{{ article.source|richtext|truncatewords_html:30 }}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row mt-4">
<div class="col-md-6">
<h4>Documentos relevantes</h4>
<ul class="list-group">
{% for item in documents_list %}
<a href="{% pageurl item %}" class="list-group-item list-group-item-action">
{{ item.title }}
</a>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}
La plantilla de listado de artículos incluye manejo condicional de medios: muestra imágenes si existen, o reproductores de video para archivos adjuntos de tipo video. Los extractos se truncarán para mantener la vista concisa.
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block content %}
<div class="container">
{% for item in published_articles %}
<div class="article-preview mb-5">
<h2><a href="{% pageurl item %}">{{ item.title }}</a></h2>
<p class="text-muted">
{{ item.publication_date }} - {{ item.source }}
</p>
{% if item.primary_image %}
{% image item.primary_image width-800 %}
{% elif item.attachment %}
<div class="ratio ratio-16x9">
<video controls class="embed-responsive-item">
<source src="{{ item.attachment.url }}" type="video/mp4">
Su navegador no soporta la etiqueta de video.
</video>
</div>
{% endif %}
<div class="mt-3">
{{ item.content|richtext|truncatewords_html:50 }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
Para la vista detallada de artículos, se implementan múltiples tipos de medios: imágenes de cabecera, videos con controles nativos y galería de imágenes con leyendas. Se incluyen navegación contextual para regresar a la sección padre y botón de impresión.
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block content %}
<article class="container py-5">
<header class="mb-5 text-center">
{% image page.cover_image width-1200 as hero_img %}
<img src="{{ hero_img.url }}" class="img-fluid rounded mb-4" alt="">
<h1>{{ page.title }}</h1>
<div class="d-flex justify-content-center gap-4 text-muted">
<span>Publicado: {{ page.publication_date }}</span>
<span>Fuente: {{ page.source }}</span>
</div>
</header>
<div class="content-body">
{{ page.content|richtext }}
{% if page.attachment %}
<div class="my-4 ratio ratio-21x9">
<video controls class="rounded">
<source src="{{ page.attachment.url }}" type="video/mp4">
<source src="{{ page.attachment.url }}" type="video/webm">
Formato no soportado. Por favor actualice su navegador.
</video>
</div>
{% endif %}
{% if page.media_gallery.all %}
<section class="gallery mt-5">
<h3 class="mb-4">Galería de imágenes</h3>
<div class="row">
{% for media in page.media_gallery.all %}
<div class="col-md-6 mb-4">
{% image media.media_file width-1000 %}
<p class="mt-2 text-center">{{ media.description }}</p>
</div>
{% endfor %}
</div>
</section>
{% endif %}
</div>
<footer class="mt-5 pt-4 border-top text-center">
<a href="{{ page.get_parent.url }}" class="btn btn-outline-secondary me-3">
Volver al listado
</a>
<button onclick="window.print()" class="btn btn-outline-primary">
Imprimir
</button>
</footer>
</article>
{% endblock %}