Compare commits

59 Commits

Author SHA1 Message Date
ae87890eec Resolve merge conflict in CustomUserAdmin 2025-01-22 08:43:18 +00:00
43c89dec9a something updated 2025-01-22 08:19:49 +00:00
005de98ecc Add Event user registration 2025-01-22 17:14:56 +09:00
82fa3c2249 2024-12-19 2024-12-19 03:58:48 +00:00
acf6e36e71 Fix admin issue 2024-12-19 12:57:57 +09:00
a0f2b01f29 Fix Ranking code step3 2024-11-12 09:09:00 +09:00
a3c90902ec Fix Ranking code step2 2024-11-12 08:59:20 +09:00
fccc55cf18 Fix Ranking code step1 2024-11-12 07:19:18 +09:00
19f12652b9 Fix MyAlbum code step6 2024-11-11 16:02:02 +09:00
cdae8dc7ec Fix MyAlbum code step5 2024-11-11 15:58:05 +09:00
c4e25de121 Fix MyAlbum code step4 2024-11-11 15:46:47 +09:00
09810a2a9a Fix MyAlbum code step3 2024-11-11 15:41:32 +09:00
a9b959a807 Fix MyAlbum code step2 2024-11-11 15:28:43 +09:00
de3e87b963 Fix MyAlbum code step1 2024-11-11 15:11:21 +09:00
0453494cca Fix printing area and options step6 2024-11-11 09:21:22 +09:00
60337c6863 Fix printing area and options step5 2024-11-11 09:17:30 +09:00
a3f602b360 Fix printing area and options step4 2024-11-11 08:53:23 +09:00
fd973575be Fix printing area and options step3 2024-11-11 08:15:08 +09:00
872f252923 Fix printing area and options step2 2024-11-11 01:13:33 +09:00
5e2b5add5c Fix printing area and options 2024-11-11 00:59:53 +09:00
9e3a940ec2 Fix missing print parameters 2024-11-11 00:46:28 +09:00
158dbeee40 Fix penalty on Excel 2024-11-11 00:37:10 +09:00
10bf6e8fa1 Fix ranking on Excel 2024-11-10 23:01:32 +09:00
18f3370f29 modify event_id as integer on GpsCheckin 2024-11-10 16:32:49 +09:00
0abfd6cdb6 add event_id on GpsCheckin 2024-11-10 16:13:03 +09:00
2f8b86b683 Fix goalimage scale 3 2024-11-10 01:44:02 +09:00
b85b04412a Fix goalimage scale 2 2024-11-10 01:41:34 +09:00
efbce943b6 Fix goalimage scale 2024-11-10 01:35:44 +09:00
02f483aa68 Fix goal image 2024-11-10 01:09:55 +09:00
7c659a0865 adjust Excel sheet and SQL 2024-11-09 19:38:15 +09:00
3f91e2080a Adjust Excel template and model 2024-11-09 19:28:11 +09:00
56e13457ab Update template ini file 2024-11-09 19:11:19 +09:00
7d6635ef01 Update Excel template 2024-11-09 19:07:34 +09:00
2ca77b604b Fix PDF issue 2024-11-09 09:50:58 +00:00
27aed10a4a Merge remote-tracking branch 'origin/extdb-3' into extdb-3 2024-11-08 14:47:32 +00:00
e6e6d059ac temporary update 2024-11-08 14:47:10 +00:00
e1928564fa Save Excel and PDF to AWS S3. 2024-11-08 23:43:31 +09:00
a0c3a82720 debug PDF generation 2024-11-08 18:42:07 +09:00
4e4bd7ac5d Front End bug fixed 2024-11-08 08:33:18 +00:00
2bf7d44cd3 Fix goaltime save 2024-11-08 14:52:31 +09:00
d22e8b5a23 final stage update bugs 2024-11-08 14:33:46 +09:00
9eb45d7e97 final stage -- still some bugs 2024-11-08 04:30:58 +00:00
2aaecb6b22 Merge remote-tracking branch 'origin/extdb-3' into extdb-3 2024-11-06 18:28:42 +00:00
6e472cf634 Generate Excel dev stege final 2024-11-06 18:26:16 +00:00
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
55 changed files with 19225 additions and 275 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
.env.swp

Binary file not shown.

View File

@ -3,6 +3,7 @@ FROM osgeo/gdal:ubuntu-small-3.4.0
WORKDIR /app
LABEL maintainer="nouffer@gmail.com"
LABEL description="Development image for the Rogaining JP"
@ -38,12 +39,63 @@ 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 \
libreoffice-java-common \
fonts-ipafont \
fonts-ipafont-gothic \
fonts-ipafont-mincho \
language-pack-ja \
fontconfig \
locales \
python3-uno # LibreOffice Python バインディング
# 日本語ロケールの設定
RUN locale-gen ja_JP.UTF-8
ENV LANG=ja_JP.UTF-8
ENV LC_ALL=ja_JP.UTF-8
ENV LANGUAGE=ja_JP:ja
# フォント設定ファイルをコピー
COPY config/fonts.conf /etc/fonts/local.conf
# フォントキャッシュの更新
RUN fc-cache -f -v
# LibreOfficeの作業ディレクトリを作成
RUN mkdir -p /var/cache/libreoffice && \
chmod 777 /var/cache/libreoffice
# フォント設定の権限を設定
RUN chmod 644 /etc/fonts/local.conf
# 作業ディレクトリとパーミッションの設定
RUN mkdir -p /app/docbase /tmp/libreoffice && \
chmod -R 777 /app/docbase /tmp/libreoffice
RUN pip install --upgrade pip
# 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
COPY ./requirements.txt /app/requirements.txt
RUN pip install boto3==1.26.137
# Install Gunicorn
RUN pip install gunicorn

View File

@ -30,6 +30,6 @@ RUN chown -R nginx:nginx /usr/share/nginx/html \
&& chown -R nginx:nginx /var/log/nginx \
&& chown -R nginx:nginx /etc/nginx/conf.d
EXPOSE 80
#EXPOSE 8100
CMD ["nginx", "-g", "daemon off;"]

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,6 @@
openpyxl>=3.0.0
pandas>=1.0.0
pillow>=8.0.0
configparser>=5.0.0
psycopg2-binary==2.9.9
requests

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",
)

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

File diff suppressed because it is too large Load Diff

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}")

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

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

164
checkpoint_summary.csv Normal file
View File

