Reports and permissions

Explicit registration

Import django_qbe2.registry.Report and django_qbe2.registry.register() in your installed app’s qbe.py. Declare a Django model, a nonempty tuple of permitted field paths and a mandatory get_queryset(request) method. Optional field_labels map only declared paths to display text. A model may be registered once; duplicates raise ImproperlyConfigured during startup.

Do not read database rows while importing the module. Report instances are shared configuration: do not store a request, a user, evaluated rows or request-specific querysets on them. The permission catalog is rebuilt for each request.

Three authorization boundaries

  1. Entry requires an authenticated, active staff user.

  2. A root report requires Django’s <app_label>.view_<model_name> permission. A related field also requires the target model’s view permission.

  3. Each root and target report supplies its own authorized rows through get_queryset(request). Django model permissions do not imply row access.

Staff status alone does not grant report access. The package does not infer ownership or tenant rules, and it does not automatically reuse a ModelAdmin’s row policy. Call an existing shared policy if your application already defines one. Superusers bypass model permission checks through Django’s normal behavior but still follow the report’s row policy. An explicit all-rows policy is your choice.

For the example’s owner-based model, an explicit policy can grant superusers all rows and scope other staff to their own rows:

def get_queryset(self, request):
    queryset = self.model.objects.all()
    if request.user.is_superuser:
        return queryset
    return queryset.filter(owner=request.user)

Choose this policy only when your application authorizes that access. owner is the example model’s field; QBE does not require that name or infer ownership.

The Install in your project example uses owner equality. In a tenant application, return the tenant-scoped queryset your authenticated request is authorized to read. Never accept a client-provided tenant ID as authorization by itself.

QuerySet contract

Return an unevaluated QuerySet of the declared model on the default database. Do not return a list, a slice, or a values()/values_list() projection. The engine applies authorized conditions, ordering, projection and report limits later. An invalid policy return raises a configuration error.

Relationship paths

Paths may traverse up to two registered ForeignKey, OneToOne or ManyToMany relationships in either direction, for example customer__name or customer__pets__name. Generic relations are unsupported. A field path may contain at most one multi-valued edge. Forward ForeignKey to_field uses the referenced target key.

Each report along the path must be visible to the user and its get_queryset(request) policy scopes the related objects. This applies to HTML, CSV/Excel exports, saved queries and shared queries. Multi-valued paths can be selected and filtered, but cannot be used for row ordering, summary grouping or numeric summary metrics. See Related model paths for collection formatting, existential filter behavior and the related-value limit.

Example: subscriptions, users and pets

Keep Subscription as the root when you want one report row per subscription. Declare each related path in that root report’s fields tuple. For example, user__email follows a forward ForeignKey or OneToOneField from Subscription to User. If Pet.user declares related_name="pets", user__pets__name follows the reverse relation and collects pet names into one cell per subscription. Use the relation’s related_query_name in a report path. If neither related_name nor related_query_name is set, Django defaults to the related model name, commonly user__pet__name; the Python instance accessor (commonly pet_set) is not the ORM lookup name.

Register the root and both related models. Every report’s row policy must match the access your application intends to grant; the example gives superusers all rows and scopes other staff to their own data:

from django.contrib.auth import get_user_model
from django_qbe2.registry import Report, register

from .models import Pet, Subscription

User = get_user_model()


@register
class UserReport(Report):
    model = User
    fields = ("email",)

    def get_queryset(self, request):
        queryset = User.objects.all()
        if request.user.is_superuser:
            return queryset
        return queryset.filter(pk=request.user.pk)


@register
class PetReport(Report):
    model = Pet
    fields = ("name",)

    def get_queryset(self, request):
        queryset = Pet.objects.all()
        if request.user.is_superuser:
            return queryset
        return queryset.filter(user=request.user)


@register
class SubscriptionReport(Report):
    model = Subscription
    fields = ("plan", "status", "user__email", "user__pets__name")

    def get_queryset(self, request):
        queryset = Subscription.objects.all()
        if request.user.is_superuser:
            return queryset
        return queryset.filter(user=request.user)

Adapt model and relation names, such as Pet.user and the reverse query name pets, to your schema. The paths must be declared on SubscriptionReport; registering UserReport and PetReport authorizes and scopes each related model, but does not automatically add its fields to the subscription report. Then select Subscriptions in the builder and choose the related columns.

This example uses two relationship hops for user__pets__name and one multi-valued edge for the user’s pets, within the supported limits. It does not change the root row policy: get_queryset() on SubscriptionReport still determines which subscriptions the user may report on.