diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 277727a..5b687ba 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/__pycache__/room_files.cpython-313.pyc b/routes/__pycache__/room_files.cpython-313.pyc index 32e3c33..325372a 100644 Binary files a/routes/__pycache__/room_files.cpython-313.pyc and b/routes/__pycache__/room_files.cpython-313.pyc differ diff --git a/routes/main.py b/routes/main.py index e539d9d..67cba4b 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1021,4 +1021,71 @@ def init_routes(main_bp): } logger.info(f"Sending response: {response_data}") - return jsonify(response_data) \ No newline at end of file + 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' + } + ) \ No newline at end of file diff --git a/routes/room_files.py b/routes/room_files.py index 1a5b194..015dae5 100644 --- a/routes/room_files.py +++ b/routes/room_files.py @@ -275,41 +275,53 @@ def upload_room_file(room_id): @login_required def download_room_file(room_id, filename): """ - Download a file from a room. + Download or preview a file from a room. Args: 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: - File download response or error message + File download/preview response or error message """ room = Room.query.get_or_404(room_id) if not user_has_permission(room, 'can_download'): abort(403) + rel_path = clean_path(request.args.get('path', '')) + preview_mode = request.args.get('preview', 'false').lower() == 'true' + # Lookup in RoomFile rf = RoomFile.query.filter_by(room_id=room_id, name=filename, path=rel_path).first() if not rf or rf.type != 'file': return jsonify({'error': 'File not found'}), 404 + 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) if not os.path.exists(file_path): return jsonify({'error': 'File not found'}), 404 + # Log the event log_event( - event_type='file_download', + event_type='file_download' if not preview_mode else 'file_preview', details={ - 'downloaded_by': f"{current_user.username} {current_user.last_name}", + 'user': f"{current_user.username} {current_user.last_name}", 'filename': filename, 'room_id': room_id, '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 ) 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('//files/', methods=['DELETE']) @login_required diff --git a/static/js/components/filePreview.js b/static/js/components/filePreview.js new file mode 100644 index 0000000..0d31b75 --- /dev/null +++ b/static/js/components/filePreview.js @@ -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 = ` + + `; + 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 = ` +
+ Loading... +
+ `; + + try { + // Handle different file types + if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) { + // Image preview + contentDiv.innerHTML = ` + ${file.name} + `; + } else if (['pdf'].includes(extension)) { + // PDF preview + contentDiv.innerHTML = ` + + `; + } else if (['mp4', 'webm'].includes(extension)) { + // Video preview + contentDiv.innerHTML = ` + + `; + } else if (['mp3', 'wav'].includes(extension)) { + // Audio preview + contentDiv.innerHTML = ` + + `; + } else { + // Default preview for other file types + contentDiv.innerHTML = ` +
+ +
${file.name}
+

Preview not available for this file type.

+ + Download + +
+ `; + } + } catch (error) { + console.error('Error previewing file:', error); + contentDiv.innerHTML = ` +
+ +
Error Loading Preview
+

Unable to load file preview. Please try downloading the file instead.

+ + Download + +
+ `; + } + + // Show the modal + this.modal.show(); + } + + close() { + this.modal.hide(); + } +} \ No newline at end of file diff --git a/static/js/rooms/viewManager.js b/static/js/rooms/viewManager.js index 2b33d2e..33926e2 100644 --- a/static/js/rooms/viewManager.js +++ b/static/js/rooms/viewManager.js @@ -13,6 +13,8 @@ * @classdesc Manages the visual representation and interaction of files in the room interface. * Handles view switching, file rendering, sorting, and UI updates. */ +import { FilePreview } from '../components/filePreview.js'; + export class ViewManager { /** * Creates a new ViewManager instance. @@ -24,6 +26,12 @@ export class ViewManager { this.currentView = 'grid'; this.sortColumn = 'name'; this.sortDirection = 'asc'; + this.filePreview = new FilePreview({ + containerId: 'roomFilePreviewModal', + onClose: () => { + // Clean up any resources if needed + } + }); console.log('[ViewManager] Initialized with roomManager:', roomManager); } @@ -349,6 +357,19 @@ export class ViewManager { `); } 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(` + + `); + } + if (this.roomManager.canDownload) { actions.push(` +