Skip to content

Blueprints and Views

CKAN extensions expose HTTP interfaces, endpoints, and web pages using Flask Blueprints. View functions are placed inside a views.py file (or views/ submodule) and registered automatically via blueprints blanket.


Defining Blueprints

Define a Blueprint instance and map your views. To ensure consistency in error states, you can register custom error handlers to render clean error templates:

views.py
from __future__ import annotations

import logging
from flask import Blueprint, jsonify
import ckan.plugins.toolkit as tk

log = logging.getLogger(__name__)

# 1. Define the blueprint
bp = Blueprint("myextension", __name__)

# Optional: export the blueprint explicitly
__all__ = ["bp"]


# 2. Register global error handlers for this blueprint
def not_found_handler(error: tk.ObjectNotFound) -> tuple[str, int]:
    """Render custom 404 page for ObjectNotFound exceptions."""
    return (
        tk.render(
            "error_document_template.html",
            {
                "code": 404,
                "content": f"Object not found: {error.message}",
                "name": "Not Found",
            },
        ),
        404,
    )

bp.register_error_handler(tk.ObjectNotFound, not_found_handler)

Implementing View Routes

Implement standard Flask routes. Use CKAN's toolkit functions to check permissions, render Jinja2 templates, and read request params.

views.py
@bp.route("/items")
def index() -> str:
    """Render items list page."""
    try:
        items = tk.get_action("myextension_item_list")({}, {}) # (1)!
    except tk.NotAuthorized:
        return tk.abort(403, "Not authorized to view items.")

    # Render templates placed inside the 'templates/' folder
    return tk.render("myextension/index.html", {"items": items})


@bp.route("/api/items/<item_id>") # (2)!
def api_show(item_id: str):
    """API endpoint returning item details."""
    try:
        # If you did not add global 404 error handler,
        # add explicit ObjectNotFound handler
        item = tk.get_action("myextension_item_show")({}, {"id": item_id})
    except tk.NotAuthorized:
        return tk.abort(403, "Not authorized to view the item.")

    return jsonify(item)
  1. You can pass empty context into auth functions and actions, when they are called from views. CKAN will add missing user and session properties automatically.
  2. Here you can use id as a name of the parameter, but the recommendation is to include entity type into parameter name. In this way you avoid shadowing of global function id() and can extend route in future with additional IDs. Think of confusing resource.show endpoint, which has id pointing at the package and resource_id pointint at the resource: it would be much better if parameters were named package_id and resource_id instead of id and resource_id.

Auto-Registration

Apply the @tk.blanket.blueprints decorator to your plugin class in plugin.py. This scans the views.py file for Flask blueprints and automatically hooks them into the CKAN routing framework.

plugin.py
import ckan.plugins as p
import ckan.plugins.toolkit as tk

@tk.blanket.blueprints  # (1)
class MyExtensionPlugin(p.SingletonPlugin):
    pass
  1. Automatic blueprint discovery removes the need to manually implement the IBlueprint interface or write route-registration boilerplate methods.