bottleneckcrm/backend/common/serializer.py

761 lines
22 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
from django.contrib.auth import authenticate
from django.contrib.auth.hashers import check_password
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_decode
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework_simplejwt.tokens import AccessToken, RefreshToken
from common.utils import CURRENCY_SYMBOLS
from common.models import (
Activity,
Address,
APISettings,
Attachments,
Comment,
Document,
Org,
Profile,
Tags,
Teams,
User,
)
class OrgAwareRefreshToken(RefreshToken):
"""
Custom RefreshToken that includes org context in the token payload.
This ensures the org context is cryptographically signed and cannot be
forged by the client. The middleware should validate org_id from this
token instead of trusting the org header.
Embedded claims (to avoid extra API calls):
- org_id: Organization UUID
- org_name: Organization name (for display)
- role: User's role in the org (ADMIN/USER)
- org_settings: Currency and locale settings
"""
@classmethod
def for_user_and_org(cls, user, org, profile=None):
"""
Generate a refresh token for a user with org context.
Args:
user: User instance
org: Org instance or org_id UUID
profile: Optional Profile instance for role
Returns:
OrgAwareRefreshToken with org claims
"""
token = cls.for_user(user)
# Add org context to the token payload
if org:
org_id = str(org.id) if hasattr(org, "id") else str(org)
token["org_id"] = org_id
# Add org_name for display (avoids /api/auth/profile call)
if hasattr(org, "name"):
token["org_name"] = org.name
# Add org settings for currency/locale
if hasattr(org, "default_currency"):
token["org_settings"] = {
"default_currency": org.default_currency or "USD",
"currency_symbol": CURRENCY_SYMBOLS.get(
org.default_currency or "USD", "$"
),
"default_country": org.default_country,
}
# Add role if profile provided (avoids /api/auth/profile call)
if profile:
token["role"] = profile.role
return token
class OrganizationSerializer(serializers.ModelSerializer):
class Meta:
model = Org
fields = ("id", "name", "api_key")
class OrgSettingsSerializer(serializers.ModelSerializer):
"""Serializer for org settings (currency, country, locale, company profile)"""
currency_symbol = serializers.SerializerMethodField()
logo_url = serializers.SerializerMethodField()
class Meta:
model = Org
fields = [
"id",
"name",
# Company profile
"company_name",
"logo",
"logo_url",
"address_line",
"city",
"state",
"postcode",
"country",
"phone",
"email",
"website",
"tax_id",
# Locale settings
"default_currency",
"default_country",
"currency_symbol",
]
read_only_fields = ["id", "currency_symbol", "logo_url"]
@extend_schema_field(str)
def get_currency_symbol(self, obj):
return CURRENCY_SYMBOLS.get(obj.default_currency or "USD", "$")
@extend_schema_field(str)
def get_logo_url(self, obj):
if obj.logo:
request = self.context.get("request")
if request:
return request.build_absolute_uri(obj.logo.url)
return obj.logo.url
return None
class TagsSerializer(serializers.ModelSerializer):
class Meta:
model = Tags
fields = (
"id",
"name",
"slug",
"color",
"description",
"is_active",
"created_at",
)
read_only_fields = ("id", "slug", "created_at")
class SocialLoginSerializer(serializers.Serializer):
token = serializers.CharField()
class CommentSerializer(serializers.ModelSerializer):
"""Serializer for Comment model using ContentType"""
content_type = serializers.SlugRelatedField(slug_field="model", read_only=True)
class Meta:
model = Comment
fields = (
"id",
"comment",
"commented_on",
"commented_by",
"content_type",
"object_id",
"org",
)
class CommentCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating comments with ContentType"""
content_type = serializers.CharField(write_only=True)
object_id = serializers.UUIDField(write_only=True)
class Meta:
model = Comment
fields = (
"comment",
"content_type",
"object_id",
)
def create(self, validated_data):
from django.contrib.contenttypes.models import ContentType
content_type_str = validated_data.pop("content_type")
try:
content_type = ContentType.objects.get(model=content_type_str.lower())
except ContentType.DoesNotExist:
raise serializers.ValidationError(
f"Invalid content type: {content_type_str}"
)
validated_data["content_type"] = content_type
return super().create(validated_data)
class LeadCommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = (
"id",
"comment",
"commented_on",
"commented_by",
)
class OrgProfileCreateSerializer(serializers.ModelSerializer):
"""
It is for creating organization
"""
name = serializers.CharField(max_length=255)
class Meta:
model = Org
fields = ["name"]
extra_kwargs = {"name": {"required": True}}
def validate_name(self, name):
if bool(re.search(r"[~\!@#\$%\^&\*\(\)\+{}\":;'/\[\]]", name)):
raise serializers.ValidationError(
"organization name should not contain any special characters"
)
if Org.objects.filter(name=name).exists():
raise serializers.ValidationError(
"Organization already exists with this name"
)
return name
class ShowOrganizationListSerializer(serializers.ModelSerializer):
"""
we are using it for show orjanization list
"""
org = OrganizationSerializer()
class Meta:
model = Profile
fields = (
"role",
"alternate_phone",
"has_sales_access",
"has_marketing_access",
"is_organization_admin",
"org",
)
class BillingAddressSerializer(serializers.ModelSerializer):
class Meta:
model = Address
fields = ("address_line", "street", "city", "state", "postcode", "country")
def __init__(self, *args, **kwargs):
account_view = kwargs.pop("account", False)
super().__init__(*args, **kwargs)
if account_view:
self.fields["address_line"].required = True
self.fields["street"].required = True
self.fields["city"].required = True
self.fields["state"].required = True
self.fields["postcode"].required = True
self.fields["country"].required = True
class CreateUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
"email",
"profile_pic",
)
def __init__(self, *args, **kwargs):
self.org = kwargs.pop("org", None)
super().__init__(*args, **kwargs)
self.fields["email"].required = True
def validate_email(self, email):
if self.instance:
if self.instance.email != email:
if not Profile.objects.filter(user__email=email, org=self.org).exists():
return email
raise serializers.ValidationError("Email already exists")
return email
if not Profile.objects.filter(user__email=email.lower(), org=self.org).exists():
return email
raise serializers.ValidationError("Given Email id already exists")
class CreateProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = (
"role",
"phone",
"alternate_phone",
"has_sales_access",
"has_marketing_access",
"is_organization_admin",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["alternate_phone"].required = False
self.fields["role"].required = True
self.fields["phone"].required = True
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "email", "profile_pic"]
class ProfileSerializer(serializers.ModelSerializer):
# address = BillingAddressSerializer()
user_details = serializers.SerializerMethodField()
@extend_schema_field(dict)
def get_user_details(self, obj):
return obj.user_details
class Meta:
model = Profile
fields = (
"id",
"user_details",
"role",
"address",
"has_marketing_access",
"has_sales_access",
"phone",
"date_of_joining",
"is_active",
"created_at",
)
class AttachmentsSerializer(serializers.ModelSerializer):
"""Serializer for Attachments model using ContentType"""
file_path = serializers.SerializerMethodField()
content_type = serializers.SlugRelatedField(slug_field="model", read_only=True)
@extend_schema_field(str)
def get_file_path(self, obj):
if obj.attachment:
return obj.attachment.url
return None
class Meta:
model = Attachments
fields = [
"id",
"created_by",
"file_name",
"created_at",
"file_path",
"content_type",
"object_id",
"org",
]
class AttachmentsCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating attachments with ContentType"""
content_type = serializers.CharField(write_only=True)
object_id = serializers.UUIDField(write_only=True)
class Meta:
model = Attachments
fields = (
"file_name",
"attachment",
"content_type",
"object_id",
)
def create(self, validated_data):
from django.contrib.contenttypes.models import ContentType
content_type_str = validated_data.pop("content_type")
try:
content_type = ContentType.objects.get(model=content_type_str.lower())
except ContentType.DoesNotExist:
raise serializers.ValidationError(
f"Invalid content type: {content_type_str}"
)
validated_data["content_type"] = content_type
return super().create(validated_data)
class DocumentSerializer(serializers.ModelSerializer):
shared_to = ProfileSerializer(read_only=True, many=True)
teams = serializers.SerializerMethodField()
created_by = UserSerializer()
org = OrganizationSerializer()
@extend_schema_field(list)
def get_teams(self, obj):
return obj.teams.all().values()
class Meta:
model = Document
fields = [
"id",
"title",
"document_file",
"status",
"shared_to",
"teams",
"created_at",
"created_by",
"org",
]
class DocumentCreateSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
request_obj = kwargs.pop("request_obj", None)
super().__init__(*args, **kwargs)
self.fields["title"].required = True
self.org = request_obj.profile.org
def validate_title(self, title):
if self.instance:
if (
Document.objects.filter(title__iexact=title, org=self.org)
.exclude(id=self.instance.id)
.exists()
):
raise serializers.ValidationError(
"Document with this Title already exists"
)
if Document.objects.filter(title__iexact=title, org=self.org).exists():
raise serializers.ValidationError("Document with this Title already exists")
return title
class Meta:
model = Document
fields = ["title", "document_file", "status", "org"]
def find_urls(string):
# website_regex = "^((http|https)://)?([A-Za-z0-9.-]+\.[A-Za-z]{2,63})?$" # (http(s)://)google.com or google.com
# website_regex = "^https?://([A-Za-z0-9.-]+\.[A-Za-z]{2,63})?$" # (http(s)://)google.com
# http(s)://google.com
website_regex = r"^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}$"
# http(s)://google.com:8000
website_regex_port = r"^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}:[0-9]{2,4}$"
url = re.findall(website_regex, string)
url_port = re.findall(website_regex_port, string)
if url and url[0] != "":
return url
return url_port
class APISettingsSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Meta:
model = APISettings
fields = ("title", "website")
def validate_website(self, website):
if website and not (
website.startswith("http://") or website.startswith("https://")
):
raise serializers.ValidationError("Please provide valid schema")
if not len(find_urls(website)) > 0:
raise serializers.ValidationError(
"Please provide a valid URL with schema and without trailing slash - Example: http://google.com"
)
return website
class APISettingsListSerializer(serializers.ModelSerializer):
created_by = UserSerializer()
lead_assigned_to = ProfileSerializer(read_only=True, many=True)
tags = serializers.SerializerMethodField()
org = OrganizationSerializer()
@extend_schema_field(list)
def get_tags(self, obj):
return obj.tags.all().values()
class Meta:
model = APISettings
fields = [
"title",
"apikey",
"website",
"created_at",
"created_by",
"lead_assigned_to",
"tags",
"org",
]
class APISettingsSwaggerSerializer(serializers.ModelSerializer):
class Meta:
model = APISettings
fields = [
"title",
"website",
"lead_assigned_to",
"tags",
]
class DocumentCreateSwaggerSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = [
"title",
"document_file",
"teams",
"shared_to",
]
class DocumentEditSwaggerSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ["title", "document_file", "teams", "shared_to", "status"]
class UserCreateSwaggerSerializer(serializers.Serializer):
"""
It is swagger for creating or updating user
"""
ROLE_CHOICES = ["ADMIN", "USER"]
email = serializers.CharField(max_length=1000, required=True)
role = serializers.ChoiceField(choices=ROLE_CHOICES, required=True)
phone = serializers.CharField(max_length=12)
alternate_phone = serializers.CharField(max_length=12)
address_line = serializers.CharField(max_length=10000, required=True)
street = serializers.CharField(max_length=1000)
city = serializers.CharField(max_length=1000)
state = serializers.CharField(max_length=1000)
pincode = serializers.CharField(max_length=1000)
country = serializers.CharField(max_length=1000)
class UserUpdateStatusSwaggerSerializer(serializers.Serializer):
STATUS_CHOICES = ["Active", "Inactive"]
status = serializers.ChoiceField(choices=STATUS_CHOICES, required=True)
# JWT Authentication Serializers for SvelteKit Integration
class LoginSerializer(serializers.Serializer):
"""Serializer for user login with email and password"""
email = serializers.EmailField(required=True)
password = serializers.CharField(required=True, write_only=True)
def validate(self, attrs):
email = attrs.get("email")
password = attrs.get("password")
if email and password:
user = authenticate(username=email, password=password)
if not user:
raise serializers.ValidationError("Invalid email or password")
if not user.is_active:
raise serializers.ValidationError("User account is disabled")
else:
raise serializers.ValidationError('Must include "email" and "password"')
attrs["user"] = user
return attrs
class RegisterSerializer(serializers.ModelSerializer):
"""Serializer for user registration"""
password = serializers.CharField(write_only=True, required=True, min_length=8)
confirm_password = serializers.CharField(write_only=True, required=True)
class Meta:
model = User
fields = ["email", "password", "confirm_password"]
def validate(self, attrs):
if attrs["password"] != attrs["confirm_password"]:
raise serializers.ValidationError(
{"password": "Password fields didn't match."}
)
return attrs
def create(self, validated_data):
validated_data.pop("confirm_password")
user = User.objects.create_user(
email=validated_data["email"],
password=validated_data["password"],
is_active=False, # User needs to activate account
)
return user
class UserDetailSerializer(serializers.ModelSerializer):
"""Detailed user serializer with profile and organizations"""
organizations = serializers.SerializerMethodField()
class Meta:
model = User
fields = ["id", "email", "profile_pic", "is_active", "organizations"]
@extend_schema_field(list)
def get_organizations(self, obj):
"""Get all organizations the user belongs to"""
profiles = Profile.objects.filter(user=obj, is_active=True)
return [
{
"id": str(profile.org.id),
"name": profile.org.name,
"role": profile.role,
"is_organization_admin": profile.is_organization_admin,
"has_sales_access": profile.has_sales_access,
"has_marketing_access": profile.has_marketing_access,
}
for profile in profiles
]
class ProfileDetailSerializer(serializers.ModelSerializer):
"""Detailed profile serializer for authenticated user"""
user = UserSerializer(read_only=True)
org = OrganizationSerializer(read_only=True)
class Meta:
model = Profile
fields = [
"id",
"user",
"org",
"role",
"is_organization_admin",
"has_sales_access",
"has_marketing_access",
"phone",
"date_of_joining",
"is_active",
]
# Activity Serializers for Dashboard Recent Activities
class ActivityUserSerializer(serializers.Serializer):
"""Simplified user info for activity display"""
id = serializers.UUIDField(source="user.id")
email = serializers.EmailField(source="user.email")
name = serializers.SerializerMethodField()
profile_pic = serializers.CharField(source="user.profile_pic", allow_null=True)
@extend_schema_field(str)
def get_name(self, obj):
"""Get display name from email"""
return obj.user.email.split("@")[0]
class ActivitySerializer(serializers.ModelSerializer):
"""Serializer for recent activities"""
user = ActivityUserSerializer(read_only=True)
action_display = serializers.CharField(source="get_action_display", read_only=True)
timestamp = serializers.DateTimeField(source="created_at", read_only=True)
humanized_time = serializers.CharField(source="created_on_arrow", read_only=True)
class Meta:
model = Activity
fields = [
"id",
"user",
"action",
"action_display",
"entity_type",
"entity_id",
"entity_name",
"description",
"timestamp",
"humanized_time",
]
class TeamsSerializer(serializers.ModelSerializer):
users = ProfileSerializer(read_only=True, many=True)
created_by = UserSerializer()
class Meta:
model = Teams
fields = (
"id",
"name",
"description",
"users",
"created_at",
"created_by",
)
class TeamCreateSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
request_obj = kwargs.pop("request_obj", None)
super().__init__(*args, **kwargs)
self.org = request_obj.profile.org
self.fields["name"].required = True
self.fields["description"].required = False
def validate_name(self, name):
if self.instance:
if (
Teams.objects.filter(name__iexact=name, org=self.org)
.exclude(id=self.instance.id)
.exists()
):
raise serializers.ValidationError("Team already exists with this name")
else:
if Teams.objects.filter(name__iexact=name).exists():
raise serializers.ValidationError("Team already exists with this name")
return name
class Meta:
model = Teams
fields = (
"name",
"description",
"created_at",
"created_by",
"org",
)
class TeamswaggerCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Teams
fields = (
"name",
"description",
"users",
)