永栄コードのマージ開始
This commit is contained in:
@ -37,6 +37,7 @@ services:
|
|||||||
#entrypoint: ["/app/wait-for.sh", "postgres-db:5432", "--", ""]
|
#entrypoint: ["/app/wait-for.sh", "postgres-db:5432", "--", ""]
|
||||||
#command: python3 manage.py runserver 0.0.0.0:8100
|
#command: python3 manage.py runserver 0.0.0.0:8100
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
rog-api:
|
rog-api:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
673
rog/admin.py
673
rog/admin.py
@ -1,10 +1,10 @@
|
|||||||
import email
|
import email
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render,redirect
|
||||||
from leaflet.admin import LeafletGeoAdmin
|
from leaflet.admin import LeafletGeoAdmin
|
||||||
from leaflet.admin import LeafletGeoAdminMixin
|
from leaflet.admin import LeafletGeoAdminMixin
|
||||||
from leaflet_admin_list.admin import LeafletAdminListMixin
|
from leaflet_admin_list.admin import LeafletAdminListMixin
|
||||||
from .models import RogUser, Location, SystemSettings, JoinedEvent, Favorite, TravelList, TravelPoint, ShapeLayers, Event, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, CustomUser, GifuAreas, UserTracks, templocation, UserUpload, EventUser, GoalImages, CheckinImages, NewEvent, NewEvent2, Team, NewCategory, Category, Entry, Member, TempUser
|
from .models import RogUser, Location, SystemSettings, JoinedEvent, Favorite, TravelList, TravelPoint, ShapeLayers, Event, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, CustomUser, GifuAreas, UserTracks, templocation, UserUpload, EventUser, GoalImages, CheckinImages, NewEvent2, Team, NewCategory, Entry, Member, TempUser,GifurogeRegister
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.urls import path,reverse
|
from django.urls import path,reverse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
@ -16,6 +16,603 @@ from django.utils.html import format_html
|
|||||||
from .forms import CSVUploadForm
|
from .forms import CSVUploadForm
|
||||||
from .views import process_csv_upload
|
from .views import process_csv_upload
|
||||||
|
|
||||||
|
from django.db.models import F # F式をインポート
|
||||||
|
from django.db import transaction
|
||||||
|
from django.contrib import messages
|
||||||
|
import csv
|
||||||
|
from io import StringIO,TextIOWrapper
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from datetime import datetime, date
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
@admin.register(GifurogeRegister)
|
||||||
|
class GifurogeRegisterAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('event_code', 'time', 'owner_name', 'email', 'team_name', 'department')
|
||||||
|
change_list_template = 'admin/rog/gifurogeregister/change_list.html' # この行を追加
|
||||||
|
|
||||||
|
def find_matching_category(self, time, department):
|
||||||
|
"""
|
||||||
|
時間とdepartmentに基づいて適切なカテゴリを見つける
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
duration = timedelta(hours=time)
|
||||||
|
|
||||||
|
# 検索前の情報出力
|
||||||
|
print(f" Searching for category with parameters:")
|
||||||
|
print(f" - Duration: {duration}")
|
||||||
|
print(f" - Department: {department}")
|
||||||
|
|
||||||
|
# 利用可能なカテゴリの一覧を出力
|
||||||
|
all_categories = NewCategory.objects.all()
|
||||||
|
print(" Available categories:")
|
||||||
|
for cat in all_categories:
|
||||||
|
#print(f" - ID: {cat.id}")
|
||||||
|
print(f" - Name: {cat.category_name}")
|
||||||
|
print(f" - Duration: {cat.duration}")
|
||||||
|
print(f" - Number: {cat.category_number}")
|
||||||
|
|
||||||
|
# カテゴリ検索のクエリをログ出力
|
||||||
|
query = NewCategory.objects.filter(
|
||||||
|
duration=duration,
|
||||||
|
category_name__startswith=department
|
||||||
|
)
|
||||||
|
print(f" Query SQL: {query.query}")
|
||||||
|
|
||||||
|
# 検索結果の取得
|
||||||
|
category = query.first()
|
||||||
|
|
||||||
|
if category:
|
||||||
|
print(f" Found matching category:")
|
||||||
|
print(f" - Name: {category.category_name}")
|
||||||
|
print(f" - Duration: {category.duration}")
|
||||||
|
print(f" - Category Number: {getattr(category, 'category_number', 'N/A')}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(" No matching category found with the following filters:")
|
||||||
|
print(f" - Duration equals: {duration}")
|
||||||
|
print(f" - Category name starts with: {department}")
|
||||||
|
|
||||||
|
return category
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error finding category: {e}")
|
||||||
|
print(f"Exception type: {type(e)}")
|
||||||
|
import traceback
|
||||||
|
print(f"Traceback: {traceback.format_exc()}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_entry_with_number(self, team, category, owner, event):
|
||||||
|
"""
|
||||||
|
カテゴリ番号をインクリメントしてエントリーを作成
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# 事前バリデーション
|
||||||
|
try:
|
||||||
|
# チームメンバーの性別をチェック
|
||||||
|
if category.female:
|
||||||
|
for member in team.members.all():
|
||||||
|
|
||||||
|
print(f" Check existing member {member.user.lastname} {member.user.firstname} female:{member.user.female}")
|
||||||
|
if not member.user.female:
|
||||||
|
raise ValidationError(f"チーム '{team.team_name}' に男性メンバーが含まれているため、"
|
||||||
|
f"カテゴリー '{category.category_name}' には参加できません。")
|
||||||
|
except ValidationError as e:
|
||||||
|
print(f"Pre-validation error: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# カテゴリを再度ロックして取得
|
||||||
|
category_for_update = NewCategory.objects.select_for_update().get(
|
||||||
|
category_name=category.category_name
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" Creating entry with following details:")
|
||||||
|
print(f" - Category: {category_for_update.category_name}")
|
||||||
|
print(f" - Current category number: {category_for_update.category_number}")
|
||||||
|
|
||||||
|
# イベントの日付を取得
|
||||||
|
entry_date = event.start_datetime.date()
|
||||||
|
|
||||||
|
# 既存のエントリーをチェック
|
||||||
|
existing_entry = Entry.objects.filter(
|
||||||
|
team=team,
|
||||||
|
event=event,
|
||||||
|
date=entry_date,
|
||||||
|
is_active=True # アクティブなエントリーのみをチェック
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_entry:
|
||||||
|
print(f" Found existing entry for team {team.team_name} on {entry_date}")
|
||||||
|
raise ValidationError(
|
||||||
|
f"Team {team.team_name} already has an entry for event {event.event_name} on {entry_date}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 現在の番号を取得してインクリメント
|
||||||
|
current_number = category_for_update.category_number
|
||||||
|
zekken_number = current_number
|
||||||
|
|
||||||
|
# カテゴリ番号をインクリメント
|
||||||
|
category_for_update.category_number = F('category_number') + 1
|
||||||
|
category_for_update.save()
|
||||||
|
|
||||||
|
# 変更後の値を取得して表示
|
||||||
|
category_for_update.refresh_from_db()
|
||||||
|
print(f" Updated category number: {category_for_update.category_number}")
|
||||||
|
|
||||||
|
# エントリーの作成
|
||||||
|
try:
|
||||||
|
entry = Entry.objects.create(
|
||||||
|
date=event.start_datetime,
|
||||||
|
team=team,
|
||||||
|
category=category,
|
||||||
|
owner=owner,
|
||||||
|
event=event,
|
||||||
|
zekken_number=zekken_number,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
# バリデーションを実行
|
||||||
|
entry.full_clean()
|
||||||
|
# 問題なければ保存
|
||||||
|
entry.save()
|
||||||
|
|
||||||
|
print(f" Created entry:")
|
||||||
|
print(f" - Team: {team.team_name}")
|
||||||
|
print(f" - Event: {event.event_name}")
|
||||||
|
print(f" - Category: {category.category_name}")
|
||||||
|
print(f" - Zekken Number: {zekken_number}")
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
print(f"Entry validation error: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating entry: {e}")
|
||||||
|
print(f"Exception type: {type(e)}")
|
||||||
|
import traceback
|
||||||
|
print(f"Traceback: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def split_full_name(self, full_name):
|
||||||
|
"""
|
||||||
|
フルネームを姓と名に分割
|
||||||
|
半角または全角スペースに対応
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 空白文字で分割(半角スペース、全角スペース、タブなど)
|
||||||
|
parts = full_name.replace(' ', ' ').split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
last_name = parts[0]
|
||||||
|
first_name = ' '.join(parts[1:]) # 名が複数単語の場合に対応
|
||||||
|
return last_name, first_name
|
||||||
|
else:
|
||||||
|
# 分割できない場合は全体を姓とする
|
||||||
|
return full_name, ''
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error splitting name '{full_name}': {e}")
|
||||||
|
return full_name, ''
|
||||||
|
|
||||||
|
def convert_japanese_date(self, date_text):
|
||||||
|
"""
|
||||||
|
日本式の日付テキストをDateField形式に変換
|
||||||
|
例: '1990年1月1日' -> datetime.date(1990, 1, 1)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not date_text or date_text.strip() == '':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 全角数字を半角数字に変換
|
||||||
|
date_text = date_text.translate(str.maketrans('0123456789', '0123456789'))
|
||||||
|
date_text = date_text.strip()
|
||||||
|
|
||||||
|
# 区切り文字の判定と分割
|
||||||
|
if '年' in date_text:
|
||||||
|
# 年月日形式の場合
|
||||||
|
date_parts = date_text.replace('年', '-').replace('月', '-').replace('日', '').split('-')
|
||||||
|
elif '/' in date_text:
|
||||||
|
# スラッシュ区切りの場合
|
||||||
|
date_parts = date_text.split('/')
|
||||||
|
elif '-' in date_text:
|
||||||
|
date_parts = date_text.split('-')
|
||||||
|
else:
|
||||||
|
print(f"Unsupported date format: {date_text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 部分の数を確認
|
||||||
|
if len(date_parts) != 3:
|
||||||
|
print(f"Invalid date parts count: {len(date_parts)} in '{date_text}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
year = int(date_parts[0])
|
||||||
|
month = int(date_parts[1])
|
||||||
|
day = int(date_parts[2])
|
||||||
|
|
||||||
|
# 簡単な妥当性チェック
|
||||||
|
if not (1900 <= year <= 2100):
|
||||||
|
print(f"Invalid year: {year}")
|
||||||
|
return None
|
||||||
|
if not (1 <= month <= 12):
|
||||||
|
print(f"Invalid month: {month}")
|
||||||
|
return None
|
||||||
|
if not (1 <= day <= 31): # 月ごとの日数チェックは省略
|
||||||
|
print(f"Invalid day: {day}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"Converted from {date_text} to year-{year} / month-{month} / day-{day}")
|
||||||
|
|
||||||
|
return date(year, month, day)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error converting date '{date_text}': {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_owner_member( self,team,row ):
|
||||||
|
"""
|
||||||
|
オーナーをチームメンバー1として作成
|
||||||
|
既存のメンバーは更新
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
owner_name = row.get('owner_name').strip()
|
||||||
|
# 姓名を分割
|
||||||
|
last_name, first_name = self.split_full_name(owner_name)
|
||||||
|
print(f" Split name - Last: {last_name}, First: {first_name}")
|
||||||
|
# 誕生日の処理
|
||||||
|
birthday = row.get(f'owner_birthday', '').strip()
|
||||||
|
birth_date = self.convert_japanese_date(birthday)
|
||||||
|
print(f" Converted birthday: {birth_date}")
|
||||||
|
|
||||||
|
# 性別の処理
|
||||||
|
sex = row.get(f'owner_sex', '').strip()
|
||||||
|
is_female = sex in ['女性','女','女子','female','girl','lady']
|
||||||
|
print(f" Sex: {sex}, is_female: {is_female}")
|
||||||
|
|
||||||
|
# メンバーを作成
|
||||||
|
member,created = Member.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
user=team.owner,
|
||||||
|
defaults={
|
||||||
|
'is_temporary': True # 仮登録
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 既存メンバーの場合は情報を更新
|
||||||
|
if not created:
|
||||||
|
member.lastname = last_name
|
||||||
|
member.firstname = first_name
|
||||||
|
member.date_of_birth = birth_date
|
||||||
|
member.female = is_female
|
||||||
|
member.is_temporary = True
|
||||||
|
member.save()
|
||||||
|
print(f" Updated existing member {last_name} {first_name}")
|
||||||
|
else:
|
||||||
|
print(f" Created new member {last_name} {first_name}")
|
||||||
|
|
||||||
|
return member
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating/updating member: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def create_members(self, team, row):
|
||||||
|
"""
|
||||||
|
チームのメンバーを作成
|
||||||
|
既存のメンバーは更新
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
created_members = []
|
||||||
|
|
||||||
|
# オーナーをメンバーに登録
|
||||||
|
member = self.create_owner_member(team,row)
|
||||||
|
created_members.append(member)
|
||||||
|
|
||||||
|
# メンバー2から5までを処理
|
||||||
|
for i in range(2, 6):
|
||||||
|
member_name = row.get(f'member{i}', '').strip()
|
||||||
|
if member_name:
|
||||||
|
print(f"===== Processing member: {member_name} =====")
|
||||||
|
|
||||||
|
# 姓名を分割
|
||||||
|
last_name, first_name = self.split_full_name(member_name)
|
||||||
|
print(f" Split name - Last: {last_name}, First: {first_name}")
|
||||||
|
|
||||||
|
# 誕生日の処理
|
||||||
|
birthday = row.get(f'birthday{i}', '').strip()
|
||||||
|
birth_date = self.convert_japanese_date(birthday)
|
||||||
|
print(f" Converted birthday: {birth_date}")
|
||||||
|
|
||||||
|
# 性別の処理
|
||||||
|
sex = row.get(f'sex{i}', '').strip()
|
||||||
|
is_female = sex in ['女性','女','女子','female','girl','lady']
|
||||||
|
print(f" Sex: {sex}, is_female: {is_female}")
|
||||||
|
|
||||||
|
# メンバー用のユーザーを作成
|
||||||
|
email = f"dummy_{team.id}_{i}@gifuai.net".lower()
|
||||||
|
member_user, created = CustomUser.objects.get_or_create(
|
||||||
|
email=email,
|
||||||
|
defaults={
|
||||||
|
'password': make_password('temporary_password'),
|
||||||
|
'lastname': last_name,
|
||||||
|
'firstname': first_name,
|
||||||
|
'date_of_birth': birth_date,
|
||||||
|
'female':is_female
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 既存ユーザーの場合も姓名を更新
|
||||||
|
if not created:
|
||||||
|
member_user.lastname = last_name
|
||||||
|
member_user.firstname = first_name
|
||||||
|
member_user.date_of_birth = birth_date
|
||||||
|
member_user.female = is_female
|
||||||
|
member_user.save()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# メンバーを作成
|
||||||
|
member,created = Member.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
user=member_user,
|
||||||
|
defaults={
|
||||||
|
'is_temporary': True # 仮登録
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 既存メンバーの場合は情報を更新
|
||||||
|
if not created:
|
||||||
|
member.is_temporary = True
|
||||||
|
member.save()
|
||||||
|
print(f" Updated existing member {member_user.lastname} {member_user.firstname}")
|
||||||
|
else:
|
||||||
|
print(f" Created new member {member_user.lastname} {member_user.firstname}")
|
||||||
|
|
||||||
|
created_members.append(member)
|
||||||
|
print(f" - Birthday: {member_user.date_of_birth}")
|
||||||
|
print(f" - Sex: {'Female' if member_user.female else 'Male'}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating/updating member: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return created_members
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating members: {e}")
|
||||||
|
print(f"Exception type: {type(e)}")
|
||||||
|
import traceback
|
||||||
|
print(f"Traceback: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path('upload-csv/', self.upload_csv, name='gifuroge_register_upload_csv'),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def upload_csv(self, request):
|
||||||
|
print("upload_csv")
|
||||||
|
if request.method == 'POST':
|
||||||
|
print("POST")
|
||||||
|
if 'csv_file' not in request.FILES:
|
||||||
|
messages.error(request, 'No file was uploaded.')
|
||||||
|
return redirect('..')
|
||||||
|
|
||||||
|
csv_file = request.FILES['csv_file']
|
||||||
|
print(f"csv_file(1) = {csv_file}")
|
||||||
|
if not csv_file.name.endswith('.csv'):
|
||||||
|
messages.error(request, 'File is not CSV type')
|
||||||
|
return redirect('..')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# BOMを考慮してファイルを読み込む
|
||||||
|
file_content = csv_file.read()
|
||||||
|
# BOMがある場合は除去
|
||||||
|
if file_content.startswith(b'\xef\xbb\xbf'):
|
||||||
|
file_content = file_content[3:]
|
||||||
|
|
||||||
|
# デコード
|
||||||
|
file_content = file_content.decode('utf-8')
|
||||||
|
csv_file = StringIO(file_content)
|
||||||
|
reader = csv.DictReader(csv_file)
|
||||||
|
|
||||||
|
print(f"csv_file(2) = {csv_file}")
|
||||||
|
print(f"reader = {reader}")
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for row in reader:
|
||||||
|
print(f" row={row}")
|
||||||
|
|
||||||
|
# オーナーの姓名を分割
|
||||||
|
owner_lastname, owner_firstname = self.split_full_name(row['owner_name'])
|
||||||
|
|
||||||
|
# パスワードをハッシュ化
|
||||||
|
hashed_password = make_password(row['password'])
|
||||||
|
|
||||||
|
# オーナーの誕生日の処理
|
||||||
|
owner_birthday = row.get('owner_birthday', '').strip()
|
||||||
|
owner_birth_date = self.convert_japanese_date(owner_birthday)
|
||||||
|
print(f" Owner birthday: {owner_birth_date}")
|
||||||
|
|
||||||
|
# オーナーの性別の処理
|
||||||
|
owner_sex = row.get('owner_sex', '').strip()
|
||||||
|
owner_is_female = owner_sex in ['女性','女','女子','female','girl','lady']
|
||||||
|
print(f" Owner sex: {owner_sex}, is_female: {owner_is_female}")
|
||||||
|
|
||||||
|
# ユーザーの取得または作成
|
||||||
|
user, created = CustomUser.objects.get_or_create(
|
||||||
|
email=row['email'],
|
||||||
|
defaults={
|
||||||
|
'password': hashed_password, # make_password(row['password'])
|
||||||
|
'lastname': owner_lastname,
|
||||||
|
'firstname': owner_firstname,
|
||||||
|
'date_of_birth': owner_birth_date,
|
||||||
|
'female': owner_is_female
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
# 既存ユーザーの場合、空のフィールドがあれば更新
|
||||||
|
should_update = False
|
||||||
|
update_fields = []
|
||||||
|
|
||||||
|
print(f" Checking existing user data for {user.email}:")
|
||||||
|
print(f" - Current lastname: '{user.lastname}'")
|
||||||
|
print(f" - Current firstname: '{user.firstname}'")
|
||||||
|
print(f" - Current birth date: {user.date_of_birth}")
|
||||||
|
print(f" - Current female: {user.female}")
|
||||||
|
|
||||||
|
# 姓が空またはNoneの場合
|
||||||
|
if not user.lastname or user.lastname.strip() == '':
|
||||||
|
user.lastname = owner_lastname
|
||||||
|
should_update = True
|
||||||
|
update_fields.append('lastname')
|
||||||
|
print(f" - Updating lastname to: {owner_lastname}")
|
||||||
|
|
||||||
|
# 名が空またはNoneの場合
|
||||||
|
if not user.firstname or user.firstname.strip() == '':
|
||||||
|
user.firstname = owner_firstname
|
||||||
|
should_update = True
|
||||||
|
update_fields.append('firstname')
|
||||||
|
print(f" - Updating firstname to: {owner_firstname}")
|
||||||
|
|
||||||
|
# 生年月日が空またはNoneの場合
|
||||||
|
if not user.date_of_birth and owner_birth_date:
|
||||||
|
user.date_of_birth = owner_birth_date
|
||||||
|
should_update = True
|
||||||
|
update_fields.append('date_of_birth')
|
||||||
|
print(f" - Updating birth date to: {owner_birth_date}")
|
||||||
|
|
||||||
|
# 性別が空またはNoneの場合
|
||||||
|
# Booleanフィールドなのでis None で判定
|
||||||
|
if user.female is None:
|
||||||
|
user.female = owner_is_female
|
||||||
|
should_update = True
|
||||||
|
update_fields.append('female')
|
||||||
|
print(f" - Updating female to: {owner_is_female}")
|
||||||
|
|
||||||
|
# パスワードが'登録済み'でない場合のみ更新
|
||||||
|
if row['password'] != '登録済み':
|
||||||
|
user.password = hashed_password
|
||||||
|
should_update = True
|
||||||
|
update_fields.append('password')
|
||||||
|
print(f" - Updating password")
|
||||||
|
|
||||||
|
# 変更があった場合のみ保存
|
||||||
|
if should_update:
|
||||||
|
try:
|
||||||
|
# 特定のフィールドのみを更新
|
||||||
|
user.save(update_fields=update_fields)
|
||||||
|
print(f" Updated user {user.email} fields: {', '.join(update_fields)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error updating user {user.email}: {str(e)}")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
print(f" No updates needed for user {user.email}")
|
||||||
|
|
||||||
|
|
||||||
|
print(f" user created...")
|
||||||
|
print(f" Owner member created: {user.lastname} {user.firstname}")
|
||||||
|
print(f" - Birthday: {user.date_of_birth}")
|
||||||
|
print(f" - Sex: {'Female' if user.female else 'Male'}")
|
||||||
|
|
||||||
|
# 適切なカテゴリを見つける
|
||||||
|
category = self.find_matching_category(
|
||||||
|
time=int(row['time']),
|
||||||
|
department=row['department']
|
||||||
|
)
|
||||||
|
|
||||||
|
if not category:
|
||||||
|
raise ValueError(
|
||||||
|
f"No matching category found for time={row['time']} minutes "
|
||||||
|
f"and department={row['department']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" Using category: {category.category_name}")
|
||||||
|
|
||||||
|
# Teamの作成(既存のチームがある場合は取得)
|
||||||
|
team, created = Team.objects.get_or_create(
|
||||||
|
team_name=row['team_name'],
|
||||||
|
defaults={
|
||||||
|
'owner': user,
|
||||||
|
'category': category
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 既存のチームの場合でもカテゴリを更新
|
||||||
|
if not created:
|
||||||
|
team.category = category
|
||||||
|
team.save()
|
||||||
|
|
||||||
|
print(" team created/updated...")
|
||||||
|
|
||||||
|
self.create_members(team, row)
|
||||||
|
|
||||||
|
# イベントの検索
|
||||||
|
try:
|
||||||
|
event_code = row['event_code']
|
||||||
|
event = NewEvent2.objects.get(event_name=event_code)
|
||||||
|
print(f" Found event: {event.event_name}")
|
||||||
|
except NewEvent2.DoesNotExist:
|
||||||
|
raise ValueError(f"Event with code {event_code} does not exist")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# エントリーの作成
|
||||||
|
entry = self.create_entry_with_number(
|
||||||
|
team=team,
|
||||||
|
category=category,
|
||||||
|
owner=user,
|
||||||
|
event=event,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" entry created...")
|
||||||
|
except ValidationError as e:
|
||||||
|
messages.error(request, str(e))
|
||||||
|
return redirect('..')
|
||||||
|
|
||||||
|
gifuroge_register = GifurogeRegister.objects.create(
|
||||||
|
event_code=row['event_code'],
|
||||||
|
time=int(row['time']),
|
||||||
|
owner_name_kana=row['owner_name_kana'],
|
||||||
|
owner_name=row['owner_name'],
|
||||||
|
owner_birthday=self.convert_japanese_date(row['owner_birthday']),
|
||||||
|
owner_sex=row['owner_sex'],
|
||||||
|
email=row['email'],
|
||||||
|
password=row['password'],
|
||||||
|
team_name=row['team_name'],
|
||||||
|
department=row['department'],
|
||||||
|
members_count=int(row['members_count']),
|
||||||
|
member2=row.get('member2', '') or None,
|
||||||
|
birthday2=self.convert_japanese_date(row.get('birthday2', '') ),
|
||||||
|
sex2=row.get('sex2', '') or None,
|
||||||
|
member3=row.get('member3', '') or None,
|
||||||
|
birthday3=self.convert_japanese_date(row.get('birthday3', '') ),
|
||||||
|
sex3=row.get('sex3', '') or None,
|
||||||
|
member4=row.get('member4', '') or None,
|
||||||
|
birthday4=self.convert_japanese_date(row.get('birthday4', '') ),
|
||||||
|
sex4=row.get('sex4', '') or None,
|
||||||
|
member5=row.get('member5', '') or None,
|
||||||
|
birthday5=self.convert_japanese_date(row.get('birthday5', '') ),
|
||||||
|
sex5=row.get('sex5', '') or None
|
||||||
|
)
|
||||||
|
print(f" saved gifuroge_register...")
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
messages.error(request, 'File encoding error. Please ensure the file is UTF-8 encoded.')
|
||||||
|
return redirect('..')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing row: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
messages.success(request, 'CSV file uploaded successfully')
|
||||||
|
return redirect('..')
|
||||||
|
|
||||||
|
return render(request, 'admin/rog/gifurogeregister/upload-csv.html')
|
||||||
|
|
||||||
class RogAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin):
|
class RogAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin):
|
||||||
list_display=['title', 'venue', 'at_date',]
|
list_display=['title', 'venue', 'at_date',]
|
||||||
|
|
||||||
@ -76,6 +673,7 @@ class UserAdminConfig(UserAdmin):
|
|||||||
data = {'form': form}
|
data = {'form': form}
|
||||||
return render(request, 'admin/load_users.html', data)
|
return render(request, 'admin/load_users.html', data)
|
||||||
|
|
||||||
|
"""
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields':('email', 'group', 'zekken_number', 'event_code', 'team_name',)}),
|
(None, {'fields':('email', 'group', 'zekken_number', 'event_code', 'team_name',)}),
|
||||||
('Permissions', {'fields':('is_staff', 'is_active', 'is_rogaining')}),
|
('Permissions', {'fields':('is_staff', 'is_active', 'is_rogaining')}),
|
||||||
@ -84,6 +682,35 @@ class UserAdminConfig(UserAdmin):
|
|||||||
add_fieldsets = (
|
add_fieldsets = (
|
||||||
(None, {'classes':('wide',), 'fields':('email', 'group','zekken_number', 'event_code', 'team_name', 'password1', 'password2')}),
|
(None, {'classes':('wide',), 'fields':('email', 'group','zekken_number', 'event_code', 'team_name', 'password1', 'password2')}),
|
||||||
)
|
)
|
||||||
|
"""
|
||||||
|
# readonly_fieldsを明示的に設定
|
||||||
|
readonly_fields = ('date_joined',) # 変更不可のフィールドのみを指定=>Personal Infoも編集可能にする。
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('email', 'password')}),
|
||||||
|
(_('Personal info'), {
|
||||||
|
'fields': ('firstname', 'lastname', 'date_of_birth', 'female'),
|
||||||
|
'classes': ('wide',) # フィールドの表示を広げる
|
||||||
|
}),
|
||||||
|
(_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_rogaining','user_permissions')}),
|
||||||
|
(_('Rogaining info'), {
|
||||||
|
'fields': ('zekken_number', 'event_code', 'team_name', 'group'),
|
||||||
|
'classes': ('wide',)
|
||||||
|
}),
|
||||||
|
(_('Important dates'), {
|
||||||
|
'fields': ('date_joined','last_login'),
|
||||||
|
'classes': ('wide',)
|
||||||
|
}), # 読み取り専用
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
#'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active', 'is_rogaining')}
|
||||||
|
'fields': ('email', 'password1', 'password2', 'lastname','firstname', 'date_of_birth', 'female','is_staff', 'is_active', 'is_rogaining')}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
search_fields = ('email', 'firstname', 'lastname', 'zekken_number', 'team_name')
|
||||||
|
ordering = ('email',)
|
||||||
|
|
||||||
class JpnSubPerfAdmin(LeafletGeoAdmin):
|
class JpnSubPerfAdmin(LeafletGeoAdmin):
|
||||||
search_fields = ('adm0_ja', 'adm1_ja', 'adm2_ja', 'name_modified', 'area_name',)
|
search_fields = ('adm0_ja', 'adm1_ja', 'adm2_ja', 'name_modified', 'area_name',)
|
||||||
@ -268,14 +895,42 @@ class TempUserAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
# CustomUserAdmin の修正(既存のものを更新)
|
# CustomUserAdmin の修正(既存のものを更新)
|
||||||
|
class CustomUserChangeForm(UserChangeForm):
|
||||||
|
class Meta(UserChangeForm.Meta):
|
||||||
|
model = CustomUser
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class CustomUserCreationForm(UserCreationForm):
|
||||||
|
class Meta(UserCreationForm.Meta):
|
||||||
|
model = CustomUser
|
||||||
|
fields = ('email', 'lastname', 'firstname', 'date_of_birth', 'female')
|
||||||
|
|
||||||
class CustomUserAdmin(UserAdmin):
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
form = CustomUserChangeForm
|
||||||
|
add_form = CustomUserCreationForm
|
||||||
|
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')
|
||||||
list_filter = ('is_staff', 'is_active', 'is_rogaining', 'group')
|
list_filter = ('is_staff', 'is_active', 'is_rogaining', 'group')
|
||||||
|
|
||||||
|
# readonly_fieldsを明示的に設定
|
||||||
|
readonly_fields = ('date_joined',) # 変更不可のフィールドのみを指定=>Personal Infoも編集可能にする。
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('email', 'password')}),
|
(None, {'fields': ('email', 'password')}),
|
||||||
('Personal info', {'fields': ('firstname', 'lastname', 'date_of_birth', 'female')}),
|
(_('Personal info'), {
|
||||||
('Permissions', {'fields': ('is_staff', 'is_active', 'is_rogaining','user_permissions')}),
|
'fields': ('firstname', 'lastname', 'date_of_birth', 'female'),
|
||||||
('Rogaining info', {'fields': ('zekken_number', 'event_code', 'team_name', 'group')}),
|
'classes': ('wide',) # フィールドの表示を広げる
|
||||||
|
}),
|
||||||
|
(_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_rogaining','user_permissions')}),
|
||||||
|
(_('Rogaining info'), {
|
||||||
|
'fields': ('zekken_number', 'event_code', 'team_name', 'group'),
|
||||||
|
'classes': ('wide',)
|
||||||
|
}),
|
||||||
|
(_('Important dates'), {
|
||||||
|
'fields': ('date_joined','last_login'),
|
||||||
|
'classes': ('wide',)
|
||||||
|
}), # 読み取り専用
|
||||||
)
|
)
|
||||||
add_fieldsets = (
|
add_fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
@ -287,6 +942,14 @@ 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(self, request, obj=None):
|
||||||
|
# スーパーユーザーの場合は読み取り専用フィールドを最小限に
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return self.readonly_fields
|
||||||
|
# 通常のスタッフユーザーの場合は追加の制限を設定可能
|
||||||
|
return self.readonly_fields + ('is_staff', 'is_superuser')
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Useractions)
|
admin.site.register(Useractions)
|
||||||
admin.site.register(RogUser, admin.ModelAdmin)
|
admin.site.register(RogUser, admin.ModelAdmin)
|
||||||
admin.site.register(Location, LocationAdmin)
|
admin.site.register(Location, LocationAdmin)
|
||||||
|
|||||||
148
rog/migration_scripts.py
Normal file
148
rog/migration_scripts.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"""
|
||||||
|
このコードは永栄コードをNoufferコードに統合するための一時変換コードです。
|
||||||
|
一旦、完全にマイグレーションでき、ランキングや走行履歴が完成したら、不要になります。
|
||||||
|
"""
|
||||||
|
import psycopg2
|
||||||
|
from PIL import Image
|
||||||
|
import PIL.ExifTags
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
def get_gps_from_image(image_path):
|
||||||
|
"""
|
||||||
|
画像ファイルからGPS情報を抽出する
|
||||||
|
Returns: (latitude, longitude) または取得できない場合は (None, None)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with Image.open(image_path) as img:
|
||||||
|
exif = {
|
||||||
|
PIL.ExifTags.TAGS[k]: v
|
||||||
|
for k, v in img._getexif().items()
|
||||||
|
if k in PIL.ExifTags.TAGS
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'GPSInfo' in exif:
|
||||||
|
gps_info = exif['GPSInfo']
|
||||||
|
|
||||||
|
# 緯度の計算
|
||||||
|
lat = gps_info[2]
|
||||||
|
lat = lat[0] + lat[1]/60 + lat[2]/3600
|
||||||
|
if gps_info[1] == 'S':
|
||||||
|
lat = -lat
|
||||||
|
|
||||||
|
# 経度の計算
|
||||||
|
lon = gps_info[4]
|
||||||
|
lon = lon[0] + lon[1]/60 + lon[2]/3600
|
||||||
|
if gps_info[3] == 'W':
|
||||||
|
lon = -lon
|
||||||
|
|
||||||
|
return lat, lon
|
||||||
|
except Exception as e:
|
||||||
|
print(f"GPS情報の抽出に失敗: {e}")
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def migrate_data():
|
||||||
|
# コンテナ環境用の接続情報
|
||||||
|
source_db = {
|
||||||
|
'dbname': 'gifuroge',
|
||||||
|
'user': 'admin', # 環境に合わせて変更
|
||||||
|
'password': 'admin123456', # 環境に合わせて変更
|
||||||
|
'host': 'localhost', # Dockerのサービス名
|
||||||
|
'port': '5432'
|
||||||
|
}
|
||||||
|
|
||||||
|
target_db = {
|
||||||
|
'dbname': 'rogdb',
|
||||||
|
'user': 'admin', # 環境に合わせて変更
|
||||||
|
'password': 'admin123456', # 環境に合わせて変更
|
||||||
|
'host': 'localhost', # Dockerのサービス名
|
||||||
|
'port': '5432'
|
||||||
|
}
|
||||||
|
|
||||||
|
source_conn = None
|
||||||
|
target_conn = None
|
||||||
|
source_cur = None
|
||||||
|
target_cur = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("ソースDBへの接続を試みています...")
|
||||||
|
source_conn = psycopg2.connect(**source_db)
|
||||||
|
source_cur = source_conn.cursor()
|
||||||
|
print("ソースDBへの接続が成功しました")
|
||||||
|
|
||||||
|
print("ターゲットDBへの接続を試みています...")
|
||||||
|
target_conn = psycopg2.connect(**target_db)
|
||||||
|
target_cur = target_conn.cursor()
|
||||||
|
print("ターゲットDBへの接続が成功しました")
|
||||||
|
|
||||||
|
print("データの取得を開始します...")
|
||||||
|
source_cur.execute("""
|
||||||
|
SELECT serial_number, zekken_number, event_code, cp_number, image_address,
|
||||||
|
goal_time, late_point, create_at, create_user,
|
||||||
|
update_at, update_user, buy_flag, colabo_company_memo
|
||||||
|
FROM gps_information
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = source_cur.fetchall()
|
||||||
|
print(f"取得したレコード数: {len(rows)}")
|
||||||
|
|
||||||
|
processed_count = 0
|
||||||
|
for row in rows:
|
||||||
|
(serial_number, zekken_number, event_code, cp_number, image_address,
|
||||||
|
goal_time, late_point, create_at, create_user,
|
||||||
|
update_at, update_user, buy_flag, colabo_company_memo) = row
|
||||||
|
|
||||||
|
latitude, longitude = None, None
|
||||||
|
if image_address and os.path.exists(image_address):
|
||||||
|
latitude, longitude = get_gps_from_image(image_address)
|
||||||
|
|
||||||
|
target_cur.execute("""
|
||||||
|
INSERT INTO gps_checkins (
|
||||||
|
path_order, zekken_number, event_code, cp_number,
|
||||||
|
lattitude, longitude, image_address,
|
||||||
|
image_receipt, image_QR, validate_location,
|
||||||
|
goal_time, late_point, create_at,
|
||||||
|
create_user, update_at, update_user,
|
||||||
|
buy_flag, colabo_company_memo, points
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||||
|
)
|
||||||
|
""", (
|
||||||
|
serial_number,
|
||||||
|
zekken_number, event_code, cp_number,
|
||||||
|
latitude, longitude, image_address,
|
||||||
|
True, True, True,
|
||||||
|
goal_time, late_point, create_at,
|
||||||
|
create_user, update_at, update_user,
|
||||||
|
buy_flag if buy_flag is not None else False,
|
||||||
|
colabo_company_memo if colabo_company_memo else '',
|
||||||
|
0
|
||||||
|
))
|
||||||
|
|
||||||
|
processed_count += 1
|
||||||
|
if processed_count % 100 == 0:
|
||||||
|
print(f"処理済みレコード数: {processed_count}")
|
||||||
|
|
||||||
|
target_conn.commit()
|
||||||
|
print(f"移行完了: {processed_count}件のレコードを処理しました")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"エラーが発生しました: {e}")
|
||||||
|
if target_conn:
|
||||||
|
target_conn.rollback()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if source_cur:
|
||||||
|
source_cur.close()
|
||||||
|
if target_cur:
|
||||||
|
target_cur.close()
|
||||||
|
if source_conn:
|
||||||
|
source_conn.close()
|
||||||
|
if target_conn:
|
||||||
|
target_conn.close()
|
||||||
|
print("すべての接続をクローズしました")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate_data()
|
||||||
142
rog/models.py
142
rog/models.py
@ -72,6 +72,31 @@ def remove_bom_inplace(path):
|
|||||||
fp.seek(-bom_length, os.SEEK_CUR)
|
fp.seek(-bom_length, os.SEEK_CUR)
|
||||||
fp.truncate()
|
fp.truncate()
|
||||||
|
|
||||||
|
class GifurogeRegister(models.Model):
|
||||||
|
event_code = models.CharField(max_length=100)
|
||||||
|
time = models.IntegerField(choices=[(3, '3時間'), (5, '5時間')])
|
||||||
|
owner_name_kana = models.CharField(max_length=100)
|
||||||
|
owner_name = models.CharField(max_length=100)
|
||||||
|
email = models.EmailField()
|
||||||
|
password = models.CharField(max_length=100)
|
||||||
|
owner_birthday = models.DateField(blank=True,null=True)
|
||||||
|
owner_sex = models.CharField(max_length=10,blank=True,null=True)
|
||||||
|
team_name = models.CharField(max_length=100)
|
||||||
|
department = models.CharField(max_length=100)
|
||||||
|
members_count = models.IntegerField()
|
||||||
|
member2 = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
birthday2 = models.DateField(blank=True,null=True)
|
||||||
|
sex2 = models.CharField(max_length=10,blank=True,null=True)
|
||||||
|
member3 = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
birthday3 = models.DateField(blank=True,null=True)
|
||||||
|
sex3 = models.CharField(max_length=10,blank=True,null=True)
|
||||||
|
member4 = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
birthday4 = models.DateField(blank=True,null=True)
|
||||||
|
sex4 = models.CharField(max_length=10,blank=True,null=True)
|
||||||
|
member5 = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
birthday5 = models.DateField(blank=True,null=True)
|
||||||
|
sex5 = models.CharField(max_length=10,blank=True,null=True)
|
||||||
|
|
||||||
|
|
||||||
class CustomUserManager(BaseUserManager):
|
class CustomUserManager(BaseUserManager):
|
||||||
|
|
||||||
@ -345,7 +370,7 @@ class Member(models.Model):
|
|||||||
unique_together = ('team', 'user')
|
unique_together = ('team', 'user')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.team.zekken_number} - {self.user.lastname} {self.user.firstname}"
|
return f"{self.team.team_name} - {self.user.lastname} {self.user.firstname}"
|
||||||
|
|
||||||
#
|
#
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
@ -504,6 +529,121 @@ class CheckinImages(models.Model):
|
|||||||
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"))
|
||||||
|
|
||||||
|
class GpsCheckin(models.Model):
|
||||||
|
path_order = models.IntegerField(
|
||||||
|
null=False,
|
||||||
|
help_text="チェックポイントの順序番号"
|
||||||
|
)
|
||||||
|
zekken_number = models.TextField(
|
||||||
|
null=False,
|
||||||
|
help_text="ゼッケン番号"
|
||||||
|
)
|
||||||
|
event_code = models.TextField(
|
||||||
|
null=False,
|
||||||
|
help_text="イベントコード"
|
||||||
|
)
|
||||||
|
cp_number = models.IntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="チェックポイント番号"
|
||||||
|
)
|
||||||
|
lattitude = models.FloatField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="緯度:写真から取得"
|
||||||
|
)
|
||||||
|
longitude = models.FloatField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="経度:写真から取得"
|
||||||
|
)
|
||||||
|
image_address = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="チェックイン画像のパス"
|
||||||
|
)
|
||||||
|
image_receipt = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="レシート画像のパス"
|
||||||
|
)
|
||||||
|
image_qr = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="QRコードスキャンフラグ"
|
||||||
|
)
|
||||||
|
validate_location = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="位置情報検証フラグ:画像認識で検証した結果"
|
||||||
|
)
|
||||||
|
goal_time = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="ゴール時刻=ゴール時のみ使用される。画像から時刻を読み取り設定する。"
|
||||||
|
)
|
||||||
|
late_point = models.IntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="遅刻ポイント:ゴールの時刻が制限時間を超えた場合、1分につき-50点が加算。"
|
||||||
|
)
|
||||||
|
create_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="作成日時:データの作成日時"
|
||||||
|
)
|
||||||
|
create_user = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="作成ユーザー"
|
||||||
|
)
|
||||||
|
update_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="更新日時"
|
||||||
|
)
|
||||||
|
update_user = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="更新ユーザー"
|
||||||
|
)
|
||||||
|
buy_flag = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="購入フラグ:協賛店で購入した場合、無条件でTRUEにする。"
|
||||||
|
)
|
||||||
|
colabo_company_memo = models.TextField(
|
||||||
|
null=False,
|
||||||
|
default='',
|
||||||
|
help_text="グループコード:複数のイベントで合算する場合に使用する"
|
||||||
|
)
|
||||||
|
points = models.IntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="ポイント:このチェックインによる獲得ポイント。通常ポイントと買い物ポイントは分離される。ゴールの場合には減点なども含む。"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'gps_checkins'
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['zekken_number', 'event_code', 'path_order'],
|
||||||
|
name='unique_gps_checkin'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['zekken_number', 'event_code','path_order'], name='idx_zekken_event'),
|
||||||
|
models.Index(fields=['create_at'], name='idx_create_at'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.event_code}-{self.zekken_number}-{self.path_order}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# 作成時・更新時のタイムスタンプを自動設定
|
||||||
|
from django.utils import timezone
|
||||||
|
if not self.create_at:
|
||||||
|
self.create_at = timezone.now()
|
||||||
|
self.update_at = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class RogUser(models.Model):
|
class RogUser(models.Model):
|
||||||
user=models.OneToOneField(CustomUser, on_delete=models.CASCADE)
|
user=models.OneToOneField(CustomUser, on_delete=models.CASCADE)
|
||||||
|
|||||||
11
rog/templates/admin/gifuroge_register_changelist.html
Normal file
11
rog/templates/admin/gifuroge_register_changelist.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
{% load i18n admin_urls %}
|
||||||
|
|
||||||
|
{% block object-tools-items %}
|
||||||
|
{{ block.super }}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:gifuroge_register_upload_csv' %}" class="addlink">
|
||||||
|
{% blocktranslate with name=opts.verbose_name %}Upload CSV{% endblocktranslate %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
||||||
11
rog/templates/admin/rog/gifurogeregister/change_list.html
Normal file
11
rog/templates/admin/rog/gifurogeregister/change_list.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
{% load i18n admin_urls %}
|
||||||
|
|
||||||
|
{% block object-tools-items %}
|
||||||
|
{{ block.super }}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:gifuroge_register_upload_csv' %}" class="addlink">
|
||||||
|
{% translate "Upload CSV" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
||||||
22
rog/templates/admin/rog/gifurogeregister/upload-csv.html
Normal file
22
rog/templates/admin/rog/gifurogeregister/upload-csv.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load i18n admin_urls %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Upload CSV File</h1>
|
||||||
|
CSV のフォーマット:
|
||||||
|
イベントコード,時間(3 or 5),代表者かな,代表者名,メール,パスワード,代表者生年月日、代表者性別,チーム名,部門,メンバー数,メンバー2,誕生日2,性別2,メンバー3,誕生日3,性別3,メンバー4,誕生日4,性別4,メンバー5,誕生日5,性別5<br>
|
||||||
|
(例)<br>
|
||||||
|
event_code,time,owner_name_kana,owner_name,email,password,owner_birthday,owner_sex,team_name,department,members_count,member2,birthday2,sex2,member3,birthday3,sex3,member4,birthday4,sex4,member5,birthday5,sex5<br>
|
||||||
|
FC岐阜,3,みやたあきら,宮田 明,hannivalscipio@gmail.com,Sachiko123,タヌキの宮家,一般,3,宮田幸子,1965-4-4,female,川本勇,1965-1-1,male,,,,,,<br>
|
||||||
|
<br>
|
||||||
|
この形式のCSVをアップロードすると、以下を実施する。<br>
|
||||||
|
1)未登録のユーザーの登録をパスワードとともに生成<br>
|
||||||
|
2)チームを生成し、メンバーを登録<br>
|
||||||
|
3)イベントコードで示すイベントにエントリー<br>
|
||||||
|
<br>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="file" name="csv_file" required>
|
||||||
|
<button type="submit">Upload</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
134
rog/transfer.py
Normal file
134
rog/transfer.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import psycopg2
|
||||||
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
def get_db_connection(dbname: str) -> psycopg2.extensions.connection:
|
||||||
|
"""データベース接続を確立する"""
|
||||||
|
try:
|
||||||
|
return psycopg2.connect(
|
||||||
|
dbname=dbname,
|
||||||
|
user='your_username', # 実際のユーザー名に変更してください
|
||||||
|
password='your_password', # 実際のパスワードに変更してください
|
||||||
|
host='localhost' # 実際のホスト名に変更してください
|
||||||
|
)
|
||||||
|
except psycopg2.Error as e:
|
||||||
|
print(f"データベース {dbname} への接続エラー: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def get_source_data() -> List[Dict]:
|
||||||
|
"""rogdbからデータを取得する"""
|
||||||
|
conn = get_db_connection('rogdb')
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT DISTINCT ON (rci.user_id, rci.cp_number)
|
||||||
|
rci.team_name,
|
||||||
|
rci.event_code,
|
||||||
|
rci.cp_number,
|
||||||
|
rci.checkinimage,
|
||||||
|
rci.checkintime,
|
||||||
|
rci.user_id,
|
||||||
|
COALESCE(p.point, 0) as late_point
|
||||||
|
FROM rog_checkinimages rci
|
||||||
|
LEFT JOIN point p ON p.user_id = rci.user_id
|
||||||
|
AND p.event_code = rci.event_code
|
||||||
|
AND p.cp_number = rci.cp_number
|
||||||
|
WHERE rci.event_code = 'FC岐阜'
|
||||||
|
ORDER BY rci.user_id, rci.cp_number, rci.checkintime DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
columns = [desc[0] for desc in cur.description]
|
||||||
|
return [dict(zip(columns, row)) for row in cur.fetchall()]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_next_serial_number(cur) -> int:
|
||||||
|
"""次のserial_numberを取得する"""
|
||||||
|
cur.execute("SELECT nextval('gps_information_serial_number_seq')")
|
||||||
|
return cur.fetchone()[0]
|
||||||
|
|
||||||
|
def insert_into_target(data: List[Dict]) -> Tuple[int, List[str]]:
|
||||||
|
"""gifurogeデータベースにデータを挿入する"""
|
||||||
|
conn = get_db_connection('gifuroge')
|
||||||
|
inserted_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
for record in data:
|
||||||
|
try:
|
||||||
|
serial_number = get_next_serial_number(cur)
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO gps_information (
|
||||||
|
serial_number,
|
||||||
|
zekken_number,
|
||||||
|
event_code,
|
||||||
|
cp_number,
|
||||||
|
image_address,
|
||||||
|
goal_time,
|
||||||
|
late_point,
|
||||||
|
create_at,
|
||||||
|
create_user,
|
||||||
|
update_at,
|
||||||
|
update_user,
|
||||||
|
buy_flag,
|
||||||
|
minus_photo_flag,
|
||||||
|
colabo_company_memo
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
serial_number,
|
||||||
|
record['team_name'],
|
||||||
|
record['event_code'],
|
||||||
|
record['cp_number'],
|
||||||
|
record['checkinimage'],
|
||||||
|
record['checkintime'].strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
record['late_point'],
|
||||||
|
record['checkintime'],
|
||||||
|
'system',
|
||||||
|
record['checkintime'],
|
||||||
|
'system',
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
''
|
||||||
|
))
|
||||||
|
inserted_count += 1
|
||||||
|
except psycopg2.Error as e:
|
||||||
|
errors.append(f"Error inserting record for team {record['team_name']}: {str(e)}")
|
||||||
|
|
||||||
|
if inserted_count % 100 == 0:
|
||||||
|
print(f"Processed {inserted_count} records...")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
except psycopg2.Error as e:
|
||||||
|
conn.rollback()
|
||||||
|
errors.append(f"Transaction error: {str(e)}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return inserted_count, errors
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("データ移行を開始します...")
|
||||||
|
|
||||||
|
# ソースデータの取得
|
||||||
|
print("ソースデータを取得中...")
|
||||||
|
source_data = get_source_data()
|
||||||
|
print(f"取得したレコード数: {len(source_data)}")
|
||||||
|
|
||||||
|
# データの挿入
|
||||||
|
print("データを移行中...")
|
||||||
|
inserted_count, errors = insert_into_target(source_data)
|
||||||
|
|
||||||
|
# 結果の表示
|
||||||
|
print("\n=== 移行結果 ===")
|
||||||
|
print(f"処理したレコード数: {inserted_count}")
|
||||||
|
print(f"エラー数: {len(errors)}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\nエラーログ:")
|
||||||
|
for error in errors:
|
||||||
|
print(f"- {error}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
20
rogaining_autoprint/Dockerfile
Normal file
20
rogaining_autoprint/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# CUPSとその他必要なパッケージのインストール
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
cups \
|
||||||
|
cups-client \
|
||||||
|
gcc \
|
||||||
|
libcups2-dev \
|
||||||
|
python3-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY ./app .
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
|
|
||||||
144
rogaining_autoprint/README
Normal file
144
rogaining_autoprint/README
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
システム管理
|
||||||
|
|
||||||
|
1. 環境変数の設定
|
||||||
|
|
||||||
|
必要な環境変数:
|
||||||
|
envCopyAWS_ACCESS_KEY_ID=your_access_key
|
||||||
|
AWS_SECRET_ACCESS_KEY=your_secret_key
|
||||||
|
AWS_REGION=us-west-2
|
||||||
|
S3_BUCKET_NAME=sumasenrogaining
|
||||||
|
LOCATION=美濃加茂
|
||||||
|
PRINTER_NAME=Brother_HL_L3240CDW_series
|
||||||
|
|
||||||
|
|
||||||
|
2.プリンター設定
|
||||||
|
|
||||||
|
3.1 CUPSの設定
|
||||||
|
|
||||||
|
# CUPSの状態確認
|
||||||
|
systemctl status cups
|
||||||
|
|
||||||
|
# CUPSが動いていない場合は起動
|
||||||
|
sudo systemctl start cups
|
||||||
|
sudo systemctl enable cups
|
||||||
|
|
||||||
|
# プリンター一覧の確認
|
||||||
|
lpstat -p -d
|
||||||
|
|
||||||
|
3.2 プリンターの権限設定
|
||||||
|
|
||||||
|
# プリンターデバイスの確認
|
||||||
|
ls -l /dev/usb/lp*
|
||||||
|
|
||||||
|
# プリンターデバイスの権限設定
|
||||||
|
sudo chmod 666 /dev/usb/lp0
|
||||||
|
|
||||||
|
3.3 ネットワークプリンターの場合
|
||||||
|
|
||||||
|
config.yamlのprinterセクションを以下のように設定:
|
||||||
|
yamlCopyprinter:
|
||||||
|
name: ipp://192.168.1.100:631/ipp/port1
|
||||||
|
|
||||||
|
4. Dockerイメージのビルドと起動
|
||||||
|
4.1 イメージのビルド
|
||||||
|
bashCopy# イメージのビルド
|
||||||
|
docker-compose build
|
||||||
|
4.2 コンテナの起動
|
||||||
|
bashCopy# コンテナの起動(バックグラウンド)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# ログの確認
|
||||||
|
docker-compose logs -f
|
||||||
|
5. 動作確認
|
||||||
|
5.1 ログの確認
|
||||||
|
bashCopy# アプリケーションログの確認
|
||||||
|
tail -f logs/app.log
|
||||||
|
|
||||||
|
# エラーログの確認
|
||||||
|
tail -f logs/error.log
|
||||||
|
5.2 テスト印刷
|
||||||
|
bashCopy# テスト用PDFファイルをS3にアップロード
|
||||||
|
aws s3 cp test.pdf s3://sumasenrogaining/美濃加茂/scoreboard/test.pdf
|
||||||
|
5.3 プロセスの確認
|
||||||
|
bashCopy# コンテナの状態確認
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# コンテナ内のプロセス確認
|
||||||
|
docker-compose exec printer ps aux
|
||||||
|
6. トラブルシューティング
|
||||||
|
6.1 印刷できない場合
|
||||||
|
|
||||||
|
CUPSの状態確認
|
||||||
|
|
||||||
|
bashCopydocker-compose exec printer lpstat -t
|
||||||
|
|
||||||
|
プリンターの権限確認
|
||||||
|
|
||||||
|
bashCopydocker-compose exec printer ls -l /dev/usb/lp0
|
||||||
|
|
||||||
|
プリンター接続の確認
|
||||||
|
|
||||||
|
bashCopydocker-compose exec printer lpinfo -v
|
||||||
|
6.2 S3接続エラーの場合
|
||||||
|
|
||||||
|
認証情報の確認
|
||||||
|
|
||||||
|
bashCopydocker-compose exec printer env | grep AWS
|
||||||
|
|
||||||
|
S3バケットへのアクセス確認
|
||||||
|
|
||||||
|
bashCopydocker-compose exec printer python3 -c "
|
||||||
|
import boto3
|
||||||
|
s3 = boto3.client('s3')
|
||||||
|
response = s3.list_objects_v2(Bucket='sumasenrogaining')
|
||||||
|
print(response)
|
||||||
|
"
|
||||||
|
6.3 ログの確認方法
|
||||||
|
bashCopy# 直近のエラーログ
|
||||||
|
docker-compose exec printer tail -f /logs/error.log
|
||||||
|
|
||||||
|
# アプリケーションログ
|
||||||
|
docker-compose exec printer tail -f /logs/app.log
|
||||||
|
7. 運用とメンテナンス
|
||||||
|
7.1 定期的なメンテナンス
|
||||||
|
bashCopy# ログローテーション確認
|
||||||
|
ls -l logs/
|
||||||
|
|
||||||
|
# ディスク使用量確認
|
||||||
|
du -sh data/ logs/
|
||||||
|
|
||||||
|
# 古いPDFファイルの削除
|
||||||
|
find data/files -name "*.pdf" -mtime +30 -delete
|
||||||
|
7.2 バックアップ
|
||||||
|
bashCopy# 設定ファイルのバックアップ
|
||||||
|
tar czf config-backup-$(date +%Y%m%d).tar.gz .env config.yaml
|
||||||
|
|
||||||
|
# 処理済みファイルログのバックアップ
|
||||||
|
cp data/${LOCATION}.filelog data/${LOCATION}.filelog.bak
|
||||||
|
7.3 アップデート手順
|
||||||
|
bashCopy# コンテナの停止
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 新しいイメージのビルド
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# コンテナの再起動
|
||||||
|
docker-compose up -d
|
||||||
|
8. セキュリティ考慮事項
|
||||||
|
8.1 認証情報の管理
|
||||||
|
|
||||||
|
.envファイルのパーミッションを600に設定
|
||||||
|
AWSアクセスキーは定期的にローテーション
|
||||||
|
本番環境では.envファイルをgitにコミットしない
|
||||||
|
|
||||||
|
8.2 ネットワークセキュリティ
|
||||||
|
|
||||||
|
コンテナネットワークは必要最小限の公開
|
||||||
|
プリンターアクセスは内部ネットワークに制限
|
||||||
|
AWS S3へのアクセスは特定のIPに制限
|
||||||
|
|
||||||
|
8.3 モニタリング
|
||||||
|
|
||||||
|
CloudWatchなどでS3アクセスログを監視
|
||||||
|
印刷ジョブの異常を検知して通知
|
||||||
|
エラーログの定期チェック
|
||||||
174
rogaining_autoprint/app/main.py
Normal file
174
rogaining_autoprint/app/main.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import cups
|
||||||
|
import yaml
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
load_dotenv()
|
||||||
|
with open('config.yaml', 'r') as f:
|
||||||
|
self.config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# 環境変数で設定値を上書き
|
||||||
|
self.config['s3']['bucket'] = os.getenv('S3_BUCKET_NAME', self.config['s3']['bucket'])
|
||||||
|
self.config['s3']['prefix'] = self.config['s3']['prefix'].replace('${LOCATION}', os.getenv('LOCATION', ''))
|
||||||
|
self.config['printer']['name'] = os.getenv('PRINTER_NAME', self.config['printer']['name'])
|
||||||
|
|
||||||
|
class S3Manager:
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
self.config = config
|
||||||
|
self.s3_client = boto3.client('s3')
|
||||||
|
|
||||||
|
def list_files(self) -> List[str]:
|
||||||
|
"""S3バケットのファイル一覧を取得"""
|
||||||
|
try:
|
||||||
|
files = []
|
||||||
|
paginator = self.s3_client.get_paginator('list_objects_v2')
|
||||||
|
for page in paginator.paginate(
|
||||||
|
Bucket=self.config.config['s3']['bucket'],
|
||||||
|
Prefix=self.config.config['s3']['prefix']
|
||||||
|
):
|
||||||
|
if 'Contents' in page:
|
||||||
|
files.extend([obj['Key'] for obj in page['Contents']])
|
||||||
|
return files
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"S3ファイル一覧の取得に失敗: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def download_file(self, key: str, local_path: str) -> bool:
|
||||||
|
"""S3からファイルをダウンロード"""
|
||||||
|
try:
|
||||||
|
self.s3_client.download_file(
|
||||||
|
self.config.config['s3']['bucket'],
|
||||||
|
key,
|
||||||
|
local_path
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"ファイルのダウンロードに失敗 {key}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
class PrinterManager:
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
self.config = config
|
||||||
|
self.conn = cups.Connection()
|
||||||
|
|
||||||
|
def print_file(self, filepath: str) -> bool:
|
||||||
|
"""ファイルを印刷"""
|
||||||
|
printer_config = self.config.config['printer']
|
||||||
|
max_attempts = printer_config['retry']['max_attempts']
|
||||||
|
delay = printer_config['retry']['delay_seconds']
|
||||||
|
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
try:
|
||||||
|
job_id = self.conn.printFile(
|
||||||
|
printer_config['name'],
|
||||||
|
filepath,
|
||||||
|
"Rogaining Score",
|
||||||
|
printer_config['options']
|
||||||
|
)
|
||||||
|
logger.info(f"印刷ジョブを送信: {filepath} (Job ID: {job_id})")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"印刷に失敗 (試行 {attempt + 1}/{max_attempts}): {e}")
|
||||||
|
if attempt < max_attempts - 1:
|
||||||
|
time.sleep(delay)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class FileManager:
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
self.config = config
|
||||||
|
self.file_log_path = Path(self.config.config['app']['save_path']) / self.config.config['app']['file_log']
|
||||||
|
self.processed_files = self._load_processed_files()
|
||||||
|
|
||||||
|
def _load_processed_files(self) -> List[str]:
|
||||||
|
"""処理済みファイルの一覧を読み込み"""
|
||||||
|
try:
|
||||||
|
if self.file_log_path.exists():
|
||||||
|
with open(self.file_log_path, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"処理済みファイル一覧の読み込みに失敗: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_processed_files(self):
|
||||||
|
"""処理済みファイルの一覧を保存"""
|
||||||
|
try:
|
||||||
|
self.file_log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(self.file_log_path, 'w') as f:
|
||||||
|
json.dump(self.processed_files, f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"処理済みファイル一覧の保存に失敗: {e}")
|
||||||
|
|
||||||
|
def get_new_files(self, current_files: List[str]) -> List[str]:
|
||||||
|
"""新規ファイルを特定"""
|
||||||
|
return list(set(current_files) - set(self.processed_files))
|
||||||
|
|
||||||
|
def setup_logging(config: Config):
|
||||||
|
"""ロギングの設定"""
|
||||||
|
log_path = Path(config.config['app']['log_path'])
|
||||||
|
log_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
logger.add(
|
||||||
|
log_path / "app.log",
|
||||||
|
rotation="1 day",
|
||||||
|
retention="30 days",
|
||||||
|
level="INFO"
|
||||||
|
)
|
||||||
|
logger.add(
|
||||||
|
log_path / config.config['app']['error_log'],
|
||||||
|
level="ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = Config()
|
||||||
|
setup_logging(config)
|
||||||
|
|
||||||
|
s3_manager = S3Manager(config)
|
||||||
|
printer_manager = PrinterManager(config)
|
||||||
|
file_manager = FileManager(config)
|
||||||
|
|
||||||
|
logger.info("スコアボード監視システムを開始")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# S3のファイル一覧を取得
|
||||||
|
current_files = s3_manager.list_files()
|
||||||
|
|
||||||
|
# 新規ファイルを特定
|
||||||
|
new_files = file_manager.get_new_files(current_files)
|
||||||
|
|
||||||
|
for file_key in new_files:
|
||||||
|
if not file_key.endswith('.pdf'):
|
||||||
|
file_manager.processed_files.append(file_key)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 保存先パスの設定
|
||||||
|
local_path = Path(config.config['app']['save_path']) / Path(file_key).name
|
||||||
|
local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ファイルのダウンロードと印刷
|
||||||
|
if s3_manager.download_file(file_key, str(local_path)):
|
||||||
|
if printer_manager.print_file(str(local_path)):
|
||||||
|
file_manager.processed_files.append(file_key)
|
||||||
|
file_manager.save_processed_files()
|
||||||
|
|
||||||
|
time.sleep(10) # ポーリング間隔
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"予期せぬエラーが発生: {e}")
|
||||||
|
time.sleep(30) # エラー時は長めの待機
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
19
rogaining_autoprint/config.yaml
Normal file
19
rogaining_autoprint/config.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
app:
|
||||||
|
save_path: /data/files
|
||||||
|
log_path: /logs
|
||||||
|
file_log: ${LOCATION}.filelog
|
||||||
|
error_log: error.log
|
||||||
|
|
||||||
|
s3:
|
||||||
|
bucket: ${S3_BUCKET_NAME}
|
||||||
|
prefix: ${LOCATION}/scoreboard/
|
||||||
|
|
||||||
|
printer:
|
||||||
|
name: ${PRINTER_NAME}
|
||||||
|
options:
|
||||||
|
PageSize: A4
|
||||||
|
orientation: landscape
|
||||||
|
sides: two-sided-long-edge
|
||||||
|
retry:
|
||||||
|
max_attempts: 3
|
||||||
|
delay_seconds: 5
|
||||||
20
rogaining_autoprint/docker-compose.yaml
Normal file
20
rogaining_autoprint/docker-compose.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
printer:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ./app:/app
|
||||||
|
- ./data:/data
|
||||||
|
- ./logs:/logs
|
||||||
|
environment:
|
||||||
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||||
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||||
|
- AWS_DEFAULT_REGION=${AWS_REGION}
|
||||||
|
restart: always
|
||||||
|
devices:
|
||||||
|
- "/dev/usb/lp0:/dev/usb/lp0"
|
||||||
|
privileged: true # プリンターアクセスに必要
|
||||||
|
|
||||||
5
rogaining_autoprint/requirements.txt
Normal file
5
rogaining_autoprint/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
boto3==1.34.11
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
pycups==2.0.1
|
||||||
|
PyYAML==6.0.1
|
||||||
|
loguru==0.7.2
|
||||||
406
tmp_point.txt
Normal file
406
tmp_point.txt
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
Best Wishes 71 2024-10-26 00:42:27+00
|
||||||
|
Best Wishes 71 2024-10-26 00:42:28+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:09+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:10+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:11+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:12+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:13+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:13+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:14+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:35+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:37+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:38+00
|
||||||
|
Best Wishes 74 2024-10-26 00:57:38+00
|
||||||
|
Best Wishes 72 2024-10-26 00:59:10+00
|
||||||
|
Best Wishes 72 2024-10-26 00:59:10+00
|
||||||
|
Best Wishes 5 2024-10-26 01:05:44+00
|
||||||
|
Best Wishes 5 2024-10-26 01:05:46+00
|
||||||
|
Best Wishes 5 2024-10-26 01:05:48+00
|
||||||
|
Best Wishes 4 2024-10-26 01:09:38+00
|
||||||
|
Best Wishes 4 2024-10-26 01:09:40+00
|
||||||
|
Best Wishes 63 2024-10-26 01:13:20+00
|
||||||
|
Best Wishes 63 2024-10-26 01:13:23+00
|
||||||
|
Best Wishes 64 2024-10-26 01:15:04+00
|
||||||
|
Best Wishes 64 2024-10-26 01:15:08+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:27+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:28+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:29+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:29+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:29+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:29+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:29+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:29+00
|
||||||
|
Best Wishes 67 2024-10-26 01:44:30+00
|
||||||
|
Best Wishes 68 2024-10-26 02:01:53+00
|
||||||
|
Best Wishes 68 2024-10-26 02:01:56+00
|
||||||
|
Best Wishes 69 2024-10-26 02:06:20+00
|
||||||
|
Best Wishes 69 2024-10-26 02:06:23+00
|
||||||
|
Best Wishes 69 2024-10-26 02:06:23+00
|
||||||
|
Best Wishes 69 2024-10-26 02:06:23+00
|
||||||
|
Best Wishes 70 2024-10-26 02:19:03+00
|
||||||
|
Best Wishes 52 2024-10-26 02:51:14+00
|
||||||
|
Best Wishes 52 2024-10-26 02:51:15+00
|
||||||
|
Best Wishes 52 2024-10-26 02:51:15+00
|
||||||
|
Best Wishes 52 2024-10-26 02:51:15+00
|
||||||
|
Best Wishes 52 2024-10-26 02:51:17+00
|
||||||
|
Best Wishes 52 2024-10-26 02:51:18+00
|
||||||
|
Best Wishes 48 2024-10-26 02:55:16+00
|
||||||
|
Best Wishes 48 2024-10-26 02:55:17+00
|
||||||
|
Best Wishes 48 2024-10-26 02:55:19+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:12+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:12+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:13+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:13+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:13+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:14+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:15+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:15+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:16+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:17+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:18+00
|
||||||
|
Best Wishes 51 2024-10-26 03:07:19+00
|
||||||
|
FC岐阜 71 2024-10-26 00:43:48+00
|
||||||
|
FC岐阜 73 2024-10-26 00:54:18+00
|
||||||
|
FC岐阜 73 2024-10-26 00:54:28+00
|
||||||
|
FC岐阜 73 2024-10-26 00:54:33+00
|
||||||
|
M sisters with D 71 2024-10-26 00:43:56+00
|
||||||
|
M sisters with D 74 2024-10-26 00:53:43+00
|
||||||
|
M sisters with D 64 2024-10-26 01:17:28+00
|
||||||
|
M sisters with D 67 2024-10-26 01:35:51+00
|
||||||
|
M sisters with D 61 2024-10-26 02:45:51+00
|
||||||
|
M sisters with D 63 2024-10-26 02:55:07+00
|
||||||
|
M sisters with D 8 2024-10-26 03:26:29+00
|
||||||
|
To the next chapter 71 2024-10-26 00:43:14+00
|
||||||
|
To the next chapter 71 2024-10-26 00:43:48+00
|
||||||
|
To the next chapter 6 2024-10-26 01:14:09+00
|
||||||
|
To the next chapter 10 2024-10-26 01:26:00+00
|
||||||
|
To the next chapter 10 2024-10-26 01:26:24+00
|
||||||
|
To the next chapter 9 2024-10-26 01:34:14+00
|
||||||
|
To the next chapter 8 2024-10-26 01:38:45+00
|
||||||
|
To the next chapter 8 2024-10-26 01:39:04+00
|
||||||
|
To the next chapter 7 2024-10-26 01:43:50+00
|
||||||
|
To the next chapter 46 2024-10-26 02:01:08+00
|
||||||
|
To the next chapter 60 2024-10-26 02:14:02+00
|
||||||
|
To the next chapter 55 2024-10-26 02:19:23+00
|
||||||
|
To the next chapter 59 2024-10-26 02:32:25+00
|
||||||
|
To the next chapter 61 2024-10-26 02:43:34+00
|
||||||
|
To the next chapter 61 2024-10-26 02:43:51+00
|
||||||
|
To the next chapter 62 2024-10-26 02:49:33+00
|
||||||
|
To the next chapter 63 2024-10-26 02:53:32+00
|
||||||
|
To the next chapter 63 2024-10-26 02:53:48+00
|
||||||
|
To the next chapter 64 2024-10-26 02:57:50+00
|
||||||
|
To the next chapter 65 2024-10-26 03:02:26+00
|
||||||
|
To the next chapter 4 2024-10-26 03:10:58+00
|
||||||
|
To the next chapter 3 2024-10-26 03:17:01+00
|
||||||
|
To the next chapter 1 2024-10-26 03:25:46+00
|
||||||
|
akira 50 2024-10-18 04:38:41+00
|
||||||
|
akira 50 2024-10-18 04:38:42+00
|
||||||
|
akira 49 2024-10-18 04:39:11+00
|
||||||
|
akira 49 2024-10-18 04:39:15+00
|
||||||
|
akira 71 2024-10-18 08:53:29+00
|
||||||
|
akira 71 2024-10-19 02:26:52+00
|
||||||
|
akira 71 2024-10-19 02:27:08+00
|
||||||
|
akira 201 2024-10-19 02:40:56+00
|
||||||
|
akira 200 2024-10-19 09:12:57+00
|
||||||
|
akira 201 2024-10-20 00:12:44+00
|
||||||
|
akira 201 2024-10-20 00:12:48+00
|
||||||
|
akira 201 2024-10-20 00:14:57+00
|
||||||
|
akira 201 2024-10-20 00:17:08+00
|
||||||
|
akira 201 2024-10-20 01:40:29+00
|
||||||
|
akira 201 2024-10-20 09:36:37+00
|
||||||
|
akira 201 2024-10-20 09:36:58+00
|
||||||
|
akira 201 2024-10-20 10:03:54+00
|
||||||
|
akira 201 2024-10-20 12:18:54+00
|
||||||
|
akira 201 2024-10-20 13:25:15+00
|
||||||
|
akira 201 2024-10-20 13:30:20+00
|
||||||
|
akira 201 2024-10-20 13:39:37+00
|
||||||
|
akira 201 2024-10-20 13:42:25+00
|
||||||
|
akira 200 2024-10-20 21:25:58+00
|
||||||
|
akira 44 2024-10-20 21:27:26+00
|
||||||
|
akira 44 2024-10-20 21:28:22+00
|
||||||
|
best wishes - 71 2024-10-26 00:42:58+00
|
||||||
|
best wishes - 74 2024-10-26 01:01:29+00
|
||||||
|
best wishes - 5 2024-10-26 01:05:36+00
|
||||||
|
best wishes - 4 2024-10-26 01:09:43+00
|
||||||
|
best wishes - 63 2024-10-26 01:13:20+00
|
||||||
|
best wishes - 68 2024-10-26 02:05:33+00
|
||||||
|
best wishes - 68 2024-10-26 02:06:06+00
|
||||||
|
best wishes - 54 2024-10-26 02:37:06+00
|
||||||
|
best wishes - 57 2024-10-26 02:45:48+00
|
||||||
|
best wishes - 53 2024-10-26 02:49:23+00
|
||||||
|
best wishes - 52 2024-10-26 02:51:04+00
|
||||||
|
best wishes - 48 2024-10-26 02:55:18+00
|
||||||
|
best wishes - 51 2024-10-26 03:04:31+00
|
||||||
|
best wishes - 33 2024-10-26 03:21:46+00
|
||||||
|
しーくん 71 2024-10-26 00:43:21+00
|
||||||
|
しーくん 71 2024-10-26 00:43:50+00
|
||||||
|
しーくん 4 2024-10-26 01:10:14+00
|
||||||
|
しーくん 4 2024-10-26 01:10:17+00
|
||||||
|
しーくん 64 2024-10-26 01:19:27+00
|
||||||
|
しーくん 65 2024-10-26 01:26:59+00
|
||||||
|
しーくん 63 2024-10-26 03:09:32+00
|
||||||
|
しーくん 63 2024-10-26 03:15:39+00
|
||||||
|
しーくん 63 2024-10-26 03:16:07+00
|
||||||
|
しーくん 74 2024-10-26 03:39:10+00
|
||||||
|
たてない 71 2024-10-26 00:38:47+00
|
||||||
|
たてない 71 2024-10-26 00:43:23+00
|
||||||
|
たてない 71 2024-10-26 00:43:50+00
|
||||||
|
たてない 71 2024-10-26 00:44:16+00
|
||||||
|
たてない 71 2024-10-26 00:45:11+00
|
||||||
|
たてない 71 2024-10-26 00:46:19+00
|
||||||
|
たてない 71 2024-10-26 00:46:40+00
|
||||||
|
たてない 5 2024-10-26 01:10:46+00
|
||||||
|
たてない 3 2024-10-26 01:43:27+00
|
||||||
|
たてない 4 2024-10-26 01:59:04+00
|
||||||
|
たてない 63 2024-10-26 02:07:28+00
|
||||||
|
たてない 63 2024-10-26 02:09:07+00
|
||||||
|
たてない 63 2024-10-26 02:09:20+00
|
||||||
|
たてない 63 2024-10-26 02:09:47+00
|
||||||
|
たてない 64 2024-10-26 02:15:58+00
|
||||||
|
たてない 64 2024-10-26 02:16:11+00
|
||||||
|
たてない 62 2024-10-26 02:23:19+00
|
||||||
|
たてない 61 2024-10-26 02:27:58+00
|
||||||
|
たてない 61 2024-10-26 02:31:27+00
|
||||||
|
たてない 61 2024-10-26 02:31:38+00
|
||||||
|
たてない 61 2024-10-26 02:31:48+00
|
||||||
|
たてない 61 2024-10-26 02:32:00+00
|
||||||
|
たてない 7 2024-10-26 03:06:20+00
|
||||||
|
たてない 8 2024-10-26 03:11:47+00
|
||||||
|
たてない 8 2024-10-26 03:13:20+00
|
||||||
|
たてない 9 2024-10-26 03:17:54+00
|
||||||
|
たてない 9 2024-10-26 03:18:03+00
|
||||||
|
とみ 74 2024-10-26 00:35:01+00
|
||||||
|
とみ 74 2024-10-26 00:35:31+00
|
||||||
|
とみ 74 2024-10-26 00:35:36+00
|
||||||
|
とみ 74 2024-10-26 00:38:23+00
|
||||||
|
とみ 73 2024-10-26 00:47:27+00
|
||||||
|
なこさんず 71 2024-10-26 00:43:25+00
|
||||||
|
なこさんず 71 2024-10-26 00:43:55+00
|
||||||
|
なこさんず 69 2024-10-26 02:11:37+00
|
||||||
|
なこさんず 66 2024-10-26 02:42:15+00
|
||||||
|
なこさんず 61 2024-10-26 02:58:52+00
|
||||||
|
なこさんず 61 2024-10-26 02:59:45+00
|
||||||
|
なこさんず 62 2024-10-26 03:07:45+00
|
||||||
|
なこさんず 63 2024-10-26 03:12:20+00
|
||||||
|
なこさんず 63 2024-10-26 03:12:34+00
|
||||||
|
なこさんず 3 2024-10-26 03:26:54+00
|
||||||
|
むじょか 71 2024-10-26 00:43:06+00
|
||||||
|
むじょか 71 2024-10-26 00:43:21+00
|
||||||
|
むじょか 4 2024-10-26 01:06:08+00
|
||||||
|
むじょか 3 2024-10-26 01:12:00+00
|
||||||
|
むじょか 64 2024-10-26 01:20:39+00
|
||||||
|
むじょか 65 2024-10-26 01:26:17+00
|
||||||
|
むじょか 67 2024-10-26 01:44:53+00
|
||||||
|
むじょか 68 2024-10-26 02:05:57+00
|
||||||
|
むじょか 69 2024-10-26 02:09:25+00
|
||||||
|
むじょか 70 2024-10-26 02:19:16+00
|
||||||
|
むじょか 66 2024-10-26 02:29:29+00
|
||||||
|
むじょか 58 2024-10-26 02:40:42+00
|
||||||
|
むじょか 59 2024-10-26 02:47:30+00
|
||||||
|
むじょか 61 2024-10-26 03:00:13+00
|
||||||
|
むじょか 61 2024-10-26 03:01:57+00
|
||||||
|
むじょか 61 2024-10-26 03:02:17+00
|
||||||
|
むじょか 62 2024-10-26 03:13:21+00
|
||||||
|
むじょか 63 2024-10-26 03:18:15+00
|
||||||
|
むじょか 63 2024-10-26 03:19:21+00
|
||||||
|
むじょか 5 2024-10-26 03:39:49+00
|
||||||
|
ウエストサイド 71 2024-10-26 00:43:23+00
|
||||||
|
ウエストサイド 71 2024-10-26 00:43:52+00
|
||||||
|
ウエストサイド 4 2024-10-26 01:06:44+00
|
||||||
|
ウエストサイド 64 2024-10-26 01:14:05+00
|
||||||
|
ウエストサイド 65 2024-10-26 01:20:39+00
|
||||||
|
ウエストサイド 69 2024-10-26 01:38:38+00
|
||||||
|
ウエストサイド 68 2024-10-26 01:42:49+00
|
||||||
|
ウエストサイド 63 2024-10-26 02:06:58+00
|
||||||
|
ウエストサイド 63 2024-10-26 02:07:16+00
|
||||||
|
ウエストサイド 63 2024-10-26 02:07:33+00
|
||||||
|
ウエストサイド 62 2024-10-26 02:11:15+00
|
||||||
|
ウエストサイド 62 2024-10-26 02:11:20+00
|
||||||
|
ウエストサイド 61 2024-10-26 02:17:22+00
|
||||||
|
ウエストサイド 61 2024-10-26 02:17:38+00
|
||||||
|
ウエストサイド 61 2024-10-26 02:17:51+00
|
||||||
|
ウエストサイド 59 2024-10-26 02:29:42+00
|
||||||
|
ウエストサイド 59 2024-10-26 02:29:48+00
|
||||||
|
ウエストサイド 58 2024-10-26 02:36:56+00
|
||||||
|
ウエストサイド 55 2024-10-26 02:49:42+00
|
||||||
|
ウエストサイド 53 2024-10-26 03:00:37+00
|
||||||
|
ウエストサイド 52 2024-10-26 03:03:23+00
|
||||||
|
ウエストサイド 48 2024-10-26 03:07:51+00
|
||||||
|
ウエストサイド 56 2024-10-26 03:16:04+00
|
||||||
|
ウエストサイド 60 2024-10-26 03:23:17+00
|
||||||
|
チームエル 71 2024-10-26 00:42:13+00
|
||||||
|
チームエル 71 2024-10-26 00:42:41+00
|
||||||
|
チームエル 4 2024-10-26 01:06:10+00
|
||||||
|
チームエル 64 2024-10-26 01:11:45+00
|
||||||
|
チームエル 64 2024-10-26 01:11:55+00
|
||||||
|
チームエル 67 2024-10-26 01:30:22+00
|
||||||
|
チームエル 68 2024-10-26 01:55:38+00
|
||||||
|
チームエル 69 2024-10-26 01:58:56+00
|
||||||
|
チームエル 70 2024-10-26 02:11:30+00
|
||||||
|
チームエル 54 2024-10-26 02:29:59+00
|
||||||
|
チームエル 55 2024-10-26 02:47:49+00
|
||||||
|
チームエル 60 2024-10-26 02:53:00+00
|
||||||
|
チームエル 59 2024-10-26 03:03:31+00
|
||||||
|
チームエル 58 2024-10-26 03:10:53+00
|
||||||
|
チームエル 61 2024-10-26 03:21:21+00
|
||||||
|
チームエル 61 2024-10-26 03:21:32+00
|
||||||
|
チームエル 61 2024-10-26 03:22:22+00
|
||||||
|
チームエル 61 2024-10-26 03:22:51+00
|
||||||
|
チームエル 62 2024-10-26 03:24:50+00
|
||||||
|
チームエル 63 2024-10-26 03:28:21+00
|
||||||
|
チームエル 63 2024-10-26 03:28:32+00
|
||||||
|
チームエル 63 2024-10-26 03:28:45+00
|
||||||
|
チームエル 63 2024-10-26 03:29:07+00
|
||||||
|
ベル 71 2024-10-26 00:42:47+00
|
||||||
|
ベル 71 2024-10-26 00:43:40+00
|
||||||
|
ベル 71 2024-10-26 00:44:42+00
|
||||||
|
ベル 71 2024-10-26 00:45:02+00
|
||||||
|
ベル 7 2024-10-26 01:15:15+00
|
||||||
|
ベル 8 2024-10-26 01:19:36+00
|
||||||
|
ベル 8 2024-10-26 01:20:38+00
|
||||||
|
ベル 8 2024-10-26 01:20:50+00
|
||||||
|
ベル 8 2024-10-26 01:21:03+00
|
||||||
|
ベル 9 2024-10-26 01:24:20+00
|
||||||
|
ベル 9 2024-10-26 01:24:27+00
|
||||||
|
ベル 9 2024-10-26 01:24:32+00
|
||||||
|
ベル 9 2024-10-26 01:24:39+00
|
||||||
|
ベル 9 2024-10-26 01:24:43+00
|
||||||
|
ベル 10 2024-10-26 01:33:49+00
|
||||||
|
ベル 10 2024-10-26 01:34:05+00
|
||||||
|
ベル 6 2024-10-26 01:49:03+00
|
||||||
|
ベル 6 2024-10-26 01:49:09+00
|
||||||
|
ベル 6 2024-10-26 01:49:12+00
|
||||||
|
ベル 5 2024-10-26 02:08:20+00
|
||||||
|
ベル 5 2024-10-26 02:08:26+00
|
||||||
|
ベル 5 2024-10-26 02:08:33+00
|
||||||
|
ベル 63 2024-10-26 02:22:51+00
|
||||||
|
ベル 63 2024-10-26 02:23:40+00
|
||||||
|
ベル 63 2024-10-26 02:23:47+00
|
||||||
|
ベル 64 2024-10-26 02:28:03+00
|
||||||
|
ベル 65 2024-10-26 02:34:15+00
|
||||||
|
ベル 69 2024-10-26 02:49:23+00
|
||||||
|
ベル 69 2024-10-26 02:49:30+00
|
||||||
|
ベル 69 2024-10-26 02:49:32+00
|
||||||
|
ベル 68 2024-10-26 02:55:18+00
|
||||||
|
ベル 70 2024-10-26 03:11:45+00
|
||||||
|
ベル 70 2024-10-26 03:24:31+00
|
||||||
|
ベル 66 2024-10-26 03:25:04+00
|
||||||
|
ベル 61 2024-10-26 03:34:27+00
|
||||||
|
ベル 61 2024-10-26 03:34:44+00
|
||||||
|
ベル 61 2024-10-26 03:34:50+00
|
||||||
|
井口心平 202 2024-10-25 23:00:02+00
|
||||||
|
井口心平 201 2024-10-25 23:14:21+00
|
||||||
|
井口心平 200 2024-10-25 23:29:03+00
|
||||||
|
井口心平 71 2024-10-26 00:43:01+00
|
||||||
|
井口心平 71 2024-10-26 00:43:51+00
|
||||||
|
井口心平 4 2024-10-26 01:04:27+00
|
||||||
|
井口心平 64 2024-10-26 01:10:42+00
|
||||||
|
井口心平 67 2024-10-26 01:22:53+00
|
||||||
|
井口心平 69 2024-10-26 01:39:33+00
|
||||||
|
井口心平 70 2024-10-26 01:46:14+00
|
||||||
|
井口心平 54 2024-10-26 01:59:32+00
|
||||||
|
井口心平 57 2024-10-26 02:04:46+00
|
||||||
|
井口心平 53 2024-10-26 02:06:32+00
|
||||||
|
井口心平 56 2024-10-26 02:09:34+00
|
||||||
|
井口心平 52 2024-10-26 02:11:23+00
|
||||||
|
井口心平 48 2024-10-26 02:13:32+00
|
||||||
|
井口心平 47 2024-10-26 02:16:45+00
|
||||||
|
井口心平 42 2024-10-26 02:21:00+00
|
||||||
|
井口心平 43 2024-10-26 02:24:42+00
|
||||||
|
井口心平 39 2024-10-26 02:35:17+00
|
||||||
|
井口心平 37 2024-10-26 02:39:20+00
|
||||||
|
井口心平 37 2024-10-26 02:39:56+00
|
||||||
|
井口心平 38 2024-10-26 02:43:10+00
|
||||||
|
井口心平 26 2024-10-26 02:55:07+00
|
||||||
|
井口心平 25 2024-10-26 03:01:47+00
|
||||||
|
井口心平 23 2024-10-26 03:12:26+00
|
||||||
|
井口心平 21 2024-10-26 03:17:51+00
|
||||||
|
井口心平 20 2024-10-26 03:23:01+00
|
||||||
|
井口心平 20 2024-10-26 03:24:24+00
|
||||||
|
井口心平 20 2024-10-26 03:24:33+00
|
||||||
|
井口心平 17 2024-10-26 03:31:50+00
|
||||||
|
井口心平 16 2024-10-26 03:35:13+00
|
||||||
|
山本哲也 71 2024-10-26 00:43:06+00
|
||||||
|
山本哲也 71 2024-10-26 00:43:22+00
|
||||||
|
山本哲也 72 2024-10-26 01:00:13+00
|
||||||
|
山本哲也 4 2024-10-26 01:04:33+00
|
||||||
|
山本哲也 63 2024-10-26 01:07:42+00
|
||||||
|
山本哲也 64 2024-10-26 01:09:10+00
|
||||||
|
山本哲也 67 2024-10-26 01:20:44+00
|
||||||
|
山本哲也 68 2024-10-26 01:38:33+00
|
||||||
|
山本哲也 69 2024-10-26 01:40:20+00
|
||||||
|
山本哲也 70 2024-10-26 01:47:00+00
|
||||||
|
山本哲也 54 2024-10-26 01:59:18+00
|
||||||
|
山本哲也 57 2024-10-26 02:04:37+00
|
||||||
|
山本哲也 53 2024-10-26 02:06:26+00
|
||||||
|
山本哲也 52 2024-10-26 02:08:48+00
|
||||||
|
山本哲也 48 2024-10-26 02:10:31+00
|
||||||
|
山本哲也 47 2024-10-26 02:12:42+00
|
||||||
|
山本哲也 43 2024-10-26 02:18:59+00
|
||||||
|
山本哲也 42 2024-10-26 02:22:53+00
|
||||||
|
山本哲也 41 2024-10-26 02:32:01+00
|
||||||
|
山本哲也 19 2024-10-26 02:43:16+00
|
||||||
|
山本哲也 18 2024-10-26 02:49:42+00
|
||||||
|
山本哲也 45 2024-10-26 02:59:37+00
|
||||||
|
山本哲也 46 2024-10-26 03:08:36+00
|
||||||
|
山本哲也 7 2024-10-26 03:23:31+00
|
||||||
|
山本哲也 8 2024-10-26 03:25:33+00
|
||||||
|
山本哲也 8 2024-10-26 03:26:20+00
|
||||||
|
山本哲也 9 2024-10-26 03:28:14+00
|
||||||
|
岐阜県もりあげ隊 71 2024-10-26 00:43:32+00
|
||||||
|
岐阜県もりあげ隊 73 2024-10-26 00:54:25+00
|
||||||
|
岐阜県もりあげ隊 73 2024-10-26 00:54:43+00
|
||||||
|
岐阜県もりあげ隊 73 2024-10-26 00:54:48+00
|
||||||
|
岐阜県もりあげ隊 5 2024-10-26 01:16:30+00
|
||||||
|
岐阜県もりあげ隊 4 2024-10-26 01:35:10+00
|
||||||
|
岐阜県もりあげ隊 65 2024-10-26 01:54:16+00
|
||||||
|
岐阜県もりあげ隊 66 2024-10-26 02:03:32+00
|
||||||
|
岐阜県もりあげ隊 59 2024-10-26 02:18:53+00
|
||||||
|
岐阜県もりあげ隊 55 2024-10-26 03:20:35+00
|
||||||
|
岐阜県もりあげ隊 55 2024-10-26 03:20:53+00
|
||||||
|
岐阜県もりあげ隊 52 2024-10-26 03:32:12+00
|
||||||
|
岐阜県もりあげ隊 48 2024-10-26 03:39:51+00
|
||||||
|
岐阜県もりあげ隊 48 2024-10-26 03:40:07+00
|
||||||
|
細田典匡 71 2024-10-26 00:43:19+00
|
||||||
|
細田典匡 4 2024-10-26 01:04:20+00
|
||||||
|
細田典匡 63 2024-10-26 01:07:27+00
|
||||||
|
細田典匡 62 2024-10-26 01:09:31+00
|
||||||
|
細田典匡 61 2024-10-26 01:11:32+00
|
||||||
|
細田典匡 66 2024-10-26 01:15:23+00
|
||||||
|
細田典匡 66 2024-10-26 01:16:01+00
|
||||||
|
細田典匡 65 2024-10-26 01:17:54+00
|
||||||
|
細田典匡 67 2024-10-26 01:29:32+00
|
||||||
|
細田典匡 68 2024-10-26 01:42:42+00
|
||||||
|
細田典匡 69 2024-10-26 01:44:41+00
|
||||||
|
細田典匡 70 2024-10-26 01:50:56+00
|
||||||
|
細田典匡 54 2024-10-26 01:59:12+00
|
||||||
|
細田典匡 57 2024-10-26 02:04:43+00
|
||||||
|
細田典匡 53 2024-10-26 02:06:39+00
|
||||||
|
細田典匡 52 2024-10-26 02:08:55+00
|
||||||
|
細田典匡 52 2024-10-26 02:09:07+00
|
||||||
|
細田典匡 48 2024-10-26 02:10:43+00
|
||||||
|
細田典匡 47 2024-10-26 02:12:33+00
|
||||||
|
細田典匡 44 2024-10-26 02:19:38+00
|
||||||
|
細田典匡 43 2024-10-26 02:22:07+00
|
||||||
|
細田典匡 42 2024-10-26 02:24:43+00
|
||||||
|
細田典匡 41 2024-10-26 02:32:22+00
|
||||||
|
細田典匡 41 2024-10-26 02:32:36+00
|
||||||
|
細田典匡 18 2024-10-26 02:39:01+00
|
||||||
|
細田典匡 19 2024-10-26 02:44:48+00
|
||||||
|
細田典匡 20 2024-10-26 02:49:11+00
|
||||||
|
細田典匡 20 2024-10-26 02:49:26+00
|
||||||
|
細田典匡 17 2024-10-26 02:57:47+00
|
||||||
|
細田典匡 16 2024-10-26 03:01:51+00
|
||||||
|
細田典匡 14 2024-10-26 03:06:45+00
|
||||||
|
細田典匡 15 2024-10-26 03:12:55+00
|
||||||
|
細田典匡 13 2024-10-26 03:23:22+00
|
||||||
|
細田典匡 11 2024-10-26 03:36:13+00
|
||||||
|
風呂の会 204 2024-10-25 22:15:14+00
|
||||||
|
風呂の会 67 2024-10-26 01:35:21+00
|
||||||
|
風呂の会 68 2024-10-26 01:54:06+00
|
||||||
|
風呂の会 69 2024-10-26 01:58:26+00
|
||||||
|
風呂の会 70 2024-10-26 02:12:51+00
|
||||||
|
風呂の会 44 2024-10-26 03:03:42+00
|
||||||
|
齋藤貴美子 71 2024-10-26 00:54:21+00
|
||||||
|
齋藤貴美子 5 2024-10-26 01:06:29+00
|
||||||
|
齋藤貴美子 6 2024-10-26 01:20:28+00
|
||||||
Reference in New Issue
Block a user