Files
smoothschedule/smoothschedule/tickets/serializers.py
poduck ae74b4c2ed feat: Multi-email ticketing system with platform email addresses
- Add PlatformEmailAddress model for managing platform-level email addresses
- Add TicketEmailAddress model for tenant-level email addresses
- Create MailServerService for IMAP integration with mail.talova.net
- Implement PlatformEmailReceiver for processing incoming platform emails
- Add email autoconfiguration for Mozilla, Microsoft, and Apple clients
- Add configurable email polling interval in platform settings
- Add "Check Emails" button on support page for manual refresh
- Add ticket counts to status tabs on support page
- Add platform email addresses management page
- Add Privacy Policy and Terms of Service pages
- Add robots.txt for SEO
- Restrict email addresses to smoothschedule.com domain only

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 17:49:09 -05:00

224 lines
10 KiB
Python

from rest_framework import serializers
from .models import Ticket, TicketComment, TicketTemplate, CannedResponse, IncomingTicketEmail, TicketEmailAddress
from smoothschedule.users.models import User
from core.models import Tenant
class TicketCommentSerializer(serializers.ModelSerializer):
author_email = serializers.ReadOnlyField(source='author.email')
author_full_name = serializers.ReadOnlyField(source='author.full_name')
source_display = serializers.CharField(source='get_source_display', read_only=True)
class Meta:
model = TicketComment
fields = [
'id', 'ticket', 'author', 'author_email', 'author_full_name',
'comment_text', 'created_at', 'is_internal', 'source', 'source_display'
]
read_only_fields = ['id', 'ticket', 'author', 'author_email', 'author_full_name', 'created_at', 'source', 'source_display']
class TicketEmailAddressListSerializer(serializers.ModelSerializer):
"""Lightweight serializer for listing email addresses (no passwords)."""
class Meta:
model = TicketEmailAddress
fields = [
'id', 'display_name', 'email_address', 'color',
'is_active', 'is_default', 'last_check_at',
'emails_processed_count', 'created_at', 'updated_at'
]
read_only_fields = ['last_check_at', 'emails_processed_count', 'created_at', 'updated_at']
class TicketSerializer(serializers.ModelSerializer):
creator_email = serializers.ReadOnlyField(source='creator.email')
creator_full_name = serializers.ReadOnlyField(source='creator.full_name')
assignee_email = serializers.ReadOnlyField(source='assignee.email')
assignee_full_name = serializers.ReadOnlyField(source='assignee.full_name')
is_overdue = serializers.ReadOnlyField()
comments = TicketCommentSerializer(many=True, read_only=True)
source_email_address = TicketEmailAddressListSerializer(read_only=True)
class Meta:
model = Ticket
fields = [
'id', 'tenant', 'creator', 'creator_email', 'creator_full_name',
'assignee', 'assignee_email', 'assignee_full_name',
'ticket_type', 'status', 'priority', 'subject', 'description', 'category',
'related_appointment_id', 'due_at', 'first_response_at', 'is_overdue',
'created_at', 'updated_at', 'resolved_at', 'comments',
'external_email', 'external_name', 'source_email_address'
]
read_only_fields = ['id', 'creator', 'creator_email', 'creator_full_name',
'is_overdue', 'created_at', 'updated_at', 'resolved_at', 'comments',
'external_email', 'external_name']
def create(self, validated_data):
# Automatically set creator to the requesting user if not provided (e.g., for platform admin creating for tenant)
if 'creator' not in validated_data and self.context['request'].user.is_authenticated:
validated_data['creator'] = self.context['request'].user
# Ensure tenant is set for non-platform tickets
if validated_data.get('ticket_type') != Ticket.TicketType.PLATFORM:
# For tenant-specific tickets, ensure the requesting user's tenant is set
if hasattr(self.context['request'].user, 'tenant') and self.context['request'].user.tenant:
validated_data['tenant'] = self.context['request'].user.tenant
else:
raise serializers.ValidationError({"tenant": "Tenant must be provided for non-platform tickets."})
elif validated_data.get('ticket_type') == Ticket.TicketType.PLATFORM and not validated_data.get('tenant'):
# If platform ticket, but a platform admin wants to associate it with a tenant
# This means the tenant should be provided explicitly in the request
pass # Let it be null or provided by platform admin
return super().create(validated_data)
def update(self, instance, validated_data):
# Prevent changing tenant or creator after creation
validated_data.pop('tenant', None)
validated_data.pop('creator', None)
return super().update(instance, validated_data)
class TicketListSerializer(serializers.ModelSerializer):
"""Lighter version of TicketSerializer for list views without comments."""
creator_email = serializers.ReadOnlyField(source='creator.email')
creator_full_name = serializers.ReadOnlyField(source='creator.full_name')
assignee_email = serializers.ReadOnlyField(source='assignee.email')
assignee_full_name = serializers.ReadOnlyField(source='assignee.full_name')
is_overdue = serializers.ReadOnlyField()
class Meta:
model = Ticket
fields = [
'id', 'tenant', 'creator', 'creator_email', 'creator_full_name',
'assignee', 'assignee_email', 'assignee_full_name',
'ticket_type', 'status', 'priority', 'subject', 'description', 'category',
'related_appointment_id', 'due_at', 'first_response_at', 'is_overdue',
'created_at', 'updated_at', 'resolved_at',
'external_email', 'external_name'
]
read_only_fields = ['id', 'creator', 'creator_email', 'creator_full_name',
'is_overdue', 'created_at', 'updated_at', 'resolved_at',
'external_email', 'external_name']
class TicketTemplateSerializer(serializers.ModelSerializer):
"""Serializer for TicketTemplate model."""
class Meta:
model = TicketTemplate
fields = [
'id', 'tenant', 'name', 'description', 'ticket_type', 'category',
'default_priority', 'subject_template', 'description_template',
'is_active', 'created_at'
]
read_only_fields = ['id', 'created_at']
def create(self, validated_data):
# Set tenant based on request context
user = self.context['request'].user
# If tenant is not provided and user has a tenant, use it
if 'tenant' not in validated_data or validated_data['tenant'] is None:
if hasattr(user, 'tenant') and user.tenant:
validated_data['tenant'] = user.tenant
# Platform admins can create platform-wide templates (tenant=null)
return super().create(validated_data)
class CannedResponseSerializer(serializers.ModelSerializer):
"""Serializer for CannedResponse model."""
created_by_email = serializers.ReadOnlyField(source='created_by.email')
created_by_full_name = serializers.ReadOnlyField(source='created_by.full_name')
class Meta:
model = CannedResponse
fields = [
'id', 'tenant', 'title', 'content', 'category', 'is_active',
'use_count', 'created_by', 'created_by_email', 'created_by_full_name',
'created_at'
]
read_only_fields = ['id', 'use_count', 'created_by', 'created_by_email',
'created_by_full_name', 'created_at']
def create(self, validated_data):
# Set created_by to requesting user
user = self.context['request'].user
validated_data['created_by'] = user
# Set tenant based on request context
if 'tenant' not in validated_data or validated_data['tenant'] is None:
if hasattr(user, 'tenant') and user.tenant:
validated_data['tenant'] = user.tenant
# Platform admins can create platform-wide responses (tenant=null)
return super().create(validated_data)
class IncomingTicketEmailSerializer(serializers.ModelSerializer):
"""Serializer for incoming email records."""
processing_status_display = serializers.CharField(source='get_processing_status_display', read_only=True)
ticket_subject = serializers.CharField(source='ticket.subject', read_only=True, default='')
class Meta:
model = IncomingTicketEmail
fields = [
'id', 'message_id', 'from_address', 'from_name', 'to_address',
'subject', 'body_text', 'extracted_reply',
'ticket', 'ticket_subject', 'matched_user', 'ticket_id_from_email',
'processing_status', 'processing_status_display', 'error_message',
'email_date', 'received_at', 'processed_at'
]
read_only_fields = fields # All fields are read-only (incoming emails are created by the system)
class IncomingTicketEmailListSerializer(serializers.ModelSerializer):
"""Lighter serializer for listing incoming emails."""
processing_status_display = serializers.CharField(source='get_processing_status_display', read_only=True)
class Meta:
model = IncomingTicketEmail
fields = [
'id', 'from_address', 'from_name', 'subject',
'ticket', 'processing_status', 'processing_status_display',
'email_date', 'received_at'
]
class TicketEmailAddressSerializer(serializers.ModelSerializer):
"""Full serializer for email addresses with all settings."""
tenant_name = serializers.SerializerMethodField()
is_imap_configured = serializers.ReadOnlyField()
is_smtp_configured = serializers.ReadOnlyField()
is_fully_configured = serializers.ReadOnlyField()
def get_tenant_name(self, obj):
return obj.tenant.name if obj.tenant else 'Platform'
class Meta:
model = TicketEmailAddress
fields = [
'id', 'tenant', 'tenant_name', 'display_name', 'email_address', 'color',
'imap_host', 'imap_port', 'imap_use_ssl', 'imap_username',
'imap_password', 'imap_folder',
'smtp_host', 'smtp_port', 'smtp_use_tls', 'smtp_use_ssl',
'smtp_username', 'smtp_password',
'is_active', 'is_default', 'last_check_at', 'last_error',
'emails_processed_count', 'created_at', 'updated_at',
'is_imap_configured', 'is_smtp_configured', 'is_fully_configured'
]
read_only_fields = ['tenant', 'tenant_name', 'last_check_at', 'last_error',
'emails_processed_count', 'created_at', 'updated_at',
'is_imap_configured', 'is_smtp_configured', 'is_fully_configured']
extra_kwargs = {
'imap_password': {'write_only': True},
'smtp_password': {'write_only': True},
}
def create(self, validated_data):
# Automatically set tenant from current user
if 'tenant' not in validated_data and self.context['request'].user.is_authenticated:
if hasattr(self.context['request'].user, 'tenant') and self.context['request'].user.tenant:
validated_data['tenant'] = self.context['request'].user.tenant
return super().create(validated_data)