@ -0,0 +1,164 @@
event_id,event_name,cp_number,sub_loc_id,location_name,category_id,category_name,normal_checkins,purchase_checkins
10,FC岐阜,-1,#-1(0),スタート(長良川競技場芝生広場),5,ソロ男子-3時間,7,0
10,FC岐阜,-1,#-1(0),スタート(長良川競技場芝生広場),6,ソロ女子-3時間,2,0
10,FC岐阜,-1,#-1(0),スタート(長良川競技場芝生広場),7,ファミリー-3時間,2,0
10,FC岐阜,-1,#-1(0),スタート(長良川競技場芝生広場),8,一般-3時間,8,0
10,FC岐阜,1,#1(35),長良公園(枝広館跡),8,一般-3時間,2,0
10,FC岐阜,3,#3(28),長良川うかいミュージアム(岐阜市長良川鵜飼伝承館),7,ファミリー-3時間,1,0
10,FC岐阜,3,#3(28),長良川うかいミュージアム(岐阜市長良川鵜飼伝承館),8,一般-3時間,4,0
10,FC岐阜,4,#4(15),高橋尚子ゴールドメダル記念碑(足形),5,ソロ男子-3時間,7,0
10,FC岐阜,4,#4(15),高橋尚子ゴールドメダル記念碑(足形),6,ソロ女子-3時間,1,0
10,FC岐阜,4,#4(15),高橋尚子ゴールドメダル記念碑(足形),7,ファミリー-3時間,2,0
10,FC岐阜,4,#4(15),高橋尚子ゴールドメダル記念碑(足形),8,一般-3時間,7,0
10,FC岐阜,4,#4(15),高橋尚子ゴールドメダル記念碑(足形),9,お試し-3時間,1,0
10,FC岐阜,5,#5(10),崇福寺・稲葉一鉄寄贈の鐘楼,5,ソロ男子-3時間,5,0
10,FC岐阜,5,#5(10),崇福寺・稲葉一鉄寄贈の鐘楼,6,ソロ女子-3時間,2,0
10,FC岐阜,5,#5(10),崇福寺・稲葉一鉄寄贈の鐘楼,7,ファミリー-3時間,2,0
10,FC岐阜,5,#5(10),崇福寺・稲葉一鉄寄贈の鐘楼,8,一般-3時間,6,0
10,FC岐阜,6,#6(40),鷺山城跡,6,ソロ女子-3時間,1,0
10,FC岐阜,6,#6(40),鷺山城跡,8,一般-3時間,2,0
10,FC岐阜,7,#7(30),岐阜県立岐阜商業高等学校,5,ソロ男子-3時間,2,0
10,FC岐阜,7,#7(30),岐阜県立岐阜商業高等学校,6,ソロ女子-3時間,1,0
10,FC岐阜,7,#7(30),岐阜県立岐阜商業高等学校,8,一般-3時間,4,0
10,FC岐阜,8,#8(45+80),パティスリー kura,5,ソロ男子-3時間,2,1
10,FC岐阜,8,#8(45+80),パティスリー kura,8,一般-3時間,4,4
10,FC岐阜,9,#9(55),大垣共立銀行 則武支店,5,ソロ男子-3時間,2,0
10,FC岐阜,9,#9(55),大垣共立銀行 則武支店,8,一般-3時間,4,0
10,FC岐阜,10,#10(48+30),ポッカサッポロ自販機-BOOKOFF則武店,6,ソロ女子-3時間,1,1
10,FC岐阜,10,#10(48+30),ポッカサッポロ自販機-BOOKOFF則武店,8,一般-3時間,2,2
10,FC岐阜,11,#11(72),御嶽神社茅萱宮,5,ソロ男子-3時間,1,0
10,FC岐阜,11,#11(72),御嶽神社茅萱宮,6,ソロ女子-3時間,1,0
10,FC岐阜,12,#12(55),眞中(みなか)神社,6,ソロ女子-3時間,1,0
10,FC岐阜,13,#13(60),江口の鵜飼発祥の地/史跡 江口のわたし,5,ソロ男子-3時間,1,0
10,FC岐阜,13,#13(60),江口の鵜飼発祥の地/史跡 江口のわたし,6,ソロ女子-3時間,1,0
10,FC岐阜,14,#14(85),鏡島湊跡(かがみしまみなと),5,ソロ男子-3時間,2,0
10,FC岐阜,14,#14(85),鏡島湊跡(かがみしまみなと),6,ソロ女子-3時間,1,0
10,FC岐阜,15,#15(45),鏡島弘法(乙津寺),5,ソロ男子-3時間,2,0
10,FC岐阜,15,#15(45),鏡島弘法(乙津寺),6,ソロ女子-3時間,1,0
10,FC岐阜,16,#16(65),岐阜市立岐阜商業高等学校,5,ソロ男子-3時間,2,0
10,FC岐阜,17,#17(43),立政寺,5,ソロ男子-3時間,2,0
10,FC岐阜,18,#18(35),本莊神社,5,ソロ男子-3時間,2,0
10,FC岐阜,19,#19(40),岐阜県美術館,5,ソロ男子-3時間,2,0
10,FC岐阜,20,#20(55+30),ポッカサッポロ自販機-大垣共立銀行エブリデープラザ,5,ソロ男子-3時間,2,2
10,FC岐阜,21,#21(62),武藤嘉門爺像,5,ソロ男子-3時間,1,0
10,FC岐阜,23,#23(95),岐阜県立岐阜総合学園高等学校,5,ソロ男子-3時間,1,0
10,FC岐阜,25,#25(76),鶉田神社,5,ソロ男子-3時間,1,0
10,FC岐阜,26,#26(74),茜部神社,5,ソロ男子-3時間,1,0
10,FC岐阜,33,#33(60),馬頭観世音菩薩,5,ソロ男子-3時間,1,0
10,FC岐阜,33,#33(60),馬頭観世音菩薩,6,ソロ女子-3時間,1,0
10,FC岐阜,34,#34(70),陸上自衛隊 日野基本射撃場,6,ソロ女子-3時間,1,0
10,FC岐阜,37,#37(45+30),ポッカサッポロ自販機-セリア茜部店,5,ソロ男子-3時間,1,1
10,FC岐阜,38,#38(40),比奈守神社,5,ソロ男子-3時間,1,0
10,FC岐阜,39,#39(35),岐阜県立加納高等学校前バス停,5,ソロ男子-3時間,1,0
10,FC岐阜,41,#41(32),中山道往来の松,5,ソロ男子-3時間,2,0
10,FC岐阜,42,#42(30),問屋町ウォールアート,5,ソロ男子-3時間,4,0
10,FC岐阜,43,#43(22),黄金の信長像,5,ソロ男子-3時間,4,0
10,FC岐阜,44,#44(25+80),名鉄協商パーキング 岐阜第2,5,ソロ男子-3時間,2,0
10,FC岐阜,45,#45(30),本荘公園,5,ソロ男子-3時間,1,0
10,FC岐阜,45,#45(30),本荘公園,6,ソロ女子-3時間,1,0
10,FC岐阜,46,#46(30),大縄場大橋公園,5,ソロ男子-3時間,2,0
10,FC岐阜,46,#46(30),大縄場大橋公園,6,ソロ女子-3時間,1,0
10,FC岐阜,46,#46(30),大縄場大橋公園,8,一般-3時間,1,0
10,FC岐阜,47,#47(25),金神社/おもかる石,5,ソロ男子-3時間,4,0
10,FC岐阜,48,#48(46),OKB岐阜中央プラザ わくわくベースG,5,ソロ男子-3時間,8,0
10,FC岐阜,48,#48(46),OKB岐阜中央プラザ わくわくベースG,6,ソロ女子-3時間,1,0
10,FC岐阜,48,#48(46),OKB岐阜中央プラザ わくわくベースG,8,一般-3時間,1,0
10,FC岐阜,51,#51(20),梅林公園,5,ソロ男子-3時間,1,0
10,FC岐阜,51,#51(20),梅林公園,6,ソロ女子-3時間,1,0
10,FC岐阜,52,#52(60),柳ヶ瀬FC岐阜勝ち神社,5,ソロ男子-3時間,7,0
10,FC岐阜,52,#52(60),柳ヶ瀬FC岐阜勝ち神社,6,ソロ女子-3時間,1,0
10,FC岐阜,52,#52(60),柳ヶ瀬FC岐阜勝ち神社,7,ファミリー-3時間,1,0
10,FC岐阜,52,#52(60),柳ヶ瀬FC岐阜勝ち神社,8,一般-3時間,1,0
10,FC岐阜,53,#53(25),美殿町の郵便ポスト,5,ソロ男子-3時間,5,0
10,FC岐阜,53,#53(25),美殿町の郵便ポスト,6,ソロ女子-3時間,1,0
10,FC岐阜,53,#53(25),美殿町の郵便ポスト,7,ファミリー-3時間,1,0
10,FC岐阜,53,#53(25),美殿町の郵便ポスト,8,一般-3時間,1,0
10,FC岐阜,54,#54(150),水道山展望台,5,ソロ男子-3時間,5,0
10,FC岐阜,54,#54(150),水道山展望台,6,ソロ女子-3時間,1,0
10,FC岐阜,54,#54(150),水道山展望台,7,ファミリー-3時間,1,0
10,FC岐阜,54,#54(150),水道山展望台,8,一般-3時間,1,0
10,FC岐阜,55,#55(30),岐阜新聞社,5,ソロ男子-3時間,4,0
10,FC岐阜,55,#55(30),岐阜新聞社,7,ファミリー-3時間,1,0
10,FC岐阜,55,#55(30),岐阜新聞社,8,一般-3時間,3,0
10,FC岐阜,56,#56(24),弥八地蔵尊堂,5,ソロ男子-3時間,2,0
10,FC岐阜,56,#56(24),弥八地蔵尊堂,7,ファミリー-3時間,1,0
10,FC岐阜,56,#56(24),弥八地蔵尊堂,8,一般-3時間,1,0
10,FC岐阜,57,#57(25),建勲神社 (岐阜 信長神社),5,ソロ男子-3時間,5,0
10,FC岐阜,57,#57(25),建勲神社 (岐阜 信長神社),6,ソロ女子-3時間,1,0
10,FC岐阜,57,#57(25),建勲神社 (岐阜 信長神社),7,ファミリー-3時間,1,0
10,FC岐阜,58,#58(65),伊奈波神社・黒龍神社龍頭石,7,ファミリー-3時間,2,0
10,FC岐阜,58,#58(65),伊奈波神社・黒龍神社龍頭石,8,一般-3時間,2,0
10,FC岐阜,59,#59(12),日下部邸跡・岐阜町本陣跡,5,ソロ男子-3時間,2,0
10,FC岐阜,59,#59(12),日下部邸跡・岐阜町本陣跡,7,ファミリー-3時間,2,0
10,FC岐阜,59,#59(12),日下部邸跡・岐阜町本陣跡,8,一般-3時間,3,0
10,FC岐阜,60,#60(25),メディアコスモスみんなの森,5,ソロ男子-3時間,1,0
10,FC岐阜,60,#60(25),メディアコスモスみんなの森,7,ファミリー-3時間,1,0
10,FC岐阜,60,#60(25),メディアコスモスみんなの森,8,一般-3時間,3,0
10,FC岐阜,61,#61(15+80),ナガラガワフレーバー,5,ソロ男子-3時間,1,0
10,FC岐阜,61,#61(15+80),ナガラガワフレーバー,7,ファミリー-3時間,2,2
10,FC岐阜,61,#61(15+80),ナガラガワフレーバー,8,一般-3時間,8,8
10,FC岐阜,62,#62(15),庚申堂,5,ソロ男子-3時間,1,0
10,FC岐阜,62,#62(15),庚申堂,7,ファミリー-3時間,2,0
10,FC岐阜,62,#62(15),庚申堂,8,一般-3時間,7,0
10,FC岐阜,63,#63(15+80),和菓子処 緑水庵 川原町店,5,ソロ男子-3時間,3,0
10,FC岐阜,63,#63(15+80),和菓子処 緑水庵 川原町店,6,ソロ女子-3時間,1,0
10,FC岐阜,63,#63(15+80),和菓子処 緑水庵 川原町店,7,ファミリー-3時間,2,1
10,FC岐阜,63,#63(15+80),和菓子処 緑水庵 川原町店,8,一般-3時間,8,8
10,FC岐阜,63,#63(15+80),和菓子処 緑水庵 川原町店,9,お試し-3時間,1,1
10,FC岐阜,64,#64(16),日中友好庭園,5,ソロ男子-3時間,4,0
10,FC岐阜,64,#64(16),日中友好庭園,6,ソロ女子-3時間,1,0
10,FC岐阜,64,#64(16),日中友好庭園,7,ファミリー-3時間,2,0
10,FC岐阜,64,#64(16),日中友好庭園,8,一般-3時間,8,0
10,FC岐阜,64,#64(16),日中友好庭園,9,お試し-3時間,1,0
10,FC岐阜,65,#65(15),板垣死すとも自由は死なず,5,ソロ男子-3時間,3,0
10,FC岐阜,65,#65(15),板垣死すとも自由は死なず,7,ファミリー-3時間,2,0
10,FC岐阜,65,#65(15),板垣死すとも自由は死なず,8,一般-3時間,6,0
10,FC岐阜,65,#65(15),板垣死すとも自由は死なず,9,お試し-3時間,1,0
10,FC岐阜,66,#66(40),岐阜大仏(正法寺),5,ソロ男子-3時間,3,0
10,FC岐阜,66,#66(40),岐阜大仏(正法寺),7,ファミリー-3時間,2,0
10,FC岐阜,66,#66(40),岐阜大仏(正法寺),8,一般-3時間,3,0
10,FC岐阜,66,#66(40),岐阜大仏(正法寺),9,お試し-3時間,1,0
10,FC岐阜,67,#67(100),めいそうの小道:中間地点,5,ソロ男子-3時間,5,0
10,FC岐阜,67,#67(100),めいそうの小道:中間地点,6,ソロ女子-3時間,1,0
10,FC岐阜,67,#67(100),めいそうの小道:中間地点,7,ファミリー-3時間,2,0
10,FC岐阜,67,#67(100),めいそうの小道:中間地点,8,一般-3時間,3,0
10,FC岐阜,68,#68(160),岐阜城,5,ソロ男子-3時間,4,0
10,FC岐阜,68,#68(160),岐阜城,6,ソロ女子-3時間,1,0
10,FC岐阜,68,#68(160),岐阜城,7,ファミリー-3時間,2,0
10,FC岐阜,68,#68(160),岐阜城,8,一般-3時間,6,0
10,FC岐阜,68,#68(160),岐阜城,9,お試し-3時間,1,0
10,FC岐阜,69,#69(150),金華山展望デッキ,5,ソロ男子-3時間,5,0
10,FC岐阜,69,#69(150),金華山展望デッキ,6,ソロ女子-3時間,1,0
10,FC岐阜,69,#69(150),金華山展望デッキ,7,ファミリー-3時間,2,0
10,FC岐阜,69,#69(150),金華山展望デッキ,8,一般-3時間,6,0
10,FC岐阜,70,#70(180),七曲り登山道岐阜城まで1000m,5,ソロ男子-3時間,5,0
10,FC岐阜,70,#70(180),七曲り登山道岐阜城まで1000m,6,ソロ女子-3時間,1,0
10,FC岐阜,70,#70(180),七曲り登山道岐阜城まで1000m,7,ファミリー-3時間,2,0
10,FC岐阜,70,#70(180),七曲り登山道岐阜城まで1000m,8,一般-3時間,5,0
10,FC岐阜,70,#70(180),七曲り登山道岐阜城まで1000m,9,お試し-3時間,1,0
10,FC岐阜,71,#71(5+5),練習ポイント,5,ソロ男子-3時間,6,5
10,FC岐阜,71,#71(5+5),練習ポイント,6,ソロ女子-3時間,2,2
10,FC岐阜,71,#71(5+5),練習ポイント,7,ファミリー-3時間,1,1
10,FC岐阜,71,#71(5+5),練習ポイント,8,一般-3時間,8,7
10,FC岐阜,71,#71(5+5),練習ポイント,9,お試し-3時間,1,1
10,FC岐阜,72,#72(5+80),岐阜ロゲコーヒー,5,ソロ男子-3時間,3,1
10,FC岐阜,72,#72(5+80),岐阜ロゲコーヒー,6,ソロ女子-3時間,1,0
10,FC岐阜,72,#72(5+80),岐阜ロゲコーヒー,7,ファミリー-3時間,1,1
10,FC岐阜,72,#72(5+80),岐阜ロゲコーヒー,8,一般-3時間,4,3
10,FC岐阜,72,#72(5+80),岐阜ロゲコーヒー,9,お試し-3時間,1,1
10,FC岐阜,73,#73(5+80),FC岐阜岐阜バス,5,ソロ男子-3時間,6,1
10,FC岐阜,73,#73(5+80),FC岐阜岐阜バス,8,一般-3時間,2,0
10,FC岐阜,73,#73(5+80),FC岐阜岐阜バス,9,お試し-3時間,1,0
10,FC岐阜,74,#74(5+80),MKPポイントカード発行,5,ソロ男子-3時間,2,1
10,FC岐阜,74,#74(5+80),MKPポイントカード発行,6,ソロ女子-3時間,1,1
10,FC岐阜,74,#74(5+80),MKPポイントカード発行,7,ファミリー-3時間,1,1
10,FC岐阜,74,#74(5+80),MKPポイントカード発行,8,一般-3時間,7,3
10,FC岐阜,74,#74(5+80),MKPポイントカード発行,9,お試し-3時間,1,1
10,FC岐阜,75,#75(5+80),小屋垣内(権太)農園,5,ソロ男子-3時間,1,0
10,FC岐阜,75,#75(5+80),小屋垣内(権太)農園,7,ファミリー-3時間,2,2
10,FC岐阜,75,#75(5+80),小屋垣内(権太)農園,8,一般-3時間,5,5
10,FC岐阜,75,#75(5+80),小屋垣内(権太)農園,9,お試し-3時間,1,0
10,FC岐阜,200,#200(15+15),穂積駅,5,ソロ男子-3時間,1,1
10,FC岐阜,201,#201(15+15),大垣駅,5,ソロ男子-3時間,1,1
10,FC岐阜,202,#202(15+15),関ケ原駅,5,ソロ男子-3時間,1,1
10,FC岐阜,204,#204(15+15),名古屋駅,5,ソロ男子-3時間,1,1
1 event_id event_name cp_number sub_loc_id location_name category_id category_name normal_checkins purchase_checkins
2 10 FC岐阜 -1 #-1(0) スタート(長良川競技場芝生広場) 5 ソロ男子-3時間 7 0
3 10 FC岐阜 -1 #-1(0) スタート(長良川競技場芝生広場) 6 ソロ女子-3時間 2 0
4 10 FC岐阜 -1 #-1(0) スタート(長良川競技場芝生広場) 7 ファミリー-3時間 2 0
5 10 FC岐阜 -1 #-1(0) スタート(長良川競技場芝生広場) 8 一般-3時間 8 0
6 10 FC岐阜 1 #1(35) 長良公園(枝広館跡) 8 一般-3時間 2 0
7 10 FC岐阜 3 #3(28) 長良川うかいミュージアム(岐阜市長良川鵜飼伝承館) 7 ファミリー-3時間 1 0
8 10 FC岐阜 3 #3(28) 長良川うかいミュージアム(岐阜市長良川鵜飼伝承館) 8 一般-3時間 4 0
9 10 FC岐阜 4 #4(15) 高橋尚子ゴールドメダル記念碑(足形) 5 ソロ男子-3時間 7 0
10 10 FC岐阜 4 #4(15) 高橋尚子ゴールドメダル記念碑(足形) 6 ソロ女子-3時間 1 0
11 10 FC岐阜 4 #4(15) 高橋尚子ゴールドメダル記念碑(足形) 7 ファミリー-3時間 2 0
12 10 FC岐阜 4 #4(15) 高橋尚子ゴールドメダル記念碑(足形) 8 一般-3時間 7 0
13 10 FC岐阜 4 #4(15) 高橋尚子ゴールドメダル記念碑(足形) 9 お試し-3時間 1 0
14 10 FC岐阜 5 #5(10) 崇福寺・稲葉一鉄寄贈の鐘楼 5 ソロ男子-3時間 5 0
15 10 FC岐阜 5 #5(10) 崇福寺・稲葉一鉄寄贈の鐘楼 6 ソロ女子-3時間 2 0
16 10 FC岐阜 5 #5(10) 崇福寺・稲葉一鉄寄贈の鐘楼 7 ファミリー-3時間 2 0
17 10 FC岐阜 5 #5(10) 崇福寺・稲葉一鉄寄贈の鐘楼 8 一般-3時間 6 0
18 10 FC岐阜 6 #6(40) 鷺山城跡 6 ソロ女子-3時間 1 0
19 10 FC岐阜 6 #6(40) 鷺山城跡 8 一般-3時間 2 0
20 10 FC岐阜 7 #7(30) 岐阜県立岐阜商業高等学校 5 ソロ男子-3時間 2 0
21 10 FC岐阜 7 #7(30) 岐阜県立岐阜商業高等学校 6 ソロ女子-3時間 1 0
22 10 FC岐阜 7 #7(30) 岐阜県立岐阜商業高等学校 8 一般-3時間 4 0
23 10 FC岐阜 8 #8(45+80) パティスリー kura 5 ソロ男子-3時間 2 1
24 10 FC岐阜 8 #8(45+80) パティスリー kura 8 一般-3時間 4 4
25 10 FC岐阜 9 #9(55) 大垣共立銀行 則武支店 5 ソロ男子-3時間 2 0
26 10 FC岐阜 9 #9(55) 大垣共立銀行 則武支店 8 一般-3時間 4 0
27 10 FC岐阜 10 #10(48+30) ポッカサッポロ自販機-BOOKOFF則武店 6 ソロ女子-3時間 1 1
28 10 FC岐阜 10 #10(48+30) ポッカサッポロ自販機-BOOKOFF則武店 8 一般-3時間 2 2
29 10 FC岐阜 11 #11(72) 御嶽神社茅萱宮 5 ソロ男子-3時間 1 0
30 10 FC岐阜 11 #11(72) 御嶽神社茅萱宮 6 ソロ女子-3時間 1 0
31 10 FC岐阜 12 #12(55) 眞中(みなか)神社 6 ソロ女子-3時間 1 0
32 10 FC岐阜 13 #13(60) 江口の鵜飼発祥の地/史跡 江口のわたし 5 ソロ男子-3時間 1 0
33 10 FC岐阜 13 #13(60) 江口の鵜飼発祥の地/史跡 江口のわたし 6 ソロ女子-3時間 1 0
34 10 FC岐阜 14 #14(85) 鏡島湊跡(かがみしまみなと) 5 ソロ男子-3時間 2 0
35 10 FC岐阜 14 #14(85) 鏡島湊跡(かがみしまみなと) 6 ソロ女子-3時間 1 0
36 10 FC岐阜 15 #15(45) 鏡島弘法(乙津寺) 5 ソロ男子-3時間 2 0
37 10 FC岐阜 15 #15(45) 鏡島弘法(乙津寺) 6 ソロ女子-3時間 1 0
38 10 FC岐阜 16 #16(65) 岐阜市立岐阜商業高等学校 5 ソロ男子-3時間 2 0
39 10 FC岐阜 17 #17(43) 立政寺 5 ソロ男子-3時間 2 0
40 10 FC岐阜 18 #18(35) 本莊神社 5 ソロ男子-3時間 2 0
41 10 FC岐阜 19 #19(40) 岐阜県美術館 5 ソロ男子-3時間 2 0
42 10 FC岐阜 20 #20(55+30) ポッカサッポロ自販機-大垣共立銀行エブリデープラザ 5 ソロ男子-3時間 2 2
43 10 FC岐阜 21 #21(62) 武藤嘉門爺像 5 ソロ男子-3時間 1 0
44 10 FC岐阜 23 #23(95) 岐阜県立岐阜総合学園高等学校 5 ソロ男子-3時間 1 0
45 10 FC岐阜 25 #25(76) 鶉田神社 5 ソロ男子-3時間 1 0
46 10 FC岐阜 26 #26(74) 茜部神社 5 ソロ男子-3時間 1 0
47 10 FC岐阜 33 #33(60) 馬頭観世音菩薩 5 ソロ男子-3時間 1 0
48 10 FC岐阜 33 #33(60) 馬頭観世音菩薩 6 ソロ女子-3時間 1 0
49 10 FC岐阜 34 #34(70) 陸上自衛隊 日野基本射撃場 6 ソロ女子-3時間 1 0
50 10 FC岐阜 37 #37(45+30) ポッカサッポロ自販機-セリア茜部店 5 ソロ男子-3時間 1 1
51 10 FC岐阜 38 #38(40) 比奈守神社 5 ソロ男子-3時間 1 0
52 10 FC岐阜 39 #39(35) 岐阜県立加納高等学校前バス停 5 ソロ男子-3時間 1 0
53 10 FC岐阜 41 #41(32) 中山道往来の松 5 ソロ男子-3時間 2 0
54 10 FC岐阜 42 #42(30) 問屋町ウォールアート 5 ソロ男子-3時間 4 0
55 10 FC岐阜 43 #43(22) 黄金の信長像 5 ソロ男子-3時間 4 0
56 10 FC岐阜 44 #44(25+80) 名鉄協商パーキング 岐阜第2 5 ソロ男子-3時間 2 0
57 10 FC岐阜 45 #45(30) 本荘公園 5 ソロ男子-3時間 1 0
58 10 FC岐阜 45 #45(30) 本荘公園 6 ソロ女子-3時間 1 0
59 10 FC岐阜 46 #46(30) 大縄場大橋公園 5 ソロ男子-3時間 2 0
60 10 FC岐阜 46 #46(30) 大縄場大橋公園 6 ソロ女子-3時間 1 0
61 10 FC岐阜 46 #46(30) 大縄場大橋公園 8 一般-3時間 1 0
62 10 FC岐阜 47 #47(25) 金神社/おもかる石 5 ソロ男子-3時間 4 0
63 10 FC岐阜 48 #48(46) OKB岐阜中央プラザ わくわくベースG 5 ソロ男子-3時間 8 0
64 10 FC岐阜 48 #48(46) OKB岐阜中央プラザ わくわくベースG 6 ソロ女子-3時間 1 0
65 10 FC岐阜 48 #48(46) OKB岐阜中央プラザ わくわくベースG 8 一般-3時間 1 0
66 10 FC岐阜 51 #51(20) 梅林公園 5 ソロ男子-3時間 1 0
67 10 FC岐阜 51 #51(20) 梅林公園 6 ソロ女子-3時間 1 0
68 10 FC岐阜 52 #52(60) 柳ヶ瀬FC岐阜勝ち神社 5 ソロ男子-3時間 7 0
69 10 FC岐阜 52 #52(60) 柳ヶ瀬FC岐阜勝ち神社 6 ソロ女子-3時間 1 0
70 10 FC岐阜 52 #52(60) 柳ヶ瀬FC岐阜勝ち神社 7 ファミリー-3時間 1 0
71 10 FC岐阜 52 #52(60) 柳ヶ瀬FC岐阜勝ち神社 8 一般-3時間 1 0
72 10 FC岐阜 53 #53(25) 美殿町の郵便ポスト 5 ソロ男子-3時間 5 0
73 10 FC岐阜 53 #53(25) 美殿町の郵便ポスト 6 ソロ女子-3時間 1 0
74 10 FC岐阜 53 #53(25) 美殿町の郵便ポスト 7 ファミリー-3時間 1 0
75 10 FC岐阜 53 #53(25) 美殿町の郵便ポスト 8 一般-3時間 1 0
76 10 FC岐阜 54 #54(150) 水道山展望台 5 ソロ男子-3時間 5 0
77 10 FC岐阜 54 #54(150) 水道山展望台 6 ソロ女子-3時間 1 0
78 10 FC岐阜 54 #54(150) 水道山展望台 7 ファミリー-3時間 1 0
79 10 FC岐阜 54 #54(150) 水道山展望台 8 一般-3時間 1 0
80 10 FC岐阜 55 #55(30) 岐阜新聞社 5 ソロ男子-3時間 4 0
81 10 FC岐阜 55 #55(30) 岐阜新聞社 7 ファミリー-3時間 1 0
82 10 FC岐阜 55 #55(30) 岐阜新聞社 8 一般-3時間 3 0
83 10 FC岐阜 56 #56(24) 弥八地蔵尊堂 5 ソロ男子-3時間 2 0
84 10 FC岐阜 56 #56(24) 弥八地蔵尊堂 7 ファミリー-3時間 1 0
85 10 FC岐阜 56 #56(24) 弥八地蔵尊堂 8 一般-3時間 1 0
86 10 FC岐阜 57 #57(25) 建勲神社 (岐阜 信長神社) 5 ソロ男子-3時間 5 0
87 10 FC岐阜 57 #57(25) 建勲神社 (岐阜 信長神社) 6 ソロ女子-3時間 1 0
88 10 FC岐阜 57 #57(25) 建勲神社 (岐阜 信長神社) 7 ファミリー-3時間 1 0
89 10 FC岐阜 58 #58(65) 伊奈波神社・黒龍神社龍頭石 7 ファミリー-3時間 2 0
90 10 FC岐阜 58 #58(65) 伊奈波神社・黒龍神社龍頭石 8 一般-3時間 2 0
91 10 FC岐阜 59 #59(12) 日下部邸跡・岐阜町本陣跡 5 ソロ男子-3時間 2 0
92 10 FC岐阜 59 #59(12) 日下部邸跡・岐阜町本陣跡 7 ファミリー-3時間 2 0
93 10 FC岐阜 59 #59(12) 日下部邸跡・岐阜町本陣跡 8 一般-3時間 3 0
94 10 FC岐阜 60 #60(25) メディアコスモスみんなの森 5 ソロ男子-3時間 1 0
95 10 FC岐阜 60 #60(25) メディアコスモスみんなの森 7 ファミリー-3時間 1 0
96 10 FC岐阜 60 #60(25) メディアコスモスみんなの森 8 一般-3時間 3 0
97 10 FC岐阜 61 #61(15+80) ナガラガワフレーバー 5 ソロ男子-3時間 1 0
98 10 FC岐阜 61 #61(15+80) ナガラガワフレーバー 7 ファミリー-3時間 2 2
99 10 FC岐阜 61 #61(15+80) ナガラガワフレーバー 8 一般-3時間 8 8
100 10 FC岐阜 62 #62(15) 庚申堂 5 ソロ男子-3時間 1 0
101 10 FC岐阜 62 #62(15) 庚申堂 7 ファミリー-3時間 2 0
102 10 FC岐阜 62 #62(15) 庚申堂 8 一般-3時間 7 0
103 10 FC岐阜 63 #63(15+80) 和菓子処 緑水庵 川原町店 5 ソロ男子-3時間 3 0
104 10 FC岐阜 63 #63(15+80) 和菓子処 緑水庵 川原町店 6 ソロ女子-3時間 1 0
105 10 FC岐阜 63 #63(15+80) 和菓子処 緑水庵 川原町店 7 ファミリー-3時間 2 1
106 10 FC岐阜 63 #63(15+80) 和菓子処 緑水庵 川原町店 8 一般-3時間 8 8
107 10 FC岐阜 63 #63(15+80) 和菓子処 緑水庵 川原町店 9 お試し-3時間 1 1
108 10 FC岐阜 64 #64(16) 日中友好庭園 5 ソロ男子-3時間 4 0
109 10 FC岐阜 64 #64(16) 日中友好庭園 6 ソロ女子-3時間 1 0
110 10 FC岐阜 64 #64(16) 日中友好庭園 7 ファミリー-3時間 2 0
111 10 FC岐阜 64 #64(16) 日中友好庭園 8 一般-3時間 8 0
112 10 FC岐阜 64 #64(16) 日中友好庭園 9 お試し-3時間 1 0
113 10 FC岐阜 65 #65(15) 板垣死すとも自由は死なず 5 ソロ男子-3時間 3 0
114 10 FC岐阜 65 #65(15) 板垣死すとも自由は死なず 7 ファミリー-3時間 2 0
115 10 FC岐阜 65 #65(15) 板垣死すとも自由は死なず 8 一般-3時間 6 0
116 10 FC岐阜 65 #65(15) 板垣死すとも自由は死なず 9 お試し-3時間 1 0
117 10 FC岐阜 66 #66(40) 岐阜大仏(正法寺) 5 ソロ男子-3時間 3 0
118 10 FC岐阜 66 #66(40) 岐阜大仏(正法寺) 7 ファミリー-3時間 2 0
119 10 FC岐阜 66 #66(40) 岐阜大仏(正法寺) 8 一般-3時間 3 0
120 10 FC岐阜 66 #66(40) 岐阜大仏(正法寺) 9 お試し-3時間 1 0
121 10 FC岐阜 67 #67(100) めいそうの小道:中間地点 5 ソロ男子-3時間 5 0
122 10 FC岐阜 67 #67(100) めいそうの小道:中間地点 6 ソロ女子-3時間 1 0
123 10 FC岐阜 67 #67(100) めいそうの小道:中間地点 7 ファミリー-3時間 2 0
124 10 FC岐阜 67 #67(100) めいそうの小道:中間地点 8 一般-3時間 3 0
125 10 FC岐阜 68 #68(160) 岐阜城 5 ソロ男子-3時間 4 0
126 10 FC岐阜 68 #68(160) 岐阜城 6 ソロ女子-3時間 1 0
127 10 FC岐阜 68 #68(160) 岐阜城 7 ファミリー-3時間 2 0
128 10 FC岐阜 68 #68(160) 岐阜城 8 一般-3時間 6 0
129 10 FC岐阜 68 #68(160) 岐阜城 9 お試し-3時間 1 0
130 10 FC岐阜 69 #69(150) 金華山展望デッキ 5 ソロ男子-3時間 5 0
131 10 FC岐阜 69 #69(150) 金華山展望デッキ 6 ソロ女子-3時間 1 0
132 10 FC岐阜 69 #69(150) 金華山展望デッキ 7 ファミリー-3時間 2 0
133 10 FC岐阜 69 #69(150) 金華山展望デッキ 8 一般-3時間 6 0
134 10 FC岐阜 70 #70(180) 七曲り登山道:岐阜城まで1000m 5 ソロ男子-3時間 5 0
135 10 FC岐阜 70 #70(180) 七曲り登山道:岐阜城まで1000m 6 ソロ女子-3時間 1 0
136 10 FC岐阜 70 #70(180) 七曲り登山道:岐阜城まで1000m 7 ファミリー-3時間 2 0
137 10 FC岐阜 70 #70(180) 七曲り登山道:岐阜城まで1000m 8 一般-3時間 5 0
138 10 FC岐阜 70 #70(180) 七曲り登山道:岐阜城まで1000m 9 お試し-3時間 1 0
139 10 FC岐阜 71 #71(5+5) 練習ポイント 5 ソロ男子-3時間 6 5
140 10 FC岐阜 71 #71(5+5) 練習ポイント 6 ソロ女子-3時間 2 2
141 10 FC岐阜 71 #71(5+5) 練習ポイント 7 ファミリー-3時間 1 1
142 10 FC岐阜 71 #71(5+5) 練習ポイント 8 一般-3時間 8 7
143 10 FC岐阜 71 #71(5+5) 練習ポイント 9 お試し-3時間 1 1
144 10 FC岐阜 72 #72(5+80) 岐阜ロゲコーヒー 5 ソロ男子-3時間 3 1
145 10 FC岐阜 72 #72(5+80) 岐阜ロゲコーヒー 6 ソロ女子-3時間 1 0
146 10 FC岐阜 72 #72(5+80) 岐阜ロゲコーヒー 7 ファミリー-3時間 1 1
147 10 FC岐阜 72 #72(5+80) 岐阜ロゲコーヒー 8 一般-3時間 4 3
148 10 FC岐阜 72 #72(5+80) 岐阜ロゲコーヒー 9 お試し-3時間 1 1
149 10 FC岐阜 73 #73(5+80) FC岐阜+岐阜バス 5 ソロ男子-3時間 6 1
150 10 FC岐阜 73 #73(5+80) FC岐阜+岐阜バス 8 一般-3時間 2 0
151 10 FC岐阜 73 #73(5+80) FC岐阜+岐阜バス 9 お試し-3時間 1 0
152 10 FC岐阜 74 #74(5+80) MKPポイントカード発行 5 ソロ男子-3時間 2 1
153 10 FC岐阜 74 #74(5+80) MKPポイントカード発行 6 ソロ女子-3時間 1 1
154 10 FC岐阜 74 #74(5+80) MKPポイントカード発行 7 ファミリー-3時間 1 1
155 10 FC岐阜 74 #74(5+80) MKPポイントカード発行 8 一般-3時間 7 3
156 10 FC岐阜 74 #74(5+80) MKPポイントカード発行 9 お試し-3時間 1 1
157 10 FC岐阜 75 #75(5+80) 小屋垣内(権太)農園 5 ソロ男子-3時間 1 0
158 10 FC岐阜 75 #75(5+80) 小屋垣内(権太)農園 7 ファミリー-3時間 2 2
159 10 FC岐阜 75 #75(5+80) 小屋垣内(権太)農園 8 一般-3時間 5 5
160 10 FC岐阜 75 #75(5+80) 小屋垣内(権太)農園 9 お試し-3時間 1 0
161 10 FC岐阜 200 #200(15+15) 穂積駅 5 ソロ男子-3時間 1 1
162 10 FC岐阜 201 #201(15+15) 大垣駅 5 ソロ男子-3時間 1 1
163 10 FC岐阜 202 #202(15+15) 関ケ原駅 5 ソロ男子-3時間 1 1
164 10 FC岐阜 204 #204(15+15) 名古屋駅 5 ソロ男子-3時間 1 1

