From 9eb45d7e978dec96a7b96f1fa15848e5dc96cf8f Mon Sep 17 00:00:00 2001 From: hayano Date: Fri, 8 Nov 2024 04:30:58 +0000 Subject: [PATCH] final stage -- still some bugs --- Dockerfile.gdal | 23 +- SumasenLibs/excel_lib/sumaexcel/sumaexcel.py | 178 +++++-- docbase/certificate.ini | 26 + docbase/certificate_template.xlsx | Bin 0 -> 11217 bytes rog/models.py | 9 +- rog/views.py | 501 +++++++++++++++++-- supervisor/html/index.html | 88 +++- 7 files changed, 743 insertions(+), 82 deletions(-) create mode 100644 docbase/certificate.ini create mode 100644 docbase/certificate_template.xlsx 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 0000000000000000000000000000000000000000..5c966953d07f76b7bd7c474385477de36669a541 GIT binary patch literal 11217 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 literal 0 HcmV?d00001 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}`);