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 WORKDIR /app
LABEL maintainer="nouffer@gmail.com" LABEL maintainer="nouffer@gmail.com"
LABEL description="Development image for the Rogaining JP" 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 \ RUN apt-get update && apt-get install -y \
python3-pip 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 --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 RUN apt-get update

View File

@ -31,9 +31,26 @@ import requests
from io import BytesIO from io import BytesIO
from PIL import Image from PIL import Image
import re 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 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( logging.basicConfig(
level=logging.INFO, # INFOレベル以上のログを表示 level=logging.INFO, # INFOレベル以上のログを表示
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
@ -76,6 +93,8 @@ class SumasenExcel:
self._page_manager = None self._page_manager = None
try: try:
logging.info("step-1")
# document base を設定 # document base を設定
self.docpath = docbase self.docpath = docbase
if not os.path.exists(docbase): if not os.path.exists(docbase):
@ -94,6 +113,8 @@ class SumasenExcel:
self.basic = basic self.basic = basic
logging.info("step-2")
# basicセクションから必要なパラメータを取得 # basicセクションから必要なパラメータを取得
template_file = basic.get("template_file") template_file = basic.get("template_file")
if not template_file: if not template_file:
@ -114,6 +135,7 @@ class SumasenExcel:
if not sections: if not sections:
logging.error("sections not found in basic section") logging.error("sections not found in basic section")
logging.info("step-3")
# セクションをリストに変換 # セクションをリストに変換
self.section_list = [s.strip() for s in sections.split(",")] self.section_list = [s.strip() for s in sections.split(",")]
@ -127,11 +149,15 @@ class SumasenExcel:
# デフォルトで作成されるシートを削除 # デフォルトで作成されるシートを削除
self.workbook.remove(self.workbook.active) self.workbook.remove(self.workbook.active)
logging.info("step-4")
# テンプレートワークブックをロード # テンプレートワークブックをロード
self.template_filepath = f"{self.docpath}/{template_file}" self.template_filepath = f"{self.docpath}/{template_file}"
if not os.path.exists(self.template_filepath): if not os.path.exists(self.template_filepath):
logging.error(f"Template file not found: {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_workbook = openpyxl.load_workbook(self.template_filepath)
self.template_sheet = self.template_workbook.active self.template_sheet = self.template_workbook.active
@ -275,6 +301,7 @@ class SumasenExcel:
logging.error(f"Error in proceed_group: {str(e)}") logging.error(f"Error in proceed_group: {str(e)}")
return {"status": False, "message": f"Exception in proceed_group : Error generating report: {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):
"""セルの値を適切な形式に変換する """セルの値を適切な形式に変換する
@ -300,53 +327,124 @@ class SumasenExcel:
jst_time = field_value.astimezone(jst) jst_time = field_value.astimezone(jst)
return jst_time.strftime('%H:%M:%S') return jst_time.strftime('%H:%M:%S')
# 文字列の場合、URLかどうかをチェック # 文字列の場合の処理
if isinstance(field_value, str): if isinstance(field_value, str):
# URL形式かどうかチェック
try: try:
result = urlparse(field_value) # 画像ファイルパスを取得する関数
# URLで、かつ画像ファイルの拡張子を持つ場合 def get_image_path(value):
if all([result.scheme, result.netloc]) and \ # URLの場合
re.search(r'\.(jpg|jpeg|png|gif|bmp)$', result.path, re.I): if value.startswith('https://'):
try: # URLからファイル名を抽出
# 画像をダウンロード filename = os.path.basename(urlparse(value).path)
response = requests.get(field_value) return os.path.join("/app/media/compressed", filename)
img = Image.open(BytesIO(response.content)) # 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
# セルの大きさを取得(ピクセル単位) # 画像パスを取得
cell_width = cell.column_dimensions.width * 6 # 概算のピクセル変換 image_path = get_image_path(field_value)
cell_height = cell.row_dimensions.height
if image_path and os.path.exists(image_path):
try:
from PIL import Image
import io
# 画像を開く
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')
# ワークシートを取得
worksheet = cell.parent
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 # デフォルト値
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_aspect = img.width / img.height img_width, img_height = img.size
img_aspect = img_width / img_height
cell_aspect = cell_width / cell_height cell_aspect = cell_width / cell_height
if img_aspect > cell_aspect: if img_aspect > cell_aspect:
width = int(cell_width) width = cell_width
height = int(width / img_aspect) height = int(width / img_aspect)
else: else:
height = int(cell_height) height = cell_height
width = int(height * img_aspect) width = int(height * img_aspect)
# 画像をリサイズしてセルに追加 # 画像をリサイズ
img = img.resize((width, height)) 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: except Exception as e:
logging.warning(f"画像の処理に失敗: {str(e)}") logging.error(f"画像の処理に失敗: {str(e)}, image_path={image_path}")
return field_value # エラーの場合はURLをそのまま返す 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: except Exception as e:
logging.warning(f"形式変換の処理に失敗: {str(e)}") logging.error(f"形式変換の処理に失敗: {str(e)}")
return field_value # エラーの場合はURLをそのまま返す import traceback
logging.error(traceback.format_exc())
return field_value
# その他の場合は文字列に変換 # その他の場合は文字列に変換
return str(field_value) return str(field_value)
def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]): def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]):
"""1レコードのデータを取得してシートの値を置き換える """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: class Meta:
db_table = 'gps_checkins' db_table = 'gps_checkins'
constraints = [
models.UniqueConstraint(
fields=['zekken_number', 'event_code', 'path_order'],
name='unique_gps_checkin'
)
]
indexes = [ 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'), models.Index(fields=['create_at'], name='idx_create_at'),
] ]
def __str__(self): 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}" 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 import traceback
from django.contrib.auth.hashers import make_password 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.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str from django.utils.encoding import force_bytes, force_str
@ -90,7 +94,7 @@ from io import BytesIO
from django.urls import get_resolver from django.urls import get_resolver
import os import os
import json import json
from django.http import HttpResponse
from sumaexcel import SumasenExcel from sumaexcel import SumasenExcel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -2538,6 +2542,80 @@ def get_checkins(request, *args, **kwargs):
@api_view(['POST']) @api_view(['POST'])
def update_checkins(request): 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: try:
with transaction.atomic(): with transaction.atomic():
update_base = request.data update_base = request.data
@ -2605,25 +2683,269 @@ def update_checkins(request):
) )
@api_view(['GET']) @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)
logger.info(f"Exporting Excel/PDF 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']
# 初期化 # 初期化
variables = { variables = {
"zekken_number":sekken_number, "zekken_number": str(zekken_number),
"event_code":request["FC岐阜"], "event_code": str(event_code),
"db":"rogdb", "db": str(db_settings['NAME']),
"username":"admin", "username": str(db_settings['USER']),
"password":"admin123456", "password": str(db_settings['PASSWORD']),
"host":"localhost", "host": str(db_settings['HOST']),
"port":"5432" "port": str(db_settings['PORT']),
"template_path": template_path
} }
excel = SumasenExcel(document="test", variables=variables, docbase="./docbase")
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']
# 初期化
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.ini の定義をベースに、
# ./docbase/certificate_template.xlsxを読み込み # ./docbase/certificate_template.xlsxを読み込み
# ./docbase/certificate_(zekken_number).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) ret = excel.make_report(variables=variables)
if ret["status"]==True: if ret["status"]==True:
filepath=ret["filepath"] filepath=ret["filepath"]
@ -2632,17 +2954,132 @@ def export_excel(request, zekken_number):
message = ret.get("message", "No message provided") message = ret.get("message", "No message provided")
logging.error(f"Excelファイル作成失敗 : ret.message={message}") 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に変換
output.seek(0) 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( response = HttpResponse(
output.read(), excel_file.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
) )
response['Content-Disposition'] = f'attachment; filename=./docbase/certificate_{zekken_number}.xlsx' excel_filename = f'certificate_{zekken_number}_{event_code}.xlsx'
response['Content-Disposition'] = f'attachment; filename="{excel_filename}"'
return response 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 ----- # ----- for Supervisor -----
@api_view(['GET']) @api_view(['GET'])

View File

@ -1132,14 +1132,98 @@ async function saveGoalTime(goalTimeStr, zekkenNumber, eventCode) {
} }
} }
// 通過証明書出力・印刷機能の実装
// 通過証明書出力機能の実装
async function exportExcel() { async function exportExcel() {
const zekkenNumber = document.querySelector('#zekkenNumber').value; const zekkenNumber = document.querySelector('#zekkenNumber').value;
const eventCode = document.querySelector('#eventCode').value; const eventCode = document.querySelector('#eventCode').value;
try { 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) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);