File preview
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1022,3 +1022,70 @@ def init_routes(main_bp):
|
|||||||
|
|
||||||
logger.info(f"Sending response: {response_data}")
|
logger.info(f"Sending response: {response_data}")
|
||||||
return jsonify(response_data)
|
return jsonify(response_data)
|
||||||
|
|
||||||
|
@main_bp.route('/settings/events/download')
|
||||||
|
@login_required
|
||||||
|
def download_events():
|
||||||
|
if not current_user.is_admin:
|
||||||
|
flash('Only administrators can download event logs.', 'error')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
# Get filter parameters
|
||||||
|
event_type = request.args.get('event_type')
|
||||||
|
date_range = request.args.get('date_range', '7d')
|
||||||
|
user_id = request.args.get('user_id')
|
||||||
|
|
||||||
|
# Calculate date range
|
||||||
|
end_date = datetime.utcnow()
|
||||||
|
if date_range == '24h':
|
||||||
|
start_date = end_date - timedelta(days=1)
|
||||||
|
elif date_range == '7d':
|
||||||
|
start_date = end_date - timedelta(days=7)
|
||||||
|
elif date_range == '30d':
|
||||||
|
start_date = end_date - timedelta(days=30)
|
||||||
|
else:
|
||||||
|
start_date = None
|
||||||
|
|
||||||
|
# Build query
|
||||||
|
query = Event.query
|
||||||
|
|
||||||
|
if event_type:
|
||||||
|
query = query.filter_by(event_type=event_type)
|
||||||
|
if start_date:
|
||||||
|
query = query.filter(Event.timestamp >= start_date)
|
||||||
|
if user_id:
|
||||||
|
query = query.filter_by(user_id=user_id)
|
||||||
|
|
||||||
|
# Get all events
|
||||||
|
events = query.order_by(Event.timestamp.desc()).all()
|
||||||
|
|
||||||
|
# Create CSV content
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
|
||||||
|
# Write header
|
||||||
|
writer.writerow(['Timestamp', 'Event Type', 'User', 'Details', 'IP Address'])
|
||||||
|
|
||||||
|
# Write data
|
||||||
|
for event in events:
|
||||||
|
user_name = f"{event.user.username} {event.user.last_name}" if event.user else "System"
|
||||||
|
writer.writerow([
|
||||||
|
event.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
event.event_type,
|
||||||
|
user_name,
|
||||||
|
str(event.details),
|
||||||
|
event.ip_address
|
||||||
|
])
|
||||||
|
|
||||||
|
# Create the response
|
||||||
|
output.seek(0)
|
||||||
|
return Response(
|
||||||
|
output,
|
||||||
|
mimetype='text/csv',
|
||||||
|
headers={
|
||||||
|
'Content-Disposition': f'attachment; filename=event_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv'
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -275,41 +275,53 @@ def upload_room_file(room_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def download_room_file(room_id, filename):
|
def download_room_file(room_id, filename):
|
||||||
"""
|
"""
|
||||||
Download a file from a room.
|
Download or preview a file from a room.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
room_id (int): ID of the room containing the file
|
room_id (int): ID of the room containing the file
|
||||||
filename (str): Name of the file to download
|
filename (str): Name of the file to download/preview
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
File download response or error message
|
File download/preview response or error message
|
||||||
"""
|
"""
|
||||||
room = Room.query.get_or_404(room_id)
|
room = Room.query.get_or_404(room_id)
|
||||||
if not user_has_permission(room, 'can_download'):
|
if not user_has_permission(room, 'can_download'):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
rel_path = clean_path(request.args.get('path', ''))
|
rel_path = clean_path(request.args.get('path', ''))
|
||||||
|
preview_mode = request.args.get('preview', 'false').lower() == 'true'
|
||||||
|
|
||||||
# Lookup in RoomFile
|
# Lookup in RoomFile
|
||||||
rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first()
|
rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first()
|
||||||
if not rf or rf.type != 'file':
|
if not rf or rf.type != 'file':
|
||||||
return jsonify({'error': 'File not found'}), 404
|
return jsonify({'error': 'File not found'}), 404
|
||||||
|
|
||||||
room_dir = get_room_dir(room_id)
|
room_dir = get_room_dir(room_id)
|
||||||
file_path = os.path.join(room_dir, rel_path, filename) if rel_path else os.path.join(room_dir, filename)
|
file_path = os.path.join(room_dir, rel_path, filename) if rel_path else os.path.join(room_dir, filename)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
return jsonify({'error': 'File not found'}), 404
|
return jsonify({'error': 'File not found'}), 404
|
||||||
|
|
||||||
|
# Log the event
|
||||||
log_event(
|
log_event(
|
||||||
event_type='file_download',
|
event_type='file_download' if not preview_mode else 'file_preview',
|
||||||
details={
|
details={
|
||||||
'downloaded_by': f"{current_user.username} {current_user.last_name}",
|
'user': f"{current_user.username} {current_user.last_name}",
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
'room_id': room_id,
|
'room_id': room_id,
|
||||||
'path': rel_path,
|
'path': rel_path,
|
||||||
'size': rf.size if rf else None
|
'size': rf.size if rf else None,
|
||||||
|
'preview': preview_mode
|
||||||
},
|
},
|
||||||
user_id=current_user.id
|
user_id=current_user.id
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return send_from_directory(os.path.dirname(file_path), filename, as_attachment=True)
|
|
||||||
|
# For preview mode, we don't set as_attachment
|
||||||
|
return send_from_directory(
|
||||||
|
os.path.dirname(file_path),
|
||||||
|
filename,
|
||||||
|
as_attachment=not preview_mode
|
||||||
|
)
|
||||||
|
|
||||||
@room_files_bp.route('/<int:room_id>/files/<path:filename>', methods=['DELETE'])
|
@room_files_bp.route('/<int:room_id>/files/<path:filename>', methods=['DELETE'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
141
static/js/components/filePreview.js
Normal file
141
static/js/components/filePreview.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
export class FilePreview {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = {
|
||||||
|
containerId: options.containerId || 'filePreviewModal',
|
||||||
|
onClose: options.onClose || (() => {}),
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
this.modal = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Create modal if it doesn't exist
|
||||||
|
if (!document.getElementById(this.options.containerId)) {
|
||||||
|
const modalHtml = `
|
||||||
|
<div class="modal fade" id="${this.options.containerId}" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">File Preview</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="${this.options.containerId}Content" class="text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modal = new bootstrap.Modal(document.getElementById(this.options.containerId));
|
||||||
|
|
||||||
|
// Add event listener for modal close
|
||||||
|
document.getElementById(this.options.containerId).addEventListener('hidden.bs.modal', () => {
|
||||||
|
this.options.onClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileIcon(filename) {
|
||||||
|
const extension = filename.split('.').pop().toLowerCase();
|
||||||
|
const iconMap = {
|
||||||
|
pdf: 'fa-file-pdf',
|
||||||
|
doc: 'fa-file-word',
|
||||||
|
docx: 'fa-file-word',
|
||||||
|
xls: 'fa-file-excel',
|
||||||
|
xlsx: 'fa-file-excel',
|
||||||
|
ppt: 'fa-file-powerpoint',
|
||||||
|
pptx: 'fa-file-powerpoint',
|
||||||
|
txt: 'fa-file-alt',
|
||||||
|
jpg: 'fa-file-image',
|
||||||
|
jpeg: 'fa-file-image',
|
||||||
|
png: 'fa-file-image',
|
||||||
|
gif: 'fa-file-image',
|
||||||
|
zip: 'fa-file-archive',
|
||||||
|
rar: 'fa-file-archive',
|
||||||
|
mp3: 'fa-file-audio',
|
||||||
|
mp4: 'fa-file-video'
|
||||||
|
};
|
||||||
|
return iconMap[extension] || 'fa-file';
|
||||||
|
}
|
||||||
|
|
||||||
|
async previewFile(file) {
|
||||||
|
const contentDiv = document.getElementById(`${this.options.containerId}Content`);
|
||||||
|
const extension = file.name.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
// Show loading spinner
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle different file types
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) {
|
||||||
|
// Image preview
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<img src="${file.url}" class="img-fluid" alt="${file.name}" style="max-height: 70vh;">
|
||||||
|
`;
|
||||||
|
} else if (['pdf'].includes(extension)) {
|
||||||
|
// PDF preview
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<iframe src="${file.url}" width="100%" height="70vh" frameborder="0"></iframe>
|
||||||
|
`;
|
||||||
|
} else if (['mp4', 'webm'].includes(extension)) {
|
||||||
|
// Video preview
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<video controls class="w-100" style="max-height: 70vh;">
|
||||||
|
<source src="${file.url}" type="video/${extension}">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
|
} else if (['mp3', 'wav'].includes(extension)) {
|
||||||
|
// Audio preview
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<audio controls class="w-100">
|
||||||
|
<source src="${file.url}" type="audio/${extension}">
|
||||||
|
Your browser does not support the audio tag.
|
||||||
|
</audio>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Default preview for other file types
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas ${this.getFileIcon(file.name)} fa-4x mb-3" style="color: var(--secondary-color);"></i>
|
||||||
|
<h5 class="mb-3">${file.name}</h5>
|
||||||
|
<p class="text-muted">Preview not available for this file type.</p>
|
||||||
|
<a href="${file.url}" class="btn btn-primary" download>
|
||||||
|
<i class="fas fa-download me-2"></i>Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error previewing file:', error);
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-exclamation-circle fa-4x mb-3 text-danger"></i>
|
||||||
|
<h5 class="mb-3">Error Loading Preview</h5>
|
||||||
|
<p class="text-muted">Unable to load file preview. Please try downloading the file instead.</p>
|
||||||
|
<a href="${file.url}" class="btn btn-primary" download>
|
||||||
|
<i class="fas fa-download me-2"></i>Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
this.modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.modal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
* @classdesc Manages the visual representation and interaction of files in the room interface.
|
* @classdesc Manages the visual representation and interaction of files in the room interface.
|
||||||
* Handles view switching, file rendering, sorting, and UI updates.
|
* Handles view switching, file rendering, sorting, and UI updates.
|
||||||
*/
|
*/
|
||||||
|
import { FilePreview } from '../components/filePreview.js';
|
||||||
|
|
||||||
export class ViewManager {
|
export class ViewManager {
|
||||||
/**
|
/**
|
||||||
* Creates a new ViewManager instance.
|
* Creates a new ViewManager instance.
|
||||||
@@ -24,6 +26,12 @@ export class ViewManager {
|
|||||||
this.currentView = 'grid';
|
this.currentView = 'grid';
|
||||||
this.sortColumn = 'name';
|
this.sortColumn = 'name';
|
||||||
this.sortDirection = 'asc';
|
this.sortDirection = 'asc';
|
||||||
|
this.filePreview = new FilePreview({
|
||||||
|
containerId: 'roomFilePreviewModal',
|
||||||
|
onClose: () => {
|
||||||
|
// Clean up any resources if needed
|
||||||
|
}
|
||||||
|
});
|
||||||
console.log('[ViewManager] Initialized with roomManager:', roomManager);
|
console.log('[ViewManager] Initialized with roomManager:', roomManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +357,19 @@ export class ViewManager {
|
|||||||
</button>
|
</button>
|
||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
|
// Check if file type is supported for preview
|
||||||
|
const extension = file.name.split('.').pop().toLowerCase();
|
||||||
|
const supportedTypes = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'mp4', 'webm', 'mp3', 'wav'];
|
||||||
|
|
||||||
|
if (supportedTypes.includes(extension)) {
|
||||||
|
actions.push(`
|
||||||
|
<button class="btn btn-sm file-action-btn" title="Preview" onclick="window.roomManager.viewManager.previewFile(${index})"
|
||||||
|
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.roomManager.canDownload) {
|
if (this.roomManager.canDownload) {
|
||||||
actions.push(`
|
actions.push(`
|
||||||
<button class="btn btn-sm file-action-btn" title="Download" onclick="window.roomManager.fileManager.downloadFile('${file.name}', '${file.path || ''}')"
|
<button class="btn btn-sm file-action-btn" title="Download" onclick="window.roomManager.fileManager.downloadFile('${file.name}', '${file.path || ''}')"
|
||||||
@@ -493,4 +514,16 @@ export class ViewManager {
|
|||||||
selectedCount: selectedItems.length
|
selectedCount: selectedItems.length
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async previewFile(index) {
|
||||||
|
const file = this.roomManager.fileManager.currentFiles[index];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const fileUrl = `/api/rooms/${this.roomManager.roomId}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path || '')}&preview=true`;
|
||||||
|
|
||||||
|
await this.filePreview.previewFile({
|
||||||
|
name: file.name,
|
||||||
|
url: fileUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -360,4 +360,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add event listener for download button
|
||||||
|
const downloadEventsBtn = document.getElementById('downloadEvents');
|
||||||
|
if (downloadEventsBtn) {
|
||||||
|
downloadEventsBtn.addEventListener('click', function() {
|
||||||
|
const eventType = document.getElementById('eventTypeFilter').value;
|
||||||
|
const dateRange = document.getElementById('dateRangeFilter').value;
|
||||||
|
const userId = document.getElementById('userFilter').value;
|
||||||
|
|
||||||
|
// Construct download URL with current filters
|
||||||
|
const downloadUrl = `/settings/events/download?event_type=${eventType}&date_range=${dateRange}&user_id=${userId}`;
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
window.location.href = downloadUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -51,6 +51,9 @@
|
|||||||
<button id="clearFilters" class="btn btn-secondary btn-sm">
|
<button id="clearFilters" class="btn btn-secondary btn-sm">
|
||||||
<i class="fas fa-times me-1"></i>Clear Filters
|
<i class="fas fa-times me-1"></i>Clear Filters
|
||||||
</button>
|
</button>
|
||||||
|
<button id="downloadEvents" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-download me-1"></i>Download CSV
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user