final stage -- still some bugs

This commit is contained in:
hayano
2024-11-08 04:30:58 +00:00
parent 2aaecb6b22
commit 9eb45d7e97
7 changed files with 743 additions and 82 deletions

View File

@ -3,6 +3,7 @@ FROM osgeo/gdal:ubuntu-small-3.4.0
WORKDIR /app
LABEL maintainer="nouffer@gmail.com"
LABEL description="Development image for the Rogaining JP"
@ -38,8 +39,28 @@ RUN apt-get install -y python3
RUN apt-get update && apt-get install -y \
python3-pip
# ベースイメージの更新とパッケージのインストール
RUN apt-get update && \
apt-get install -y \
libreoffice \
libreoffice-calc \
libreoffice-writer \
python3-uno # LibreOffice Python バインディング
# 作業ディレクトリとパーミッションの設定
RUN mkdir -p /app/docbase /tmp/libreoffice && \
chmod -R 777 /app/docbase /tmp/libreoffice
RUN pip install --upgrade pip
RUN pip install -e ./SumasenLibs/excel_lib
# Copy the package directory first
COPY SumasenLibs/excel_lib /app/SumasenLibs/excel_lib
COPY ./docbase /app/docbase
# Install the package in editable mode
RUN pip install -e /app/SumasenLibs/excel_lib
RUN apt-get update

View File

