Compare commits

15 Commits

Author SHA1 Message Date
106ab0e94e implement sumaexcel step-1 2024-11-07 03:24:15 +09:00
7f4d37d40c generate Excel stage-3: debug row height and fonts 2024-11-06 18:45:10 +09:00
4a2a5de476 Generate Excel stage-3 2024-11-06 09:30:42 +00:00
15815d5f06 Generate Excel stage-2 2024-11-06 18:29:16 +09:00
768dd6e261 Generate Excel stage-2 2024-11-06 09:17:30 +00:00
139c0987bc Generate Excel stage 2 2024-11-06 17:56:24 +09:00
ceb783d6bd Generate Excel file step 1 2024-11-06 07:35:17 +00:00
a714557eef Revert "update db setting on sample.py"
This reverts commit 586f341897.
2024-11-06 16:29:34 +09:00
586f341897 update db setting on sample.py 2024-11-05 11:11:03 +09:00
0c2dfec7dd basic debugging step 1 2024-11-05 07:46:21 +09:00
d6464c1369 Sumasen Lib step 2 2024-11-03 19:53:23 +09:00
338643b0d7 add sumasen_lib 2024-11-03 10:49:42 +00:00
e992e834da fix goaltime issue on server side 2024-11-03 05:16:05 +00:00
c6969d7afa Finish supervisor , 残りはExcelとセキュリティ. 2024-11-02 23:53:34 +00:00
82d0e55945 Supervisor: 残=新規・保存・印刷・時計表示 2024-10-30 08:12:31 +00:00
36 changed files with 12172 additions and 198 deletions

BIN
.env.swp

Binary file not shown.

View File

@ -39,6 +39,7 @@ RUN apt-get update && apt-get install -y \
python3-pip python3-pip
RUN pip install --upgrade pip RUN pip install --upgrade pip
RUN pip install -e ./SumasenLibs/excel_lib
RUN apt-get update RUN apt-get update

File diff suppressed because it is too large Load Diff

