Django site#
The publishing layer for the corpus the News pipeline pipeline produces. A Django project run to search, visualize, alert on, and share open-source reporting across their team.
Django earns its place here because the heavy lifting (ORM, admin, auth, sessions, CSRF, middleware, migrations) is already built; write models, views, and templates against those rails rather than re-inventing them.
Architecture#
Django follows the MVT pattern (Model, View, Template), the Django spin on MVC. A request flows top-to-bottom through a middleware stack, gets matched to a URL route, the view queries models, renders a template, the response unwinds back up the middleware stack.
flowchart LR
A[Client] -->|HTTP| B[WSGI/ASGI server]
B --> C[Middleware stack]
C --> D[URL router]
D --> E[View]
E -->|ORM| F[(Database)]
E --> G[Template]
G -->|HTML/JSON| C
C -->|Response| A
Layer |
Responsibility |
|---|---|
WSGI / ASGI server |
|
Middleware stack |
Auth, sessions, CSRF, security headers, gzip, logging. Order matters; outermost wraps everything. |
URL router |
Maps URL pattern to view callable. |
View |
Function or class. Reads request, queries models, returns response (HTML, JSON, redirect, file). |
Template |
Django template language. Server-rendered HTML with inheritance and includes. |
Model |
ORM declaration. Defines schema, indexes, constraints, querysets. |
Migrations |
Generated from model changes ( |
Project layout#
The standard Django layout, one project (the site) with multiple apps (each a self-contained domain).
site/
├── manage.py
├── pyproject.toml
├── site/ # the Django *project* (settings)
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── corpus/ # an app
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── views.py
│ ├── urls.py
│ ├── forms.py
│ ├── templates/corpus/
│ │ ├── base.html
│ │ ├── search.html
│ │ └── article_detail.html
│ └── migrations/
├── alerts/ # another app
│ ├── models.py
│ ├── tasks.py # celery tasks (if used)
│ └── views.py
└── static/
Models#
The data the corpus app stores. Indexes named explicitly because
operator queries filter on publish_date and sentiment.
# corpus/models.py
from django.db import models
from django.contrib.auth import get_user_model
class Article(models.Model):
url = models.URLField(max_length=2048, unique=True)
title = models.CharField(max_length=512)
text = models.TextField()
fetched_at = models.DateTimeField()
publish_date = models.DateTimeField(null=True, blank=True)
sentiment = models.FloatField()
organizations = models.JSONField(default=list)
persons = models.JSONField(default=list)
class Meta:
indexes = [
models.Index(fields=["publish_date"]),
models.Index(fields=["sentiment"]),
]
def __str__(self):
return self.title
class SavedQuery(models.Model):
owner = models.ForeignKey(get_user_model(),
on_delete=models.CASCADE)
name = models.CharField(max_length=200)
q = models.CharField(max_length=512)
entity = models.CharField(max_length=200, blank=True)
min_sent = models.FloatField(null=True, blank=True)
max_sent = models.FloatField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
Views#
Function-based views for simple endpoints, class-based for listing / detail. The corpus app exposes search and detail.
# corpus/views.py
from django.shortcuts import render, get_object_or_404
from django.db.models import Q
from .models import Article
def search(request):
q = request.GET.get("q", "")
entity = request.GET.get("entity", "")
results = Article.objects.all()
if q:
results = results.filter(
Q(title__icontains=q) | Q(text__icontains=q)
)
if entity:
results = results.filter(
Q(organizations__icontains=entity) |
Q(persons__icontains=entity)
)
return render(request, "corpus/search.html", {
"q": q, "entity": entity,
"results": results.order_by("-publish_date")[:50],
})
def article_detail(request, pk):
art = get_object_or_404(Article, pk=pk)
return render(request, "corpus/article_detail.html",
{"article": art})
# corpus/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.search, name="search"),
path("article/<int:pk>/", views.article_detail, name="article"),
]
Templates#
Server-rendered HTML with inheritance.
{# corpus/templates/corpus/base.html #}
<!doctype html>
<html>
<head><title>{% block title %}News{% endblock %}</title></head>
<body>
<header><a href="{% url 'search' %}">Search</a></header>
{% block content %}{% endblock %}
</body>
</html>
Admin#
Django’s built-in admin gives the operator a CRUD UI for free. Register the model, add list-display and filter hints.
# corpus/admin.py
from django.contrib import admin
from .models import Article, SavedQuery
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ("title", "publish_date", "sentiment")
list_filter = ("publish_date",)
search_fields = ("title", "text")
ordering = ("-publish_date",)
admin.site.register(SavedQuery)
Auth#
Django ships sessions, password hashing (PBKDF2 or Argon2 if
configured), permissions, groups, and a @login_required
decorator. Add django-allauth for OAuth and SSO when the team
authenticates against an IdP.
from django.contrib.auth.decorators import login_required
@login_required
def search(request):
...
Alerts#
Per-user saved queries that run on a schedule and email or webhook the user when new articles match.
sequenceDiagram
participant Cron as Celery beat
participant Worker as Celery worker
participant DB as Database
participant SMTP as SMTP / webhook
Cron->>Worker: tick (every 5m)
Worker->>DB: load SavedQuery rows
loop for each SavedQuery
Worker->>DB: query Article matching filters
DB-->>Worker: new matches since last run
Worker->>SMTP: send email / POST webhook
end
The worker process runs Celery against Redis (broker) or
PostgreSQL (with django-celery-beat). See Frameworks
for the Celery side.
Common Tasks#
Bootstrap a Django project.
$ uv add django
$ uv run django-admin startproject site .
$ uv run python manage.py startapp corpus
$ uv run python manage.py migrate
Make and apply migrations after a model change.
$ uv run python manage.py makemigrations
$ uv run python manage.py migrate
Create a superuser for the admin.
$ uv run python manage.py createsuperuser
Run the dev server.
$ uv run python manage.py runserver 0.0.0.0:8000
Run a one-off shell against the project (ORM available).
$ uv run python manage.py shell
>>> from corpus.models import Article
>>> Article.objects.filter(sentiment__lt=-0.5).count()
Load the tabulated corpus into the database.
$ uv run python manage.py load_corpus ../news/data/tabulated/corpus.parquet
Run tests.
$ uv run python manage.py test
References#
News pipeline for the upstream pipeline that produces the corpus this site serves.
Frameworks for Django, Celery, and the broader framework chapter.
Django REST Framework for JSON APIs.
django-allauth for OAuth and SSO.
Celery for the alert worker.