diff --git a/backend/app.py b/backend/app.py index e632c4e..0f7c71c 100755 --- a/backend/app.py +++ b/backend/app.py @@ -99,6 +99,7 @@ def init_db(): comments TEXT, aborted INTEGER DEFAULT 0, abort_reason TEXT, + waiting_approval INTEGER DEFAULT 0, FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE ); @@ -341,13 +342,15 @@ def socket_to_dict(socket): return None latest_job = get_latest_job_for_socket(socket['id']) + waiting_jobs = get_waiting_jobs_for_socket(socket['id']) return { 'id': socket['id'], 'name': socket['name'], 'description': socket['description'], 'status': socket['status'], - 'latestJob': job_to_dict(latest_job) if latest_job else None + 'latestJob': job_to_dict(latest_job) if latest_job else None, + 'waitingJobs': [job_to_dict(job) for job in waiting_jobs] if waiting_jobs else [] } # Job-Verwaltung @@ -374,27 +377,40 @@ def get_expired_jobs(): rows = db.execute(''' SELECT * FROM job WHERE aborted = 0 + AND waiting_approval = 0 AND datetime(start_at, '+' || duration_in_minutes || ' minutes') <= datetime(?) ''', (now,)).fetchall() return [dict(row) for row in rows] -def create_job(socket_id, user_id, duration_in_minutes, comments=None): +def get_waiting_jobs_for_socket(socket_id): + """Findet alle Jobs, die auf Freischaltung für eine bestimmte Steckdose warten.""" + db = get_db() + rows = db.execute(''' + SELECT * FROM job + WHERE socket_id = ? + AND aborted = 0 + AND waiting_approval = 1 + ORDER BY start_at ASC + ''', (socket_id,)).fetchall() + return [dict(row) for row in rows] + +def create_job(socket_id, user_id, duration_in_minutes, comments=None, waiting_approval=0): job_id = str(uuid.uuid4()) start_at = datetime.datetime.utcnow() db = get_db() db.execute( '''INSERT INTO job - (id, socket_id, user_id, start_at, duration_in_minutes, comments, aborted, abort_reason) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', - (job_id, socket_id, user_id, start_at.isoformat(), duration_in_minutes, comments, 0, None) + (id, socket_id, user_id, start_at, duration_in_minutes, comments, aborted, abort_reason, waiting_approval) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (job_id, socket_id, user_id, start_at.isoformat(), duration_in_minutes, comments, 0, None, waiting_approval) ) db.commit() return get_job_by_id(job_id) def update_job(job_id, socket_id=None, user_id=None, duration_in_minutes=None, - comments=None, aborted=None, abort_reason=None): + comments=None, aborted=None, abort_reason=None, waiting_approval=None): job = get_job_by_id(job_id) if not job: return None @@ -426,6 +442,10 @@ def update_job(job_id, socket_id=None, user_id=None, duration_in_minutes=None, values.append('abort_reason = ?') params.append(abort_reason) + if waiting_approval is not None: + values.append('waiting_approval = ?') + params.append(1 if waiting_approval else 0) + if not values: return job @@ -462,6 +482,9 @@ def job_to_dict(job): if not job: return None + # Bei älteren Jobs könnte waiting_approval fehlen, deshalb mit get abrufen und Default setzen + waiting_approval = job.get('waiting_approval', 0) if isinstance(job, dict) else getattr(job, 'waiting_approval', 0) + return { 'id': job['id'], 'socketId': job['socket_id'], @@ -471,6 +494,7 @@ def job_to_dict(job): 'comments': job['comments'], 'aborted': bool(job['aborted']), 'abortReason': job['abort_reason'], + 'waitingApproval': bool(waiting_approval), 'remainingMinutes': calculate_remaining_time(job) } @@ -708,16 +732,32 @@ def create_job_endpoint(): if not socket: return jsonify({'message': 'Steckdose nicht gefunden!'}), 404 - if socket['status'] != 0: # 0 = available - return jsonify({'message': 'Steckdose ist nicht verfügbar!'}), 400 - duration = int(data['durationInMinutes']) + allow_queued_jobs = data.get('allowQueuedJobs', False) + # Prüfen, ob der Drucker bereits belegt ist + if socket['status'] != 0: # 0 = available + if allow_queued_jobs: + # Erstelle einen Job, der auf Freischaltung wartet + job = create_job( + socket_id=socket['id'], + user_id=g.current_user['id'], + duration_in_minutes=duration, + comments=data.get('comments', ''), + waiting_approval=1 # Job wartet auf Freischaltung + ) + app.logger.info(f"Wartender Job {job['id']} für belegten Drucker {socket['id']} erstellt.") + return jsonify(job_to_dict(job)), 201 + else: + return jsonify({'message': 'Steckdose ist nicht verfügbar!'}), 400 + + # Normaler Job für verfügbaren Drucker job = create_job( socket_id=socket['id'], user_id=g.current_user['id'], duration_in_minutes=duration, - comments=data.get('comments', '') + comments=data.get('comments', ''), + waiting_approval=0 # Job ist sofort aktiv ) # Steckdose als belegt markieren @@ -726,9 +766,13 @@ def create_job_endpoint(): # Steckdose einschalten, falls IP-Adresse hinterlegt ist if socket['ip_address']: try: - turn_on_socket(socket['ip_address']) + success = turn_on_socket(socket['ip_address']) + if success: + app.logger.info(f"Steckdose {socket['ip_address']} für Job {job['id']} eingeschaltet.") + else: + app.logger.warning(f"Konnte Steckdose {socket['ip_address']} für Job {job['id']} nicht einschalten.") except Exception as e: - app.logger.error(f"Fehler beim Einschalten der Steckdose: {e}") + app.logger.error(f"Fehler beim Einschalten der Steckdose {socket['ip_address']}: {e}") return jsonify(job_to_dict(job)), 201 @@ -854,6 +898,51 @@ def extend_job(job_id): return jsonify(job_to_dict(updated_job)) +@app.route('/api/jobs//approve', methods=['POST']) +@login_required +def approve_job(job_id): + """Aktiviert einen wartenden Job und schaltet die Steckdose ein.""" + # Nur Admins oder der Job-Ersteller können Jobs freischalten + job = get_job_by_id(job_id) + if not job: + return jsonify({'message': 'Job nicht gefunden!'}), 404 + + if g.current_user['role'] != 'admin' and job['user_id'] != g.current_user['id']: + return jsonify({'message': 'Keine Berechtigung für diesen Job!'}), 403 + + # Prüfen, ob Job auf Freischaltung wartet + waiting_approval = job.get('waiting_approval', 0) + if not waiting_approval: + return jsonify({'message': 'Dieser Job wartet nicht auf Freischaltung!'}), 400 + + # Drucker abrufen + socket = get_socket_by_id(job['socket_id']) + if not socket: + return jsonify({'message': 'Drucker nicht gefunden!'}), 404 + + # Prüfen, ob der Drucker verfügbar ist + if socket['status'] != 0: # 0 = available + return jsonify({'message': 'Drucker ist noch belegt! Bitte warten, bis der laufende Job beendet ist.'}), 400 + + # Job aktualisieren + updated_job = update_job(job_id, waiting_approval=0) + + # Steckdose als belegt markieren + update_socket(socket['id'], status=1) # 1 = busy + + # Steckdose einschalten, falls IP-Adresse hinterlegt ist + if socket['ip_address']: + try: + success = turn_on_socket(socket['ip_address']) + if success: + app.logger.info(f"Steckdose {socket['ip_address']} für freigeschalteten Job {job['id']} eingeschaltet.") + else: + app.logger.warning(f"Konnte Steckdose {socket['ip_address']} für freigeschalteten Job {job['id']} nicht einschalten.") + except Exception as e: + app.logger.error(f"Fehler beim Einschalten der Steckdose {socket['ip_address']}: {e}") + + return jsonify(job_to_dict(updated_job)) + @app.route('/api/jobs//comments', methods=['PUT']) @login_required def update_job_comments(job_id): @@ -1011,12 +1100,13 @@ def stats(): } }) -# Tägliche Überprüfung der Jobs und automatische Abschaltung der Steckdosen +# Regelmäßige Überprüfung der Jobs und automatische Abschaltung der Steckdosen @app.cli.command("check-jobs") def check_jobs(): - """Überprüft abgelaufene Jobs und schaltet Steckdosen aus.""" + """Überprüft abgelaufene Jobs und schaltet Steckdosen automatisch aus.""" with app.app_context(): expired_jobs = get_expired_jobs() + handled_jobs = 0 for job in expired_jobs: socket = get_socket_by_id(job['socket_id']) @@ -1024,6 +1114,7 @@ def check_jobs(): if socket and socket['status'] == 1: # busy update_socket(socket['id'], status=0) # available app.logger.info(f"Job {job['id']} abgelaufen. Steckdose {socket['id']} auf verfügbar gesetzt.") + handled_jobs += 1 # Steckdose ausschalten, falls IP-Adresse hinterlegt ist if socket['ip_address']: @@ -1033,7 +1124,7 @@ def check_jobs(): try: success = turn_off_socket(socket['ip_address']) if success: - app.logger.info(f"Steckdose {socket['ip_address']} für abgelaufenen Job {job['id']} ausgeschaltet (Versuch {attempt}).") + app.logger.info(f"Steckdose {socket['ip_address']} für abgelaufenen Job {job['id']} automatisch ausgeschaltet (Versuch {attempt}).") break app.logger.warning(f"Konnte Steckdose {socket['ip_address']} nicht ausschalten (Versuch {attempt}/{max_attempts}).") except Exception as e: @@ -1044,7 +1135,7 @@ def check_jobs(): import time time.sleep(1) - app.logger.info(f"{len(expired_jobs)} abgelaufene Jobs überprüft und Steckdosen aktualisiert.") + app.logger.info(f"{len(expired_jobs)} abgelaufene Jobs überprüft, {handled_jobs} Steckdosen aktualisiert.") @app.route('/api/job//status', methods=['GET']) def job_status(job_id): diff --git a/backend/migrations/versions/add_waiting_approval.py b/backend/migrations/versions/add_waiting_approval.py new file mode 100644 index 0000000..523c8a5 --- /dev/null +++ b/backend/migrations/versions/add_waiting_approval.py @@ -0,0 +1,42 @@ +"""Add waiting_approval column to job table + +Revision ID: add_waiting_approval +Revises: af3faaa3844c +Create Date: 2025-03-12 14:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'add_waiting_approval' +down_revision = 'af3faaa3844c' +branch_labels = None +depends_on = None + + +def upgrade(): + # Füge die neue Spalte waiting_approval zur job-Tabelle hinzu + with op.batch_alter_table('job', schema=None) as batch_op: + batch_op.add_column(sa.Column('waiting_approval', sa.Integer(), server_default='0', nullable=False)) + + # SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert + try: + with op.batch_alter_table('print_job', schema=None) as batch_op: + batch_op.add_column(sa.Column('waiting_approval', sa.Boolean(), server_default='0', nullable=False)) + except Exception as e: + print(f"Migration für print_job-Tabelle übersprungen: {e}") + + +def downgrade(): + # Entferne die waiting_approval-Spalte aus der job-Tabelle + with op.batch_alter_table('job', schema=None) as batch_op: + batch_op.drop_column('waiting_approval') + + # SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert + try: + with op.batch_alter_table('print_job', schema=None) as batch_op: + batch_op.drop_column('waiting_approval') + except Exception as e: + print(f"Downgrade für print_job-Tabelle übersprungen: {e}") \ No newline at end of file