永栄コードのマージ開始
This commit is contained in:
1299
rog/admin.py
1299
rog/admin.py
File diff suppressed because it is too large
Load Diff
148
rog/migration_scripts.py
Normal file
148
rog/migration_scripts.py
Normal file
@ -0,0 +1,148 @@
|
||||
"""
|
||||
このコードは永栄コードをNoufferコードに統合するための一時変換コードです。
|
||||
一旦、完全にマイグレーションでき、ランキングや走行履歴が完成したら、不要になります。
|
||||
"""
|
||||
import psycopg2
|
||||
from PIL import Image
|
||||
import PIL.ExifTags
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
def get_gps_from_image(image_path):
|
||||
"""
|
||||
画像ファイルからGPS情報を抽出する
|
||||
Returns: (latitude, longitude) または取得できない場合は (None, None)
|
||||
"""
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
exif = {
|
||||
PIL.ExifTags.TAGS[k]: v
|
||||
for k, v in img._getexif().items()
|
||||
if k in PIL.ExifTags.TAGS
|
||||
}
|
||||
|
||||
if 'GPSInfo' in exif:
|
||||
gps_info = exif['GPSInfo']
|
||||
|
||||
# 緯度の計算
|
||||
lat = gps_info[2]
|
||||
lat = lat[0] + lat[1]/60 + lat[2]/3600
|
||||
if gps_info[1] == 'S':
|
||||
lat = -lat
|
||||
|
||||
# 経度の計算
|
||||
lon = gps_info[4]
|
||||
lon = lon[0] + lon[1]/60 + lon[2]/3600
|
||||
if gps_info[3] == 'W':
|
||||
lon = -lon
|
||||
|
||||
return lat, lon
|
||||
except Exception as e:
|
||||
print(f"GPS情報の抽出に失敗: {e}")
|
||||
|
||||
return None, None
|
||||
|
||||
def migrate_data():
|
||||
# コンテナ環境用の接続情報
|
||||
source_db = {
|
||||
'dbname': 'gifuroge',
|
||||
'user': 'admin', # 環境に合わせて変更
|
||||
'password': 'admin123456', # 環境に合わせて変更
|
||||
'host': 'localhost', # Dockerのサービス名
|
||||
'port': '5432'
|
||||
}
|
||||
|
||||
target_db = {
|
||||
'dbname': 'rogdb',
|
||||
'user': 'admin', # 環境に合わせて変更
|
||||
'password': 'admin123456', # 環境に合わせて変更
|
||||
'host': 'localhost', # Dockerのサービス名
|
||||
'port': '5432'
|
||||
}
|
||||
|
||||
source_conn = None
|
||||
target_conn = None
|
||||
source_cur = None
|
||||
target_cur = None
|
||||
|
||||
try:
|
||||
print("ソースDBへの接続を試みています...")
|
||||
source_conn = psycopg2.connect(**source_db)
|
||||
source_cur = source_conn.cursor()
|
||||
print("ソースDBへの接続が成功しました")
|
||||
|
||||
print("ターゲットDBへの接続を試みています...")
|
||||
target_conn = psycopg2.connect(**target_db)
|
||||
target_cur = target_conn.cursor()
|
||||
print("ターゲットDBへの接続が成功しました")
|
||||
|
||||
print("データの取得を開始します...")
|
||||
source_cur.execute("""
|
||||
SELECT serial_number, zekken_number, event_code, cp_number, image_address,
|
||||
goal_time, late_point, create_at, create_user,
|
||||
update_at, update_user, buy_flag, colabo_company_memo
|
||||
FROM gps_information
|
||||
""")
|
||||
|
||||
rows = source_cur.fetchall()
|
||||
print(f"取得したレコード数: {len(rows)}")
|
||||
|
||||
processed_count = 0
|
||||
for row in rows:
|
||||
(serial_number, zekken_number, event_code, cp_number, image_address,
|
||||
goal_time, late_point, create_at, create_user,
|
||||
update_at, update_user, buy_flag, colabo_company_memo) = row
|
||||
|
||||
latitude, longitude = None, None
|
||||
if image_address and os.path.exists(image_address):
|
||||
latitude, longitude = get_gps_from_image(image_address)
|
||||
|
||||
target_cur.execute("""
|
||||
INSERT INTO gps_checkins (
|
||||
path_order, zekken_number, event_code, cp_number,
|
||||
lattitude, longitude, image_address,
|
||||
image_receipt, image_QR, validate_location,
|
||||
goal_time, late_point, create_at,
|
||||
create_user, update_at, update_user,
|
||||
buy_flag, colabo_company_memo, points
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||
)
|
||||
""", (
|
||||
serial_number,
|
||||
zekken_number, event_code, cp_number,
|
||||
latitude, longitude, image_address,
|
||||
True, True, True,
|
||||
goal_time, late_point, create_at,
|
||||
create_user, update_at, update_user,
|
||||
buy_flag if buy_flag is not None else False,
|
||||
colabo_company_memo if colabo_company_memo else '',
|
||||
0
|
||||
))
|
||||
|
||||
processed_count += 1
|
||||
if processed_count % 100 == 0:
|
||||
print(f"処理済みレコード数: {processed_count}")
|
||||
|
||||
target_conn.commit()
|
||||
print(f"移行完了: {processed_count}件のレコードを処理しました")
|
||||
|
||||
except Exception as e:
|
||||
print(f"エラーが発生しました: {e}")
|
||||
if target_conn:
|
||||
target_conn.rollback()
|
||||
|
||||
finally:
|
||||
if source_cur:
|
||||
source_cur.close()
|
||||
if target_cur:
|
||||
target_cur.close()
|
||||
if source_conn:
|
||||
source_conn.close()
|
||||
if target_conn:
|
||||
target_conn.close()
|
||||
print("すべての接続をクローズしました")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_data()
|
||||
142
rog/models.py
142
rog/models.py
@ -72,6 +72,31 @@ def remove_bom_inplace(path):
|
||||
fp.seek(-bom_length, os.SEEK_CUR)
|
||||
fp.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)
|
||||
|
||||
11
rog/templates/admin/gifuroge_register_changelist.html
Normal file
11
rog/templates/admin/gifuroge_register_changelist.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n admin_urls %}
|
||||
|
||||
{% block object-tools-items %}
|
||||
{{ block.super }}
|
||||
<li>
|
||||
<a href="{% url 'admin:gifuroge_register_upload_csv' %}" class="addlink">
|
||||
{% blocktranslate with name=opts.verbose_name %}Upload CSV{% endblocktranslate %}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
11
rog/templates/admin/rog/gifurogeregister/change_list.html
Normal file
11
rog/templates/admin/rog/gifurogeregister/change_list.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n admin_urls %}
|
||||
|
||||
{% block object-tools-items %}
|
||||
{{ block.super }}
|
||||
<li>
|
||||
<a href="{% url 'admin:gifuroge_register_upload_csv' %}" class="addlink">
|
||||
{% translate "Upload CSV" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
22
rog/templates/admin/rog/gifurogeregister/upload-csv.html
Normal file
22
rog/templates/admin/rog/gifurogeregister/upload-csv.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n admin_urls %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Upload CSV File</h1>
|
||||
CSV のフォーマット:
|
||||
イベントコード,時間(3 or 5),代表者かな,代表者名,メール,パスワード,代表者生年月日、代表者性別,チーム名,部門,メンバー数,メンバー2,誕生日2,性別2,メンバー3,誕生日3,性別3,メンバー4,誕生日4,性別4,メンバー5,誕生日5,性別5<br>
|
||||
(例)<br>
|
||||
event_code,time,owner_name_kana,owner_name,email,password,owner_birthday,owner_sex,team_name,department,members_count,member2,birthday2,sex2,member3,birthday3,sex3,member4,birthday4,sex4,member5,birthday5,sex5<br>
|
||||
FC岐阜,3,みやたあきら,宮田 明,hannivalscipio@gmail.com,Sachiko123,タヌキの宮家,一般,3,宮田幸子,1965-4-4,female,川本勇,1965-1-1,male,,,,,,<br>
|
||||
<br>
|
||||
この形式のCSVをアップロードすると、以下を実施する。<br>
|
||||
1)未登録のユーザーの登録をパスワードとともに生成<br>
|
||||
2)チームを生成し、メンバーを登録<br>
|
||||
3)イベントコードで示すイベントにエントリー<br>
|
||||
<br>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<input type="file" name="csv_file" required>
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
134
rog/transfer.py
Normal file
134
rog/transfer.py
Normal file
@ -0,0 +1,134 @@
|
||||
import psycopg2
|
||||
from datetime import datetime
|
||||
import sys
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
def get_db_connection(dbname: str) -> psycopg2.extensions.connection:
|
||||
"""データベース接続を確立する"""
|
||||
try:
|
||||
return psycopg2.connect(
|
||||
dbname=dbname,
|
||||
user='your_username', # 実際のユーザー名に変更してください
|
||||
password='your_password', # 実際のパスワードに変更してください
|
||||
host='localhost' # 実際のホスト名に変更してください
|
||||
)
|
||||
except psycopg2.Error as e:
|
||||
print(f"データベース {dbname} への接続エラー: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def get_source_data() -> List[Dict]:
|
||||
"""rogdbからデータを取得する"""
|
||||
conn = get_db_connection('rogdb')
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT ON (rci.user_id, rci.cp_number)
|
||||
rci.team_name,
|
||||
rci.event_code,
|
||||
rci.cp_number,
|
||||
rci.checkinimage,
|
||||
rci.checkintime,
|
||||
rci.user_id,
|
||||
COALESCE(p.point, 0) as late_point
|
||||
FROM rog_checkinimages rci
|
||||
LEFT JOIN point p ON p.user_id = rci.user_id
|
||||
AND p.event_code = rci.event_code
|
||||
AND p.cp_number = rci.cp_number
|
||||
WHERE rci.event_code = 'FC岐阜'
|
||||
ORDER BY rci.user_id, rci.cp_number, rci.checkintime DESC
|
||||
""")
|
||||
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
return [dict(zip(columns, row)) for row in cur.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_next_serial_number(cur) -> int:
|
||||
"""次のserial_numberを取得する"""
|
||||
cur.execute("SELECT nextval('gps_information_serial_number_seq')")
|
||||
return cur.fetchone()[0]
|
||||
|
||||
def insert_into_target(data: List[Dict]) -> Tuple[int, List[str]]:
|
||||
"""gifurogeデータベースにデータを挿入する"""
|
||||
conn = get_db_connection('gifuroge')
|
||||
inserted_count = 0
|
||||
errors = []
|
||||
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
for record in data:
|
||||
try:
|
||||
serial_number = get_next_serial_number(cur)
|
||||
cur.execute("""
|
||||
INSERT INTO gps_information (
|
||||
serial_number,
|
||||
zekken_number,
|
||||
event_code,
|
||||
cp_number,
|
||||
image_address,
|
||||
goal_time,
|
||||
late_point,
|
||||
create_at,
|
||||
create_user,
|
||||
update_at,
|
||||
update_user,
|
||||
buy_flag,
|
||||
minus_photo_flag,
|
||||
colabo_company_memo
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
serial_number,
|
||||
record['team_name'],
|
||||
record['event_code'],
|
||||
record['cp_number'],
|
||||
record['checkinimage'],
|
||||
record['checkintime'].strftime('%Y-%m-%d %H:%M:%S'),
|
||||
record['late_point'],
|
||||
record['checkintime'],
|
||||
'system',
|
||||
record['checkintime'],
|
||||
'system',
|
||||
False,
|
||||
False,
|
||||
''
|
||||
))
|
||||
inserted_count += 1
|
||||
except psycopg2.Error as e:
|
||||
errors.append(f"Error inserting record for team {record['team_name']}: {str(e)}")
|
||||
|
||||
if inserted_count % 100 == 0:
|
||||
print(f"Processed {inserted_count} records...")
|
||||
|
||||
conn.commit()
|
||||
except psycopg2.Error as e:
|
||||
conn.rollback()
|
||||
errors.append(f"Transaction error: {str(e)}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return inserted_count, errors
|
||||
|
||||
def main():
|
||||
print("データ移行を開始します...")
|
||||
|
||||
# ソースデータの取得
|
||||
print("ソースデータを取得中...")
|
||||
source_data = get_source_data()
|
||||
print(f"取得したレコード数: {len(source_data)}")
|
||||
|
||||
# データの挿入
|
||||
print("データを移行中...")
|
||||
inserted_count, errors = insert_into_target(source_data)
|
||||
|
||||
# 結果の表示
|
||||
print("\n=== 移行結果 ===")
|
||||
print(f"処理したレコード数: {inserted_count}")
|
||||
print(f"エラー数: {len(errors)}")
|
||||
|
||||
if errors:
|
||||
print("\nエラーログ:")
|
||||
for error in errors:
|
||||
print(f"- {error}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user