69
config/fonts.conf Normal file
View File

@ -0,0 +1,69 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/usr/share/fonts</dir>
<!-- デフォルトのサンセリフフォントをIPAexGothicに設定 -->
<match target="pattern">
<test qual="any" name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexGothic</string>
</edit>
</match>
<!-- デフォルトのセリフフォントをIPAexMinchoに設定 -->
<match target="pattern">
<test qual="any" name="family">
<string>serif</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexMincho</string>
</edit>
</match>
<!-- MS Gothic の代替としてIPAexGothicを使用 -->
<match target="pattern">
<test name="family">
<string>MS Gothic</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexGothic</string>
</edit>
</match>
<!-- MS Mincho の代替としてIPAexMinchoを使用 -->
<match target="pattern">
<test name="family">
<string>MS Mincho</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexMincho</string>
</edit>
</match>
<!-- ビットマップフォントを無効化 -->
<match target="font">
<edit name="embeddedbitmap" mode="assign">
<bool>false</bool>
</edit>
</match>
<!-- フォントのヒンティング設定 -->
<match target="font">
<edit name="hintstyle" mode="assign">
<const>hintslight</const>
</edit>
<edit name="rgba" mode="assign">
<const>rgb</const>
</edit>
</match>
<!-- アンチエイリアス設定 -->
<match target="font">
<edit name="antialias" mode="assign">
<bool>true</bool>
</edit>
</match>
</fontconfig>