@ -31,9 +31,26 @@ import requests
from io import BytesIO
from PIL import Image
import re
from django.http import HttpResponse
import uuid
import unicodedata
import tempfile
import os
import logging
from openpyxl.utils import get_column_letter
from openpyxl.drawing.image import Image as XLImage
from openpyxl.drawing.spreadsheet_drawing import OneCellAnchor, AnchorMarker
from openpyxl.utils.units import pixels_to_EMU
from PIL import Image
import logging
logging.getLogger('PIL').setLevel(logging.WARNING)
logging.getLogger('openpyxl').setLevel(logging.WARNING)
logging.getLogger('PIL.PngImagePlugin').setLevel(logging.WARNING)
logging.getLogger('PIL.TiffImagePlugin').setLevel(logging.WARNING)
logging.basicConfig(
level=logging.INFO, # INFOレベル以上のログを表示
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
@ -76,6 +93,8 @@ class SumasenExcel:
self._page_manager = None
try:
logging.info("step-1")
# document base を設定
self.docpath = docbase
if not os.path.exists(docbase):
@ -94,6 +113,8 @@ class SumasenExcel:
self.basic = basic
logging.info("step-2")
# basicセクションから必要なパラメータを取得
template_file = basic.get("template_file")
if not template_file:
@ -114,6 +135,7 @@ class SumasenExcel:
if not sections:
logging.error("sections not found in basic section")
logging.info("step-3")
# セクションをリストに変換
self.section_list = [s.strip() for s in sections.split(",")]
@ -127,11 +149,15 @@ class SumasenExcel:
# デフォルトで作成されるシートを削除
self.workbook.remove(self.workbook.active)
logging.info("step-4")
# テンプレートワークブックをロード
self.template_filepath = f"{self.docpath}/{template_file}"
if not os.path.exists(self.template_filepath):
logging.error(f"Template file not found: {self.template_filepath}")
logging.info("step-5")
self.template_workbook = openpyxl.load_workbook(self.template_filepath)
self.template_sheet = self.template_workbook.active
@ -275,7 +301,8 @@ class SumasenExcel:
logging.error(f"Error in proceed_group: {str(e)}")
return {"status": False, "message": f"Exception in proceed_group : Error generating report: {str(e)}"}
def format_cell_value(self,field_value, cell):
def format_cell_value(self, field_value, cell):
"""セルの値を適切な形式に変換する
Args:
@ -300,53 +327,124 @@ class SumasenExcel:
jst_time = field_value.astimezone(jst)
return jst_time.strftime('%H:%M:%S')
# 文字列の場合、URLかどうかをチェック
# 文字列の場合の処理
if isinstance(field_value, str):
# URL形式かどうかチェック
try:
result = urlparse(field_value)
# URLで、かつ画像ファイルの拡張子を持つ場合
if all([result.scheme, result.netloc]) and \
re.search(r'\.(jpg|jpeg|png|gif|bmp)$', result.path, re.I):
# 画像ファイルパスを取得する関数
def get_image_path(value):
# URLの場合
if value.startswith('https://'):
# URLからファイル名を抽出
filename = os.path.basename(urlparse(value).path)
return os.path.join("/app/media/compressed", filename)
# checkinパスの場合
elif value.startswith('checkin/'):
return os.path.join("/app/media", value)
# ファイル名のみの場合
elif re.search(r'\.(jpg|jpeg|png|gif|bmp|mpo)$', value, re.I):
return os.path.join("/app/media/compressed", value)
return None
# 画像パスを取得
image_path = get_image_path(field_value)
if image_path and os.path.exists(image_path):
try:
# 画像をダウンロード
response = requests.get(field_value)
img = Image.open(BytesIO(response.content))
from PIL import Image
import io
# セルの大きさを取得(ピクセル単位)
cell_width = cell.column_dimensions.width * 6 # 概算のピクセル変換
cell_height = cell.row_dimensions.height
# 画像を開く
with Image.open(image_path) as img:
# RGBAの場合はRGBに変換
if img.mode in ('RGBA', 'LA'):
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[-1])
img = background
elif img.mode not in ('RGB', 'L'):
img = img.convert('RGB')
# アスペクト比を保持しながらリサイズ
img_aspect = img.width / img.height
cell_aspect = cell_width / cell_height
# ワークシートを取得
worksheet = cell.parent
if img_aspect > cell_aspect:
width = int(cell_width)
height = int(width / img_aspect)
else:
height = int(cell_height)
width = int(height * img_aspect)
try:
# 列の幅を取得(文字単位からピクセルに変換)
column_letter = get_column_letter(cell.column)
column_width = worksheet.column_dimensions[column_letter].width
cell_width = int((column_width or 8.43) * 7.5) # 8.43は標準の文字幅
except Exception:
cell_width = 100 # デフォルト値
# 画像をリサイズしてセルに追加
img = img.resize((width, height))
try:
# 行の高さを取得(ポイント単位からピクセルに変換)
row_height = worksheet.row_dimensions[cell.row].height
cell_height = int((row_height or 15) * 1.33) # 15は標準の行の高さ
except Exception:
cell_height = 20 # デフォルト値
# 最小サイズを設定
cell_width = max(cell_width, 100)
cell_height = max(cell_height, 20)
# 最大サイズを設定
max_width = 800
max_height = 600
cell_width = min(cell_width, max_width)
cell_height = min(cell_height, max_height)
# アスペクト比を保持しながらリサイズ
img_width, img_height = img.size
img_aspect = img_width / img_height
cell_aspect = cell_width / cell_height
if img_aspect > cell_aspect:
width = cell_width
height = int(width / img_aspect)
else:
height = cell_height
width = int(height * img_aspect)
# 画像をリサイズ
img_resized = img.resize((width, height), Image.BICUBIC)
# BytesIOオブジェクトを作成して画像を保存
img_byte_arr = io.BytesIO()
img_resized.save(img_byte_arr, format='JPEG', quality=85, optimize=True)
img_byte_arr.seek(0)
# OpenPyXLのImageオブジェクトを作成
from openpyxl.drawing.image import Image as XLImage
excel_image = XLImage(img_byte_arr)
# 画像の配置
excel_image.anchor = f"{column_letter}{cell.row}"
# 画像をワークシートに追加
worksheet.add_image(excel_image)
return ""
# 画像をセルに追加実装方法はExcelライブラリに依存
if hasattr(cell, 'add_image'): # 使用するライブラリによって適切なメソッドを使用
cell.add_image(img)
return "" # 画像を追加したので、テキスト値は空にする
except Exception as e:
logging.warning(f"画像の処理に失敗: {str(e)}")
return field_value # エラーの場合はURLをそのまま返す
logging.error(f"画像の処理に失敗: {str(e)}, image_path={image_path}")
import traceback
logging.error(traceback.format_exc())
return field_value
else:
if image_path:
logging.warning(f"画像ファイルが存在しません: {image_path}")
return field_value
except Exception as e:
logging.warning(f"形式変換の処理に失敗: {str(e)}")
return field_value # エラーの場合はURLをそのまま返す
logging.error(f"形式変換の処理に失敗: {str(e)}")
import traceback
logging.error(traceback.format_exc())
return field_value
# その他の場合は文字列に変換
return str(field_value)
def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]):
"""1レコードのデータを取得してシートの値を置き換える

26
docbase/certificate.ini Normal file
View File

@ -0,0 +1,26 @@
[basic]
template_file=certificate_template.xlsx
doc_file=certificate_[zekken_number].xlsx
sections=section1
maxcol=10
column_width=3,5,16,16,16,16,16,8,8,12,3
[section1]
template_sheet=certificate
sheet_name=certificate
groups=group1,group2
fit_to_width=1
orientation=portrait
[section1.group1]
table_name=mv_entry_details
where=zekken_number='[zekken_number]' and event_name='[event_code]'
group_range=A1:J12
[section1.group2]
table_name=v_checkins_locations
where=zekken_number='[zekken_number]' and event_code='[event_code]'
sort=path_order
group_range=A13:J13

Binary file not shown.

View File

@ -631,17 +631,12 @@ class GpsCheckin(models.Model):
class Meta:
db_table = 'gps_checkins'
constraints = [
models.UniqueConstraint(
fields=['zekken_number', 'event_code', 'path_order'],
name='unique_gps_checkin'
)
]
indexes = [
models.Index(fields=['zekken_number', 'event_code','path_order'], name='idx_zekken_event'),
models.Index(fields=['zekken_number', 'event_code', 'path_order'], name='idx_zekken_event'),
models.Index(fields=['create_at'], name='idx_create_at'),
]
def __str__(self):
return f"{self.event_code}-{self.zekken_number}-{self.path_order}-buy:{self.buy_flag}-valid:{self.validate_location}-point:{self.points}"

View File

@ -5,6 +5,10 @@ User = get_user_model()
import traceback
from django.contrib.auth.hashers import make_password
import subprocess # subprocessモジュールを追加
import tempfile # tempfileモジュールを追加
import shutil # shutilモジュールを追加
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
@ -90,7 +94,7 @@ from io import BytesIO
from django.urls import get_resolver
import os
import json
from django.http import HttpResponse
from sumaexcel import SumasenExcel
logger = logging.getLogger(__name__)
@ -2538,6 +2542,80 @@ def get_checkins(request, *args, **kwargs):
@api_view(['POST'])
def update_checkins(request):
try:
with transaction.atomic():
update_base = request.data
logger.info(f"Processing update data: {update_base}")
zekken_number = update_base['zekken_number']
event_code = update_base['event_code']
# 既存レコードの更新
for update in update_base['checkins']:
if 'id' in update and int(update['id']) > 0:
try:
checkin = GpsCheckin.objects.get(id=update['id'])
logger.info(f"Updating existing checkin: {checkin}")
# 既存レコードの更新
checkin.path_order = update['order']
checkin.buy_flag = update.get('buy_flag', False)
checkin.validate_location = update.get('validation', False)
checkin.points = update.get('points', 0)
checkin.update_at = timezone.now()
checkin.update_user = request.user.email if request.user.is_authenticated else None
checkin.save()
logger.info(f"Updated existing checkin result: {checkin}")
except GpsCheckin.DoesNotExist:
logger.error(f"Checkin with id {update['id']} not found")
continue # エラーを無視して次のレコードの処理を継続
# 新規レコードの作成
for update in update_base['checkins']:
if 'id' in update and int(update['id']) == 0:
logger.info(f"Creating new checkin: {update}")
try:
checkin = GpsCheckin.objects.create(
zekken_number=zekken_number,
event_code=event_code,
path_order=update['order'],
cp_number=update['cp_number'],
validate_location=update.get('validation', False),
buy_flag=update.get('buy_flag', False),
points=update.get('points', 0),
create_at=timezone.now(),
update_at=timezone.now(),
create_user=request.user.email if request.user.is_authenticated else None,
update_user=request.user.email if request.user.is_authenticated else None
)
logger.info(f"Created new checkin: {checkin}")
except Exception as e:
logger.error(f"Error creating new checkin: {str(e)}")
continue # エラーを無視して次のレコードの処理を継続
# 更新後のデータを順序付けて取得
updated_checkins = GpsCheckin.objects.filter(
zekken_number=zekken_number,
event_code=event_code
).order_by('path_order')
return Response({
'status': 'success',
'message': 'Checkins updated successfully',
'data': [{'id': c.id, 'path_order': c.path_order} for c in updated_checkins]
})
except Exception as e:
logger.error(f"Error in update_checkins: {str(e)}", exc_info=True)
return Response(
{"error": "Failed to update checkins", "detail": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
def update_checkins_old(request):
try:
with transaction.atomic():
update_base = request.data
@ -2605,43 +2683,402 @@ def update_checkins(request):
)
@api_view(['GET'])
def export_excel(request, zekken_number):
def export_excel(request, zekken_number, event_code):
temp_dir = None
try:
# パラメータを文字列型に変換
zekken_number = str(zekken_number)
event_code = str(event_code)
# 初期化
variables = {
"zekken_number":sekken_number,
"event_code":request["FC岐阜"],
"db":"rogdb",
"username":"admin",
"password":"admin123456",
"host":"localhost",
"port":"5432"
}
excel = SumasenExcel(document="test", variables=variables, docbase="./docbase")
# ./docbase/certificate.ini の定義をベースに、
# ./docbase/certificate_template.xlsxを読み込み
# ./docbase/certificate_(zekken_number).xlsxを作成する
logger.info(f"Exporting Excel/PDF for zekken_number: {zekken_number}, event_code: {event_code}")
# シート初期化
ret = excel.make_report(variables=variables)
if ret["status"]==True:
filepath=ret["filepath"]
logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
else:
message = ret.get("message", "No message provided")
logging.error(f"Excelファイル作成失敗 : ret.message={message}")
# 入力値の検証
if not zekken_number or not event_code:
logger.error("Missing required parameters")
return Response(
{"error": "Both zekken_number and event_code are required"},
status=status.HTTP_400_BAD_REQUEST
)
# docbaseディレクトリのパスを絶対パスで設定
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
docbase_path = os.path.join(base_dir, 'docbase')
# ディレクトリ存在確認と作成
os.makedirs(docbase_path, exist_ok=True)
# 設定ファイルのパス
template_path = os.path.join(docbase_path, 'certificate_template.xlsx')
ini_path = os.path.join(docbase_path, 'certificate.ini')
# テンプレートと設定ファイルの存在確認
if not os.path.exists(template_path):
logger.error(f"Template file not found: {template_path}")
return Response(
{"error": "Excel template file missing"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if not os.path.exists(ini_path):
logger.error(f"INI file not found: {ini_path}")
return Response(
{"error": "Configuration file missing"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# Docker環境用のデータベース設定を使用
db_settings = settings.DATABASES['default']
# 初期化
variables = {
"zekken_number": str(zekken_number),
"event_code": str(event_code),
"db": str(db_settings['NAME']),
"username": str(db_settings['USER']),
"password": str(db_settings['PASSWORD']),
"host": str(db_settings['HOST']),
"port": str(db_settings['PORT']),
"template_path": template_path
}
try:
excel = SumasenExcel(document="certificate", variables=variables, docbase=docbase_path)
ret = excel.make_report(variables=variables)
if ret["status"] != True:
message = ret.get("message", "No message provided")
logger.error(f"Excelファイル作成失敗 : ret.message={message}")
return Response(
{"error": f"Excel generation failed: {message}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
excel_path = ret.get("filepath")
if not excel_path or not os.path.exists(excel_path):
logger.error(f"Output file not found: {excel_path}")
return Response(
{"error": "Generated file not found"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# フォーマット指定excel or pdf
format_type = request.query_params.get('format', 'pdf')
if format_type.lower() == 'pdf':
try:
# 一時ディレクトリを作成
temp_dir = tempfile.mkdtemp()
temp_excel = os.path.join(temp_dir, f'certificate_{zekken_number}.xlsx')
temp_pdf = os.path.join(temp_dir, f'certificate_{zekken_number}.pdf')
# Excelファイルを一時ディレクトリにコピー
shutil.copy2(excel_path, temp_excel)
# 一時ディレクトリのパーミッションを設定
os.chmod(temp_dir, 0o777)
os.chmod(temp_excel, 0o666)
logger.info(f"Converting Excel to PDF in temp directory: {temp_dir}")
# LibreOfficeを使用してExcelをPDFに変換
conversion_command = [
'soffice', # LibreOfficeの代替コマンド
'--headless',
'--convert-to',
'pdf',
'--outdir',
temp_dir,
temp_excel
]
logger.debug(f"Running conversion command: {' '.join(conversion_command)}")
# 環境変数を設定
env = os.environ.copy()
env['HOME'] = temp_dir # LibreOfficeの設定ディレクトリを一時ディレクトリに設定
# 変換プロセスを実行
process = subprocess.run(
conversion_command,
env=env,
capture_output=True,
text=True,
check=True
)
logger.debug(f"Conversion output: {process.stdout}")
# PDFファイルの存在確認
if not os.path.exists(temp_pdf):
logger.error("PDF conversion failed - output file not found")
return Response(
{"error": "PDF conversion failed - output file not found"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# PDFファイルを読み込んでレスポンスを返す
with open(temp_pdf, 'rb') as pdf_file:
pdf_content = pdf_file.read()
response = HttpResponse(pdf_content, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="certificate_{zekken_number}_{event_code}.pdf"'
return response
except subprocess.CalledProcessError as e:
logger.error(f"Error converting to PDF: {str(e)}\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}")
return Response(
{"error": f"PDF conversion failed: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
finally:
# 一時ディレクトリの削除
if temp_dir and os.path.exists(temp_dir):
try:
shutil.rmtree(temp_dir)
logger.debug(f"Temporary directory removed: {temp_dir}")
except Exception as e:
logger.warning(f"Failed to remove temporary directory: {str(e)}")
else: # Excel形式の場合
with open(excel_path, 'rb') as excel_file:
response = HttpResponse(
excel_file.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = f'attachment; filename="certificate_{zekken_number}_{event_code}.xlsx"'
return response
except Exception as e:
logger.error(f"Error in Excel/PDF generation: {str(e)}", exc_info=True)
return Response(
{"error": f"File generation failed: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except Exception as e:
logger.error(f"Error in export_excel: {str(e)}", exc_info=True)
return Response(
{"error": "Failed to export file", "detail": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
finally:
# 確実に一時ディレクトリを削除
if temp_dir and os.path.exists(temp_dir):
try:
shutil.rmtree(temp_dir)
except Exception as e:
logger.warning(f"Failed to remove temporary directory in finally block: {str(e)}")
@api_view(['GET'])
def export_exceli_old2(request,zekken_number, event_code):
try:
# パラメータを文字列型に変換
zekken_number = str(zekken_number)
event_code = str(event_code)
logger.info(f"Exporting Excel for zekken_number: {zekken_number}, event_code: {event_code}")
# 入力値の検証
if not zekken_number or not event_code:
logger.error("Missing required parameters")
return Response(
{"error": "Both zekken_number and event_code are required"},
status=status.HTTP_400_BAD_REQUEST
)
# docbaseディレクトリのパスを絶対パスで設定
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
docbase_path = os.path.join(base_dir, 'docbase')
# ディレクトリ存在確認と作成
os.makedirs(docbase_path, exist_ok=True)
# 設定ファイルのパス
template_path = os.path.join(docbase_path, 'certificate_template.xlsx')
ini_path = os.path.join(docbase_path, 'certificate.ini')
# テンプレートと設定ファイルの存在確認
if not os.path.exists(template_path):
logger.error(f"Template file not found: {template_path}")
return Response(
{"error": "Excel template file missing"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if not os.path.exists(ini_path):
logger.error(f"INI file not found: {ini_path}")
return Response(
{"error": "Configuration file missing"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# Docker環境用のデータベース設定を使用
db_settings = settings.DATABASES['default']
# レスポンスの生成
output.seek(0)
response = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = f'attachment; filename=./docbase/certificate_{zekken_number}.xlsx'
return response
# 初期化
variables = {
"zekken_number":str(zekken_number),
"event_code":str(event_code),
"db":str(db_settings['NAME']), #"rogdb",
"username":str(db_settings['USER']), #"admin",
"password":str(db_settings['PASSWORD']), #"admin123456",
"host":str(db_settings['HOST']), # Docker Composeのサービス名を使用 # "localhost",
"port":str(db_settings['PORT']), #"5432",
"template_path": template_path
}
# データベース接続情報のログ出力(パスワードは除く)
logger.info(f"Attempting database connection to {variables['host']}:{variables['port']} "
f"with user {variables['username']} and database {variables['db']}")
try:
excel = SumasenExcel(document="certificate", variables=variables, docbase=docbase_path)
# ./docbase/certificate.ini の定義をベースに、
# ./docbase/certificate_template.xlsxを読み込み
# ./docbase/certificate_(zekken_number).xlsxを作成する
# シート初期化
logger.info("Generating report with variables: %s",
{k: v for k, v in variables.items() if k != 'password'}) # パスワードを除外
ret = excel.make_report(variables=variables)
if ret["status"]==True:
filepath=ret["filepath"]
logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
else:
message = ret.get("message", "No message provided")
logging.error(f"Excelファイル作成失敗 : ret.message={message}")
output_path = ret.get("filepath")
if not output_path or not os.path.exists(output_path):
logger.error(f"Output file not found: {output_path}")
return Response(
{"error": "Generated file not found"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
excel_path = output_path
# PDFのファイル名を生成
pdf_filename = f'certificate_{zekken_number}_{event_code}.pdf'
pdf_path = os.path.join(docbase_path, pdf_filename)
# フォーマット指定excel or pdf
format_type = request.query_params.get('format', 'pdf') # 'excel'
if format_type.lower() == 'pdf':
try:
# 一時ディレクトリを作成
temp_dir = tempfile.mkdtemp()
temp_excel = os.path.join(temp_dir, f'certificate_{zekken_number}.xlsx')
temp_pdf = os.path.join(temp_dir, f'certificate_{zekken_number}.pdf')
# Excelファイルを一時ディレクトリにコピー
shutil.copy2(excel_path, temp_excel)
# 一時ディレクトリのパーミッションを設定
os.chmod(temp_dir, 0o777)
os.chmod(temp_excel, 0o666)
logger.info(f"Converting Excel to PDF in temp directory: {temp_dir}")
# LibreOfficeを使用してExcelをPDFに変換
conversion_command = [
'soffice',
'--headless',
'--convert-to',
'pdf',
'--outdir',
temp_dir,
temp_excel
]
logger.debug(f"Running conversion command: {' '.join(conversion_command)}")
# 環境変数を設定
env = os.environ.copy()
env['HOME'] = temp_dir # LibreOfficeの設定ディレクトリを一時ディレクトリに設定
# 変換プロセスを実行
process = subprocess.run(
conversion_command,
env=env,
capture_output=True,
text=True,
check=True
)
logger.debug(f"Conversion output: {process.stdout}")
# PDFファイルの存在確認
if not os.path.exists(temp_pdf):
logger.error("PDF conversion failed - output file not found")
return Response(
{"error": "PDF conversion failed - output file not found"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# PDFファイルを読み込んでレスポンスを返す
with open(temp_pdf, 'rb') as pdf_file:
pdf_content = pdf_file.read()
response = HttpResponse(pdf_content, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="certificate_{zekken_number}_{event_code}.pdf"'
return response
except subprocess.CalledProcessError as e:
logger.error(f"Error converting to PDF: {str(e)}\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}")
return Response(
{"error": "PDF conversion failed"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
finally:
# 一時ディレクトリの削除
if temp_dir and os.path.exists(temp_dir):
try:
shutil.rmtree(temp_dir)
logger.debug(f"Temporary directory removed: {temp_dir}")
except Exception as e:
logger.warning(f"Failed to remove temporary directory: {str(e)}")
else: # Excel形式の場合
with open(excel_path, 'rb') as excel_file:
response = HttpResponse(
excel_file.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
excel_filename = f'certificate_{zekken_number}_{event_code}.xlsx'
response['Content-Disposition'] = f'attachment; filename="{excel_filename}"'
return response
except Exception as e:
logger.error(f"Error in Excel/PDF generation: {str(e)}", exc_info=True)
return Response(
{"error": f"File generation failed: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except Exception as e:
logger.error(f"Error in export_excel: {str(e)}", exc_info=True)
return Response(
{"error": "Failed to export file", "detail": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
finally:
# 確実に一時ディレクトリを削除
if temp_dir and os.path.exists(temp_dir):
try:
shutil.rmtree(temp_dir)
except Exception as e:
logger.warning(f"Failed to remove temporary directory in finally block: {str(e)}")
# ----- for Supervisor -----

View File

@ -1132,14 +1132,98 @@ async function saveGoalTime(goalTimeStr, zekkenNumber, eventCode) {
}
}
// 通過証明書出力機能の実装
// 通過証明書出力・印刷機能の実装
async function exportExcel() {
const zekkenNumber = document.querySelector('#zekkenNumber').value;
const eventCode = document.querySelector('#eventCode').value;
try {
const response = await fetch(`${API_BASE_URL}/export_excel/${zekkenNumber}/${eventCode}`);
const response = await fetch(`${API_BASE_URL}/export_excel/${zekkenNumber}/${eventCode}/`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Blobとしてレスポンスを取得
const blob = await response.blob();
// BlobをURLに変換
const url = window.URL.createObjectURL(blob);
// 印刷方法の選択を提供する関数
const printPDF = () => {
// IEとその他のブラウザで異なる処理を行う
if (window.navigator.msSaveOrOpenBlob) {
// IEの場合
window.navigator.msSaveOrOpenBlob(blob, `通過証明書_${zekkenNumber}_${eventCode}.pdf`);
} else {
// その他のブラウザの場合
// iframeを作成して印刷用のコンテナとして使用
const printFrame = document.createElement('iframe');
printFrame.style.display = 'none';
printFrame.src = url;
printFrame.onload = () => {
try {
// iframe内のPDFを印刷
printFrame.contentWindow.print();
} catch (error) {
console.error('印刷プロセス中にエラーが発生しました:', error);
// 印刷に失敗した場合、新しいタブでPDFを開く
window.open(url, '_blank');
} finally {
// 少し遅延してからクリーンアップ
setTimeout(() => {
document.body.removeChild(printFrame);
window.URL.revokeObjectURL(url);
}, 1000);
}
};
document.body.appendChild(printFrame);
}
};
// 確認ダイアログを表示
const userChoice = window.confirm('PDFを印刷しますか\n「キャンセル」を選択すると保存できます。');
if (userChoice) {
// 印刷を実行
printPDF();
} else {
// PDFを保存
const a = document.createElement('a');
a.href = url;
a.download = `通過証明書_${zekkenNumber}_${eventCode}.pdf`;
document.body.appendChild(a);
a.click();
// クリーンアップ
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
} catch (error) {
console.error('エクスポート中にエラーが発生しました:', error);
alert('エクスポート中にエラーが発生しました');
}
}
// エラーハンドリングのためのユーティリティ関数
function handlePrintError(error) {
console.error('印刷中にエラーが発生しました:', error);
alert('印刷中にエラーが発生しました。PDFを新しいタブで開きます。');
}
// 通過証明書出力機能の実装
async function exportExcel_old() {
const zekkenNumber = document.querySelector('#zekkenNumber').value;
const eventCode = document.querySelector('#eventCode').value;
try {
const response = await fetch(`${API_BASE_URL}/export_excel/${zekkenNumber}/${eventCode}/`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);