- 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>
224 lines
10 KiB
Python
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)
|