永栄コードのマージ開始

This commit is contained in:
hayano
2024-10-27 18:22:01 +00:00
parent b8d7029965
commit 051916f9f6
15 changed files with 2237 additions and 319 deletions

View File

@ -37,6 +37,7 @@ services:
#entrypoint: ["/app/wait-for.sh", "postgres-db:5432", "--", ""]
#command: python3 manage.py runserver 0.0.0.0:8100
networks:
rog-api:
driver: bridge

View File

@ -1,10 +1,10 @@
import email
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 LeafletGeoAdminMixin
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.urls import path,reverse
from django.shortcuts import render
@ -16,6 +16,603 @@ from django.utils.html import format_html
from .forms import CSVUploadForm
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'))
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):
list_display=['title', 'venue', 'at_date',]
@ -76,6 +673,7 @@ class UserAdminConfig(UserAdmin):
data = {'form': form}
return render(request, 'admin/load_users.html', data)
"""
fieldsets = (
(None, {'fields':('email', 'group', 'zekken_number', 'event_code', 'team_name',)}),
('Permissions', {'fields':('is_staff', 'is_active', 'is_rogaining')}),
@ -84,6 +682,35 @@ class UserAdminConfig(UserAdmin):
add_fieldsets = (
(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):
search_fields = ('adm0_ja', 'adm1_ja', 'adm2_ja', 'name_modified', 'area_name',)
@ -268,14 +895,42 @@ class TempUserAdmin(admin.ModelAdmin):
# 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):
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_filter = ('is_staff', 'is_active', 'is_rogaining', 'group')
# readonly_fieldsを明示的に設定
readonly_fields = ('date_joined',) # 変更不可のフィールドのみを指定=>Personal Infoも編集可能にする。
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('firstname', 'lastname', 'date_of_birth', 'female')}),
('Permissions', {'fields': ('is_staff', 'is_active', 'is_rogaining','user_permissions')}),
('Rogaining info', {'fields': ('zekken_number', 'event_code', 'team_name', 'group')}),
(_('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, {
@ -287,6 +942,14 @@ class CustomUserAdmin(UserAdmin):
search_fields = ('email', 'firstname', 'lastname', 'zekken_number', 'team_name')
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(RogUser, admin.ModelAdmin)
admin.site.register(Location, LocationAdmin)

148
rog/migration_scripts.py Normal file
View 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()

View File

@ -72,6 +72,31 @@ def remove_bom_inplace(path):
fp.seek(-bom_length, os.SEEK_CUR)
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):
@ -345,7 +370,7 @@ class Member(models.Model):
unique_together = ('team', 'user')
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):
@ -504,6 +529,121 @@ class CheckinImages(models.Model):
event_code = models.CharField(_("event code"), max_length=255)
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):
user=models.OneToOneField(CustomUser, on_delete=models.CASCADE)

View 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 %}

View 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 %}

View 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
View 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()

View 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
View 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アクセスログを監視
印刷ジョブの異常を検知して通知
エラーログの定期チェック

View 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()

View 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

View 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 # プリンターアクセスに必要

View 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
View 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