Init Repo
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# Admin API Key Management
|
||||
|
||||
This document describes the enhanced API key management functionality that allows administrators to create and manage API keys for any user in the system.
|
||||
|
||||
## Overview
|
||||
|
||||
The VFX Project Management System now supports comprehensive API key management with different permission levels:
|
||||
|
||||
- **Developers**: Can create and manage their own API keys
|
||||
- **Admins**: Can create and manage API keys for any user in the system
|
||||
|
||||
## Admin Capabilities
|
||||
|
||||
### 1. Create API Keys for Any User
|
||||
|
||||
Admins can create API keys for any approved user in the system using two methods:
|
||||
|
||||
#### Method 1: General Endpoint with user_id Parameter
|
||||
```http
|
||||
POST /auth/api-keys
|
||||
Authorization: Bearer <admin_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Integration Key for John Doe",
|
||||
"scopes": ["read:projects", "read:tasks"],
|
||||
"user_id": 123,
|
||||
"expires_at": "2024-12-31T23:59:59"
|
||||
}
|
||||
```
|
||||
|
||||
#### Method 2: Admin-Specific Endpoint
|
||||
```http
|
||||
POST /auth/admin/users/123/api-keys
|
||||
Authorization: Bearer <admin_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Integration Key for John Doe",
|
||||
"scopes": ["read:projects", "read:tasks"],
|
||||
"expires_at": "2024-12-31T23:59:59"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. View All API Keys
|
||||
|
||||
Admins can view all API keys in the system:
|
||||
|
||||
```http
|
||||
GET /auth/api-keys
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
Response includes user email for each API key:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": 123,
|
||||
"user_email": "john.doe@example.com",
|
||||
"name": "Integration Key",
|
||||
"scopes": ["read:projects", "read:tasks"],
|
||||
"is_active": true,
|
||||
"expires_at": "2024-12-31T23:59:59Z",
|
||||
"last_used_at": "2024-01-15T10:30:00Z",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 3. View API Keys for Specific User
|
||||
|
||||
```http
|
||||
GET /auth/admin/users/123/api-keys
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
### 4. Manage Any API Key
|
||||
|
||||
Admins can update or delete any API key in the system:
|
||||
|
||||
```http
|
||||
PUT /auth/api-keys/456
|
||||
DELETE /auth/api-keys/456
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
### 5. View Usage Logs for Any API Key
|
||||
|
||||
```http
|
||||
GET /auth/api-keys/456/usage
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
## API Key Scopes
|
||||
|
||||
Available scopes for API keys:
|
||||
|
||||
- `read:projects` - Read access to all projects
|
||||
- `read:tasks` - Read access to all tasks
|
||||
- `read:submissions` - Read access to all submissions
|
||||
- `read:users` - Read access to user information
|
||||
- `write:tasks` - Write access to tasks
|
||||
- `write:submissions` - Write access to submissions
|
||||
- `admin:users` - Administrative access to user management
|
||||
- `full:access` - Full system access
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **API Key Hashing**: All API keys are hashed before storage using SHA-256
|
||||
2. **Usage Logging**: Every API request is logged with timestamp, endpoint, method, IP address, and user agent
|
||||
3. **Expiration**: API keys can have expiration dates
|
||||
4. **Revocation**: API keys can be deactivated or deleted at any time
|
||||
5. **Scope-based Access**: Fine-grained permissions control what each API key can access
|
||||
|
||||
## Developer vs Admin Permissions
|
||||
|
||||
| Action | Developer | Admin |
|
||||
|--------|-----------|-------|
|
||||
| Create own API keys | ✅ | ✅ |
|
||||
| Create API keys for others | ❌ | ✅ |
|
||||
| View own API keys | ✅ | ✅ |
|
||||
| View all API keys | ❌ | ✅ |
|
||||
| Update own API keys | ✅ | ✅ |
|
||||
| Update any API key | ❌ | ✅ |
|
||||
| Delete own API keys | ✅ | ✅ |
|
||||
| Delete any API key | ❌ | ✅ |
|
||||
| View own usage logs | ✅ | ✅ |
|
||||
| View any usage logs | ❌ | ✅ |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating an API Key for a Developer
|
||||
|
||||
As an admin, you can create API keys for developers who need to integrate external tools:
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Admin login
|
||||
admin_token = login_as_admin()
|
||||
|
||||
# Create API key for developer
|
||||
response = requests.post("http://localhost:8000/auth/api-keys",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={
|
||||
"name": "CI/CD Pipeline Integration",
|
||||
"scopes": ["read:projects", "read:tasks", "write:submissions"],
|
||||
"user_id": 456, # Developer's user ID
|
||||
"expires_at": "2024-12-31T23:59:59"
|
||||
}
|
||||
)
|
||||
|
||||
api_key_data = response.json()
|
||||
print(f"API Key: {api_key_data['token']}")
|
||||
```
|
||||
|
||||
### Monitoring API Usage
|
||||
|
||||
Admins can monitor how API keys are being used:
|
||||
|
||||
```python
|
||||
# Get usage logs for an API key
|
||||
response = requests.get(f"http://localhost:8000/auth/api-keys/123/usage",
|
||||
headers={"Authorization": f"Bearer {admin_token}"}
|
||||
)
|
||||
|
||||
usage_logs = response.json()
|
||||
for log in usage_logs:
|
||||
print(f"{log['timestamp']}: {log['method']} {log['endpoint']} from {log['ip_address']}")
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Principle of Least Privilege**: Only grant the minimum scopes necessary
|
||||
2. **Regular Rotation**: Set expiration dates and rotate API keys regularly
|
||||
3. **Monitor Usage**: Regularly review API key usage logs
|
||||
4. **Revoke Unused Keys**: Delete or deactivate API keys that are no longer needed
|
||||
5. **Secure Distribution**: Share API keys securely and never commit them to version control
|
||||
|
||||
## Testing
|
||||
|
||||
Use the provided test script to verify admin functionality:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python test_admin_api_keys.py
|
||||
```
|
||||
|
||||
This script demonstrates all admin API key management capabilities.
|
||||
@@ -0,0 +1,168 @@
|
||||
# Bulk Actions Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of bulk action endpoints for the task management system. These endpoints allow coordinators and admins to perform batch operations on multiple tasks simultaneously.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Bulk Status Update
|
||||
|
||||
**Endpoint:** `PUT /tasks/bulk/status`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"task_ids": [1, 2, 3],
|
||||
"status": "in_progress"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success_count": 3,
|
||||
"failed_count": 0,
|
||||
"errors": null
|
||||
}
|
||||
```
|
||||
|
||||
**Permissions:**
|
||||
- Coordinators and admins can update any tasks
|
||||
- Artists can only update their own assigned tasks
|
||||
|
||||
**Features:**
|
||||
- Atomic transaction handling - either all tasks update or none
|
||||
- Permission validation for all tasks before making changes
|
||||
- Detailed error reporting for failed tasks
|
||||
|
||||
### 2. Bulk Assignment
|
||||
|
||||
**Endpoint:** `PUT /tasks/bulk/assign`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"task_ids": [1, 2, 3],
|
||||
"assigned_user_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success_count": 3,
|
||||
"failed_count": 0,
|
||||
"errors": null
|
||||
}
|
||||
```
|
||||
|
||||
**Permissions:**
|
||||
- Only coordinators and admins can perform bulk assignments
|
||||
|
||||
**Features:**
|
||||
- Atomic transaction handling
|
||||
- Validates user is a member of all task projects
|
||||
- Sends notifications to assigned user for each task
|
||||
- Detailed error reporting for failed tasks
|
||||
|
||||
## Schemas
|
||||
|
||||
### BulkStatusUpdate
|
||||
```python
|
||||
class BulkStatusUpdate(BaseModel):
|
||||
task_ids: List[int] = Field(..., min_length=1)
|
||||
status: TaskStatus
|
||||
```
|
||||
|
||||
### BulkAssignment
|
||||
```python
|
||||
class BulkAssignment(BaseModel):
|
||||
task_ids: List[int] = Field(..., min_length=1)
|
||||
assigned_user_id: int
|
||||
```
|
||||
|
||||
### BulkActionResult
|
||||
```python
|
||||
class BulkActionResult(BaseModel):
|
||||
success_count: int
|
||||
failed_count: int
|
||||
errors: Optional[List[dict]] = None
|
||||
```
|
||||
|
||||
## Atomicity
|
||||
|
||||
Both endpoints implement atomic transactions:
|
||||
|
||||
1. All tasks are fetched in a single query
|
||||
2. All validations (permissions, project membership) are performed before any changes
|
||||
3. If any validation fails, the transaction is rolled back and no changes are made
|
||||
4. Only if all validations pass are the changes committed
|
||||
|
||||
This ensures that partial updates never occur - either all tasks are updated successfully or none are.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Errors are returned in a structured format:
|
||||
|
||||
```json
|
||||
{
|
||||
"success_count": 1,
|
||||
"failed_count": 2,
|
||||
"errors": [
|
||||
{
|
||||
"task_id": 99,
|
||||
"error": "Task not found"
|
||||
},
|
||||
{
|
||||
"task_id": 100,
|
||||
"error": "Not authorized to update this task"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Common error scenarios:
|
||||
- Task not found
|
||||
- Insufficient permissions
|
||||
- User not a project member (for assignments)
|
||||
|
||||
## Testing
|
||||
|
||||
A comprehensive test script is available at `backend/test_bulk_actions.py` that tests:
|
||||
|
||||
1. Successful bulk status updates
|
||||
2. Successful bulk assignments
|
||||
3. Error handling with invalid task IDs
|
||||
4. Atomicity with mixed valid/invalid IDs
|
||||
5. Permission validation
|
||||
|
||||
Run tests with:
|
||||
```bash
|
||||
python test_bulk_actions.py
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Route Ordering
|
||||
|
||||
The bulk action endpoints are placed BEFORE the `/{task_id}` routes in the router to prevent FastAPI from trying to match "bulk" as a task_id parameter.
|
||||
|
||||
### Transaction Management
|
||||
|
||||
SQLAlchemy's session management is used for transactions:
|
||||
- `db.commit()` commits all changes
|
||||
- `db.rollback()` reverts all changes if errors occur
|
||||
|
||||
### Notifications
|
||||
|
||||
The bulk assignment endpoint sends individual notifications for each assigned task using the existing `notification_service.notify_task_assigned()` function.
|
||||
|
||||
## Requirements Validated
|
||||
|
||||
This implementation satisfies the following requirements from the spec:
|
||||
|
||||
- **4.2**: Bulk status update with atomic transaction handling
|
||||
- **4.4**: Error handling and rollback on failure
|
||||
- **5.3**: Bulk assignment with atomic transaction handling
|
||||
- **5.5**: Error handling and rollback on failure
|
||||
@@ -0,0 +1,172 @@
|
||||
# Custom Task Status Creation Endpoint Implementation
|
||||
|
||||
## Overview
|
||||
Implemented the POST endpoint for creating custom task statuses in projects. This allows coordinators and admins to define project-specific task statuses beyond the built-in system statuses.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Endpoint
|
||||
- **Route**: `POST /projects/{project_id}/task-statuses`
|
||||
- **Status Code**: 201 Created
|
||||
- **Authorization**: Requires coordinator or admin role
|
||||
|
||||
### Features Implemented
|
||||
|
||||
#### 1. Status Name Uniqueness Validation
|
||||
- Validates that the status name is unique within the project
|
||||
- Case-insensitive comparison to prevent duplicates like "In Review" and "in review"
|
||||
- Checks against both existing custom statuses and system statuses
|
||||
- Returns 409 Conflict if duplicate found
|
||||
|
||||
#### 2. Auto-Assign Color from Palette
|
||||
- Defines a default color palette with 10 distinct colors:
|
||||
- Purple (#8B5CF6)
|
||||
- Pink (#EC4899)
|
||||
- Teal (#14B8A6)
|
||||
- Orange (#F97316)
|
||||
- Cyan (#06B6D4)
|
||||
- Lime (#84CC16)
|
||||
- Violet (#A855F7)
|
||||
- Rose (#F43F5E)
|
||||
- Sky (#22D3EE)
|
||||
- Yellow (#FACC15)
|
||||
- Automatically assigns the first unused color from the palette
|
||||
- If all colors are used, cycles back through the palette
|
||||
- Users can override by providing a custom color in hex format
|
||||
|
||||
#### 3. Unique Status ID Generation
|
||||
- Generates unique IDs using UUID4 with format: `custom_{8-char-hex}`
|
||||
- Example: `custom_0c7ba931`
|
||||
- Ensures no ID collisions
|
||||
|
||||
#### 4. JSON Column Updates
|
||||
- Uses SQLAlchemy's `flag_modified()` to properly track changes to JSON columns
|
||||
- Ensures database updates are persisted correctly
|
||||
|
||||
#### 5. Status Ordering
|
||||
- Automatically assigns order based on existing statuses
|
||||
- New statuses are appended to the end (max_order + 1)
|
||||
- Maintains consistent ordering for UI display
|
||||
|
||||
### Request Schema
|
||||
```json
|
||||
{
|
||||
"name": "Ready for Review",
|
||||
"color": "#8B5CF6" // Optional - auto-assigned if not provided
|
||||
}
|
||||
```
|
||||
|
||||
### Response Schema
|
||||
```json
|
||||
{
|
||||
"message": "Custom task status 'Ready for Review' created successfully",
|
||||
"status": {
|
||||
"id": "custom_0c7ba931",
|
||||
"name": "Ready for Review",
|
||||
"color": "#EC4899",
|
||||
"order": 3,
|
||||
"is_default": false
|
||||
},
|
||||
"all_statuses": {
|
||||
"statuses": [...], // All custom statuses
|
||||
"system_statuses": [...], // Built-in system statuses
|
||||
"default_status_id": "not_started"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Rules
|
||||
|
||||
#### Name Validation (via Pydantic schema)
|
||||
- Minimum length: 1 character
|
||||
- Maximum length: 50 characters
|
||||
- Whitespace is trimmed
|
||||
- Cannot be empty after trimming
|
||||
|
||||
#### Color Validation (via Pydantic schema)
|
||||
- Must be valid hex color code format: `#RRGGBB`
|
||||
- Example: `#FF5733`
|
||||
- Normalized to uppercase
|
||||
- Optional field
|
||||
|
||||
### Error Responses
|
||||
|
||||
#### 404 Not Found
|
||||
```json
|
||||
{
|
||||
"detail": "Project not found"
|
||||
}
|
||||
```
|
||||
|
||||
#### 409 Conflict - Duplicate Name
|
||||
```json
|
||||
{
|
||||
"detail": "Status with name 'Ready for Review' already exists in this project"
|
||||
}
|
||||
```
|
||||
|
||||
#### 409 Conflict - System Status Name
|
||||
```json
|
||||
{
|
||||
"detail": "Status name 'In Progress' conflicts with a system status"
|
||||
}
|
||||
```
|
||||
|
||||
#### 422 Unprocessable Entity - Validation Error
|
||||
```json
|
||||
{
|
||||
"detail": "1 validation error for CustomTaskStatusCreate\nname\n String should have at least 1 character..."
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Results
|
||||
All validation and functionality tests passed:
|
||||
|
||||
1. ✅ Create status without color (auto-assigned from palette)
|
||||
2. ✅ Create status with custom color
|
||||
3. ✅ Reject duplicate status names (409 Conflict)
|
||||
4. ✅ Reject empty status names (422 Validation Error)
|
||||
5. ✅ Reject invalid color formats (422 Validation Error)
|
||||
6. ✅ Reject system status name conflicts (409 Conflict)
|
||||
7. ✅ Proper ordering of statuses
|
||||
8. ✅ Unique ID generation
|
||||
9. ✅ JSON column updates with flag_modified
|
||||
|
||||
### Test Files
|
||||
- `backend/test_create_custom_task_status.py` - Comprehensive test suite
|
||||
- `backend/test_custom_status_validation.py` - Validation-focused tests
|
||||
|
||||
## Database Schema
|
||||
The custom task statuses are stored in the `projects.custom_task_statuses` JSON column:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "custom_review",
|
||||
"name": "In Review",
|
||||
"color": "#8B5CF6",
|
||||
"order": 0,
|
||||
"is_default": false
|
||||
},
|
||||
{
|
||||
"id": "custom_blocked",
|
||||
"name": "Blocked",
|
||||
"color": "#DC2626",
|
||||
"order": 1,
|
||||
"is_default": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Requirements Satisfied
|
||||
- ✅ Requirement 1.2: Create new custom task status
|
||||
- ✅ Requirement 1.3: Validate status name uniqueness within project
|
||||
- ✅ Requirement 1.4: Auto-assign color from palette if not provided
|
||||
|
||||
## Next Steps
|
||||
The following endpoints still need to be implemented:
|
||||
- PUT endpoint for updating custom status (Task 5)
|
||||
- DELETE endpoint for deleting custom status (Task 6)
|
||||
- PATCH endpoint for reordering statuses (Task 7)
|
||||
@@ -0,0 +1,137 @@
|
||||
# Custom Task Status DELETE Endpoint Implementation
|
||||
|
||||
## Overview
|
||||
Implemented the DELETE endpoint for removing custom task statuses from projects with comprehensive validation and task reassignment support.
|
||||
|
||||
## Endpoint Details
|
||||
|
||||
### DELETE `/projects/{project_id}/task-statuses/{status_id}`
|
||||
|
||||
**Authentication Required:** Coordinator or Admin
|
||||
|
||||
**Query Parameters:**
|
||||
- `reassign_to_status_id` (optional): Status ID to reassign tasks to if the status being deleted is in use
|
||||
|
||||
**Response:** 200 OK with CustomTaskStatusResponse containing:
|
||||
- Success message
|
||||
- All remaining statuses (system + custom)
|
||||
- Updated default status ID
|
||||
|
||||
## Implementation Features
|
||||
|
||||
### 1. Status In-Use Check
|
||||
- Queries all tasks in the project to check if any use the status being deleted
|
||||
- Returns detailed error (422) if status is in use and no reassignment provided:
|
||||
```json
|
||||
{
|
||||
"error": "Cannot delete status 'X' because it is currently in use by N task(s)",
|
||||
"status_id": "custom_abc123",
|
||||
"status_name": "In Review",
|
||||
"task_count": 5,
|
||||
"task_ids": [1, 2, 3, 4, 5]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Task Reassignment
|
||||
- Supports optional `reassign_to_status_id` query parameter
|
||||
- Validates reassignment target exists (can be system or custom status)
|
||||
- Prevents reassigning to the same status being deleted
|
||||
- Automatically updates all affected tasks to the new status
|
||||
- Includes reassignment count in success message
|
||||
|
||||
### 3. Default Status Management
|
||||
- Detects if the status being deleted is the default status
|
||||
- Automatically assigns the first remaining custom status as the new default
|
||||
- Ensures there's always a default status after deletion
|
||||
|
||||
### 4. Last Status Protection
|
||||
- Prevents deletion of the last custom status
|
||||
- Returns 422 error with clear message
|
||||
- Ensures at least one custom status always remains
|
||||
|
||||
### 5. Error Handling
|
||||
- 404: Status not found
|
||||
- 404: Project not found
|
||||
- 400: Invalid reassignment status ID
|
||||
- 422: Status in use without reassignment
|
||||
- 422: Attempting to delete last status
|
||||
- 500: Database operation failure
|
||||
|
||||
## Code Structure
|
||||
|
||||
```python
|
||||
@router.delete("/{project_id}/task-statuses/{status_id}")
|
||||
async def delete_custom_task_status(
|
||||
project_id: int,
|
||||
status_id: str,
|
||||
reassign_to_status_id: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_coordinator_or_admin)
|
||||
):
|
||||
# 1. Verify project exists
|
||||
# 2. Load custom statuses from JSON
|
||||
# 3. Find status to delete
|
||||
# 4. Prevent deletion of last status
|
||||
# 5. Check if status is in use
|
||||
# 6. Handle reassignment if needed
|
||||
# 7. Auto-assign new default if needed
|
||||
# 8. Update database with flag_modified
|
||||
# 9. Return success response with all statuses
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
Comprehensive test suite in `test_delete_custom_task_status.py`:
|
||||
|
||||
1. ✅ Delete unused custom status
|
||||
2. ✅ Delete status in use without reassignment (error)
|
||||
3. ✅ Delete status in use with reassignment
|
||||
4. ✅ Delete default status (auto-assign new default)
|
||||
5. ✅ Prevent deletion of last status
|
||||
6. ✅ Delete non-existent status (error)
|
||||
|
||||
## Requirements Validation
|
||||
|
||||
✅ **3.1**: Check if status is in use by any tasks
|
||||
✅ **3.2**: Return error with task count and IDs if in use
|
||||
✅ **3.3**: Support optional reassignment of tasks to another status
|
||||
✅ **3.4**: Auto-assign new default if deleting default status
|
||||
✅ **3.5**: Prevent deletion of last status
|
||||
|
||||
## Database Considerations
|
||||
|
||||
- Uses `flag_modified()` for JSON column updates (required for SQLAlchemy to detect changes)
|
||||
- Transactional: All changes (status deletion + task reassignments) happen in one transaction
|
||||
- Rollback on any error to maintain data consistency
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Delete unused status
|
||||
```bash
|
||||
DELETE /projects/1/task-statuses/custom_abc123
|
||||
```
|
||||
|
||||
### Delete status with reassignment
|
||||
```bash
|
||||
DELETE /projects/1/task-statuses/custom_abc123?reassign_to_status_id=not_started
|
||||
```
|
||||
|
||||
### Delete status with reassignment to another custom status
|
||||
```bash
|
||||
DELETE /projects/1/task-statuses/custom_abc123?reassign_to_status_id=custom_xyz789
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
- Works seamlessly with existing custom status endpoints (GET, POST, PUT)
|
||||
- Maintains consistency with system statuses (cannot delete system statuses)
|
||||
- Properly updates the AllTaskStatusesResponse to reflect changes
|
||||
- Frontend can use the returned `all_statuses` to update UI immediately
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for future iterations:
|
||||
- Bulk delete with single reassignment target
|
||||
- Soft delete with archive functionality
|
||||
- Status usage analytics before deletion
|
||||
- Undo/restore deleted statuses
|
||||
@@ -0,0 +1,194 @@
|
||||
# Custom Task Status GET Endpoint Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Implemented the GET endpoint for retrieving all task statuses (system + custom) for a project as part of the custom task status management feature.
|
||||
|
||||
## Endpoint Details
|
||||
|
||||
### GET /projects/{project_id}/task-statuses
|
||||
|
||||
**Description**: Retrieves all task statuses (both system and custom) for a specific project.
|
||||
|
||||
**Authentication**: Required (JWT Bearer token)
|
||||
|
||||
**Authorization**:
|
||||
- Artists: Can only access projects they are members of
|
||||
- Coordinators/Directors/Developers/Admins: Can access all projects
|
||||
|
||||
**Path Parameters**:
|
||||
- `project_id` (int): The ID of the project
|
||||
|
||||
**Response Schema**: `AllTaskStatusesResponse`
|
||||
```json
|
||||
{
|
||||
"statuses": [
|
||||
{
|
||||
"id": "custom_review",
|
||||
"name": "In Review",
|
||||
"color": "#8B5CF6",
|
||||
"order": 0,
|
||||
"is_default": false
|
||||
}
|
||||
],
|
||||
"system_statuses": [
|
||||
{
|
||||
"id": "not_started",
|
||||
"name": "Not Started",
|
||||
"color": "#6B7280",
|
||||
"is_system": true
|
||||
},
|
||||
{
|
||||
"id": "in_progress",
|
||||
"name": "In Progress",
|
||||
"color": "#3B82F6",
|
||||
"is_system": true
|
||||
},
|
||||
{
|
||||
"id": "submitted",
|
||||
"name": "Submitted",
|
||||
"color": "#F59E0B",
|
||||
"is_system": true
|
||||
},
|
||||
{
|
||||
"id": "approved",
|
||||
"name": "Approved",
|
||||
"color": "#10B981",
|
||||
"is_system": true
|
||||
},
|
||||
{
|
||||
"id": "retake",
|
||||
"name": "Retake",
|
||||
"color": "#EF4444",
|
||||
"is_system": true
|
||||
}
|
||||
],
|
||||
"default_status_id": "not_started"
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes**:
|
||||
- `200 OK`: Successfully retrieved task statuses
|
||||
- `403 Forbidden`: User does not have access to the project
|
||||
- `404 Not Found`: Project does not exist
|
||||
|
||||
## System Task Statuses
|
||||
|
||||
The following system statuses are always available:
|
||||
|
||||
| ID | Name | Color | Description |
|
||||
|----|------|-------|-------------|
|
||||
| not_started | Not Started | #6B7280 | Task has not been started |
|
||||
| in_progress | In Progress | #3B82F6 | Task is currently being worked on |
|
||||
| submitted | Submitted | #F59E0B | Work has been submitted for review |
|
||||
| approved | Approved | #10B981 | Work has been approved |
|
||||
| retake | Retake | #EF4444 | Work needs to be redone |
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Location
|
||||
- File: `backend/routers/projects.py`
|
||||
- Function: `get_all_task_statuses()`
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **Project Validation**: Verifies the project exists before returning statuses
|
||||
2. **Access Control**: Enforces role-based access control (artists can only access projects they're members of)
|
||||
3. **System Statuses**: Always returns the 5 built-in system statuses
|
||||
4. **Custom Statuses**: Returns project-specific custom statuses if defined
|
||||
5. **Default Status**: Identifies which status is the default for new tasks
|
||||
6. **JSON Handling**: Properly handles both JSON string and dict formats for custom_task_statuses field
|
||||
|
||||
### Database Schema
|
||||
|
||||
Custom statuses are stored in the `projects` table:
|
||||
- Column: `custom_task_statuses` (JSON)
|
||||
- Format: Array of status objects with id, name, color, order, and is_default fields
|
||||
|
||||
### Access Control Logic
|
||||
|
||||
```python
|
||||
# Artists can only access projects they're members of
|
||||
if current_user.role == UserRole.ARTIST:
|
||||
member = db.query(ProjectMember).filter(
|
||||
ProjectMember.project_id == project_id,
|
||||
ProjectMember.user_id == current_user.id
|
||||
).first()
|
||||
if not member:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this project"
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Files Created
|
||||
|
||||
1. **test_task_statuses.py**: Basic functionality test
|
||||
- Tests retrieval of system statuses
|
||||
- Validates response structure
|
||||
- Verifies all system statuses are present
|
||||
|
||||
2. **test_task_statuses_with_custom.py**: Custom status test
|
||||
- Creates custom statuses in database
|
||||
- Tests retrieval of both system and custom statuses
|
||||
- Validates default status identification
|
||||
|
||||
3. **test_task_statuses_access.py**: Access control test
|
||||
- Tests artist access control (member vs non-member)
|
||||
- Tests coordinator access to all projects
|
||||
|
||||
4. **test_task_statuses_errors.py**: Error handling test
|
||||
- Tests 404 for non-existent projects
|
||||
- Tests 401/403 for unauthorized access
|
||||
|
||||
### Test Results
|
||||
|
||||
All tests passed successfully:
|
||||
- ✅ System statuses correctly returned
|
||||
- ✅ Custom statuses correctly returned
|
||||
- ✅ Default status correctly identified
|
||||
- ✅ Access control working for artists
|
||||
- ✅ Coordinators can access all projects
|
||||
- ✅ 404 returned for non-existent projects
|
||||
- ✅ Unauthorized access properly blocked
|
||||
|
||||
## Requirements Validation
|
||||
|
||||
This implementation satisfies the following requirements:
|
||||
|
||||
- **Requirement 1.1**: ✅ Displays task status information for project settings
|
||||
- **Requirement 9.2**: ✅ Only shows statuses from the task's project (project-specific)
|
||||
|
||||
## Usage Example
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Login
|
||||
response = requests.post(
|
||||
"http://localhost:8000/auth/login",
|
||||
json={"email": "user@example.com", "password": "password"}
|
||||
)
|
||||
token = response.json()["access_token"]
|
||||
|
||||
# Get task statuses for project
|
||||
response = requests.get(
|
||||
"http://localhost:8000/projects/1/task-statuses",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
|
||||
statuses = response.json()
|
||||
print(f"System statuses: {len(statuses['system_statuses'])}")
|
||||
print(f"Custom statuses: {len(statuses['statuses'])}")
|
||||
print(f"Default status: {statuses['default_status_id']}")
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
The following endpoints still need to be implemented:
|
||||
- POST /projects/{project_id}/task-statuses - Create custom status
|
||||
- PUT /projects/{project_id}/task-statuses/{status_id} - Update custom status
|
||||
- DELETE /projects/{project_id}/task-statuses/{status_id} - Delete custom status
|
||||
- PATCH /projects/{project_id}/task-statuses/reorder - Reorder statuses
|
||||
@@ -0,0 +1,115 @@
|
||||
# Custom Task Status Migration - Task 1 Implementation Summary
|
||||
|
||||
## Overview
|
||||
Successfully implemented database schema changes and migration to support custom task statuses in the VFX Project Management System.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Database Schema Updates
|
||||
|
||||
#### Project Model (`backend/models/project.py`)
|
||||
- ✅ Added `custom_task_statuses` JSON column to store project-specific custom statuses
|
||||
- Column initialized with empty array `[]` for all existing projects
|
||||
|
||||
#### Task Model (`backend/models/task.py`)
|
||||
- ✅ Changed `status` field from `Enum(TaskStatus)` to `String`
|
||||
- Updated default value from `TaskStatus.NOT_STARTED` to `"not_started"`
|
||||
- Maintains backward compatibility with existing system statuses
|
||||
|
||||
### 2. Schema Updates (`backend/schemas/task.py`)
|
||||
- ✅ Updated `TaskBase.status` from `TaskStatus` enum to `str` type
|
||||
- ✅ Updated `TaskUpdate.status` from `Optional[TaskStatus]` to `Optional[str]`
|
||||
- ✅ Updated `TaskStatusUpdate.status` from `TaskStatus` to `str`
|
||||
- Changed default value to `"not_started"` (lowercase string)
|
||||
|
||||
### 3. Router Updates
|
||||
Updated all routers to use string values instead of TaskStatus enum:
|
||||
|
||||
#### Assets Router (`backend/routers/assets.py`)
|
||||
- ✅ Changed task creation to use `status="not_started"`
|
||||
- ✅ Updated status initialization in task status aggregation
|
||||
- ✅ Updated status filtering to use string comparison
|
||||
- ✅ Updated status sorting to use string-based status order
|
||||
|
||||
#### Reviews Router (`backend/routers/reviews.py`)
|
||||
- ✅ Updated submission status checks to use `"submitted"` string
|
||||
- ✅ Changed approval status update to `"approved"` string
|
||||
- ✅ Changed retake status update to `"retake"` string
|
||||
|
||||
#### Shots Router (`backend/routers/shots.py`)
|
||||
- ✅ Changed task creation to use `status="not_started"`
|
||||
- ✅ Updated status initialization in task status aggregation
|
||||
- ✅ Updated status filtering to use string comparison
|
||||
- ✅ Updated status sorting to use string-based status order
|
||||
|
||||
### 4. Migration Script (`backend/migrate_custom_task_statuses.py`)
|
||||
Created comprehensive migration script that:
|
||||
- ✅ Adds `custom_task_statuses` column to projects table
|
||||
- ✅ Initializes column with empty array `[]` for existing projects
|
||||
- ✅ Converts existing uppercase enum values to lowercase strings:
|
||||
- `NOT_STARTED` → `not_started`
|
||||
- `IN_PROGRESS` → `in_progress`
|
||||
- `SUBMITTED` → `submitted`
|
||||
- `APPROVED` → `approved`
|
||||
- `RETAKE` → `retake`
|
||||
- ✅ Provides detailed logging of conversion process
|
||||
- ✅ Includes verification steps
|
||||
|
||||
### 5. Test Script (`backend/test_custom_task_status_migration.py`)
|
||||
Created verification test that confirms:
|
||||
- ✅ `custom_task_statuses` column exists in projects table
|
||||
- ✅ Task status column supports string values
|
||||
- ✅ All existing task statuses are valid lowercase strings
|
||||
- ✅ `custom_task_statuses` is initialized as empty array
|
||||
- ✅ Displays task status distribution
|
||||
|
||||
## Migration Results
|
||||
|
||||
### Database Changes
|
||||
```
|
||||
Projects Table:
|
||||
- Added column: custom_task_statuses (TEXT/JSON)
|
||||
|
||||
Tasks Table:
|
||||
- Status column: VARCHAR(11) (already TEXT, no change needed)
|
||||
```
|
||||
|
||||
### Data Conversion
|
||||
Successfully converted 105 tasks:
|
||||
- 91 tasks: `NOT_STARTED` → `not_started`
|
||||
- 9 tasks: `IN_PROGRESS` → `in_progress`
|
||||
- 4 tasks: `RETAKE` → `retake`
|
||||
- 1 task: `APPROVED` → `approved`
|
||||
|
||||
## System Status Values
|
||||
The following system statuses remain available:
|
||||
- `not_started` - Task has not been started
|
||||
- `in_progress` - Task is currently being worked on
|
||||
- `submitted` - Task work has been submitted for review
|
||||
- `approved` - Task has been approved
|
||||
- `retake` - Task requires revisions
|
||||
|
||||
## Backward Compatibility
|
||||
- ✅ All existing tasks continue to work with lowercase string statuses
|
||||
- ✅ System statuses are always available across all projects
|
||||
- ✅ No breaking changes to existing API endpoints
|
||||
- ✅ Frontend can continue using existing status values
|
||||
|
||||
## Next Steps
|
||||
With the database schema and migration complete, the system is ready for:
|
||||
1. Backend API endpoints for custom status CRUD operations (Task 2-8)
|
||||
2. Frontend components for custom status management (Task 10-21)
|
||||
3. Integration with task creation and status update workflows
|
||||
|
||||
## Testing
|
||||
All tests passed successfully:
|
||||
- ✅ Backend imports without errors
|
||||
- ✅ Database schema verification
|
||||
- ✅ Data conversion verification
|
||||
- ✅ Status value validation
|
||||
|
||||
## Requirements Validated
|
||||
- ✅ Requirement 6.1: System statuses remain available
|
||||
- ✅ Requirement 6.2: Backward compatibility maintained
|
||||
- ✅ Requirement 6.3: Existing tasks continue to work
|
||||
- ✅ Requirement 9.1: Database schema supports custom statuses
|
||||
@@ -0,0 +1,381 @@
|
||||
# Custom Task Status Reorder Endpoint
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the PATCH endpoint for reordering custom task statuses within a project. The endpoint allows coordinators and administrators to change the display order of custom task statuses.
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
PATCH /projects/{project_id}/task-statuses/reorder
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
Requires JWT authentication with coordinator or admin role.
|
||||
|
||||
## Request
|
||||
|
||||
### Path Parameters
|
||||
|
||||
- `project_id` (integer, required): The ID of the project
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"status_ids": ["custom_abc123", "custom_def456", "custom_ghi789"]
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `status_ids` (array of strings, required): Ordered list of status IDs in the desired sequence
|
||||
- Must contain all existing custom status IDs for the project
|
||||
- Cannot contain duplicates
|
||||
- Cannot be empty
|
||||
|
||||
## Response
|
||||
|
||||
### Success Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Custom task statuses reordered successfully",
|
||||
"status": null,
|
||||
"all_statuses": {
|
||||
"statuses": [
|
||||
{
|
||||
"id": "custom_abc123",
|
||||
"name": "Review",
|
||||
"color": "#9333EA",
|
||||
"order": 0,
|
||||
"is_default": false
|
||||
},
|
||||
{
|
||||
"id": "custom_def456",
|
||||
"name": "Blocked",
|
||||
"color": "#DC2626",
|
||||
"order": 1,
|
||||
"is_default": true
|
||||
},
|
||||
{
|
||||
"id": "custom_ghi789",
|
||||
"name": "Ready for Delivery",
|
||||
"color": "#059669",
|
||||
"order": 2,
|
||||
"is_default": false
|
||||
}
|
||||
],
|
||||
"system_statuses": [
|
||||
{
|
||||
"id": "not_started",
|
||||
"name": "Not Started",
|
||||
"color": "#6B7280",
|
||||
"is_system": true
|
||||
},
|
||||
{
|
||||
"id": "in_progress",
|
||||
"name": "In Progress",
|
||||
"color": "#3B82F6",
|
||||
"is_system": true
|
||||
},
|
||||
{
|
||||
"id": "submitted",
|
||||
"name": "Submitted",
|
||||
"color": "#F59E0B",
|
||||
"is_system": true
|
||||
},
|
||||
{
|
||||
"id": "approved",
|
||||
"name": "Approved",
|
||||
"color": "#10B981",
|
||||
"is_system": true
|
||||
},
|
||||
{
|
||||
"id": "retake",
|
||||
"name": "Retake",
|
||||
"color": "#EF4444",
|
||||
"is_system": true
|
||||
}
|
||||
],
|
||||
"default_status_id": "custom_def456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
|
||||
#### 400 Bad Request - Missing Status IDs
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Missing status IDs in reorder request: custom_xyz999"
|
||||
}
|
||||
```
|
||||
|
||||
Occurs when the request doesn't include all existing custom status IDs.
|
||||
|
||||
#### 400 Bad Request - Invalid Status IDs
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Status IDs not found: invalid_id_12345"
|
||||
}
|
||||
```
|
||||
|
||||
Occurs when the request includes status IDs that don't exist in the project.
|
||||
|
||||
#### 403 Forbidden
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Insufficient permissions"
|
||||
}
|
||||
```
|
||||
|
||||
Occurs when the user doesn't have coordinator or admin role.
|
||||
|
||||
#### 404 Not Found
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Project not found"
|
||||
}
|
||||
```
|
||||
|
||||
Occurs when the specified project doesn't exist.
|
||||
|
||||
#### 422 Unprocessable Entity - Duplicate IDs
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "1 validation error for CustomTaskStatusReorder\nstatus_ids\n Value error, Status IDs list contains duplicates"
|
||||
}
|
||||
```
|
||||
|
||||
Occurs when the request contains duplicate status IDs.
|
||||
|
||||
#### 422 Unprocessable Entity - Empty List
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "1 validation error for CustomTaskStatusReorder\nstatus_ids\n Value error, Status IDs list cannot be empty"
|
||||
}
|
||||
```
|
||||
|
||||
Occurs when the request contains an empty status_ids array.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Validation
|
||||
|
||||
1. **Project Existence**: Verifies the project exists
|
||||
2. **Permission Check**: Ensures user has coordinator or admin role
|
||||
3. **Complete List**: Validates that all existing custom status IDs are included
|
||||
4. **No Missing IDs**: Ensures no status IDs are omitted
|
||||
5. **No Invalid IDs**: Ensures all provided IDs exist in the project
|
||||
6. **No Duplicates**: Validates the list contains no duplicate IDs (handled by Pydantic schema)
|
||||
|
||||
### Order Update Process
|
||||
|
||||
1. Parse and validate the reorder request
|
||||
2. Retrieve existing custom statuses from the project
|
||||
3. Create a mapping of status_id to status data
|
||||
4. Reorder statuses according to the provided list
|
||||
5. Update the `order` field for each status (0-indexed)
|
||||
6. Save the reordered list to the database
|
||||
7. Use `flag_modified()` to ensure JSON column changes are persisted
|
||||
|
||||
### Database Changes
|
||||
|
||||
- Updates the `custom_task_statuses` JSON column in the `projects` table
|
||||
- Each status object's `order` field is updated to match its position in the new list
|
||||
- Uses SQLAlchemy's `flag_modified()` to ensure JSON column changes are detected
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Python (requests)
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Login and get token
|
||||
response = requests.post(
|
||||
"http://localhost:8000/auth/login",
|
||||
json={"email": "admin@vfx.com", "password": "admin123"}
|
||||
)
|
||||
token = response.json()["access_token"]
|
||||
|
||||
# Reorder statuses
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
data = {
|
||||
"status_ids": [
|
||||
"custom_abc123",
|
||||
"custom_def456",
|
||||
"custom_ghi789"
|
||||
]
|
||||
}
|
||||
|
||||
response = requests.patch(
|
||||
"http://localhost:8000/projects/1/task-statuses/reorder",
|
||||
headers=headers,
|
||||
json=data
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print(f"✅ {result['message']}")
|
||||
print(f"Reordered {len(result['all_statuses']['statuses'])} statuses")
|
||||
else:
|
||||
print(f"❌ Error: {response.json()['detail']}")
|
||||
```
|
||||
|
||||
### JavaScript (fetch)
|
||||
|
||||
```javascript
|
||||
// Assuming you have a token from login
|
||||
const token = "your_jwt_token_here";
|
||||
|
||||
const reorderStatuses = async (projectId, statusIds) => {
|
||||
const response = await fetch(
|
||||
`http://localhost:8000/projects/${projectId}/task-statuses/reorder`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status_ids: statusIds
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('✅', result.message);
|
||||
return result.all_statuses;
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('❌ Error:', error.detail);
|
||||
throw new Error(error.detail);
|
||||
}
|
||||
};
|
||||
|
||||
// Usage
|
||||
const statusIds = [
|
||||
'custom_abc123',
|
||||
'custom_def456',
|
||||
'custom_ghi789'
|
||||
];
|
||||
|
||||
reorderStatuses(1, statusIds)
|
||||
.then(allStatuses => {
|
||||
console.log('New order:', allStatuses.statuses);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to reorder:', error);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Drag-and-Drop Implementation
|
||||
|
||||
The frontend should implement drag-and-drop functionality using a library like `vue-draggable-next`:
|
||||
|
||||
1. Display statuses in their current order
|
||||
2. Allow users to drag statuses to reorder them
|
||||
3. On drop, collect the new order of status IDs
|
||||
4. Call the reorder endpoint with the new order
|
||||
5. Update the UI optimistically or wait for the response
|
||||
6. Handle errors by reverting to the previous order
|
||||
|
||||
### Example Vue Component
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<draggable
|
||||
v-model="statuses"
|
||||
@end="handleReorder"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="status-item">
|
||||
<span class="drag-handle">⋮⋮</span>
|
||||
<span :style="{ color: element.color }">
|
||||
{{ element.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import { reorderTaskStatuses } from '@/services/customTaskStatus';
|
||||
|
||||
const props = defineProps({
|
||||
projectId: Number,
|
||||
initialStatuses: Array
|
||||
});
|
||||
|
||||
const statuses = ref([...props.initialStatuses]);
|
||||
|
||||
const handleReorder = async () => {
|
||||
const statusIds = statuses.value.map(s => s.id);
|
||||
|
||||
try {
|
||||
const result = await reorderTaskStatuses(props.projectId, statusIds);
|
||||
// Update with server response
|
||||
statuses.value = result.all_statuses.statuses;
|
||||
} catch (error) {
|
||||
// Revert to original order on error
|
||||
statuses.value = [...props.initialStatuses];
|
||||
console.error('Failed to reorder:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
A comprehensive test script is available at `backend/test_reorder_custom_task_status.py` that tests:
|
||||
|
||||
1. ✅ Successful reordering (reversing order)
|
||||
2. ✅ Order field updates correctly
|
||||
3. ✅ Rejection of incomplete status lists
|
||||
4. ✅ Rejection of invalid status IDs
|
||||
5. ✅ Rejection of duplicate status IDs
|
||||
|
||||
Run the test with:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python test_reorder_custom_task_status.py
|
||||
```
|
||||
|
||||
## Requirements Validation
|
||||
|
||||
This endpoint satisfies the following requirements from the custom task status specification:
|
||||
|
||||
- **Requirement 4.1**: ✅ Displays statuses in their defined order
|
||||
- **Requirement 4.2**: ✅ Updates the order when user reorders statuses
|
||||
- **Requirement 4.3**: ✅ Updates display order in all dropdowns and filters
|
||||
- **Requirement 4.4**: ✅ Validates all status IDs are present
|
||||
|
||||
## Related Endpoints
|
||||
|
||||
- `GET /projects/{project_id}/task-statuses` - Get all task statuses
|
||||
- `POST /projects/{project_id}/task-statuses` - Create a custom status
|
||||
- `PUT /projects/{project_id}/task-statuses/{status_id}` - Update a custom status
|
||||
- `DELETE /projects/{project_id}/task-statuses/{status_id}` - Delete a custom status
|
||||
|
||||
## Notes
|
||||
|
||||
- System statuses (not_started, in_progress, submitted, approved, retake) cannot be reordered
|
||||
- Only custom statuses can be reordered
|
||||
- The order field is 0-indexed
|
||||
- Reordering does not affect the default status designation
|
||||
- The endpoint uses `flag_modified()` to ensure JSON column changes are persisted to the database
|
||||
@@ -0,0 +1,260 @@
|
||||
# Custom Task Status Update Endpoint
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of the PUT endpoint for updating custom task statuses in a project.
|
||||
|
||||
**Endpoint:** `PUT /projects/{project_id}/task-statuses/{status_id}`
|
||||
|
||||
**Requirements Implemented:**
|
||||
- 2.1: Support updating name
|
||||
- 2.2: Support updating color
|
||||
- 2.3: Support updating is_default flag
|
||||
- 5.2: If setting as default, unset other default statuses
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Endpoint Signature
|
||||
|
||||
```python
|
||||
@router.put("/{project_id}/task-statuses/{status_id}")
|
||||
async def update_custom_task_status(
|
||||
project_id: int,
|
||||
status_id: str,
|
||||
status_update: dict,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_coordinator_or_admin)
|
||||
):
|
||||
```
|
||||
|
||||
### Request Body Schema
|
||||
|
||||
Uses `CustomTaskStatusUpdate` schema:
|
||||
|
||||
```python
|
||||
{
|
||||
"name": "string (optional)", # New status name (1-50 chars)
|
||||
"color": "string (optional)", # Hex color code (e.g., #FF5733)
|
||||
"is_default": "boolean (optional)" # Set as default status
|
||||
}
|
||||
```
|
||||
|
||||
### Response Schema
|
||||
|
||||
Returns `CustomTaskStatusResponse`:
|
||||
|
||||
```python
|
||||
{
|
||||
"message": "string",
|
||||
"status": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"color": "string",
|
||||
"order": "integer",
|
||||
"is_default": "boolean"
|
||||
},
|
||||
"all_statuses": {
|
||||
"statuses": [...], # All custom statuses
|
||||
"system_statuses": [...], # System statuses
|
||||
"default_status_id": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Name Update (Requirement 2.1)
|
||||
|
||||
- Validates name uniqueness within project
|
||||
- Checks against other custom statuses
|
||||
- Checks against system status names
|
||||
- Returns 409 Conflict if name already exists
|
||||
|
||||
```python
|
||||
# Validate name uniqueness if name is being changed
|
||||
if status_update.name is not None and status_update.name != status_to_update.get('name'):
|
||||
# Check against other custom statuses
|
||||
existing_names = [
|
||||
s.get('name', '').lower()
|
||||
for i, s in enumerate(custom_statuses_data)
|
||||
if isinstance(s, dict) and i != status_index
|
||||
]
|
||||
|
||||
# Check against system statuses
|
||||
system_names = [s['name'].lower() for s in SYSTEM_TASK_STATUSES]
|
||||
|
||||
if status_update.name.lower() in existing_names:
|
||||
raise HTTPException(status_code=409, detail="Name already exists")
|
||||
|
||||
if status_update.name.lower() in system_names:
|
||||
raise HTTPException(status_code=409, detail="Conflicts with system status")
|
||||
```
|
||||
|
||||
### 2. Color Update (Requirement 2.2)
|
||||
|
||||
- Accepts hex color codes (e.g., #FF5733)
|
||||
- Validates color format via schema
|
||||
- Updates color independently of other fields
|
||||
|
||||
```python
|
||||
# Update color if provided
|
||||
if status_update.color is not None:
|
||||
status_to_update['color'] = status_update.color
|
||||
```
|
||||
|
||||
### 3. Default Status Management (Requirement 2.3, 5.2)
|
||||
|
||||
- When setting a status as default, automatically unsets all other defaults
|
||||
- Ensures only one default status exists at a time
|
||||
- Allows unsetting default status
|
||||
|
||||
```python
|
||||
# Handle is_default flag
|
||||
if status_update.is_default is not None:
|
||||
if status_update.is_default:
|
||||
# If setting as default, unset other default statuses
|
||||
for status_data in custom_statuses_data:
|
||||
if isinstance(status_data, dict):
|
||||
status_data['is_default'] = False
|
||||
|
||||
# Set this status as default
|
||||
status_to_update['is_default'] = True
|
||||
else:
|
||||
# Just unset this status as default
|
||||
status_to_update['is_default'] = False
|
||||
```
|
||||
|
||||
### 4. JSON Column Updates
|
||||
|
||||
Uses `flag_modified` to ensure SQLAlchemy detects changes to JSON columns:
|
||||
|
||||
```python
|
||||
# Update the status in the list
|
||||
custom_statuses_data[status_index] = status_to_update
|
||||
db_project.custom_task_statuses = custom_statuses_data
|
||||
|
||||
# Use flag_modified for JSON column updates
|
||||
flag_modified(db_project, 'custom_task_statuses')
|
||||
|
||||
db.commit()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### 404 Not Found
|
||||
- Project doesn't exist
|
||||
- Status ID not found in project
|
||||
|
||||
### 409 Conflict
|
||||
- Status name already exists in project
|
||||
- Status name conflicts with system status
|
||||
|
||||
### 403 Forbidden
|
||||
- User is not coordinator or admin
|
||||
|
||||
### 422 Unprocessable Entity
|
||||
- Invalid request body format
|
||||
- Invalid color format
|
||||
- Invalid name length
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Update Status Name
|
||||
|
||||
```bash
|
||||
curl -X PUT "http://localhost:8000/projects/1/task-statuses/custom_abc123" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "New Status Name"}'
|
||||
```
|
||||
|
||||
### Update Status Color
|
||||
|
||||
```bash
|
||||
curl -X PUT "http://localhost:8000/projects/1/task-statuses/custom_abc123" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"color": "#00FF00"}'
|
||||
```
|
||||
|
||||
### Update Both Name and Color
|
||||
|
||||
```bash
|
||||
curl -X PUT "http://localhost:8000/projects/1/task-statuses/custom_abc123" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "Updated Status", "color": "#0000FF"}'
|
||||
```
|
||||
|
||||
### Set as Default Status
|
||||
|
||||
```bash
|
||||
curl -X PUT "http://localhost:8000/projects/1/task-statuses/custom_abc123" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"is_default": true}'
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
To test this endpoint:
|
||||
|
||||
1. Start the backend server:
|
||||
```bash
|
||||
cd backend
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
2. Create a custom status first (if needed):
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/projects/1/task-statuses" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "Test Status", "color": "#FF5733"}'
|
||||
```
|
||||
|
||||
3. Update the status using the examples above
|
||||
|
||||
4. Verify changes by getting all statuses:
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/projects/1/task-statuses" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
## Integration with Frontend
|
||||
|
||||
The frontend can use this endpoint to:
|
||||
|
||||
1. Update status names when users edit them
|
||||
2. Change status colors via color picker
|
||||
3. Set/unset default status via toggle or button
|
||||
4. Update multiple fields at once
|
||||
|
||||
The response includes the updated status and all statuses, allowing the frontend to update its state in a single request.
|
||||
|
||||
## Database Schema
|
||||
|
||||
The custom task statuses are stored in the `projects` table as a JSON column:
|
||||
|
||||
```sql
|
||||
custom_task_statuses JSON -- Array of status objects
|
||||
```
|
||||
|
||||
Each status object has the structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "custom_abc123",
|
||||
"name": "Status Name",
|
||||
"color": "#FF5733",
|
||||
"order": 0,
|
||||
"is_default": false
|
||||
}
|
||||
```
|
||||
|
||||
## Related Endpoints
|
||||
|
||||
- `GET /projects/{project_id}/task-statuses` - Get all statuses
|
||||
- `POST /projects/{project_id}/task-statuses` - Create new status
|
||||
- `DELETE /projects/{project_id}/task-statuses/{status_id}` - Delete status (to be implemented)
|
||||
- `PATCH /projects/{project_id}/task-statuses/reorder` - Reorder statuses (to be implemented)
|
||||
@@ -0,0 +1,231 @@
|
||||
# Data Consistency and Real-time Updates Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of data consistency checks and real-time update propagation for the shot-asset-task-status-optimization feature. The implementation ensures that individual task updates remain consistent with aggregated views and provides real-time update propagation mechanisms.
|
||||
|
||||
## Requirements Addressed
|
||||
|
||||
This implementation addresses the following requirements from the specification:
|
||||
|
||||
- **Requirement 3.3**: Data consistency between individual task updates and aggregated views
|
||||
- **Requirement 4.5**: Real-time update propagation to aggregated data
|
||||
- **Task 14**: Data Consistency and Real-time Updates
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **DataConsistencyService** (`backend/services/data_consistency.py`)
|
||||
- Main service for validating consistency between individual tasks and aggregated data
|
||||
- Provides bulk validation and reporting capabilities
|
||||
- Handles real-time update propagation
|
||||
|
||||
2. **Data Consistency API** (`backend/routers/data_consistency.py`)
|
||||
- REST API endpoints for consistency validation and monitoring
|
||||
- Health check and reporting endpoints
|
||||
- Administrative tools for consistency management
|
||||
|
||||
3. **Task Update Hooks** (integrated into `backend/routers/tasks.py`)
|
||||
- Automatic consistency validation on task status updates
|
||||
- Propagation logging and error handling
|
||||
- Integration with existing task update workflows
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Data Consistency Validation
|
||||
|
||||
The system validates consistency by:
|
||||
|
||||
1. **Fetching Individual Task Records**: Queries all active tasks for a shot or asset
|
||||
2. **Building Expected Aggregated Data**: Constructs the expected task_status and task_details from individual tasks
|
||||
3. **Fetching Actual Aggregated Data**: Uses the optimized queries to get current aggregated data
|
||||
4. **Comparing Results**: Identifies inconsistencies between expected and actual data
|
||||
|
||||
#### Validation Process
|
||||
|
||||
```python
|
||||
def validate_task_aggregation_consistency(self, entity_id: int, entity_type: str) -> Dict[str, Any]:
|
||||
# Get individual task records
|
||||
tasks = self.db.query(Task).filter(conditions).all()
|
||||
|
||||
# Build expected aggregated data
|
||||
expected_task_status = {}
|
||||
expected_task_details = []
|
||||
|
||||
# Get actual aggregated data using optimized queries
|
||||
aggregated_data = self._get_shot_aggregated_data(entity_id) # or asset
|
||||
|
||||
# Compare and identify inconsistencies
|
||||
inconsistencies = []
|
||||
# ... comparison logic
|
||||
|
||||
return {
|
||||
'valid': len(inconsistencies) == 0,
|
||||
'inconsistencies': inconsistencies,
|
||||
# ... additional metadata
|
||||
}
|
||||
```
|
||||
|
||||
### Real-time Update Propagation
|
||||
|
||||
The system ensures real-time consistency through:
|
||||
|
||||
1. **Task Update Hooks**: Automatically triggered on task status changes
|
||||
2. **Consistency Validation**: Validates aggregated data after each update
|
||||
3. **Propagation Logging**: Records all update propagations for monitoring
|
||||
4. **Error Handling**: Logs inconsistencies without failing user operations
|
||||
|
||||
#### Update Propagation Flow
|
||||
|
||||
```python
|
||||
def propagate_task_update(self, task_id: int, old_status: str, new_status: str) -> Dict[str, Any]:
|
||||
# Get task and determine parent entity
|
||||
task = self.db.query(Task).filter(Task.id == task_id).first()
|
||||
|
||||
# Validate consistency after update
|
||||
validation_result = self.validate_task_aggregation_consistency(entity_id, entity_type)
|
||||
|
||||
# Log propagation results
|
||||
propagation_log = {
|
||||
'task_id': task_id,
|
||||
'entity_type': entity_type,
|
||||
'entity_id': entity_id,
|
||||
'old_status': old_status,
|
||||
'new_status': new_status,
|
||||
'consistency_valid': validation_result['valid'],
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
return propagation_log
|
||||
```
|
||||
|
||||
### Integration with Task Updates
|
||||
|
||||
The consistency system is integrated into existing task update endpoints:
|
||||
|
||||
1. **Individual Task Updates** (`PUT /tasks/{task_id}`)
|
||||
2. **Task Status Updates** (`PUT /tasks/{task_id}/status`)
|
||||
3. **Bulk Status Updates** (`PUT /tasks/bulk/status`)
|
||||
|
||||
Each endpoint now includes:
|
||||
- Pre-update status capture
|
||||
- Post-update consistency validation
|
||||
- Propagation logging
|
||||
- Error handling that doesn't disrupt user operations
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Data Consistency Endpoints
|
||||
|
||||
All endpoints are prefixed with `/data-consistency` and require admin or coordinator permissions.
|
||||
|
||||
#### Validation Endpoints
|
||||
|
||||
- `GET /data-consistency/validate/{entity_type}/{entity_id}`
|
||||
- Validate consistency for a specific shot or asset
|
||||
- Returns detailed validation results and any inconsistencies found
|
||||
|
||||
- `POST /data-consistency/validate/bulk`
|
||||
- Validate consistency for multiple entities at once
|
||||
- Supports up to 100 entities per request
|
||||
|
||||
#### Reporting Endpoints
|
||||
|
||||
- `GET /data-consistency/report?project_id={id}`
|
||||
- Generate comprehensive consistency report
|
||||
- Optional project filtering
|
||||
- Returns summary statistics and detailed results
|
||||
|
||||
- `GET /data-consistency/health?project_id={id}`
|
||||
- Quick health check for data consistency
|
||||
- Returns overall system health status
|
||||
- Useful for monitoring and alerting
|
||||
|
||||
#### Management Endpoints
|
||||
|
||||
- `POST /data-consistency/propagate/{task_id}`
|
||||
- Manually trigger update propagation for a task
|
||||
- Useful for debugging and maintenance
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
The implementation includes comprehensive unit tests:
|
||||
|
||||
- **test_data_consistency.py**: Core functionality testing
|
||||
- Data consistency validation
|
||||
- Real-time update propagation
|
||||
- Consistency reporting
|
||||
- Bulk validation operations
|
||||
|
||||
### API Integration Tests
|
||||
|
||||
- **test_data_consistency_api.py**: API endpoint testing
|
||||
- Authentication and authorization
|
||||
- Endpoint functionality
|
||||
- Error handling
|
||||
- Response format validation
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run core functionality tests
|
||||
cd backend
|
||||
python test_data_consistency.py
|
||||
|
||||
# Run API integration tests (requires running server)
|
||||
python test_data_consistency_api.py
|
||||
```
|
||||
|
||||
## Monitoring and Maintenance
|
||||
|
||||
### Consistency Health Monitoring
|
||||
|
||||
The system provides several monitoring capabilities:
|
||||
|
||||
1. **Health Check Endpoint**: Quick status overview
|
||||
2. **Detailed Reports**: Comprehensive consistency analysis
|
||||
3. **Propagation Logging**: Audit trail of all updates
|
||||
4. **Error Logging**: Automatic logging of consistency issues
|
||||
|
||||
### Maintenance Operations
|
||||
|
||||
1. **Bulk Validation**: Validate consistency across multiple entities
|
||||
2. **Manual Propagation**: Force update propagation for specific tasks
|
||||
3. **Consistency Reports**: Generate detailed analysis reports
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Consistency validation uses the same optimized queries as the main system
|
||||
- Bulk operations are limited to prevent performance impact
|
||||
- Validation is performed asynchronously to avoid blocking user operations
|
||||
- Logging is designed to be lightweight and non-intrusive
|
||||
|
||||
## Error Handling
|
||||
|
||||
The system is designed to be resilient:
|
||||
|
||||
1. **Non-blocking Operations**: Consistency issues don't prevent task updates
|
||||
2. **Graceful Degradation**: System continues to function even with consistency problems
|
||||
3. **Comprehensive Logging**: All issues are logged for investigation
|
||||
4. **Recovery Mechanisms**: Manual tools available for fixing inconsistencies
|
||||
|
||||
## Configuration
|
||||
|
||||
The data consistency system requires no additional configuration and integrates seamlessly with the existing system. All settings use the same database connection and authentication mechanisms as the main application.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for future versions:
|
||||
|
||||
1. **Automated Repair**: Automatic fixing of detected inconsistencies
|
||||
2. **Real-time Notifications**: Alert administrators of consistency issues
|
||||
3. **Performance Metrics**: Detailed performance monitoring and optimization
|
||||
4. **Batch Processing**: Scheduled consistency validation jobs
|
||||
5. **Custom Validation Rules**: Project-specific consistency requirements
|
||||
|
||||
## Conclusion
|
||||
|
||||
The data consistency implementation provides robust validation and monitoring capabilities while maintaining system performance and reliability. It ensures that the optimized query system continues to provide accurate data while offering tools for monitoring and maintaining data integrity over time.
|
||||
@@ -0,0 +1,221 @@
|
||||
# Default Asset Task Creation Implementation
|
||||
|
||||
## Overview
|
||||
This document describes the implementation of automatic default task creation for assets based on their category (Task 5.3).
|
||||
|
||||
## Requirements Implemented
|
||||
- **17.1**: Automatic task generation when assets are created based on asset category
|
||||
- **17.2**: Default task templates for each asset category (modeling, surfacing, rigging)
|
||||
- **17.3**: Customizable task creation options for coordinators
|
||||
- **17.4**: Default task naming conventions
|
||||
- **17.5**: API endpoint for retrieving default tasks by asset category
|
||||
- **17.6**: Proper task naming
|
||||
- **17.7**: Unassigned task creation
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### Default Task Templates
|
||||
Located in `backend/routers/assets.py`:
|
||||
|
||||
```python
|
||||
DEFAULT_ASSET_TASKS = {
|
||||
AssetCategory.CHARACTERS: ["modeling", "surfacing", "rigging"],
|
||||
AssetCategory.PROPS: ["modeling", "surfacing"],
|
||||
AssetCategory.SETS: ["modeling", "surfacing"],
|
||||
AssetCategory.VEHICLES: ["modeling", "surfacing", "rigging"]
|
||||
}
|
||||
```
|
||||
|
||||
### Key Functions
|
||||
|
||||
#### `get_default_asset_task_types(category: AssetCategory) -> List[str]`
|
||||
Returns the default task types for a given asset category.
|
||||
|
||||
#### `get_all_asset_task_types(project_id: int, db: Session) -> List[str]`
|
||||
Returns all task types (standard + custom) for assets in a project.
|
||||
|
||||
#### `create_default_tasks_for_asset(asset: Asset, task_types: List[str], db: Session) -> List[Task]`
|
||||
Creates default tasks for an asset with proper naming conventions:
|
||||
- Task name format: `{asset_name} - {task_type.title()}`
|
||||
- Tasks are created with status `NOT_STARTED`
|
||||
- Tasks are left unassigned (assigned_user_id = None)
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### `GET /assets/default-tasks/{category}`
|
||||
Returns default task types for an asset category.
|
||||
|
||||
**Query Parameters:**
|
||||
- `project_id` (optional): Include custom task types for the project
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
["modeling", "surfacing", "rigging"]
|
||||
```
|
||||
|
||||
#### `POST /assets/?project_id={project_id}`
|
||||
Creates a new asset with optional default tasks.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Hero Character",
|
||||
"category": "characters",
|
||||
"description": "Main character asset",
|
||||
"status": "not_started",
|
||||
"create_default_tasks": true,
|
||||
"selected_task_types": ["modeling", "surfacing", "rigging"]
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `create_default_tasks` (boolean): Whether to create default tasks (default: true)
|
||||
- `selected_task_types` (array, optional): Specific task types to create. If not provided, uses category defaults.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Hero Character",
|
||||
"category": "characters",
|
||||
"status": "not_started",
|
||||
"project_id": 1,
|
||||
"task_count": 3,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### AssetForm Component
|
||||
Located in `frontend/src/components/asset/AssetForm.vue`
|
||||
|
||||
**Features:**
|
||||
1. **Default Tasks Toggle**: Checkbox to enable/disable default task creation
|
||||
2. **Task Preview**: Shows which tasks will be created based on category
|
||||
3. **Custom Task Selection**: Allows coordinators to select specific tasks to create
|
||||
4. **Confirmation Dialog**: Shows preview of tasks before creation
|
||||
|
||||
**Key Sections:**
|
||||
```vue
|
||||
<!-- Default Tasks Section -->
|
||||
<div v-if="!isEdit" class="space-y-4 border-t pt-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="create-default-tasks"
|
||||
v-model:checked="formData.create_default_tasks"
|
||||
/>
|
||||
<Label>Create default tasks for this asset</Label>
|
||||
</div>
|
||||
|
||||
<!-- Task Selection -->
|
||||
<div v-if="formData.create_default_tasks && formData.category">
|
||||
<div v-for="taskType in defaultTasks" :key="taskType">
|
||||
<Checkbox
|
||||
:checked="selectedTaskTypes.includes(taskType)"
|
||||
@update:checked="toggleTaskType(taskType)"
|
||||
/>
|
||||
<Label>{{ formatTaskType(taskType) }}</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Asset Service
|
||||
Located in `frontend/src/services/asset.ts`
|
||||
|
||||
**Methods:**
|
||||
```typescript
|
||||
async getDefaultTasksForCategory(
|
||||
category: AssetCategory,
|
||||
projectId?: number
|
||||
): Promise<string[]>
|
||||
|
||||
async createAsset(
|
||||
projectId: number,
|
||||
data: AssetCreate
|
||||
): Promise<Asset>
|
||||
```
|
||||
|
||||
## Task Naming Convention
|
||||
Tasks are automatically named using the format:
|
||||
```
|
||||
{Asset Name} - {Task Type}
|
||||
```
|
||||
|
||||
Examples:
|
||||
- "Hero Character - Modeling"
|
||||
- "Hero Character - Surfacing"
|
||||
- "Hero Character - Rigging"
|
||||
- "Sword Prop - Modeling"
|
||||
- "Sword Prop - Surfacing"
|
||||
|
||||
## Task Assignment
|
||||
All default tasks are created **unassigned** (assigned_user_id = null). Coordinators must manually assign tasks to artists after creation.
|
||||
|
||||
## Category-Specific Defaults
|
||||
|
||||
| Category | Default Tasks |
|
||||
|------------|-----------------------------------|
|
||||
| Characters | Modeling, Surfacing, Rigging |
|
||||
| Props | Modeling, Surfacing |
|
||||
| Sets | Modeling, Surfacing |
|
||||
| Vehicles | Modeling, Surfacing, Rigging |
|
||||
|
||||
## Custom Task Types
|
||||
The system supports custom task types per project. When a project has custom asset task types defined, they are included in the available task types for selection during asset creation.
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend Test
|
||||
Run `backend/test_default_asset_tasks.py` to verify:
|
||||
- Default task templates for each category
|
||||
- Asset creation with default tasks
|
||||
- Task naming conventions
|
||||
- Custom task selection
|
||||
- Unassigned task creation
|
||||
|
||||
### Frontend Test
|
||||
Open `frontend/test-default-asset-tasks.html` to test:
|
||||
- Default task retrieval
|
||||
- Asset creation with task selection
|
||||
- Task verification
|
||||
|
||||
## Usage Example
|
||||
|
||||
### Creating an Asset with Default Tasks
|
||||
1. Navigate to Assets section in a project
|
||||
2. Click "Create Asset"
|
||||
3. Fill in asset details (name, category, description)
|
||||
4. Ensure "Create default tasks" is checked
|
||||
5. Review the task preview
|
||||
6. Optionally customize which tasks to create
|
||||
7. Click "Create Asset"
|
||||
8. Confirm task creation in the dialog
|
||||
|
||||
### Creating an Asset without Default Tasks
|
||||
1. Follow steps 1-3 above
|
||||
2. Uncheck "Create default tasks"
|
||||
3. Click "Create Asset"
|
||||
4. Asset is created with no tasks
|
||||
|
||||
### Custom Task Selection
|
||||
1. Follow steps 1-4 above
|
||||
2. Uncheck specific tasks you don't want to create
|
||||
3. Click "Create Asset"
|
||||
4. Only selected tasks will be created
|
||||
|
||||
## Integration with Custom Task Types
|
||||
When custom task types are defined for a project (via Task 19), they are automatically included in the available task types for asset creation. The default task templates remain the same, but coordinators can select custom task types during asset creation.
|
||||
|
||||
## Error Handling
|
||||
- Validates that selected task types are valid (standard or custom)
|
||||
- Prevents duplicate asset names within a project
|
||||
- Returns appropriate error messages for invalid requests
|
||||
- Handles missing project or category gracefully
|
||||
|
||||
## Performance Considerations
|
||||
- Tasks are created in a single database transaction
|
||||
- Bulk task creation is efficient using SQLAlchemy's bulk operations
|
||||
- Task count is returned immediately after creation without additional queries
|
||||
@@ -0,0 +1,155 @@
|
||||
# FastAPI Trailing Slash Issue - 307 Redirect & 403 Forbidden
|
||||
|
||||
## Problem Description
|
||||
|
||||
When making authenticated API calls to FastAPI endpoints, a mismatch in trailing slashes between the frontend request and backend route definition causes a **307 Temporary Redirect** that **loses the authentication header**, resulting in a **403 Forbidden** error.
|
||||
|
||||
## Root Cause
|
||||
|
||||
FastAPI automatically redirects requests to add or remove trailing slashes to match the route definition:
|
||||
- If route is defined as `@router.get("/tasks/")` (with slash) and you call `/tasks` (without slash) → 307 redirect to `/tasks/`
|
||||
- If route is defined as `@router.get("/tasks")` (without slash) and you call `/tasks/` (with slash) → 307 redirect to `/tasks`
|
||||
|
||||
**The problem:** HTTP redirects (307) do NOT preserve the `Authorization` header by default, so the redirected request arrives without authentication, causing a 403 Forbidden error.
|
||||
|
||||
## Symptoms
|
||||
|
||||
### Backend Logs
|
||||
```
|
||||
INFO: 127.0.0.1:59653 - "GET /tasks?shot_id=12 HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:59615 - "GET /tasks/?shot_id=12 HTTP/1.1" 403 Forbidden
|
||||
```
|
||||
|
||||
### Frontend Console
|
||||
```
|
||||
GET http://localhost:8000/tasks/?shot_id=12 403 (Forbidden)
|
||||
AxiosError {message: 'Request failed with status code 403', ...}
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
**Always ensure trailing slashes match between frontend API calls and backend route definitions.**
|
||||
|
||||
### Option 1: Add Trailing Slash to Frontend (Recommended)
|
||||
|
||||
**Frontend Service:**
|
||||
```typescript
|
||||
// ❌ WRONG - No trailing slash
|
||||
const response = await apiClient.get(`/tasks?${params}`)
|
||||
|
||||
// ✅ CORRECT - With trailing slash
|
||||
const response = await apiClient.get(`/tasks/?${params}`)
|
||||
```
|
||||
|
||||
**Backend Route:**
|
||||
```python
|
||||
# Route defined WITH trailing slash
|
||||
@router.get("/tasks/")
|
||||
async def get_tasks(...):
|
||||
...
|
||||
```
|
||||
|
||||
### Option 2: Remove Trailing Slash from Backend
|
||||
|
||||
**Backend Route:**
|
||||
```python
|
||||
# Route defined WITHOUT trailing slash
|
||||
@router.get("/tasks")
|
||||
async def get_tasks(...):
|
||||
...
|
||||
```
|
||||
|
||||
**Frontend Service:**
|
||||
```typescript
|
||||
// Call WITHOUT trailing slash
|
||||
const response = await apiClient.get(`/tasks?${params}`)
|
||||
```
|
||||
|
||||
## Prevention Checklist
|
||||
|
||||
When adding or modifying routes, **always check**:
|
||||
|
||||
1. **Backend Route Definition** - Does it have a trailing slash?
|
||||
```python
|
||||
@router.get("/endpoint/") # Has trailing slash
|
||||
@router.get("/endpoint") # No trailing slash
|
||||
```
|
||||
|
||||
2. **Frontend API Call** - Does it match the backend?
|
||||
```typescript
|
||||
apiClient.get(`/endpoint/`) // Has trailing slash
|
||||
apiClient.get(`/endpoint`) // No trailing slash
|
||||
```
|
||||
|
||||
3. **Query Parameters** - Trailing slash goes BEFORE the `?`
|
||||
```typescript
|
||||
// ✅ CORRECT
|
||||
apiClient.get(`/tasks/?shot_id=12`)
|
||||
|
||||
// ❌ WRONG
|
||||
apiClient.get(`/tasks?shot_id=12/`)
|
||||
```
|
||||
|
||||
4. **Path Parameters** - Usually no trailing slash
|
||||
```typescript
|
||||
// ✅ CORRECT
|
||||
apiClient.get(`/tasks/${taskId}`)
|
||||
|
||||
// ❌ WRONG (usually)
|
||||
apiClient.get(`/tasks/${taskId}/`)
|
||||
```
|
||||
|
||||
## Historical Issues Fixed
|
||||
|
||||
### Issue 1: Shots Endpoint (Fixed)
|
||||
- **Problem:** Frontend called `/shots/1/` but backend defined `/shots/{shot_id}`
|
||||
- **Solution:** Changed frontend to call `/shots/1` (no trailing slash)
|
||||
- **Files:** `frontend/src/services/shot.ts`
|
||||
|
||||
### Issue 2: Tasks Endpoint (Fixed)
|
||||
- **Problem:** Frontend called `/tasks?shot_id=12` but backend defined `/tasks/`
|
||||
- **Solution:** Changed frontend to call `/tasks/?shot_id=12` (with trailing slash)
|
||||
- **Files:** `frontend/src/services/task.ts`
|
||||
|
||||
## Testing
|
||||
|
||||
To verify there's no redirect issue:
|
||||
|
||||
1. **Check backend logs** - Should see only ONE request, not two:
|
||||
```
|
||||
✅ GOOD:
|
||||
INFO: "GET /tasks/?shot_id=12 HTTP/1.1" 200 OK
|
||||
|
||||
❌ BAD (redirect happening):
|
||||
INFO: "GET /tasks?shot_id=12 HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: "GET /tasks/?shot_id=12 HTTP/1.1" 403 Forbidden
|
||||
```
|
||||
|
||||
2. **Check frontend network tab** - Should see 200 OK, not 307 or 403
|
||||
|
||||
3. **Test with authentication** - Ensure authenticated endpoints work correctly
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Common Patterns
|
||||
|
||||
| Endpoint Type | Backend Route | Frontend Call |
|
||||
|--------------|---------------|---------------|
|
||||
| List with query params | `@router.get("/items/")` | `get("/items/?param=value")` |
|
||||
| Get by ID | `@router.get("/items/{id}")` | `get("/items/123")` |
|
||||
| Create | `@router.post("/items/")` | `post("/items/", data)` |
|
||||
| Update by ID | `@router.put("/items/{id}")` | `put("/items/123", data)` |
|
||||
| Delete by ID | `@router.delete("/items/{id}")` | `delete("/items/123")` |
|
||||
|
||||
## Related Files
|
||||
|
||||
- Backend routes: `backend/routers/*.py`
|
||||
- Frontend services: `frontend/src/services/*.ts`
|
||||
- API client: `frontend/src/services/api.ts`
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- This issue only affects authenticated endpoints because the `Authorization` header is lost during redirect
|
||||
- Public endpoints might not show this issue as clearly
|
||||
- Always test with actual authentication tokens, not just in development mode
|
||||
- Consider adding a linter rule or pre-commit hook to check for trailing slash consistency
|
||||
@@ -0,0 +1,341 @@
|
||||
# File Path Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers the database migration scripts created to convert absolute file paths to relative paths in the VFX Project Management System. This migration addresses the issue where absolute paths stored in the database become invalid when deploying to different environments, particularly Linux.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The system previously stored absolute file paths like:
|
||||
- `D:\Repo\LinkDesk\backend\uploads\submissions\98\v001_render.jpg`
|
||||
- `/home/user/vfx-system/backend/uploads/attachments/123/reference.pdf`
|
||||
|
||||
These paths become invalid when:
|
||||
- Deploying to different operating systems
|
||||
- Moving the backend directory
|
||||
- Running in containerized environments
|
||||
|
||||
## Solution
|
||||
|
||||
Convert all absolute paths to relative paths:
|
||||
- `uploads/submissions/98/v001_render.jpg`
|
||||
- `uploads/attachments/123/reference.pdf`
|
||||
- `uploads/project_thumbnails/project_1_thumbnail.jpg`
|
||||
|
||||
## Migration Scripts
|
||||
|
||||
### 1. `migrate_file_paths_to_relative.py`
|
||||
|
||||
**Purpose**: Basic migration script for development use.
|
||||
|
||||
**Features**:
|
||||
- Converts absolute paths to relative paths in all three tables
|
||||
- Basic validation and error handling
|
||||
- Interactive confirmation prompt
|
||||
- Detailed logging
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
cd backend
|
||||
python migrate_file_paths_to_relative.py
|
||||
```
|
||||
|
||||
### 2. `migrate_file_paths_production.py`
|
||||
|
||||
**Purpose**: Production-ready migration script with comprehensive validation.
|
||||
|
||||
**Features**:
|
||||
- Automatic database backup creation
|
||||
- Pre-migration analysis of problematic paths
|
||||
- Enhanced error handling and recovery
|
||||
- Comprehensive validation of results
|
||||
- Detailed logging with timestamps
|
||||
- Support for multiple absolute path patterns
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
cd backend
|
||||
python migrate_file_paths_production.py
|
||||
```
|
||||
|
||||
### 3. `validate_migration_results.py`
|
||||
|
||||
**Purpose**: Validation script to verify migration success.
|
||||
|
||||
**Features**:
|
||||
- Validates all paths are now relative
|
||||
- Checks file accessibility
|
||||
- Verifies path format consistency
|
||||
- Comprehensive reporting
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
cd backend
|
||||
python validate_migration_results.py
|
||||
```
|
||||
|
||||
## Tables Affected
|
||||
|
||||
### 1. `submissions` table
|
||||
- **Column**: `file_path`
|
||||
- **Purpose**: Stores paths to user-submitted work files
|
||||
- **Example conversion**:
|
||||
- Before: `D:\Repo\LinkDesk\backend\uploads\submissions\98\v001_render.jpg`
|
||||
- After: `uploads/submissions/98/v001_render.jpg`
|
||||
|
||||
### 2. `task_attachments` table
|
||||
- **Column**: `file_path`
|
||||
- **Purpose**: Stores paths to task attachment files
|
||||
- **Example conversion**:
|
||||
- Before: `D:\Repo\LinkDesk\backend\uploads\attachments\98\reference.pdf`
|
||||
- After: `uploads/attachments/98/reference.pdf`
|
||||
|
||||
### 3. `projects` table
|
||||
- **Column**: `thumbnail_path`
|
||||
- **Purpose**: Stores paths to project thumbnail images
|
||||
- **Example conversion**:
|
||||
- Before: `D:\Repo\LinkDesk\backend\uploads\project_thumbnails\project_1_thumb.jpg`
|
||||
- After: `uploads/project_thumbnails/project_1_thumb.jpg`
|
||||
|
||||
## Migration Process
|
||||
|
||||
### Pre-Migration Steps
|
||||
|
||||
1. **Backup Database**:
|
||||
```bash
|
||||
cp database.db database_backup_$(date +%Y%m%d_%H%M%S).db
|
||||
```
|
||||
|
||||
2. **Verify Current State**:
|
||||
```bash
|
||||
python validate_migration_results.py
|
||||
```
|
||||
|
||||
### Running Migration
|
||||
|
||||
1. **Development Environment**:
|
||||
```bash
|
||||
python migrate_file_paths_to_relative.py
|
||||
```
|
||||
|
||||
2. **Production Environment**:
|
||||
```bash
|
||||
python migrate_file_paths_production.py
|
||||
```
|
||||
|
||||
### Post-Migration Validation
|
||||
|
||||
1. **Validate Results**:
|
||||
```bash
|
||||
python validate_migration_results.py
|
||||
```
|
||||
|
||||
2. **Test File Serving**:
|
||||
- Start the backend server
|
||||
- Test thumbnail URLs in the frontend
|
||||
- Verify file downloads work correctly
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
||||
1. **File Not Found**:
|
||||
- **Issue**: Original file doesn't exist at expected location
|
||||
- **Solution**: Migration logs the issue but continues processing
|
||||
- **Action**: Review log files and manually fix missing files
|
||||
|
||||
2. **Path Conversion Failure**:
|
||||
- **Issue**: Cannot determine relative path from absolute path
|
||||
- **Solution**: Migration attempts multiple conversion strategies
|
||||
- **Action**: Review problematic paths in log files
|
||||
|
||||
3. **Database Transaction Failure**:
|
||||
- **Issue**: Database error during migration
|
||||
- **Solution**: Automatic rollback preserves data integrity
|
||||
- **Action**: Fix underlying issue and re-run migration
|
||||
|
||||
### Recovery Procedures
|
||||
|
||||
1. **Restore from Backup**:
|
||||
```bash
|
||||
cp database_backup_YYYYMMDD_HHMMSS.db database.db
|
||||
```
|
||||
|
||||
2. **Partial Migration Recovery**:
|
||||
- Migration is transactional per table
|
||||
- Individual table failures don't affect other tables
|
||||
- Re-run migration to complete partial migrations
|
||||
|
||||
## Validation Criteria
|
||||
|
||||
### Success Criteria
|
||||
|
||||
1. **All paths are relative**: No absolute paths remain in database
|
||||
2. **Files are accessible**: All relative paths resolve to existing files
|
||||
3. **Format consistency**: All paths follow expected format patterns
|
||||
4. **No data loss**: All original file references are preserved
|
||||
|
||||
### Validation Checks
|
||||
|
||||
1. **Path Format Validation**:
|
||||
- Submissions: `uploads/submissions/[task_id]/[filename]`
|
||||
- Attachments: `uploads/attachments/[task_id]/[filename]`
|
||||
- Projects: `uploads/project_thumbnails/[filename]`
|
||||
|
||||
2. **File Accessibility**:
|
||||
- All relative paths resolve to existing files
|
||||
- File permissions allow read access
|
||||
|
||||
3. **Database Integrity**:
|
||||
- No NULL values introduced
|
||||
- All foreign key relationships preserved
|
||||
- No duplicate entries created
|
||||
|
||||
## Logging and Monitoring
|
||||
|
||||
### Log Files
|
||||
|
||||
- **Development**: `migration_file_paths.log`
|
||||
- **Production**: `migration_file_paths_YYYYMMDD_HHMMSS.log`
|
||||
|
||||
### Log Levels
|
||||
|
||||
- **INFO**: Normal migration progress
|
||||
- **WARNING**: Non-critical issues (missing files, etc.)
|
||||
- **ERROR**: Critical issues that prevent migration
|
||||
|
||||
### Monitoring Points
|
||||
|
||||
1. **Conversion Statistics**:
|
||||
- Total files processed
|
||||
- Files successfully converted
|
||||
- Files skipped (already relative)
|
||||
- Errors encountered
|
||||
|
||||
2. **Validation Results**:
|
||||
- Path format compliance
|
||||
- File accessibility
|
||||
- Database integrity
|
||||
|
||||
## Integration with FileHandler
|
||||
|
||||
### Updated FileHandler Behavior
|
||||
|
||||
After migration, the FileHandler class should be updated to:
|
||||
|
||||
1. **Store only relative paths** in database
|
||||
2. **Resolve relative paths** to absolute paths for file operations
|
||||
3. **Handle both formats** during transition period
|
||||
|
||||
### Code Changes Required
|
||||
|
||||
```python
|
||||
# Before migration
|
||||
file_path = "/absolute/path/to/file.jpg"
|
||||
|
||||
# After migration
|
||||
file_path = "uploads/submissions/123/file.jpg"
|
||||
absolute_path = file_handler.resolve_absolute_path(file_path)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
1. **File Upload**: Verify new uploads store relative paths
|
||||
2. **File Serving**: Verify existing files serve correctly
|
||||
3. **Thumbnail Generation**: Verify thumbnails work with relative paths
|
||||
4. **Cross-Platform**: Test on different operating systems
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
# Test file serving endpoints
|
||||
curl http://localhost:8000/files/submissions/98/v001_render.jpg
|
||||
|
||||
# Test thumbnail URLs
|
||||
curl http://localhost:8000/files/thumbnails/v001_render_thumb.jpg
|
||||
|
||||
# Test project thumbnails
|
||||
curl http://localhost:8000/files/project_thumbnails/project_1_thumb.jpg
|
||||
```
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
|
||||
- [ ] Database backup created
|
||||
- [ ] Migration scripts tested in staging
|
||||
- [ ] Validation scripts ready
|
||||
- [ ] Rollback procedure documented
|
||||
|
||||
### During Deployment
|
||||
|
||||
- [ ] Stop application services
|
||||
- [ ] Run migration script
|
||||
- [ ] Validate migration results
|
||||
- [ ] Update FileHandler code
|
||||
- [ ] Start application services
|
||||
|
||||
### Post-Deployment
|
||||
|
||||
- [ ] Test file serving endpoints
|
||||
- [ ] Verify thumbnail URLs work
|
||||
- [ ] Monitor application logs
|
||||
- [ ] Confirm no absolute paths in new uploads
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Problems
|
||||
|
||||
1. **Migration appears to do nothing**:
|
||||
- Check if paths are already relative
|
||||
- Review log files for details
|
||||
|
||||
2. **Files not accessible after migration**:
|
||||
- Verify file permissions
|
||||
- Check if files were moved or deleted
|
||||
- Validate relative path resolution
|
||||
|
||||
3. **Thumbnails not displaying**:
|
||||
- Check thumbnail file existence
|
||||
- Verify thumbnail path format
|
||||
- Test thumbnail serving endpoints
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check current path formats
|
||||
python -c "
|
||||
from models.task import Submission
|
||||
from database import SessionLocal
|
||||
db = SessionLocal()
|
||||
for s in db.query(Submission).limit(5):
|
||||
print(f'ID: {s.id}, Path: {s.file_path}')
|
||||
"
|
||||
|
||||
# Test path resolution
|
||||
python -c "
|
||||
from utils.file_handler import file_handler
|
||||
from pathlib import Path
|
||||
backend_dir = Path(__file__).parent
|
||||
test_path = 'uploads/submissions/98/test.jpg'
|
||||
resolved = backend_dir / test_path
|
||||
print(f'Resolved: {resolved}')
|
||||
print(f'Exists: {resolved.exists()}')
|
||||
"
|
||||
```
|
||||
|
||||
## Requirements Addressed
|
||||
|
||||
This migration addresses the following requirements from the specification:
|
||||
|
||||
- **1.1**: Convert absolute paths to relative paths in submissions table
|
||||
- **1.2**: Convert absolute paths to relative paths in task_attachments table
|
||||
- **1.3**: Convert absolute paths to relative paths in projects table (thumbnail_path)
|
||||
- **1.4**: Add validation and error handling for problematic paths
|
||||
- **1.5**: Ensure all file paths in database are relative to backend directory
|
||||
|
||||
## Conclusion
|
||||
|
||||
The file path migration scripts provide a comprehensive solution for converting absolute file paths to relative paths, ensuring cross-platform compatibility and system portability. The migration includes robust error handling, validation, and recovery procedures suitable for both development and production environments.
|
||||
@@ -0,0 +1,246 @@
|
||||
# File Handling System Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The VFX Project Management System includes a comprehensive file handling system that provides secure file upload, storage, serving, and access control for task attachments and work submissions.
|
||||
|
||||
## Features
|
||||
|
||||
### File Upload System
|
||||
- **Secure file validation** with format and size restrictions
|
||||
- **Organized directory structure** for efficient file storage
|
||||
- **Unique filename generation** to prevent conflicts
|
||||
- **Automatic thumbnail generation** for image files
|
||||
- **Version control** for submissions
|
||||
|
||||
### File Serving and Access Control
|
||||
- **Authenticated file serving** with role-based permissions
|
||||
- **Thumbnail serving** for quick previews
|
||||
- **Video streaming** for media playback
|
||||
- **File information API** for metadata access
|
||||
|
||||
## Supported File Formats
|
||||
|
||||
### Video Formats
|
||||
- `.mov` - QuickTime Movie
|
||||
- `.mp4` - MPEG-4 Video
|
||||
- `.avi` - Audio Video Interleave
|
||||
- `.mkv` - Matroska Video
|
||||
- `.webm` - WebM Video
|
||||
|
||||
### Image Formats
|
||||
- `.exr` - OpenEXR (High Dynamic Range)
|
||||
- `.jpg`, `.jpeg` - JPEG Image
|
||||
- `.png` - Portable Network Graphics
|
||||
- `.tiff`, `.tif` - Tagged Image File Format
|
||||
- `.dpx` - Digital Picture Exchange
|
||||
- `.hdr` - High Dynamic Range Image
|
||||
|
||||
### Document Formats
|
||||
- `.pdf` - Portable Document Format
|
||||
- `.txt` - Plain Text
|
||||
- `.doc`, `.docx` - Microsoft Word Document
|
||||
|
||||
### Archive Formats
|
||||
- `.zip` - ZIP Archive
|
||||
- `.rar` - RAR Archive
|
||||
- `.7z` - 7-Zip Archive
|
||||
|
||||
## File Size Limits
|
||||
|
||||
- **Task Attachments**: 10MB maximum
|
||||
- **Work Submissions**: 500MB maximum
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### File Upload Endpoints (via Tasks Router)
|
||||
|
||||
#### Upload Task Attachment
|
||||
```
|
||||
POST /tasks/{task_id}/attachments
|
||||
```
|
||||
- Uploads a file attachment to a task
|
||||
- Creates thumbnail for image files
|
||||
- Returns attachment metadata with serving URLs
|
||||
|
||||
#### Submit Work
|
||||
```
|
||||
POST /tasks/{task_id}/submit
|
||||
```
|
||||
- Submits work file for review
|
||||
- Automatically versions submissions
|
||||
- Updates task status to "submitted"
|
||||
|
||||
### File Serving Endpoints
|
||||
|
||||
#### Serve Attachment
|
||||
```
|
||||
GET /files/attachments/{attachment_id}
|
||||
GET /files/attachments/{attachment_id}?thumbnail=true
|
||||
```
|
||||
- Serves attachment files with access control
|
||||
- Optional thumbnail parameter for image previews
|
||||
|
||||
#### Serve Submission
|
||||
```
|
||||
GET /files/submissions/{submission_id}
|
||||
GET /files/submissions/{submission_id}?thumbnail=true
|
||||
```
|
||||
- Serves submission files with access control
|
||||
- Optional thumbnail parameter for image previews
|
||||
|
||||
#### Stream Submission
|
||||
```
|
||||
GET /files/submissions/{submission_id}/stream
|
||||
```
|
||||
- Streams video files for playback
|
||||
- Supports range requests for efficient streaming
|
||||
|
||||
#### File Information
|
||||
```
|
||||
GET /files/info/attachment/{attachment_id}
|
||||
GET /files/info/submission/{submission_id}
|
||||
```
|
||||
- Returns file metadata and information
|
||||
- Includes file type detection and existence status
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
backend/uploads/
|
||||
├── attachments/
|
||||
│ └── {task_id}/
|
||||
│ └── {unique_filename}
|
||||
├── submissions/
|
||||
│ └── {task_id}/
|
||||
│ └── {versioned_filename}
|
||||
└── thumbnails/
|
||||
└── {filename}_thumb.jpg
|
||||
```
|
||||
|
||||
## Access Control
|
||||
|
||||
### Permission Matrix
|
||||
|
||||
| User Role | Own Tasks | Other Tasks | Review Access |
|
||||
|-------------|-----------|-------------|---------------|
|
||||
| Artist | ✓ | ✗ | ✗ |
|
||||
| Coordinator | ✓ | ✓ | ✓ |
|
||||
| Director | ✓ | ✓ | ✓ |
|
||||
| Admin | ✓ | ✓ | ✓ |
|
||||
|
||||
### Security Features
|
||||
|
||||
- **Authentication required** for all file operations
|
||||
- **Role-based access control** for file serving
|
||||
- **File type validation** to prevent malicious uploads
|
||||
- **File size limits** to prevent abuse
|
||||
- **Unique filename generation** to prevent conflicts
|
||||
- **Path traversal protection** through controlled directory structure
|
||||
|
||||
## File Handler Utility
|
||||
|
||||
The `FileHandler` class provides core functionality:
|
||||
|
||||
### Key Methods
|
||||
|
||||
- `validate_file()` - Validates file format and size
|
||||
- `save_file()` - Saves uploaded file with unique naming
|
||||
- `delete_file()` - Removes file and associated thumbnail
|
||||
- `create_thumbnail()` - Generates image thumbnails
|
||||
- `get_file_info()` - Returns file metadata
|
||||
- `is_image_file()` / `is_video_file()` - File type detection
|
||||
|
||||
### Configuration
|
||||
|
||||
```python
|
||||
# File size limits
|
||||
MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
MAX_SUBMISSION_SIZE = 500 * 1024 * 1024 # 500MB
|
||||
|
||||
# Thumbnail settings
|
||||
THUMBNAIL_SIZE = (200, 200)
|
||||
THUMBNAIL_QUALITY = 85
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Frontend Integration
|
||||
|
||||
```javascript
|
||||
// Upload attachment
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('attachment_type', 'reference');
|
||||
formData.append('description', 'Reference image');
|
||||
|
||||
const response = await fetch(`/tasks/${taskId}/attachments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const attachment = await response.json();
|
||||
console.log('Download URL:', attachment.download_url);
|
||||
console.log('Thumbnail URL:', attachment.thumbnail_url);
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Submit work
|
||||
const formData = new FormData();
|
||||
formData.append('file', workFile);
|
||||
formData.append('notes', 'Final version ready for review');
|
||||
|
||||
const response = await fetch(`/tasks/${taskId}/submit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const submission = await response.json();
|
||||
console.log('Stream URL:', submission.stream_url);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Responses
|
||||
|
||||
- `400 Bad Request` - Invalid file format or missing filename
|
||||
- `403 Forbidden` - Insufficient permissions to access file
|
||||
- `404 Not Found` - File or associated task not found
|
||||
- `413 Payload Too Large` - File exceeds size limits
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "File too large. Maximum size is 10MB"
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Chunked streaming** for large video files
|
||||
- **On-demand thumbnail generation** with caching
|
||||
- **Efficient file serving** with proper MIME types
|
||||
- **Range request support** for video streaming
|
||||
|
||||
## Maintenance
|
||||
|
||||
### File Cleanup
|
||||
|
||||
The system does not automatically clean up orphaned files. Consider implementing:
|
||||
|
||||
- Periodic cleanup of files without database references
|
||||
- Archive old submissions after project completion
|
||||
- Monitor disk usage and implement retention policies
|
||||
|
||||
### Backup Considerations
|
||||
|
||||
- Include upload directories in backup procedures
|
||||
- Consider separate backup strategy for large media files
|
||||
- Implement file integrity checks for critical submissions
|
||||
@@ -0,0 +1,338 @@
|
||||
# Project Settings Implementation Summary
|
||||
|
||||
## Overview
|
||||
Task 5.5 implements project-specific settings for upload location and default task templates, allowing coordinators to customize workflows and file organization for each project's unique requirements.
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### Database Schema
|
||||
The Project model already includes the necessary fields:
|
||||
- `upload_data_location` (String): Custom upload storage path for project files
|
||||
- `asset_task_templates` (JSON): Default tasks per asset category
|
||||
- `shot_task_templates` (JSON): Default tasks for shots
|
||||
- `enabled_asset_tasks` (JSON): Enabled/disabled asset tasks per category
|
||||
- `enabled_shot_tasks` (JSON): Enabled/disabled shot tasks
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### GET /projects/{project_id}/settings
|
||||
- **Description**: Retrieve project-specific settings
|
||||
- **Authorization**: Any project member
|
||||
- **Response**: ProjectSettings schema with all settings fields
|
||||
- **Default Values**: Returns default templates if not configured
|
||||
|
||||
#### PUT /projects/{project_id}/settings
|
||||
- **Description**: Update project-specific settings
|
||||
- **Authorization**: Coordinator or Admin only
|
||||
- **Request Body**: ProjectSettingsUpdate schema (all fields optional)
|
||||
- **Response**: Updated ProjectSettings
|
||||
- **Features**:
|
||||
- Partial updates supported
|
||||
- Validates asset categories and task templates
|
||||
- Persists changes to database
|
||||
|
||||
### Schemas (backend/schemas/project.py)
|
||||
|
||||
```python
|
||||
class ProjectSettings(BaseModel):
|
||||
"""Project-specific settings for upload location and task templates"""
|
||||
upload_data_location: Optional[str]
|
||||
asset_task_templates: Optional[Dict[str, List[str]]]
|
||||
shot_task_templates: Optional[List[str]]
|
||||
enabled_asset_tasks: Optional[Dict[str, List[str]]]
|
||||
enabled_shot_tasks: Optional[List[str]]
|
||||
|
||||
class ProjectSettingsUpdate(BaseModel):
|
||||
"""Update project settings (all fields optional)"""
|
||||
upload_data_location: Optional[str]
|
||||
asset_task_templates: Optional[Dict[str, List[str]]]
|
||||
shot_task_templates: Optional[List[str]]
|
||||
enabled_asset_tasks: Optional[Dict[str, List[str]]]
|
||||
enabled_shot_tasks: Optional[List[str]]
|
||||
```
|
||||
|
||||
### Default Values
|
||||
|
||||
```python
|
||||
DEFAULT_ASSET_TASKS = {
|
||||
"characters": ["modeling", "surfacing", "rigging"],
|
||||
"props": ["modeling", "surfacing"],
|
||||
"sets": ["modeling", "surfacing"],
|
||||
"vehicles": ["modeling", "surfacing", "rigging"]
|
||||
}
|
||||
|
||||
DEFAULT_SHOT_TASKS = ["layout", "animation", "simulation", "lighting", "compositing"]
|
||||
```
|
||||
|
||||
### Validation
|
||||
- Asset categories must be one of: characters, props, sets, vehicles
|
||||
- Task templates must be lists of strings
|
||||
- Enabled tasks must match the template structure
|
||||
- Invalid categories or formats return 422 validation errors
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Components
|
||||
|
||||
#### 1. UploadLocationConfig.vue
|
||||
**Location**: `frontend/src/components/settings/UploadLocationConfig.vue`
|
||||
|
||||
**Features**:
|
||||
- Input field for upload data location path
|
||||
- Clear button to reset location
|
||||
- Example paths for Windows, Linux/Mac, and Network drives
|
||||
- Info box with usage guidelines
|
||||
- Save/Cancel actions
|
||||
|
||||
**Props**:
|
||||
- `initialLocation`: Current upload location
|
||||
- `isSaving`: Loading state during save
|
||||
|
||||
**Events**:
|
||||
- `save`: Emits new location string
|
||||
- `cancel`: Emits cancel action
|
||||
|
||||
#### 2. DefaultTaskTemplatesEditor.vue
|
||||
**Location**: `frontend/src/components/settings/DefaultTaskTemplatesEditor.vue`
|
||||
|
||||
**Features**:
|
||||
- Asset task templates table with checkboxes per category
|
||||
- Shot task templates table with enable/disable checkboxes
|
||||
- Dynamic loading of custom task types from API
|
||||
- Edit/Delete buttons for custom task types
|
||||
- Template preview showing what tasks will be created
|
||||
- Reset to defaults button
|
||||
- Integration with CustomTaskTypeManager
|
||||
|
||||
**Props**:
|
||||
- `projectId`: Project ID for loading custom task types
|
||||
- `initialAssetTemplates`: Current asset templates
|
||||
- `initialShotTemplates`: Current shot templates
|
||||
- `isSaving`: Loading state during save
|
||||
|
||||
**Events**:
|
||||
- `save`: Emits { assetTemplates, shotTemplates }
|
||||
- `cancel`: Emits cancel action
|
||||
- `editCustomTaskType`: Emits (taskType, category) for editing
|
||||
- `deleteCustomTaskType`: Emits (taskType, category) for deletion
|
||||
|
||||
**Methods**:
|
||||
- `refreshTaskTypes()`: Exposed method to reload task types after custom type changes
|
||||
|
||||
#### 3. ProjectSettingsView.vue
|
||||
**Location**: `frontend/src/views/ProjectSettingsView.vue`
|
||||
|
||||
**Features**:
|
||||
- Tabbed interface with 6 tabs:
|
||||
- General: Project basic information
|
||||
- Episodes: Episode management
|
||||
- Team: Project members
|
||||
- Technical: Technical specifications
|
||||
- Tasks: Custom task types and templates
|
||||
- Storage: Upload location configuration
|
||||
- Loads project settings on mount
|
||||
- Handles save operations for both components
|
||||
- Integrates with CustomTaskTypeManager
|
||||
- Provides navigation between tabs for edit/delete operations
|
||||
|
||||
**Tab Structure**:
|
||||
```
|
||||
General | Episodes | Team | Technical | Tasks | Storage
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
#### projectService (frontend/src/services/project.ts)
|
||||
|
||||
```typescript
|
||||
export interface ProjectSettings {
|
||||
upload_data_location?: string
|
||||
asset_task_templates?: AssetTaskTemplates
|
||||
shot_task_templates?: string[]
|
||||
enabled_asset_tasks?: Record<string, string[]>
|
||||
enabled_shot_tasks?: string[]
|
||||
}
|
||||
|
||||
async getProjectSettings(projectId: number): Promise<ProjectSettings>
|
||||
async updateProjectSettings(projectId: number, settings: ProjectSettings): Promise<ProjectSettings>
|
||||
```
|
||||
|
||||
### User Flow
|
||||
|
||||
1. **Navigate to Project Settings**:
|
||||
- User clicks on project settings from project page
|
||||
- ProjectSettingsView loads with tabbed interface
|
||||
|
||||
2. **Configure Upload Location** (Storage Tab):
|
||||
- User enters custom upload path
|
||||
- Clicks "Save Configuration"
|
||||
- System validates and saves to backend
|
||||
- Toast notification confirms success
|
||||
|
||||
3. **Configure Task Templates** (Tasks Tab):
|
||||
- User sees two sections:
|
||||
- Custom Task Type Manager (top)
|
||||
- Default Task Templates Editor (bottom)
|
||||
- User can add/edit/delete custom task types
|
||||
- User toggles checkboxes for each task type per category
|
||||
- Preview shows what tasks will be created
|
||||
- Clicks "Save Templates"
|
||||
- System validates and saves to backend
|
||||
- Toast notification confirms success
|
||||
|
||||
4. **Integration with Asset/Shot Creation**:
|
||||
- When creating assets, system uses project-specific templates
|
||||
- When creating shots, system uses project-specific templates
|
||||
- Custom task types appear in templates automatically
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend Tests
|
||||
**File**: `backend/test_project_settings_api.py`
|
||||
|
||||
**Test Cases**:
|
||||
1. ✅ GET project settings returns defaults
|
||||
2. ✅ PUT updates all settings fields
|
||||
3. ✅ Settings persist across requests
|
||||
4. ✅ Partial updates work correctly
|
||||
5. ✅ Invalid project ID returns 404
|
||||
|
||||
**Run Tests**:
|
||||
```bash
|
||||
cd backend
|
||||
python test_project_settings_api.py
|
||||
```
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
#### Backend:
|
||||
- [x] GET /projects/{id}/settings returns default values
|
||||
- [x] PUT /projects/{id}/settings updates all fields
|
||||
- [x] PUT /projects/{id}/settings supports partial updates
|
||||
- [x] Settings persist in database
|
||||
- [x] Invalid project ID returns 404
|
||||
- [x] Non-coordinator users cannot update settings
|
||||
- [x] Validation errors for invalid categories
|
||||
|
||||
#### Frontend:
|
||||
- [ ] Upload location input accepts and saves paths
|
||||
- [ ] Task template checkboxes toggle correctly
|
||||
- [ ] Preview updates when templates change
|
||||
- [ ] Reset to defaults button works
|
||||
- [ ] Save button shows loading state
|
||||
- [ ] Toast notifications appear on success/error
|
||||
- [ ] Settings persist after page refresh
|
||||
- [ ] Custom task types appear in templates
|
||||
- [ ] Edit/Delete custom task type navigation works
|
||||
|
||||
## Requirements Coverage
|
||||
|
||||
This implementation satisfies the following requirements:
|
||||
|
||||
### Requirement 19: Project Settings for Upload Location and Default Tasks
|
||||
|
||||
**19.1** ✅ Configure upload data storage locations per project
|
||||
- Upload location field in Project model
|
||||
- API endpoint to get/update upload location
|
||||
- Frontend component with path input
|
||||
|
||||
**19.2** ✅ Define custom default task templates for asset creation per project
|
||||
- Asset task templates stored as JSON in Project model
|
||||
- API endpoints to manage templates
|
||||
- Frontend editor with category-specific checkboxes
|
||||
|
||||
**19.3** ✅ Define custom default task templates for shot creation per project
|
||||
- Shot task templates stored as JSON in Project model
|
||||
- API endpoints to manage templates
|
||||
- Frontend editor with task type checkboxes
|
||||
|
||||
**19.4** ✅ Support different task templates for different asset categories
|
||||
- Templates organized by category (characters, props, sets, vehicles)
|
||||
- Each category can have different task lists
|
||||
- Frontend table shows all categories
|
||||
|
||||
**19.5** ✅ Support different task templates for different shot types
|
||||
- Shot templates stored as list
|
||||
- Can be customized per project
|
||||
- Frontend table shows all shot task types
|
||||
|
||||
**19.6** ✅ Enable or disable specific default tasks per project
|
||||
- Enabled tasks tracked separately from templates
|
||||
- Can disable tasks without removing from templates
|
||||
- Frontend checkboxes control enabled state
|
||||
|
||||
**19.7** ✅ Apply project-specific upload locations to all file uploads
|
||||
- Upload location stored in project settings
|
||||
- Available for file upload handlers to use
|
||||
- Displayed to users during upload
|
||||
|
||||
**19.8** ✅ Use project-specific default task templates when creating assets and shots
|
||||
- Templates loaded from project settings
|
||||
- Applied during asset/shot creation
|
||||
- Custom task types included automatically
|
||||
|
||||
**19.9** ✅ Provide project settings interface for coordinators
|
||||
- Dedicated settings view with tabs
|
||||
- Coordinator-only access for updates
|
||||
- All project members can view settings
|
||||
|
||||
## Database Migration
|
||||
|
||||
The migration script `backend/migrate_project_settings.py` adds the necessary columns:
|
||||
- Checks if columns already exist
|
||||
- Adds columns if missing
|
||||
- Sets default values for existing projects
|
||||
- Safe to run multiple times
|
||||
|
||||
**Run Migration**:
|
||||
```bash
|
||||
cd backend
|
||||
python migrate_project_settings.py
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### With Asset Creation
|
||||
- Asset creation reads `asset_task_templates` from project settings
|
||||
- Creates tasks based on asset category
|
||||
- Respects `enabled_asset_tasks` to skip disabled tasks
|
||||
- Includes custom task types from project
|
||||
|
||||
### With Shot Creation
|
||||
- Shot creation reads `shot_task_templates` from project settings
|
||||
- Creates tasks for all enabled shot task types
|
||||
- Respects `enabled_shot_tasks` to skip disabled tasks
|
||||
- Includes custom task types from project
|
||||
|
||||
### With File Uploads
|
||||
- File upload handlers can read `upload_data_location`
|
||||
- Use project-specific path if configured
|
||||
- Fall back to default location if not set
|
||||
|
||||
### With Custom Task Types
|
||||
- Custom task types automatically appear in templates
|
||||
- Edit/Delete operations navigate to CustomTaskTypeManager
|
||||
- Template editor refreshes after custom type changes
|
||||
- Seamless integration between components
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Template Presets**: Save and share template configurations across projects
|
||||
2. **Path Validation**: Verify upload paths exist and are writable
|
||||
3. **Template History**: Track changes to templates over time
|
||||
4. **Bulk Template Update**: Apply template changes to existing assets/shots
|
||||
5. **Template Import/Export**: Share templates between projects or installations
|
||||
6. **Advanced Path Configuration**: Separate paths for different file types
|
||||
7. **Template Inheritance**: Base templates on other projects
|
||||
8. **Conditional Templates**: Different templates based on project type or status
|
||||
|
||||
## Notes
|
||||
|
||||
- All settings are optional and have sensible defaults
|
||||
- Settings can be updated independently (partial updates)
|
||||
- Changes take effect immediately for new assets/shots
|
||||
- Existing assets/shots are not affected by template changes
|
||||
- Coordinators and admins can manage settings
|
||||
- All project members can view settings
|
||||
- Settings are project-specific and don't affect other projects
|
||||
- Custom task types integrate seamlessly with templates
|
||||
@@ -0,0 +1,208 @@
|
||||
# Project Thumbnail Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of project thumbnail upload functionality for the VFX Project Management System.
|
||||
|
||||
## Requirements
|
||||
|
||||
Based on Requirement 2.1, the system allows coordinators and administrators to:
|
||||
- Upload thumbnail images for projects
|
||||
- Replace existing thumbnails
|
||||
- Delete thumbnails
|
||||
- View thumbnails on project cards
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Database Changes
|
||||
|
||||
**Migration Script**: `backend/migrate_project_thumbnail.py`
|
||||
|
||||
Added `thumbnail_path` column to the `projects` table:
|
||||
```sql
|
||||
ALTER TABLE projects ADD COLUMN thumbnail_path VARCHAR
|
||||
```
|
||||
|
||||
**Model Update**: `backend/models/project.py`
|
||||
```python
|
||||
thumbnail_path = Column(String, nullable=True) # Path to project thumbnail image
|
||||
```
|
||||
|
||||
### 2. API Endpoints
|
||||
|
||||
#### Upload Thumbnail
|
||||
**Endpoint**: `POST /api/projects/{project_id}/thumbnail`
|
||||
**Access**: Coordinators and Admins only
|
||||
**Request**: Multipart form data with image file
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"message": "Thumbnail uploaded successfully",
|
||||
"thumbnail_url": "/api/files/projects/{project_id}/thumbnail"
|
||||
}
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Validates file format (jpg, jpeg, png, gif, webp)
|
||||
- Validates file size (max 10MB)
|
||||
- Processes and resizes images to max 800x600 while maintaining aspect ratio
|
||||
- Converts images with transparency to RGB with white background
|
||||
- Generates unique filenames with timestamp and hash
|
||||
- Automatically deletes old thumbnail when uploading new one
|
||||
- Stores processed images in `uploads/project_thumbnails/`
|
||||
|
||||
#### Delete Thumbnail
|
||||
**Endpoint**: `DELETE /api/projects/{project_id}/thumbnail`
|
||||
**Access**: Coordinators and Admins only
|
||||
**Response**: 204 No Content
|
||||
|
||||
**Features**:
|
||||
- Removes thumbnail file from filesystem
|
||||
- Clears `thumbnail_path` in database
|
||||
- Returns 404 if project has no thumbnail
|
||||
|
||||
#### Serve Thumbnail
|
||||
**Endpoint**: `GET /api/files/projects/{project_id}/thumbnail`
|
||||
**Access**: All authenticated users (with project access)
|
||||
**Response**: Image file with appropriate content-type
|
||||
|
||||
**Features**:
|
||||
- Checks user has access to the project
|
||||
- Artists can only access thumbnails for projects they're members of
|
||||
- Returns 404 if thumbnail doesn't exist
|
||||
- Serves image with proper MIME type
|
||||
|
||||
### 3. Schema Updates
|
||||
|
||||
**File**: `backend/schemas/project.py`
|
||||
|
||||
Added `thumbnail_url` field to response schemas:
|
||||
|
||||
```python
|
||||
class ProjectResponse(ProjectBase):
|
||||
# ... existing fields ...
|
||||
thumbnail_url: Optional[str] = None
|
||||
|
||||
class ProjectListResponse(BaseModel):
|
||||
# ... existing fields ...
|
||||
thumbnail_url: Optional[str] = None
|
||||
```
|
||||
|
||||
### 4. Router Updates
|
||||
|
||||
**File**: `backend/routers/projects.py`
|
||||
|
||||
Updated project endpoints to include thumbnail URL:
|
||||
|
||||
```python
|
||||
# In list_projects
|
||||
if project.thumbnail_path:
|
||||
project_data.thumbnail_url = f"/api/files/projects/{project.id}/thumbnail"
|
||||
|
||||
# In get_project
|
||||
if project.thumbnail_path:
|
||||
project_data.thumbnail_url = f"/api/files/projects/{project.id}/thumbnail"
|
||||
|
||||
# In update_project
|
||||
'thumbnail_url': f"/api/files/projects/{db_project.id}/thumbnail" if db_project.thumbnail_path else None
|
||||
```
|
||||
|
||||
## File Storage Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
└── uploads/
|
||||
└── project_thumbnails/
|
||||
├── project_1_20241119_123456_a1b2c3d4.jpg
|
||||
├── project_2_20241119_123457_e5f6g7h8.jpg
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Image Processing
|
||||
|
||||
The system processes uploaded thumbnails as follows:
|
||||
|
||||
1. **Format Validation**: Only accepts jpg, jpeg, png, gif, webp
|
||||
2. **Size Validation**: Maximum 10MB file size
|
||||
3. **Color Conversion**: Converts RGBA/LA/P modes to RGB with white background
|
||||
4. **Resizing**: Maintains aspect ratio while fitting within 800x600 pixels
|
||||
5. **Optimization**: Saves as JPEG with 90% quality and optimization enabled
|
||||
|
||||
## Access Control
|
||||
|
||||
| Role | Upload | Delete | View |
|
||||
|------|--------|--------|------|
|
||||
| Admin | ✓ | ✓ | ✓ |
|
||||
| Coordinator | ✓ | ✓ | ✓ |
|
||||
| Director | ✗ | ✗ | ✓ |
|
||||
| Artist | ✗ | ✗ | ✓ (own projects only) |
|
||||
| Developer | ✗ | ✗ | ✓ |
|
||||
|
||||
## Testing
|
||||
|
||||
A test script is provided at `backend/test_project_thumbnail.py` that verifies:
|
||||
1. Thumbnail upload
|
||||
2. Thumbnail URL in project responses
|
||||
3. Thumbnail download
|
||||
4. Thumbnail deletion
|
||||
5. Thumbnail removal verification
|
||||
|
||||
To run the test:
|
||||
```bash
|
||||
cd backend
|
||||
python test_project_thumbnail.py
|
||||
```
|
||||
|
||||
**Note**: The backend server must be running on `http://localhost:8000`
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Status Code | Description |
|
||||
|-------|-------------|-------------|
|
||||
| Invalid file format | 400 | File extension not in allowed list |
|
||||
| File too large | 413 | File exceeds 10MB limit |
|
||||
| Image processing failed | 400 | PIL failed to process image |
|
||||
| Project not found | 404 | Invalid project_id |
|
||||
| No thumbnail | 404 | Project has no thumbnail |
|
||||
| Access denied | 403 | User lacks permission |
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
The frontend can now:
|
||||
1. Display thumbnails on project cards using `project.thumbnail_url`
|
||||
2. Upload thumbnails in project settings
|
||||
3. Replace existing thumbnails
|
||||
4. Remove thumbnails
|
||||
5. Show placeholder when no thumbnail exists
|
||||
|
||||
Example usage:
|
||||
```typescript
|
||||
// Display thumbnail
|
||||
<img :src="project.thumbnail_url || '/default-project-thumbnail.png'" />
|
||||
|
||||
// Upload thumbnail
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await apiClient.post(`/projects/${projectId}/thumbnail`, formData)
|
||||
|
||||
// Delete thumbnail
|
||||
await apiClient.delete(`/projects/${projectId}/thumbnail`)
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
The following frontend tasks remain to be implemented:
|
||||
- Task 22: Implement frontend project thumbnail upload component
|
||||
- Task 22.1: Add thumbnail preview and management
|
||||
- Task 22.2: Integrate thumbnail upload in project settings
|
||||
- Task 22.3: Update project card to display thumbnails
|
||||
- Task 22.4: Update project type definitions
|
||||
|
||||
## Related Files
|
||||
|
||||
- `backend/models/project.py` - Project model with thumbnail_path
|
||||
- `backend/routers/projects.py` - Thumbnail upload/delete endpoints
|
||||
- `backend/routers/files.py` - Thumbnail serving endpoint
|
||||
- `backend/schemas/project.py` - Response schemas with thumbnail_url
|
||||
- `backend/migrate_project_thumbnail.py` - Database migration script
|
||||
- `backend/test_project_thumbnail.py` - Test script
|
||||
@@ -0,0 +1,110 @@
|
||||
# Shot Access 403 Error Fix
|
||||
|
||||
## Problem
|
||||
Admin users were getting 403 Forbidden errors when trying to access shot details, even though they should have access to all shots.
|
||||
|
||||
## Root Cause
|
||||
The `check_episode_access()` function in `backend/routers/shots.py` was using implicit logic:
|
||||
- It only checked if the user was an **artist**
|
||||
- If not an artist, it assumed access was granted
|
||||
- However, this logic failed because it didn't explicitly handle the case where a user has `is_admin=True`
|
||||
|
||||
The original code:
|
||||
```python
|
||||
# Check project access for artists
|
||||
if current_user.role == UserRole.ARTIST:
|
||||
member = db.query(ProjectMember).filter(
|
||||
ProjectMember.project_id == episode.project_id,
|
||||
ProjectMember.user_id == current_user.id
|
||||
).first()
|
||||
if not member:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this project"
|
||||
)
|
||||
|
||||
return episode
|
||||
```
|
||||
|
||||
## Solution
|
||||
Added explicit checks for admins and coordinators at the beginning of the function:
|
||||
|
||||
```python
|
||||
# Admins and coordinators have access to all episodes
|
||||
if current_user.is_admin or current_user.role == UserRole.COORDINATOR:
|
||||
return episode
|
||||
|
||||
# Check project access for artists and other roles
|
||||
if current_user.role == UserRole.ARTIST:
|
||||
member = db.query(ProjectMember).filter(
|
||||
ProjectMember.project_id == episode.project_id,
|
||||
ProjectMember.user_id == current_user.id
|
||||
).first()
|
||||
if not member:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this project"
|
||||
)
|
||||
|
||||
return episode
|
||||
```
|
||||
|
||||
## Changes Made
|
||||
|
||||
**File**: `backend/routers/shots.py`
|
||||
**Function**: `check_episode_access()` (lines 44-72)
|
||||
|
||||
### Before
|
||||
- Implicit access for non-artists
|
||||
- No explicit admin check
|
||||
|
||||
### After
|
||||
- Explicit early return for admins (`is_admin=True`)
|
||||
- Explicit early return for coordinators (`role=COORDINATOR`)
|
||||
- Clear access control logic
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Script
|
||||
Created `backend/test_admin_shot_access.py` to verify:
|
||||
- ✓ Admins can access any episode
|
||||
- ✓ Coordinators can access any episode
|
||||
- ✓ Artists can only access episodes in their projects
|
||||
|
||||
### Manual Testing
|
||||
1. Login as admin
|
||||
2. Navigate to any project
|
||||
3. Click on any shot
|
||||
4. Shot detail panel should open without 403 error
|
||||
|
||||
## Impact
|
||||
|
||||
### Fixed
|
||||
- Admins can now access all shot details
|
||||
- Coordinators can access all shot details
|
||||
- No more 403 errors for privileged users
|
||||
|
||||
### Unchanged
|
||||
- Artists still require project membership
|
||||
- Security model remains intact
|
||||
- No breaking changes to API
|
||||
|
||||
## Related Files
|
||||
- `backend/routers/shots.py` - Main fix
|
||||
- `backend/test_admin_shot_access.py` - Test script
|
||||
- `frontend/src/components/shot/ShotDetailPanel.vue` - Frontend component (no changes needed)
|
||||
|
||||
## Deployment Notes
|
||||
- No database migration required
|
||||
- No frontend changes required
|
||||
- Backend restart required to apply fix
|
||||
- Backward compatible
|
||||
|
||||
## Prevention
|
||||
This issue occurred because the access control logic relied on implicit behavior. Future improvements:
|
||||
1. Always use explicit checks for privileged roles
|
||||
2. Add comprehensive access control tests
|
||||
3. Document access control logic clearly
|
||||
|
||||
## Date
|
||||
November 17, 2025
|
||||
@@ -0,0 +1,96 @@
|
||||
# Shot Project ID Soft Deletion Service Update
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the updates made to the soft deletion services to ensure `project_id` is properly preserved during soft deletion and recovery operations for shots.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Shot Soft Deletion Service (`backend/services/shot_soft_deletion.py`)
|
||||
|
||||
#### DeletionInfo Class Updates
|
||||
- **Added `project_id` field**: The `DeletionInfo` class now includes a `project_id` field to store the project ID of the shot being deleted.
|
||||
- **Updated initialization**: The `get_deletion_info` method now populates `project_id` directly from `shot.project_id` instead of deriving it from the episode relationship.
|
||||
|
||||
#### Activity Logging Updates
|
||||
- **Direct project_id usage**: The `_log_shot_deletion` method now uses `shot.project_id` directly for the activity's `project_id` field.
|
||||
- **Enhanced metadata**: Added `project_id` and `project_name` to the activity metadata for better tracking.
|
||||
|
||||
### 2. Recovery Service (`backend/services/recovery_service.py`)
|
||||
|
||||
#### DeletedShot Class Updates
|
||||
- **Added `project_id` field**: The `DeletedShot` class now includes a `project_id` field for consistency.
|
||||
|
||||
#### RecoveryInfo Class Updates
|
||||
- **Added `project_id` field**: The `RecoveryInfo` class now includes a `project_id` field to track project information during recovery operations.
|
||||
|
||||
#### Service Method Updates
|
||||
- **Updated `get_deleted_shots`**: Now populates `project_id` directly from `shot.project_id`.
|
||||
- **Updated `preview_shot_recovery`**: Now includes `project_id` in the recovery information.
|
||||
- **Updated `_log_shot_recovery`**: Uses `shot.project_id` directly and includes project information in activity metadata.
|
||||
|
||||
### 3. Admin Router (`backend/routers/admin.py`)
|
||||
|
||||
#### API Response Updates
|
||||
- **Enhanced deleted shots endpoint**: The `/admin/deleted-shots/` endpoint now includes `project_id` in the response.
|
||||
- **Enhanced recovery preview endpoint**: The `/admin/shots/{shot_id}/recovery-preview` endpoint now includes `project_id` in the response.
|
||||
|
||||
## Key Benefits
|
||||
|
||||
### 1. Data Integrity
|
||||
- **Direct relationship**: Using `shot.project_id` directly ensures data integrity and eliminates dependency on episode relationships for project identification.
|
||||
- **Consistent tracking**: Project information is consistently preserved throughout the soft deletion and recovery lifecycle.
|
||||
|
||||
### 2. Performance Improvement
|
||||
- **Reduced joins**: Direct access to `project_id` reduces the need for complex joins through episode relationships.
|
||||
- **Faster queries**: Project filtering can now use direct `project_id` comparisons.
|
||||
|
||||
### 3. API Enhancement
|
||||
- **Complete information**: API responses now include both `project_id` and `project_name` for better frontend integration.
|
||||
- **Filtering support**: The existing project filtering functionality now works more efficiently with direct `project_id` access.
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
All changes maintain backward compatibility:
|
||||
- Existing API endpoints continue to work as before
|
||||
- Additional fields are added without removing existing functionality
|
||||
- Episode relationships are still maintained and used where appropriate
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Coverage
|
||||
- **Unit tests**: Existing soft deletion service tests continue to pass
|
||||
- **Integration tests**: New tests verify `project_id` preservation throughout the deletion/recovery cycle
|
||||
- **API tests**: Tests verify that API endpoints return `project_id` information correctly
|
||||
|
||||
### Test Files Created
|
||||
1. `test_project_id_preservation_simple.py` - Tests core `project_id` preservation functionality
|
||||
2. `test_admin_api_project_id.py` - Tests API endpoint responses (requires running server)
|
||||
|
||||
## Requirements Validation
|
||||
|
||||
This implementation satisfies the following requirements from the specification:
|
||||
|
||||
### Requirement 4.5
|
||||
> "WHEN soft deleting shots, THE VFX_System SHALL preserve project_id information for recovery operations"
|
||||
|
||||
**✅ Satisfied**:
|
||||
- `project_id` is preserved in the shot record during soft deletion
|
||||
- Recovery operations maintain `project_id` consistency
|
||||
- All related data maintains project relationships
|
||||
|
||||
### Additional Benefits
|
||||
- **Enhanced logging**: Activity logs now include complete project information
|
||||
- **Improved debugging**: Project context is available in all deletion/recovery operations
|
||||
- **Better admin tools**: Admin interfaces have access to project information for better management
|
||||
|
||||
## Migration Notes
|
||||
|
||||
No database migration is required for these changes as they work with the existing `project_id` column that was added in the previous migration. The changes are purely at the service and API layer to ensure proper utilization of the existing `project_id` field.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential future improvements could include:
|
||||
- Adding project-specific deletion policies
|
||||
- Enhanced project-based recovery filtering
|
||||
- Project-scoped deletion statistics and reporting
|
||||
@@ -0,0 +1,222 @@
|
||||
# Shot Task Creation Endpoint
|
||||
|
||||
## Overview
|
||||
|
||||
Added a new backend endpoint to create tasks for shots, enabling AJAX-based task status editing in the frontend shot table.
|
||||
|
||||
## Endpoint Details
|
||||
|
||||
### POST /shots/{shot_id}/tasks
|
||||
|
||||
**Purpose**: Create a new task for a specific shot
|
||||
|
||||
**Location**: `backend/routers/shots.py` (after `get_shot` endpoint)
|
||||
|
||||
**Authentication**: Requires coordinator or admin role
|
||||
|
||||
**Parameters**:
|
||||
- `shot_id` (path): The ID of the shot
|
||||
- `task_type` (query): The type of task to create (e.g., "layout", "animation")
|
||||
|
||||
**Request Example**:
|
||||
```
|
||||
POST /shots/1/tasks?task_type=layout
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**Response** (201 Created):
|
||||
```json
|
||||
{
|
||||
"task_type": "layout",
|
||||
"status": "not_started",
|
||||
"task_id": 123,
|
||||
"assigned_user_id": null
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK - if task already exists):
|
||||
```json
|
||||
{
|
||||
"task_type": "layout",
|
||||
"status": "in_progress",
|
||||
"task_id": 123,
|
||||
"assigned_user_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
```python
|
||||
@router.post("/{shot_id}/tasks", response_model=TaskStatusInfo, status_code=status.HTTP_201_CREATED)
|
||||
async def create_shot_task(
|
||||
shot_id: int,
|
||||
task_type: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_coordinator_or_admin)
|
||||
):
|
||||
"""Create a new task for a shot"""
|
||||
shot = db.query(Shot).filter(Shot.id == shot_id).first()
|
||||
|
||||
if not shot:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Shot not found"
|
||||
)
|
||||
|
||||
# Check episode access
|
||||
episode = check_episode_access(shot.episode_id, current_user, db)
|
||||
|
||||
# Check if task already exists
|
||||
existing_task = db.query(Task).filter(
|
||||
Task.shot_id == shot_id,
|
||||
Task.task_type == task_type
|
||||
).first()
|
||||
|
||||
if existing_task:
|
||||
# Return existing task info instead of error (idempotent)
|
||||
return TaskStatusInfo(
|
||||
task_type=existing_task.task_type,
|
||||
status=existing_task.status,
|
||||
task_id=existing_task.id,
|
||||
assigned_user_id=existing_task.assigned_user_id
|
||||
)
|
||||
|
||||
# Create the task
|
||||
task_name = f"{shot.name} - {task_type.title()}"
|
||||
db_task = Task(
|
||||
project_id=episode.project_id,
|
||||
shot_id=shot.id,
|
||||
task_type=task_type,
|
||||
name=task_name,
|
||||
description=f"{task_type.title()} task for {shot.name}",
|
||||
status=TaskStatus.NOT_STARTED
|
||||
)
|
||||
|
||||
db.add(db_task)
|
||||
db.commit()
|
||||
db.refresh(db_task)
|
||||
|
||||
return TaskStatusInfo(
|
||||
task_type=db_task.task_type,
|
||||
status=db_task.status,
|
||||
task_id=db_task.id,
|
||||
assigned_user_id=db_task.assigned_user_id
|
||||
)
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Idempotent Operation
|
||||
- If task already exists, returns existing task info
|
||||
- No error thrown for duplicate creation attempts
|
||||
- Simplifies frontend logic (no need to check existence first)
|
||||
|
||||
### 2. Permission Validation
|
||||
- Requires coordinator or admin role
|
||||
- Uses `require_coordinator_or_admin` dependency
|
||||
- Artists cannot create tasks directly
|
||||
|
||||
### 3. Access Control
|
||||
- Validates shot exists
|
||||
- Checks episode access via `check_episode_access`
|
||||
- Ensures user has permission to access the project
|
||||
|
||||
### 4. Automatic Task Naming
|
||||
- Format: `{shot_name} - {task_type}`
|
||||
- Example: "SH010 - Layout"
|
||||
- Consistent with asset task naming
|
||||
|
||||
### 5. Default Values
|
||||
- Status: `NOT_STARTED`
|
||||
- Description: Auto-generated
|
||||
- Project ID: Inherited from episode
|
||||
- No assignee initially
|
||||
|
||||
## Error Responses
|
||||
|
||||
### 404 Not Found
|
||||
```json
|
||||
{
|
||||
"detail": "Shot not found"
|
||||
}
|
||||
```
|
||||
|
||||
### 403 Forbidden
|
||||
```json
|
||||
{
|
||||
"detail": "Insufficient permissions"
|
||||
}
|
||||
```
|
||||
|
||||
Or:
|
||||
```json
|
||||
{
|
||||
"detail": "Access denied to this project"
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison with Asset Endpoint
|
||||
|
||||
The shot task creation endpoint mirrors the asset version:
|
||||
|
||||
| Feature | Asset Endpoint | Shot Endpoint |
|
||||
|---------|---------------|---------------|
|
||||
| Path | `/assets/{asset_id}/tasks` | `/shots/{shot_id}/tasks` |
|
||||
| Permission | Coordinator/Admin | Coordinator/Admin |
|
||||
| Idempotent | ✅ Yes | ✅ Yes |
|
||||
| Access Check | `check_project_access` | `check_episode_access` |
|
||||
| Task Naming | `{asset.name} - {type}` | `{shot.name} - {type}` |
|
||||
| Project ID | From asset | From episode |
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
This endpoint is called by:
|
||||
- `frontend/src/services/task.ts` - `createShotTask()` method
|
||||
- `frontend/src/components/shot/EditableTaskStatus.vue` - When changing status for non-existent task
|
||||
|
||||
**Usage Flow**:
|
||||
1. User changes task status in shot table
|
||||
2. Frontend checks if task exists (via `taskId` prop)
|
||||
3. If no task exists, calls `createShotTask(shotId, taskType)`
|
||||
4. Backend creates task and returns task ID
|
||||
5. Frontend then calls `updateTaskStatus(taskId, newStatus)`
|
||||
6. Status is updated and table refreshes
|
||||
|
||||
## Testing
|
||||
|
||||
Test file created: `backend/test_shot_task_creation.py`
|
||||
|
||||
**Manual Testing**:
|
||||
1. Navigate to project shots tab
|
||||
2. Switch to table view
|
||||
3. Click on a task status cell for a shot without that task
|
||||
4. Select a status
|
||||
5. Verify:
|
||||
- Task is created
|
||||
- Status is set
|
||||
- No errors in console
|
||||
- Table updates correctly
|
||||
|
||||
## Related Files
|
||||
|
||||
- `backend/routers/shots.py` - Endpoint implementation
|
||||
- `backend/routers/assets.py` - Reference implementation for assets
|
||||
- `backend/schemas/shot.py` - TaskStatusInfo schema
|
||||
- `backend/models/task.py` - Task model
|
||||
- `frontend/src/services/task.ts` - Frontend service calling this endpoint
|
||||
- `frontend/src/components/shot/EditableTaskStatus.vue` - Component using this endpoint
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
1. **Bulk Task Creation**: Create multiple tasks at once
|
||||
2. **Custom Defaults**: Allow project-specific default task settings
|
||||
3. **Template Support**: Use task templates for consistent setup
|
||||
4. **Validation**: Validate task_type against allowed types
|
||||
5. **Webhooks**: Trigger notifications when tasks are created
|
||||
6. **Audit Log**: Track who created which tasks and when
|
||||
|
||||
## Conclusion
|
||||
|
||||
The shot task creation endpoint successfully enables AJAX-based task status editing in the shot table, providing a seamless user experience without page refreshes. The implementation follows the same pattern as the asset endpoint, ensuring consistency across the application.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Soft Deletion Commit Fix
|
||||
|
||||
## Issue Description
|
||||
|
||||
The soft deletion functionality for shots and assets was not properly setting the `deleted_by` and `deleted_at` fields in the database. This was happening because the soft deletion services were using `db.flush()` to send SQL statements to the database but were not calling `db.commit()` to actually commit the transaction.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The issue was in the following services and their corresponding router endpoints:
|
||||
|
||||
1. **ShotSoftDeletionService** (`backend/services/shot_soft_deletion.py`)
|
||||
- Method: `soft_delete_shot_cascade()`
|
||||
- Used `db.flush()` but no `db.commit()`
|
||||
|
||||
2. **AssetSoftDeletionService** (`backend/services/asset_soft_deletion.py`)
|
||||
- Method: `soft_delete_asset_cascade()`
|
||||
- Used `db.flush()` but no `db.commit()`
|
||||
|
||||
3. **RecoveryService** (`backend/services/recovery_service.py`)
|
||||
- Methods: `recover_shot()`, `recover_asset()`, `bulk_recover_shots()`, `bulk_recover_assets()`
|
||||
- Used `db.flush()` but no `db.commit()`
|
||||
|
||||
4. **BatchOperationsService** (`backend/services/batch_operations.py`)
|
||||
- Methods: `batch_delete_shots()`, `batch_delete_assets()`, `batch_recover_shots()`, `batch_recover_assets()`
|
||||
- Used `db.flush()` but no `db.commit()`
|
||||
|
||||
## The Difference Between flush() and commit()
|
||||
|
||||
- `db.flush()`: Sends pending SQL statements to the database within the current transaction, but doesn't commit the transaction
|
||||
- `db.commit()`: Actually commits the transaction, making the changes permanent in the database
|
||||
|
||||
Without `db.commit()`, the changes were only visible within the current transaction and would be lost when the transaction ended.
|
||||
|
||||
## Files Fixed
|
||||
|
||||
### Router Endpoints (Added db.commit() calls):
|
||||
|
||||
1. **backend/routers/shots.py**
|
||||
- `delete_shot()` endpoint - Added commit after successful soft deletion
|
||||
|
||||
2. **backend/routers/assets.py**
|
||||
- `delete_asset()` endpoint - Added commit after successful soft deletion
|
||||
|
||||
3. **backend/routers/admin.py**
|
||||
- `recover_shot()` endpoint - Added commit after successful recovery
|
||||
- `recover_asset()` endpoint - Added commit after successful recovery
|
||||
- `bulk_recover_shots()` endpoint - Added commit after successful bulk recovery
|
||||
- `bulk_recover_assets()` endpoint - Added commit after successful bulk recovery
|
||||
- `batch_delete_shots()` endpoint - Added commit after successful batch deletion
|
||||
- `batch_delete_assets()` endpoint - Added commit after successful batch deletion
|
||||
- `batch_recover_shots_enhanced()` endpoint - Added commit after successful batch recovery
|
||||
- `batch_recover_assets_enhanced()` endpoint - Added commit after successful batch recovery
|
||||
|
||||
### Pattern Applied:
|
||||
|
||||
```python
|
||||
# Before (incorrect):
|
||||
result = service.soft_delete_shot_cascade(shot_id, db, current_user)
|
||||
|
||||
if not result.success:
|
||||
raise HTTPException(...)
|
||||
|
||||
return response_data
|
||||
|
||||
# After (correct):
|
||||
result = service.soft_delete_shot_cascade(shot_id, db, current_user)
|
||||
|
||||
if not result.success:
|
||||
db.rollback() # Also added rollback on failure
|
||||
raise HTTPException(...)
|
||||
|
||||
# Commit the transaction
|
||||
db.commit()
|
||||
|
||||
return response_data
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Created `backend/test_shot_deletion_fix.py` to verify the fix works correctly. The test confirms that both `deleted_at` and `deleted_by` fields are now properly set when performing soft deletion operations.
|
||||
|
||||
## Impact
|
||||
|
||||
This fix ensures that:
|
||||
|
||||
1. Soft deletion operations properly set `deleted_by` and `deleted_at` fields
|
||||
2. Recovery operations properly clear these fields
|
||||
3. Batch operations work correctly
|
||||
4. Database transactions are properly committed
|
||||
5. Failed operations are properly rolled back
|
||||
|
||||
## Prevention
|
||||
|
||||
To prevent similar issues in the future:
|
||||
|
||||
1. Always pair `db.flush()` with `db.commit()` in router endpoints
|
||||
2. Add `db.rollback()` calls in error handling
|
||||
3. Consider adding database transaction tests to verify commits work correctly
|
||||
4. Review all service methods that modify database state to ensure proper transaction handling
|
||||
@@ -0,0 +1,60 @@
|
||||
# Task 18: Schema Fix for Custom Status Support
|
||||
|
||||
## Issue
|
||||
The backend was throwing a validation error when trying to return custom task statuses:
|
||||
```
|
||||
pydantic_core._pydantic_core.ValidationError: 1 validation error for TaskStatusInfo
|
||||
status
|
||||
Input should be 'not_started', 'in_progress', 'submitted', 'approved' or 'retake' [type=enum, input_value='custom_ready', input_type=str]
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
The `TaskStatusInfo` schema in both `backend/schemas/asset.py` and `backend/schemas/shot.py` was still using the `TaskStatus` enum for the `status` field, which only accepts the five system statuses.
|
||||
|
||||
## Fix Applied
|
||||
|
||||
### backend/schemas/asset.py
|
||||
Changed the `status` field from `TaskStatus` enum to `str`:
|
||||
```python
|
||||
class TaskStatusInfo(BaseModel):
|
||||
task_type: str # Changed from TaskType enum to str to support custom task types
|
||||
status: str # Changed from TaskStatus enum to str to support custom statuses
|
||||
task_id: Optional[int] = None
|
||||
assigned_user_id: Optional[int] = None
|
||||
```
|
||||
|
||||
### backend/schemas/shot.py
|
||||
Changed the `status` field from `TaskStatus` enum to `str`:
|
||||
```python
|
||||
class TaskStatusInfo(BaseModel):
|
||||
"""Task status information for table display"""
|
||||
task_type: str # String to support custom task types
|
||||
status: str # Changed from TaskStatus enum to str to support custom statuses
|
||||
task_id: Optional[int] = None
|
||||
assigned_user_id: Optional[int] = None
|
||||
```
|
||||
|
||||
### backend/schemas/task.py
|
||||
Changed the `status` field from `TaskStatus` enum to `str` in `TaskListResponse`:
|
||||
```python
|
||||
class TaskListResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
task_type: str # Changed from TaskType enum to str to support custom task types
|
||||
status: str # Changed from TaskStatus enum to str to support custom statuses
|
||||
deadline: Optional[date] = None
|
||||
# ... other fields
|
||||
```
|
||||
|
||||
## Impact
|
||||
- Asset list endpoint (`GET /assets/`) now returns custom statuses correctly
|
||||
- Shot list endpoint (`GET /shots/`) now returns custom statuses correctly
|
||||
- Task list endpoint (`GET /tasks/`) now returns custom statuses correctly
|
||||
- All endpoints can now include custom status IDs in their responses
|
||||
|
||||
## Testing
|
||||
After this fix, the following should work:
|
||||
1. Create a custom status in a project
|
||||
2. Assign the custom status to a task
|
||||
3. View the asset or shot list - the custom status should appear without validation errors
|
||||
4. The frontend EditableTaskStatus components should display and update custom statuses correctly
|
||||
@@ -0,0 +1,188 @@
|
||||
# Task 5 Implementation Summary: Update Custom Task Status Endpoint
|
||||
|
||||
## Task Description
|
||||
|
||||
Implement PUT endpoint for updating custom task statuses in a project.
|
||||
|
||||
**Requirements Implemented:**
|
||||
- 2.1: Support updating name
|
||||
- 2.2: Support updating color
|
||||
- 2.3: Support updating is_default flag
|
||||
- 5.2: If setting as default, unset other default statuses
|
||||
|
||||
## Implementation
|
||||
|
||||
### Endpoint Added
|
||||
|
||||
**File:** `backend/routers/projects.py`
|
||||
|
||||
**Endpoint:** `PUT /projects/{project_id}/task-statuses/{status_id}`
|
||||
|
||||
**Authorization:** Requires coordinator or admin role
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **Name Update (Requirement 2.1)**
|
||||
- Validates name uniqueness within project
|
||||
- Checks against other custom statuses
|
||||
- Checks against system status names
|
||||
- Returns 409 Conflict if name already exists
|
||||
|
||||
2. **Color Update (Requirement 2.2)**
|
||||
- Accepts hex color codes (e.g., #FF5733)
|
||||
- Validates color format via schema
|
||||
- Updates color independently of other fields
|
||||
|
||||
3. **Default Status Management (Requirement 2.3, 5.2)**
|
||||
- When setting a status as default, automatically unsets all other defaults
|
||||
- Ensures only one default status exists at a time
|
||||
- Allows unsetting default status
|
||||
|
||||
4. **JSON Column Updates**
|
||||
- Uses `flag_modified` to ensure SQLAlchemy detects changes to JSON columns
|
||||
- Properly persists changes to database
|
||||
|
||||
### Request/Response
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string (optional)", // New status name (1-50 chars)
|
||||
"color": "string (optional)", // Hex color code (e.g., #FF5733)
|
||||
"is_default": "boolean (optional)" // Set as default status
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Custom task status updated successfully",
|
||||
"status": {
|
||||
"id": "custom_abc123",
|
||||
"name": "Updated Name",
|
||||
"color": "#00FF00",
|
||||
"order": 0,
|
||||
"is_default": true
|
||||
},
|
||||
"all_statuses": {
|
||||
"statuses": [...], // All custom statuses
|
||||
"system_statuses": [...], // System statuses
|
||||
"default_status_id": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **404 Not Found:** Project or status doesn't exist
|
||||
- **409 Conflict:** Name already exists or conflicts with system status
|
||||
- **403 Forbidden:** User is not coordinator or admin
|
||||
- **422 Unprocessable Entity:** Invalid request body format
|
||||
|
||||
### Code Structure
|
||||
|
||||
The implementation follows this flow:
|
||||
|
||||
1. Validate input using `CustomTaskStatusUpdate` schema
|
||||
2. Verify project exists
|
||||
3. Load custom statuses from JSON column
|
||||
4. Find status to update by ID
|
||||
5. Validate name uniqueness if name is being changed
|
||||
6. Update name, color, and/or is_default flag
|
||||
7. If setting as default, unset all other defaults
|
||||
8. Update status in the list
|
||||
9. Use `flag_modified` to mark JSON column as changed
|
||||
10. Commit to database
|
||||
11. Return updated status and all statuses
|
||||
|
||||
### Testing
|
||||
|
||||
Created test files:
|
||||
- `backend/test_update_status_manual.py` - Manual test script (requires running server)
|
||||
- `backend/docs/custom-task-status-update-endpoint.md` - Complete documentation
|
||||
|
||||
### Validation
|
||||
|
||||
The implementation includes comprehensive validation:
|
||||
|
||||
✅ Name uniqueness within project
|
||||
✅ System status name conflict detection
|
||||
✅ Color format validation (hex codes)
|
||||
✅ Only one default status at a time
|
||||
✅ Proper JSON column updates with flag_modified
|
||||
✅ Non-existent status handling
|
||||
✅ Authorization checks
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **backend/routers/projects.py**
|
||||
- Added `update_custom_task_status` endpoint (lines ~1506-1640)
|
||||
|
||||
## Files Created
|
||||
|
||||
1. **backend/docs/custom-task-status-update-endpoint.md**
|
||||
- Complete endpoint documentation
|
||||
- Usage examples
|
||||
- Error handling details
|
||||
|
||||
2. **backend/test_update_status_manual.py**
|
||||
- Manual test script for verification
|
||||
- Tests all requirements
|
||||
|
||||
3. **backend/docs/task-5-implementation-summary.md**
|
||||
- This summary document
|
||||
|
||||
## Next Steps
|
||||
|
||||
The next task in the implementation plan is:
|
||||
|
||||
**Task 6:** Backend: Implement DELETE endpoint for deleting custom status
|
||||
- Check if status is in use by any tasks
|
||||
- Support optional reassignment of tasks to another status
|
||||
- If deleting default status, auto-assign new default
|
||||
- Prevent deletion of last status
|
||||
|
||||
## Verification
|
||||
|
||||
To verify the implementation:
|
||||
|
||||
1. Start the backend server:
|
||||
```bash
|
||||
cd backend
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
2. Run the manual test script:
|
||||
```bash
|
||||
python test_update_status_manual.py
|
||||
```
|
||||
|
||||
3. Or test manually using curl:
|
||||
```bash
|
||||
# Update name
|
||||
curl -X PUT "http://localhost:8000/projects/1/task-statuses/custom_abc123" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "New Name"}'
|
||||
|
||||
# Update color
|
||||
curl -X PUT "http://localhost:8000/projects/1/task-statuses/custom_abc123" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"color": "#00FF00"}'
|
||||
|
||||
# Set as default
|
||||
curl -X PUT "http://localhost:8000/projects/1/task-statuses/custom_abc123" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"is_default": true}'
|
||||
```
|
||||
|
||||
## Requirements Coverage
|
||||
|
||||
✅ **Requirement 2.1:** Support updating name - Implemented with uniqueness validation
|
||||
✅ **Requirement 2.2:** Support updating color - Implemented with format validation
|
||||
✅ **Requirement 2.3:** Support updating is_default flag - Implemented
|
||||
✅ **Requirement 5.2:** If setting as default, unset other default statuses - Implemented
|
||||
|
||||
All requirements for Task 5 have been successfully implemented and verified.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Task 5 Implementation Verification
|
||||
|
||||
## Task: Backend - Implement PUT endpoint for updating custom status
|
||||
|
||||
**Status:** ✅ COMPLETED
|
||||
|
||||
## Requirements Verification
|
||||
|
||||
### Requirement 2.1: Support updating name
|
||||
✅ **IMPLEMENTED**
|
||||
- Location: `backend/routers/projects.py` lines 1565-1593
|
||||
- The endpoint accepts an optional `name` field in the request body
|
||||
- Updates the status name when provided
|
||||
- Validates name uniqueness before updating
|
||||
|
||||
### Requirement 2.2: Support updating color
|
||||
✅ **IMPLEMENTED**
|
||||
- Location: `backend/routers/projects.py` lines 1595-1597
|
||||
- The endpoint accepts an optional `color` field in the request body
|
||||
- Updates the status color when provided
|
||||
- Color format validation is handled by the `CustomTaskStatusUpdate` schema
|
||||
|
||||
### Requirement 2.3: Support updating is_default flag
|
||||
✅ **IMPLEMENTED**
|
||||
- Location: `backend/routers/projects.py` lines 1599-1611
|
||||
- The endpoint accepts an optional `is_default` field in the request body
|
||||
- Updates the default status flag when provided
|
||||
- Handles both setting and unsetting the default flag
|
||||
|
||||
### Requirement 5.2: If setting as default, unset other default statuses
|
||||
✅ **IMPLEMENTED**
|
||||
- Location: `backend/routers/projects.py` lines 1601-1607
|
||||
- When `is_default` is set to `True`, the endpoint automatically unsets all other default statuses
|
||||
- Ensures only one default status exists at a time
|
||||
- This is done by iterating through all custom statuses and setting their `is_default` to `False` before setting the current status as default
|
||||
|
||||
### Additional Requirement: Validate name uniqueness if name is changed
|
||||
✅ **IMPLEMENTED**
|
||||
- Location: `backend/routers/projects.py` lines 1565-1593
|
||||
- Validates that the new name doesn't conflict with:
|
||||
- Other custom statuses in the same project
|
||||
- System status names
|
||||
- Returns 409 Conflict error if name already exists
|
||||
|
||||
### Additional Requirement: Use flag_modified for JSON column updates
|
||||
✅ **IMPLEMENTED**
|
||||
- Location: `backend/routers/projects.py` line 1617
|
||||
- Uses `flag_modified(db_project, 'custom_task_statuses')` to ensure SQLAlchemy detects changes to the JSON column
|
||||
- This is critical for proper database persistence of JSON column updates
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Endpoint Signature
|
||||
```python
|
||||
@router.put("/{project_id}/task-statuses/{status_id}")
|
||||
async def update_custom_task_status(
|
||||
project_id: int,
|
||||
status_id: str,
|
||||
status_update: dict,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_coordinator_or_admin)
|
||||
)
|
||||
```
|
||||
|
||||
### Request Body Schema
|
||||
Uses `CustomTaskStatusUpdate` from `backend/schemas/custom_task_status.py`:
|
||||
```python
|
||||
{
|
||||
"name": "string (optional)", # 1-50 characters
|
||||
"color": "string (optional)", # Hex color code (e.g., #FF5733)
|
||||
"is_default": "boolean (optional)" # Set as default status
|
||||
}
|
||||
```
|
||||
|
||||
### Response Schema
|
||||
Returns `CustomTaskStatusResponse`:
|
||||
```python
|
||||
{
|
||||
"message": "string",
|
||||
"status": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"color": "string",
|
||||
"order": "integer",
|
||||
"is_default": "boolean"
|
||||
},
|
||||
"all_statuses": {
|
||||
"statuses": [...], # All custom statuses
|
||||
"system_statuses": [...], # System statuses
|
||||
"default_status_id": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Partial Updates**: All fields are optional, allowing partial updates
|
||||
2. **Name Uniqueness Validation**: Prevents duplicate names within a project
|
||||
3. **System Status Protection**: Prevents using system status names
|
||||
4. **Default Status Management**: Automatically manages default status uniqueness
|
||||
5. **JSON Column Handling**: Properly uses `flag_modified` for database persistence
|
||||
6. **Authorization**: Requires coordinator or admin role
|
||||
7. **Comprehensive Error Handling**: Returns appropriate HTTP status codes
|
||||
|
||||
## Error Responses
|
||||
|
||||
- **404 Not Found**: Project or status doesn't exist
|
||||
- **409 Conflict**: Name already exists or conflicts with system status
|
||||
- **403 Forbidden**: User lacks coordinator/admin permissions
|
||||
- **422 Unprocessable Entity**: Invalid request body format
|
||||
|
||||
## Testing
|
||||
|
||||
Comprehensive test suite exists at `backend/test_update_custom_task_status.py` covering:
|
||||
- ✅ Status name update
|
||||
- ✅ Status color update
|
||||
- ✅ Both name and color update
|
||||
- ✅ Name uniqueness validation
|
||||
- ✅ System status name conflict detection
|
||||
- ✅ Setting status as default
|
||||
- ✅ Changing default status (unsets previous default)
|
||||
- ✅ Unsetting default status
|
||||
- ✅ Non-existent status handling
|
||||
- ✅ JSON column persistence verification
|
||||
|
||||
## Documentation
|
||||
|
||||
Complete documentation available at:
|
||||
- `backend/docs/custom-task-status-update-endpoint.md`
|
||||
|
||||
## Conclusion
|
||||
|
||||
Task 5 is **FULLY IMPLEMENTED** and meets all specified requirements:
|
||||
- ✅ PUT endpoint added to `backend/routers/projects.py`
|
||||
- ✅ Supports updating name, color, and is_default flag
|
||||
- ✅ Validates name uniqueness when name is changed
|
||||
- ✅ Unsets other default statuses when setting a new default
|
||||
- ✅ Uses `flag_modified` for JSON column updates
|
||||
- ✅ Comprehensive test coverage
|
||||
- ✅ Complete documentation
|
||||
|
||||
The implementation is production-ready and follows best practices for FastAPI development.
|
||||
@@ -0,0 +1,240 @@
|
||||
# Task 5 Requirements Checklist
|
||||
|
||||
## Task: Backend: Implement PUT endpoint for updating custom status
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
#### ✅ Requirement 2.1: Support updating name
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
# Validate name uniqueness if name is being changed
|
||||
if status_update.name is not None and status_update.name != status_to_update.get('name'):
|
||||
# Check against other custom statuses
|
||||
existing_names = [
|
||||
s.get('name', '').lower()
|
||||
for i, s in enumerate(custom_statuses_data)
|
||||
if isinstance(s, dict) and i != status_index
|
||||
]
|
||||
|
||||
# Check against system statuses
|
||||
system_names = [s['name'].lower() for s in SYSTEM_TASK_STATUSES]
|
||||
|
||||
if status_update.name.lower() in existing_names:
|
||||
raise HTTPException(status_code=409, ...)
|
||||
|
||||
if status_update.name.lower() in system_names:
|
||||
raise HTTPException(status_code=409, ...)
|
||||
|
||||
# Update name
|
||||
status_to_update['name'] = status_update.name
|
||||
```
|
||||
|
||||
**Verified:**
|
||||
- ✅ Name can be updated
|
||||
- ✅ Name validation works
|
||||
- ✅ Uniqueness check implemented
|
||||
- ✅ Returns updated status
|
||||
|
||||
---
|
||||
|
||||
#### ✅ Requirement 2.2: Support updating color
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
# Update color if provided
|
||||
if status_update.color is not None:
|
||||
status_to_update['color'] = status_update.color
|
||||
```
|
||||
|
||||
**Verified:**
|
||||
- ✅ Color can be updated
|
||||
- ✅ Color format validated by schema (hex codes)
|
||||
- ✅ Color updates independently of other fields
|
||||
- ✅ Returns updated status
|
||||
|
||||
---
|
||||
|
||||
#### ✅ Requirement 2.3: Support updating is_default flag
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
# Handle is_default flag
|
||||
if status_update.is_default is not None:
|
||||
if status_update.is_default:
|
||||
# If setting as default, unset other default statuses
|
||||
for status_data in custom_statuses_data:
|
||||
if isinstance(status_data, dict):
|
||||
status_data['is_default'] = False
|
||||
|
||||
# Set this status as default
|
||||
status_to_update['is_default'] = True
|
||||
else:
|
||||
# Just unset this status as default
|
||||
status_to_update['is_default'] = False
|
||||
```
|
||||
|
||||
**Verified:**
|
||||
- ✅ is_default flag can be updated
|
||||
- ✅ Can set status as default
|
||||
- ✅ Can unset status as default
|
||||
- ✅ Returns updated status
|
||||
|
||||
---
|
||||
|
||||
#### ✅ Requirement 5.2: If setting as default, unset other default statuses
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
if status_update.is_default:
|
||||
# If setting as default, unset other default statuses
|
||||
for status_data in custom_statuses_data:
|
||||
if isinstance(status_data, dict):
|
||||
status_data['is_default'] = False
|
||||
|
||||
# Set this status as default
|
||||
status_to_update['is_default'] = True
|
||||
```
|
||||
|
||||
**Verified:**
|
||||
- ✅ When setting a status as default, all other defaults are unset
|
||||
- ✅ Only one default status exists at a time
|
||||
- ✅ Default status ID is correctly returned in response
|
||||
- ✅ Previous default is properly unset
|
||||
|
||||
---
|
||||
|
||||
#### ✅ Additional Requirement: Validate name uniqueness if name is changed
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
# Validate name uniqueness if name is being changed
|
||||
if status_update.name is not None and status_update.name != status_to_update.get('name'):
|
||||
# Check against other custom statuses
|
||||
existing_names = [...]
|
||||
|
||||
# Check against system statuses
|
||||
system_names = [...]
|
||||
|
||||
if status_update.name.lower() in existing_names:
|
||||
raise HTTPException(status_code=409, ...)
|
||||
|
||||
if status_update.name.lower() in system_names:
|
||||
raise HTTPException(status_code=409, ...)
|
||||
```
|
||||
|
||||
**Verified:**
|
||||
- ✅ Name uniqueness validated within project
|
||||
- ✅ Checks against other custom statuses
|
||||
- ✅ Checks against system status names
|
||||
- ✅ Returns 409 Conflict on duplicate
|
||||
|
||||
---
|
||||
|
||||
#### ✅ Additional Requirement: Use flag_modified for JSON column updates
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
# Update the status in the list
|
||||
custom_statuses_data[status_index] = status_to_update
|
||||
db_project.custom_task_statuses = custom_statuses_data
|
||||
|
||||
# Use flag_modified for JSON column updates
|
||||
flag_modified(db_project, 'custom_task_statuses')
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_project)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(...)
|
||||
```
|
||||
|
||||
**Verified:**
|
||||
- ✅ flag_modified is used
|
||||
- ✅ Changes persist to database
|
||||
- ✅ JSON column updates work correctly
|
||||
- ✅ Rollback on error
|
||||
|
||||
---
|
||||
|
||||
### Endpoint Details
|
||||
|
||||
**Route:** `PUT /projects/{project_id}/task-statuses/{status_id}`
|
||||
|
||||
**Authorization:** Requires coordinator or admin role
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string (optional)",
|
||||
"color": "string (optional)",
|
||||
"is_default": "boolean (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `CustomTaskStatusResponse`
|
||||
```json
|
||||
{
|
||||
"message": "string",
|
||||
"status": { ... },
|
||||
"all_statuses": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Error Codes:**
|
||||
- 404: Project or status not found
|
||||
- 409: Name conflict (duplicate or system status)
|
||||
- 403: Insufficient permissions
|
||||
- 422: Invalid request body
|
||||
|
||||
---
|
||||
|
||||
### Test Coverage
|
||||
|
||||
**Manual Tests Created:**
|
||||
- ✅ `test_update_status_manual.py` - Comprehensive manual test script
|
||||
- ✅ Tests all requirements
|
||||
- ✅ Tests error conditions
|
||||
- ✅ Tests edge cases
|
||||
|
||||
**Documentation Created:**
|
||||
- ✅ `custom-task-status-update-endpoint.md` - Complete API documentation
|
||||
- ✅ `task-5-implementation-summary.md` - Implementation summary
|
||||
- ✅ `task-5-requirements-checklist.md` - This checklist
|
||||
|
||||
---
|
||||
|
||||
### Code Quality
|
||||
|
||||
**Validation:**
|
||||
- ✅ No syntax errors
|
||||
- ✅ No linting errors
|
||||
- ✅ Follows existing code patterns
|
||||
- ✅ Proper error handling
|
||||
- ✅ Comprehensive validation
|
||||
- ✅ Clear error messages
|
||||
|
||||
**Best Practices:**
|
||||
- ✅ Uses dependency injection
|
||||
- ✅ Proper authorization checks
|
||||
- ✅ Transaction management (commit/rollback)
|
||||
- ✅ Input validation via Pydantic schemas
|
||||
- ✅ Consistent response format
|
||||
- ✅ Proper HTTP status codes
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **All requirements implemented and verified**
|
||||
|
||||
The PUT endpoint for updating custom task statuses has been successfully implemented with:
|
||||
- Full support for updating name, color, and is_default flag
|
||||
- Comprehensive validation (uniqueness, format, conflicts)
|
||||
- Proper default status management (only one default at a time)
|
||||
- Correct JSON column updates using flag_modified
|
||||
- Complete error handling and authorization
|
||||
- Comprehensive documentation and test scripts
|
||||
|
||||
**Task Status:** ✅ COMPLETED
|
||||
@@ -0,0 +1,277 @@
|
||||
# Task 8: Backend Task Endpoints Custom Status Support
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation adds support for custom task statuses to all task-related endpoints in the backend. The system now validates statuses against both system statuses and project-specific custom statuses, ensuring proper status resolution across project boundaries.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Added Helper Functions (`backend/routers/tasks.py`)
|
||||
|
||||
#### `get_project_default_status(db: Session, project_id: int) -> str`
|
||||
- Retrieves the default status for a project
|
||||
- Checks for custom statuses with `is_default=True` flag
|
||||
- Falls back to system default "not_started" if no custom default is set
|
||||
- Handles JSON parsing for custom_task_statuses field
|
||||
|
||||
#### `validate_task_status(db: Session, project_id: int, status_value: str) -> bool`
|
||||
- Validates that a status exists for a specific project
|
||||
- Checks system statuses first (not_started, in_progress, submitted, approved, retake)
|
||||
- Then checks project-specific custom statuses
|
||||
- Returns True if valid, False otherwise
|
||||
- Ensures status isolation between projects
|
||||
|
||||
### 2. Updated Task Creation Endpoint
|
||||
|
||||
**Endpoint**: `POST /tasks/`
|
||||
|
||||
**Changes**:
|
||||
- Uses `get_project_default_status()` when no status is specified
|
||||
- Validates provided status using `validate_task_status()`
|
||||
- Returns 400 error if invalid status is provided
|
||||
- Maintains backward compatibility with system statuses
|
||||
|
||||
**Example**:
|
||||
```python
|
||||
# Use default status if not specified
|
||||
task_data = task.model_dump()
|
||||
if not task_data.get('status') or task_data['status'] == 'not_started':
|
||||
task_data['status'] = get_project_default_status(db, task.project_id)
|
||||
else:
|
||||
# Validate the provided status
|
||||
if not validate_task_status(db, task.project_id, task_data['status']):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid status '{task_data['status']}' for this project"
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Updated Task Status Update Endpoint
|
||||
|
||||
**Endpoint**: `PUT /tasks/{task_id}/status`
|
||||
|
||||
**Changes**:
|
||||
- Validates new status against project's available statuses
|
||||
- Returns 400 error if status is invalid for the task's project
|
||||
- Supports both system and custom statuses
|
||||
|
||||
**Example**:
|
||||
```python
|
||||
# Validate the status for the task's project
|
||||
if not validate_task_status(db, task.project_id, status_update.status):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid status '{status_update.status}' for this project"
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Updated Task Update Endpoint
|
||||
|
||||
**Endpoint**: `PUT /tasks/{task_id}`
|
||||
|
||||
**Changes**:
|
||||
- Validates status field if it's being updated
|
||||
- Checks status validity before applying update
|
||||
- Maintains existing permission checks
|
||||
|
||||
### 5. Updated Bulk Status Update Endpoint
|
||||
|
||||
**Endpoint**: `PUT /tasks/bulk/status`
|
||||
|
||||
**Changes**:
|
||||
- Validates status for each task's project before updating
|
||||
- Ensures atomic updates - all tasks succeed or all fail
|
||||
- Provides detailed error messages for invalid statuses
|
||||
- Handles tasks from different projects correctly
|
||||
|
||||
**Example**:
|
||||
```python
|
||||
# Validate status for the task's project
|
||||
if not validate_task_status(db, task.project_id, bulk_update.status):
|
||||
errors.append({
|
||||
"task_id": task_id,
|
||||
"error": f"Invalid status '{bulk_update.status}' for task's project"
|
||||
})
|
||||
failed_count += 1
|
||||
continue
|
||||
```
|
||||
|
||||
## Requirements Addressed
|
||||
|
||||
### Requirement 5.3: Default Status for New Tasks
|
||||
✅ Task creation uses project's default status when not specified
|
||||
✅ Falls back to system default "not_started" for projects without custom statuses
|
||||
|
||||
### Requirement 6.4: Backward Compatibility
|
||||
✅ System statuses (not_started, in_progress, submitted, approved, retake) remain valid for all projects
|
||||
✅ Existing tasks with system statuses continue to work
|
||||
|
||||
### Requirement 6.5: Status Resolution
|
||||
✅ Status validation checks both system and custom statuses
|
||||
✅ Custom statuses are project-specific and don't leak between projects
|
||||
|
||||
### Requirement 10.1: Bulk Status Update Validation
|
||||
✅ Bulk updates validate status for each task's project
|
||||
✅ Atomic updates ensure consistency
|
||||
|
||||
### Requirement 10.2: Cross-Project Handling
|
||||
✅ Tasks from different projects can be updated in bulk
|
||||
✅ Each task's status is validated against its own project's statuses
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Created comprehensive test suite (`backend/test_task_custom_status_support.py`) covering:
|
||||
|
||||
1. **Task Creation with Default Status**
|
||||
- ✅ Default status correctly identified from custom statuses
|
||||
- ✅ Tasks created without explicit status use project default
|
||||
- ✅ Tasks can be created with explicit custom status
|
||||
|
||||
2. **Status Validation**
|
||||
- ✅ All system statuses validated successfully
|
||||
- ✅ Custom statuses validated for their project
|
||||
- ✅ Invalid statuses correctly rejected
|
||||
|
||||
3. **Status Resolution Across Projects**
|
||||
- ✅ Project 1 custom statuses rejected for Project 2
|
||||
- ✅ Project 2 custom statuses rejected for Project 1
|
||||
- ✅ System statuses valid for all projects
|
||||
- ✅ Each project has its own default status
|
||||
|
||||
4. **Bulk Status Update Validation**
|
||||
- ✅ Status validated for each task's project
|
||||
- ✅ System statuses valid for all tasks
|
||||
- ✅ Custom statuses only valid for their project
|
||||
|
||||
5. **Projects Without Custom Statuses**
|
||||
- ✅ Use system default "not_started"
|
||||
- ✅ System statuses remain valid
|
||||
- ✅ Custom statuses from other projects rejected
|
||||
|
||||
### Test Results
|
||||
|
||||
```
|
||||
============================================================
|
||||
Testing Task Endpoints Custom Status Support
|
||||
============================================================
|
||||
|
||||
=== Test 1: Task Creation with Default Status ===
|
||||
✓ Default status correctly identified: custom_todo
|
||||
✓ Task created with default status: custom_todo
|
||||
✓ Task created with explicit custom status: custom_doing
|
||||
✓ Test 1 PASSED
|
||||
|
||||
=== Test 2: Status Validation ===
|
||||
✓ All system statuses validated successfully
|
||||
✓ All custom statuses validated successfully
|
||||
✓ Invalid statuses correctly rejected
|
||||
✓ Test 2 PASSED
|
||||
|
||||
=== Test 3: Status Resolution Across Projects ===
|
||||
✓ Project 1 custom status correctly rejected for Project 2
|
||||
✓ Project 2 custom status correctly rejected for Project 1
|
||||
✓ System statuses valid for both projects
|
||||
✓ Project 1 default: custom_todo, Project 2 default: project2_status
|
||||
✓ Test 3 PASSED
|
||||
|
||||
=== Test 4: Bulk Status Update Validation ===
|
||||
✓ Validated 2 tasks from Project 1
|
||||
✓ Validated 0 tasks from Project 2
|
||||
✓ System statuses valid for all 2 tasks
|
||||
✓ Test 4 PASSED
|
||||
|
||||
=== Test 5: Project Without Custom Statuses ===
|
||||
✓ Project without custom statuses uses system default: not_started
|
||||
✓ System statuses valid for project without custom statuses
|
||||
✓ Custom statuses from other projects correctly rejected
|
||||
✓ Test 5 PASSED
|
||||
|
||||
============================================================
|
||||
✓ ALL TESTS PASSED
|
||||
============================================================
|
||||
```
|
||||
|
||||
## API Behavior
|
||||
|
||||
### Task Creation
|
||||
```json
|
||||
POST /tasks/
|
||||
{
|
||||
"project_id": 1,
|
||||
"name": "New Task",
|
||||
"task_type": "modeling",
|
||||
// status not specified - will use project default
|
||||
}
|
||||
```
|
||||
|
||||
### Task Status Update
|
||||
```json
|
||||
PUT /tasks/123/status
|
||||
{
|
||||
"status": "custom_todo" // Must be valid for task's project
|
||||
}
|
||||
```
|
||||
|
||||
### Bulk Status Update
|
||||
```json
|
||||
PUT /tasks/bulk/status
|
||||
{
|
||||
"task_ids": [1, 2, 3],
|
||||
"status": "custom_doing" // Validated for each task's project
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Invalid Status Error
|
||||
```json
|
||||
{
|
||||
"detail": "Invalid status 'custom_nonexistent' for this project"
|
||||
}
|
||||
```
|
||||
|
||||
### Bulk Update Errors
|
||||
```json
|
||||
{
|
||||
"success_count": 2,
|
||||
"failed_count": 1,
|
||||
"errors": [
|
||||
{
|
||||
"task_id": 3,
|
||||
"error": "Invalid status 'custom_todo' for task's project"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
No database changes required. The implementation uses existing fields:
|
||||
- `projects.custom_task_statuses` (JSON) - stores custom statuses
|
||||
- `tasks.status` (String) - stores status value
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
✅ **Fully backward compatible**
|
||||
- Existing tasks with system statuses continue to work
|
||||
- Projects without custom statuses use system defaults
|
||||
- System statuses remain valid for all projects
|
||||
- No migration required
|
||||
|
||||
## Next Steps
|
||||
|
||||
The frontend components should be updated to:
|
||||
1. Fetch custom statuses from `/projects/{id}/task-statuses` endpoint
|
||||
2. Display custom status colors in UI
|
||||
3. Use custom statuses in status dropdowns and filters
|
||||
4. Handle validation errors from backend
|
||||
|
||||
## Related Files
|
||||
|
||||
- `backend/routers/tasks.py` - Main implementation
|
||||
- `backend/routers/projects.py` - Custom status management endpoints
|
||||
- `backend/models/task.py` - Task model (status field is String)
|
||||
- `backend/models/project.py` - Project model (custom_task_statuses field)
|
||||
- `backend/test_task_custom_status_support.py` - Test suite
|
||||
@@ -0,0 +1,101 @@
|
||||
# Task Status Index Optimization Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the database schema and index optimization implemented for the shot-asset-task-status-optimization feature. The optimization addresses the N+1 query problem identified in the current shot and asset data fetching patterns.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The current implementation suffers from N+1 query patterns:
|
||||
- **Main Query**: Fetches shots/assets first
|
||||
- **Per-Entity Query**: For each shot/asset, runs separate query for tasks
|
||||
- **Application-Level Aggregation**: Task status building happens in Python loops
|
||||
|
||||
For 100 shots, this results in 101 database queries (1 for shots + 100 for tasks).
|
||||
|
||||
## Solution Implemented
|
||||
|
||||
### New Database Indexes Created
|
||||
|
||||
The following indexes were created to optimize task status queries:
|
||||
|
||||
1. **`idx_tasks_shot_id_active`**
|
||||
- Optimizes task lookups by shot_id (active tasks only)
|
||||
- Includes WHERE clause: `deleted_at IS NULL`
|
||||
|
||||
2. **`idx_tasks_asset_id_active`**
|
||||
- Optimizes task lookups by asset_id (active tasks only)
|
||||
- Includes WHERE clause: `deleted_at IS NULL`
|
||||
|
||||
3. **`idx_tasks_status_type_active`**
|
||||
- Optimizes task status and type filtering
|
||||
- Covers: `(status, task_type)` with `deleted_at IS NULL`
|
||||
|
||||
4. **`idx_tasks_shot_status_type_active`**
|
||||
- Composite index for shot + status + type queries
|
||||
- Covers: `(shot_id, status, task_type)` with `deleted_at IS NULL`
|
||||
|
||||
5. **`idx_tasks_asset_status_type_active`**
|
||||
- Composite index for asset + status + type queries
|
||||
- Covers: `(asset_id, status, task_type)` with `deleted_at IS NULL`
|
||||
|
||||
6. **`idx_tasks_details_shot`**
|
||||
- Optimizes queries needing full task details for shots
|
||||
- Covers: `(shot_id, id, task_type, status, assigned_user_id, updated_at)`
|
||||
|
||||
7. **`idx_tasks_details_asset`**
|
||||
- Optimizes queries needing full task details for assets
|
||||
- Covers: `(asset_id, id, task_type, status, assigned_user_id, updated_at)`
|
||||
|
||||
8. **`idx_tasks_project_status_active`**
|
||||
- Optimizes project-wide task queries with status filtering
|
||||
- Covers: `(project_id, status, task_type)` with `deleted_at IS NULL`
|
||||
|
||||
### Performance Results
|
||||
|
||||
Testing with the current dataset (1,444 tasks, 441 shots, 15 assets):
|
||||
|
||||
| Query Type | Execution Time | Performance |
|
||||
|------------|----------------|-------------|
|
||||
| Shot list with task aggregation (441 shots) | 6ms | ✅ Excellent |
|
||||
| Asset list with task aggregation (15 assets) | 1ms | ✅ Excellent |
|
||||
| Project dashboard (1,444 tasks) | 1ms | ✅ Excellent |
|
||||
| Task browser with filtering | 1ms | ✅ Excellent |
|
||||
| Complex aggregation statistics | 4ms | ✅ Excellent |
|
||||
|
||||
**All queries perform well under the 500ms requirement**, with most completing in under 10ms.
|
||||
|
||||
### Index Usage Verification
|
||||
|
||||
Query plan analysis confirms that all new indexes are being used correctly:
|
||||
|
||||
- ✅ `idx_tasks_shot_id_active` used for shot task lookups
|
||||
- ✅ `idx_tasks_asset_id_active` used for asset task lookups
|
||||
- ✅ `idx_tasks_status_type_active` used for status filtering
|
||||
- ✅ `idx_tasks_shot_status_type_active` used for shot+status combinations
|
||||
- ✅ `idx_tasks_asset_status_type_active` used for asset+status combinations
|
||||
|
||||
## Files Created
|
||||
|
||||
1. **`create_task_status_indexes.py`** - Main index creation script
|
||||
2. **`test_index_performance.py`** - Performance testing with realistic queries
|
||||
3. **`test_index_scalability.py`** - Scalability testing with current dataset
|
||||
4. **`check_indexes.py`** - Utility to inspect current database indexes
|
||||
|
||||
## Next Steps
|
||||
|
||||
The database optimization is complete and ready for the next phase:
|
||||
|
||||
1. **Backend Router Optimization** - Implement optimized query patterns in shot/asset routers
|
||||
2. **Frontend Component Updates** - Remove redundant API calls in components
|
||||
3. **Integration Testing** - Test end-to-end performance improvements
|
||||
|
||||
## Requirements Validation
|
||||
|
||||
This implementation satisfies the following requirements:
|
||||
|
||||
- ✅ **Requirement 3.1**: Uses optimized SQL joins for single database round trips
|
||||
- ✅ **Requirement 3.2**: Maintains query performance through proper indexing strategies
|
||||
- ✅ **Requirement 1.5 & 2.5**: Completes data fetching in under 500ms for 100+ entities
|
||||
|
||||
The database schema optimization provides the foundation for eliminating N+1 query patterns and achieving significant performance improvements in shot and asset data table rendering.
|
||||
Reference in New Issue
Block a user