From 051916f9f6784a33159e4dc84d752bc388e53e98 Mon Sep 17 00:00:00 2001 From: hayano Date: Sun, 27 Oct 2024 18:22:01 +0000 Subject: [PATCH] =?UTF-8?q?=E6=B0=B8=E6=A0=84=E3=82=B3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=81=AE=E3=83=9E=E3=83=BC=E3=82=B8=E9=96=8B=E5=A7=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 1 + rog/admin.py | 1299 +++++++++++++---- rog/migration_scripts.py | 148 ++ rog/models.py | 142 +- .../admin/gifuroge_register_changelist.html | 11 + .../rog/gifurogeregister/change_list.html | 11 + .../rog/gifurogeregister/upload-csv.html | 22 + rog/transfer.py | 134 ++ rogaining_autoprint/Dockerfile | 20 + rogaining_autoprint/README | 144 ++ rogaining_autoprint/app/main.py | 174 +++ rogaining_autoprint/config.yaml | 19 + rogaining_autoprint/docker-compose.yaml | 20 + rogaining_autoprint/requirements.txt | 5 + tmp_point.txt | 406 ++++++ 15 files changed, 2237 insertions(+), 319 deletions(-) create mode 100644 rog/migration_scripts.py create mode 100644 rog/templates/admin/gifuroge_register_changelist.html create mode 100644 rog/templates/admin/rog/gifurogeregister/change_list.html create mode 100644 rog/templates/admin/rog/gifurogeregister/upload-csv.html create mode 100644 rog/transfer.py create mode 100644 rogaining_autoprint/Dockerfile create mode 100644 rogaining_autoprint/README create mode 100644 rogaining_autoprint/app/main.py create mode 100644 rogaining_autoprint/config.yaml create mode 100644 rogaining_autoprint/docker-compose.yaml create mode 100644 rogaining_autoprint/requirements.txt create mode 100644 tmp_point.txt diff --git a/docker-compose.yaml b/docker-compose.yaml index adfff2e..e091c8f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 diff --git a/rog/admin.py b/rog/admin.py index 9274d55..c23ba9a 100644 --- a/rog/admin.py +++ b/rog/admin.py @@ -1,318 +1,981 @@ -import email -from django.contrib import admin -from django.shortcuts import render -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 django.contrib.auth.admin import UserAdmin -from django.urls import path,reverse -from django.shortcuts import render -from django import forms; -import requests - -from django.http import HttpResponseRedirect -from django.utils.html import format_html -from .forms import CSVUploadForm -from .views import process_csv_upload - -class RogAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin): - list_display=['title', 'venue', 'at_date',] - -class ShopAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin): - list_display=['name',] - -class EventRouteAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin): - list_display=['name',] - -class ShopRouteAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin): - list_display=['name',] - -class loadUserForm(forms.Form): - server_url = forms.CharField(label="Load Data from *" ,initial='https://natnats.mobilous.com/get_team_list', widget=forms.Textarea(attrs={"rows":2, "cols":95})) - - -class UserAdminConfig(UserAdmin): - search_fields = ('email', 'group', 'zekken_number', 'event_code', 'team_name', 'is_rogaining') - list_filter = ('email', 'group', 'is_rogaining') - ordering = ('email',) - list_display = ('email', 'group','zekken_number', 'event_code', 'team_name', 'is_active', 'is_staff', 'is_rogaining') - - def get_urls(self): - urls = super().get_urls() - new_url = [path('load-users/', self.loadUsers),] - return new_url + urls - - def loadUsers(self, request): - - if request.method == "POST": - frm = loadUserForm(request.POST) - if frm.is_valid(): - print(frm.cleaned_data['server_url']) - #load json from server - url = frm.cleaned_data['server_url'] - response = requests.get(url) - data = response.json() - print("-------Event code--------") - print(data) - print("-------Event code--------") - for i in data: - _exist = CustomUser.objects.filter(email=i["zekken_number"]).delete() - other_fields.setDefaut('zekken_number',i['zekken_number']) - other_fields.setdefault('is_staff', True) - other_fields.setdefault('is_superuser', False) - other_fields.setdefault('is_active', True) - other_fields.setdefault('event_code', i['event_code']) - other_fields.setdefault('team_name', i['team_name']) - other_fields.setdefault('group', '大垣-初心者') - - usr = CustomUser.objects.create_user( - email=i["zekken_number"], - password=i['password'], - **other_fields - ) - - form = loadUserForm() - 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')}), - ) - - add_fieldsets = ( - (None, {'classes':('wide',), 'fields':('email', 'group','zekken_number', 'event_code', 'team_name', 'password1', 'password2')}), - ) - -class JpnSubPerfAdmin(LeafletGeoAdmin): - search_fields = ('adm0_ja', 'adm1_ja', 'adm2_ja', 'name_modified', 'area_name',) - list_filter = ('adm0_ja', 'adm0_ja', 'name_modified',) - ordering = ('adm0_ja',) - list_display = ('adm0_ja','adm1_ja','adm2_ja' ,'name_modified', 'area_name',) - -class LocationAdmin(LeafletGeoAdmin): - search_fields = ('location_id', 'cp', 'location_name', 'category', 'event_name','group',) - list_filter = ('event_name', 'group',) - ordering = ('location_id', 'cp',) - list_display = ('location_id','sub_loc_id', 'cp', 'location_name', 'photos', 'category', 'group', 'event_name', 'event_active', 'auto_checkin', 'checkin_radius', 'checkin_point', 'buy_point',) - - -def tranfer_to_location(modeladmin, request, queryset): - tmp_locs = templocation.objects.all(); - for l in tmp_locs : - found = Location.objects.filter(location_id = l.location_id).exists() - if found: - Location.objects.filter(location_id = l.location_id).update( - sub_loc_id = l.sub_loc_id, - cp = l.cp, - location_name = l.location_name, - category = l.category, - subcategory = l.subcategory, - zip = l.zip, - address = l.address, - prefecture = l.prefecture, - area = l.area, - city = l.city, - latitude = l.latitude, - longitude = l.longitude, - photos = l.photos, - videos = l.videos, - webcontents = l.webcontents, - status = l.status, - portal = l.portal, - group = l.group, - phone = l.phone, - fax = l.fax, - email = l.email, - facility = l.facility, - remark = l.remark, - tags = l.tags, - hidden_location = l.hidden_location, - auto_checkin = l.auto_checkin, - checkin_radius = l.checkin_radius, - checkin_point = l.checkin_point, - buy_point = l.buy_point, - evaluation_value = l.evaluation_value, - shop_closed = l.shop_closed, - shop_shutdown = l.shop_shutdown, - opening_hours_mon = l.opening_hours_mon, - opening_hours_tue = l.opening_hours_tue, - opening_hours_wed = l.opening_hours_wed, - opening_hours_thu = l.opening_hours_thu, - opening_hours_fri = l.opening_hours_fri, - opening_hours_sat = l.opening_hours_sat, - opening_hours_sun = l.opening_hours_sun, - geom=l.geom - ) - else: - loc = Location( - location_id=l.location_id, - sub_loc_id = l.sub_loc_id, - cp = l.cp, - location_name = l.location_name, - category = l.category, - subcategory = l.subcategory, - zip = l.zip, - address = l.address, - prefecture = l.prefecture, - area = l.area, - city = l.city, - latitude = l.latitude, - longitude = l.longitude, - photos = l.photos, - videos = l.videos, - webcontents = l.webcontents, - status = l.status, - portal = l.portal, - group = l.group, - phone = l.phone, - fax = l.fax, - email = l.email, - facility = l.facility, - remark = l.remark, - tags = l.tags, - hidden_location = l.hidden_location, - auto_checkin = l.auto_checkin, - checkin_radius = l.checkin_radius, - checkin_point = l.checkin_point, - buy_point = l.buy_point, - evaluation_value = l.evaluation_value, - shop_closed = l.shop_closed, - shop_shutdown = l.shop_shutdown, - opening_hours_mon = l.opening_hours_mon, - opening_hours_tue = l.opening_hours_tue, - opening_hours_wed = l.opening_hours_wed, - opening_hours_thu = l.opening_hours_thu, - opening_hours_fri = l.opening_hours_fri, - opening_hours_sat = l.opening_hours_sat, - opening_hours_sun = l.opening_hours_sun, - geom=l.geom - ) - loc.save() - l.delete() -tranfer_to_location.short_description = "Transfer all locations in temp table to location table" - - -class TempLocationAdmin(LeafletGeoAdmin): - search_fields = ('location_id', 'cp', 'location_name', 'category', 'event_name',) - list_filter = ('category', 'event_name',) - ordering = ('location_id', 'cp',) - list_display = ('location_id','cp', 'location_name', 'category', 'event_name', 'event_active', 'auto_checkin', 'checkin_radius', 'checkin_point', 'buy_point',) - actions = [tranfer_to_location,] - - -@admin.register(NewEvent2) -class NewEvent2Admin(admin.ModelAdmin): - list_display = ['event_name', 'start_datetime', 'end_datetime', 'csv_upload_button'] - - def get_urls(self): - urls = super().get_urls() - my_urls = [ - path('csv-upload/', self.admin_site.admin_view(self.csv_upload_view), name='newevent2_csv_upload'), - ] - return my_urls + urls - - def csv_upload_view(self, request): - if request.method == 'POST': - form = CSVUploadForm(request.POST, request.FILES) - if form.is_valid(): - csv_file = request.FILES['csv_file'] - event = form.cleaned_data['event'] - process_csv_upload(csv_file, event) - self.message_user(request, "CSV file has been processed successfully.") - return HttpResponseRedirect("../") - else: - form = CSVUploadForm() - - return render(request, 'admin/csv_upload.html', {'form': form}) - - def csv_upload_button(self, obj): - url = reverse('admin:newevent2_csv_upload') - return format_html('CSVアップロード', url) - csv_upload_button.short_description = 'CSV Upload' - - def changelist_view(self, request, extra_context=None): - extra_context = extra_context or {} - extra_context['csv_upload_url'] = reverse('admin:newevent2_csv_upload') - return super().changelist_view(request, extra_context=extra_context) - - -@admin.register(Team) -class TeamAdmin(admin.ModelAdmin): - list_display = ['team_name', 'owner'] - search_fields = ['team_name', 'owner__email'] - -@admin.register(NewCategory) -class NewCategoryAdmin(admin.ModelAdmin): - list_display = ['category_name', 'category_number', 'duration', 'num_of_member', 'family', 'female'] - list_filter = ['family', 'female'] - search_fields = ['category_name'] - -@admin.register(Entry) -class EntryAdmin(admin.ModelAdmin): - list_display = ['team', 'event', 'category', 'date'] - list_filter = ['event', 'category'] - search_fields = ['team__team_name', 'event__event_name'] - -@admin.register(Member) -class MemberAdmin(admin.ModelAdmin): - list_display = ['team', 'user'] - search_fields = ['team__team_name', 'user__email'] - -@admin.register(TempUser) -class TempUserAdmin(admin.ModelAdmin): - list_display = ['email', 'is_rogaining', 'zekken_number', 'event_code', 'team_name', 'group', 'created_at', 'expires_at'] - list_filter = ['is_rogaining', 'group'] - search_fields = ['email', 'zekken_number', 'team_name'] - - -# CustomUserAdmin の修正(既存のものを更新) -class CustomUserAdmin(UserAdmin): - 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') - 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')}), - ) - 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',) - -admin.site.register(Useractions) -admin.site.register(RogUser, admin.ModelAdmin) -admin.site.register(Location, LocationAdmin) -admin.site.register(SystemSettings, admin.ModelAdmin) -admin.site.register(JoinedEvent, admin.ModelAdmin) -admin.site.register(Favorite, admin.ModelAdmin) -admin.site.register(TravelList, admin.ModelAdmin) -admin.site.register(TravelPoint, admin.ModelAdmin) -admin.site.register(Event, admin.ModelAdmin) -admin.site.register(Location_line, LeafletGeoAdmin) -admin.site.register(Location_polygon, LeafletGeoAdmin) -admin.site.register(JpnAdminMainPerf, LeafletGeoAdmin) -admin.site.register(UserTracks, LeafletGeoAdmin); -#admin.site.register(JpnAdminPerf, LeafletGeoAdmin) -admin.site.register(GifuAreas, LeafletGeoAdmin) -admin.site.register(ShapeLayers, admin.ModelAdmin) -admin.site.register(UserUpload, admin.ModelAdmin) -admin.site.register(EventUser, admin.ModelAdmin) -#admin.site.register(UserUploadUser, admin.ModelAdmin) -#admin.site.register(ShapeFileLocations, admin.ModelAdmin) - -admin.site.register(CustomUser, UserAdminConfig) -admin.site.register(templocation, TempLocationAdmin) -admin.site.register(GoalImages, admin.ModelAdmin) -admin.site.register(CheckinImages, admin.ModelAdmin) - - - - +import email +from django.contrib import admin +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, 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 +from django import forms; +import requests + +from django.http import HttpResponseRedirect +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', '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',] + +class ShopAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin): + list_display=['name',] + +class EventRouteAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin): + list_display=['name',] + +class ShopRouteAdmin(LeafletAdminListMixin, LeafletGeoAdminMixin, admin.ModelAdmin): + list_display=['name',] + +class loadUserForm(forms.Form): + server_url = forms.CharField(label="Load Data from *" ,initial='https://natnats.mobilous.com/get_team_list', widget=forms.Textarea(attrs={"rows":2, "cols":95})) + + +class UserAdminConfig(UserAdmin): + search_fields = ('email', 'group', 'zekken_number', 'event_code', 'team_name', 'is_rogaining') + list_filter = ('email', 'group', 'is_rogaining') + ordering = ('email',) + list_display = ('email', 'group','zekken_number', 'event_code', 'team_name', 'is_active', 'is_staff', 'is_rogaining') + + def get_urls(self): + urls = super().get_urls() + new_url = [path('load-users/', self.loadUsers),] + return new_url + urls + + def loadUsers(self, request): + + if request.method == "POST": + frm = loadUserForm(request.POST) + if frm.is_valid(): + print(frm.cleaned_data['server_url']) + #load json from server + url = frm.cleaned_data['server_url'] + response = requests.get(url) + data = response.json() + print("-------Event code--------") + print(data) + print("-------Event code--------") + for i in data: + _exist = CustomUser.objects.filter(email=i["zekken_number"]).delete() + other_fields.setDefaut('zekken_number',i['zekken_number']) + other_fields.setdefault('is_staff', True) + other_fields.setdefault('is_superuser', False) + other_fields.setdefault('is_active', True) + other_fields.setdefault('event_code', i['event_code']) + other_fields.setdefault('team_name', i['team_name']) + other_fields.setdefault('group', '大垣-初心者') + + usr = CustomUser.objects.create_user( + email=i["zekken_number"], + password=i['password'], + **other_fields + ) + + form = loadUserForm() + 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')}), + ) + + 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',) + list_filter = ('adm0_ja', 'adm0_ja', 'name_modified',) + ordering = ('adm0_ja',) + list_display = ('adm0_ja','adm1_ja','adm2_ja' ,'name_modified', 'area_name',) + +class LocationAdmin(LeafletGeoAdmin): + search_fields = ('location_id', 'cp', 'location_name', 'category', 'event_name','group',) + list_filter = ('event_name', 'group',) + ordering = ('location_id', 'cp',) + list_display = ('location_id','sub_loc_id', 'cp', 'location_name', 'photos', 'category', 'group', 'event_name', 'event_active', 'auto_checkin', 'checkin_radius', 'checkin_point', 'buy_point',) + + +def tranfer_to_location(modeladmin, request, queryset): + tmp_locs = templocation.objects.all(); + for l in tmp_locs : + found = Location.objects.filter(location_id = l.location_id).exists() + if found: + Location.objects.filter(location_id = l.location_id).update( + sub_loc_id = l.sub_loc_id, + cp = l.cp, + location_name = l.location_name, + category = l.category, + subcategory = l.subcategory, + zip = l.zip, + address = l.address, + prefecture = l.prefecture, + area = l.area, + city = l.city, + latitude = l.latitude, + longitude = l.longitude, + photos = l.photos, + videos = l.videos, + webcontents = l.webcontents, + status = l.status, + portal = l.portal, + group = l.group, + phone = l.phone, + fax = l.fax, + email = l.email, + facility = l.facility, + remark = l.remark, + tags = l.tags, + hidden_location = l.hidden_location, + auto_checkin = l.auto_checkin, + checkin_radius = l.checkin_radius, + checkin_point = l.checkin_point, + buy_point = l.buy_point, + evaluation_value = l.evaluation_value, + shop_closed = l.shop_closed, + shop_shutdown = l.shop_shutdown, + opening_hours_mon = l.opening_hours_mon, + opening_hours_tue = l.opening_hours_tue, + opening_hours_wed = l.opening_hours_wed, + opening_hours_thu = l.opening_hours_thu, + opening_hours_fri = l.opening_hours_fri, + opening_hours_sat = l.opening_hours_sat, + opening_hours_sun = l.opening_hours_sun, + geom=l.geom + ) + else: + loc = Location( + location_id=l.location_id, + sub_loc_id = l.sub_loc_id, + cp = l.cp, + location_name = l.location_name, + category = l.category, + subcategory = l.subcategory, + zip = l.zip, + address = l.address, + prefecture = l.prefecture, + area = l.area, + city = l.city, + latitude = l.latitude, + longitude = l.longitude, + photos = l.photos, + videos = l.videos, + webcontents = l.webcontents, + status = l.status, + portal = l.portal, + group = l.group, + phone = l.phone, + fax = l.fax, + email = l.email, + facility = l.facility, + remark = l.remark, + tags = l.tags, + hidden_location = l.hidden_location, + auto_checkin = l.auto_checkin, + checkin_radius = l.checkin_radius, + checkin_point = l.checkin_point, + buy_point = l.buy_point, + evaluation_value = l.evaluation_value, + shop_closed = l.shop_closed, + shop_shutdown = l.shop_shutdown, + opening_hours_mon = l.opening_hours_mon, + opening_hours_tue = l.opening_hours_tue, + opening_hours_wed = l.opening_hours_wed, + opening_hours_thu = l.opening_hours_thu, + opening_hours_fri = l.opening_hours_fri, + opening_hours_sat = l.opening_hours_sat, + opening_hours_sun = l.opening_hours_sun, + geom=l.geom + ) + loc.save() + l.delete() +tranfer_to_location.short_description = "Transfer all locations in temp table to location table" + + +class TempLocationAdmin(LeafletGeoAdmin): + search_fields = ('location_id', 'cp', 'location_name', 'category', 'event_name',) + list_filter = ('category', 'event_name',) + ordering = ('location_id', 'cp',) + list_display = ('location_id','cp', 'location_name', 'category', 'event_name', 'event_active', 'auto_checkin', 'checkin_radius', 'checkin_point', 'buy_point',) + actions = [tranfer_to_location,] + + +@admin.register(NewEvent2) +class NewEvent2Admin(admin.ModelAdmin): + list_display = ['event_name', 'start_datetime', 'end_datetime', 'csv_upload_button'] + + def get_urls(self): + urls = super().get_urls() + my_urls = [ + path('csv-upload/', self.admin_site.admin_view(self.csv_upload_view), name='newevent2_csv_upload'), + ] + return my_urls + urls + + def csv_upload_view(self, request): + if request.method == 'POST': + form = CSVUploadForm(request.POST, request.FILES) + if form.is_valid(): + csv_file = request.FILES['csv_file'] + event = form.cleaned_data['event'] + process_csv_upload(csv_file, event) + self.message_user(request, "CSV file has been processed successfully.") + return HttpResponseRedirect("../") + else: + form = CSVUploadForm() + + return render(request, 'admin/csv_upload.html', {'form': form}) + + def csv_upload_button(self, obj): + url = reverse('admin:newevent2_csv_upload') + return format_html('CSVアップロード', url) + csv_upload_button.short_description = 'CSV Upload' + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + extra_context['csv_upload_url'] = reverse('admin:newevent2_csv_upload') + return super().changelist_view(request, extra_context=extra_context) + + +@admin.register(Team) +class TeamAdmin(admin.ModelAdmin): + list_display = ['team_name', 'owner'] + search_fields = ['team_name', 'owner__email'] + +@admin.register(NewCategory) +class NewCategoryAdmin(admin.ModelAdmin): + list_display = ['category_name', 'category_number', 'duration', 'num_of_member', 'family', 'female'] + list_filter = ['family', 'female'] + search_fields = ['category_name'] + +@admin.register(Entry) +class EntryAdmin(admin.ModelAdmin): + list_display = ['team', 'event', 'category', 'date'] + list_filter = ['event', 'category'] + search_fields = ['team__team_name', 'event__event_name'] + +@admin.register(Member) +class MemberAdmin(admin.ModelAdmin): + list_display = ['team', 'user'] + search_fields = ['team__team_name', 'user__email'] + +@admin.register(TempUser) +class TempUserAdmin(admin.ModelAdmin): + list_display = ['email', 'is_rogaining', 'zekken_number', 'event_code', 'team_name', 'group', 'created_at', 'expires_at'] + list_filter = ['is_rogaining', 'group'] + search_fields = ['email', 'zekken_number', 'team_name'] + + +# 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'), + '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',) + + 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) +admin.site.register(SystemSettings, admin.ModelAdmin) +admin.site.register(JoinedEvent, admin.ModelAdmin) +admin.site.register(Favorite, admin.ModelAdmin) +admin.site.register(TravelList, admin.ModelAdmin) +admin.site.register(TravelPoint, admin.ModelAdmin) +admin.site.register(Event, admin.ModelAdmin) +admin.site.register(Location_line, LeafletGeoAdmin) +admin.site.register(Location_polygon, LeafletGeoAdmin) +admin.site.register(JpnAdminMainPerf, LeafletGeoAdmin) +admin.site.register(UserTracks, LeafletGeoAdmin); +#admin.site.register(JpnAdminPerf, LeafletGeoAdmin) +admin.site.register(GifuAreas, LeafletGeoAdmin) +admin.site.register(ShapeLayers, admin.ModelAdmin) +admin.site.register(UserUpload, admin.ModelAdmin) +admin.site.register(EventUser, admin.ModelAdmin) +#admin.site.register(UserUploadUser, admin.ModelAdmin) +#admin.site.register(ShapeFileLocations, admin.ModelAdmin) + +admin.site.register(CustomUser, UserAdminConfig) +admin.site.register(templocation, TempLocationAdmin) +admin.site.register(GoalImages, admin.ModelAdmin) +admin.site.register(CheckinImages, admin.ModelAdmin) + + + + diff --git a/rog/migration_scripts.py b/rog/migration_scripts.py new file mode 100644 index 0000000..958ce04 --- /dev/null +++ b/rog/migration_scripts.py @@ -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() diff --git a/rog/models.py b/rog/models.py index d12d5d5..4339589 100644 --- a/rog/models.py +++ b/rog/models.py @@ -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) diff --git a/rog/templates/admin/gifuroge_register_changelist.html b/rog/templates/admin/gifuroge_register_changelist.html new file mode 100644 index 0000000..93000f4 --- /dev/null +++ b/rog/templates/admin/gifuroge_register_changelist.html @@ -0,0 +1,11 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} + {{ block.super }} +
  • + + {% blocktranslate with name=opts.verbose_name %}Upload CSV{% endblocktranslate %} + +
  • +{% endblock %} diff --git a/rog/templates/admin/rog/gifurogeregister/change_list.html b/rog/templates/admin/rog/gifurogeregister/change_list.html new file mode 100644 index 0000000..f17ba27 --- /dev/null +++ b/rog/templates/admin/rog/gifurogeregister/change_list.html @@ -0,0 +1,11 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} + {{ block.super }} +
  • + + {% translate "Upload CSV" %} + +
  • +{% endblock %} diff --git a/rog/templates/admin/rog/gifurogeregister/upload-csv.html b/rog/templates/admin/rog/gifurogeregister/upload-csv.html new file mode 100644 index 0000000..d789f82 --- /dev/null +++ b/rog/templates/admin/rog/gifurogeregister/upload-csv.html @@ -0,0 +1,22 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls %} + +{% block content %} +

    Upload CSV File

    + CSV のフォーマット: + イベントコード,時間(3 or 5),代表者かな,代表者名,メール,パスワード,代表者生年月日、代表者性別,チーム名,部門,メンバー数,メンバー2,誕生日2,性別2,メンバー3,誕生日3,性別3,メンバー4,誕生日4,性別4,メンバー5,誕生日5,性別5
    + (例)
    + 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
    + FC岐阜,3,みやたあきら,宮田 明,hannivalscipio@gmail.com,Sachiko123,タヌキの宮家,一般,3,宮田幸子,1965-4-4,female,川本勇,1965-1-1,male,,,,,,
    +
    + この形式のCSVをアップロードすると、以下を実施する。
    + 1)未登録のユーザーの登録をパスワードとともに生成
    + 2)チームを生成し、メンバーを登録
    + 3)イベントコードで示すイベントにエントリー
    +
    +
    + {% csrf_token %} + + +
    +{% endblock %} diff --git a/rog/transfer.py b/rog/transfer.py new file mode 100644 index 0000000..27a7c64 --- /dev/null +++ b/rog/transfer.py @@ -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() diff --git a/rogaining_autoprint/Dockerfile b/rogaining_autoprint/Dockerfile new file mode 100644 index 0000000..caa59a9 --- /dev/null +++ b/rogaining_autoprint/Dockerfile @@ -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"] + diff --git a/rogaining_autoprint/README b/rogaining_autoprint/README new file mode 100644 index 0000000..d7042f2 --- /dev/null +++ b/rogaining_autoprint/README @@ -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アクセスログを監視 +印刷ジョブの異常を検知して通知 +エラーログの定期チェック diff --git a/rogaining_autoprint/app/main.py b/rogaining_autoprint/app/main.py new file mode 100644 index 0000000..9a5e466 --- /dev/null +++ b/rogaining_autoprint/app/main.py @@ -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() + + diff --git a/rogaining_autoprint/config.yaml b/rogaining_autoprint/config.yaml new file mode 100644 index 0000000..d84be40 --- /dev/null +++ b/rogaining_autoprint/config.yaml @@ -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 diff --git a/rogaining_autoprint/docker-compose.yaml b/rogaining_autoprint/docker-compose.yaml new file mode 100644 index 0000000..5591c63 --- /dev/null +++ b/rogaining_autoprint/docker-compose.yaml @@ -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 # プリンターアクセスに必要 + diff --git a/rogaining_autoprint/requirements.txt b/rogaining_autoprint/requirements.txt new file mode 100644 index 0000000..c75e446 --- /dev/null +++ b/rogaining_autoprint/requirements.txt @@ -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 diff --git a/tmp_point.txt b/tmp_point.txt new file mode 100644 index 0000000..7d2d6ad --- /dev/null +++ b/tmp_point.txt @@ -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