Django reusable app to manage user workspaces
Project description
django-workspaces
Multi-workspace support for Django. Allows users to switch between isolated workspaces within the same application, with full sync and async support.
Table of Contents
- Installation
- Quick Start
- Configuration
- Usage
- Signals
- Custom workspace model
- Django Channels
- Object-level permissions
- Header-based resolution
- Type hints
- License
Installation
pip install django-workspaces
Quick Start
1. Add to INSTALLED_APPS and configure the middleware:
# settings.py
INSTALLED_APPS = [
...
"django_workspaces",
]
MIDDLEWARE = [
...
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_workspaces.middleware.workspace_middleware", # after auth
]
2. Run migrations:
python manage.py migrate
3. Create workspaces and assign them to users, then use request.workspace in your views.
Configuration
All settings are optional and go in settings.py:
| Setting | Default | Description |
|---|---|---|
WORKSPACE_MODEL |
"django_workspaces.Workspace" |
Swappable workspace model — see Custom workspace model |
WORKSPACE_ID_HEADER |
None |
HTTP header used to resolve the workspace by ID — see Header-based resolution |
WORKSPACE_CHECK_OBJECT_PERMISSIONS |
False |
Enforce view_<model> object-level permission when entering a workspace — see Object-level permissions |
Usage
Accessing the workspace in views
After adding the middleware, every request has a workspace property:
# views.py
def dashboard(request):
workspace = request.workspace # raises Http404 if none found
return render(request, "dashboard.html", {"workspace": workspace})
For async views:
async def dashboard(request):
workspace = await request.aworkspace()
return render(request, "dashboard.html", {"workspace": workspace})
Entering and leaving workspaces
Use enter_workspace to set the active workspace for a user and leave_workspace to unset it.
from django_workspaces import enter_workspace, leave_workspace, switch_workspace
# Enter a workspace — accepts a request, an ASGI scope, or (user, workspace, session)
def select_workspace(request, workspace_id):
workspace = get_object_or_404(Workspace, pk=workspace_id)
enter_workspace(request, workspace=workspace)
return redirect("dashboard")
# Leave the current workspace
def deselect_workspace(request):
leave_workspace(request)
return redirect("home")
# Switch directly from one workspace to another
def switch(request, workspace_id):
workspace = get_object_or_404(Workspace, pk=workspace_id)
switch_workspace(request, workspace=workspace)
return redirect("dashboard")
Async equivalents are available as aenter_workspace, aleave_workspace, and aswitch_workspace:
from django_workspaces import aenter_workspace, aleave_workspace
async def select_workspace(request, workspace_id):
workspace = await Workspace.objects.aget(pk=workspace_id)
await aenter_workspace(request, workspace=workspace)
return redirect("dashboard")
All three functions also accept (user, workspace, session) directly, which is useful outside of a request/response cycle:
enter_workspace(request.user, workspace=workspace, session=request.session)
Resolving a workspace manually
get_workspace resolves the active workspace from a request without the middleware:
from django_workspaces import get_workspace
def my_view(request):
workspace = get_workspace(request)
...
resolve_workspace accepts a user and session directly:
from django_workspaces import resolve_workspace
workspace = resolve_workspace(user, session)
Signals
django_workspaces exposes three signals:
| Signal | Sent when | Key arguments |
|---|---|---|
workspace_requested |
No workspace in session; a default is being looked up | user, request (optional) |
workspace_entered |
User enters a workspace | user, workspace |
workspace_exited |
User leaves a workspace | user, workspace |
Setting a default workspace
Connect to workspace_requested to automatically assign a workspace when none is set in the session. The signal expects the handler to return a workspace instance (or None):
# apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
from django_workspaces.signals import workspace_requested
workspace_requested.connect(get_default_workspace)
def get_default_workspace(sender, user, **kwargs):
"""Return the first workspace the user has access to."""
return sender.objects.filter(members=user).first()
A common use case is persisting the last workspace a user visited, so it can be restored on their next session. Connect workspace_entered to save the preference and workspace_requested to restore it:
# myapp/models.py
class WorkspacePreference(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="workspace_preference",
)
last_workspace = models.ForeignKey(
settings.WORKSPACE_MODEL,
on_delete=models.SET_NULL,
null=True,
)
# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
from django_workspaces.signals import workspace_entered, workspace_requested
workspace_entered.connect(save_last_workspace)
workspace_requested.connect(restore_last_workspace)
def save_last_workspace(sender, user, workspace, **kwargs):
"""Persist the workspace the user just entered."""
WorkspacePreference.objects.update_or_create(
user=user,
defaults={"last_workspace": workspace},
)
def restore_last_workspace(sender, user, **kwargs):
"""Return the last workspace the user visited, if any."""
pref = WorkspacePreference.objects.filter(user=user).select_related("last_workspace").first()
return pref.last_workspace if pref else None
With this setup, the first time a user makes a request without a workspace in their session, workspace_requested fires and restore_last_workspace returns their previous workspace automatically.
Custom workspace model
To add fields to the workspace, define a custom model and point WORKSPACE_MODEL to it — similar to AUTH_USER_MODEL.
# myapp/models.py
from django_workspaces.models import AbstractWorkspace
class Project(AbstractWorkspace):
slug = models.SlugField(unique=True)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="projects",
)
class Meta(AbstractWorkspace.Meta):
pass
# settings.py
WORKSPACE_MODEL = "myapp.Project"
Then run python manage.py makemigrations and python manage.py migrate.
Note:
WORKSPACE_MODELmust be set before the first migration is run, just likeAUTH_USER_MODEL.
To retrieve the active workspace model at runtime:
from django_workspaces import get_workspace_model
Workspace = get_workspace_model()
Django Channels
For WebSocket or other ASGI consumers, install the Channels extra:
pip install django-workspaces[channels]
Use WorkspaceMiddlewareStack in your ASGI routing:
# asgi.py
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from django_workspaces.contrib.channels.middleware import WorkspaceMiddlewareStack
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": WorkspaceMiddlewareStack(
URLRouter(websocket_urlpatterns)
),
})
The workspace is then available on the scope:
class MyConsumer(AsyncWebsocketConsumer):
async def connect(self):
workspace = self.scope["workspace"]
...
WorkspaceMiddlewareStack is a shortcut for AuthMiddlewareStack(WorkspaceMiddleware(inner)). If you need finer control, compose the middleware manually:
from channels.auth import AuthMiddlewareStack
from channels.sessions import SessionMiddlewareStack
from django_workspaces.contrib.channels.middleware import WorkspaceMiddleware
application = ProtocolTypeRouter({
"websocket": SessionMiddlewareStack(
AuthMiddlewareStack(
WorkspaceMiddleware(
URLRouter(websocket_urlpatterns)
)
)
),
})
Object-level permissions
Enable WORKSPACE_CHECK_OBJECT_PERMISSIONS to require users to have the view_<model> object permission before entering a workspace:
# settings.py
WORKSPACE_CHECK_OBJECT_PERMISSIONS = True
With this enabled, enter_workspace (and its async variant) raises PermissionDenied if the user does not have the view_workspace permission on the target workspace. The permission codename follows Django's convention: {app_label}.view_{model_name}.
This works with any Django-compatible permission backend, including django-guardian for row-level permissions:
from guardian.shortcuts import assign_perm
# Grant a user access to a specific workspace
assign_perm("view_workspace", user, workspace)
Header-based resolution
For API scenarios where the client specifies the workspace per request, configure WORKSPACE_ID_HEADER:
# settings.py
WORKSPACE_ID_HEADER = "x-workspace-id"
When set, get_workspace (and request.workspace) will look for this header first and resolve the workspace by its primary key. Session-based resolution is used as a fallback.
GET /api/data/ HTTP/1.1
X-Workspace-Id: 42
Note: Header-based resolution is a read-only lookup — it does not call
enter_workspaceinternally. As a consequence, theworkspace_enteredandworkspace_exitedsignals are not fired for requests that resolve the workspace through the header. If your application relies on those signals (e.g. to track the last visited workspace), prefer session-based resolution or callenter_workspaceexplicitly in your authentication flow.
Type hints
When using the middleware, import the enhanced request type for accurate type checking:
from django_workspaces.types import HttpRequest
def my_view(request: HttpRequest):
workspace = request.workspace # typed as AbstractWorkspace
License
django-workspaces is distributed under the terms of the MIT license.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_workspaces-0.2.2.tar.gz.
File metadata
- Download URL: django_workspaces-0.2.2.tar.gz
- Upload date:
- Size: 19.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a50612dfabbadf248b34abd9bbe8e33916deb1e2524976184580d1c4ef537755
|
|
| MD5 |
28fbe1bff695aa4f16a2a65f84103028
|
|
| BLAKE2b-256 |
4f8fdfc4a80ea5a5308dcb5cca9d9c4e96027634d373cc6a0a8c786caedf4646
|
Provenance
The following attestation bundles were made for django_workspaces-0.2.2.tar.gz:
Publisher:
release.yml on hartungstenio/django-workspaces
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_workspaces-0.2.2.tar.gz -
Subject digest:
a50612dfabbadf248b34abd9bbe8e33916deb1e2524976184580d1c4ef537755 - Sigstore transparency entry: 1174671923
- Sigstore integration time:
-
Permalink:
hartungstenio/django-workspaces@bb586a7303e40ce8743f29d3c9b1a40901d01ebc -
Branch / Tag:
refs/tags/0.2.2 - Owner: https://github.com/hartungstenio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bb586a7303e40ce8743f29d3c9b1a40901d01ebc -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_workspaces-0.2.2-py3-none-any.whl.
File metadata
- Download URL: django_workspaces-0.2.2-py3-none-any.whl
- Upload date:
- Size: 13.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
984b05bc01cdeef45cfd8089ac5b341e34eea0b5ccc366b6c017ed5cd007e103
|
|
| MD5 |
4a13b58ead8a5838156badc6354b8f0d
|
|
| BLAKE2b-256 |
a020819d88bca9e429fedf9c751e2cc16c95bd58a196327aa4d0653f4a1dfa4b
|
Provenance
The following attestation bundles were made for django_workspaces-0.2.2-py3-none-any.whl:
Publisher:
release.yml on hartungstenio/django-workspaces
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_workspaces-0.2.2-py3-none-any.whl -
Subject digest:
984b05bc01cdeef45cfd8089ac5b341e34eea0b5ccc366b6c017ed5cd007e103 - Sigstore transparency entry: 1174671999
- Sigstore integration time:
-
Permalink:
hartungstenio/django-workspaces@bb586a7303e40ce8743f29d3c9b1a40901d01ebc -
Branch / Tag:
refs/tags/0.2.2 - Owner: https://github.com/hartungstenio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bb586a7303e40ce8743f29d3c9b1a40901d01ebc -
Trigger Event:
release
-
Statement type: