diff --git a/app/__init__.py b/app/__init__.py index d3ed74f..35e7219 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -39,7 +39,7 @@ def create_app(config_name=None): login_manager.login_message_category = 'info' # ── Upload directory ────────────────────────────────────────────────────── - upload_dir = app.config.get('UPLOAD_FOLDER', 'app/static/uploads') + upload_dir = app.config['UPLOAD_FOLDER'] # always absolute — set in config.py os.makedirs(upload_dir, exist_ok=True) # ── Logging ─────────────────────────────────────────────────────────────── diff --git a/app/routes/admin.py b/app/routes/admin.py index 7a0fd0a..d12bc3e 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -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//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) diff --git a/app/templates/admin/kb_edit.html b/app/templates/admin/kb_edit.html index efe670c..e49b09c 100644 --- a/app/templates/admin/kb_edit.html +++ b/app/templates/admin/kb_edit.html @@ -40,7 +40,9 @@ {% if article %}Edit: {{ article.title[:50] }}{% else %}Create New Article{% endif %}
-
+
- +
{% if article %} - {% set existing = article.attachments.all() %} - {% if existing %} -
- -
- {% for att in existing %} -
- - - {{ att.filename }} - - {{ (att.file_size / 1024)|int }} KB - - - -
- {% endfor %} -
+ +
+ +
+
- {% endif %} +
{% endif %}