27
docbase/certificate.ini Normal file
View File

@ -0,0 +1,27 @@
[basic]
template_file=certificate_template.xlsx
doc_file=certificate_[zekken_number].xlsx
sections=section1
maxcol=10
column_width=3,5,16,16,16,20,16,8,8,12,3
output_path=media/reports/[event_code]
[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:K15
[section1.group2]
table_name=v_checkins_locations
where=zekken_number='[zekken_number]' and event_code='[event_code]'
sort=path_order
group_range=A16:J16

Binary file not shown.

View File

@ -1,5 +1,3 @@
version: "3.9"
services:
# postgres-db:
# image: kartoza/postgis:12.0
@ -22,11 +20,11 @@ services:
build:
context: .
dockerfile: Dockerfile.gdal
command: python3 manage.py runserver 0.0.0.0:8100
# command: python3 manage.py runserver 0.0.0.0:8100
volumes:
- .:/app
ports:
- 8100:8100
- 8000:8000
env_file:
- .env
restart: "on-failure"
@ -53,9 +51,11 @@ services:
- type: volume
source: nginx_logs
target: /var/log/nginx
- media_data:/app/media:ro
- type: bind
source: ./media
target: /usr/share/nginx/html/media
ports:
- "80:80"
- "8100:8100"
depends_on:
- api
networks:
@ -73,4 +73,3 @@ volumes:
geoserver-data:
static_volume:
nginx_logs:
media_data:

111
q Normal file
View File

@ -0,0 +1,111 @@
List of relations
Schema | Name | Type | Owner
----------+----------------------------------------+-------------------+-------
public | auth_group | table | admin
public | auth_group_id_seq | sequence | admin
public | auth_group_permissions | table | admin
public | auth_group_permissions_id_seq | sequence | admin
public | auth_permission | table | admin
public | auth_permission_id_seq | sequence | admin
public | authtoken_token | table | admin
public | django_admin_log | table | admin
public | django_admin_log_id_seq | sequence | admin
public | django_content_type | table | admin
public | django_content_type_id_seq | sequence | admin
public | django_migrations | table | admin
public | django_migrations_backup | table | admin
public | django_migrations_id_seq | sequence | admin
public | django_session | table | admin
public | geography_columns | view | admin
public | geometry_columns | view | admin
public | gifu_areas | table | admin
public | gifu_areas_id_seq | sequence | admin
public | gps_checkins | table | admin
public | gps_checkins_backup | table | admin
public | gps_checkins_id_seq | sequence | admin
public | jpn_admin_main_perf | table | admin
public | jpn_admin_main_perf_id_seq | sequence | admin
public | knox_authtoken | table | admin
public | mv_entry_details | materialized view | admin
public | raster_columns | view | admin
public | raster_overviews | view | admin
public | rog_category | table | admin
public | rog_checkinimages | table | admin
public | rog_checkinimages_id_seq | sequence | admin
public | rog_customuser | table | admin
public | rog_customuser_groups | table | admin
public | rog_customuser_groups_id_seq | sequence | admin
public | rog_customuser_id_seq | sequence | admin
public | rog_customuser_user_permissions | table | admin
public | rog_customuser_user_permissions_id_seq | sequence | admin
public | rog_entry | table | admin
public | rog_entry_id_seq | sequence | admin
public | rog_entrymember | table | admin
public | rog_entrymember_id_seq | sequence | admin
public | rog_event | table | admin
public | rog_event_id_seq | sequence | admin
public | rog_eventuser | table | admin
public | rog_eventuser_id_seq | sequence | admin
public | rog_favorite | table | admin
public | rog_favorite_id_seq | sequence | admin
public | rog_gifurogeregister | table | admin
public | rog_gifurogeregister_id_seq | sequence | admin
public | rog_goalimages | table | admin
public | rog_goalimages_id_seq | sequence | admin
public | rog_joinedevent | table | admin
public | rog_joinedevent_id_seq | sequence | admin
public | rog_location | table | admin
public | rog_location_id_seq | sequence | admin
public | rog_location_line | table | admin
public | rog_location_line_id_seq | sequence | admin
public | rog_location_polygon | table | admin
public | rog_location_polygon_id_seq | sequence | admin
public | rog_member | table | admin
public | rog_member_id_seq | sequence | admin
public | rog_newcategory | table | admin
public | rog_newcategory_id_seq | sequence | admin
public | rog_newevent | table | admin
public | rog_newevent2 | table | admin
public | rog_newevent2_id_seq | sequence | admin
public | rog_roguser | table | admin
public | rog_roguser_id_seq | sequence | admin
public | rog_shapefilelocations | table | admin
public | rog_shapefilelocations_id_seq | sequence | admin
public | rog_shapelayers | table | admin
public | rog_shapelayers_id_seq | sequence | admin
public | rog_systemsettings | table | admin
public | rog_systemsettings_id_seq | sequence | admin
public | rog_team | table | admin
public | rog_team_id_seq | sequence | admin
public | rog_templocation | table | admin
public | rog_templocation_id_seq | sequence | admin
public | rog_tempuser | table | admin
public | rog_tempuser_id_seq | sequence | admin
public | rog_testmodel | table | admin
public | rog_testmodel_id_seq | sequence | admin
public | rog_travellist | table | admin
public | rog_travellist_id_seq | sequence | admin
public | rog_travelpoint | table | admin
public | rog_travelpoint_id_seq | sequence | admin
public | rog_useractions | table | admin
public | rog_useractions_id_seq | sequence | admin
public | rog_usertracks | table | admin
public | rog_usertracks_id_seq | sequence | admin
public | rog_userupload | table | admin
public | rog_userupload_id_seq | sequence | admin
public | rog_useruploaduser | table | admin
public | rog_useruploaduser_id_seq | sequence | admin
public | spatial_ref_sys | table | admin
public | temp_gifuroge_team | table | admin
public | tmp_checkin | table | admin
public | tmp_checkpoint_table | table | admin
public | tmp_goalimage | table | admin
public | tmp_point | table | admin
public | v_category_rankings | view | admin
public | v_checkin_summary | view | admin
public | v_checkins_locations | view | admin
topology | layer | table | admin
topology | topology | table | admin
topology | topology_id_seq | sequence | admin
(106 rows)

BIN
rog/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -29,6 +29,24 @@ from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.utils.translation import gettext_lazy as _
from .services.csv_processor import EntryCSVProcessor
@admin.register(Entry)
class EntryAdmin(admin.ModelAdmin):
list_display = ['team', 'event', 'category', 'date', 'is_active']
def get_urls(self):
from django.urls import path
urls = super().get_urls()
custom_urls = [
path('upload-csv/', self.upload_csv_view, name='entry_upload_csv'),
]
return custom_urls + urls
def upload_csv_view(self, request):
processor = EntryCSVProcessor()
return processor.process_upload(request)
@admin.register(GifurogeRegister)
class GifurogeRegisterAdmin(admin.ModelAdmin):
list_display = ('event_code', 'time', 'owner_name', 'email', 'team_name', 'department')
@ -905,13 +923,16 @@ class CustomUserCreationForm(UserCreationForm):
model = CustomUser
fields = ('email', 'lastname', 'firstname', 'date_of_birth', 'female')
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm
add_form = CustomUserCreationForm
model = CustomUser
#model = CustomUser
list_display = ('email', 'is_staff', 'is_active', 'is_rogaining', 'zekken_number', 'event_code', 'team_name', 'group', 'firstname', 'lastname')
search_fields = ('egit mail', 'firstname', 'lastname', 'zekken_number')
list_filter = ('is_staff', 'is_active', 'is_rogaining', 'group')
ordering = ('email',)
# readonly_fieldsを明示的に設定
readonly_fields = ('date_joined',) # 変更不可のフィールドのみを指定=>Personal Infoも編集可能にする。
@ -942,40 +963,14 @@ class CustomUserAdmin(UserAdmin):
search_fields = ('email', 'firstname', 'lastname', 'zekken_number', 'team_name')
ordering = ('email',)
def get_readonly_fields(self, request, obj=None):
def get_readonly_fields_old(self, request, obj=None):
# スーパーユーザーの場合は読み取り専用フィールドを最小限に
if request.user.is_superuser:
return self.readonly_fields
# 通常のスタッフユーザーの場合は追加の制限を設定可能
return self.readonly_fields + ('is_staff', 'is_superuser')
admin.site.register(Useractions)
admin.site.register(RogUser, admin.ModelAdmin)
admin.site.register(Location, LocationAdmin)
admin.site.register(SystemSettings, admin.ModelAdmin)
admin.site.register(JoinedEvent, admin.ModelAdmin)
admin.site.register(Favorite, admin.ModelAdmin)
admin.site.register(TravelList, admin.ModelAdmin)
admin.site.register(TravelPoint, admin.ModelAdmin)
admin.site.register(Event, admin.ModelAdmin)
admin.site.register(Location_line, LeafletGeoAdmin)
admin.site.register(Location_polygon, LeafletGeoAdmin)
admin.site.register(JpnAdminMainPerf, LeafletGeoAdmin)
admin.site.register(UserTracks, LeafletGeoAdmin);
#admin.site.register(JpnAdminPerf, LeafletGeoAdmin)
admin.site.register(GifuAreas, LeafletGeoAdmin)
admin.site.register(ShapeLayers, admin.ModelAdmin)
admin.site.register(UserUpload, admin.ModelAdmin)
admin.site.register(EventUser, admin.ModelAdmin)
#admin.site.register(UserUploadUser, admin.ModelAdmin)
#admin.site.register(ShapeFileLocations, admin.ModelAdmin)
admin.site.register(CustomUser, UserAdminConfig)
admin.site.register(templocation, TempLocationAdmin)
admin.site.register(GoalImages, admin.ModelAdmin)
admin.site.register(CheckinImages, admin.ModelAdmin)
def get_readonly_fields(self, request, obj=None):
if request.user.is_superuser:
return ('date_joined', 'last_login')
return ('date_joined', 'last_login', 'is_staff', 'is_superuser')

View File

@ -310,6 +310,7 @@ class TempUser(models.Model):
class NewEvent2(models.Model):
event_name = models.CharField(max_length=255, unique=True)
event_description=models.TextField(max_length=255,blank=True, null=True)
start_datetime = models.DateTimeField(default=timezone.now)
end_datetime = models.DateTimeField()
deadlineDateTime = models.DateTimeField(null=True, blank=True)
@ -323,6 +324,8 @@ class NewEvent2(models.Model):
class_solo_male = models.BooleanField(default=True)
class_solo_female = models.BooleanField(default=True)
self_rogaining = models.BooleanField(default=False)
def __str__(self):
return f"{self.event_name} - From:{self.start_datetime} To:{self.end_datetime}"
@ -516,10 +519,15 @@ class EntryMember(models.Model):
class GoalImages(models.Model):
user=models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING)
goalimage = models.FileField(upload_to='goals/%y%m%d', blank=True, null=True)
goaltime = models.DateTimeField(_("Goal time"), auto_now=False, auto_now_add=False)
goaltime = models.DateTimeField(_("Goal time"), blank=True, null=True,auto_now=False, auto_now_add=False)
team_name = models.CharField(_("Team name"), max_length=255)
event_code = models.CharField(_("event code"), max_length=255)
cp_number = models.IntegerField(_("CP numner"))
zekken_number = models.TextField(
null=True, # False にする
blank=True, # False にする
help_text="ゼッケン番号"
)
class CheckinImages(models.Model):
user=models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING)
@ -530,6 +538,7 @@ class CheckinImages(models.Model):
cp_number = models.IntegerField(_("CP numner"))
class GpsCheckin(models.Model):
id = models.AutoField(primary_key=True) # 明示的にidフィールドを追加
path_order = models.IntegerField(
null=False,
help_text="チェックポイントの順序番号"
@ -538,6 +547,11 @@ class GpsCheckin(models.Model):
null=False,
help_text="ゼッケン番号"
)
event_id = models.IntegerField(
null=True,
blank=True,
help_text="イベントID"
)
event_code = models.TextField(
null=False,
help_text="イベントコード"
@ -623,19 +637,14 @@ 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}"
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):
# 作成時・更新時のタイムスタンプを自動設定

