Introduce workspace tenancy boundaries

This commit is contained in:
rucki
2026-09-09 17:46:10 +02:00
parent 7f203a7117
commit 291c8db471
11 changed files with 565 additions and 118 deletions
+46 -11
View File
@@ -3,7 +3,7 @@ from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from .models import ApiKey, Attachment, Collection, Item, Kanban, Tag, Team, TeamInvite, UserInvite, UserPreference
from .models import ApiKey, Attachment, Collection, Item, Kanban, Tag, Team, TeamInvite, UserInvite, UserPreference, Workspace, get_or_create_personal_workspace
class MultipleFileInput(forms.ClearableFileInput):
@@ -73,6 +73,20 @@ class UserPreferenceForm(forms.ModelForm):
}
class WorkspaceForm(forms.ModelForm):
class Meta:
model = Workspace
fields = ['name', 'slug']
labels = {'name': _('Workspace name'), 'slug': _('Short name')}
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Company or workspace name')}),
'slug': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'firma'}),
}
def clean_slug(self):
return self.cleaned_data['slug'].strip().lower()
class UserInviteForm(forms.ModelForm):
email = forms.EmailField(required=True, label=_('Email'), widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'person@example.com', 'required': True}))
@@ -96,6 +110,11 @@ class InviteRegistrationForm(UserCreationForm):
class TeamForm(forms.ModelForm):
def __init__(self, *args, user=None, workspace=None, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
self.workspace = workspace or (get_or_create_personal_workspace(user) if user and user.is_authenticated else None)
class Meta:
model = Team
fields = ['name', 'slug']
@@ -106,7 +125,15 @@ class TeamForm(forms.ModelForm):
}
def clean_slug(self):
return self.cleaned_data['slug'].strip().lstrip('#').lower()
slug = self.cleaned_data['slug'].strip().lstrip('#').lower()
if self.workspace and Team.objects.filter(workspace=self.workspace, slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError(_('This team tag is already used in this workspace.'))
return slug
def save(self, commit=True):
if self.workspace:
self.instance.workspace = self.workspace
return super().save(commit=commit)
class TeamInviteForm(forms.ModelForm):
@@ -120,6 +147,11 @@ class TeamInviteForm(forms.ModelForm):
class KanbanForm(forms.ModelForm):
tag_name = forms.CharField(label=_('Tag'), widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'my2dos'}))
def __init__(self, *args, user=None, workspace=None, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
self.workspace = workspace or (get_or_create_personal_workspace(user) if user and user.is_authenticated else None)
class Meta:
model = Kanban
fields = ['name', 'tag_name']
@@ -127,7 +159,8 @@ class KanbanForm(forms.ModelForm):
def save(self, commit=True):
tag_name = self.cleaned_data['tag_name'].strip().lstrip('#').lower()
tag, _ = Tag.objects.get_or_create(name=tag_name)
tag, _ = Tag.objects.get_or_create(workspace=self.workspace, name=tag_name)
self.instance.workspace = self.workspace
self.instance.tag = tag
return super().save(commit=commit)
@@ -135,19 +168,20 @@ class KanbanForm(forms.ModelForm):
class CollectionForm(forms.ModelForm):
team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False, label=_('Team'), widget=forms.Select(attrs={'class': 'form-select'}))
def __init__(self, *args, user=None, **kwargs):
def __init__(self, *args, user=None, workspace=None, **kwargs):
super().__init__(*args, **kwargs)
self.fields['visibility'].choices = [
(Item.Visibility.PRIVATE, _('Private')),
(Item.Visibility.PUBLIC, _('Public')),
]
if user and user.is_authenticated:
team_ids = user.team_memberships.values_list('team_id', flat=True)
self.fields['team'].queryset = Team.objects.filter(pk__in=team_ids).order_by('name')
workspace = self.instance.workspace if self.instance and self.instance.pk else (workspace or get_or_create_personal_workspace(user))
team_ids = user.team_memberships.filter(team__workspace=workspace).values_list('team_id', flat=True)
self.fields['team'].queryset = Team.objects.filter(workspace=workspace, pk__in=team_ids).order_by('name')
self.fields['team'].initial = self.instance.team_id if self.instance and self.instance.pk else None
item_scope = Q(items__owner=user) | Q(items__team_id__in=team_ids)
team_slugs = Team.objects.values_list('slug', flat=True)
self.fields['filter_tags'].queryset = Tag.objects.filter(item_scope).exclude(name__in=team_slugs).distinct().order_by('name')
item_scope = Q(items__workspace=workspace) & (Q(items__owner=user) | Q(items__team_id__in=team_ids))
team_slugs = Team.objects.filter(workspace=workspace).values_list('slug', flat=True)
self.fields['filter_tags'].queryset = Tag.objects.filter(workspace=workspace).filter(item_scope).exclude(name__in=team_slugs).distinct().order_by('name')
else:
self.fields['team'].queryset = Team.objects.none()
self.fields['filter_tags'].queryset = Tag.objects.none()
@@ -189,10 +223,11 @@ class CollectionForm(forms.ModelForm):
class ApiKeyForm(forms.ModelForm):
def __init__(self, *args, user=None, **kwargs):
def __init__(self, *args, user=None, workspace=None, **kwargs):
super().__init__(*args, **kwargs)
if user and user.is_authenticated:
self.fields['tags'].queryset = Tag.objects.filter(items__owner=user).distinct().order_by('name')
workspace = workspace or get_or_create_personal_workspace(user)
self.fields['tags'].queryset = Tag.objects.filter(workspace=workspace, items__owner=user).distinct().order_by('name')
else:
self.fields['tags'].queryset = Tag.objects.none()