Reusable app for profiles, their URL paths, members/roles, and metadata. A Profile is the public-facing identity (name, avatar, banner, biography, URL handle) attached to any ProfilableModel (User, Organization, …), and it is the actor/target other social features (follows, blocks, reports, comments, chats) hang off of.
baseapp_profiles follows the plugin architecture: it registers itself as a plugin for settings aggregation and wires its middleware, permissions backend, GraphQL roots, shared services and shared GraphQL interfaces through the registry — no direct cross-package imports.
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_profiles.plugin:ProfilesPlugin), so most wiring goes through plugin_registry.
- Add
baseapp_profilestoINSTALLED_APPS:
INSTALLED_APPS = [
# ...
"baseapp_profiles",
# ...
]- Wire the middleware slot —
CurrentProfileMiddlewaremust come afterdjango.contrib.auth.middleware.AuthenticationMiddleware:
MIDDLEWARE = [
# ... AuthenticationMiddleware above ...
*plugin_registry.get("MIDDLEWARE", "baseapp_profiles"),
# ...
]- Wire the auth backend slot —
ProfilesPermissionsBackendis contributed via the registry:
AUTHENTICATION_BACKENDS = [
# ...
*plugin_registry.get("AUTHENTICATION_BACKENDS", "baseapp_profiles"),
# ...
]-
Make sure your project's
graphql.pycomposes the schema viaplugin_registry.get_all_graphql_*()soProfilesQueriesandProfilesMutationsare picked up automatically. The GraphQL middleware slot (GRAPHENE__MIDDLEWARE, also keyedbaseapp_profiles) carries the GraphQL-sideCurrentProfileMiddleware. -
Define the concrete models (see models) and point the swapper settings at them:
BASEAPP_PROFILES_PROFILE_MODEL = "profiles.Profile"
BASEAPP_PROFILES_PROFILEUSERROLE_MODEL = "profiles.ProfileUserRole"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 (see How to develop).
| Abstract | Concrete reference | Purpose |
|---|---|---|
AbstractProfile |
Profile |
Public identity: name (trigger-maintained), image, banner_image, biography, status (public/private), owner FK, and a generic target. Inherits DocumentIdMixin, so any profile can be the target of follows/blocks/reports/comments/chats. |
AbstractProfileUserRole |
ProfileUserRole |
Membership join row (profile.members): a user's role (manager/admin/…) and status (pending/active/…) on a profile. |
Profile also inherits PageMixin (when baseapp_pages is installed) so it owns URL paths.
ProfilableModel is an abstract model mixin that automatically keeps a Profile in sync with your model via PostgreSQL triggers — no Python signals required.
- On INSERT: if
profile_owner_sqlis defined, a trigger creates aProfilerow and links it back to your model via theprofileFK (usesON CONFLICT … DO UPDATEso it is safe to re-run). - On UPDATE: a trigger updates
profile.namewhenever the columns referenced inprofile_name_sqlchange.
Both triggers are registered automatically at class-definition time — no manual wiring needed.
Inherit from ProfilableModel and define at least profile_name_sql:
from baseapp_profiles.models import ProfilableModel
class MyModel(ProfilableModel):
# Required: SQL expression using NEW.<col> references that produces the profile name.
# The framework automatically wraps the whole expression with TRIM(COALESCE(..., ''))
# so a fully-NULL result never reaches profile.name.
profile_name_sql = "NEW.first_name || ' ' || NEW.last_name"
# Optional: SQL expression for the profile owner_id on INSERT.
# When provided, the trigger creates and links a Profile automatically.
# Omit when ownership is determined later by application logic (e.g. Organization).
profile_owner_sql = "NEW.id"If any of the referenced columns are nullable, wrap them individually with COALESCE to prevent a single NULL from making the entire expression NULL (PostgreSQL: NULL || anything = NULL):
# first_name and last_name are nullable on this model
profile_name_sql = "COALESCE(NEW.first_name, '') || ' ' || COALESCE(NEW.last_name, '')"After adding or changing these attributes, run makemigrations to capture the updated trigger SQL:
./manage.py makemigrationsTwo post-save signal handlers are available for features that still require Python-level logic:
-
create_profile_url_path— callsprofile.create_url_path()after creation whenbaseapp_pagesis installed. Connect it if your model needs URL paths:from django.db.models.signals import post_save from baseapp_profiles.signals import create_profile_url_path post_save.connect(create_profile_url_path, sender=MyModel)
-
update_user_profile(deprecated) — fallback for projects that have not yet addedprofile_owner_sql. Creates the profile via Python if the trigger did not run. New projects should useprofile_owner_sqlinstead.
When baseapp_pages is installed, each profile owns a URL handle (e.g. /JonathanDoe). Handle generation lives on the model; uniqueness/collision resolution lives in the pages.url_path shared service.
profile.generate_url_path(profile_name=None)— builds the handle (with leading slash) fromprofile_nameorself.name. The name is folded to a URL-safe ASCII handle viabaseapp_profiles.utils.to_ascii_handle(accents transliterated —Döe→Doe; emoji and other non-alphanumerics dropped). When the name folds to nothing (e.g. an emoji-only name), it falls back to the local-part of the owner's email; as a last resortpad_handlepads with random digits to an 8-char minimum. The result is not collision-checked.profile.create_url_path(profile_name=None)— builds the handle, then asks thepages.url_pathservice to resolve uniqueness (appending a numeric suffix when taken) before persisting theURLPath.Profile.generate_url_path_str(profile_name)(classmethod) — collision-resolved suggestion for an arbitrary string (no owner context, so no email fallback). Used byProfileUpdateSerializer.validate_url_pathto suggest a free handle when the requested one is taken.
Profile.save() calls create_url_path() on creation, and the create_profile_url_path signal does the same for ProfilableModel subclasses.
| Field | Description |
|---|---|
profile(id) |
RelayNode fetch of a single Profile. get_node enforces view_profile. |
allProfiles |
Filterable connection of profiles (ProfileFilter). Non-superusers only see PUBLIC profiles plus their own. |
| Field | Purpose |
|---|---|
profileCreate |
Create a profile (optionally targeting another object via target). |
profileUpdate |
Edit name, images, biography, phone, and URL handle (url_path). |
profileDelete |
Delete a profile. |
profileRoleUpdate |
Add/update a member's role/status on a profile. |
profileRemoveMember |
Remove a member from a profile. |
Profiles publishes two interfaces via the registry — consume them by name (no direct import):
ProfileInterface— exposes a singleprofilefield for objects that have an associated profile.ProfilesInterface— exposes aprofilesconnection for objects that have many.
from baseapp_core.graphql import Node as RelayNode
from baseapp_core.plugins import graphql_shared_interfaces
class MyObjectType(DjangoObjectType):
class Meta:
interfaces = graphql_shared_interfaces.get(RelayNode, "ProfileInterface")
model = MyModelProfileObjectType itself consumes interfaces published by other packages, resolved by name so they degrade gracefully when the package is absent: ProfileActivityLogInterface, PageInterface, FollowsInterface, ReportsInterface, BlocksInterface, ChatRoomsInterface (plus PermissionsInterface, which comes from the always-present baseapp_core).
To avoid N+1 on those interfaces' count fields, BaseProfileObjectType.pre_optimization_hook annotates the queryset through each metadata service it finds (commentable_metadata, followable_metadata, reportable_metadata). A project subclassing BaseProfileObjectType must preserve this hook.
-
profiles.graphql(GraphQLProfileService) — lets other packages obtain the (possibly swapped) Profile ObjectType / connection edge and create a profile from a mutation without importingbaseapp_profilesdirectly:from baseapp_core.plugins import shared_services if service := shared_services.get("profiles.graphql"): ObjectType = service.get_profile_object_type() edge = service.create_profile_from_mutation(info, target_instance, data)
-
profiles.jwt_profile(shared serializer,JWTProfileSerializer) — registered viaregister_shared_serializers; consumed bybaseapp_authto embed the current profile in the JWT/user payload.
All optional — behaviour degrades gracefully when the providing package is absent:
pages.url_path— URL handle creation / collision resolution (see URL paths).commentable_metadata/followable_metadata/reportable_metadata— queryset annotations for the corresponding interface count fields.
baseapp_blocks, baseapp_follows, baseapp_reports, baseapp_comments, baseapp_pages, baseapp.activity_log, baseapp_chats — each enriches ProfileObjectType when installed.
CurrentProfileMiddleware reads the Current-Profile request header (a relay id); when present and the user may use that profile, it sets request.user.current_profile, otherwise it defaults to request.user.profile. Many resolvers and mutations (e.g. blocks/follows actor disambiguation) rely on current_profile.
Subclass ProfilesPermissionsBackend to customize permissions, then replace the plugin entry in AUTHENTICATION_BACKENDS with your subclass:
from baseapp_profiles.permissions import ProfilesPermissionsBackend
class MyProfilesPermissionsBackend(ProfilesPermissionsBackend):
def has_perm(self, user_obj, perm, obj=None):
return super().has_perm(user_obj, perm, obj)AUTHENTICATION_BACKENDS = [
# ...
# *plugin_registry.get("AUTHENTICATION_BACKENDS", "baseapp_profiles"),
"myapp.permissions.MyProfilesPermissionsBackend",
# ...
]General development instructions can be found in the main README.
Because the models are abstract + swappable with no concrete models shipped, create a local app (we suggest apps/social/profiles/) implementing the concrete models:
from baseapp_profiles.models import AbstractProfile, AbstractProfileUserRole
class Profile(AbstractProfile):
class Meta(AbstractProfile.Meta):
pass
class ProfileUserRole(AbstractProfileUserRole):
class Meta(AbstractProfileUserRole.Meta):
passThen point swapper at them:
BASEAPP_PROFILES_PROFILE_MODEL = "profiles.Profile"
BASEAPP_PROFILES_PROFILEUSERROLE_MODEL = "profiles.ProfileUserRole"If your project had profiles before adopting baseapp_profiles, follow MIGRATION.md for extending your model from ProfilableModel and backfilling profiles from the legacy data.