Fix article editor
This commit is contained in:
+78
-4
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
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_login import login_required, current_user
|
||||
@@ -226,8 +227,47 @@ def all_tickets():
|
||||
@login_required
|
||||
@it_required
|
||||
def kb_list():
|
||||
articles = KnowledgeBase.query.order_by(KnowledgeBase.created_at.desc()).all()
|
||||
return render_template('admin/kb_list.html', articles=articles)
|
||||
q = request.args.get('q', '').strip()
|
||||
category = request.args.get('category', '')
|
||||
author_id = request.args.get('author_id', '', type=str)
|
||||
published = request.args.get('published', '') # 'yes' | 'no' | ''
|
||||
|
||||
query = KnowledgeBase.query
|
||||
|
||||
if q:
|
||||
query = query.filter(
|
||||
KnowledgeBase.title.ilike(f'%{q}%') |
|
||||
KnowledgeBase.tags.ilike(f'%{q}%')
|
||||
)
|
||||
if category:
|
||||
query = query.filter_by(category=category)
|
||||
if author_id:
|
||||
query = query.filter_by(author_id=int(author_id))
|
||||
if published == 'yes':
|
||||
query = query.filter_by(is_published=True)
|
||||
elif published == 'no':
|
||||
query = query.filter_by(is_published=False)
|
||||
|
||||
articles = query.order_by(KnowledgeBase.created_at.desc()).all()
|
||||
|
||||
# Build filter option lists from existing data
|
||||
categories = sorted({a.category for a in KnowledgeBase.query.with_entities(KnowledgeBase.category).distinct() if a.category})
|
||||
authors = User.query.filter(
|
||||
User.id.in_(
|
||||
db.session.query(KnowledgeBase.author_id).distinct()
|
||||
),
|
||||
User.is_active == True,
|
||||
).order_by(User.full_name).all()
|
||||
|
||||
return render_template('admin/kb_list.html',
|
||||
articles = articles,
|
||||
categories = categories,
|
||||
authors = authors,
|
||||
q = q,
|
||||
sel_category = category,
|
||||
sel_author_id = author_id,
|
||||
sel_published = published,
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -452,14 +492,48 @@ def kb_delete(article_id):
|
||||
|
||||
# ─── Activity Log ─────────────────────────────────────────────────────────────
|
||||
|
||||
@admin_bp.route('/logs')
|
||||
@admin_bp.route('/logs', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def activity_logs():
|
||||
# ── Cleanup action ────────────────────────────────────────────────────────
|
||||
if request.method == 'POST':
|
||||
days = request.form.get('days', type=int)
|
||||
if days not in (7, 30, 60, 90):
|
||||
flash('Invalid retention period selected.', 'danger')
|
||||
return redirect(url_for('admin.activity_logs'))
|
||||
|
||||
cutoff = datetime.utcnow() - timedelta(days=days)
|
||||
deleted = ActivityLog.query.filter(ActivityLog.created_at < cutoff).delete()
|
||||
db.session.commit()
|
||||
log_action(current_user.id, 'activity_log_cleanup', 'activity_log', None,
|
||||
f'deleted={deleted} older_than={days}_days cutoff={cutoff.strftime("%Y-%m-%d")}')
|
||||
db.session.commit()
|
||||
logger.info(f'[ACTIVITY LOG CLEANUP] deleted={deleted} days={days} by user_id={current_user.id}')
|
||||
flash(f'Deleted {deleted:,} log entr{"y" if deleted == 1 else "ies"} older than {days} days.', 'success')
|
||||
return redirect(url_for('admin.activity_logs'))
|
||||
|
||||
# ── Stats for the summary bar ─────────────────────────────────────────────
|
||||
from sqlalchemy import func
|
||||
total = ActivityLog.query.count()
|
||||
oldest = db.session.query(func.min(ActivityLog.created_at)).scalar()
|
||||
counts_by_retention = {}
|
||||
for days in (7, 30, 60, 90):
|
||||
cutoff = datetime.utcnow() - timedelta(days=days)
|
||||
counts_by_retention[days] = ActivityLog.query.filter(
|
||||
ActivityLog.created_at < cutoff
|
||||
).count()
|
||||
|
||||
# ── Paginated log listing ─────────────────────────────────────────────────
|
||||
page = request.args.get('page', 1, type=int)
|
||||
logs = ActivityLog.query.order_by(ActivityLog.created_at.desc()).paginate(
|
||||
page=page, per_page=50)
|
||||
return render_template('admin/activity_logs.html', logs=logs)
|
||||
return render_template('admin/activity_logs.html',
|
||||
logs = logs,
|
||||
total = total,
|
||||
oldest = oldest,
|
||||
counts_by_retention= counts_by_retention,
|
||||
)
|
||||
|
||||
|
||||
def _roles():
|
||||
|
||||
+42
-5
@@ -261,8 +261,19 @@ def update_ticket(ticket_id):
|
||||
changes.append(f'priority: {old_priority} → {new_priority}')
|
||||
|
||||
if new_assigned != old_assigned:
|
||||
# Resolve user IDs to full names for human-readable history entries.
|
||||
# None means unassigned.
|
||||
def _user_label(uid):
|
||||
if uid is None:
|
||||
return 'Unassigned'
|
||||
u = User.query.get(uid)
|
||||
return u.full_name if u else f'User #{uid}'
|
||||
|
||||
ticket.assigned_to_id = new_assigned
|
||||
log_ticket_history(ticket, 'assigned_to', str(old_assigned), str(new_assigned), current_user.id)
|
||||
log_ticket_history(ticket, 'assigned_to',
|
||||
_user_label(old_assigned),
|
||||
_user_label(new_assigned),
|
||||
current_user.id)
|
||||
changes.append(f'assigned_to: {old_assigned} → {new_assigned}')
|
||||
notify_assignment(ticket, current_user)
|
||||
|
||||
@@ -339,9 +350,35 @@ def mark_notifications_read():
|
||||
@tickets_bp.route('/kb')
|
||||
@login_required
|
||||
def knowledge_base():
|
||||
articles = KnowledgeBase.query.filter_by(is_published=True).order_by(
|
||||
KnowledgeBase.view_count.desc()).all()
|
||||
return render_template('tickets/knowledge_base.html', articles=articles)
|
||||
q = request.args.get('q', '').strip()
|
||||
category = request.args.get('category', '')
|
||||
sort = request.args.get('sort', 'popular') # 'popular' | 'newest'
|
||||
|
||||
query = KnowledgeBase.query.filter_by(is_published=True)
|
||||
|
||||
if q:
|
||||
query = query.filter(
|
||||
KnowledgeBase.title.ilike(f'%{q}%') |
|
||||
KnowledgeBase.tags.ilike(f'%{q}%')
|
||||
)
|
||||
if category:
|
||||
query = query.filter_by(category=category)
|
||||
|
||||
if sort == 'newest':
|
||||
query = query.order_by(KnowledgeBase.updated_at.desc())
|
||||
else:
|
||||
query = query.order_by(KnowledgeBase.view_count.desc())
|
||||
|
||||
articles = query.all()
|
||||
categories = sorted({a.category for a in KnowledgeBase.query.filter_by(is_published=True).with_entities(KnowledgeBase.category).distinct() if a.category})
|
||||
|
||||
return render_template('tickets/knowledge_base.html',
|
||||
articles = articles,
|
||||
categories = categories,
|
||||
q = q,
|
||||
sel_category = category,
|
||||
sort = sort,
|
||||
)
|
||||
|
||||
|
||||
@tickets_bp.route('/kb/<int:article_id>')
|
||||
@@ -367,4 +404,4 @@ def _categories():
|
||||
return [TicketCategory.HARDWARE, TicketCategory.SOFTWARE,
|
||||
TicketCategory.NETWORK, TicketCategory.ACCESS,
|
||||
TicketCategory.EMAIL, TicketCategory.PRINTER,
|
||||
TicketCategory.PHONE, TicketCategory.SECURITY, TicketCategory.OTHER]
|
||||
TicketCategory.PHONE, TicketCategory.SECURITY, TicketCategory.OTHER]
|
||||
Reference in New Issue
Block a user