bottleneckcrm/backend/common/base.py
Ashwin Kumar fc05248c28
revamp and upgrade to modern stack (#562)
revamp and upgrade to modern stack
2025-11-29 10:07:50 +05:30

153 lines
4.8 KiB
Python

import uuid
# Third party imports
from crum import get_current_user
# Django imports
from django.db import models
# Module imports
from common.mixins import AuditModel
class AssignableMixin(models.Model):
"""
Mixin for models that have assigned_to (Profile) and teams (Teams) fields.
Provides helper methods for getting users from teams and assignments.
Models using this mixin must define:
- assigned_to = ManyToManyField(Profile, ...)
- teams = ManyToManyField(Teams, ...)
"""
class Meta:
abstract = True
@property
def get_team_users(self):
"""Get all users who are members of assigned teams."""
from common.models import Profile
team_user_ids = list(self.teams.values_list("users__id", flat=True))
return Profile.objects.filter(id__in=team_user_ids)
@property
def get_team_and_assigned_users(self):
"""Get all users from both teams and direct assignments."""
from common.models import Profile
team_user_ids = list(self.teams.values_list("users__id", flat=True))
assigned_user_ids = list(self.assigned_to.values_list("id", flat=True))
user_ids = team_user_ids + assigned_user_ids
return Profile.objects.filter(id__in=user_ids)
@property
def get_assigned_users_not_in_teams(self):
"""Get users who are directly assigned but not part of any assigned team."""
from common.models import Profile
team_user_ids = list(self.teams.values_list("users__id", flat=True))
assigned_user_ids = list(self.assigned_to.values_list("id", flat=True))
user_ids = set(assigned_user_ids) - set(team_user_ids)
return Profile.objects.filter(id__in=list(user_ids))
class BaseModel(AuditModel):
id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
)
class Meta:
abstract = True
def save(self, *args, **kwargs):
user = get_current_user()
if user is None or user.is_anonymous:
self.created_by = None
self.updated_by = None
super(BaseModel, self).save(*args, **kwargs)
else:
# Check if the model is being created or updated
if self._state.adding:
# If created only set created_by value: set updated_by to None
self.created_by = user
self.updated_by = None
# If updated only set updated_by value don't touch created_by
self.updated_by = user
super(BaseModel, self).save(*args, **kwargs)
def __str__(self):
return str(self.id)
class OrgScopedQuerySet(models.QuerySet):
"""QuerySet with automatic organization filtering support"""
def for_org(self, org):
"""Filter queryset by organization"""
return self.filter(org=org)
def for_request(self, request):
"""Filter queryset by organization from request"""
if hasattr(request, "profile") and request.profile:
return self.filter(org=request.profile.org)
return self.none()
class OrgScopedManager(models.Manager):
"""Manager that uses OrgScopedQuerySet"""
def get_queryset(self):
return OrgScopedQuerySet(self.model, using=self._db)
def for_org(self, org):
return self.get_queryset().for_org(org)
def for_request(self, request):
return self.get_queryset().for_request(request)
class BaseOrgModel(BaseModel):
"""
Base model for all organization-scoped entities.
All tenant-owned models should inherit from this instead of BaseModel.
This ensures:
- Required org field (no null values)
- Per-org indexes for performance
- OrgScopedManager for easier filtering
RLS Protection:
---------------
Models inheriting from BaseOrgModel are protected by PostgreSQL Row-Level
Security (RLS) policies. The database automatically filters rows by the
current organization context (app.current_org session variable).
RLS Configuration: See common/rls/__init__.py for policy definitions.
Management: Use `python manage.py manage_rls --status` to check status.
IMPORTANT: RLS is bypassed if the database user is a superuser.
"""
org = models.ForeignKey(
"common.Org",
on_delete=models.CASCADE,
related_name="%(class)s_set",
help_text="Organization this record belongs to",
)
objects = OrgScopedManager()
class Meta:
abstract = True
indexes = [
models.Index(fields=["org", "-created_at"]),
]
def save(self, *args, **kwargs):
# Validate org is set
if not self.org_id:
from django.core.exceptions import ValidationError
raise ValidationError("Organization (org) is required for this model")
super().save(*args, **kwargs)