Initial commit: add compliance_checks table, per-check metadata on assets, and compliance audit trail
This commit is contained in:
145
app/routes/assignments.py
Normal file
145
app/routes/assignments.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import json
|
||||
from datetime import date
|
||||
from flask import (Blueprint, render_template, redirect, url_for,
|
||||
flash, request, current_app)
|
||||
from flask_login import login_required, current_user
|
||||
from app.extensions import db
|
||||
from app.models.assignment import Assignment
|
||||
from app.models.asset import Asset
|
||||
from app.models.user import User
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
bp = Blueprint('assignments', __name__, url_prefix='/assignments')
|
||||
|
||||
|
||||
def _log(action, record_id, description, old=None, new=None):
|
||||
entry = AuditLog(
|
||||
table_name='assignments',
|
||||
record_id=record_id,
|
||||
action=action,
|
||||
old_values=json.dumps(old) if old else None,
|
||||
new_values=json.dumps(new) if new else None,
|
||||
performed_by_id=current_user.id,
|
||||
ip_address=request.remote_addr,
|
||||
description=description,
|
||||
)
|
||||
db.session.add(entry)
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
active_only = request.args.get('active', '1') == '1'
|
||||
q = request.args.get('q', '').strip()
|
||||
|
||||
query = Assignment.query
|
||||
if active_only:
|
||||
query = query.filter_by(is_active=True)
|
||||
|
||||
pagination = query.order_by(Assignment.assigned_date.desc()).paginate(
|
||||
page=page, per_page=current_app.config['ITEMS_PER_PAGE'], error_out=False
|
||||
)
|
||||
return render_template('assignments/index.html',
|
||||
pagination=pagination, active_only=active_only, q=q)
|
||||
|
||||
|
||||
@bp.route('/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create():
|
||||
# Pre-fill from query params (used from asset / user detail pages)
|
||||
preselect_asset_id = request.args.get('asset_id', type=int)
|
||||
preselect_user_id = request.args.get('user_id', type=int)
|
||||
|
||||
if request.method == 'POST':
|
||||
user_id = request.form.get('user_id', type=int)
|
||||
asset_id = request.form.get('asset_id', type=int)
|
||||
assigned_date_str = request.form.get('assigned_date', '')
|
||||
notes = request.form.get('notes', '').strip() or None
|
||||
|
||||
user = User.query.get(user_id) if user_id else None
|
||||
asset = Asset.query.get(asset_id) if asset_id else None
|
||||
|
||||
errors = []
|
||||
if not user:
|
||||
errors.append('User is required.')
|
||||
if not asset:
|
||||
errors.append('Asset is required.')
|
||||
if user and user.is_masked:
|
||||
errors.append('Cannot assign assets to a masked user.')
|
||||
if asset and asset.status == 'assigned':
|
||||
errors.append(f'Asset {asset.serial_number} is already assigned.')
|
||||
if asset and asset.status in ('retired', 'lost'):
|
||||
errors.append(f'Asset {asset.serial_number} is {asset.status} and cannot be assigned.')
|
||||
|
||||
if errors:
|
||||
for e in errors:
|
||||
flash(e, 'danger')
|
||||
return render_template('assignments/form.html',
|
||||
preselect_asset_id=asset_id,
|
||||
preselect_user_id=user_id)
|
||||
|
||||
try:
|
||||
assigned_date = date.fromisoformat(assigned_date_str) if assigned_date_str else date.today()
|
||||
except ValueError:
|
||||
assigned_date = date.today()
|
||||
|
||||
assignment = Assignment(
|
||||
asset_id=asset.id,
|
||||
user_id=user.id,
|
||||
assigned_date=assigned_date,
|
||||
assigned_by_id=current_user.id,
|
||||
notes=notes,
|
||||
is_active=True,
|
||||
)
|
||||
asset.status = 'assigned'
|
||||
db.session.add(assignment)
|
||||
db.session.flush()
|
||||
_log('assign', assignment.id,
|
||||
f'Assigned asset SN={asset.serial_number} to WID={user.windows_id}',
|
||||
new={'asset_sn': asset.serial_number, 'user_wid': user.windows_id,
|
||||
'date': str(assigned_date)})
|
||||
db.session.commit()
|
||||
flash(f'Asset {asset.serial_number} assigned to {user.display_name}.', 'success')
|
||||
return redirect(url_for('assignments.index'))
|
||||
|
||||
return render_template('assignments/form.html',
|
||||
preselect_asset_id=preselect_asset_id,
|
||||
preselect_user_id=preselect_user_id)
|
||||
|
||||
|
||||
@bp.route('/<int:assignment_id>/return', methods=['POST'])
|
||||
@login_required
|
||||
def return_asset(assignment_id):
|
||||
assignment = Assignment.query.get_or_404(assignment_id)
|
||||
|
||||
if not assignment.is_active:
|
||||
flash('This assignment is already closed.', 'info')
|
||||
return redirect(url_for('assignments.index'))
|
||||
|
||||
returned_date_str = request.form.get('returned_date', '')
|
||||
try:
|
||||
returned_date = date.fromisoformat(returned_date_str) if returned_date_str else date.today()
|
||||
except ValueError:
|
||||
returned_date = date.today()
|
||||
|
||||
assignment.returned_date = returned_date
|
||||
assignment.returned_by_id = current_user.id
|
||||
assignment.is_active = False
|
||||
assignment.notes = (assignment.notes or '') + ('\n' + request.form.get('return_notes', '').strip() if request.form.get('return_notes') else '')
|
||||
|
||||
# Only set asset back to available if no other active assignment (safety check)
|
||||
other_active = Assignment.query.filter(
|
||||
Assignment.asset_id == assignment.asset_id,
|
||||
Assignment.is_active == True, # noqa: E712
|
||||
Assignment.id != assignment.id,
|
||||
).first()
|
||||
if not other_active:
|
||||
assignment.asset.status = 'available'
|
||||
|
||||
_log('return', assignment.id,
|
||||
f'Returned asset SN={assignment.asset.serial_number} from WID={assignment.user.windows_id}',
|
||||
new={'returned_date': str(returned_date)})
|
||||
db.session.commit()
|
||||
flash(f'Asset {assignment.asset.serial_number} returned.', 'success')
|
||||
return redirect(url_for('assignments.index'))
|
||||
Reference in New Issue
Block a user