342
rog/postgres_views.sql Normal file
View File

@ -0,0 +1,342 @@
-- まず既存のビューをすべて削除
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_id,
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_id,event_code, zekken_number;
-- カテゴリー内ランキング計算用ビュー
CREATE VIEW v_category_rankings_old 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 OR REPLACE VIEW v_category_rankings AS
WITH completion_status AS (
SELECT
e.id,
e.event_id,
e.category_id,
CAST(e.zekken_number AS TEXT) as zekken_number,
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 completion_status,
COALESCE(cs.total_points, 0) as raw_points,
COALESCE(cs.normal_points, 0) as normal_points,
COALESCE(cs.bonus_points, 0) as bonus_points,
COALESCE(cs.penalty_points, 0) as original_penalty_points,
-- 遅刻ペナルティの計算1秒でも遅れたら、その分数に応じて-50点/分)
CASE
WHEN gi.goaltime > ev.end_datetime THEN
(CEIL(EXTRACT(EPOCH FROM (gi.goaltime - ev.end_datetime)) / 60)) * (-50)
ELSE 0
END as late_penalty_points,
gi.goaltime,
ev.end_datetime
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
LEFT JOIN rog_goalimages gi ON e.owner_id = gi.user_id
AND gi.event_code = ev.event_name
WHERE
e.is_active = true
),
points_calculation AS (
SELECT
*,
-- 総合ポイントの再計算(遅刻ペナルティを含む)
raw_points + late_penalty_points as total_points
FROM completion_status
),
valid_rankings AS (
-- 完走者のみを対象とした順位付け
SELECT
*,
DENSE_RANK() OVER (
PARTITION BY event_id, category_id
ORDER BY
total_points DESC,
CASE
WHEN completion_status = '完走' THEN 1
WHEN completion_status = '完走(遅刻)' THEN 2
END,
goaltime
) as valid_rank
FROM points_calculation
WHERE completion_status IN ('完走', '完走(遅刻)')
)
SELECT
cs.id,
cs.event_id,
cs.category_id,
cs.zekken_number,
cs.raw_points as original_total_points,
cs.normal_points,
cs.bonus_points,
cs.original_penalty_points,
CASE
WHEN cs.completion_status IN ('完走(遅刻)', '失格') AND cs.goaltime IS NOT NULL THEN cs.late_penalty_points
ELSE 0
END as late_penalty_points,
ROUND(pc.total_points) as total_points,
cs.completion_status,
CASE
WHEN cs.completion_status IN ('完走', '完走(遅刻)') THEN CAST(vr.valid_rank AS TEXT)
WHEN cs.completion_status = '失格' THEN '失格'
WHEN cs.completion_status = '棄権' THEN '棄権'
END as ranking,
COUNT(*) FILTER (WHERE cs.completion_status IN ('完走', '完走(遅刻)'))
OVER (PARTITION BY cs.event_id, cs.category_id) as total_valid_participants
FROM
completion_status cs
JOIN points_calculation pc ON cs.id = pc.id
LEFT JOIN valid_rankings vr ON cs.id = vr.id;
-- マテリアライズドビューの作成
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.event_description,
ev.start_datetime,
ev.end_datetime,
ev."deadlineDateTime",
TO_CHAR(ev.start_datetime::date, 'YYYY/MM/DD') as event_date,
-- カテゴリー情報
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.normal_points, 0) as normal_points,
COALESCE(cs.bonus_points, 0) as bonus_points,
COALESCE(cs.total_checkins, 0) as checkin_count,
COALESCE(cs.purchase_count, 0) as purchase_count,
cr.late_penalty_points as penalty_points, -- 遅刻ペナルティを使用
cr.total_points as total_points, -- v_category_rankingsの総合ポイントを使用
-- ゴール情報
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_valid_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 e.event_id = cs.event_id -- この行を変更
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 e.owner_id = gi.user_id
AND gi.event_code = ev.event_name -- ゴール情報の結合条件も修正
GROUP BY
e.id, e.zekken_number, e.is_active, e."hasParticipated", e."hasGoaled", e.date,
ev.event_name,ev.event_description, 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.original_total_points, cr.late_penalty_points, cr.total_points,
cr.completion_status, cr.ranking, cr.total_valid_participants,
gi.goalimage, gi.goaltime,
e.owner_id;
-- インデックスの再作成
CREATE UNIQUE INDEX idx_mv_entry_details_event_zekken
ON mv_entry_details(id, 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
AND l."group" LIKE '%' || g.event_code || '%'
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
-- チェックポイントごとの集計ビューを作成
DROP VIEW IF EXISTS v_checkpoint_summary CASCADE;
CREATE OR REPLACE VIEW v_checkpoint_summary AS
WITH checkpoint_counts AS (
SELECT
e.event_id,
ev.event_name,
gc.cp_number,
l.sub_loc_id,
l.location_name,
e.category_id,
nc.category_name,
COUNT(CASE
WHEN gc.validate_location = true AND gc.buy_flag = false
THEN 1
END) as normal_checkins,
COUNT(CASE
WHEN gc.validate_location = true AND gc.buy_flag = true
THEN 1
END) as purchase_checkins
FROM
rog_entry e
JOIN rog_newevent2 ev ON e.event_id = ev.id
JOIN rog_newcategory nc ON e.category_id = nc.id
JOIN gps_checkins gc ON ev.event_name = gc.event_code
AND CAST(e.zekken_number AS TEXT) = gc.zekken_number
LEFT JOIN rog_location l ON gc.cp_number = l.cp
AND l."group" LIKE '%' || gc.event_code || '%'
WHERE
e.is_active = true
AND gc.validate_location = true
GROUP BY
e.event_id,
ev.event_name,
gc.cp_number,
l.sub_loc_id,
l.location_name,
e.category_id,
nc.category_name
)
SELECT
event_id,
event_name,
cp_number,
sub_loc_id,
location_name,
category_id,
category_name,
normal_checkins,
purchase_checkins
FROM
checkpoint_counts
ORDER BY
event_name,
cp_number,
category_id;

View File

@ -27,6 +27,7 @@ from django.shortcuts import get_object_or_404
from django.utils import timezone
from datetime import datetime, date
logger = logging.getLogger(__name__)
class LocationCatSerializer(serializers.ModelSerializer):
@ -876,3 +877,37 @@ class UserLastGoalTimeSerializer(serializers.Serializer):
user_email = serializers.EmailField()
last_goal_time = serializers.DateTimeField()
class LoginUserSerializer_old(serializers.Serializer):
identifier = serializers.CharField(required=True) # メールアドレスまたはゼッケン番号
password = serializers.CharField(required=True)
def validate(self, data):
identifier = data.get('identifier')
password = data.get('password')
if not identifier or not password:
raise serializers.ValidationError('認証情報を入力してください。')
# ゼッケン番号かメールアドレスかを判定
if '@' in identifier:
# メールアドレスの場合
user = authenticate(username=identifier, password=password)
else:
# ゼッケン番号の場合
try:
# ゼッケン番号からユーザーを検索
user = CustomUser.objects.filter(zekken_number=identifier).first()
if user:
# パスワード認証
if not user.check_password(password):
user = None
except ValueError:
user = None
if user and user.is_active:
return user
elif user and not user.is_active:
raise serializers.ValidationError('アカウントが有効化されていません。')
else:
raise serializers.ValidationError('認証情報が正しくありません。')

View File

@ -0,0 +1,214 @@
# services/csv_processor.py
from typing import Dict, Any
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.hashers import make_password
from django.core.exceptions import ValidationError
from django.db import transaction
from datetime import timedelta
import csv
from ..models import CustomUser, Team, Member, NewCategory, Entry, NewEvent2
from ..utils.date_converter import DateConverter
from ..utils.name_splitter import NameSplitter
class EntryCSVProcessor:
def __init__(self):
self.date_converter = DateConverter()
self.name_splitter = NameSplitter()
def process_upload(self, request):
"""
CSVファイルのアップロードとデータ処理を行う
"""
if request.method == 'POST':
try:
if 'csv_file' not in request.FILES:
messages.error(request, 'No file was uploaded.')
return redirect('..')
csv_file = request.FILES['csv_file']
if not csv_file.name.endswith('.csv'):
messages.error(request, 'File is not CSV type')
return redirect('..')
# BOMを考慮してファイルを読み込む
file_content = csv_file.read()
if file_content.startswith(b'\xef\xbb\xbf'):
file_content = file_content[3:]
decoded_file = file_content.decode('utf-8').splitlines()
reader = csv.DictReader(decoded_file)
for row in reader:
try:
self.process_csv_row(row)
except Exception as e:
messages.error(request, f'Error in row: {str(e)}')
return redirect('..')
messages.success(request, 'CSV file processed successfully')
return redirect('..')
except Exception as e:
messages.error(request, f'Error processing CSV: {str(e)}')
return redirect('..')
return render(request, 'admin/entry/upload_csv.html')
def process_csv_row(self, row: Dict[str, Any]) -> None:
"""
CSVの1行のデータを処理する
"""
try:
with transaction.atomic():
# 1) ユーザーの作成/取得
user = self._get_or_create_user(row)
if not user:
raise ValidationError("Failed to create/get user")
# 2) チームの作成/取得とカテゴリの設定
team = self._get_or_create_team(row, user)
if not team:
raise ValidationError("Failed to create/get team")
# 3) メンバーの作成/更新
self._process_team_members(row, team, user)
# 4) エントリーの作成
self._create_entry(row, team, user)
except Exception as e:
raise ValidationError(f"Error processing row: {str(e)}")
def _get_or_create_user(self, row: Dict[str, Any]) -> CustomUser:
"""
メールアドレスでユーザーを検索し、存在しない場合は新規作成
"""
user = CustomUser.objects.filter(email=row['email']).first()
if not user:
last_name, first_name = self.name_splitter.split_full_name(row['owner_name'])
birth_date = self.date_converter.convert_date(row['owner_birthday'])
is_female = row['owner_sex'] in ['女性', '', '女子', 'female']
user = CustomUser.objects.create(
email=row['email'],
password=make_password(row['password']),
firstname=first_name,
lastname=last_name,
date_of_birth=birth_date,
female=is_female,
is_active=True
)
return user
def _get_or_create_team(self, row: Dict[str, Any], user: CustomUser) -> Team:
"""
チーム名でチームを検索し、存在しない場合は新規作成
既存チームの場合はメンバー構成を確認し、必要に応じて新バージョンを作成
"""
team_name = row['team_name']
base_team_name = team_name
version = 1
while Team.objects.filter(team_name=team_name).exists():
existing_team = Team.objects.get(team_name=team_name)
if self._check_same_members(existing_team, row, user):
return existing_team
version += 1
team_name = f"{base_team_name}_v{version}"
# 新規チームを作成
category = self._get_or_create_category(row)
team = Team.objects.create(
team_name=team_name,
owner=user,
category=category
)
return team
def _get_or_create_category(self, row: Dict[str, Any]) -> NewCategory:
"""
時間とデパートメントに基づいてカテゴリを取得または作成
"""
category_name = f"{row['department']}_{row['time']}h"
category, _ = NewCategory.objects.get_or_create(
category_name=category_name,
defaults={
'duration': timedelta(hours=int(row['time'])),
'num_of_member': int(row['members_count'])
}
)
return category
def _check_same_members(self, team: Team, row: Dict[str, Any], owner: CustomUser) -> bool:
"""
既存チームと新しいメンバー構成が同じかどうかをチェック
"""
existing_members = set(member.user.email for member in team.members.all())
new_members = {owner.email}
for i in range(2, int(row['members_count']) + 1):
if row.get(f'member{i}'):
new_members.add(f"dummy_{team.team_name}_{i}@example.com")
return existing_members == new_members
def _process_team_members(self, row: Dict[str, Any], team: Team, owner: CustomUser) -> None:
"""
チームメンバーを処理(オーナーとその他のメンバー)
"""
# オーナーをメンバーとして追加
Member.objects.get_or_create(
team=team,
user=owner,
defaults={'is_temporary': False}
)
# 追加メンバーの処理
for i in range(2, int(row['members_count']) + 1):
if row.get(f'member{i}'):
self._create_team_member(row, team, i)
def _create_team_member(self, row: Dict[str, Any], team: Team, member_num: int) -> None:
"""
チームの追加メンバーを作成
"""
last_name, first_name = self.name_splitter.split_full_name(row[f'member{member_num}'])
birth_date = self.date_converter.convert_date(row[f'birthday{member_num}'])
is_female = row.get(f'sex{member_num}', '') in ['女性', '', '女子', 'female']
dummy_email = f"dummy_{team.team_name}_{member_num}@example.com"
dummy_user, _ = CustomUser.objects.get_or_create(
email=dummy_email,
defaults={
'password': make_password('dummy_password'),
'firstname': first_name,
'lastname': last_name,
'date_of_birth': birth_date,
'female': is_female
}
)
Member.objects.get_or_create(
team=team,
user=dummy_user,
defaults={'is_temporary': True}
)
def _create_entry(self, row: Dict[str, Any], team: Team, owner: CustomUser) -> None:
"""
エントリーを作成
"""
try:
event = NewEvent2.objects.get(event_name=row['event_code'])
Entry.objects.create(
team=team,
event=event,
category=team.category,
date=event.start_datetime,
owner=owner,
is_active=False
)
except NewEvent2.DoesNotExist:
raise ValidationError(f"Event with code {row['event_code']} does not exist")

View File

@ -1,7 +1,7 @@
from sys import prefix
from rest_framework import urlpatterns
from rest_framework.routers import DefaultRouter
from .views import LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView, NewCategoryViewSet,LocationInBound2,UserLastGoalTimeView,TeamEntriesView,update_entry_status,get_events,get_zekken_numbers,get_team_info,get_checkins,update_checkins,export_excel,debug_urls
from .views import LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView, NewCategoryViewSet,LocationInBound2,UserLastGoalTimeView,TeamEntriesView,update_entry_status,get_events,get_zekken_numbers,get_team_info,get_checkins,update_checkins,export_excel,debug_urls,get_ranking, all_ranking_top3
from django.urls import path, include
@ -124,6 +124,13 @@ urlpatterns += [
path('export_excel/<int:zekken_number>/<str:event_code>/', views.export_excel, name='export_excel'),
# for Supervisor Web app
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'),
path('get-photolist/', views.get_photo_list, name='get-photolist'),
path('api/rankings/<str:event_code>/<str:category_name>/', get_ranking, name='get_ranking'),
path('api/rankings/top3/<str:event_code>/', all_ranking_top3, name='all_ranking_top3'),
]
if settings.DEBUG:

View File

@ -1,10 +1,13 @@
import os
from botocore.exceptions import ClientError
from django.template.loader import render_to_string
from django.conf import settings
import logging
import boto3
from django.core.mail import send_mail
from django.urls import reverse
import uuid
import environ
logger = logging.getLogger(__name__)
@ -57,7 +60,7 @@ def send_reset_password_email(email,activation_link):
#
def send_team_join_email(request,sender,user,team):
activation_link = request.build_absolute_uri(
reverse('activate-member', args=[user.id, team.id])
reverse('rog:activate-member', args=[user.id, team.id])
)
logger.info(f"request: {request}")
@ -81,7 +84,7 @@ def send_invitation_email(sender,request,user_email,team):
verification_code = uuid.uuid4() # UUIDを生成
activation_link = request.build_absolute_uri(
reverse('activate-new-member', args=[verification_code, team.id])
reverse('rog:activate-new-member', args=[verification_code, team.id])
)
@ -111,3 +114,267 @@ def send_invitaion_and_verification_email(user, team, activation_link):
subject, body = load_email_template('invitation_and_verification_email.txt', context)
share_send_email(subject,body,user.email)
class S3Bucket:
def __init__(self, bucket_name=None, aws_access_key_id=None, aws_secret_access_key=None, region_name=None):
self.aws_access_key_id = aws_access_key_id
self.aws_secret_access_key = aws_secret_access_key
self.region_name = region_name
self.bucket_name = bucket_name
self.s3_client = self.connect(bucket_name,aws_access_key_id, aws_secret_access_key, region_name)
def __str__(self):
return f"s3://{self.bucket_name}"
def __repr__(self):
return f"S3File(bucket_name={self.bucket_name})"
# AWS S3 への接続
def connect(self,bucket_name=None, aws_access_key_id=None, aws_secret_access_key=None, region_name=None):
"""
S3クライアントの作成
Args: .env から取得
aws_access_key_id (str, optional): AWSアクセスキーID
aws_secret_access_key (str, optional): AWSシークレットアクセスキー
region_name (str): AWSリージョン名
Returns:
boto3.client: S3クライアント
"""
try:
if aws_access_key_id and aws_secret_access_key:
s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name
)
else:
env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(env_file=".env")
if bucket_name==None:
bucket_name = env("S3_BUCKET_NAME")
aws_access_key_id = env("AWS_ACCESS_KEY")
aws_secret_access_key = env("AWS_SECRET_ACCESS_KEY")
region_name = env("S3_REGION")
s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name
)
return s3_client
except Exception as e:
logger.error(f"S3クライアントの作成に失敗しました: {str(e)}")
raise
def upload_file(self, file_path, s3_key=None):
"""
ファイルをS3バケットにアップロード
Args:
file_path (str): アップロードするローカルファイルのパス
bucket_name (str): アップロード先のS3バケット名
s3_key (str, optional): S3内でのファイルパス指定がない場合はファイル名を使用
s3_client (boto3.client, optional): S3クライアント
Returns:
bool: アップロードの成功・失敗
"""
logger = logging.getLogger(__name__)
try:
# S3キーが指定されていない場合は、ファイル名を使用
if s3_key is None:
s3_key = os.path.basename(file_path)
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect()
# ファイルのアップロード
logger.info(f"アップロード開始: {file_path} → s3://{self.bucket_name}/{s3_key}")
self.s3_client.upload_file(file_path, self.bucket_name, s3_key)
logger.info("アップロード完了")
return True
except FileNotFoundError:
logger.error(f"ファイルが見つかりません: {file_path}")
return False
except ClientError as e:
logger.error(f"S3アップロードエラー: {str(e)}")
return False
except Exception as e:
logger.error(f"予期しないエラーが発生しました: {str(e)}")
return False
def upload_directory(self, directory_path, prefix=''):
"""
ディレクトリ内のすべてのファイルをS3バケットにアップロード
Args:
directory_path (str): アップロードするローカルディレクトリのパス
bucket_name (str): アップロード先のS3バケット名
prefix (str, optional): S3内でのプレフィックスフォルダパス
s3_client (boto3.client, optional): S3クライアント
Returns:
tuple: (成功したファイル数, 失敗したファイル数)
"""
logger = logging.getLogger(__name__)
success_count = 0
failure_count = 0
try:
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect()
# ディレクトリ内のすべてのファイルを処理
for root, _, files in os.walk(directory_path):
for file in files:
local_path = os.path.join(root, file)
# S3キーの作成相対パスを維持
relative_path = os.path.relpath(local_path, directory_path)
s3_key = os.path.join(prefix, relative_path).replace('\\', '/')
# ファイルのアップロード
if self.upload_file(local_path, s3_key):
success_count += 1
else:
failure_count += 1
logger.info(f"アップロード完了: 成功 {success_count} 件, 失敗 {failure_count}")
return success_count, failure_count
except Exception as e:
logger.error(f"ディレクトリのアップロードに失敗しました: {str(e)}")
return success_count, failure_count
def download_file(self, s3_key, file_path):
"""
S3バケットからファイルをダウンロード
Args:
bucket_name (str): ダウンロード元のS3バケット名
s3_key (str): ダウンロードするファイルのS3キー
file_path (str): ダウンロード先のローカルファイルパス
s3_client (boto3.client, optional): S3クライアント
Returns:
bool: ダウンロードの成功・失敗
"""
logger = logging.getLogger(__name__)
try:
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect_to_s3()
# ファイルのダウンロード
logger.info(f"ダウンロード開始: s3://{self.bucket_name}/{s3_key}{file_path}")
self.s3_client.download_file(self.bucket_name, s3_key, file_path)
logger.info("ダウンロード完了")
return True
except FileNotFoundError:
logger.error(f"ファイルが見つかりません: s3://{self.bucket_name}/{s3_key}")
return False
except ClientError as e:
logger.error(f"S3ダウンロードエラー: {str(e)}")
return False
except Exception as e:
logger.error(f"予期しないエラーが発生しました: {str(e)}")
return False
def download_directory(self, prefix, directory_path):
"""
S3バケットからディレクトリをダウンロード
Args:
bucket_name (str): ダウンロード元のS3バケット名
prefix (str): ダウンロードするディレクトリのプレフィックス(フォルダパス)
directory_path (str): ダウンロード先のローカルディレクトリパス
s3_client (boto3.client, optional): S3クライアント
Returns:
tuple: (成功したファイル数, 失敗したファイル数)
"""
logger = logging.getLogger(__name__)
success_count = 0
failure_count = 0
try:
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect()
# プレフィックスに一致するオブジェクトをリスト
paginator = self.s3_client.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=prefix)
for page in pages:
if 'Contents' in page:
for obj in page['Contents']:
s3_key = obj['Key']
relative_path = os.path.relpath(s3_key, prefix)
local_path = os.path.join(directory_path, relative_path)
# ローカルディレクトリが存在しない場合は作成
local_dir = os.path.dirname(local_path)
if not os.path.exists(local_dir):
os.makedirs(local_dir)
# ファイルのダウンロード
if self.download_file(self.bucket_name, s3_key, local_path):
success_count += 1
else:
failure_count += 1
logger.info(f"ダウンロード完了: 成功 {success_count} 件, 失敗 {failure_count}")
return success_count, failure_count
except Exception as e:
logger.error(f"ディレクトリのダウンロードに失敗しました: {str(e)}")
return success_count, failure_count
def delete_object(self, s3_key):
"""
S3バケットからオブジェクトを削除
Args:
bucket_name (str): 削除するオブジェクトが存在するS3バケット名
s3_key (str): 削除するオブジェクトのS3キー
s3_client (boto3.client, optional): S3クライアント
Returns:
bool: 削除の成功・失敗
"""
logger = logging.getLogger(__name__)
try:
# S3クライアントが指定されていない場合は新規作成
if self.s3_client is None:
self.s3_client = self.connect()
# オブジェクトの削除
logger.info(f"削除開始: s3://{self.bucket_name}/{s3_key}")
self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key)
logger.info("削除完了")
return True
except ClientError as e:
logger.error(f"S3削除エラー: {str(e)}")
return False
except Exception as e:
logger.error(f"予期しないエラーが発生しました: {str(e)}")
return False

