| icon | lucide/layout-template |
|---|
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.
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:
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)Implement standard Flask routes. Use CKAN's toolkit functions to check permissions, render Jinja2 templates, and read request params.
@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)- You can pass empty context into auth functions and actions, when they are
called from views. CKAN will add missing
userandsessionproperties automatically. - Here you can use
idas a name of the parameter, but the recommendation is to include entity type into parameter name. In this way you avoid shadowing of global functionid()and can extend route in future with additional IDs. Think of confusingresource.showendpoint, which hasidpointing at the package andresource_idpointint at the resource: it would be much better if parameters were namedpackage_idandresource_idinstead ofidandresource_id.
When calling actions (tk.get_action) or checking permissions
(tk.check_access) inside a Flask blueprint view function, you do not need
to manually populate the context dictionary with the current logged-in user
or the database session.
CKAN's internal Flask context wrappers automatically intercept logic layer calls originating from view routes. Under the hood, CKAN enriches the context by injecting the active database session and the currently authenticated session user.
For standard actions, passing an empty context dictionary {} is the recommended practice.
/// admonition type: example
@bp.route("/item/<item_id>/edit")
def edit_item(item_id: str):
# CKAN automatically populates {} with the logged-in user and active DB session
try:
tk.check_access("myextension_item_update", {}, {"id": item_id})
item = tk.get_action("myextension_item_show")({}, {"id": item_id})
except tk.NotAuthorized:
return tk.abort(403)
except tk.ObjectNotFound:
return tk.abort(404)
return tk.render("myextension/edit.html", {"item": item})///
You should pass explicit context parameters (such as a hardcoded username or a specific database state) only if the view explicitly needs to execute logic on behalf of a different user (for example, an administrator performing a resource transfer on behalf of another user, or executing system task tasks via an internal daemon context):
/// admonition type: example
@bp.route("/admin/transfer/<item_id>")
def admin_transfer(item_id: str):
# Only admins can access this route
tk.check_access("sysadmin", {}, {})
# Explicitly run the action on behalf of the target user, not the admin
context = {"user": "target_user_name"}
tk.get_action("myextension_item_update")(context, {"id": item_id, "owner_id": "new_owner"})
return "Transferred successfully"///
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.
import ckan.plugins as p
import ckan.plugins.toolkit as tk
@tk.blanket.blueprints # (1)
class MyExtensionPlugin(p.SingletonPlugin):
pass- Automatic blueprint discovery removes the need to manually implement the
IBlueprintinterface or write route-registration boilerplate methods.