Ok
This commit is contained in:
0
routes/__init__.py
Normal file
0
routes/__init__.py
Normal file
76
routes/admin.py
Normal file
76
routes/admin.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.security import generate_password_hash
|
||||
from models import User, db
|
||||
from utils import admin_required
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
@admin_bp.before_request
|
||||
@login_required
|
||||
@admin_required
|
||||
def before_request():
|
||||
"""在處理此藍圖中的任何請求之前,確保使用者是已登入的管理員。"""
|
||||
pass
|
||||
|
||||
@admin_bp.route('/users')
|
||||
def user_list():
|
||||
users = User.query.all()
|
||||
return render_template('user_management.html', users=users)
|
||||
|
||||
@admin_bp.route('/users/create', methods=['POST'])
|
||||
def create_user():
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
role = request.form.get('role')
|
||||
|
||||
if not all([username, password, role]):
|
||||
flash('所有欄位都是必填的!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('該使用者名稱已存在!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
new_user = User(
|
||||
username=username,
|
||||
password_hash=generate_password_hash(password),
|
||||
role=role
|
||||
)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
flash('新使用者已成功建立!', 'success')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
@admin_bp.route('/users/edit/<int:user_id>', methods=['POST'])
|
||||
def edit_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
new_role = request.form.get('role')
|
||||
new_password = request.form.get('password')
|
||||
|
||||
if new_role:
|
||||
# 防止 admin 修改自己的角色,導致失去管理權限
|
||||
if user.id == current_user.id and user.role == 'admin' and new_role != 'admin':
|
||||
flash('無法變更自己的管理員角色!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
user.role = new_role
|
||||
|
||||
if new_password:
|
||||
user.password_hash = generate_password_hash(new_password)
|
||||
|
||||
db.session.commit()
|
||||
flash(f"使用者 '{user.username}' 的資料已更新。", 'success')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
@admin_bp.route('/users/delete/<int:user_id>', methods=['POST'])
|
||||
def delete_user(user_id):
|
||||
# 避免 admin 刪除自己
|
||||
if user_id == current_user.id:
|
||||
flash('無法刪除自己的帳號!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(f"使用者 '{user.username}' 已被刪除。", 'success')
|
||||
return redirect(url_for('admin.user_list'))
|
37
routes/auth.py
Normal file
37
routes/auth.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_user, logout_user, login_required
|
||||
from werkzeug.security import check_password_hash
|
||||
from models import User, db
|
||||
from datetime import datetime
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user:
|
||||
print(f"🔍 嘗試登入使用者:{username}")
|
||||
else:
|
||||
print("⚠️ 使用者不存在")
|
||||
|
||||
if user and check_password_hash(user.password_hash, password):
|
||||
login_user(user)
|
||||
user.last_login = datetime.now()
|
||||
db.session.commit()
|
||||
print("✅ 登入成功")
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
else:
|
||||
print("❌ 登入失敗,帳號或密碼錯誤")
|
||||
flash('帳號或密碼錯誤,請重新輸入', 'danger')
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('auth.login'))
|
379
routes/temp_spec.py
Normal file
379
routes/temp_spec.py
Normal file
@@ -0,0 +1,379 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, send_file, current_app, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime, timedelta
|
||||
from models import TempSpec, db, Upload, SpecHistory
|
||||
from utils import fill_template, editor_or_admin_required, add_history_log, admin_required
|
||||
import os
|
||||
import tempfile
|
||||
from werkzeug.utils import secure_filename
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
import mistune
|
||||
|
||||
temp_spec_bp = Blueprint('temp_spec', __name__)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@temp_spec_bp.before_request
|
||||
@login_required
|
||||
def before_request():
|
||||
"""在處理此藍圖中的任何請求之前,確保使用者已登入。"""
|
||||
pass
|
||||
|
||||
def _generate_next_spec_code():
|
||||
"""
|
||||
產生下一個暫時規範編號。
|
||||
規則: PE + 民國年(3碼) + 月份(2碼) + 流水號(2碼)
|
||||
"""
|
||||
now = datetime.now()
|
||||
roc_year = now.year - 1911
|
||||
prefix = f"PE{roc_year}{now.strftime('%m')}"
|
||||
|
||||
latest_spec = TempSpec.query.filter(
|
||||
TempSpec.spec_code.startswith(prefix)
|
||||
).order_by(TempSpec.spec_code.desc()).first()
|
||||
|
||||
if latest_spec:
|
||||
last_seq = int(latest_spec.spec_code[-2:])
|
||||
new_seq = last_seq + 1
|
||||
else:
|
||||
new_seq = 1
|
||||
|
||||
return f"{prefix}{new_seq:02d}"
|
||||
|
||||
@temp_spec_bp.route('/preview', methods=['POST'])
|
||||
def preview_spec():
|
||||
"""產生預覽 PDF 並返回"""
|
||||
data = request.json
|
||||
|
||||
values = {
|
||||
'serial_number': data.get('serial_number', 'PREVIEW-SN'),
|
||||
'theme': data.get('theme', 'PREVIEW-THEME'),
|
||||
'applicant': data.get('applicant', ''),
|
||||
'applicant_phone': data.get('applicant_phone', ''),
|
||||
'station': data.get('station', ''),
|
||||
'tccs_info': data.get('tccs_info', ''),
|
||||
'start_date': data.get('start_date', datetime.today().strftime('%Y-%m-%d')),
|
||||
'end_date': (datetime.today() + timedelta(days=30)).strftime('%Y-%m-%d'),
|
||||
'package': data.get('package', ''),
|
||||
'lot_number': data.get('lot_number', ''),
|
||||
'equipment_type': data.get('equipment_type', ''),
|
||||
'change_before': data.get('change_before', ''),
|
||||
'change_after': data.get('change_after', ''),
|
||||
'data_needs': data.get('data_needs', ''),
|
||||
}
|
||||
|
||||
temp_docx_path = tempfile.mktemp(suffix=".docx")
|
||||
temp_pdf_path = tempfile.mktemp(suffix=".pdf")
|
||||
|
||||
try:
|
||||
template_path = os.path.join(BASE_DIR, 'template_with_placeholders.docx')
|
||||
fill_template(values, template_path, temp_docx_path, temp_pdf_path)
|
||||
|
||||
with open(temp_pdf_path, 'rb') as f:
|
||||
pdf_data = f.read()
|
||||
|
||||
import io
|
||||
return_data = io.BytesIO(pdf_data)
|
||||
|
||||
try:
|
||||
if os.path.exists(temp_docx_path):
|
||||
os.remove(temp_docx_path)
|
||||
if os.path.exists(temp_pdf_path):
|
||||
os.remove(temp_pdf_path)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"無法刪除暫存檔: {e}")
|
||||
|
||||
return send_file(return_data, mimetype='application/pdf')
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"預覽生成失敗: {e}")
|
||||
if os.path.exists(temp_docx_path):
|
||||
os.remove(temp_docx_path)
|
||||
if os.path.exists(temp_pdf_path):
|
||||
os.remove(temp_pdf_path)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@temp_spec_bp.route('/create', methods=['GET', 'POST'])
|
||||
@editor_or_admin_required
|
||||
def create_temp_spec():
|
||||
if request.method == 'POST':
|
||||
data = request.form
|
||||
now = datetime.now()
|
||||
serial_number = _generate_next_spec_code()
|
||||
stations = request.form.getlist('station')
|
||||
if '其他' in stations and data.get('station_other'):
|
||||
stations[stations.index('其他')] = data.get('station_other')
|
||||
station_str = ', '.join(stations)
|
||||
tccs_info = f"{data.get('tccs_level', '')} ({data.get('tccs_4m', '')})"
|
||||
|
||||
values = {
|
||||
'serial_number': serial_number,
|
||||
'theme': data['theme'],
|
||||
'applicant': data.get('applicant', ''),
|
||||
'applicant_phone': data.get('applicant_phone', ''),
|
||||
'station': station_str,
|
||||
'tccs_info': tccs_info,
|
||||
'start_date': data.get('start_date', ''),
|
||||
'package': data.get('package', ''),
|
||||
'lot_number': data.get('lot_number', ''),
|
||||
'equipment_type': data.get('equipment_type', ''),
|
||||
'change_before': data.get('change_before', ''),
|
||||
'change_after': data.get('change_after', ''),
|
||||
'data_needs': data.get('data_needs', ''),
|
||||
}
|
||||
|
||||
generated_folder = os.path.join(BASE_DIR, current_app.config['GENERATED_FOLDER'])
|
||||
os.makedirs(generated_folder, exist_ok=True)
|
||||
word_path = os.path.join(generated_folder, f"{values['serial_number']}.docx")
|
||||
pdf_path = os.path.join(generated_folder, f"{values['serial_number']}.pdf")
|
||||
|
||||
db_content_parts = []
|
||||
db_content_parts.append("變更前:\n")
|
||||
db_content_parts.append(values['change_before'])
|
||||
db_content_parts.append("\n\n變更後:\n")
|
||||
db_content_parts.append(values['change_after'])
|
||||
db_content_parts.append("\n\n資料收集需求:\n")
|
||||
db_content_parts.append(values['data_needs'])
|
||||
db_content = "".join(db_content_parts)
|
||||
|
||||
try:
|
||||
start_date_obj = datetime.strptime(values['start_date'], '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
start_date_obj = datetime.today().date()
|
||||
|
||||
end_date_obj = start_date_obj + timedelta(days=30)
|
||||
values['end_date'] = end_date_obj.strftime('%Y-%m-%d')
|
||||
|
||||
spec = TempSpec(
|
||||
spec_code=values['serial_number'],
|
||||
applicant=values['applicant'],
|
||||
title=values['theme'],
|
||||
content=db_content,
|
||||
start_date=start_date_obj,
|
||||
end_date=end_date_obj,
|
||||
created_at=now,
|
||||
status='pending_approval'
|
||||
)
|
||||
db.session.add(spec)
|
||||
db.session.flush()
|
||||
add_history_log(spec.id, '建立', f"建立暫時規範,編號為 {spec.spec_code}")
|
||||
db.session.commit()
|
||||
|
||||
# 在產生用於下載的 PDF 前,需將 Markdown 轉為 HTML
|
||||
values['change_before'] = mistune.html(values['change_before'])
|
||||
values['change_after'] = mistune.html(values['change_after'])
|
||||
try:
|
||||
fill_template(values, os.path.join(BASE_DIR, 'template_with_placeholders.docx'), word_path, pdf_path)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"檔案生成失敗: {e}")
|
||||
flash('檔案生成失敗,可能是 Word 模板或 PDF 轉換器問題,請聯絡管理員。', 'danger')
|
||||
return redirect(url_for('temp_spec.create_temp_spec'))
|
||||
|
||||
return send_file(word_path, as_attachment=True)
|
||||
|
||||
next_spec_code = _generate_next_spec_code()
|
||||
return render_template('create_temp_spec.html', next_spec_code=next_spec_code)
|
||||
|
||||
@temp_spec_bp.route('/list')
|
||||
def spec_list():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
query = request.args.get('query', '')
|
||||
status_filter = request.args.get('status', '')
|
||||
specs_query = TempSpec.query
|
||||
|
||||
if query:
|
||||
search_term = f"%{query}%"
|
||||
specs_query = specs_query.filter(
|
||||
db.or_(
|
||||
TempSpec.spec_code.ilike(search_term),
|
||||
TempSpec.title.ilike(search_term)
|
||||
)
|
||||
)
|
||||
|
||||
if status_filter:
|
||||
specs_query = specs_query.filter(TempSpec.status == status_filter)
|
||||
|
||||
pagination = specs_query.order_by(TempSpec.created_at.desc()).paginate(
|
||||
page=page, per_page=15, error_out=False
|
||||
)
|
||||
|
||||
specs = pagination.items
|
||||
return render_template('spec_list.html', specs=specs, pagination=pagination, query=query, status=status_filter)
|
||||
|
||||
@temp_spec_bp.route('/activate/<int:spec_id>', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def activate_spec(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
if request.method == 'POST':
|
||||
uploaded_file = request.files.get('signed_file')
|
||||
if not uploaded_file or uploaded_file.filename == '':
|
||||
flash('您必須上傳一個檔案。', 'danger')
|
||||
return redirect(url_for('temp_spec.activate_spec', spec_id=spec.id))
|
||||
|
||||
filename = secure_filename(f"{spec.spec_code}_signed_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf")
|
||||
upload_folder = os.path.join(BASE_DIR, current_app.config['UPLOAD_FOLDER'])
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
file_path = os.path.join(upload_folder, filename)
|
||||
uploaded_file.save(file_path)
|
||||
|
||||
new_upload = Upload(
|
||||
temp_spec_id=spec.id,
|
||||
filename=filename,
|
||||
upload_time=datetime.now()
|
||||
)
|
||||
db.session.add(new_upload)
|
||||
|
||||
spec.status = 'active'
|
||||
add_history_log(spec.id, '啟用', f"上傳已簽核檔案 '{filename}'")
|
||||
db.session.commit()
|
||||
flash(f"規範 '{spec.spec_code}' 已生效!", 'success')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
return render_template('activate_spec.html', spec=spec)
|
||||
|
||||
@temp_spec_bp.route('/terminate/<int:spec_id>', methods=['GET', 'POST'])
|
||||
@editor_or_admin_required
|
||||
def terminate_spec(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
if request.method == 'POST':
|
||||
reason = request.form.get('reason')
|
||||
if not reason:
|
||||
flash('請填寫提早結束的原因。', 'danger')
|
||||
return redirect(url_for('temp_spec.terminate_spec', spec_id=spec.id))
|
||||
|
||||
spec.status = 'terminated'
|
||||
spec.termination_reason = reason
|
||||
spec.end_date = datetime.today().date()
|
||||
add_history_log(spec.id, '終止', f"原因: {reason}")
|
||||
db.session.commit()
|
||||
flash(f"規範 '{spec.spec_code}' 已被提早終止。", 'warning')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
return render_template('terminate_spec.html', spec=spec)
|
||||
|
||||
@temp_spec_bp.route('/download_initial/<int:spec_id>')
|
||||
def download_initial_pdf(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
generated_folder = os.path.join(BASE_DIR, current_app.config['GENERATED_FOLDER'])
|
||||
pdf_path = os.path.join(generated_folder, f"{spec.spec_code}.pdf")
|
||||
|
||||
if not os.path.exists(pdf_path):
|
||||
flash('找不到最初產生的 PDF 檔案,可能已被刪除或移動。', 'danger')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
return send_file(pdf_path, as_attachment=True)
|
||||
|
||||
@temp_spec_bp.route('/download_initial_word/<int:spec_id>')
|
||||
@login_required
|
||||
def download_initial_word(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
# 安全性檢查:只有 editor 和 admin 可以下載 word
|
||||
if current_user.role not in ['editor', 'admin']:
|
||||
flash('權限不足,無法下載 Word 檔案。', 'danger')
|
||||
abort(403)
|
||||
|
||||
generated_folder = os.path.join(BASE_DIR, current_app.config['GENERATED_FOLDER'])
|
||||
word_path = os.path.join(generated_folder, f"{spec.spec_code}.docx")
|
||||
|
||||
if not os.path.exists(word_path):
|
||||
flash('找不到最初產生的 Word 檔案,可能已被刪除或移動。', 'danger')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
return send_file(word_path, as_attachment=True)
|
||||
|
||||
@temp_spec_bp.route('/download_signed/<int:spec_id>')
|
||||
def download_signed_pdf(spec_id):
|
||||
latest_upload = Upload.query.filter_by(temp_spec_id=spec_id).order_by(Upload.upload_time.desc()).first()
|
||||
|
||||
if not latest_upload:
|
||||
flash('找不到任何已上傳的簽核檔案。', 'danger')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
upload_folder = os.path.join(BASE_DIR, current_app.config['UPLOAD_FOLDER'])
|
||||
return send_file(os.path.join(upload_folder, latest_upload.filename), as_attachment=True)
|
||||
|
||||
@temp_spec_bp.route('/extend/<int:spec_id>', methods=['GET', 'POST'])
|
||||
@editor_or_admin_required
|
||||
def extend_spec(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
new_end_date_str = request.form.get('new_end_date')
|
||||
uploaded_file = request.files.get('new_file')
|
||||
|
||||
if not new_end_date_str:
|
||||
flash('請選擇新的結束日期', 'danger')
|
||||
return redirect(url_for('temp_spec.extend_spec', spec_id=spec.id))
|
||||
|
||||
spec.end_date = datetime.strptime(new_end_date_str, '%Y-%m-%d').date()
|
||||
spec.extension_count += 1
|
||||
spec.status = 'active'
|
||||
|
||||
if uploaded_file and uploaded_file.filename != '':
|
||||
filename = secure_filename(uploaded_file.filename)
|
||||
upload_folder = os.path.join(BASE_DIR, current_app.config['UPLOAD_FOLDER'])
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
file_path = os.path.join(upload_folder, filename)
|
||||
uploaded_file.save(file_path)
|
||||
|
||||
new_upload = Upload(
|
||||
temp_spec_id=spec.id,
|
||||
filename=filename,
|
||||
upload_time=datetime.now()
|
||||
)
|
||||
db.session.add(new_upload)
|
||||
|
||||
details = f"展延結束日期至 {spec.end_date.strftime('%Y-%m-%d')}"
|
||||
if 'new_upload' in locals():
|
||||
details += f",並上傳新檔案 '{new_upload.filename}'"
|
||||
add_history_log(spec.id, '展延', details)
|
||||
|
||||
db.session.commit()
|
||||
flash(f"規範 '{spec.spec_code}' 已成功展延!", 'success')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
default_new_end_date = spec.end_date + timedelta(days=30)
|
||||
return render_template('extend_spec.html', spec=spec, default_new_end_date=default_new_end_date)
|
||||
|
||||
@temp_spec_bp.route('/history/<int:spec_id>')
|
||||
def spec_history(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
history = SpecHistory.query.filter_by(spec_id=spec_id).order_by(SpecHistory.timestamp.desc()).all()
|
||||
return render_template('spec_history.html', spec=spec, history=history)
|
||||
|
||||
@temp_spec_bp.route('/delete/<int:spec_id>', methods=['POST'])
|
||||
@admin_required
|
||||
def delete_spec(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
spec_code = spec.spec_code
|
||||
|
||||
files_to_delete = []
|
||||
generated_folder = os.path.join(BASE_DIR, current_app.config['GENERATED_FOLDER'])
|
||||
files_to_delete.append(os.path.join(generated_folder, f"{spec.spec_code}.docx"))
|
||||
files_to_delete.append(os.path.join(generated_folder, f"{spec.spec_code}.pdf"))
|
||||
|
||||
upload_folder = os.path.join(BASE_DIR, current_app.config['UPLOAD_FOLDER'])
|
||||
for upload_record in spec.uploads:
|
||||
files_to_delete.append(os.path.join(upload_folder, upload_record.filename))
|
||||
|
||||
image_folder = os.path.join(BASE_DIR, 'static', 'uploads', 'images')
|
||||
if spec.content:
|
||||
image_urls = re.findall(r'!\[.*?\]\((.*?)\)', spec.content)
|
||||
for url in image_urls:
|
||||
if url.startswith('/static/uploads/images/'):
|
||||
img_filename = os.path.basename(url)
|
||||
files_to_delete.append(os.path.join(image_folder, img_filename))
|
||||
|
||||
for f_path in files_to_delete:
|
||||
try:
|
||||
if os.path.exists(f_path):
|
||||
os.remove(f_path)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"刪除檔案失敗: {f_path}, 原因: {e}")
|
||||
|
||||
db.session.delete(spec)
|
||||
db.session.commit()
|
||||
|
||||
flash(f"規範 '{spec_code}' 及其所有相關檔案已成功刪除。", 'success')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
29
routes/upload.py
Normal file
29
routes/upload.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
import os
|
||||
import time
|
||||
|
||||
upload_bp = Blueprint('upload', __name__)
|
||||
|
||||
@upload_bp.route('/image', methods=['POST'])
|
||||
def upload_image():
|
||||
file = request.files.get('file')
|
||||
if not file:
|
||||
return jsonify({'error': 'No file part'}), 400
|
||||
|
||||
# 建立一個獨特的檔名
|
||||
extension = os.path.splitext(file.filename)[1]
|
||||
filename = f"{int(time.time())}_{secure_filename(file.filename)}"
|
||||
|
||||
# 確保上傳資料夾存在
|
||||
# 為了讓圖片能被網頁存取,我們將它存在 static 資料夾下
|
||||
image_folder = os.path.join(current_app.static_folder, 'uploads', 'images')
|
||||
os.makedirs(image_folder, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(image_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# 回傳 TinyMCE 需要的 JSON 格式
|
||||
# 路徑必須是相對於網域根目錄的 URL
|
||||
location = f"/static/uploads/images/{filename}"
|
||||
return jsonify({'location': location})
|
Reference in New Issue
Block a user