django-workspaces 0.0.1a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_workspaces/__init__.py +80 -0
- django_workspaces/_compat.py +17 -0
- django_workspaces/apps.py +12 -0
- django_workspaces/middleware.py +39 -0
- django_workspaces/migrations/0001_initial.py +29 -0
- django_workspaces/migrations/__init__.py +0 -0
- django_workspaces/models.py +33 -0
- django_workspaces/py.typed +1 -0
- django_workspaces/signals.py +17 -0
- django_workspaces/types.py +23 -0
- django_workspaces-0.0.1a1.dist-info/METADATA +53 -0
- django_workspaces-0.0.1a1.dist-info/RECORD +14 -0
- django_workspaces-0.0.1a1.dist-info/WHEEL +4 -0
- django_workspaces-0.0.1a1.dist-info/licenses/LICENSE.txt +9 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Django reusable app to manage user workspaces.
|
|
2
|
+
|
|
3
|
+
'Workspace' in this package is a unit of work. Each session always has one
|
|
4
|
+
active workspace. Anything can be added to a workspace.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
+
|
|
9
|
+
import django_stubs_ext
|
|
10
|
+
from django.apps import apps as django_apps
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.http import Http404, HttpRequest
|
|
13
|
+
from django.shortcuts import aget_object_or_404, get_object_or_404
|
|
14
|
+
|
|
15
|
+
from .signals import workspace_requested
|
|
16
|
+
from .types import _Workspace, _WorkspaceModel
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from django.contrib.auth.models import AbstractUser, AnonymousUser
|
|
20
|
+
|
|
21
|
+
django_stubs_ext.monkeypatch()
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"aget_workspace",
|
|
25
|
+
"get_workspace",
|
|
26
|
+
"get_workspace_model",
|
|
27
|
+
"workspace_requested",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
SESSION_KEY = "_workspace_id"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_workspace_model() -> _WorkspaceModel:
|
|
34
|
+
"""Return the workspace model that is active for this project.
|
|
35
|
+
|
|
36
|
+
The workspace model defaults to :class:`django_workspaces.models.Workspace`, and can
|
|
37
|
+
be swapped through the ``WORKSPACE_MODEL`` setting.
|
|
38
|
+
"""
|
|
39
|
+
workspace_model_name: str = getattr(settings, "WORKSPACE_MODEL", "django_workspaces.Workspace")
|
|
40
|
+
return django_apps.get_model(workspace_model_name, require_ready=False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_workspace(request: HttpRequest) -> _Workspace:
|
|
44
|
+
"""Return the workspace model instance associated with the given request."""
|
|
45
|
+
Workspace: _WorkspaceModel = get_workspace_model() # noqa: N806
|
|
46
|
+
user: AbstractUser | AnonymousUser = request.user
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
workspace_id = Workspace._meta.pk.to_python(request.session[SESSION_KEY]) # noqa: SLF001
|
|
50
|
+
except KeyError as exc:
|
|
51
|
+
responses = workspace_requested.send(Workspace, user=user, request=request)
|
|
52
|
+
if not responses:
|
|
53
|
+
msg = "Could not find a workspace"
|
|
54
|
+
raise Http404(msg) from exc
|
|
55
|
+
|
|
56
|
+
_, workspace = cast("tuple[Any, _Workspace]", responses[0])
|
|
57
|
+
else:
|
|
58
|
+
workspace = get_object_or_404(Workspace, pk=workspace_id)
|
|
59
|
+
|
|
60
|
+
return workspace
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def aget_workspace(request: HttpRequest) -> _Workspace:
|
|
64
|
+
"""Async version of :func:`get_workspace`."""
|
|
65
|
+
Workspace: _WorkspaceModel = get_workspace_model() # noqa: N806
|
|
66
|
+
user: AbstractUser | AnonymousUser = await request.auser()
|
|
67
|
+
|
|
68
|
+
session_workspace = await request.session.aget(SESSION_KEY)
|
|
69
|
+
if session_workspace is None:
|
|
70
|
+
responses = await workspace_requested.asend(Workspace, user=user, request=request)
|
|
71
|
+
if not responses:
|
|
72
|
+
msg = "Could not find a workspace"
|
|
73
|
+
raise Http404(msg)
|
|
74
|
+
|
|
75
|
+
_, workspace = cast("tuple[Any, _Workspace]", responses[0])
|
|
76
|
+
else:
|
|
77
|
+
workspace_id = Workspace._meta.pk.to_python(session_workspace) # noqa: SLF001
|
|
78
|
+
workspace = await aget_object_or_404(Workspace, pk=workspace_id)
|
|
79
|
+
|
|
80
|
+
return workspace
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info >= (3, 12):
|
|
4
|
+
from inspect import iscoroutinefunction, markcoroutinefunction
|
|
5
|
+
else:
|
|
6
|
+
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
|
|
7
|
+
|
|
8
|
+
if sys.version_info >= (3, 13):
|
|
9
|
+
from typing import TypeIs
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import TypeIs
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"TypeIs",
|
|
15
|
+
"iscoroutinefunction",
|
|
16
|
+
"markcoroutinefunction",
|
|
17
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Read by Django to configure :mod:`django_workspaces`."""
|
|
2
|
+
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WorkspacesConfig(AppConfig):
|
|
8
|
+
""":mod:`django_workspaces` app configuration."""
|
|
9
|
+
|
|
10
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
11
|
+
name = "django_workspaces"
|
|
12
|
+
verbose_name = _("Workspaces")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Workspace middlewares."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
8
|
+
from django.http import HttpResponseBase
|
|
9
|
+
from django.utils.decorators import sync_and_async_middleware
|
|
10
|
+
from django.utils.functional import SimpleLazyObject
|
|
11
|
+
|
|
12
|
+
from . import aget_workspace, get_workspace
|
|
13
|
+
from ._compat import iscoroutinefunction, markcoroutinefunction
|
|
14
|
+
from .types import HttpRequest, _Workspace
|
|
15
|
+
|
|
16
|
+
_Middleware = Callable[[HttpRequest], HttpResponseBase] | Callable[[HttpRequest], Awaitable[HttpResponseBase]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@sync_and_async_middleware
|
|
20
|
+
def workspace_middleware(get_response: _Middleware, /) -> _Middleware:
|
|
21
|
+
"""Django middleware to add the current workspace to every request.
|
|
22
|
+
|
|
23
|
+
Adds the property `workspace` to use in sync contexts, and the
|
|
24
|
+
`aworkspace` corourine function for async contexts.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def middleware(request: HttpRequest) -> HttpResponseBase:
|
|
28
|
+
if not hasattr(request, "user"):
|
|
29
|
+
msg: str = "The workspace middleware requires Django's authentication middleware"
|
|
30
|
+
raise ImproperlyConfigured(msg)
|
|
31
|
+
|
|
32
|
+
request.workspace = cast("_Workspace", SimpleLazyObject(partial(get_workspace, request)))
|
|
33
|
+
request.aworkspace = partial(aget_workspace, request)
|
|
34
|
+
return get_response(request) # type: ignore[return-value]
|
|
35
|
+
|
|
36
|
+
if iscoroutinefunction(get_response):
|
|
37
|
+
middleware = markcoroutinefunction(middleware)
|
|
38
|
+
|
|
39
|
+
return middleware
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
initial = True
|
|
6
|
+
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
operations = [
|
|
10
|
+
migrations.CreateModel(
|
|
11
|
+
name="Workspace",
|
|
12
|
+
fields=[
|
|
13
|
+
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
|
14
|
+
(
|
|
15
|
+
"name",
|
|
16
|
+
models.CharField(
|
|
17
|
+
db_comment="Workspace name",
|
|
18
|
+
help_text="Required. 255 characters or fewer.",
|
|
19
|
+
max_length=255,
|
|
20
|
+
verbose_name="name",
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
options={
|
|
25
|
+
"abstract": False,
|
|
26
|
+
"swappable": "WORKSPACE_MODEL",
|
|
27
|
+
},
|
|
28
|
+
),
|
|
29
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Workspace models."""
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
from django_stubs_ext.db.models import TypedModelMeta
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AbstractWorkspace(models.Model):
|
|
9
|
+
"""Abstract base class implementing a workspace.
|
|
10
|
+
|
|
11
|
+
Custom workspace models should inherit from this class.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
name = models.CharField(
|
|
15
|
+
_("name"),
|
|
16
|
+
max_length=255,
|
|
17
|
+
help_text=_("Required. 255 characters or fewer."),
|
|
18
|
+
db_comment="Workspace name",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class Meta(TypedModelMeta):
|
|
22
|
+
abstract = True
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
"""Return a string representation of the workspace."""
|
|
26
|
+
return self.name
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Workspace(AbstractWorkspace):
|
|
30
|
+
"""Default workspace model."""
|
|
31
|
+
|
|
32
|
+
class Meta(AbstractWorkspace.Meta):
|
|
33
|
+
swappable = "WORKSPACE_MODEL"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PEP-561 marker file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Dispatched signals."""
|
|
2
|
+
|
|
3
|
+
from django.dispatch import Signal
|
|
4
|
+
|
|
5
|
+
workspace_requested = Signal()
|
|
6
|
+
"""Dispatched when a workspace is not found in the current session.
|
|
7
|
+
|
|
8
|
+
This should look up the user's preferences to get the latest or default workspace.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
sender: The current workspace model.
|
|
12
|
+
user: The user requesting a workspace.
|
|
13
|
+
request: The current request. Optional.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
A workspace instance if could find a default workspace. None otherwise.
|
|
17
|
+
"""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Type helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
5
|
+
|
|
6
|
+
from django.http import HttpRequest as DjangoHttpRequest
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .models import AbstractWorkspace
|
|
10
|
+
|
|
11
|
+
_Workspace: TypeAlias = "AbstractWorkspace"
|
|
12
|
+
"""Placeholder type for the current workspace.
|
|
13
|
+
|
|
14
|
+
The mypy plugin will refine it someday."""
|
|
15
|
+
|
|
16
|
+
_WorkspaceModel: TypeAlias = type[_Workspace] # noqa: PYI047
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HttpRequest(DjangoHttpRequest):
|
|
20
|
+
"""HTTP request with workspace."""
|
|
21
|
+
|
|
22
|
+
workspace: _Workspace
|
|
23
|
+
aworkspace: Callable[[], Awaitable[_Workspace]]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-workspaces
|
|
3
|
+
Version: 0.0.1a1
|
|
4
|
+
Summary: Django reusable app to manage user workspaces
|
|
5
|
+
Project-URL: Documentation, https://github.com/hartungstenio/django-workspaces#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/hartungstenio/django-workspaces/issues
|
|
7
|
+
Project-URL: Source, https://github.com/hartungstenio/django-workspaces
|
|
8
|
+
Author-email: Christian Hartung <hartung@live.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 5.2
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
24
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: django
|
|
29
|
+
Requires-Dist: django-stubs-ext
|
|
30
|
+
Requires-Dist: typing-extensions; python_version < '3.13'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# django-workspaces
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/django-workspaces)
|
|
36
|
+
[](https://pypi.org/project/django-workspaces)
|
|
37
|
+
|
|
38
|
+
-----
|
|
39
|
+
|
|
40
|
+
## Table of Contents
|
|
41
|
+
|
|
42
|
+
- [Installation](#installation)
|
|
43
|
+
- [License](#license)
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```console
|
|
48
|
+
pip install django-workspaces
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
`django-workspaces` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
django_workspaces/__init__.py,sha256=hp5hUNZij9nR2iukGqbZAyxWMZxG2w3LCRxyfvNPG9o,2806
|
|
2
|
+
django_workspaces/_compat.py,sha256=vLgdGvaSApBvHJZ3JK7S-dRk1v_TcFf81zyxHzE6NIE,384
|
|
3
|
+
django_workspaces/apps.py,sha256=z6wVAXJjYHEg2UpyfDZY-YciGc4T46VxcBxHKXyRNkQ,365
|
|
4
|
+
django_workspaces/middleware.py,sha256=-RSe99WOUk2fs32znJ9fuUdFGY-ZZFdzNWreub3WwHg,1504
|
|
5
|
+
django_workspaces/models.py,sha256=EALgPW4gGw6CMMKqzagbzWLPO4I06-u6MLXa6BL_ymM,829
|
|
6
|
+
django_workspaces/py.typed,sha256=Qn1PtAFCQCHxVY6FV1vsRH98Re39dTxwl3lQRksOGpY,20
|
|
7
|
+
django_workspaces/signals.py,sha256=aspua_A38H-QSym7qiZ5SOE7jB50SY3S3m2N4Tzxj5w,471
|
|
8
|
+
django_workspaces/types.py,sha256=JawPN4lIPKDERS4pm83z44sy7ea2MmOUFJbjAwQQ4YA,589
|
|
9
|
+
django_workspaces/migrations/0001_initial.py,sha256=FKwAb9uyrYVhBi6PznmG9skoXLV2bwO0SKw-mUUGfVA,833
|
|
10
|
+
django_workspaces/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
django_workspaces-0.0.1a1.dist-info/METADATA,sha256=A--DX4jj1QKAc6ssZXfYW0fRHVbUXNLG8QXUkE_6DD8,1930
|
|
12
|
+
django_workspaces-0.0.1a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
13
|
+
django_workspaces-0.0.1a1.dist-info/licenses/LICENSE.txt,sha256=kw90zQM3KTw6c7cenDnJY8bGSv6MM46DpIXHiU9g_h8,1101
|
|
14
|
+
django_workspaces-0.0.1a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Christian Hartung <hartung@live.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|