永栄コードのマージ開始

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", "--", ""] #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

View File

@ -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'))
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
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.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)

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