1087
LineBot/userpostgres.rb Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,19 @@
# SumasenExcel Library
Excel操作のためのシンプルなPythonライブラリです。
## インストール方法
```bash
pip install -e .
## 使用方法
from sumaexcel import SumasenExcel
excel = SumasenExcel("path/to/file.xlsx")
data = excel.read_excel()
## ライセンス
MIT License

View File

@ -0,0 +1,20 @@
version: '3.8'
services:
python:
build:
context: ..
dockerfile: docker/python/Dockerfile
volumes:
- ..:/app
environment:
- PYTHONPATH=/app
- POSTGRES_DB=rogdb
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=admin123456
- POSTGRES_HOST=localhost
- POSTGRES_PORT=5432
network_mode: "host"
tty: true
container_name: python_container # コンテナ名を明示的に指定

View File

@ -0,0 +1,26 @@
FROM python:3.9-slim
WORKDIR /app
# GPGキーの更新とパッケージのインストール
RUN apt-get update --allow-insecure-repositories && \
apt-get install -y --allow-unauthenticated python3-dev libpq-dev postgresql-client && \
rm -rf /var/lib/apt/lists/*
# Pythonパッケージのインストール
COPY requirements.txt .
COPY setup.py .
COPY README.md .
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
# 開発用パッケージのインストール
RUN pip install --no-cache-dir --upgrade pip \
pytest \
pytest-cov \
flake8
# パッケージのインストール
RUN pip install -e .

View File

@ -0,0 +1,5 @@
openpyxl>=3.0.0
pandas>=1.0.0
pillow>=8.0.0
configparser>=5.0.0
psycopg2-binary==2.9.9

View File

@ -0,0 +1,25 @@
# setup.py
from setuptools import setup, find_packages
setup(
name="sumaexcel",
version="0.1.0",
packages=find_packages(),
install_requires=[
"openpyxl>=3.0.0",
"pandas>=1.0.0"
],
author="Akira Miyata",
author_email="akira.miyata@sumasen.net",
description="Excel handling library",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://github.com/akiramiyata/sumaexcel",
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires=">=3.6",
)

Binary file not shown.

View File

@ -0,0 +1,4 @@
from .sumaexcel import SumasenExcel
__version__ = "0.1.0"
__all__ = ["SumasenExcel"]

View File

@ -0,0 +1,102 @@
# sumaexcel/conditional.py
from typing import Dict, Any, List, Union
from openpyxl.formatting.rule import Rule, ColorScaleRule, DataBarRule, IconSetRule
from openpyxl.styles import PatternFill, Font, Border, Side
from openpyxl.worksheet.worksheet import Worksheet
class ConditionalFormatManager:
"""Handle conditional formatting in Excel"""
def __init__(self, worksheet: Worksheet):
self.worksheet = worksheet
def add_color_scale(
self,
cell_range: str,
min_color: str = "00FF0000", # Red
mid_color: str = "00FFFF00", # Yellow
max_color: str = "0000FF00" # Green
) -> None:
"""Add color scale conditional formatting"""
rule = ColorScaleRule(
start_type='min',
start_color=min_color,
mid_type='percentile',
mid_value=50,
mid_color=mid_color,
end_type='max',
end_color=max_color
)
self.worksheet.conditional_formatting.add(cell_range, rule)
def add_data_bar(
self,
cell_range: str,
color: str = "000000FF", # Blue
show_value: bool = True
) -> None:
"""Add data bar conditional formatting"""
rule = DataBarRule(
start_type='min',
end_type='max',
color=color,
showValue=show_value
)
self.worksheet.conditional_formatting.add(cell_range, rule)
def add_icon_set(
self,
cell_range: str,
icon_style: str = '3Arrows', # '3Arrows', '3TrafficLights', '3Signs'
reverse_icons: bool = False
) -> None:
"""Add icon set conditional formatting"""
rule = IconSetRule(
icon_style=icon_style,
type='percent',
values=[0, 33, 67],
reverse_icons=reverse_icons
)
self.worksheet.conditional_formatting.add(cell_range, rule)
def add_custom_rule(
self,
cell_range: str,
rule_type: str,
formula: str,
fill_color: str = None,
font_color: str = None,
bold: bool = None,
border_style: str = None,
border_color: str = None
) -> None:
"""Add custom conditional formatting rule"""
dxf = {}
if fill_color:
dxf['fill'] = PatternFill(start_color=fill_color, end_color=fill_color)
if font_color or bold is not None:
dxf['font'] = Font(color=font_color, bold=bold)
if border_style and border_color:
side = Side(style=border_style, color=border_color)
dxf['border'] = Border(left=side, right=side, top=side, bottom=side)
rule = Rule(type=rule_type, formula=[formula], dxf=dxf)
self.worksheet.conditional_formatting.add(cell_range, rule)
def copy_conditional_format(
self,
source_range: str,
target_range: str
) -> None:
"""Copy conditional formatting from one range to another"""
source_rules = self.worksheet.conditional_formatting.get(source_range)
if source_rules:
for rule in source_rules:
self.worksheet.conditional_formatting.add(target_range, rule)
def clear_conditional_format(
self,
cell_range: str
) -> None:
"""Clear conditional formatting from specified range"""
self.worksheet.conditional_formatting.delete(cell_range)

View File

@ -0,0 +1,166 @@
# config_handler.py
#
import configparser
import os
from typing import Any, Dict, Optional
import configparser
import os
import re
from typing import Any, Dict, Optional
class ConfigHandler:
"""変数置換機能付きの設定ファイル管理クラス"""
def __init__(self, ini_file_path: str, variables: Dict[str, str] = None):
"""
Args:
ini_file_path (str): INIファイルのパス
variables (Dict[str, str], optional): 置換用の変数辞書
"""
self.ini_file_path = ini_file_path
self.variables = variables or {}
self.config = configparser.ConfigParser()
self.load_config()
def _substitute_variables(self, text: str) -> str:
"""
テキスト内の変数を置換する
Args:
text (str): 置換対象のテキスト
Returns:
str: 置換後のテキスト
"""
# ${var}形式の変数を置換
pattern1 = r'\${([^}]+)}'
# [var]形式の変数を置換
pattern2 = r'\[([^\]]+)\]'
def replace_var(match):
var_name = match.group(1)
return self.variables.get(var_name, match.group(0))
# 両方のパターンで置換を実行
text = re.sub(pattern1, replace_var, text)
text = re.sub(pattern2, replace_var, text)
return text
def load_config(self) -> None:
"""設定ファイルを読み込み、変数を置換する"""
if not os.path.exists(self.ini_file_path):
raise FileNotFoundError(f"設定ファイルが見つかりません: {self.ini_file_path}")
# まず生のテキストとして読み込む
with open(self.ini_file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 変数を置換
substituted_content = self._substitute_variables(content)
# 置換済みの内容を StringIO 経由で configparser に読み込ませる
from io import StringIO
self.config.read_file(StringIO(substituted_content))
def get_value(self, section: str, key: str, default: Any = None) -> Optional[str]:
"""
指定されたセクションのキーの値を取得する
Args:
section (str): セクション名
key (str): キー名
default (Any): デフォルト値(オプション)
Returns:
Optional[str]: 設定値。存在しない場合はデフォルト値
"""
try:
return self.config[section][key]
except KeyError:
return default
def get_section(self, section: str) -> Dict[str, str]:
"""
指定されたセクションの全ての設定を取得する
Args:
section (str): セクション名
Returns:
Dict[str, str]: セクションの設定をディクショナリで返す
"""
try:
return dict(self.config[section])
except KeyError:
return {}
def get_all_sections(self) -> Dict[str, Dict[str, str]]:
"""
全てのセクションの設定を取得する
Returns:
Dict[str, Dict[str, str]]: 全セクションの設定をネストされたディクショナリで返す
"""
return {section: dict(self.config[section]) for section in self.config.sections()}
# 使用例
if __name__ == "__main__":
# サンプルのINIファイル作成
sample_ini = """
[Database]
host = localhost
port = 5432
database = mydb
user = admin
password = secret
[Application]
debug = true
log_level = INFO
max_connections = 100
[Paths]
data_dir = /var/data
log_file = /var/log/app.log
"""
# サンプルINIファイルを作成
with open('config.ini', 'w', encoding='utf-8') as f:
f.write(sample_ini)
# 設定を読み込んで使用
config = ConfigHandler('config.ini')
# 特定の値を取得
db_host = config.get_value('Database', 'host')
db_port = config.get_value('Database', 'port')
print(f"Database connection: {db_host}:{db_port}")
# セクション全体を取得
db_config = config.get_section('Database')
print("Database configuration:", db_config)
# 全ての設定を取得
all_config = config.get_all_sections()
print("All configurations:", all_config)
# サンプル:
# # 設定ファイルから値を取得
# config = ConfigHandler('config.ini')
#
# # データベース設定を取得
# db_host = config.get_value('Database', 'host')
# db_port = config.get_value('Database', 'port')
# db_name = config.get_value('Database', 'database')
#
# # アプリケーション設定を取得
# debug_mode = config.get_value('Application', 'debug')
# log_level = config.get_value('Application', 'log_level')
#

View File

@ -0,0 +1,77 @@
# sumaexcel/image.py
from typing import Optional, Tuple, Union
from pathlib import Path
import os
from PIL import Image
from openpyxl.drawing.image import Image as XLImage
from openpyxl.worksheet.worksheet import Worksheet
class ImageManager:
"""Handle image operations in Excel"""
def __init__(self, worksheet: Worksheet):
self.worksheet = worksheet
self.temp_dir = Path("/tmp/sumaexcel_images")
self.temp_dir.mkdir(parents=True, exist_ok=True)
def add_image(
self,
image_path: Union[str, Path],
cell_coordinates: Tuple[int, int],
size: Optional[Tuple[int, int]] = None,
keep_aspect_ratio: bool = True,
anchor_type: str = 'absolute'
) -> None:
"""Add image to worksheet at specified position"""
# Convert path to Path object
image_path = Path(image_path)
# Open and process image
with Image.open(image_path) as img:
# Get original size
orig_width, orig_height = img.size
# Calculate new size if specified
if size:
target_width, target_height = size
if keep_aspect_ratio:
ratio = min(target_width/orig_width, target_height/orig_height)
target_width = int(orig_width * ratio)
target_height = int(orig_height * ratio)
# Resize image
img = img.resize((target_width, target_height), Image.LANCZOS)
# Save temporary resized image
temp_path = self.temp_dir / f"temp_{image_path.name}"
img.save(temp_path)
image_path = temp_path
# Create Excel image object
excel_image = XLImage(str(image_path))
# Add to worksheet
self.worksheet.add_image(excel_image, anchor=f'{cell_coordinates[0]}{cell_coordinates[1]}')
def add_image_absolute(
self,
image_path: Union[str, Path],
position: Tuple[int, int],
size: Optional[Tuple[int, int]] = None
) -> None:
"""Add image with absolute positioning"""
excel_image = XLImage(str(image_path))
if size:
excel_image.width, excel_image.height = size
excel_image.anchor = 'absolute'
excel_image.top, excel_image.left = position
self.worksheet.add_image(excel_image)
def cleanup(self) -> None:
"""Clean up temporary files"""
for file in self.temp_dir.glob("temp_*"):
file.unlink()
def __del__(self):
"""Cleanup on object destruction"""
self.cleanup()

View File

@ -0,0 +1,96 @@
# sumaexcel/merge.py
from typing import List, Tuple, Dict
from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.worksheet.merge import MergedCellRange
class MergeManager:
"""Handle merge cell operations"""
def __init__(self, worksheet: Worksheet):
self.worksheet = worksheet
self._merged_ranges: List[MergedCellRange] = []
self._load_merged_ranges()
def _load_merged_ranges(self) -> None:
"""Load existing merged ranges from worksheet"""
self._merged_ranges = list(self.worksheet.merged_cells.ranges)
def merge_cells(
self,
start_row: int,
start_col: int,
end_row: int,
end_col: int
) -> None:
"""Merge cells in specified range"""
self.worksheet.merge_cells(
start_row=start_row,
start_column=start_col,
end_row=end_row,
end_column=end_col
)
self._load_merged_ranges()
def unmerge_cells(
self,
start_row: int,
start_col: int,
end_row: int,
end_col: int
) -> None:
"""Unmerge cells in specified range"""
self.worksheet.unmerge_cells(
start_row=start_row,
start_column=start_col,
end_row=end_row,
end_column=end_col
)
self._load_merged_ranges()
def copy_merged_cells(
self,
source_range: Tuple[int, int, int, int],
target_start_row: int,
target_start_col: int
) -> None:
"""Copy merged cells from source range to target position"""
src_row1, src_col1, src_row2, src_col2 = source_range
row_offset = target_start_row - src_row1
col_offset = target_start_col - src_col1
for merged_range in self._merged_ranges:
if (src_row1 <= merged_range.min_row <= src_row2 and
src_col1 <= merged_range.min_col <= src_col2):
new_row1 = merged_range.min_row + row_offset
new_col1 = merged_range.min_col + col_offset
new_row2 = merged_range.max_row + row_offset
new_col2 = merged_range.max_col + col_offset
self.merge_cells(new_row1, new_col1, new_row2, new_col2)
def shift_merged_cells(
self,
start_row: int,
rows: int = 0,
cols: int = 0
) -> None:
"""Shift merged cells by specified number of rows and columns"""
new_ranges = []
for merged_range in self._merged_ranges:
if merged_range.min_row >= start_row:
new_row1 = merged_range.min_row + rows
new_col1 = merged_range.min_col + cols
new_row2 = merged_range.max_row + rows
new_col2 = merged_range.max_col + cols
self.worksheet.unmerge_cells(
start_row=merged_range.min_row,
start_column=merged_range.min_col,
end_row=merged_range.max_row,
end_column=merged_range.max_col
)
new_ranges.append((new_row1, new_col1, new_row2, new_col2))
for new_range in new_ranges:
self.merge_cells(*new_range)

View File

@ -0,0 +1,148 @@
# sumaexcel/page.py
from typing import Optional, Dict, Any, Union
from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.worksheet.page import PageMargins, PrintPageSetup
# sumaexcel/page.py (continued)
class PageManager:
"""Handle page setup and header/footer settings"""
def __init__(self, worksheet: Worksheet):
self.worksheet = worksheet
def set_page_setup(
self,
orientation: str = 'portrait',
paper_size: int = 9, # A4
fit_to_height: Optional[int] = None,
fit_to_width: Optional[int] = None,
scale: Optional[int] = None
) -> None:
"""Configure page setup
Args:
orientation: 'portrait' or 'landscape'
paper_size: paper size (e.g., 9 for A4)
fit_to_height: number of pages tall
fit_to_width: number of pages wide
scale: zoom scale (1-400)
"""
setup = PrintPageSetup(
orientation=orientation,
paperSize=paper_size,
scale=scale,
fitToHeight=fit_to_height,
fitToWidth=fit_to_width
)
self.worksheet.page_setup = setup
def set_margins(
self,
left: float = 0.7,
right: float = 0.7,
top: float = 0.75,
bottom: float = 0.75,
header: float = 0.3,
footer: float = 0.3
) -> None:
"""Set page margins in inches"""
margins = PageMargins(
left=left,
right=right,
top=top,
bottom=bottom,
header=header,
footer=footer
)
self.worksheet.page_margins = margins
def set_header_footer(
self,
odd_header: Optional[str] = None,
odd_footer: Optional[str] = None,
even_header: Optional[str] = None,
even_footer: Optional[str] = None,
first_header: Optional[str] = None,
first_footer: Optional[str] = None,
different_first: bool = False,
different_odd_even: bool = False
) -> None:
"""Set headers and footers
Format codes:
- &P: Page number
- &N: Total pages
- &D: Date
- &T: Time
- &[Tab]: Sheet name
- &[Path]: File path
- &[File]: File name
- &[Tab]: Worksheet name
"""
self.worksheet.oddHeader.left = odd_header or ""
self.worksheet.oddFooter.left = odd_footer or ""
if different_odd_even:
self.worksheet.evenHeader.left = even_header or ""
self.worksheet.evenFooter.left = even_footer or ""
if different_first:
self.worksheet.firstHeader.left = first_header or ""
self.worksheet.firstFooter.left = first_footer or ""
self.worksheet.differentFirst = different_first
self.worksheet.differentOddEven = different_odd_even
def set_print_area(self, range_string: str) -> None:
"""Set print area
Args:
range_string: Cell range in A1 notation (e.g., 'A1:H42')
"""
self.worksheet.print_area = range_string
def set_print_title_rows(self, rows: str) -> None:
"""Set rows to repeat at top of each page
Args:
rows: Row range (e.g., '1:3')
"""
self.worksheet.print_title_rows = rows
def set_print_title_columns(self, cols: str) -> None:
"""Set columns to repeat at left of each page
Args:
cols: Column range (e.g., 'A:B')
"""
self.worksheet.print_title_cols = cols
def set_print_options(
self,
grid_lines: bool = False,
horizontal_centered: bool = False,
vertical_centered: bool = False,
headers: bool = False
) -> None:
"""Set print options"""
self.worksheet.print_gridlines = grid_lines
self.worksheet.print_options.horizontalCentered = horizontal_centered
self.worksheet.print_options.verticalCentered = vertical_centered
self.worksheet.print_options.headers = headers
class PaperSizes:
"""Standard paper size constants"""
LETTER = 1
LETTER_SMALL = 2
TABLOID = 3
LEDGER = 4
LEGAL = 5
STATEMENT = 6
EXECUTIVE = 7
A3 = 8
A4 = 9
A4_SMALL = 10
A5 = 11
B4 = 12
B5 = 13

View File

@ -0,0 +1,115 @@
# sumaexcel/styles.py
from typing import Dict, Any, Optional, Union
from openpyxl.styles import (
Font, PatternFill, Alignment, Border, Side,
NamedStyle, Protection, Color
)
from openpyxl.styles.differential import DifferentialStyle
from openpyxl.formatting.rule import Rule
from openpyxl.worksheet.worksheet import Worksheet
class StyleManager:
"""Excel style management class"""
@staticmethod
def create_font(
name: str = "Arial",
size: int = 11,
bold: bool = False,
italic: bool = False,
color: str = "000000",
underline: str = None,
strike: bool = False
) -> Font:
"""Create a Font object with specified parameters"""
return Font(
name=name,
size=size,
bold=bold,
italic=italic,
color=color,
underline=underline,
strike=strike
)
@staticmethod
def create_fill(
fill_type: str = "solid",
start_color: str = "FFFFFF",
end_color: str = None
) -> PatternFill:
"""Create a PatternFill object"""
return PatternFill(
fill_type=fill_type,
start_color=start_color,
end_color=end_color or start_color
)
@staticmethod
def create_border(
style: str = "thin",
color: str = "000000"
) -> Border:
"""Create a Border object"""
side = Side(style=style, color=color)
return Border(
left=side,
right=side,
top=side,
bottom=side
)
@staticmethod
def create_alignment(
horizontal: str = "general",
vertical: str = "bottom",
wrap_text: bool = False,
shrink_to_fit: bool = False,
indent: int = 0
) -> Alignment:
"""Create an Alignment object"""
return Alignment(
horizontal=horizontal,
vertical=vertical,
wrap_text=wrap_text,
shrink_to_fit=shrink_to_fit,
indent=indent
)
@staticmethod
def copy_style(source_cell: Any, target_cell: Any) -> None:
"""Copy all style properties from source cell to target cell"""
target_cell.font = Font(
name=source_cell.font.name,
size=source_cell.font.size,
bold=source_cell.font.bold,
italic=source_cell.font.italic,
color=source_cell.font.color,
underline=source_cell.font.underline,
strike=source_cell.font.strike
)
if source_cell.fill.patternType != None:
target_cell.fill = PatternFill(
fill_type=source_cell.fill.patternType,
start_color=source_cell.fill.start_color.rgb,
end_color=source_cell.fill.end_color.rgb
)
target_cell.border = Border(
left=source_cell.border.left,
right=source_cell.border.right,
top=source_cell.border.top,
bottom=source_cell.border.bottom
)
target_cell.alignment = Alignment(
horizontal=source_cell.alignment.horizontal,
vertical=source_cell.alignment.vertical,
wrap_text=source_cell.alignment.wrap_text,
shrink_to_fit=source_cell.alignment.shrink_to_fit,
indent=source_cell.alignment.indent
)
if source_cell.number_format:
target_cell.number_format = source_cell.number_format

View File

@ -0,0 +1,628 @@
# sumaexcel/excel.py
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
import pandas as pd
from typing import Optional, Dict, List, Union, Any
import os
import shutil
from datetime import datetime
from typing import Optional, Dict, Any, Union, Tuple
from pathlib import Path
# psycopg2のインポートを追加
import psycopg2
import psycopg2.extras
from copy import copy
from openpyxl.utils import range_boundaries
from .config_handler import ConfigHandler # ini file のロード
#from .styles import StyleManager
#from .merge import MergeManager
#from .image import ImageManager
#from .conditional import ConditionalFormatManager
from .page import PageManager, PaperSizes
import logging
logging.basicConfig(
level=logging.INFO, # INFOレベル以上のログを表示
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
class SumasenExcel:
"""Enhanced Excel handling class with extended functionality"""
def __init__(self, document: str, variables: Dict[str, Any],
docbase: str = "./docbase") -> Dict[str, str]:
"""Initialize Excel document with basic settings
Args:
document: Document name
variables: Variables for document
docbase: Base directory for documents (default: "./docbase")
Returns:
Dict with status (true/false) and optional error message
"""
self.debug = True
self.workbook = None
self.template_filepath = None
self.output_filepath = None
self.current_sheet = None
self.dbname = None
self.user = None
self.password = None
self.host = None
self.port = None
self._style_manager = None
self._merge_manager = None
self._image_manager = None
self._conditional_manager = None
self._page_manager = None
try:
# document base を設定
self.docpath = docbase
if not os.path.exists(docbase):
logging.error(f"Document base directory not found: {docbase}")
# ini fileをロード
self.ini_file_path = f"{docbase}/{document}.ini"
self.conf = ConfigHandler(self.ini_file_path, variables)
if not os.path.exists(self.ini_file_path):
logging.error(f"INI file not found: {self.ini_file_path}")
# basic section をロード
basic = self.conf.get_section("basic")
if not basic:
logging.error(f"Basic section not found in INI file: {self.ini_file_path}")
self.basic = basic
# basicセクションから必要なパラメータを取得
template_file = basic.get("template_file")
if not template_file:
logging.error("template_file not found in basic section")
doc_file = basic.get("doc_file")
if not doc_file:
logging.error("doc_file not found in basic section")
self.maxcol = basic.get("maxcol")
if not self.maxcol:
self.maxcol = 100 # デフォルト値を設定
logging.warning("maxcol not found in basic section. Defaulting to 100")
else:
self.maxcol = int(self.maxcol)
sections = basic.get("sections")
if not sections:
logging.error("sections not found in basic section")
# セクションをリストに変換
self.section_list = [s.strip() for s in sections.split(",")]
if not self.section_list:
logging.error("sections not found in basic section")
# 出力ファイルパスを設定
self.output_filepath = f"{self.docpath}/{doc_file}"
# 新規のExcelワークブックを作成
self.workbook = openpyxl.Workbook()
# デフォルトで作成されるシートを削除
self.workbook.remove(self.workbook.active)
# テンプレートワークブックをロード
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}")
self.template_workbook = openpyxl.load_workbook(self.template_filepath)
self.template_sheet = self.template_workbook.active
except Exception as e:
logging.error(f"Error initializing Excel document: {str(e)}")
def make_report(self,variables: Dict[str, Any]):
"""レポートを生成する"""
logging.info("make_report step-1")
try:
ret_status = True
# セクションごとに処理を実行(ワークシート単位)
for section in self.section_list:
ret = self.proceed_section(section,variables)
if ret["status"] == False:
message = ret.get("message", "No message provided")
return {"status": False, "message": f"Fail generating section: {section}...{message}"}
# 生成したワークブックを保存
self.workbook.save(self.output_filepath)
return {"status": True, "message": f"Report generated successfully : {self.output_filepath}", "filepath":self.output_filepath}
except Exception as e:
return {"status": False, "message": f"Exception in make_report: Error generating report: {str(e)}"}
def proceed_section(self, section: str, variables: Dict[str, Any]):
print(f"make_report.proceed_section step-1:section={section}")
try:
# セクションの設定を取得
section_config = self.conf.get_section(section)
# セクションが存在しない場合はスキップ
if not section_config:
return {"status": False, "message": f"Error no section found: {section}"}
# テンプレートシートをコピー
template_sheet_name = section_config.get("template_sheet")
if not template_sheet_name or template_sheet_name not in self.template_workbook.sheetnames:
return {"status": False, "message": f"Error no template sheet found: {template_sheet_name}"}
# シートの名前を設定
new_sheet = self.workbook.create_sheet(title=section_config.get("sheet_name", section))
self.worksheet = new_sheet
self.dbname=variables.get('db')
self.user=variables.get('username')
self.password=variables.get('password')
self.host=variables.get('host','postgres')
self.port=variables.get('port','5432')
if not self.dbname or not self.user or not self.password or not self.host or not self.port:
return {"status": False, "message": f"Error no database connection information"}
print(f"db={self.dbname},user={self.user},pass={self.password},host={self.host},port={self.port}")
# PostgreSQLに接続
self.conn = psycopg2.connect(
dbname=self.dbname,
user=self.user,
password=self.password,
host=self.host,
port=self.port
)
#self._style_manager = StyleManager()
#self._merge_manager = MergeManager(self.current_sheet)
#self._image_manager = ImageManager(self.current_sheet)
#self._conditional_manager = ConditionalFormatManager(self.current_sheet)
#self._page_manager = PageManager(self.current_sheet)
# シートの幅を設定
fit_to_width = section_config.get("fit_to_width")
if fit_to_width:
new_sheet.sheet_view.zoomScaleNormal = float(fit_to_width)
# シートの向きを設定
orientation = section_config.get("orientation")
new_sheet.sheet_view.orientation = orientation if orientation else "portrait"
self.current_worksheet = new_sheet
# グループ定義を取得
groups = section_config.get("groups")
if not groups:
return {"status": False, "message": f"Error no group definitions found: {section}"}
# グループをリストに変換
group_list = [g.strip() for g in groups.split(",")]
if not group_list:
return {"status": False, "message": f"Error invalid group definitions found: {section}"}
# 各グループの設定を取得
for group in group_list:
section_group = f"{section}.{group}"
ret = self.proceed_group(section_group,variables)
if ret["status"] == False:
return ret
return {"status": True, "message": f"Success generating section: {section}"}
except Exception as e:
return {"status": False, "message": f"Exception in proceed_section : Error generating report: {str(e)}"}
def proceed_group(self,group:str,variables: Dict[str, Any]):
logging.info(f"make_report.proceed_group step-1:section={group}")
try:
group_config = self.conf.get_section(group)
if not group_config:
return {"status": False, "message": f"Error no group section found: {group}"}
# グループの処理パラメータを取得
group_range = group_config.get("group_range")
table = group_config.get("table_name")
where = group_config.get("where")
if not where or not table or not group_range:
return {"status": False, "message": f"Error invalid group parameters: {group_config}"}
sort = group_config.get("sort")
if not sort:
ret = self.proceed_one_record(table,where,group_range,variables)
if ret.get("status") == True:
return {"status": True, "message": f"Success generating group: {group}"}
else:
ret = self.proceed_all_records(table,where,sort,group_range,variables)
if ret.get("status") == True:
return {"status": True, "message": f"Success generating group: {group}"}
except Exception as e:
logging.error(f"Error in proceed_group: {str(e)}")
return {"status": False, "message": f"Exception in proceed_group : Error generating report: {str(e)}"}
def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]):
"""1レコードのデータを取得してシートの値を置き換える
Args:
table: テーブル名
where: WHERE句
group_range: 処理対象範囲
variables: DB接続情報を含む変数辞書
"""
try:
print(f"make_report.proceed_one_record step-1:table={table},where={where},group_range={group_range}")
# まずself.template_sheetの指定範囲のセルをself.current_sheetにコピーする。
self.copy_template_to_current(group_range,group_range)
print(f"step-1")
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
# SQLクエリを実行
query = f"SELECT * FROM {table} WHERE {where} LIMIT 1"
cursor.execute(query)
record = cursor.fetchone()
print(f"query={query}")
print(f"record={record}")
if record:
# group_rangeの範囲内のセルを走査
for row in self.current_worksheet:
for cell in row:
if cell.value and isinstance(cell.value, str):
# [field_name]形式の文字列を検索
import re
matches = re.findall(r'\[(.*?)\]', cell.value)
# マッチした場合、フィールド値で置換
if matches:
new_value = cell.value
for field_name in matches:
if field_name in record:
new_value = new_value.replace(
f'[{field_name}]',
str(record[field_name])
)
cell.value = new_value
cursor.close()
return {"status": True, "message": f"Success generating group: "}
except Exception as e:
logging.error(f"Error in proceed_one_record: {str(e)}")
return {"status": False, "message": f"Exception in proceed_one_record:Error generating report: {str(e)}"}
def get_column_letter(self, cell_reference):
"""
セル参照(例:'A12')から列文字を取得
"""
if isinstance(cell_reference, str):
# アルファベット部分を抽出
column = ''.join(c for c in cell_reference if c.isalpha())
if column:
return column
return 'A' # デフォルト値
def get_row_number(self,cell_reference):
"""
セル参照(例:'A12')から行番号を取得
"""
if isinstance(cell_reference, str):
# 数字部分のみを抽出
digits = ''.join(c for c in cell_reference if c.isdigit())
if digits:
return int(digits)
return int(cell_reference)
def proceed_all_records(self, table: str, where: str, sort: str, group_range: str, variables: Dict[str, Any]):
"""複数レコードを取得してシートの値を置き換える
Args:
table: テーブル名
where: WHERE句
sort: ORDER BY句
group_range: 処理対象範囲
variables: DB接続情報を含む変数辞書
"""
print(f"make_report.proceed_all_record step-1:table={table},where={where},group_range={group_range}")
try:
# グループ範囲の行数を取得(セル参照対応)
if not group_range or ':' not in group_range:
raise ValueError(f"Invalid group_range format: {group_range}")
# グループ範囲の行数を取得
range_parts = group_range.split(':')
logging.info(f"Processing range_parts: {range_parts}") # デバッグ用ログ
start_row = self.get_row_number(range_parts[0].strip())
start_col = self.get_column_letter(range_parts[0].strip())
end_row = self.get_row_number(range_parts[1].strip())
end_col = self.get_column_letter(range_parts[1].strip())
if start_row > end_row:
raise ValueError(f"Invalid row range: start_row ({start_row}) > end_row ({end_row})")
template_rows = end_row - start_row + 1
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
# SQLクエリを実行
query = f"SELECT * FROM {table} WHERE {where} ORDER BY {sort}"
cursor.execute(query)
records = cursor.fetchall()
print(f"query={query}, records={len(records)}")
current_row = start_row
for record in records:
# テンプレート範囲をコピー
self.copy_template_to_current(f"{start_col}{start_row}:{end_col}{end_row}",
f"{start_col}{current_row}:{end_col}{current_row + template_rows - 1}"
)
# コピーした範囲内のセルを走査して値を置換
for row in range(current_row, current_row + template_rows):
for cell in self.current_worksheet[row]:
if cell.value and isinstance(cell.value, str):
# [field_name]形式の文字列を検索
import re
matches = re.findall(r'\[(.*?)\]', cell.value)
# マッチした場合、フィールド値で置換
if matches:
new_value = cell.value
for field_name in matches:
if field_name in record:
new_value = new_value.replace(
f'[{field_name}]',
str(record[field_name])
)
cell.value = new_value
current_row += template_rows
cursor.close()
self.conn.close()
return {"status": True, "message": "Success processing all records"}
except Exception as e:
logging.error(f"Error in proceed_all_record: {str(e)}")
return {"status": False, "message": f"Exception in proceed_all_record:Error processing records: {str(e)}"}
def copy_template_to_current(self, orig_range, target_range):
try:
print(f"orig_rage={orig_range},target_range={target_range}")
# 範囲をパースする
orig_min_col, orig_min_row, orig_max_col, orig_max_row = range_boundaries(orig_range)
target_min_col, target_min_row, target_max_col, target_max_row = range_boundaries(target_range)
print(f"min_col, min_row, max_col, max_row = {orig_min_col}, {orig_min_row}, {orig_max_col}, {orig_max_row}")
print(f"min_col, min_row, max_col, max_row = {target_min_col}, {target_min_row}, {target_max_col}, {target_max_row}")
# Get template sheet name from ini file
section_config = self.conf.get_section(self.section_list[0]) # 現在の処理中のセクション
template_sheet_name = section_config.get("template_sheet")
if not template_sheet_name:
raise ValueError("Template sheet name not found in configuration")
# Create new sheet with template name if it doesn't exist
if template_sheet_name not in self.workbook.sheetnames:
self.current_sheet = self.workbook.create_sheet(template_sheet_name)
else:
self.current_sheet = self.workbook[template_sheet_name]
# Remove default sheet if it exists
if 'Sheet' in self.workbook.sheetnames:
del self.workbook['Sheet']
# Copy column widths
for col in range(orig_min_col, orig_max_col + 1):
col_letter = get_column_letter(col)
if col_letter in self.template_sheet.column_dimensions:
self.current_sheet.column_dimensions[col_letter].width = \
self.template_sheet.column_dimensions[col_letter].width
# Copy row heights
for row in range(orig_min_row, orig_max_row + 1):
target_row = row - orig_min_row + target_min_row
if row in self.template_sheet.row_dimensions:
source_height = self.template_sheet.row_dimensions[row].height
if source_height is not None:
if target_row not in self.current_sheet.row_dimensions:
self.current_sheet.row_dimensions[target_row] = openpyxl.worksheet.dimensions.RowDimension(target_row)
self.current_sheet.row_dimensions[target_row].height = source_height
# Copy merged cells
for merged_range in self.template_sheet.merged_cells:
min_col, min_row, max_col, max_row = range_boundaries(str(merged_range))
# Check if merge range intersects with our copy range
if (min_col >= orig_min_col and max_col <= orig_max_col and
min_row >= orig_min_row and max_row <= orig_max_row):
# Calculate target merge range
target_merge_min_row = target_min_row + (min_row - orig_min_row)
target_merge_max_row = target_min_row + (max_row - orig_min_row)
target_merge_range = f"{get_column_letter(min_col)}{target_merge_min_row}:" \
f"{get_column_letter(max_col)}{target_merge_max_row}"
self.current_sheet.merge_cells(target_merge_range)
# Copy cell contents and styles
row_offset = target_min_row - orig_min_row
for row in range(orig_min_row, orig_max_row + 1):
for col in range(orig_min_col, orig_max_col + 1):
source_cell = self.template_sheet.cell(row=row, column=col)
target_cell = self.current_sheet.cell(row=row+row_offset, column=col)
# Copy value
target_cell.value = source_cell.value
# Copy styles
if source_cell.has_style:
target_cell.font = copy(source_cell.font)
target_cell.border = copy(source_cell.border)
target_cell.fill = copy(source_cell.fill)
target_cell.number_format = source_cell.number_format
target_cell.protection = copy(source_cell.protection)
target_cell.alignment = copy(source_cell.alignment)
# Copy page setup
target_page_setup = self.current_sheet.page_setup
source_page_setup = self.template_sheet.page_setup
# Copy supported page setup attributes
copyable_attrs = [
'paperSize',
'orientation',
'fitToHeight',
'fitToWidth',
'scale'
]
for attr in copyable_attrs:
try:
if hasattr(source_page_setup, attr):
setattr(target_page_setup, attr, getattr(source_page_setup, attr))
except Exception as e:
logging.warning(f"Could not copy page setup attribute {attr}: {str(e)}")
# Copy margins
target_margins = self.current_sheet.page_margins
source_margins = self.template_sheet.page_margins
margin_attrs = ['left', 'right', 'top', 'bottom', 'header', 'footer']
for attr in margin_attrs:
try:
if hasattr(source_margins, attr):
setattr(target_margins, attr, getattr(source_margins, attr))
except Exception as e:
logging.warning(f"Could not copy margin attribute {attr}: {str(e)}")
# Copy print options
target_print = self.current_sheet.print_options
source_print = self.template_sheet.print_options
print_attrs = [
'horizontalCentered',
'verticalCentered',
'gridLines',
'gridLinesSet'
]
for attr in print_attrs:
try:
if hasattr(source_print, attr):
setattr(target_print, attr, getattr(source_print, attr))
except Exception as e:
logging.warning(f"Could not copy print option {attr}: {str(e)}")
return {"status": True, "message": "Successfully copied template range"}
except Exception as e:
logging.error(f"Error in copy_template_to_current: {str(e)}")
return {"status": False, "message": f"Exception in copy_template_to_current: {str(e)}"}
# Style operations
def apply_style(
self,
cell_range: str,
font: Dict[str, Any] = None,
fill: Dict[str, Any] = None,
border: Dict[str, Any] = None,
alignment: Dict[str, Any] = None
) -> None:
"""Apply styles to cell range"""
for row in self.current_sheet[cell_range]:
for cell in row:
if font:
cell.font = self._style_manager.create_font(**font)
if fill:
cell.fill = self._style_manager.create_fill(**fill)
if border:
cell.border = self._style_manager.create_border(**border)
if alignment:
cell.alignment = self._style_manager.create_alignment(**alignment)
# Merge operations
def merge_range(
self,
start_row: int,
start_col: int,
end_row: int,
end_col: int
) -> None:
"""Merge cell range"""
self._merge_manager.merge_cells(start_row, start_col, end_row, end_col)
# Image operations
def add_image(
self,
image_path: Union[str, Path],
position: Tuple[int, int],
size: Optional[Tuple[int, int]] = None
) -> None:
"""Add image to worksheet"""
self._image_manager.add_image(image_path, position, size)
# Conditional formatting
def add_conditional_format(
self,
cell_range: str,
format_type: str,
**kwargs
) -> None:
"""Add conditional formatting"""
if format_type == 'color_scale':
self._conditional_manager.add_color_scale(cell_range, **kwargs)
elif format_type == 'data_bar':
self._conditional_manager.add_data_bar(cell_range, **kwargs)
elif format_type == 'icon_set':
self._conditional_manager.add_icon_set(cell_range, **kwargs)
elif format_type == 'custom':
self._conditional_manager.add_custom_rule(cell_range, **kwargs)
# Page setup
def setup_page(
self,
orientation: str = 'portrait',
paper_size: int = PaperSizes.A4,
margins: Dict[str, float] = None,
header_footer: Dict[str, Any] = None
) -> None:
"""Configure page setup"""
self._page_manager.set_page_setup(
orientation=orientation,
paper_size=paper_size
)
if margins:
self._page_manager.set_margins(**margins)
if header_footer:
self._page_manager.set_header_footer(**header_footer)
def cleanup(self) -> None:
"""Cleanup temporary files"""
if self._image_manager:
self._image_manager.cleanup()
def __del__(self):
"""Destructor"""
self.cleanup()

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,28 @@
from sumaexcel import SumasenExcel
import logging
# 初期化
# 初期化
variables = {
"zekken_number":"5033",
"event_code":"FC岐阜",
"db":"rogdb",
"username":"admin",
"password":"admin123456",
"host":"localhost",
"port":"5432"
}
excel = SumasenExcel(document="test", variables=variables, docbase="./testdata")
logging.info("Excelファイル作成 step-1")
# シート初期化
ret = excel.make_report(variables=variables)
logging.info(f"Excelファイル作成 step-2 : ret={ret}")
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}")

24
SumasenLibs/excel_lib/testdata/test.ini vendored Normal file
View File

@ -0,0 +1,24 @@
[basic]
template_file=certificate_template.xlsx
doc_file=certificate_[zekken_number].xlsx
sections=section1
maxcol=8
[section1]
template_sheet=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:H11
[section1.group2]
table_name=gps_checkins
where=zekken_number='[zekken_number]' and event_code='[event_code]'
sort=path_order
group_range=A12:H12

View File

@ -53,7 +53,9 @@ services:
- type: volume - type: volume
source: nginx_logs source: nginx_logs
target: /var/log/nginx target: /var/log/nginx
- media_data:/app/media:ro - type: bind
source: ./media
target: /usr/share/nginx/html/media
ports: ports:
- "80:80" - "80:80"
depends_on: depends_on:
@ -73,4 +75,3 @@ volumes:
geoserver-data: geoserver-data:
static_volume: static_volume:
nginx_logs: nginx_logs:
media_data:

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -323,6 +323,8 @@ class NewEvent2(models.Model):
class_solo_male = models.BooleanField(default=True) class_solo_male = models.BooleanField(default=True)
class_solo_female = models.BooleanField(default=True) class_solo_female = models.BooleanField(default=True)
self_rogaining = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return f"{self.event_name} - From:{self.start_datetime} To:{self.end_datetime}" return f"{self.event_name} - From:{self.start_datetime} To:{self.end_datetime}"
@ -520,6 +522,11 @@ class GoalImages(models.Model):
team_name = models.CharField(_("Team name"), max_length=255) team_name = models.CharField(_("Team name"), max_length=255)
event_code = models.CharField(_("event code"), max_length=255) event_code = models.CharField(_("event code"), max_length=255)
cp_number = models.IntegerField(_("CP numner")) cp_number = models.IntegerField(_("CP numner"))
zekken_number = models.TextField(
null=True, # False にする
blank=True, # False にする
help_text="ゼッケン番号"
)
class CheckinImages(models.Model): class CheckinImages(models.Model):
user=models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING) user=models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING)
@ -530,6 +537,7 @@ class CheckinImages(models.Model):
cp_number = models.IntegerField(_("CP numner")) cp_number = models.IntegerField(_("CP numner"))
class GpsCheckin(models.Model): class GpsCheckin(models.Model):
id = models.AutoField(primary_key=True) # 明示的にidフィールドを追加
path_order = models.IntegerField( path_order = models.IntegerField(
null=False, null=False,
help_text="チェックポイントの順序番号" help_text="チェックポイントの順序番号"
@ -635,7 +643,7 @@ class GpsCheckin(models.Model):
] ]
def __str__(self): def __str__(self):
return f"{self.event_code}-{self.zekken_number}-{self.path_order}" return f"{self.event_code}-{self.zekken_number}-{self.path_order}-buy:{self.buy_flag}-valid:{self.validate_location}-point:{self.points}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# 作成時・更新時のタイムスタンプを自動設定 # 作成時・更新時のタイムスタンプを自動設定

189
rog/postgres_views.sql Normal file
View File

@ -0,0 +1,189 @@
-- まず既存のビューをすべて削除
DROP MATERIALIZED VIEW IF EXISTS mv_entry_details CASCADE;
DROP VIEW IF EXISTS v_category_rankings CASCADE;
DROP VIEW IF EXISTS v_checkin_summary CASCADE;
-- チェックポイントの集計用ビュー
CREATE VIEW v_checkin_summary AS
SELECT
event_code,
zekken_number, -- 文字列として保持
COUNT(*) as total_checkins,
COUNT(CASE WHEN buy_flag THEN 1 END) as purchase_count,
SUM(points) as total_points,
SUM(CASE WHEN buy_flag THEN points ELSE 0 END) as bonus_points,
SUM(CASE WHEN NOT buy_flag THEN points ELSE 0 END) as normal_points,
SUM(COALESCE(late_point, 0)) as penalty_points,
MAX(create_at) as last_checkin
FROM
gps_checkins
GROUP BY
event_code, zekken_number;
-- カテゴリー内ランキング計算用ビュー
CREATE VIEW v_category_rankings AS
SELECT
e.id,
e.event_id,
ev.event_name,
e.category_id,
CAST(e.zekken_number AS TEXT) as zekken_number, -- 数値を文字列に変換
COALESCE(cs.total_points, 0) as total_score,
RANK() OVER (PARTITION BY e.event_id, e.category_id
ORDER BY COALESCE(cs.total_points, 0) DESC) as ranking,
COUNT(*) OVER (PARTITION BY e.event_id, e.category_id) as total_participants
FROM
rog_entry e
JOIN rog_newevent2 ev ON e.event_id = ev.id
LEFT JOIN v_checkin_summary cs ON ev.event_name = cs.event_code
AND CAST(e.zekken_number AS TEXT) = cs.zekken_number
WHERE
e.is_active = true;
-- マテリアライズドビューの作成
-- マテリアライズドビューの再作成
CREATE MATERIALIZED VIEW mv_entry_details AS
SELECT
-- 既存のフィールド
e.id,
CAST(e.zekken_number AS TEXT) as zekken_number,
e.is_active,
e."hasParticipated",
e."hasGoaled",
e.date as entry_date,
-- イベント情報
ev.event_name,
ev.start_datetime,
ev.end_datetime,
ev."deadlineDateTime",
-- カテゴリー情報
nc.category_name,
nc.category_number,
nc.duration,
nc.num_of_member,
nc.family as is_family_category,
nc.female as is_female_category,
-- チーム情報
t.team_name,
-- オーナー情報
cu.email as owner_email,
cu.firstname as owner_firstname,
cu.lastname as owner_lastname,
cu.date_of_birth as owner_birth_date,
cu.female as owner_is_female,
-- スコア情報
COALESCE(cs.total_points, 0) as total_points,
COALESCE(cs.normal_points, 0) as normal_points,
COALESCE(cs.bonus_points, 0) as bonus_points,
COALESCE(cs.penalty_points, 0) as penalty_points,
COALESCE(cs.total_checkins, 0) as checkin_count,
COALESCE(cs.purchase_count, 0) as purchase_count,
-- ゴール情報
gi.goalimage as goal_image,
gi.goaltime as goal_time,
-- 完走状態の判定を追加
CASE
WHEN gi.goaltime IS NULL THEN '棄権'
WHEN gi.goaltime <= ev.end_datetime THEN '完走'
WHEN gi.goaltime > ev.end_datetime AND
gi.goaltime <= ev.end_datetime + INTERVAL '15 minutes' THEN '完走(遅刻)'
ELSE '失格'
END as validation,
-- ランキング情報
cr.ranking as category_rank,
cr.total_participants,
-- チームメンバー情報JSON形式で格納
jsonb_agg(
jsonb_build_object(
'email', m.user_id,
'firstname', m.firstname,
'lastname', m.lastname,
'birth_date', m.date_of_birth,
'is_female', m.female,
'is_temporary', m.is_temporary,
'status', CASE
WHEN m.is_temporary THEN 'TEMPORARY'
WHEN m.date_of_birth IS NULL THEN 'PENDING'
ELSE 'ACTIVE'
END,
'member_type', CASE
WHEN m.user_id = e.owner_id THEN 'OWNER'
ELSE 'MEMBER'
END
) ORDER BY
CASE WHEN m.user_id = e.owner_id THEN 0 ELSE 1 END, -- オーナーを最初に
m.id
) FILTER (WHERE m.id IS NOT NULL) as team_members
FROM
rog_entry e
INNER JOIN rog_newevent2 ev ON e.event_id = ev.id
INNER JOIN rog_newcategory nc ON e.category_id = nc.id
INNER JOIN rog_team t ON e.team_id = t.id
LEFT JOIN rog_customuser cu ON e.owner_id = cu.id
LEFT JOIN v_checkin_summary cs ON ev.event_name = cs.event_code
AND CAST(e.zekken_number AS TEXT) = cs.zekken_number
LEFT JOIN v_category_rankings cr ON e.id = cr.id
LEFT JOIN rog_member m ON t.id = m.team_id
LEFT JOIN rog_goalimages gi ON ev.event_name = gi.event_code
AND CAST(e.zekken_number AS TEXT) = gi.zekken_number
GROUP BY
e.id, e.zekken_number, e.is_active, e."hasParticipated", e."hasGoaled", e.date,
ev.event_name, ev.start_datetime, ev.end_datetime, ev."deadlineDateTime",
nc.category_name, nc.category_number, nc.duration, nc.num_of_member,
nc.family, nc.female,
t.team_name,
cu.email, cu.firstname, cu.lastname, cu.date_of_birth, cu.female,
cs.total_points, cs.normal_points, cs.bonus_points, cs.penalty_points,
cs.total_checkins, cs.purchase_count, cs.last_checkin,
cr.ranking, cr.total_participants,
gi.goalimage, gi.goaltime,
e.owner_id;
-- インデックスの再作成
CREATE UNIQUE INDEX idx_mv_entry_details_event_zekken
ON mv_entry_details(event_name, zekken_number);
-- ビューの更新
REFRESH MATERIALIZED VIEW mv_entry_details;
-- チェックインと位置情報を結合したビューを作成
DROP VIEW IF EXISTS v_checkins_locations CASCADE;
CREATE OR REPLACE VIEW v_checkins_locations AS
SELECT
g.event_code,
g.zekken_number,
g.path_order,
g.cp_number,
l.sub_loc_id,
l.location_name,
l.photos,
g.image_address,
g.create_at,
g.buy_flag,
g.validate_location,
g.points
FROM
gps_checkins g
LEFT JOIN rog_location l ON g.cp_number = l.cp
ORDER BY
g.event_code,
g.zekken_number,
g.path_order;
-- インデックスのサジェスチョン(実際のテーブルに適用する必要があります)
/*
CREATE INDEX idx_gps_checkins_cp_number ON gps_checkins(cp_number);
CREATE INDEX idx_rog_location_cp ON rog_location(cp);
*/

View File

@ -124,6 +124,9 @@ urlpatterns += [
path('export_excel/<int:zekken_number>/<str:event_code>/', views.export_excel, name='export_excel'), path('export_excel/<int:zekken_number>/<str:event_code>/', views.export_excel, name='export_excel'),
# for Supervisor Web app # for Supervisor Web app
path('test/', views.test_api, name='test_api'), path('test/', views.test_api, name='test_api'),
path('update-goal-time/', views.update_goal_time, name='update-goal-time'),
path('get-goalimage/', views.get_goalimage, name='get-goalimage'),
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@ -89,6 +89,9 @@ from io import BytesIO
from django.urls import get_resolver from django.urls import get_resolver
import os import os
import json
from sumaexcel import SumasenExcel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -227,6 +230,43 @@ class LocationViewSet(viewsets.ModelViewSet):
serializer_class=LocationSerializer serializer_class=LocationSerializer
filter_fields=["prefecture", "location_name"] filter_fields=["prefecture", "location_name"]
def get_queryset(self):
queryset = Location.objects.all()
logger.info("=== Location API Called ===")
# リクエストパラメータの確認
group_filter = self.request.query_params.get('group__contains')
logger.info(f"Request params: {dict(self.request.query_params)}")
logger.info(f"Group filter: {group_filter}")
if group_filter:
# フィルタ適用前のデータ数
total_count = queryset.count()
logger.info(f"Total locations before filter: {total_count}")
# フィルタの適用
queryset = queryset.filter(group__contains=group_filter)
# フィルタ適用後のデータ数
filtered_count = queryset.count()
logger.info(f"Filtered locations count: {filtered_count}")
# フィルタされたデータのサンプル最初の5件
sample_data = queryset[:5]
logger.info("Sample of filtered data:")
for loc in sample_data:
logger.info(f"ID: {loc.id}, Name: {loc.location_name}, Group: {loc.group}")
return queryset
def list(self, request, *args, **kwargs):
try:
response = super().list(request, *args, **kwargs)
logger.info(f"Response data count: {len(response.data['features'])}")
return response
except Exception as e:
logger.error(f"Error in list method: {str(e)}", exc_info=True)
raise
class Location_lineViewSet(viewsets.ModelViewSet): class Location_lineViewSet(viewsets.ModelViewSet):
queryset=Location_line.objects.all() queryset=Location_line.objects.all()
@ -2362,18 +2402,45 @@ def get_team_info(request, zekken_number):
entry = Entry.objects.select_related('team','event').get(zekken_number=zekken_number) entry = Entry.objects.select_related('team','event').get(zekken_number=zekken_number)
members = Member.objects.filter(team=entry.team) members = Member.objects.filter(team=entry.team)
# チームのゴール時間を取得 start_datetime = entry.event.start_datetime #イベントの規定スタート時刻
if entry.event.self_rogaining:
#get_checkinsの中で、
# start_datetime = -1(ロゲ開始)のcreate_at
logger.debug(f"self.rogaining={entry.event.self_rogaining} => start_datetime = -1(ロゲ開始)のcreate_at")
# チームの最初のゴール時間を取得
goal_record = GoalImages.objects.filter( goal_record = GoalImages.objects.filter(
team_name=entry.team.team_name, team_name=entry.team.team_name,
event_code=entry.event.event_name event_code=entry.event.event_name
).order_by('-goaltime').first() ).order_by('goaltime').first()
# Nullチェックを追加してからログ出力
goalimage_url = None
goaltime = None
if goal_record:
try:
goaltime = goal_record.goaltime
if goal_record.goalimage and hasattr(goal_record.goalimage, 'url'):
goalimage_url = request.build_absolute_uri(goal_record.goalimage.url)
logger.info(f"get_team_info record.goalimage_url={goalimage_url}")
else:
logger.info("Goal record exists but no image found")
except ValueError as e:
logger.warning(f"Error accessing goal image: {str(e)}")
else:
logger.info("No goal record found for team")
return Response({ return Response({
'team_name': entry.team.team_name, 'team_name': entry.team.team_name,
'members': ', '.join([f"{m.lastname} {m.firstname}" for m in members]), 'members': ', '.join([f"{m.lastname} {m.firstname}" for m in members]),
'event_code': entry.event.event_name, 'event_code': entry.event.event_name,
'start_datetime': entry.event.start_datetime, 'start_datetime': entry.event.start_datetime,
'end_datetime': goal_record.goaltime if goal_record else None 'end_datetime': goaltime, #goal_record.goaltime if goal_record else None,
'goal_photo': goalimage_url, #goal_record.goalimage if goal_record else None,
'duration': entry.category.duration.total_seconds()
}) })
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -2468,53 +2535,104 @@ def get_checkins(request, *args, **kwargs):
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
@api_view(['POST']) @api_view(['POST'])
def update_checkins(request): def update_checkins(request):
try:
with transaction.atomic(): with transaction.atomic():
for update in request.data: 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:
# 既存レコードの更新
logger.info(f"Updating existing checkin : {update}")
try:
checkin = GpsCheckin.objects.get(id=update['id']) checkin = GpsCheckin.objects.get(id=update['id'])
checkin.path_order = update['path_order'] logger.info(f"Updating existing checkin: {checkin}")
checkin.validate_location = update['validate_location']
# 既存レコードの更新
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.save() checkin.save()
return Response({'status': 'success'}) logger.info(f"Updated existing checkin result: {checkin}")
except GpsCheckin.DoesNotExist:
logger.error(f"Checkin with id {update['id']} not found")
return Response(
{"error": f"Checkin with id {update['id']} not found"},
status=status.HTTP_404_NOT_FOUND
)
for update in update_base['checkins']:
if 'id' in update and int(update['id'])==0:
# 新規レコードの作成
logger.info("Creating new checkin:{update}")
try:
checkin = GpsCheckin.objects.create(
zekken_number=update_base['zekken_number'],
event_code=update_base['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"Updated existing checkin result: {checkin}")
except KeyError as e:
logger.error(f"Missing required field: {str(e)}")
return Response(
{"error": f"Missing required field: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST
)
return Response({'status': 'success', 'message': 'Checkins updated successfully'})
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(['GET']) @api_view(['GET'])
def export_excel(request, zekken_number): def export_excel(request, zekken_number):
# エントリー情報の取得
entry = Entry.objects.select_related('team').get(zekken_number=zekken_number)
checkins = GpsCheckin.objects.filter(zekken_number=zekken_number).order_by('path_order')
# Excelファイルの生成 # 初期化
output = BytesIO() variables = {
workbook = xlsxwriter.Workbook(output) "zekken_number":sekken_number,
worksheet = workbook.add_worksheet('通過証明書') "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を作成する
# スタイルの定義 # シート初期化
header_format = workbook.add_format({ ret = excel.make_report(variables=variables)
'bold': True, if ret["status"]==True:
'bg_color': '#CCCCCC', filepath=ret["filepath"]
'border': 1 logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
}) else:
message = ret.get("message", "No message provided")
logging.error(f"Excelファイル作成失敗 : ret.message={message}")
# ヘッダー情報の書き込み
worksheet.write('A1', 'チーム名', header_format)
worksheet.write('B1', entry.team.team_name)
worksheet.write('A2', 'ゼッケン番号', header_format)
worksheet.write('B2', zekken_number)
# チェックインデータの書き込み
headers = ['順序', 'CP番号', 'チェックイン時刻', '検証', 'ポイント']
for col, header in enumerate(headers):
worksheet.write(3, col, header, header_format)
for row, checkin in enumerate(checkins, start=4):
worksheet.write(row, 0, checkin.path_order)
worksheet.write(row, 1, checkin.cp_number)
worksheet.write(row, 2, checkin.create_at.strftime('%Y-%m-%d %H:%M:%S'))
worksheet.write(row, 3, '' if checkin.validate_location else '')
worksheet.write(row, 4, checkin.points)
workbook.close()
# レスポンスの生成 # レスポンスの生成
output.seek(0) output.seek(0)
@ -2522,7 +2640,7 @@ def export_excel(request, zekken_number):
output.read(), output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
) )
response['Content-Disposition'] = f'attachment; filename=通過証明書_{zekken_number}.xlsx' response['Content-Disposition'] = f'attachment; filename=./docbase/certificate_{zekken_number}.xlsx'
return response return response
# ----- for Supervisor ----- # ----- for Supervisor -----
@ -2532,3 +2650,145 @@ def test_api(request):
logger.debug("Test API endpoint called") logger.debug("Test API endpoint called")
return JsonResponse({"status": "API is working"}) return JsonResponse({"status": "API is working"})
@api_view(['GET'])
#@authentication_classes([TokenAuthentication])
#@permission_classes([IsAuthenticated])
def get_goalimage(request):
"""
ゼッケン番号とイベントコードに基づいてゴール画像情報を取得するエンドポイント
Parameters:
zekken_number (str): ゼッケン番号
event_code (str): イベントコード
Returns:
Response: ゴール画像情報を含むJSONレスポンス
"""
try:
logger.debug(f"get_goalimage called with params: {request.GET}")
# リクエストパラメータを取得
zekken_number = request.GET.get('zekken_number')
event_code = request.GET.get('event_code')
logger.debug(f"Searching for goal records with zekken_number={zekken_number}, event_code={event_code}")
# パラメータの検証
if not zekken_number or not event_code:
return Response(
{"error": "zekken_number and event_code are required"},
status=status.HTTP_400_BAD_REQUEST
)
# ゴール画像レコードを検索(最も早いゴール時間のレコードを取得)
goal_records = GoalImages.objects.filter(
zekken_number=zekken_number,
event_code=event_code
).order_by('goaltime')
logger.debug(f"Found {goal_records.count()} goal records")
if not goal_records.exists():
return Response(
{"message": "No goal records found"},
status=status.HTTP_404_NOT_FOUND
)
# 最も早いゴール時間のレコードcp_number = 0を探す
valid_goal = goal_records.filter(cp_number=0).first()
if not valid_goal:
return Response(
{"message": "No valid goal record found"},
status=status.HTTP_404_NOT_FOUND
)
# シリアライザでデータを整形
serializer = GolaImageSerializer(valid_goal)
# レスポンスデータの構築
response_data = {
"goal_record": serializer.data,
"total_records": goal_records.count(),
"has_multiple_records": goal_records.count() > 1
}
logger.info(f"Retrieved goal record for zekken_number {zekken_number} in event {event_code}")
return Response(response_data)
except Exception as e:
logger.error(f"Error retrieving goal record: {str(e)}", exc_info=True)
return Response(
{"error": f"Failed to retrieve goal record: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
#@authentication_classes([TokenAuthentication])
#@permission_classes([IsAuthenticated])
def update_goal_time(request):
try:
logger.info(f"update_goal_time:{request}")
# リクエストからデータを取得
zekken_number = request.data.get('zekken_number')
event_code = request.data.get('event_code')
team_name = request.data.get('team_name')
goal_time_str = request.data.get('goaltime')
logger.info(f"zekken_number={zekken_number},event_code={event_code},team_name={team_name},goal_time={goal_time_str}")
# 入力バリデーション
#if not all([zekken_number, event_code, team_name, goal_time_str]):
# return Response(
# {"error": "Missing required fields"},
# status=status.HTTP_400_BAD_REQUEST
# )
try:
# 文字列からdatetimeオブジェクトに変換
goal_time = datetime.strptime(goal_time_str, '%Y-%m-%dT%H:%M:%S')
except ValueError:
return Response(
{"error": "Invalid goal time format"},
status=status.HTTP_400_BAD_REQUEST
)
# 既存のゴール記録を探す
goal_record = GoalImages.objects.filter(
team_name=team_name,
event_code=event_code
).first()
if goal_record:
# 既存の記録を更新
goal_record.goaltime = goal_time
goal_record.save()
logger.info(f"Updated goal time as {goal_time} for team {team_name} in event {event_code}")
else:
# 新しい記録を作成
entry = Entry.objects.get(zekken_number=zekken_number, event__event_name=event_code)
GoalImages.objects.create(
user=entry.owner,
goaltime=goal_time,
team_name=team_name,
event_code=event_code,
cp_number=0 # ゴール地点を表すCP番号
)
logger.info(f"Created new goal time record for team {team_name} in event {event_code}")
return Response({"message": "Goal time updated successfully"})
except Entry.DoesNotExist:
logger.error(f"Entry not found for zekken_number {zekken_number} in event {event_code}")
return Response(
{"error": "Entry not found"},
status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
logger.error(f"Error updating goal time: {str(e)}")
return Response(
{"error": f"Failed to update goal time: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)

Binary file not shown.

File diff suppressed because it is too large Load Diff

BIN
templates/.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>アクティベーション成功</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
.message {
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
</style>
</head>
<body>
<div class="message">
<h1>アクティベーション成功</h1>
<p>{{ message }}</p>
</div>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>メール確認成功</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
.message {
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
</style>
</head>
<body>
<div class="message">
<h1>メール確認成功</h1>
<p>{{ message }}</p>
</div>
</body>
</html>