Reusable app that lets users report any model, customisable for the project's needs.
pip install baseapp-backendAdd baseapp_reports to INSTALLED_APPS. The package registers itself as a plugin
(see baseapp_reports.plugin:ReportsPlugin), so:
ReportsQueries/ReportsMutationsare contributed viaplugin_registry.get_all_graphql_queries()/get_all_graphql_mutations().ReportsPermissionsBackendis contributed viaplugin_registry.get("AUTHENTICATION_BACKENDS", "baseapp_reports").- The
ReportsInterfaceGraphQL interface is registered by name; consumers fetch it fromgraphql_shared_interfacesinstead of importing it directly. - A
reportable_metadatashared service exposesget_reports_count(obj)andannotate_queryset(qs)for any object with aDocumentId. Consumers should use this service instead of inheriting from a mixin.
Request the interface by name in your object type's Meta.interfaces:
from baseapp_core.graphql import Node as RelayNode
from baseapp_core.plugins import graphql_shared_interfaces
class UserNode(DjangoObjectType):
class Meta:
interfaces = graphql_shared_interfaces.get(RelayNode, "ReportsInterface")When baseapp_reports is installed the registry returns the real
ReportsInterface; when it is not, the call falls back to the defaults so the
type still works.
Nothing to do. The plugin entry-point in pyproject.toml registers
ReportsQueries and ReportsMutations, and the project's root Query /
Mutation should already spread *plugin_registry.get_all_graphql_queries()
and *plugin_registry.get_all_graphql_mutations(). reportCreate and
allReportTypes show up automatically.
Reports counts are stored in ReportableMetadata (one row per DocumentId),
not on the reportable model itself. Read counts via the shared service:
from baseapp_core.plugins import shared_services
service = shared_services.get("reportable_metadata")
reports_count = service.get_reports_count(my_obj) # dict like {"total": 0, "spam": 0, ...}For querysets that will resolve reportsCount for many rows, annotate up front to
avoid N+1:
qs = service.annotate_queryset(qs)The Report.save() / Report.delete() hooks already maintain
ReportableMetadata.reports_count for you whenever a report is added or
removed.
Define a concrete model that subclasses the abstracts in your project:
# myproject/reports/models.py
from baseapp_reports.models import (
AbstractReport,
AbstractReportType,
AbstractReportableMetadata,
)
class ReportType(AbstractReportType):
class Meta(AbstractReportType.Meta):
pass
class Report(AbstractReport):
class Meta(AbstractReport.Meta):
pass
class ReportableMetadata(AbstractReportableMetadata):
class Meta(AbstractReportableMetadata.Meta):
passAdd the new app to INSTALLED_APPS, run makemigrations / migrate, and point
the swapper settings at it:
# settings.py
BASEAPP_REPORTS_REPORT_MODEL = "reports.Report"
BASEAPP_REPORTS_REPORTTYPE_MODEL = "reports.ReportType"
BASEAPP_REPORTS_REPORTABLEMETADATA_MODEL = "reports.ReportableMetadata"AbstractReportFactory helps you build factories for the swapped Report model:
import factory
from baseapp_reports.tests.factories import AbstractReportFactory
class CommentFactory(factory.django.DjangoModelFactory):
class Meta:
model = "comments.Comment"
class CommentReportFactory(AbstractReportFactory):
target = factory.SubFactory(CommentFactory)
class Meta:
model = "reports.Report" # or "baseapp_reports.Report" if you didn't swapOlder projects inherited a ReportableModel mixin that added a reports_count
JSONField directly on the reportable model. That mixin has been removed. To
migrate:
-
Add a migration that creates the
ReportableMetadatatable for your project (mirrorbaseapp_reports/migrations/0007_reportablemetadata.py). -
Add a follow-up migration that calls
migrate_legacy_reports_count_to_metadata(...)frombaseapp_reports.migration_helpers.convert_legacy_reports_count_to_metadata_helper, passing your reportable model's app label and name. After the data is moved,RemoveFieldthe legacyreports_countcolumn.If that app must also run with
baseapp_reportsuninstalled, keep the migration graph optional: make the("reports", "…reportablemetadata")dependency conditional (only add it when"reports" in apps.app_configs) and guard the backfill (apps.get_model("reports", "ReportableMetadata")inside atry/except LookupErrorthat returns early). TheRemoveField/ trigger-regen operations touch your own table, so they stay unconditional. -
Optionally re-seed any drift with
seed_reportable_metadata_from_reports(...)frombaseapp_reports.migration_helpers.seed_reportable_metadata_from_reports_helper.
Clone the monorepo into your backend directory:
git clone git@github.com:silverlogic/baseapp-backend.gitThen install editable:
pip install -e baseapp-backend