Upgrade code
This commit is contained in:
+108
-7
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort, jsonify, current_app, send_from_directory
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort, jsonify, current_app, send_from_directory, Response
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
import bleach
|
||||
@@ -340,7 +342,7 @@ def create_user():
|
||||
@login_required
|
||||
@admin_required
|
||||
def edit_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
user = db.session.get(User, user_id) or abort(404)
|
||||
if request.method == 'POST':
|
||||
old_role = user.role
|
||||
user.full_name = request.form.get('full_name', user.full_name).strip()
|
||||
@@ -370,7 +372,7 @@ def edit_user(user_id):
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
user = db.session.get(User, user_id) or abort(404)
|
||||
if user.id == current_user.id:
|
||||
flash('You cannot delete your own account.', 'danger')
|
||||
return redirect(url_for('admin.users'))
|
||||
@@ -392,6 +394,7 @@ def all_tickets():
|
||||
status = request.args.get('status', '')
|
||||
priority = request.args.get('priority', '')
|
||||
assigned = request.args.get('assigned', '')
|
||||
search = request.args.get('q', '').strip()
|
||||
|
||||
q = Ticket.query
|
||||
if status: q = q.filter_by(status=status)
|
||||
@@ -399,9 +402,30 @@ def all_tickets():
|
||||
if assigned == 'me': q = q.filter_by(assigned_to_id=current_user.id)
|
||||
elif assigned == 'unassigned': q = q.filter_by(assigned_to_id=None)
|
||||
|
||||
if search:
|
||||
from app.models import Comment
|
||||
submitter_alias = db.aliased(User)
|
||||
assignee_alias = db.aliased(User)
|
||||
q = (
|
||||
q
|
||||
.outerjoin(submitter_alias, submitter_alias.id == Ticket.created_by_id)
|
||||
.outerjoin(assignee_alias, assignee_alias.id == Ticket.assigned_to_id)
|
||||
.outerjoin(Comment, Comment.ticket_id == Ticket.id)
|
||||
.filter(
|
||||
Ticket.title.ilike(f'%{search}%') |
|
||||
Ticket.ticket_number.ilike(f'%{search}%') |
|
||||
Ticket.description.ilike(f'%{search}%') |
|
||||
submitter_alias.full_name.ilike(f'%{search}%') |
|
||||
assignee_alias.full_name.ilike(f'%{search}%') |
|
||||
Comment.body.ilike(f'%{search}%')
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
tickets = q.order_by(Ticket.created_at.desc()).paginate(page=page, per_page=25)
|
||||
return render_template('admin/tickets.html', tickets=tickets,
|
||||
status=status, priority=priority, assigned=assigned)
|
||||
status=status, priority=priority, assigned=assigned,
|
||||
search=search)
|
||||
|
||||
|
||||
# ─── Knowledge Base Management ────────────────────────────────────────────────
|
||||
@@ -623,7 +647,7 @@ def kb_new():
|
||||
@login_required
|
||||
@it_required
|
||||
def kb_edit(article_id):
|
||||
article = KnowledgeBase.query.get_or_404(article_id)
|
||||
article = db.session.get(KnowledgeBase, article_id) or abort(404)
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
article.title = request.form.get('title', article.title).strip()
|
||||
@@ -658,7 +682,7 @@ def kb_edit(article_id):
|
||||
@it_required
|
||||
def kb_toggle_publish(article_id):
|
||||
"""Quick publish/unpublish toggle — callable from the article list."""
|
||||
article = KnowledgeBase.query.get_or_404(article_id)
|
||||
article = db.session.get(KnowledgeBase, article_id) or abort(404)
|
||||
article.is_published = not article.is_published
|
||||
state = 'published' if article.is_published else 'unpublished'
|
||||
log_action(current_user.id, f'kb_{state}', 'knowledge_base', article.id,
|
||||
@@ -673,7 +697,7 @@ def kb_toggle_publish(article_id):
|
||||
@login_required
|
||||
@it_required
|
||||
def kb_delete(article_id):
|
||||
article = KnowledgeBase.query.get_or_404(article_id)
|
||||
article = db.session.get(KnowledgeBase, article_id) or abort(404)
|
||||
upload_dir = current_app.config['UPLOAD_FOLDER']
|
||||
|
||||
# Remove physical files before the cascade deletes the KBAttachment rows.
|
||||
@@ -740,6 +764,83 @@ def activity_logs():
|
||||
)
|
||||
|
||||
|
||||
@admin_bp.route('/tickets/export')
|
||||
@login_required
|
||||
@admin_required
|
||||
def export_tickets():
|
||||
"""Stream a CSV of tickets matching the current filter params."""
|
||||
status = request.args.get('status', '')
|
||||
priority = request.args.get('priority', '')
|
||||
assigned = request.args.get('assigned', '')
|
||||
search = request.args.get('q', '').strip()
|
||||
|
||||
q = Ticket.query
|
||||
if status: q = q.filter_by(status=status)
|
||||
if priority: q = q.filter_by(priority=priority)
|
||||
if assigned == 'me': q = q.filter_by(assigned_to_id=current_user.id)
|
||||
elif assigned == 'unassigned': q = q.filter_by(assigned_to_id=None)
|
||||
|
||||
if search:
|
||||
submitter_alias = db.aliased(User)
|
||||
assignee_alias = db.aliased(User)
|
||||
q = (
|
||||
q
|
||||
.outerjoin(submitter_alias, submitter_alias.id == Ticket.created_by_id)
|
||||
.outerjoin(assignee_alias, assignee_alias.id == Ticket.assigned_to_id)
|
||||
.outerjoin(Comment, Comment.ticket_id == Ticket.id)
|
||||
.filter(
|
||||
Ticket.title.ilike(f'%{search}%') |
|
||||
Ticket.ticket_number.ilike(f'%{search}%') |
|
||||
Ticket.description.ilike(f'%{search}%') |
|
||||
submitter_alias.full_name.ilike(f'%{search}%') |
|
||||
assignee_alias.full_name.ilike(f'%{search}%') |
|
||||
Comment.body.ilike(f'%{search}%')
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
tickets = q.order_by(Ticket.created_at.desc()).all()
|
||||
|
||||
log_action(current_user.id, 'ticket_export', 'ticket', None,
|
||||
f'status={status} priority={priority} assigned={assigned} q={search} count={len(tickets)}')
|
||||
logger.info(f'[ADMIN EXPORT] tickets count={len(tickets)} by admin_id={current_user.id}')
|
||||
|
||||
# Build the entire CSV in memory within the request context so the
|
||||
# SQLAlchemy session remains active for all relationship accesses
|
||||
# (creator, assignee). Streaming generators execute outside the
|
||||
# request context and cause DetachedInstanceError on lazy-loaded attrs.
|
||||
buf = io.StringIO()
|
||||
writer = csv.writer(buf)
|
||||
writer.writerow([
|
||||
'Ticket #', 'Title', 'Category', 'Status', 'Priority',
|
||||
'Submitted By', 'Assigned To', 'Created', 'Updated',
|
||||
'Resolved', 'Due Date', 'AI Generated', 'Resolution Notes',
|
||||
])
|
||||
for t in tickets:
|
||||
writer.writerow([
|
||||
t.ticket_number,
|
||||
t.title,
|
||||
t.category.replace('_', ' ').title(),
|
||||
t.status.replace('_', ' ').title(),
|
||||
t.priority.title(),
|
||||
t.creator.full_name,
|
||||
t.assignee.full_name if t.assignee else '',
|
||||
t.created_at.strftime('%Y-%m-%d %H:%M'),
|
||||
t.updated_at.strftime('%Y-%m-%d %H:%M') if t.updated_at else '',
|
||||
t.resolved_at.strftime('%Y-%m-%d %H:%M') if t.resolved_at else '',
|
||||
t.due_date.strftime('%Y-%m-%d') if t.due_date else '',
|
||||
'Yes' if t.ai_generated else 'No',
|
||||
t.resolution_notes or '',
|
||||
])
|
||||
|
||||
filename = f"tickets_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
return Response(
|
||||
buf.getvalue(),
|
||||
mimetype = 'text/csv',
|
||||
headers = {'Content-Disposition': f'attachment; filename={filename}'},
|
||||
)
|
||||
|
||||
|
||||
def _roles():
|
||||
return [UserRole.EMPLOYEE, UserRole.IT_STAFF, UserRole.ADMIN]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user