Compare commits

2 Commits

Author SHA1 Message Date
e01d2e7ea6 Supervisor update3 2024-10-30 06:57:51 +09:00
d017da17d4 supervisor step3 2024-10-29 14:07:31 +00:00
52 changed files with 1197 additions and 18677 deletions

View File

@ -3,7 +3,6 @@ FROM osgeo/gdal:ubuntu-small-3.4.0
WORKDIR /app WORKDIR /app
LABEL maintainer="nouffer@gmail.com" LABEL maintainer="nouffer@gmail.com"
LABEL description="Development image for the Rogaining JP" LABEL description="Development image for the Rogaining JP"
@ -39,63 +38,12 @@ RUN apt-get install -y python3
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
python3-pip python3-pip
# ベースイメージの更新とパッケージのインストール
RUN apt-get update && \
apt-get install -y \
libreoffice \
libreoffice-calc \
libreoffice-writer \
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 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 RUN apt-get update
COPY ./requirements.txt /app/requirements.txt COPY ./requirements.txt /app/requirements.txt
RUN pip install boto3==1.26.137
# Install Gunicorn # Install Gunicorn
RUN pip install gunicorn RUN pip install gunicorn

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
# 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

@ -1,20 +0,0 @@
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

@ -1,26 +0,0 @@
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

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

View File

@ -1,25 +0,0 @@
# 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

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

View File

@ -1,102 +0,0 @@
# 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

@ -1,166 +0,0 @@
# 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

@ -1,77 +0,0 @@
# 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

@ -1,96 +0,0 @@
# 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

@ -1,148 +0,0 @@
# 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

@ -1,115 +0,0 @@
# 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

View File

@ -1,28 +0,0 @@
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}")

View File

@ -1,26 +0,0 @@
[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

View File

@ -1,69 +0,0 @@
<?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>

View File

@ -48,6 +48,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.gis', 'django.contrib.gis',
'rest_framework', 'rest_framework',
'rest_framework.authtoken',
'rest_framework_gis', 'rest_framework_gis',
'knox', 'knox',
'leaflet', 'leaflet',
@ -215,7 +216,10 @@ LEAFLET_CONFIG = {
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication', ), 'DEFAULT_AUTHENTICATION_CLASSES': ['knox.auth.TokenAuthentication','rest_framework.authentication.TokenAuthentication', ],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
} }

View File

