Reusable app that lets one profile block/unblock another. Blocks are stored as actor → target Profile relations, and per-object blocker / blocking counts are kept on a decoupled BlockableMetadata row (one per DocumentId) so the blocked model never has to carry counter columns. Today blocks are wired for profiles, but the DocumentId-based metadata follows the plugin architecture so it can be extended to any documentable model.
Install the package with pip install baseapp-backend.
If you want to develop, install using this other guide.
The package registers itself as a plugin (see baseapp_blocks.plugin:BlocksPlugin), so most wiring happens through the plugin registry.
- Add
baseapp_blockstoINSTALLED_APPS:
INSTALLED_APPS = [
# ...
"baseapp_blocks",
# ...
]- Wire the auth backend slot —
BlocksPermissionsBackendis contributed via the registry:
AUTHENTICATION_BACKENDS = [
# ...
*plugin_registry.get("AUTHENTICATION_BACKENDS", "baseapp_blocks"),
# ...
]-
Make sure your project's
graphql.pycomposes the schema viaplugin_registry.get_all_graphql_*()soBlocksMutations(blockToggle) is picked up automatically. -
Define the concrete models (see models) and point the swapper settings at them:
BASEAPP_BLOCKS_BLOCK_MODEL = "blocks.Block"
BASEAPP_BLOCKS_BLOCKABLEMETADATA_MODEL = "blocks.BlockableMetadata"Run ./manage.py makemigrations / ./manage.py migrate after defining them.
Both models are abstract + swappable, and the package ships no concrete models or migrations — your project must subclass the abstracts in a local app and point the swapper settings at them.
| Abstract | Concrete reference | Purpose |
|---|---|---|
AbstractBlock |
Block |
A block relation. actor / target are Profile FKs (related_name blocking / blockers); user records who created it. unique_together (actor, target). |
AbstractBlockableMetadata |
BlockableMetadata |
Per-object blockers_count / blocking_count, keyed by a OneToOneField to DocumentId. Decouples counters from the blocked model. |
Both inherit DocumentIdMixin, so any block object can be the target of mentions, comments, follows, etc. without extra wiring.
Block.save() / Block.delete() keep the metadata counts in sync automatically by calling the blockable_metadata shared service whenever a block is created or removed.
Minimal concrete definition:
# myproject/blocks/models.py
from baseapp_blocks.models import AbstractBlock, AbstractBlockableMetadata
class Block(AbstractBlock):
class Meta(AbstractBlock.Meta):
pass
class BlockableMetadata(AbstractBlockableMetadata):
class Meta(AbstractBlockableMetadata.Meta):
pass| Field | Purpose |
|---|---|
blockToggle |
Toggle a block between an actor profile and a target profile. Creates the block if absent (and refuses to block yourself), deletes it if present. Returns the new block edge (or blockDeletedId), plus the refreshed target and actor. |
mutation BlockButtonMutation($input: BlockToggleInput!) {
blockToggle(input: $input) {
block {
node {
id
}
}
blockDeletedId
target {
id
blockersCount
isBlockedByMe
}
actor {
id
blockingCount
}
}
}Blocks publishes one shared interface via the registry — consuming object types opt in by name instead of importing it directly:
from baseapp_core.graphql import Node as RelayNode
from baseapp_core.plugins import graphql_shared_interfaces
class ProfileObjectType(DjangoObjectType):
class Meta:
interfaces = graphql_shared_interfaces.get(RelayNode, "BlocksInterface")
model = ProfileWhen baseapp_blocks is installed the registry returns the real BlocksInterface; when it isn't, the call falls back to defaults so the type still works.
The interface exposes (all permission-gated):
blockers/blocking— paginatedBlockconnections.blockersCount/blockingCount— counts read fromBlockableMetadata. Both ship aquery_optimizerfield-level hook so the counts are annotated on demand and never N+1.isBlockedByMe(profileId)— whether the current user (or the given profile) blocks this object.
BlocksPermissionsBackend resolves the following object-level permissions:
baseapp_blocks.add_block/baseapp_blocks.add_block_with_profile— create a block (the latter checksuse_profileon the acting profile).baseapp_blocks.delete_block— owner of the block, the acting profile, or a moderator.baseapp_blocks.view_block-blockers/-blockers_count/-blocking/-blocking_count— gate the interface fields above.
Blocks registers two services in apps.py.ready():
-
blockable_metadata(BlockableMetadataService) — read/maintain blocker & blocking counts for anyDocumentId-backed object:get_metadata/get_or_create_metadata,get_blockers_count/get_blocking_count,recompute_blockers_count/recompute_blocking_count, andannotate_querysetfor bulk (non-GraphQL) callers. The GraphQL path attaches each count on demand via the interface's optimizer hooks.from baseapp_core.plugins import shared_services if service := shared_services.get("blockable_metadata"): count = service.get_blockers_count(profile)
-
blocks.lookup(BlockLookupService) —exclude_blocked_from_foreign_queryset(queryset, info). Lets other apps (e.g. comments) drop rows authored by profiles the current user blocks or is blocked by. Call it before the query optimizer evaluates the queryset:from baseapp_core.plugins import shared_services if service := shared_services.get("blocks.lookup"): queryset = service.exclude_blocked_from_foreign_queryset(queryset, info)
baseapp_profiles—Block.actor/Block.targetareProfileFKs and theBlocksInterfaceresolvers disambiguate by the current profile.
Older projects inherited a BlockableModel mixin that added blockers_count / blocking_count columns directly on the blocked model (typically Profile). That mixin has been removed. To migrate:
- Add a migration that creates the
BlockableMetadatatable for your project (mirrortestproject/blocks/migrations/0002_blockablemetadata.py). - Add a follow-up migration that calls
migrate_legacy_block_counts_to_metadata(...)frombaseapp_blocks.migration_helpers.convert_legacy_block_counts_to_metadata_helper, passing your model's app label and name. Usereverse_migrate_legacy_block_counts_from_metadata(...)as the reverse code. After the data is moved,RemoveFieldthe legacy columns.
Clone the project inside your project's backend dir:
git clone git@github.com:silverlogic/baseapp-backend.git
And manually install the package:
pip install -e baseapp-backend/baseapp-blocks
The -e flag means any change you make in the cloned repo files will be reflected in the project. Run the test suite from the backend root:
docker compose run --rm web pytest baseapp_blocks/