diff --git a/Dockerfile.gdal b/Dockerfile.gdal index 0b61846..0fcd129 100644 --- a/Dockerfile.gdal +++ b/Dockerfile.gdal @@ -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 diff --git a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py index 3db4b40..600f74e 100644 --- a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py +++ b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py @@ -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: @@ -288,62 +315,133 @@ class SumasenExcel: # Noneの場合は空文字を返す if field_value is None: return "" - + # 真偽値の場合 if isinstance(field_value, bool): return "OK" if field_value else "-" - + # 日時型の場合 if isinstance(field_value, datetime): jst = pytz.timezone('Asia/Tokyo') # UTC -> JST変換 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 - - # アスペクト比を保持しながらリサイズ - img_aspect = img.width / img.height - cell_aspect = cell_width / cell_height - - if img_aspect > cell_aspect: - width = int(cell_width) - height = int(width / img_aspect) - else: - height = int(cell_height) - width = int(height * img_aspect) - - # 画像をリサイズしてセルに追加 - img = img.resize((width, height)) - - # 画像をセルに追加(実装方法はExcelライブラリに依存) - if hasattr(cell, 'add_image'): # 使用するライブラリによって適切なメソッドを使用 - cell.add_image(img) - return "" # 画像を追加したので、テキスト値は空にする + # 画像を開く + 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_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 "" + 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) + diff --git a/docbase/certificate.ini b/docbase/certificate.ini new file mode 100644 index 0000000..e59f05a --- /dev/null +++ b/docbase/certificate.ini @@ -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 + diff --git a/docbase/certificate_template.xlsx b/docbase/certificate_template.xlsx new file mode 100644 index 0000000..5c96695 Binary files /dev/null and b/docbase/certificate_template.xlsx differ diff --git a/rog/models.py b/rog/models.py index f7be855..1ea4ae0 100644 --- a/rog/models.py +++ b/rog/models.py @@ -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}" diff --git a/rog/views.py b/rog/views.py index ee287cc..9804674 100644 --- a/rog/views.py +++ b/rog/views.py @@ -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 ----- diff --git a/supervisor/html/index.html b/supervisor/html/index.html index e86d25d..ef95905 100755 --- a/supervisor/html/index.html +++ b/supervisor/html/index.html @@ -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}/`); + + 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() { + 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}`); + const response = await fetch(`${API_BASE_URL}/export_excel/${zekkenNumber}/${eventCode}/`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`);