Skip to content

Commit 490a7ee

Browse files
committed
Implement Cloudinary integration with backend services and frontend components
1 parent fa03032 commit 490a7ee

8 files changed

Lines changed: 553 additions & 0 deletions

File tree

backend/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from routes.legal_ai import bp as legal_ai
44
from routes.contracts import contracts
55
from routes.performance import performance
6+
from routes.uploads import uploads
67
from dotenv import load_dotenv
78
import os
89
from datetime import datetime
@@ -22,6 +23,7 @@
2223
app.register_blueprint(legal_ai)
2324
app.register_blueprint(contracts)
2425
app.register_blueprint(performance)
26+
app.register_blueprint(uploads)
2527

2628
# Create data directory if it doesn't exist
2729
if not os.path.exists('data'):

backend/routes/uploads.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from flask import Blueprint, request, jsonify, current_app
2+
import os
3+
import uuid
4+
import tempfile
5+
from werkzeug.utils import secure_filename
6+
from services.cloudinary_service import cloudinary_service, DOCUMENT_UPLOAD_PRESET, TEMPLATE_UPLOAD_PRESET, USER_UPLOAD_PRESET
7+
8+
# Create blueprint
9+
uploads = Blueprint('uploads', __name__, url_prefix='/api/uploads')
10+
11+
@uploads.route('/signature', methods=['GET'])
12+
def get_upload_signature():
13+
"""Get a signature for direct uploads from frontend"""
14+
preset_type = request.args.get('type', 'user')
15+
16+
# Determine which preset to use
17+
if preset_type == 'document':
18+
preset_name = DOCUMENT_UPLOAD_PRESET
19+
elif preset_type == 'template':
20+
preset_name = TEMPLATE_UPLOAD_PRESET
21+
else:
22+
preset_name = USER_UPLOAD_PRESET
23+
24+
try:
25+
# Get signature from service
26+
result = cloudinary_service.get_upload_signature(preset_name)
27+
return jsonify(result)
28+
except Exception as e:
29+
current_app.logger.error(f"Error getting upload signature: {str(e)}")
30+
return jsonify({"error": "Could not generate upload signature"}), 500
31+
32+
@uploads.route('/server', methods=['POST'])
33+
def upload_file():
34+
"""Upload a file from the server to Cloudinary"""
35+
if 'file' not in request.files:
36+
return jsonify({"error": "No file part"}), 400
37+
38+
file = request.files['file']
39+
if file.filename == '':
40+
return jsonify({"error": "No selected file"}), 400
41+
42+
# Get parameters from request
43+
folder = request.form.get('folder')
44+
resource_type = request.form.get('resource_type', 'auto')
45+
public_id = request.form.get('public_id')
46+
47+
# Save the file temporarily
48+
try:
49+
filename = secure_filename(file.filename)
50+
temp_file_path = os.path.join(tempfile.gettempdir(), f"{uuid.uuid4()}_{filename}")
51+
file.save(temp_file_path)
52+
53+
# Upload to Cloudinary
54+
result = cloudinary_service.upload_file(
55+
temp_file_path,
56+
public_id=public_id,
57+
folder=folder,
58+
resource_type=resource_type
59+
)
60+
61+
# Clean up the temp file
62+
os.remove(temp_file_path)
63+
64+
return jsonify(result)
65+
except Exception as e:
66+
current_app.logger.error(f"Error uploading file: {str(e)}")
67+
# Clean up the temp file if it exists
68+
if 'temp_file_path' in locals() and os.path.exists(temp_file_path):
69+
os.remove(temp_file_path)
70+
71+
return jsonify({"error": "Failed to upload file"}), 500
72+
73+
@uploads.route('/<public_id>', methods=['DELETE'])
74+
def delete_file(public_id):
75+
"""Delete a file from Cloudinary"""
76+
resource_type = request.args.get('resource_type', 'auto')
77+
78+
try:
79+
result = cloudinary_service.delete_file(public_id, resource_type)
80+
return jsonify(result)
81+
except Exception as e:
82+
current_app.logger.error(f"Error deleting file: {str(e)}")
83+
return jsonify({"error": "Failed to delete file"}), 500
84+
85+
@uploads.route('/list', methods=['GET'])
86+
def list_files():
87+
"""List files in a folder"""
88+
folder = request.args.get('folder', '')
89+
resource_type = request.args.get('resource_type', 'auto')
90+
max_results = request.args.get('max_results', 100, type=int)
91+
92+
try:
93+
result = cloudinary_service.list_files(folder, resource_type, max_results)
94+
return jsonify(result)
95+
except Exception as e:
96+
current_app.logger.error(f"Error listing files: {str(e)}")
97+
return jsonify({"error": "Failed to list files"}), 500
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import os
2+
import logging
3+
from dotenv import load_dotenv
4+
import cloudinary
5+
import cloudinary.uploader
6+
import cloudinary.api
7+
from cloudinary.utils import cloudinary_url
8+
9+
# Load environment variables
10+
load_dotenv()
11+
12+
# Configure Cloudinary
13+
cloudinary.config(
14+
cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'),
15+
api_key=os.getenv('CLOUDINARY_API_KEY'),
16+
api_secret=os.getenv('CLOUDINARY_API_SECRET')
17+
)
18+
19+
# Initialize logger
20+
logger = logging.getLogger(__name__)
21+
22+
# Constants for Cloudinary folders and presets
23+
CASE_FOLDER = 'smartprobono/case_documents'
24+
TEMPLATE_FOLDER = 'smartprobono/document_templates'
25+
USER_FOLDER = 'smartprobono/user_uploads'
26+
27+
DOCUMENT_UPLOAD_PRESET = 'document_uploads'
28+
TEMPLATE_UPLOAD_PRESET = 'template_uploads'
29+
USER_UPLOAD_PRESET = 'user_uploads'
30+
31+
32+
class CloudinaryService:
33+
"""Service for handling Cloudinary operations"""
34+
35+
@staticmethod
36+
def get_upload_signature(preset_name=USER_UPLOAD_PRESET):
37+
"""Get a signature for direct upload from frontend"""
38+
try:
39+
timestamp = cloudinary.utils.now()
40+
signature = cloudinary.utils.api_sign_request({
41+
'timestamp': timestamp,
42+
'upload_preset': preset_name
43+
}, cloudinary.config().api_secret)
44+
45+
return {
46+
'signature': signature,
47+
'timestamp': timestamp,
48+
'cloudName': cloudinary.config().cloud_name,
49+
'apiKey': cloudinary.config().api_key,
50+
'uploadPreset': preset_name
51+
}
52+
except Exception as e:
53+
logger.error(f"Error generating upload signature: {e}")
54+
raise
55+
56+
@staticmethod
57+
def upload_file(file_path, public_id=None, folder=USER_FOLDER, resource_type='auto', **options):
58+
"""Upload a file to Cloudinary from the server"""
59+
try:
60+
upload_options = {
61+
'folder': folder,
62+
'resource_type': resource_type,
63+
**options
64+
}
65+
66+
if public_id:
67+
upload_options['public_id'] = public_id
68+
69+
result = cloudinary.uploader.upload(file_path, **upload_options)
70+
return result
71+
except Exception as e:
72+
logger.error(f"Error uploading file: {e}")
73+
raise
74+
75+
@staticmethod
76+
def generate_url(public_id, **options):
77+
"""Generate a URL for a Cloudinary resource"""
78+
try:
79+
url, options = cloudinary_url(public_id, **options)
80+
return url
81+
except Exception as e:
82+
logger.error(f"Error generating URL: {e}")
83+
raise
84+
85+
@staticmethod
86+
def delete_file(public_id, resource_type='auto'):
87+
"""Delete a file from Cloudinary"""
88+
try:
89+
result = cloudinary.uploader.destroy(public_id, resource_type=resource_type)
90+
return result
91+
except Exception as e:
92+
logger.error(f"Error deleting file: {e}")
93+
raise
94+
95+
@staticmethod
96+
def list_files(folder, resource_type='auto', max_results=100):
97+
"""List files in a folder"""
98+
try:
99+
result = cloudinary.api.resources(
100+
type='upload',
101+
prefix=folder,
102+
resource_type=resource_type,
103+
max_results=max_results
104+
)
105+
return result.get('resources', [])
106+
except Exception as e:
107+
logger.error(f"Error listing files: {e}")
108+
raise
109+
110+
@staticmethod
111+
def create_folder(folder):
112+
"""Create a new folder in Cloudinary"""
113+
try:
114+
result = cloudinary.api.create_folder(folder)
115+
return result
116+
except Exception as e:
117+
logger.error(f"Error creating folder: {e}")
118+
raise
119+
120+
121+
# Create singleton instance
122+
cloudinary_service = CloudinaryService()

