from flask import request, jsonify from app import db from app.api import bp from app.models import PrintJob, Printer, User from app.auth.routes import token_required, admin_required from datetime import datetime, timedelta @bp.route('/jobs', methods=['GET']) @token_required def get_jobs(): """Get jobs for the current user or all jobs for admin""" is_admin = request.user_role == 'admin' user_id = request.user_id # Parse query parameters status = request.args.get('status') # active, upcoming, completed, aborted, all printer_id = request.args.get('printer_id') # Base query query = PrintJob.query # Filter by user unless admin if not is_admin: query = query.filter_by(user_id=user_id) # Filter by printer if provided if printer_id: query = query.filter_by(printer_id=printer_id) # Apply status filter now = datetime.utcnow() if status == 'active': query = query.filter_by(aborted=False) \ .filter(PrintJob.start_at <= now) \ .filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now) elif status == 'upcoming': query = query.filter_by(aborted=False) \ .filter(PrintJob.start_at > now) elif status == 'completed': query = query.filter_by(aborted=False) \ .filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) <= now) elif status == 'aborted': query = query.filter_by(aborted=True) # Order by start time, most recent first query = query.order_by(PrintJob.start_at.desc()) # Execute query jobs = query.all() result = [job.to_dict() for job in jobs] return jsonify(result) @bp.route('/jobs/', methods=['GET']) @token_required def get_job(job_id): """Get a specific job""" job = PrintJob.query.get_or_404(job_id) # Check permissions is_admin = request.user_role == 'admin' user_id = request.user_id if not is_admin and job.user_id != user_id: return jsonify({'error': 'Not authorized to view this job'}), 403 return jsonify(job.to_dict()) @bp.route('/jobs', methods=['POST']) @token_required def create_job(): """Create a new print job (reserve a printer)""" data = request.get_json() or {} required_fields = ['printer_id', 'start_at', 'duration_in_minutes'] for field in required_fields: if field not in data: return jsonify({'error': f'Missing required field: {field}'}), 400 # Validate printer printer = Printer.query.get(data['printer_id']) if not printer: return jsonify({'error': 'Printer not found'}), 404 if printer.status != 0: # Not operational return jsonify({'error': 'Printer is not operational'}), 400 # Parse start time try: start_at = datetime.fromisoformat(data['start_at'].replace('Z', '+00:00')) except ValueError: return jsonify({'error': 'Invalid start_at format'}), 400 # Validate duration try: duration = int(data['duration_in_minutes']) if duration <= 0 or duration > 480: # Max 8 hours return jsonify({'error': 'Invalid duration (must be between 1 and 480 minutes)'}), 400 except ValueError: return jsonify({'error': 'Duration must be a number'}), 400 end_at = start_at + timedelta(minutes=duration) # Check if the printer is available during the requested time conflicting_jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \ .filter( (PrintJob.start_at < end_at) & (PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > start_at) ) \ .all() if conflicting_jobs: return jsonify({'error': 'Printer is not available during the requested time'}), 409 # Create job job = PrintJob( printer_id=data['printer_id'], user_id=request.user_id, start_at=start_at, duration_in_minutes=duration, comments=data.get('comments', '') ) db.session.add(job) db.session.commit() return jsonify(job.to_dict()), 201 @bp.route('/jobs/', methods=['PUT']) @token_required def update_job(job_id): """Update a job""" job = PrintJob.query.get_or_404(job_id) # Check permissions is_admin = request.user_role == 'admin' user_id = request.user_id if not is_admin and job.user_id != user_id: return jsonify({'error': 'Not authorized to update this job'}), 403 data = request.get_json() or {} # Only allow certain fields to be updated if 'comments' in data: job.comments = data['comments'] # Admin or owner can abort a job if 'aborted' in data and data['aborted'] and not job.aborted: job.aborted = True job.abort_reason = data.get('abort_reason', '') # Admin or owner can extend a job if it's active now = datetime.utcnow() is_active = (not job.aborted and job.start_at <= now and job.get_end_time() > now) if 'extend_minutes' in data and is_active: try: extend_minutes = int(data['extend_minutes']) if extend_minutes <= 0 or extend_minutes > 120: # Max extend 2 hours return jsonify({'error': 'Invalid extension (must be between 1 and 120 minutes)'}), 400 new_end_time = job.get_end_time() + timedelta(minutes=extend_minutes) # Check for conflicts with the extension conflicting_jobs = PrintJob.query.filter_by(printer_id=job.printer_id, aborted=False) \ .filter(PrintJob.id != job.id) \ .filter(PrintJob.start_at < new_end_time) \ .filter(PrintJob.start_at > job.get_end_time()) \ .all() if conflicting_jobs: return jsonify({'error': 'Cannot extend job due to conflicts with other reservations'}), 409 job.duration_in_minutes += extend_minutes except ValueError: return jsonify({'error': 'Extend minutes must be a number'}), 400 db.session.commit() return jsonify(job.to_dict()) @bp.route('/jobs/', methods=['DELETE']) @token_required def delete_job(job_id): """Delete a job (cancel reservation)""" job = PrintJob.query.get_or_404(job_id) # Check permissions is_admin = request.user_role == 'admin' user_id = request.user_id if not is_admin and job.user_id != user_id: return jsonify({'error': 'Not authorized to delete this job'}), 403 # Only allow deletion of upcoming jobs now = datetime.utcnow() if job.start_at <= now and not is_admin: return jsonify({'error': 'Cannot delete an active or completed job'}), 400 db.session.delete(job) db.session.commit() return jsonify({'message': 'Job deleted successfully'}) @bp.route('/jobs//remaining-time', methods=['GET']) def get_remaining_time(job_id): """Get remaining time for a job (public endpoint)""" job = PrintJob.query.get_or_404(job_id) remaining_seconds = job.get_remaining_time() return jsonify({ 'job_id': job.id, 'remaining_seconds': remaining_seconds, 'is_active': job.is_active() })