Enhance Article editor 2

This commit is contained in:
2026-03-25 16:19:11 -04:00
parent 3ba7eae390
commit bee7146073
4 changed files with 360 additions and 247 deletions
+75 -47
View File
@@ -212,7 +212,7 @@ def _save_kb_file(file, article_id):
filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else 'bin'
stored_name = f"{uuid.uuid4().hex}.{ext}"
upload_dir = current_app.config.get('UPLOAD_FOLDER', 'app/static/uploads')
upload_dir = current_app.config['UPLOAD_FOLDER']
os.makedirs(upload_dir, exist_ok=True)
filepath = os.path.join(upload_dir, stored_name)
file.save(filepath)
@@ -245,11 +245,12 @@ def kb_upload_image():
return jsonify({'error': 'Only image files are accepted (PNG, JPG, GIF, WEBP)'}), 400
stored_name = f"{uuid.uuid4().hex}.{ext}"
upload_dir = current_app.config.get('UPLOAD_FOLDER', 'app/static/uploads')
upload_dir = current_app.config['UPLOAD_FOLDER']
os.makedirs(upload_dir, exist_ok=True)
f.save(os.path.join(upload_dir, stored_name))
logger.info(f'[KB IMAGE UPLOAD] stored_name={stored_name} by user_id={current_user.id}')
return jsonify({'location': f'/admin/kb/files/{stored_name}'})
location = url_for('admin.kb_serve_file', stored_name=stored_name)
return jsonify({'location': location})
# ── File-serve route (images embedded in articles + attachment downloads) ─────
@@ -258,7 +259,7 @@ def kb_upload_image():
@login_required
def kb_serve_file(stored_name):
"""Serve a KB attachment file. Login required — no public access."""
upload_dir = current_app.config.get('UPLOAD_FOLDER', 'app/static/uploads')
upload_dir = current_app.config['UPLOAD_FOLDER']
return send_from_directory(upload_dir, stored_name)
@@ -269,7 +270,7 @@ def kb_serve_file(stored_name):
@it_required
def kb_delete_attachment(article_id, att_id):
att = KBAttachment.query.filter_by(id=att_id, article_id=article_id).first_or_404()
upload_dir = current_app.config.get('UPLOAD_FOLDER', 'app/static/uploads')
upload_dir = current_app.config['UPLOAD_FOLDER']
filepath = os.path.join(upload_dir, att.stored_name)
if os.path.exists(filepath):
os.remove(filepath)
@@ -278,10 +279,33 @@ def kb_delete_attachment(article_id, att_id):
logger.info(f'[KB ATTACHMENT DELETE] att_id={att.id} article_id={article_id} by user_id={current_user.id}')
db.session.delete(att)
db.session.commit()
flash(f'Attachment "{att.filename}" deleted.', 'success')
return redirect(url_for('admin.kb_edit', article_id=article_id))
logger.info(f'[KB ATTACHMENT DELETE] att_id={att.id} article_id={article_id} completed')
# Return JSON so the edit page can remove the row without a full reload
return jsonify({'ok': True, 'att_id': att.id})
@admin_bp.route('/kb/<int:article_id>/attachments', methods=['GET'])
@login_required
@it_required
def kb_get_attachments(article_id):
"""Return current attachment list as JSON for dynamic UI refresh."""
from app.models import KBAttachment
atts = KBAttachment.query.filter_by(article_id=article_id).all()
return jsonify({'attachments': [
{
'id': a.id,
'filename': a.filename,
'stored_name': a.stored_name,
'file_size': a.file_size,
'mime_type': a.mime_type or '',
'url': url_for('admin.kb_serve_file', stored_name=a.stored_name),
'delete_url': url_for('admin.kb_delete_attachment',
article_id=article_id, att_id=a.id),
}
for a in atts
]})
# ── Create article ─────────────────────────────────────────────────────────────
@admin_bp.route('/kb/new', methods=['GET', 'POST'])
@@ -289,31 +313,33 @@ def kb_delete_attachment(article_id, att_id):
@it_required
def kb_new():
if request.method == 'POST':
article = KnowledgeBase(
title = request.form.get('title', '').strip(),
body = request.form.get('body', '').strip(),
category = request.form.get('category', ''),
tags = request.form.get('tags', ''),
author_id = current_user.id,
is_published= bool(request.form.get('is_published')) and not bool(request.form.get('_save_as_draft')),
)
db.session.add(article)
db.session.flush() # get article.id before saving attachments
try:
article = KnowledgeBase(
title = request.form.get('title', '').strip(),
body = request.form.get('body', '').strip(),
category = request.form.get('category', ''),
tags = request.form.get('tags', ''),
author_id = current_user.id,
is_published= bool(request.form.get('is_published')) and not bool(request.form.get('_save_as_draft')),
)
db.session.add(article)
db.session.flush()
for f in request.files.getlist('attachments'):
if f and f.filename and _kb_allowed(f.filename):
att = _save_kb_file(f, article.id)
db.session.add(att)
for f in request.files.getlist('attachments'):
if f and f.filename and _kb_allowed(f.filename):
att = _save_kb_file(f, article.id)
db.session.add(att)
db.session.commit()
log_action(current_user.id, 'kb_create', 'knowledge_base', article.id,
f'title={article.title}')
logger.info(f'[KB CREATE] article_id={article.id} by user_id={current_user.id}')
flash('Article created successfully.', 'success')
# XHR fetch submit: return JSON so JS can redirect to the final URL
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or 'multipart/form-data' in request.content_type:
return jsonify({'redirect': url_for('admin.kb_list')})
return redirect(url_for('admin.kb_list'))
db.session.commit()
log_action(current_user.id, 'kb_create', 'knowledge_base', article.id,
f'title={article.title}')
logger.info(f'[KB CREATE] article_id={article.id} by user_id={current_user.id}')
flash('Article created successfully.', 'success')
return jsonify({'ok': True, 'article_id': article.id, 'redirect': url_for('admin.kb_list')})
except Exception as exc:
db.session.rollback()
logger.error(f'[KB CREATE ERROR] {exc}')
return jsonify({'error': str(exc)}), 500
return render_template('admin/kb_edit.html', article=None)
@@ -325,25 +351,27 @@ def kb_new():
def kb_edit(article_id):
article = KnowledgeBase.query.get_or_404(article_id)
if request.method == 'POST':
article.title = request.form.get('title', article.title).strip()
article.body = request.form.get('body', article.body).strip()
article.category = request.form.get('category', article.category)
article.tags = request.form.get('tags', article.tags)
article.is_published= bool(request.form.get('is_published')) and not bool(request.form.get('_save_as_draft'))
try:
article.title = request.form.get('title', article.title).strip()
article.body = request.form.get('body', article.body).strip()
article.category = request.form.get('category', article.category)
article.tags = request.form.get('tags', article.tags)
article.is_published= bool(request.form.get('is_published')) and not bool(request.form.get('_save_as_draft'))
for f in request.files.getlist('attachments'):
if f and f.filename and _kb_allowed(f.filename):
att = _save_kb_file(f, article.id)
db.session.add(att)
for f in request.files.getlist('attachments'):
if f and f.filename and _kb_allowed(f.filename):
att = _save_kb_file(f, article.id)
db.session.add(att)
db.session.commit()
log_action(current_user.id, 'kb_edit', 'knowledge_base', article.id)
logger.info(f'[KB EDIT] article_id={article.id} by user_id={current_user.id}')
flash('Article updated successfully.', 'success')
# XHR fetch submit: return JSON so JS can redirect to the final URL
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or 'multipart/form-data' in request.content_type:
return jsonify({'redirect': url_for('admin.kb_list')})
return redirect(url_for('admin.kb_list'))
db.session.commit()
log_action(current_user.id, 'kb_edit', 'knowledge_base', article.id)
logger.info(f'[KB EDIT] article_id={article.id} by user_id={current_user.id}')
flash('Article updated successfully.', 'success')
return jsonify({'ok': True, 'article_id': article.id, 'redirect': url_for('admin.kb_list')})
except Exception as exc:
db.session.rollback()
logger.error(f'[KB EDIT ERROR] {exc}')
return jsonify({'error': str(exc)}), 500
return render_template('admin/kb_edit.html', article=article)