Skip to content
Skip to the article
In Frappe: 20 articles
Frappe

Portal Views

Use list-mode web forms with sidebar navigation to show logged-in users their own records in a portal.

Updated
Tags
  • frappe
  • web-forms
  • portal
  • website
Reading time
4 min

Authenticated list-mode web forms that present a filtered table of records to logged-in users, combined with sidebar navigation for a portal experience.

Purpose

Portal Views expose a "my documents" list to logged-in users (e.g., guardians viewing their players' applications). They use the same Web Form infrastructure but in list mode, applying per-user document permissions so each user sees only their own records.

File Structure

mymodule/web_form/my_portal_view/
├── my_portal_view.json    # Web form definition (show_list: 1, login_required: 1)
├── my_portal_view.py      # Server-side get_context() for list filtering, sidebar
└── my_portal_view.js      # Client-side enhancements (optional)

JSON Configuration

Key settings that distinguish a portal view from a normal web form:

{
    "doctype": "Web Form",
    "name": "my-portal-view",
    "doc_type": "Player Application",
    "route": "portal/applications",
    "module": "My Module",
    "is_standard": 1,
    "published": 1,

    "login_required": 1,
    "show_list": 1,
    "allow_edit": 0,
    "allow_multiple": 0,
    "apply_document_permissions": 1,

    "list_columns": [
        {"fieldname": "player_full", "fieldtype": "Data", "label": "Player"},
        {"fieldname": "season", "fieldtype": "Link", "label": "Season", "options": "Season"},
        {"fieldname": "status", "fieldtype": "Select", "label": "Status"},
        {"fieldname": "paid", "fieldtype": "Check", "label": "Paid"}
    ],

    "breadcrumbs": "[{\"label\":\"Home\",\"route\":\"/\"},{\"label\":\"Portal\",\"route\":\"portal\"}]"
}
PropertyPurpose
login_required: 1Redirect unauthenticated users to login
show_list: 1Display records as a list instead of a single form
apply_document_permissions: 1Respect doctype-level has_permission
list_columnsColumns to show in the list table

Server-Side Context (get_context)

The server hook filters the list to only records the current user should see:

import frappe
from frappe.query_builder import DocType

def get_context(context):
    # 1. Require login
    if frappe.session.user == "Guest":
        frappe.throw("Please log in to view this page.", frappe.PermissionError)

    # 2. Website sidebar for portal navigation
    context.show_sidebar = True
    context.sidebar_items = frappe.get_all(
        "Website Sidebar Item",
        filters={"parent": "Parent Portal"},
        fields=["title", "route", "group"],
        order_by="idx",
    )

    # 3. Filter list to current user's records
    if context.get("is_list"):
        context.list_filters = get_user_filters()


def get_user_filters():
    """Return filters that restrict the record list to the current user's family."""
    Contact = DocType("Contact")
    Guardian = DocType("Family Guardian")
    Player = DocType("Player")

    # Find players linked to the current user
    player_names = (
        frappe.qb.from_(Contact)
        .join(Guardian).on(Guardian.guardian == Contact.name)
        .join(Player).on(Player.family == Guardian.parent)
        .select(Player.name)
        .where(Contact.email_id == frappe.session.user)
        .run(as_list=True)
    )

    flat = [p[0] for p in player_names]

    if flat:
        return {"player": ["in", flat]}
    else:
        # Return impossible filter to show empty list
        return {"player": ["=", "__none__"]}

How List Filtering Works

Frappe applies context.list_filters as additional WHERE clauses on the list query. This works in conjunction with apply_document_permissions: 1 for defense-in-depth:

  1. apply_document_permissions – Frappe checks has_permission() on each record

  2. list_filters – Pre-filters the query so only relevant records are fetched

Use both together. list_filters improves performance by narrowing the query, while apply_document_permissions ensures no unauthorized access slips through.

Website Sidebar

Portal navigation is defined in a Website Sidebar document. Each item links to a portal route:

context.show_sidebar = True
context.sidebar_items = frappe.get_all(
    "Website Sidebar Item",
    filters={"parent": "Parent Portal"},
    fields=["title", "route", "group"],
    order_by="idx",
)

This renders a sidebar with links like:

  • My Players → /portal/players

  • Applications → /portal/applications

  • Payments → /portal/payments

The sidebar is shared across multiple portal views by referencing the same Website Sidebar parent.

List Columns

Define which columns appear in the list table:

"list_columns": [
    {"fieldname": "player_full", "fieldtype": "Data", "label": "Player"},
    {"fieldname": "season", "fieldtype": "Link", "label": "Season", "options": "Season"},
    {"fieldname": "status", "fieldtype": "Select", "label": "Status"},
    {"fieldname": "paid", "fieldtype": "Check", "label": "Paid"}
]

Columns should match fields that are on the underlying doctype. Link fields render as plain text in the list view.

Client-Side Enhancements

Minimal client-side code for list-mode views, but frappe.ready() still runs:

frappe.ready(function () {
    // Customize list page title
    const heading = document.querySelector(".web-form-title h1");
    if (heading) {
        heading.textContent = "My Applications";
    }
});

Nested Portal Routes

Portal views can be nested under a common prefix to group related views:

play/portal/applications    → Player Application list
play/portal/players         → Player list
play/portal/payments        → Payment history

The breadcrumbs configuration maintains navigation context:

"breadcrumbs": "[{\"label\":\"Home\",\"route\":\"/\"},{\"label\":\"Portal\",\"route\":\"portal\"}]"

Integration with Web Forms

A portal view and a web form can target the same doctype:

ConcernConfiguration
Public entry formlogin_required: 0, show_list: 0
Authenticated portallogin_required: 1, show_list: 1, apply_document_permissions: 1

This allows a two-pronged approach: a public form for new submissions, and a portal for existing users to review their records.

Checklist

  • Set login_required: 1 for portal views

  • Set show_list: 1 and apply_document_permissions: 1

  • Implement list_filters in get_context() for per-user filtering

  • Configure list_columns to show relevant fields

  • Create a Website Sidebar document for portal navigation

  • Set context.show_sidebar = True in get_context()

  • Add breadcrumbs for navigation context

  • Test as both the target user and another user to verify filtering

This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.