frontend/src/App.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import RightsPage from './pages/RightsPage';
2525
import Services from './pages/Services';
2626
import Contact from './pages/Contact';
2727
import NotFoundPage from './pages/NotFoundPage';
28+
import DocumentsPage from './pages/DocumentsPage';
2829

2930
// Layout components for nested routes
3031
const ServicesLayout = () => (
@@ -112,6 +113,9 @@ const App = () => {
112113
<Route path="/resources/*" element={<ResourcesLayout />} />
113114
<Route path="/contact" element={<Contact />} />
114115

116+
{/* Document Management route */}
117+
<Route path="/documents" element={<DocumentsPage />} />
118+
115119
{/* Legal Chat route */}
116120
<Route path="/legal-chat/*" element={<LegalChatLayout />} />
117121

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React, { useState } from 'react';
2+
import { Box, Card, CardContent, CardHeader, Typography, Divider, Link, Grid, Paper } from '@mui/material';
3+
import FileUpload from './FileUpload';
4+
5+
const DocumentUpload = () => {
6+
const [uploadedDocument, setUploadedDocument] = useState(null);
7+
const [uploadedTemplate, setUploadedTemplate] = useState(null);
8+
const [uploadedUserFile, setUploadedUserFile] = useState(null);
9+
10+
const handleDocumentUpload = (fileData) => {
11+
console.log('Document uploaded:', fileData);
12+
setUploadedDocument(fileData);
13+
};
14+
15+
const handleTemplateUpload = (fileData) => {
16+
console.log('Template uploaded:', fileData);
17+
setUploadedTemplate(fileData);
18+
};
19+
20+
const handleUserFileUpload = (fileData) => {
21+
console.log('User file uploaded:', fileData);
22+
setUploadedUserFile(fileData);
23+
};
24+
25+
const renderUploadedFile = (fileData, title) => {
26+
if (!fileData) return null;
27+
28+
return (
29+
<Paper elevation={2} sx={{ p: 2, mb: 2, bgcolor: 'background.paper' }}>
30+
<Typography variant="subtitle1" fontWeight="bold">{title}</Typography>
31+
<Typography variant="body2">File name: {fileData.original_filename}</Typography>
32+
<Typography variant="body2">Size: {Math.round(fileData.bytes / 1024)} KB</Typography>
33+
<Typography variant="body2">Type: {fileData.format || 'Document'}</Typography>
34+
<Typography variant="body2">
35+
URL: <Link href={fileData.secure_url} target="_blank" rel="noopener">{fileData.secure_url}</Link>
36+
</Typography>
37+
38+
{fileData.resource_type === 'image' && (
39+
<Box sx={{ mt: 2, textAlign: 'center' }}>
40+
<img
41+
src={fileData.secure_url}
42+
alt={fileData.original_filename}
43+
style={{ maxWidth: '100%', maxHeight: '200px' }}
44+
/>
45+
</Box>
46+
)}
47+
</Paper>
48+
);
49+
};
50+
51+
return (
52+
<Card>
53+
<CardHeader title="Document Upload" />
54+
<Divider />
55+
<CardContent>
56+
<Grid container spacing={3}>
57+
<Grid item xs={12} md={4}>
58+
<Box>
59+
<Typography variant="h6">Case Documents</Typography>
60+
<Typography variant="body2" color="text.secondary" paragraph>
61+
Upload documents related to legal cases.
62+
Supported formats: PDF, DOC, DOCX
63+
</Typography>
64+
<FileUpload
65+
onUploadComplete={handleDocumentUpload}
66+
uploadType="document"
67+
buttonText="Select Case Document"
68+
allowedFormats={['pdf', 'doc', 'docx']}
69+
/>
70+
{renderUploadedFile(uploadedDocument, 'Uploaded Case Document')}
71+
</Box>
72+
</Grid>
73+
74+
<Grid item xs={12} md={4}>
75+
<Box>
76+
<Typography variant="h6">Document Templates</Typography>
77+
<Typography variant="body2" color="text.secondary" paragraph>
78+
Upload document templates for generating legal documents.
79+
Supported formats: DOC, DOCX, TXT, HTML
80+
</Typography>
81+
<FileUpload
82+
onUploadComplete={handleTemplateUpload}
83+
uploadType="template"
84+
buttonText="Select Template"
85+
allowedFormats={['doc', 'docx', 'txt', 'html']}
86+
/>
87+
{renderUploadedFile(uploadedTemplate, 'Uploaded Template')}
88+
</Box>
89+
</Grid>
90+
91+
<Grid item xs={12} md={4}>
92+
<Box>
93+
<Typography variant="h6">User Files</Typography>
94+
<Typography variant="body2" color="text.secondary" paragraph>
95+
Upload general files and images.
96+
Supported formats: PDF, DOC, DOCX, JPG, JPEG, PNG
97+
</Typography>
98+
<FileUpload
99+
onUploadComplete={handleUserFileUpload}
100+
uploadType="user"
101+
buttonText="Select File"
102+
allowedFormats={['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png']}
103+
/>
104+
{renderUploadedFile(uploadedUserFile, 'Uploaded User File')}
105+
</Box>
106+
</Grid>
107+
</Grid>
108+
</CardContent>
109+
</Card>
110+
);
111+
};
112+
113+
export default DocumentUpload;

0 commit comments

Comments
 (0)