Upgrade code

This commit is contained in:
2026-03-30 14:04:58 -04:00
parent bf29ccb287
commit 53eaf1c76d
12 changed files with 390 additions and 58 deletions
+53 -20
View File
@@ -15,7 +15,7 @@ from app.services.notification_service import (
notify_comment_added, notify_assignment,
)
from app.services.log_service import log_action, log_ticket_history
from app.services.validation_service import validate_file
from app.services.validation_service import validate_file, render_comment_body
tickets_bp = Blueprint('tickets', __name__)
logger = logging.getLogger(__name__)
@@ -154,10 +154,22 @@ def ticket_list():
if category:
query = query.filter_by(category=category)
if search:
query = query.filter(
Ticket.title.ilike(f'%{search}%') |
Ticket.ticket_number.ilike(f'%{search}%') |
Ticket.description.ilike(f'%{search}%')
# Extend search to cover comments and assignee name via outer joins.
# distinct() prevents duplicate ticket rows when multiple comments match.
from app.models import Comment
assignee_alias = db.aliased(User)
query = (
query
.outerjoin(Comment, Comment.ticket_id == Ticket.id)
.outerjoin(assignee_alias, assignee_alias.id == Ticket.assigned_to_id)
.filter(
Ticket.title.ilike(f'%{search}%') |
Ticket.ticket_number.ilike(f'%{search}%') |
Ticket.description.ilike(f'%{search}%') |
Comment.body.ilike(f'%{search}%') |
assignee_alias.full_name.ilike(f'%{search}%')
)
.distinct()
)
tickets = query.order_by(Ticket.created_at.desc()).paginate(page=page, per_page=20)
@@ -173,7 +185,7 @@ def ticket_list():
@tickets_bp.route('/tickets/<int:ticket_id>', methods=['GET', 'POST'])
@login_required
def ticket_detail(ticket_id):
ticket = Ticket.query.get_or_404(ticket_id)
ticket = db.session.get(Ticket, ticket_id) or abort(404)
# Employees can only view their own tickets
if not current_user.is_it_staff and ticket.created_by_id != current_user.id:
@@ -189,20 +201,41 @@ def ticket_detail(ticket_id):
comment = Comment(
ticket_id = ticket.id,
author_id = current_user.id,
body = body,
body = render_comment_body(body),
is_internal= is_internal,
)
db.session.add(comment)
db.session.flush()
for f in request.files.getlist('attachments'):
if f and f.filename:
file_error = validate_file(f, ALLOWED_EXT)
if file_error:
logger.warning(f'[COMMENT UPLOAD REJECTED] {file_error} filename="{f.filename}" user_id={current_user.id}')
continue
save_attachment(f, ticket_id=ticket.id,
comment_id=comment.id, uploader_id=current_user.id)
# ── Attachment limits (mirrors client-side constants in detail.html)
MAX_COMMENT_FILES = 5
MAX_COMMENT_BYTES = 25 * 1024 * 1024 # 25 MB total per comment
uploaded_files = [f for f in request.files.getlist('attachments') if f and f.filename]
if len(uploaded_files) > MAX_COMMENT_FILES:
db.session.rollback()
logger.warning(f'[COMMENT UPLOAD REJECTED] Too many files ({len(uploaded_files)}) ticket_id={ticket.id} user_id={current_user.id}')
flash(f'Too many attachments — maximum {MAX_COMMENT_FILES} files per comment.', 'danger')
return redirect(url_for('tickets.ticket_detail', ticket_id=ticket.id))
total_bytes = 0
for f in uploaded_files:
f.stream.seek(0, 2)
total_bytes += f.stream.tell()
f.stream.seek(0)
if total_bytes > MAX_COMMENT_BYTES:
db.session.rollback()
logger.warning(f'[COMMENT UPLOAD REJECTED] Total size {total_bytes} exceeds limit ticket_id={ticket.id} user_id={current_user.id}')
flash(f'Total attachment size exceeds the 25 MB limit per comment.', 'danger')
return redirect(url_for('tickets.ticket_detail', ticket_id=ticket.id))
for f in uploaded_files:
file_error = validate_file(f, ALLOWED_EXT)
if file_error:
logger.warning(f'[COMMENT UPLOAD REJECTED] {file_error} filename="{f.filename}" user_id={current_user.id}')
continue
save_attachment(f, ticket_id=ticket.id,
comment_id=comment.id, uploader_id=current_user.id)
log_action(current_user.id, 'comment_create', 'comment', comment.id,
f'ticket_id={ticket.id} internal={is_internal}')
@@ -239,7 +272,7 @@ def update_ticket(ticket_id):
if not current_user.is_it_staff:
abort(403)
ticket = Ticket.query.get_or_404(ticket_id)
ticket = db.session.get(Ticket, ticket_id) or abort(404)
old_status = ticket.status
old_priority = ticket.priority
old_assigned = ticket.assigned_to_id
@@ -317,7 +350,7 @@ def update_ticket(ticket_id):
@tickets_bp.route('/comments/<int:comment_id>/delete', methods=['POST'])
@login_required
def delete_comment(comment_id):
comment = Comment.query.get_or_404(comment_id)
comment = db.session.get(Comment, comment_id) or abort(404)
if not current_user.is_it_staff and comment.author_id != current_user.id:
abort(403)
ticket_id = comment.ticket_id
@@ -335,13 +368,13 @@ def delete_comment(comment_id):
@tickets_bp.route('/attachments/<int:att_id>')
@login_required
def download_attachment(att_id):
att = Attachment.query.get_or_404(att_id)
att = db.session.get(Attachment, att_id) or abort(404)
# Authorization: employees may only download attachments belonging to
# their own tickets. IT staff have unrestricted access across all tickets.
# att.ticket_id is the authoritative link — comment attachments also carry
# the parent ticket_id, so this check covers both ticket and comment files.
if not current_user.is_it_staff:
ticket = Ticket.query.get_or_404(att.ticket_id)
ticket = db.session.get(Ticket, att.ticket_id) or abort(404)
if ticket.created_by_id != current_user.id:
logger.warning(
f'[ATTACHMENT ACCESS DENIED] att_id={att_id} ticket_id={att.ticket_id} '
@@ -416,7 +449,7 @@ def knowledge_base():
@tickets_bp.route('/kb/<int:article_id>')
@login_required
def kb_article(article_id):
article = KnowledgeBase.query.get_or_404(article_id)
article = db.session.get(KnowledgeBase, article_id) or abort(404)
# Increment view_count atomically at the SQL level. A Python-level
# read-modify-write (article.view_count += 1) is not safe under concurrent
# requests: two simultaneous reads both see the same value and one