@ -1,27 +0,0 @@
[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.

BIN
rog/.DS_Store vendored

Binary file not shown.

View File

@ -29,24 +29,6 @@ from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserChangeForm, UserCreationForm from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.utils.translation import gettext_lazy as _ 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) @admin.register(GifurogeRegister)
class GifurogeRegisterAdmin(admin.ModelAdmin): class GifurogeRegisterAdmin(admin.ModelAdmin):
list_display = ('event_code', 'time', 'owner_name', 'email', 'team_name', 'department') list_display = ('event_code', 'time', 'owner_name', 'email', 'team_name', 'department')
@ -923,16 +905,13 @@ class CustomUserCreationForm(UserCreationForm):
model = CustomUser model = CustomUser
fields = ('email', 'lastname', 'firstname', 'date_of_birth', 'female') fields = ('email', 'lastname', 'firstname', 'date_of_birth', 'female')
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm form = CustomUserChangeForm
add_form = CustomUserCreationForm 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') 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') list_filter = ('is_staff', 'is_active', 'is_rogaining', 'group')
ordering = ('email',)
# readonly_fieldsを明示的に設定 # readonly_fieldsを明示的に設定
readonly_fields = ('date_joined',) # 変更不可のフィールドのみを指定=>Personal Infoも編集可能にする。 readonly_fields = ('date_joined',) # 変更不可のフィールドのみを指定=>Personal Infoも編集可能にする。
@ -963,17 +942,13 @@ class CustomUserAdmin(UserAdmin):
search_fields = ('email', 'firstname', 'lastname', 'zekken_number', 'team_name') search_fields = ('email', 'firstname', 'lastname', 'zekken_number', 'team_name')
ordering = ('email',) ordering = ('email',)
def get_readonly_fields_old(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
# スーパーユーザーの場合は読み取り専用フィールドを最小限に # スーパーユーザーの場合は読み取り専用フィールドを最小限に
if request.user.is_superuser: if request.user.is_superuser:
return self.readonly_fields return self.readonly_fields
# 通常のスタッフユーザーの場合は追加の制限を設定可能 # 通常のスタッフユーザーの場合は追加の制限を設定可能
return self.readonly_fields + ('is_staff', 'is_superuser') return self.readonly_fields + ('is_staff', 'is_superuser')
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')
admin.site.register(Useractions) admin.site.register(Useractions)
admin.site.register(RogUser, admin.ModelAdmin) admin.site.register(RogUser, admin.ModelAdmin)

View File

@ -310,7 +310,6 @@ class TempUser(models.Model):
class NewEvent2(models.Model): class NewEvent2(models.Model):
event_name = models.CharField(max_length=255, unique=True) 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) start_datetime = models.DateTimeField(default=timezone.now)
end_datetime = models.DateTimeField() end_datetime = models.DateTimeField()
deadlineDateTime = models.DateTimeField(null=True, blank=True) deadlineDateTime = models.DateTimeField(null=True, blank=True)
@ -519,15 +518,10 @@ class EntryMember(models.Model):
class GoalImages(models.Model): class GoalImages(models.Model):
user=models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING) user=models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING)
goalimage = models.FileField(upload_to='goals/%y%m%d', blank=True, null=True) goalimage = models.FileField(upload_to='goals/%y%m%d', blank=True, null=True)
goaltime = models.DateTimeField(_("Goal time"), blank=True, null=True,auto_now=False, auto_now_add=False) goaltime = models.DateTimeField(_("Goal time"), auto_now=False, auto_now_add=False)
team_name = models.CharField(_("Team name"), max_length=255) team_name = models.CharField(_("Team name"), max_length=255)
event_code = models.CharField(_("event code"), max_length=255) event_code = models.CharField(_("event code"), max_length=255)
cp_number = models.IntegerField(_("CP numner")) cp_number = models.IntegerField(_("CP numner"))
zekken_number = models.TextField(
null=True, # False にする
blank=True, # False にする
help_text="ゼッケン番号"
)
class CheckinImages(models.Model): class CheckinImages(models.Model):
user=models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING) user=models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING)
@ -538,7 +532,6 @@ class CheckinImages(models.Model):
cp_number = models.IntegerField(_("CP numner")) cp_number = models.IntegerField(_("CP numner"))
class GpsCheckin(models.Model): class GpsCheckin(models.Model):
id = models.AutoField(primary_key=True) # 明示的にidフィールドを追加
path_order = models.IntegerField( path_order = models.IntegerField(
null=False, null=False,
help_text="チェックポイントの順序番号" help_text="チェックポイントの順序番号"
@ -547,11 +540,6 @@ class GpsCheckin(models.Model):
null=False, null=False,
help_text="ゼッケン番号" help_text="ゼッケン番号"
) )
event_id = models.IntegerField(
null=True,
blank=True,
help_text="イベントID"
)
event_code = models.TextField( event_code = models.TextField(
null=False, null=False,
help_text="イベントコード" help_text="イベントコード"
@ -637,14 +625,19 @@ class GpsCheckin(models.Model):
class Meta: class Meta:
db_table = 'gps_checkins' db_table = 'gps_checkins'
constraints = [
models.UniqueConstraint(
fields=['zekken_number', 'event_code', 'path_order'],
name='unique_gps_checkin'
)
]
indexes = [ indexes = [
models.Index(fields=['zekken_number', 'event_code','path_order'], name='idx_zekken_event'), models.Index(fields=['zekken_number', 'event_code','path_order'], name='idx_zekken_event'),
models.Index(fields=['create_at'], name='idx_create_at'), models.Index(fields=['create_at'], name='idx_create_at'),
] ]
def __str__(self): def __str__(self):
return f"{self.event_code}-{self.zekken_number}-{self.path_order}-buy:{self.buy_flag}-valid:{self.validate_location}-point:{self.points}" return f"{self.event_code}-{self.zekken_number}-{self.path_order}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# 作成時・更新時のタイムスタンプを自動設定 # 作成時・更新時のタイムスタンプを自動設定

View File

@ -1,342 +0,0 @@
-- まず既存のビューをすべて削除
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,7 +27,6 @@ from django.shortcuts import get_object_or_404
from django.utils import timezone from django.utils import timezone
from datetime import datetime, date from datetime import datetime, date
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LocationCatSerializer(serializers.ModelSerializer): class LocationCatSerializer(serializers.ModelSerializer):
@ -877,37 +876,3 @@ class UserLastGoalTimeSerializer(serializers.Serializer):
user_email = serializers.EmailField() user_email = serializers.EmailField()
last_goal_time = serializers.DateTimeField() 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

@ -1,214 +0,0 @@
# 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 sys import prefix
from rest_framework import urlpatterns from rest_framework import urlpatterns
from rest_framework.routers import DefaultRouter 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,get_ranking, all_ranking_top3 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 django.urls import path, include from django.urls import path, include
@ -124,13 +124,6 @@ urlpatterns += [
path('export_excel/<int:zekken_number>/<str:event_code>/', views.export_excel, name='export_excel'), path('export_excel/<int:zekken_number>/<str:event_code>/', views.export_excel, name='export_excel'),
# for Supervisor Web app # for Supervisor Web app
path('test/', views.test_api, name='test_api'), path('test/', views.test_api, name='test_api'),
path('update-goal-time/', views.update_goal_time, name='update-goal-time'),
path('get-goalimage/', views.get_goalimage, name='get-goalimage'),
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: if settings.DEBUG:

View File

@ -1,13 +1,10 @@
import os import os
from botocore.exceptions import ClientError
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.conf import settings from django.conf import settings
import logging import logging
import boto3
from django.core.mail import send_mail from django.core.mail import send_mail
from django.urls import reverse from django.urls import reverse
import uuid import uuid
import environ
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -114,267 +111,3 @@ def send_invitaion_and_verification_email(user, team, activation_link):
subject, body = load_email_template('invitation_and_verification_email.txt', context) subject, body = load_email_template('invitation_and_verification_email.txt', context)
share_send_email(subject,body,user.email) 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

@ -1,83 +0,0 @@
# 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)

View File

@ -1,119 +0,0 @@
# 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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="${csrfToken}">
<title>スーパーバイザーパネル</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-50">
<div id="app" class="container mx-auto p-4"></div>
<!-- テンプレート -->
<template id="supervisor-panel-template">
<div class="bg-white rounded-lg shadow-lg p-6 mb-6">
<h1 class="text-2xl font-bold mb-6" role="heading" aria-level="1">スーパーバイザーパネル</h1>
<!-- 選択フォーム -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div>
<label for="eventCode" class="block text-sm font-medium text-gray-700 mb-2">イベントコード</label>
<select id="eventCode" class="w-full border border-gray-300 rounded-md px-3 py-2" aria-label="イベントを選択">
<option value="">イベントを選択</option>
</select>
</div>
<div>
<label for="zekkenNumber" class="block text-sm font-medium text-gray-700 mb-2">ゼッケン番号</label>
<select id="zekkenNumber" class="w-full border border-gray-300 rounded-md px-3 py-2" aria-label="ゼッケン番号を選択">
<option value="">ゼッケン番号を選択</option>
</select>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">チーム名</div>
<div id="teamName" class="font-semibold" aria-live="polite">-</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">メンバー</div>
<div id="members" class="font-semibold" aria-live="polite">-</div>
</div>
</div>
<!-- チーム情報サマリー -->
<div id="team-summary" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<!-- スタート時刻 -->
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">スタート時刻</div>
<div id="startTime" class="font-semibold" aria-live="polite">-</div>
</div>
<!-- ゴール時刻 -->
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">ゴール時刻</div>
<div class="goal-time-container">
<span id="goalTimeDisplay" class="goal-time-display cursor-pointer" role="button" tabindex="0" aria-label="ゴール時刻を編集">-</span>
<input type="datetime-local" id="goalTimeInput" class="goal-time-input hidden border rounded px-2 py-1" aria-label="ゴール時刻入力">
</div>
</div>
<!-- ゴール判定 -->
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">判定</div>
<div id="validate" class="font-semibold text-blue-600" aria-live="polite">-</div>
</div>
<!-- 得点サマリー -->
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">総合得点</div>
<div id="totalScore" class="font-semibold text-blue-600" aria-live="polite">-</div>
</div>
</div>
<!-- チェックインデータテーブル -->
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200" role="grid">
<thead class="bg-gray-50">
<tr>
<th class="w-8" scope="col"></th>
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase" scope="col">走行順</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" scope="col">規定写真</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" scope="col">撮影写真</th>
<th class="px-5 py-3 text-left text-xs font-medium text-gray-500 uppercase" scope="col">CP名称</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" scope="col">通過時刻</th>
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase" scope="col">通過審査</th>
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase" scope="col">買物</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase" scope="col">獲得点数</th>
<th class="w-8" scope="col"></th>
</tr>
</thead>
<tbody id="checkinList" class="bg-white divide-y divide-gray-200">
<!-- JavaScript で動的に生成 -->
</tbody>
</table>
</div>
<!-- アクションボタン -->
<div class="mt-6 flex justify-end space-x-4">
<button id="addCpButton" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" aria-label="新規CP追加">
新規CP追加
</button>
<button id="saveButton" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" aria-label="変更を保存">
保存
</button>
<button id="exportButton" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2" aria-label="通過証明書を出力">
通過証明書出力
</button>
</div>
</div>
</template>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,72 @@
// js/ApiClient.js
export class ApiClient {
constructor({ baseUrl, authToken, csrfToken }) {
this.baseUrl = baseUrl;
this.authToken = authToken;
this.csrfToken = csrfToken;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
'Authorization': `Token ${this.authToken}`,
'X-CSRF-Token': this.csrfToken
};
try {
const response = await fetch(url, {
...options,
headers: {
...headers,
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// イベント関連のAPI
async getEvents() {
return this.request('/new-events/');
}
async getZekkenNumbers(eventCode) {
return this.request(`/zekken_numbers/${eventCode}`);
}
// チーム関連のAPI
async getTeamInfo(zekkenNumber) {
return this.request(`/team_info/${zekkenNumber}/`);
}
async getCheckins(zekkenNumber, eventCode) {
return this.request(`/checkins/${zekkenNumber}/${eventCode}/`);
}
async updateCheckin(checkinId, data) {
return this.request(`/checkins/${checkinId}`, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async deleteCheckin(checkinId) {
return this.request(`/checkins/${checkinId}`, {
method: 'DELETE'
});
}
}

View File

@ -0,0 +1,276 @@
// js/SupervisorPanel.js
import { CheckinList } from './components/CheckinList.js';
import { TeamSummary } from './components/TeamSummary.js';
import { PointsCalculator } from './utils/PointsCalculator.js';
import { DateFormatter } from './utils/DateFormatter.js';
import { NotificationService } from './services/NotificationService.js';
export class SupervisorPanel {
constructor({ element, template, apiClient, eventBus }) {
this.element = element;
this.template = template;
this.apiClient = apiClient;
this.eventBus = eventBus;
this.notification = new NotificationService();
this.pointsCalculator = new PointsCalculator();
this.state = {
currentEvent: null,
currentZekken: null,
teamData: null,
checkins: []
};
}
async initialize() {
this.render();
this.initializeComponents();
this.bindEvents();
await this.loadInitialData();
}
render() {
this.element.innerHTML = this.template.innerHTML;
// コンポーネントの初期化
this.checkinList = new CheckinList({
element: document.getElementById('checkinList'),
onUpdate: this.handleCheckinUpdate.bind(this)
});
this.teamSummary = new TeamSummary({
element: document.getElementById('team-summary'),
onGoalTimeUpdate: this.handleGoalTimeUpdate.bind(this)
});
}
initializeComponents() {
// Sortable.jsの初期化
new Sortable(document.getElementById('checkinList'), {
animation: 150,
onEnd: this.handlePathOrderChange.bind(this)
});
}
bindEvents() {
// イベント選択
document.getElementById('eventCode').addEventListener('change',
this.handleEventChange.bind(this));
// ゼッケン番号選択
document.getElementById('zekkenNumber').addEventListener('change',
this.handleZekkenChange.bind(this));
// ボタンのイベントハンドラ
document.getElementById('addCpButton').addEventListener('click',
this.handleAddCP.bind(this));
document.getElementById('saveButton').addEventListener('click',
this.handleSave.bind(this));
document.getElementById('exportButton').addEventListener('click',
this.handleExport.bind(this));
}
async loadInitialData() {
try {
const events = await this.apiClient.getEvents();
this.populateEventSelect(events);
} catch (error) {
this.notification.showError('イベントの読み込みに失敗しました');
}
}
async handleEventChange(event) {
const eventCode = event.target.value;
if (!eventCode) return;
try {
const zekkenNumbers = await this.apiClient.getZekkenNumbers(eventCode);
this.populateZekkenSelect(zekkenNumbers);
this.state.currentEvent = eventCode;
} catch (error) {
this.notification.showError('ゼッケン番号の読み込みに失敗しました');
}
}
async handleZekkenChange(event) {
const zekkenNumber = event.target.value;
if (!zekkenNumber || !this.state.currentEvent) return;
try {
const [teamData, checkins] = await Promise.all([
this.apiClient.getTeamInfo(zekkenNumber),
this.apiClient.getCheckins(zekkenNumber, this.state.currentEvent)
]);
this.state.currentZekken = zekkenNumber;
this.state.teamData = teamData;
this.state.checkins = checkins;
this.updateUI();
} catch (error) {
this.notification.showError('チームデータの読み込みに失敗しました');
}
}
async handleGoalTimeUpdate(newTime) {
if (!this.state.teamData) return;
try {
const response = await this.apiClient.updateTeamGoalTime(
this.state.currentZekken,
newTime
);
this.state.teamData.end_datetime = newTime;
this.validateGoalTime();
this.teamSummary.update(this.state.teamData);
} catch (error) {
this.notification.showError('ゴール時刻の更新に失敗しました');
}
}
async handleCheckinUpdate(checkinId, updates) {
try {
const response = await this.apiClient.updateCheckin(checkinId, updates);
const index = this.state.checkins.findIndex(c => c.id === checkinId);
if (index !== -1) {
this.state.checkins[index] = { ...this.state.checkins[index], ...updates };
this.calculatePoints();
this.updateUI();
}
} catch (error) {
this.notification.showError('チェックインの更新に失敗しました');
}
}
async handlePathOrderChange(event) {
const newOrder = Array.from(event.to.children).map((element, index) => ({
id: element.dataset.id,
path_order: index + 1
}));
try {
await this.apiClient.updatePathOrders(newOrder);
this.state.checkins = this.state.checkins.map(checkin => {
const orderUpdate = newOrder.find(update => update.id === checkin.id);
return orderUpdate ? { ...checkin, path_order: orderUpdate.path_order } : checkin;
});
} catch (error) {
this.notification.showError('走行順の更新に失敗しました');
}
}
async handleAddCP() {
try {
const newCP = await this.showAddCPModal();
if (!newCP) return;
const response = await this.apiClient.addCheckin(
this.state.currentZekken,
newCP
);
this.state.checkins.push(response);
this.updateUI();
} catch (error) {
this.notification.showError('CPの追加に失敗しました');
}
}
async handleSave() {
try {
await this.apiClient.saveAllChanges({
zekkenNumber: this.state.currentZekken,
checkins: this.state.checkins,
teamData: this.state.teamData
});
this.notification.showSuccess('保存が完了しました');
} catch (error) {
this.notification.showError('保存に失敗しました');
}
}
handleExport() {
if (!this.state.currentZekken) {
this.notification.showError('ゼッケン番号を選択してください');
return;
}
const exportUrl = `${this.apiClient.baseUrl}/export-excel/${this.state.currentZekken}`;
window.open(exportUrl, '_blank');
}
validateGoalTime() {
if (!this.state.teamData || !this.state.teamData.end_datetime) return;
const endTime = new Date(this.state.teamData.end_datetime);
const eventEndTime = new Date(this.state.teamData.event_end_time);
const timeDiff = (endTime - eventEndTime) / (1000 * 60);
this.state.teamData.validation = {
status: timeDiff <= 15 ? '合格' : '失格',
latePoints: timeDiff > 15 ? Math.floor(timeDiff - 15) * -50 : 0
};
}
calculatePoints() {
const points = this.pointsCalculator.calculate({
checkins: this.state.checkins,
latePoints: this.state.teamData?.validation?.latePoints || 0
});
this.state.points = points;
}
updateUI() {
// チーム情報の更新
this.teamSummary.update(this.state.teamData);
// チェックインリストの更新
this.checkinList.update(this.state.checkins);
// ポイントの再計算と表示
this.calculatePoints();
this.updatePointsDisplay();
}
updatePointsDisplay() {
const { totalPoints, buyPoints, latePoints, finalPoints } = this.state.points;
document.getElementById('totalPoints').textContent = totalPoints;
document.getElementById('buyPoints').textContent = buyPoints;
document.getElementById('latePoints').textContent = latePoints;
document.getElementById('finalPoints').textContent = finalPoints;
}
populateEventSelect(events) {
const select = document.getElementById('eventCode');
select.innerHTML = '<option value="">イベントを選択</option>';
events.forEach(event => {
const option = document.createElement('option');
option.value = event.code;
option.textContent = this.escapeHtml(event.name);
select.appendChild(option);
});
}
populateZekkenSelect(numbers) {
const select = document.getElementById('zekkenNumber');
select.innerHTML = '<option value="">ゼッケン番号を選択</option>';
numbers.forEach(number => {
const option = document.createElement('option');
option.value = number;
option.textContent = this.escapeHtml(number.toString());
select.appendChild(option);
});
}
escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
}

171
supervisor/html/js/main.js Normal file
View File

@ -0,0 +1,171 @@
// js/main.js
// EventBus
const EventBus = {
listeners: {},
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
},
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
};
// NotificationService
class NotificationService {
constructor() {
this.toastElement = document.getElementById('toast');
}
showMessage(message, type = 'info') {
this.toastElement.textContent = message;
this.toastElement.className = `fixed bottom-4 right-4 px-6 py-3 rounded shadow-lg ${
type === 'error' ? 'bg-red-500' : 'bg-green-500'
} text-white`;
this.toastElement.classList.remove('hidden');
setTimeout(() => {
this.toastElement.classList.add('hidden');
}, 3000);
}
showError(message) {
this.showMessage(message, 'error');
}
showSuccess(message) {
this.showMessage(message, 'success');
}
}
// ApiClient
class ApiClient {
constructor({ baseUrl, authToken, csrfToken }) {
this.baseUrl = baseUrl;
this.authToken = authToken;
this.csrfToken = csrfToken;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
'Authorization': `Token ${this.authToken}`,
'X-CSRF-Token': this.csrfToken
};
try {
const response = await fetch(url, {
...options,
headers: {
...headers,
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// API methods
async getEvents() {
return this.request('/new-events/');
}
async getZekkenNumbers(eventCode) {
return this.request(`/zekken_numbers/${eventCode}`);
}
async getTeamInfo(zekkenNumber) {
return this.request(`/team_info/${zekkenNumber}/`);
}
// ... その他のAPI methods
}
// PointsCalculator
class PointsCalculator {
calculate({ checkins, latePoints = 0 }) {
const totalPoints = this.calculateTotalPoints(checkins);
const buyPoints = this.calculateBuyPoints(checkins);
return {
totalPoints,
buyPoints,
latePoints,
finalPoints: totalPoints + buyPoints + latePoints
};
}
calculateTotalPoints(checkins) {
return checkins.reduce((total, checkin) => {
if (checkin.validate_location && !checkin.buy_flag) {
return total + (checkin.checkin_point || 0);
}
return total;
}, 0);
}
calculateBuyPoints(checkins) {
return checkins.reduce((total, checkin) => {
if (checkin.validate_location && checkin.buy_flag) {
return total + (checkin.buy_point || 0);
}
return total;
}, 0);
}
}
// SupervisorPanel - メインアプリケーションクラス
class SupervisorPanel {
constructor(options) {
this.element = options.element;
this.apiClient = new ApiClient(options.apiConfig);
this.notification = new NotificationService();
this.pointsCalculator = new PointsCalculator();
this.eventBus = EventBus;
this.state = {
currentEvent: null,
currentZekken: null,
teamData: null,
checkins: []
};
}
// ... SupervisorPanelの実装 ...
}
// アプリケーションの初期化
document.addEventListener('DOMContentLoaded', () => {
const app = new SupervisorPanel({
element: document.getElementById('app'),
apiConfig: {
baseUrl: '/api',
authToken: localStorage.getItem('authToken'),
csrfToken: document.querySelector('meta[name="csrf-token"]').content
}
});
app.initialize();
});

View File

@ -0,0 +1,32 @@
// js/utils/PointsCalculator.js
export class PointsCalculator {
calculate({ checkins, latePoints = 0 }) {
const totalPoints = this.calculateTotalPoints(checkins);
const buyPoints = this.calculateBuyPoints(checkins);
return {
totalPoints,
buyPoints,
latePoints,
finalPoints: totalPoints + buyPoints + latePoints
};
}
calculateTotalPoints(checkins) {
return checkins.reduce((total, checkin) => {
if (checkin.validate_location && !checkin.buy_flag) {
return total + (checkin.checkin_point || 0);
}
return total;
}, 0);
}
calculateBuyPoints(checkins) {
return checkins.reduce((total, checkin) => {
if (checkin.validate_location && checkin.buy_flag) {
return total + (checkin.buy_point || 0);
}
return total;
}, 0);
}
}

27
supervisor/html/js/vi Normal file
View File

@ -0,0 +1,27 @@
// js/services/NotificationService.js
export class NotificationService {
constructor() {
this.toastElement = document.getElementById('toast');
}
showMessage(message, type = 'info') {
this.toastElement.textContent = message;
this.toastElement.className = `fixed bottom-4 right-4 px-6 py-3 rounded shadow-lg ${
type === 'error' ? 'bg-red-500' : 'bg-green-500'
} text-white`;
this.toastElement.classList.remove('hidden');
setTimeout(() => {
this.toastElement.classList.add('hidden');
}, 3000);
}
showError(message) {
this.showMessage(message, 'error');
}
showSuccess(message) {
this.showMessage(message, 'success');
}
}

View File

@ -0,0 +1,26 @@
<!-- login.html -->
<form id="loginForm" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">メールアドレス</label>
<input type="email" id="email" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">パスワード</label>
<input type="password" id="password" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
</div>
<button type="submit"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
ログイン
</button>
</form>
<script>
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
await login(email, password);
});
</script>

View File

@ -1,344 +0,0 @@
<!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

@ -1,486 +0,0 @@
<!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

@ -1,293 +0,0 @@
<!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>

BIN
templates/.DS_Store vendored

Binary file not shown.

View File

@ -1,32 +0,0 @@
<!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

@ -1,32 +0,0 @@
<!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>