View File

@ -0,0 +1,83 @@
# utils/date_converter.py
from datetime import datetime, date
from typing import Optional
class DateConverter:
"""
日本語の日付文字列を扱うユーティリティクラス
"""
def convert_date(self, date_str: str) -> Optional[date]:
"""
日本語の日付文字列をdateオブジェクトに変換する
Args:
date_str: 変換する日付文字列(例: '1990年1月1日' or '1990-01-01' or '1990/01/01'
Returns:
変換されたdateオブジェクト。変換できない場合はNone
"""
if not date_str or date_str.strip() == '':
return None
try:
# 全角数字を半角数字に変換
date_str = date_str.translate(
str.maketrans('', '0123456789')
)
date_str = date_str.strip()
# 区切り文字の判定と分割
if '' in date_str:
# 年月日形式の場合
date_parts = date_str.replace('', '/').replace('', '/').replace('', '').split('/')
elif '/' in date_str:
# スラッシュ区切りの場合
date_parts = date_str.split('/')
elif '-' in date_str:
# ハイフン区切りの場合
date_parts = date_str.split('-')
else:
return None
# 部分の数を確認
if len(date_parts) != 3:
return None
year = int(date_parts[0])
month = int(date_parts[1])
day = int(date_parts[2])
# 簡単な妥当性チェック
if not (1900 <= year <= 2100):
return None
if not (1 <= month <= 12):
return None
if not (1 <= day <= 31):
return None
return date(year, month, day)
except (ValueError, IndexError, TypeError):
return None
def format_date(self, d: date, format_type: str = 'ja') -> str:
"""
dateオブジェクトを指定された形式の文字列に変換する
Args:
d: 変換するdateオブジェクト
format_type: 出力形式 ('ja': 日本語形式, 'iso': ISO形式)
Returns:
変換された日付文字列
"""
if not isinstance(d, date):
return ''
if format_type == 'ja':
return f"{d.year}{d.month}{d.day}"
elif format_type == 'iso':
return d.isoformat()
else:
return str(d)

119
rog/utils/name_splitter.py Normal file
View File

