Restructured 13 Django apps from flat/mixed organization into 5 logical
domain packages following cookiecutter-django conventions:
- identity/: core (tenant/domain models, middleware, mixins), users
- scheduling/: schedule, contracts, analytics
- communication/: notifications, credits, mobile, messaging
- commerce/: payments, tickets
- platform/: admin, api
Key changes:
- Moved all apps to smoothschedule/smoothschedule/{domain}/{app}/
- Updated all import paths across the codebase
- Updated settings (base.py, multitenancy.py, test.py)
- Updated URL configuration in config/urls.py
- Updated middleware and permission paths
- Preserved app_label in AppConfig for migration compatibility
- Updated CLAUDE.md documentation with new structure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.5 KiB
Calendar Sync Feature Integration Guide
This document explains how the calendar sync feature permission system works and provides examples for implementation.
Overview
The calendar sync feature allows tenants to:
- Connect to Google Calendar or Outlook Calendar via OAuth
- Sync calendar events into the scheduling system
- Manage multiple calendar integrations
The feature is gated by the can_use_calendar_sync permission that must be enabled at the tenant level.
Architecture
1. Database Model (Tenant)
Added to /home/poduck/Desktop/smoothschedule2/smoothschedule/core/models.py:
can_use_calendar_sync = models.BooleanField(
default=False,
help_text="Whether this business can sync Google Calendar and other calendar providers"
)
2. Permission Check (HasFeaturePermission)
Updated in /home/poduck/Desktop/smoothschedule2/smoothschedule/core/permissions.py:
FEATURE_NAMES = {
# ... other features ...
'can_use_calendar_sync': 'Calendar Sync',
}
The HasFeaturePermission factory function creates a DRF permission class:
from core.permissions import HasFeaturePermission
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
3. OAuth Integration
Modified in /home/poduck/Desktop/smoothschedule2/smoothschedule/core/oauth_views.py:
GoogleOAuthInitiateView: Checks permission when purpose is "calendar"MicrosoftOAuthInitiateView: Checks permission when purpose is "calendar"
def post(self, request):
purpose = request.data.get('purpose', 'email')
# Check calendar sync permission if purpose is calendar
if purpose == 'calendar':
calendar_permission = HasFeaturePermission('can_use_calendar_sync')
if not calendar_permission().has_permission(request, self):
return Response({
'success': False,
'error': 'Your current plan does not include Calendar Sync.',
}, status=status.HTTP_403_FORBIDDEN)
# Continue with OAuth flow...
4. Calendar Sync Views
Created in /home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/calendar_sync_views.py:
CalendarListView: Lists connected calendarsCalendarSyncView: Syncs calendar eventsCalendarDeleteView: Disconnects calendar integrationCalendarStatusView: Gets calendar sync status
Permission Pattern in Views
from core.permissions import HasFeaturePermission
from rest_framework.permissions import IsAuthenticated
class CalendarSyncPermission(IsAuthenticated):
"""Custom permission combining auth + feature check"""
def has_permission(self, request, view):
if not super().has_permission(request, view):
return False
tenant = getattr(request, 'tenant', None)
if not tenant:
return False
return tenant.has_feature('can_use_calendar_sync')
class CalendarListView(APIView):
permission_classes = [CalendarSyncPermission]
def get(self, request):
# This endpoint is only accessible if:
# 1. User is authenticated
# 2. Tenant has can_use_calendar_sync enabled
...
Usage Examples
1. Enable Calendar Sync for a Tenant
# Via Django shell or management command
from core.models import Tenant
tenant = Tenant.objects.get(schema_name='demo')
tenant.can_use_calendar_sync = True
tenant.save()
# Check if tenant has feature
if tenant.has_feature('can_use_calendar_sync'):
print("Calendar sync enabled!")
2. API Endpoints
Initiate Google Calendar OAuth
POST /api/oauth/google/initiate/
Content-Type: application/json
{
"purpose": "calendar"
}
# Response (if permission granted):
{
"success": true,
"authorization_url": "https://accounts.google.com/o/oauth2/auth?..."
}
# Response (if permission denied):
{
"success": false,
"error": "Your current plan does not include Calendar Sync. Please upgrade..."
}
List Connected Calendars
GET /api/calendar/list/
# Response:
{
"success": true,
"calendars": [
{
"id": 1,
"provider": "Google",
"email": "user@gmail.com",
"is_valid": true,
"is_expired": false,
"last_used_at": "2025-12-02T10:30:00Z",
"created_at": "2025-12-01T08:15:00Z"
}
]
}
Sync Calendar Events
POST /api/calendar/sync/
Content-Type: application/json
{
"credential_id": 1,
"calendar_id": "primary",
"start_date": "2025-12-01",
"end_date": "2025-12-31"
}
# Response:
{
"success": true,
"message": "Calendar sync started for user@gmail.com"
}
Check Calendar Sync Status
GET /api/calendar/status/
# Response (if feature enabled):
{
"success": true,
"can_use_calendar_sync": true,
"total_connected": 2,
"feature_enabled": true
}
# Response (if feature not enabled):
{
"success": true,
"can_use_calendar_sync": false,
"message": "Calendar Sync feature is not available for your plan",
"total_connected": 0
}
Permission Flow Diagram
User Request to Calendar Endpoint
↓
[Is User Authenticated?]
├─ NO → 401 Unauthorized
└─ YES ↓
[Is Request in Tenant Context?]
├─ NO → 400 Bad Request
└─ YES ↓
[Does Tenant have can_use_calendar_sync?]
├─ NO → 403 Permission Denied (upgrade message)
└─ YES ↓
[Process Request]
├─ Success → 200 OK
└─ Error → 500 Server Error
Integration with ViewSets
For ModelViewSet endpoints (like scheduling events from calendar sync):
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from core.permissions import HasFeaturePermission
from schedule.models import Event
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
# Permission for create/update/delete operations
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
def get_queryset(self):
# Only show events that user's tenant can access
return Event.objects.filter(tenant=self.request.tenant)
@action(detail=False, methods=['post'])
def sync_from_calendar(self, request):
"""Create events from a calendar sync"""
# This action is protected by the permission_classes
calendar_id = request.data.get('calendar_id')
# ... implement sync logic ...
Note: If you only want to gate specific actions, override get_permissions():
def get_permissions(self):
if self.action in ['create', 'update', 'destroy', 'sync_from_calendar']:
# Only allow these actions if calendar sync is enabled
return [IsAuthenticated(), HasFeaturePermission('can_use_calendar_sync')()]
# Read-only actions don't require calendar sync permission
return [IsAuthenticated()]
Migration
The migration 0016_tenant_can_use_calendar_sync.py adds the field to existing tenants with default value of False.
To apply the migration:
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
docker compose -f docker-compose.local.yml exec django python manage.py migrate
Testing
Test Permission Denied
from django.test import TestCase, RequestFactory
from rest_framework.test import APITestCase
from core.models import Tenant
from smoothschedule.identity.users.models import User
class CalendarSyncTests(APITestCase):
def setUp(self):
self.tenant = Tenant.objects.create(
schema_name='test',
name='Test Tenant',
can_use_calendar_sync=False # Feature disabled
)
self.user = User.objects.create_user(
email='user@test.com',
password='testpass',
tenant=self.tenant
)
self.client.force_authenticate(user=self.user)
def test_calendar_sync_permission_denied(self):
"""Test that users without permission cannot access calendar sync"""
response = self.client.post('/api/calendar/list/')
# Should return 403 Forbidden with upgrade message
self.assertEqual(response.status_code, 403)
self.assertIn('upgrade', response.json()['error'].lower())
def test_calendar_sync_permission_granted(self):
"""Test that users with permission can access calendar sync"""
self.tenant.can_use_calendar_sync = True
self.tenant.save()
response = self.client.get('/api/calendar/list/')
# Should return 200 OK
self.assertEqual(response.status_code, 200)
Security Considerations
- Permission is checked at view level: The
CalendarSyncPermissionchecks that the tenant has the feature enabled - Tenant isolation: OAuthCredential queries filter by tenant to ensure data isolation
- Token security: Tokens are stored encrypted at rest (configured in settings)
- CSRF protection: OAuth state parameter prevents CSRF attacks
- Audit logging: All calendar sync operations are logged with tenant and user information
Related Files
- Model:
/home/poduck/Desktop/smoothschedule2/smoothschedule/core/models.py(Tenant.can_use_calendar_sync) - Permission:
/home/poduck/Desktop/smoothschedule2/smoothschedule/core/permissions.py(HasFeaturePermission) - OAuth Views:
/home/poduck/Desktop/smoothschedule2/smoothschedule/core/oauth_views.py - Calendar Views:
/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/calendar_sync_views.py - Migration:
/home/poduck/Desktop/smoothschedule2/smoothschedule/core/migrations/0016_tenant_can_use_calendar_sync.py