feat: Add comprehensive plugin documentation and advanced template system

Added complete plugin documentation with visual mockups and expanded template
variable system with CONTEXT, DATE helpers, and default values.

Backend Changes:
- Extended template_parser.py to support all new template types
- Added PROMPT with default values: {{PROMPT:var|desc|default}}
- Added CONTEXT variables: {{CONTEXT:business_name}}, {{CONTEXT:owner_email}}
- Added DATE helpers: {{DATE:today}}, {{DATE:+7d}}, {{DATE:monday}}
- Implemented date expression evaluation for relative dates
- Updated compile_template to handle all template types
- Added context parameter for business data auto-fill

Frontend Changes:
- Created comprehensive HelpPluginDocs.tsx with Stripe-style API docs
- Added visual mockup of plugin configuration form
- Documented all template types with examples and benefits
- Added Command Reference section with allowed/blocked Python commands
- Documented all HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Added URL whitelisting requirements and approval process
- Created Platform Staff management page with edit modal
- Added can_approve_plugins and can_whitelist_urls permissions

Platform Staff Features:
- List all platform_manager and platform_support users
- Edit user details with role-based permissions
- Superusers can edit anyone
- Platform managers can only edit platform_support users
- Permission cascade: users can only grant permissions they have
- Real-time updates via React Query cache invalidation

Documentation Highlights:
- 4 template types: PROMPT, CONTEXT, DATE, and automatic validation
- Visual form mockup showing exactly what users see
- All allowed control flow (if/elif/else, for, while, try/except, etc.)
- All allowed built-in functions (len, range, min, max, etc.)
- All blocked operations (import, exec, eval, class/function defs)
- Complete HTTP API reference with examples
- URL whitelisting process: contact pluginaccess@smoothschedule.com

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-11-28 20:54:07 -05:00
parent a9719a5fd2
commit 3fef0d5749
46 changed files with 8883 additions and 555 deletions

View File

@@ -8,10 +8,11 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework.decorators import action
from django.core.exceptions import ValidationError as DjangoValidationError
from .models import Resource, Event, Participant, ResourceType
from .models import Resource, Event, Participant, ResourceType, ScheduledTask, TaskExecutionLog
from .serializers import (
ResourceSerializer, EventSerializer, ParticipantSerializer,
CustomerSerializer, ServiceSerializer, ResourceTypeSerializer, StaffSerializer
CustomerSerializer, ServiceSerializer, ResourceTypeSerializer, StaffSerializer,
ScheduledTaskSerializer, TaskExecutionLogSerializer, PluginInfoSerializer
)
from .models import Service
from core.permissions import HasQuota
@@ -416,3 +417,191 @@ class StaffViewSet(viewsets.ModelViewSet):
'is_active': staff.is_active,
'message': f"Staff member {'activated' if staff.is_active else 'deactivated'} successfully."
})
class ScheduledTaskViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing scheduled tasks.
Permissions:
- Must be authenticated
- Only owners/managers can create/update/delete
Features:
- List all scheduled tasks
- Create new scheduled tasks
- Update existing tasks
- Delete tasks
- Pause/resume tasks
- Trigger manual execution
- View execution logs
"""
queryset = ScheduledTask.objects.all()
serializer_class = ScheduledTaskSerializer
permission_classes = [AllowAny] # TODO: Change to IsAuthenticated for production
ordering = ['-created_at']
def perform_create(self, serializer):
"""Set created_by to current user"""
# TODO: Uncomment when auth is enabled
# serializer.save(created_by=self.request.user)
serializer.save()
@action(detail=True, methods=['post'])
def pause(self, request, pk=None):
"""Pause a scheduled task"""
task = self.get_object()
if task.status == ScheduledTask.Status.PAUSED:
return Response(
{'error': 'Task is already paused'},
status=status.HTTP_400_BAD_REQUEST
)
task.status = ScheduledTask.Status.PAUSED
task.save(update_fields=['status'])
return Response({
'id': task.id,
'status': task.status,
'message': 'Task paused successfully'
})
@action(detail=True, methods=['post'])
def resume(self, request, pk=None):
"""Resume a paused scheduled task"""
task = self.get_object()
if task.status != ScheduledTask.Status.PAUSED:
return Response(
{'error': 'Task is not paused'},
status=status.HTTP_400_BAD_REQUEST
)
task.status = ScheduledTask.Status.ACTIVE
task.update_next_run_time()
task.save(update_fields=['status'])
return Response({
'id': task.id,
'status': task.status,
'next_run_at': task.next_run_at,
'message': 'Task resumed successfully'
})
@action(detail=True, methods=['post'])
def execute(self, request, pk=None):
"""Manually trigger task execution"""
task = self.get_object()
# Import here to avoid circular dependency
from .tasks import execute_scheduled_task
# Queue the task for immediate execution
result = execute_scheduled_task.delay(task.id)
return Response({
'id': task.id,
'celery_task_id': result.id,
'message': 'Task queued for execution'
})
@action(detail=True, methods=['get'])
def logs(self, request, pk=None):
"""Get execution logs for this task"""
task = self.get_object()
# Get pagination parameters
limit = int(request.query_params.get('limit', 20))
offset = int(request.query_params.get('offset', 0))
logs = task.execution_logs.all()[offset:offset + limit]
serializer = TaskExecutionLogSerializer(logs, many=True)
return Response({
'count': task.execution_logs.count(),
'results': serializer.data
})
class TaskExecutionLogViewSet(viewsets.ReadOnlyModelViewSet):
"""
API endpoint for viewing task execution logs (read-only).
Features:
- List all execution logs
- Filter by task, status, date range
- View individual log details
"""
queryset = TaskExecutionLog.objects.select_related('scheduled_task').all()
serializer_class = TaskExecutionLogSerializer
permission_classes = [AllowAny] # TODO: Change to IsAuthenticated for production
ordering = ['-started_at']
def get_queryset(self):
"""Filter logs by query parameters"""
queryset = super().get_queryset()
# Filter by scheduled task
task_id = self.request.query_params.get('task_id')
if task_id:
queryset = queryset.filter(scheduled_task_id=task_id)
# Filter by status
status_filter = self.request.query_params.get('status')
if status_filter:
queryset = queryset.filter(status=status_filter)
return queryset
class PluginViewSet(viewsets.ViewSet):
"""
API endpoint for listing available plugins.
Features:
- List all registered plugins
- Get plugin details
- List plugins by category
"""
permission_classes = [AllowAny] # TODO: Change to IsAuthenticated for production
def list(self, request):
"""List all available plugins"""
from .plugins import registry
plugins = registry.list_all()
serializer = PluginInfoSerializer(plugins, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def by_category(self, request):
"""List plugins grouped by category"""
from .plugins import registry
plugins_by_category = registry.list_by_category()
return Response(plugins_by_category)
def retrieve(self, request, pk=None):
"""Get details for a specific plugin"""
from .plugins import registry
plugin_class = registry.get(pk)
if not plugin_class:
return Response(
{'error': f"Plugin '{pk}' not found"},
status=status.HTTP_404_NOT_FOUND
)
plugin_info = {
'name': plugin_class.name,
'display_name': plugin_class.display_name,
'description': plugin_class.description,
'category': plugin_class.category,
'config_schema': plugin_class.config_schema,
}
serializer = PluginInfoSerializer(plugin_info)
return Response(serializer.data)