@ -0,0 +1,119 @@
# utils/name_splitter.py
from typing import Tuple
class NameSplitter:
"""
日本語の氏名を扱うユーティリティクラス
"""
def split_full_name(self, full_name: str) -> Tuple[str, str]:
"""
フルネームを姓と名に分割する
Args:
full_name: 分割する氏名(例: '山田 太郎' or '山田 太郎' or 'Yamada Taro'
Returns:
(姓, 名)のタプル。分割できない場合は(フルネーム, '')を返す
"""
if not full_name:
return ('', '')
try:
# 空白文字で分割(半角スペース、全角スペース、タブなど)
parts = full_name.replace(' ', ' ').split()
if len(parts) >= 2:
last_name = parts[0]
first_name = ' '.join(parts[1:]) # 名が複数単語の場合に対応
return (last_name.strip(), first_name.strip())
else:
# 分割できない場合は全体を姓とする
return (full_name.strip(), '')
except Exception:
return (full_name.strip(), '')
def join_name(self, last_name: str, first_name: str, format_type: str = 'ja') -> str:
"""
姓と名を結合して一つの文字列にする
Args:
last_name: 姓
first_name: 名
format_type: 出力形式 ('ja': 日本語形式, 'en': 英語形式)
Returns:
結合された氏名文字列
"""
last_name = last_name.strip()
first_name = first_name.strip()
if not last_name and not first_name:
return ''
if not first_name:
return last_name
if not last_name:
return first_name
if format_type == 'ja':
return f"{last_name} {first_name}" # 全角スペース
elif format_type == 'en':
return f"{first_name} {last_name}" # 英語形式:名 姓
else:
return f"{last_name} {first_name}" # デフォルト:半角スペース
def normalize_name(self, name: str) -> str:
"""
名前の正規化を行う
Args:
name: 正規化する名前文字列
Returns:
正規化された名前文字列
"""
if not name:
return ''
# 空白文字の正規化
name = ' '.join(name.split()) # 連続する空白を単一の半角スペースに
# 全角英数字を半角に変換
name = name.translate(str.maketrans({
' ': ' ', # 全角スペースを半角に
'': '.',
'': ',',
'': '!',
'': '?',
'': ':',
'': ';',
}))
return name.strip()
def is_valid_name(self, name: str) -> bool:
"""
名前が有効かどうかをチェックする
Args:
name: チェックする名前文字列
Returns:
名前が有効な場合はTrue、そうでない場合はFalse
"""
if not name or not name.strip():
return False
# 最小文字数チェック
if len(name.strip()) < 2:
return False
# 記号のチェック(一般的でない記号が含まれていないか)
invalid_chars = set('!@#$%^&*()_+=<>?/\\|~`')
if any(char in invalid_chars for char in name):
return False
return True

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,344 @@
<!DOCTYPE html>
<html>
<head>
<title>ランキング</title>
<style>
.box2 { margin: 10px; padding: 10px; border: 1px solid #ccc; }
.best3 { margin: 5px 0; padding: 5px; }
.span2 { margin-left: 20px; }
.span3 { font-weight: bold; }
.span6 { display: inline-block; width: 30px; }
.black { background-color: #f0f0f0; padding: 10px; margin-bottom: 20px; }
.arrow { margin: 10px 0; }
.arrow2 { margin: 10px 0; }
.disqualified { color: #999; }
.status {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
margin-left: 8px;
}
.status-retired {
background-color: #ffebee;
color: #c62828;
}
.status-finished {
background-color: #e8f5e9;
color: #2e7d32;
}
.status-running {
background-color: #e3f2fd;
color: #1565c0;
}
select {
padding: 8px;
margin: 5px 0;
min-width: 200px;
}
button {
padding: 8px 16px;
margin: 10px 0;
background-color: #4a90e2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #357abd;
}
</style>
</head>
<body>
<div id="ranking">
<div class="black">
<span class="span3"></span>
<span style="font-size: 24px">ランキング</span>
</div>
<div class="arrow">
<div>イベントを選択してください</div>
<select id="eventSelect">
<option value="">イベントを選択してください</option>
</select>
</div>
<div class="arrow2">
<div>クラスを選択してください</div>
<select id="classSelect" disabled>
<option value="">クラスを選択してください</option>
</select>
</div>
<button id="toggleButton" onclick="toggleView()">TOP3表示</button>
<div id="normalRanking">
<div id="teamList"></div>
</div>
<div id="top3Ranking" style="display: none;">
<div id="top3List"></div>
</div>
</div>
<script>
const API_BASE_URL = 'https://rogaining.sumasen.net';
let showTop3 = false;
// ページ読み込み時にイベント一覧を取得
document.addEventListener('DOMContentLoaded', async () => {
await loadEvents();
setupEventListeners();
});
// 日時のフォーマット
function formatDateTime(dateStr) {
if (!dateStr) return '未ゴール';
const date = new Date(dateStr);
return date.toLocaleString('ja-JP', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// イベントリスナーの設定
function setupEventListeners() {
document.getElementById('eventSelect').addEventListener('change', async function() {
const eventCode = this.value;
if (eventCode) {
await loadClasses(eventCode);
await updateRankings();
}
});
document.getElementById('classSelect').addEventListener('change', async function() {
await updateRankings();
});
}
// イベント一覧の取得と表示
async function loadEvents() {
try {
const response = await fetch(`${API_BASE_URL}/api/newevent2/`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
mode: 'cors'
});
const events = await response.json();
const select = document.getElementById('eventSelect');
events.filter(event => event.public).forEach(event => {
const option = document.createElement('option');
option.value = event.event_name;
option.textContent = event.event_name;
select.appendChild(option);
});
} catch (error) {
console.error('イベント一覧の取得に失敗:', error);
}
}
// クラス一覧の取得と表示
async function loadClasses(eventCode) {
try {
const response = await fetch(`${API_BASE_URL}/api/categories/${eventCode}/`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
mode: 'cors'
});
const classes = await response.json();
const select = document.getElementById('classSelect');
select.innerHTML = '<option value="">クラスを選択してください</option>';
select.disabled = false;
classes.forEach(cls => {
const option = document.createElement('option');
option.value = cls.category_name;
option.textContent = cls.category_name;
select.appendChild(option);
});
} catch (error) {
console.error('クラス一覧の取得に失敗:', error);
}
}
// ランキングの更新
async function updateRankings() {
const eventCode = document.getElementById('eventSelect').value;
if (!eventCode) return;
try {
if (showTop3) {
await loadTop3Rankings(eventCode);
} else {
const classCode = document.getElementById('classSelect').value;
if (classCode) {
await loadClassRankings(eventCode, classCode);
}
}
} catch (error) {
console.error('ランキングの取得に失敗:', error);
}
}
// クラス別ランキングの表示
async function loadClassRankings(eventCode, classCode) {
const response = await fetch(`${API_BASE_URL}/api/rankings/${eventCode}/${classCode}/`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
mode: 'cors'
});
const rankingData = await response.json();
displayNormalRankings(rankingData);
}
// TOP3ランキングの表示
async function loadTop3Rankings(eventCode) {
const response = await fetch(`${API_BASE_URL}/api/rankings/top3/${eventCode}/`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
mode: 'cors'
});
const rankingData = await response.json();
displayTop3Rankings(rankingData);
}
// 通常ランキングの表示処理
function displayNormalRankings(rankingData) {
const container = document.getElementById('teamList');
container.innerHTML = '';
// 有効なランキングの表示
rankingData.rankings.forEach((team, index) => {
const div = document.createElement('div');
div.className = 'best';
const statusClass = team.status === '棄権' ? 'status-retired' :
team.status === 'ゴール' ? 'status-finished' : 'status-running';
div.innerHTML = `
${index + 1}. ${team.team_name} (${team.zekken_number})
<span class="span2">合計得点:${team.final_point}</span>
<span class="span2">獲得ポイント:${team.point}</span>
<span class="span2">遅刻減点: ${team.late_point}</span>
<span class="span2">最終更新: ${formatDateTime(team.last_checkin)}</span>
<span class="status ${statusClass}">(${team.status})</span>
`;
container.appendChild(div);
});
// 失格チームの表示
if (rankingData.disqualified && rankingData.disqualified.length > 0) {
const disqHeader = document.createElement('div');
disqHeader.className = 'disqualified-header';
disqHeader.innerHTML = '<h3>失格チーム</h3>';
container.appendChild(disqHeader);
rankingData.disqualified.forEach(team => {
const div = document.createElement('div');
div.className = 'best disqualified';
const statusClass = team.status === '棄権' ? 'status-retired' :
team.status === 'ゴール' ? 'status-finished' : 'status-running';
div.innerHTML = `
${team.team_name} (${team.zekken_number})
<span class="span2">獲得ポイント:${team.point}</span>
<span class="span2">理由: ${team.reason}</span>
<span class="span2">最終更新: ${formatDateTime(team.last_checkin)}</span>
<span class="status ${statusClass}">(${team.status})</span>
`;
container.appendChild(div);
});
}
}
// TOP3ランキングの表示処理
function displayTop3Rankings(rankingData) {
const container = document.getElementById('top3List');
container.innerHTML = '';
Object.entries(rankingData).forEach(([category, data]) => {
const categoryDiv = document.createElement('div');
categoryDiv.className = 'box2';
const categoryHeader = document.createElement('h3');
categoryHeader.textContent = category;
categoryDiv.appendChild(categoryHeader);
// 有効なランキングの表示
data.rankings.forEach((team, index) => {
const teamDiv = document.createElement('div');
teamDiv.className = 'best3';
const statusClass = team.status === '棄権' ? 'status-retired' :
team.status === 'ゴール' ? 'status-finished' : 'status-running';
teamDiv.innerHTML = `
<span class="span6">${index + 1}</span>
${team.team_name} (${team.zekken_number})
<span class="status ${statusClass}">(${team.status})</span><br>
合計得点:${team.final_point} (獲得:${team.point} 減点:${team.late_point})<br>
最終更新: ${formatDateTime(team.last_checkin)}
`;
categoryDiv.appendChild(teamDiv);
});
// 失格チームの表示
if (data.disqualified && data.disqualified.length > 0) {
const disqHeader = document.createElement('div');
disqHeader.className = 'disqualified-header';
disqHeader.innerHTML = '<h4>失格チーム</h4>';
categoryDiv.appendChild(disqHeader);
data.disqualified.forEach(team => {
const teamDiv = document.createElement('div');
teamDiv.className = 'best3 disqualified';
const statusClass = team.status === '棄権' ? 'status-retired' :
team.status === 'ゴール' ? 'status-finished' : 'status-running';
teamDiv.innerHTML = `
${team.team_name} (${team.zekken_number})<br>
獲得ポイント:${team.point} 理由:${team.reason}<br>
最終更新: ${formatDateTime(team.last_checkin)}
<span class="status ${statusClass}">(${team.status})</span>
`;
categoryDiv.appendChild(teamDiv);
});
}
container.appendChild(categoryDiv);
});
}
// 表示モードの切り替え
function toggleView() {
showTop3 = !showTop3;
const button = document.getElementById('toggleButton');
const normalRanking = document.getElementById('normalRanking');
const top3Ranking = document.getElementById('top3Ranking');
const classSelect = document.getElementById('classSelect');
button.textContent = showTop3 ? 'クラス別ランキング' : 'TOP3表示';
normalRanking.style.display = showTop3 ? 'none' : 'block';
top3Ranking.style.display = showTop3 ? 'block' : 'none';
classSelect.disabled = showTop3;
updateRankings();
}
</script>
</body>
</html>

View File

@ -0,0 +1,486 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ランキング</title>
<style>
.flex-slave {
margin: 10px;
}
#map {
width: 800px;
height: 600px;
}
#best{
position: relative;
line-height: 1.5em;
background: #fff;
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .5);
margin: 0 auto;
margin-top: 20px;
border-radius:8px;
counter-increment: count-ex1-5;
content: counter(number);
padding: 0px 10px 10px 30px;
margin:0px 0px 0px 5px;
}
#best::marker{
font-weight:bold;
color: #ff0801;
}
#best li{
counter-increment: count-ex1-5;
content: counter(number);
padding: 8px 10px 0px 0px;
margin:8px 0px 0px 8px;
line-height: 2em;
}
li::marker{
font-weight:bold;
color: #ff0801;
line-height: 1;
}
#best::before {
position: absolute;
background: #ff0801;
color: #FFF;
font-size: 15px;
border-radius: 50%;
left: 0;
width: 23px;
height: 23px;
line-height: 22px;
text-align: center;
top: 5px;
font-family: "Font Awesome 5 Free";
content: "\f521";
font-weight: 900;
margin:6px 0px 0px 10px;
}
.black{
background-color: #000;
}
h5 {
position: relative;
padding: 0px 0px 0px 70px;
background-image: linear-gradient(0deg, #b8751e 0%, #ffce08 37%, #fefeb2 47%, #fafad6 50%, #fefeb2 53%, #e1ce08 63%, #b8751e 100%);
-webkit-background-clip: text;
color: transparent;
display:flex;
align-items: center;
height:60px;
}
h5 .span3 {
position: absolute;
top: -10px;
left: 0px;
display: inline-block;
width: 52px;
height: px;
text-align: center;
background: #fa4141;
}
h5 .span3:before,
h5 .span3:after {
position: absolute;
content: '';
}
h5 .span3:before {
right: -10px;
width: 0;
height: 0;
border-right: 10px solid transparent;
border-bottom: 10px solid #d90606;
}
h5 .span3:after {
top: 50%;
left: 0;
display: block;
height: %;
border: 26px solid #fa4141;
border-bottom-width: 15px;
border-bottom-color: transparent;
}
h5 .span3 i {
position: relative;
z-index: 1;
color: #fff100;
padding-top: 10px;
font-size: 24px;
}
.best
select.name{
display: block;
margin-bottom: -10px;
}
.best2 li{
position: relative;
overflow: hidden;
padding: 1.5rem 2rem 1.5rem 130px;
word-break: break-all;
border-top: 3px solid #000;
border-radius: 12px 0 0 0;
margin: 10px 0px 0px 0px;
}
.best2 span{
font-size: 40px;
font-size: 4rem;
position: absolute;
top: 0;
left: 0;
display: block;
padding: 3px 20px;
color: #fff;
border-radius: 10px 0 20px 10px;
background: #000;
}
.button3{
color: #fff;
border: 2px solid #fff;
border-radius: 0;
background-image: -webkit-linear-gradient(left, #fa709a 0%, #fee140 100%);
background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
-webkit-box-shadow: 0 5px 5px rgba(0, 0, 0, .1);
box-shadow: 0 3px 5px rgba(0, 0, 0, .1);
border-radius: 100vh;
font-family: "Arial", "メイリオ";
/*letter-spacing: 0.1em;*/
padding: 7px 25px 7px 25px;
}
.button3:hover{
-webkit-transform: translate(0, -2px);
transform: translate(0, -2px);
color: #fff;
-webkit-box-shadow: 0 8px 15px rgba(0, 0, 0, .2);
box-shadow: 0 8px 15px rgba(0, 0, 0, .2);
font-family: "Arial", "メイリオ";
}
#best::before {
position: absolute;
background: #ff0801;
color: #FFF;
font-size: 15px;
border-radius: 50%;
left: 0;
width: 23px;
height: 23px;
line-height: 22px;
text-align: center;
top: 5px;
font-family: "Font Awesome 5 Free";
content: "\f521";
font-weight: 900;
margin:6px 0px 0px 10px;
}
select {
display: block;
border-left: solid 10px #27acd9;
padding: 0.75rem 1.5rem;
border-color: transparent transparent transparent blue;
font-weight: bold;
box-shadow: 3px 5px 3px -2px #aaaaaa,3px 3px 2px 0px #ffffff inset;
margin-bottom: 10px;
width: 250px;
}
.box2{
position: relative;
line-height: 1.5em;
background: #fff;
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .5);
margin: 0 auto;
margin-top: 20px;
border-radius:8px;
counter-increment: count-ex1-5;
content: counter(number);
padding: 10px 10px 10px 30px;
margin:10px 0px 0px 5px;
}
.best2::before {
position: absolute;
background: #ff0801;
color: #FFF;
font-size: 15px;
border-radius: 50%;
left: 0;
width: 23px;
height: 23px;
line-height: 22px;
text-align: center;
top: 5px;
font-family: "Font Awesome 5 Free";
content: "\f521";
font-weight: 900;
margin: 10px 0px 0px 10px;
}
h3{
margin: 3px 0px 0px 10px;
}
@media screen and (max-width: 767px) {
#best{
position: relative;
line-height: 1.5em;
background: #fff;
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .5);
margin: 0 auto;
margin-top: 20px;
border-radius:8px;
counter-increment: count-ex1-5;
content: counter(number);
padding: 0px 10px 10px 30px;
margin:0px 0px 0px 5px;
}
#best::marker{
font-weight:bold;
color: #ff0801;
}
#best li{
counter-increment: count-ex1-5;
content: counter(number);
padding: 8px 10px 0px 0px;
margin:8px 0px 0px 8px;
line-height: 2em;
margin: 10px 0px 0px 0px;
}
li::marker{
font-weight:bold;
color: #ff0801;
line-height: 1;
}
select{
width:100%;
}
.button3{
width: 100%;
text-align: center;
font-size: 24px;
}
select.name{
margin-bottom: -20px;
margin-top: -10px;
}
.arrow{
position: relative;
}
.arrow::after {
color: #828282;
position: absolute;
top:18px; /* 矢印の位置 */
right: 25px; /* 矢印の位置 */
width: 13px; /* 矢印の大きさ */
height: 13px; /* 矢印の大きさ */
border-top: 3px solid #58504A; /* 矢印の線 */
border-right: 3px solid #58504A; /* 矢印の線 */
-webkit-transform: rotate(135deg); /* 矢印の傾き */
transform: rotate(135deg); /* 矢印の傾き */
pointer-events: none; /* 矢印部分もクリック可能にする */
content: "";
border-color: #828282;
}
.arrow2{
position: relative;
}
.arrow2::after {
color: #828282;
position: absolute;
top:20px; /* 矢印の位置 */
right: 25px; /* 矢印の位置 */
width: 13px; /* 矢印の大きさ */
height: 13px; /* 矢印の大きさ */
border-top: 3px solid #58504A; /* 矢印の線 */
border-right: 3px solid #58504A; /* 矢印の線 */
-webkit-transform: rotate(135deg); /* 矢印の傾き */
transform: rotate(135deg); /* 矢印の傾き */
pointer-events: none; /* 矢印部分もクリック可能にする */
content: "";
border-color: #828282;
}
.score {
text-align: right;
}
</style>
</head>
<body>
<div id="ranking">
<div class="black">
<h5><span class="span3"><i class="fas fa-crown"></i></span>
<span style="font-size: 24px">ランキング</span></h5>
</div>
<form @submit.prevent="ranking_view">
<div class="arrow">
<select class="name" v-model="selectedEvent">
<option disabled value="">イベント一覧</option>
<option selected value="FC岐阜">with FC岐阜</option>
<!--
<option value="関ケ原2410">関ケ原-2024年10月</option>
<option value="養老2410">養老-2024年10月</option>
<option value="大垣2410">大垣-2024年10月</option>
<option value="各務原2410">各務原-2024年10月</option>
<option value="多治見2410">多治見-2024年10月</option>
<option value="美濃加茂2410">美濃加茂-2024年10月</option>
<option value="下呂2410">下呂-2024年10月</option>
<option value="郡上2410">郡上-2024年10月</option>
<option value="高山2410">高山-2024年10月</option>
<option value="関ケ原2409">関ケ原-2024年9月</option>
<option value="養老2409">養老-2024年9月</option>
<option value="大垣2409">大垣-2024年9月</option>
<option value="各務原2409">各務原-2024年9月</option>
<option value="多治見2409">多治見-2024年9月</option>
<option value="美濃加茂2409">美濃加茂-2024年9月</option>
<option value="下呂2409">下呂-2024年9月</option>
<option value="郡上2409">郡上-2024年9月</option>
<option value="高山2409">高山-2024年9月</option>
-->
<option value="美濃加茂">岐阜ロゲin美濃加茂</option>
<option value="養老ロゲ">養老町</option>
<option value="岐阜市">岐阜市</option>
<option value="大垣2">岐阜ロゲin大垣@イオンモール大垣</option>
<option value="大垣">岐阜ロゲin大垣</option>
<option value="多治見">岐阜ロゲin多治見</option>
<option value="各務原">岐阜ロゲin各務原</option>
<option value="下呂">岐阜ロゲin下呂温泉</option>
<option value="郡上">岐阜ロゲin郡上</option>
<option value="高山">岐阜ロゲin高山</option>
</select>
<div class="arrow2">
<select v-model="selectedClass">
<option selected value="top3">top3</option>
<option value="3時間一般">3時間一般</option>
<option value="3時間ファミリー">3時間ファミリー</option>
<option value="3時間自転車">3時間自転車</option>
<option value="3時間ソロ男子">3時間ソロ男子</option>
<option value="3時間ソロ女子">3時間ソロ女子</option>
<option value="3時間パラロゲ">3時間パラロゲ</option>
<option value="5時間一般">5時間一般</option>
<option value="5時間ファミリー">5時間ファミリー</option>
<option value="5時間自転車">5時間自転車</option>
<option value="5時間ソロ男子">5時間ソロ男子</option>
<option value="5時間ソロ女子">5時間ソロ女子</option>
</div>
</select>
<button class="button3" type="submit">CLICK</button>
</div>
</form>
<ol v-if="top_three_flag == false" >
<div id="best" v-for="team in team_list">
<li>
{{ team.team_name }}({{ team.zekken_number }})<br/><p class="score"><span class="span2">合計得点:{{ team.point }}</span> <span class="span2">内減点: {{ team.late_point }}</span></p>
</li>
</div>
</ol>
<div v-if="top_three_flag == true">
<div class="box2" v-for="(teams, index) in team_list">
<h3> {{ index }} </h3>
<ol>
<div class="best3" v-for="team in teams">
<span class="span6"><li class="best2"></span>
{{ team.team_name }}({{ team.zekken_number }})<br/><p class="score">合計得点:{{ team.point }} 内減点: {{ team.late_point }}</p>
</li>
</ol>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.1/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
axios.defaults.baseURL = 'https://rogaining.sumasen.net/gifuroge';
axios.default.withCridentials = true;
(function() {
'use strict';
var vm = new Vue({
el: '#ranking',
data: {
selectedEvent: "FC岐阜",
selectedClass: "top3",
team_list: [],
top_three_flag: false,
three_pop: [],
interval: 1
},
mounted : function(){
var int = this.interval * 60 * 1000
setInterval(function() {this.ranking_view()}.bind(this), int);
},
methods: {
ranking_view: function(){
if (this.selectedClass == 'top3'){
this.top_three_flag = true
var url = "/all_ranking_top3?event=" + this.selectedEvent
axios
.get(url)
.then(response => ( this.team_list = response.data))}
else {
this.top_three_flag = false
var url = "/get_ranking?class=" + this.selectedClass + '&event=' + this.selectedEvent
axios
.get(url)
.then(response => ( this.team_list = response.data ))}
}
}
});
})();
</script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,293 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>岐阜ロゲwith FC岐阜 Myアルバム|岐阜aiネットワーク</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="./css/reset.css">
<link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">
<script src="https://kit.fontawesome.com/94e0c17dd1.js" crossorigin="anonymous"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap" rel="stylesheet">
<!-- drawer.css -->
<link rel="stylesheet" href="./css/drawer.min.css">
<!-- Meta -->
<meta name="description" content="岐阜ロゲin岐阜市のMyアルバムページです。">
<meta name="keywords" content="FC岐阜ロゲイニング map 岐阜aiネットワーク ran">
<meta name="robot" content="index,follow,noarchive">
<meta name="author" content="岐阜aiネットワーク">
<meta name="language" content="ja">
<!-- Favicon -->
<link rel="shortcut icon" href="favicon.png">
<style>
/* ここにCSSスタイルを記述 */
.view {
max-width: 1000px;
margin: 50px auto 20px;
padding: 0 20px;
}
section.view input,
button,
select {
font-size: 16px;
}
select,input{
padding: 4px;
}
div#photoList {
display: flex;
flex-wrap: wrap;
max-width: 1200px;
/* margin: 0 10px; */
margin: 0 auto;
}
.event-photo {
width: 100%;
max-width: calc(32.33% - 2px);
margin: 5px;
/* その他のスタイル */
}
.viewtop{
min-height: calc(50vh - 50px);
}
@media screen and (max-width:768px) {
.event-photo {
width: 100%;
max-width: calc(31% - 2px);
margin: 5px;
/* その他のスタイル */
}
.viewtop{
min-height: auto;
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<header>
<div id="headerContent">
<div id="sp_headerContent">
<h1><a href="https://www.gifuai.net/"><img src="./img/rogo.png"></a></h1>
<a id="headerMenuBtn" href="#"><img src="./img/menu_open.svg" alt="メニューを開く"></a>
</div><!--sp_headerContent-->
<div id="headerMenu">
<h2>メニュー<a id="headerMenuClose" href="#"><img class="close" src="./img/btn_close_02.svg" alt="閉じる"></a>
</h2>
<!--ナビバー左側-->
<div class="left">
<ul class="utility">
<h1><a href="https://www.gifuai.net/"><img src="./img/rogo.png"></a></h1>
</ul>
</div><!--left-->
<!--ナビバー右側-->
<div class="right">
<ul class="utility">
<li><a href="https://www.gifuai.net/">ホーム</a></li>
<li><a href="https://www.gifuai.net/?page_id=60043">岐阜ロゲ</a></li>
<li><a href="https://www.gifuai.net/?page_id=4427">自治会SNS</a></li>
<li><a href="https://www.gifuai.net/?page_id=9370">会員・寄付金募集</a></li>
<li><a href="https://www.gifuai.net/?page_id=12434">フォトギャラリー</a></li>
<li><a href="https://www.gifuai.net/?page_id=52511">プレスリリース</a></li>
</ul>
</div><!--right-->
</div><!--headerMenu-->
</div><!--headerContent-->
</header>
<div class="to_classification">
<div class="to_class_box">
<div class="to_class_tebox">
<div class="to_class_text">
<h1>Myアルバム</h1>
</div>
</div>
<div class="to_class_img">
<img src="./img/title_event.png">
</div>
</div>
</div>
<section class="viewtop">
<section class="view">
<!-- イベント選択 -->
<select id="eventSelect">
<option disabled="disabled" value="">イベント一覧</option>
<option selected="selected" value="FC岐阜">FC岐阜</option>
<!-- 他のイベントオプションを追加 -->
</select>
<!-- ゼッケン番号入力 -->
<input type="text" id="zekkenInput" placeholder="ゼッケン番号">
<!-- パスワード入力 -->
<input type="password" id="passwordInput" placeholder="パスワード">
<!-- 検索ボタン -->
<button onclick="searchPhotos()">写真リストを検索</button>
</section>
<!-- 結果表示エリア -->
<div id="photoList"></div>
</section>
<footer class="gifu_fotter">
<div class="footer_menubox">
<div><a href="https://www.gifuai.net/"><img src="./img/rogo.png"></a></div>
<div class="footer_menu">
<ul class="footer_menulink">
<li><a href="https://www.gifuai.net/">ホーム</a></li>
<li><a href="https://www.gifuai.net/?page_id=4806">information</a></li>
<li><a
href="https://docs.google.com/forms/d/e/1FAIpQLScEXBGEZroAR6F8z2OKhjXn74PhZ5bcSheZVlGlGjz12Iu1JA/viewform">お問い合わせ</a>
</li>
</ul>
<ul class="footer_menulogo">
<li><a href="https://twitter.com/GifuK7"><img src="./img/Xlogo.svg" alt="Xロゴ"></a></li>
<li><a href="https://www.facebook.com/gifu.ai.network/"><img src="./img/facebook_logo.svg"
alt="Facebookロゴ"></a></li>
<li><a href="https://www.instagram.com/gifuainetwork/?igshid=MzMyNGUyNmU2YQ%3D%3D"><img
src="./img/Instagram_logo.svg" alt="instagramロゴ"></a></li>
<li><a href=""><img></a></li>
</ul>
</div>
</div>
<div class="f_copy">Copyright©NPO岐阜aiネットワーク</div>
</footer>
<script>
function searchPhotos() {
var selectedEvent = document.getElementById('eventSelect').value;
var selectedZekken = document.getElementById('zekkenInput').value;
var inputedPassword = document.getElementById('passwordInput').value;
// login関数を実行して写真リストを取得
login(selectedEvent, selectedZekken, inputedPassword);
}
async function Login(selectedEvent, selectedZekken, inputedPassword) {
const event = selectedEvent;
const identifier = selectedZekken;
const password = inputedPassword;
try {
const response = await fetch('/api/login/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
identifier: identifier, // メールアドレスまたはゼッケン番号
password: password
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'ログインに失敗しました');
}
// ログイン成功時の処理
localStorage.setItem('authToken', data.token);
localStorage.setItem('userData', JSON.stringify(data.user));
var URL = "https://rogaining.sumasen.net/api/get-photolist?event=" + selectedEvent + "&zekken=" + selectedZekken + "&pw=" + inputedPassword;
axios.get(URL)
.then(function (response) {
displayPhotos(response.data); // 写真リストを表示する関数にレスポンスオブジェクトを渡す
})
.catch(function (error) {
console.error("login function error: ", error);
});
} catch (error) {
// エラーメッセージを表示
errorMessage.textContent = error.message;
errorMessage.style.display = 'block';
} finally {
// 送信ボタンを再度有効化
submitButton.disabled = false;
submitButton.textContent = 'ログイン';
}
}
// login関数内で写真リストをDOMに表示する処理を追加
function login_old(selectedEvent, selectedZekken, inputedPassword) {
var URL = "https://rogaining.sumasen.net/api/get-photolist?event=" + selectedEvent + "&zekken=" + selectedZekken + "&pw=" + inputedPassword;
axios.get(URL)
.then(function (response) {
displayPhotos(response.data); // 写真リストを表示する関数にレスポンスオブジェクトを渡す
})
.catch(function (error) {
console.error("login function error: ", error);
});
}
// 写真リストを表示する関数
function displayPhotos(response) {
var photoListDiv = document.getElementById('photoList');
photoListDiv.innerHTML = ''; // 既存の内容をクリア
// レスポンス全体をログに出力
console.log('Response object:', response);
// レスポンスオブジェクトからphoto_list配列を取得
var photos = response.photo_list;
// photo_listの内容をログに出力
console.log('Photo list array:', photos);
// 'photos'が配列であることを確認
if (Array.isArray(photos)) {
photos.forEach(function (photodata,index) {
// 各写真のURLをインデックス付きでログに出力
console.log(`Photo ${index + 1} data:`, photodata);
// photodataのプロパティの存在確認とcp_numberの条件チェック
if (!photodata.hasOwnProperty('photo_url') ||
!photodata.hasOwnProperty('cp_number') ||
photodata.cp_number <= 0) { // cp_numberが0以下の場合はスキップ
console.log(`Skipping photo at index ${index}. cp_number: ${photodata.cp_number}`);
return; // この写真をスキップ
}
// img要素を作成
var img = document.createElement('img');
img.src = photodata.photo_url; // 写真のURLをsrc属性に設定
img.className = 'event-photo'; // クラス名を設定
img.alt = 'Photo cp=${photodata.cp_number}'; // 代替テキストを設定
// 画像の読み込みエラーをキャッチ
img.onerror = function() {
console.error(`Failed to load image ${index + 1}:`, photodata.photorl);
};
// 画像を表示エリアに追加
photoListDiv.appendChild(img);
});
} else {
photoListDiv.innerHTML = 'ゼッケン番号とパスワードが一致していません。もう一度入力をお願いします。'
console.error('Expected photos to be an array, but received:', photos);
}
}
</script>
<script src="jquery-2.1.3.min.js"></script>
<script src="./js/main.js"></script>
<link rel="stylesheet" href="./js/drawer.min.js">
</body>
</html>

View File

@ -1,6 +1,6 @@
# HTTPS server
server {
listen 80;
listen 8100;
server_name rogaining.sumasen.net localhost;
access_log /var/log/nginx/api_access.log;
@ -11,7 +11,7 @@ server {
# Django admin
location ~ ^/(admin|api)/ {
proxy_pass http://api:8100;
proxy_pass http://api:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -36,9 +36,22 @@ server {
}
location /static/ {
alias /app/static/;
expires 1h;
add_header Cache-Control "public, no-transform";
proxy_pass http://api:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-CSRFToken $http_x_csrf_token;
# タイムアウト設定
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
send_timeout 300;
# alias /app/static/;
# expires 1h;
# add_header Cache-Control "public, no-transform";
}
location = / {

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>