Finish basic API implementation

This commit is contained in:
2025-08-27 15:01:06 +09:00
parent fff9bce9e7
commit cc9edb9932
19 changed files with 3844 additions and 5 deletions

View File

@ -6,6 +6,11 @@ from pyexpat import model
from sre_constants import CH_LOCALE
from typing import ChainMap
from django.contrib.gis.db import models
from django.contrib.postgres.fields import ArrayField
try:
from django.db.models import JSONField
except ImportError:
from django.contrib.postgres.fields import JSONField
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User
from django.db.models.signals import post_save, post_delete, pre_save
@ -308,6 +313,210 @@ class TempUser(models.Model):
def is_valid(self):
return timezone.now() <= self.expires_at
class AppVersion(models.Model):
"""アプリバージョン管理モデル"""
PLATFORM_CHOICES = [
('android', 'Android'),
('ios', 'iOS'),
]
version = models.CharField(max_length=20, help_text="セマンティックバージョン (1.2.3)")
platform = models.CharField(max_length=10, choices=PLATFORM_CHOICES)
build_number = models.CharField(max_length=20, blank=True, null=True)
is_latest = models.BooleanField(default=False, help_text="最新版フラグ")
is_required = models.BooleanField(default=False, help_text="強制更新フラグ")
update_message = models.TextField(blank=True, null=True, help_text="ユーザー向け更新メッセージ")
download_url = models.URLField(blank=True, null=True, help_text="アプリストアURL")
release_date = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'app_versions'
unique_together = ['version', 'platform']
indexes = [
models.Index(fields=['platform'], name='idx_app_versions_platform'),
models.Index(
fields=['is_latest'],
condition=models.Q(is_latest=True),
name='idx_app_versions_latest_true'
),
]
def __str__(self):
return f"{self.platform} {self.version}"
def save(self, *args, **kwargs):
"""最新版フラグが設定された場合、同一プラットフォームの他のバージョンを非最新にする"""
if self.is_latest:
AppVersion.objects.filter(
platform=self.platform,
is_latest=True
).exclude(pk=self.pk).update(is_latest=False)
super().save(*args, **kwargs)
@classmethod
def compare_versions(cls, version1, version2):
"""セマンティックバージョンの比較"""
def version_tuple(v):
return tuple(map(int, v.split('.')))
v1 = version_tuple(version1)
v2 = version_tuple(version2)
if v1 < v2:
return -1
elif v1 > v2:
return 1
else:
return 0
@classmethod
def get_latest_version(cls, platform):
"""指定プラットフォームの最新バージョンを取得"""
try:
return cls.objects.filter(platform=platform, is_latest=True).first()
except cls.DoesNotExist:
return None
class CheckinExtended(models.Model):
"""チェックイン拡張情報モデル"""
VALIDATION_STATUS_CHOICES = [
('pending', 'Pending'),
('approved', 'Approved'),
('rejected', 'Rejected'),
('requires_review', 'Requires Review'),
]
gpslog = models.ForeignKey('GpsCheckin', on_delete=models.CASCADE, related_name='extended_info')
# GPS拡張情報
gps_latitude = models.DecimalField(max_digits=10, decimal_places=8, null=True, blank=True)
gps_longitude = models.DecimalField(max_digits=11, decimal_places=8, null=True, blank=True)
gps_accuracy = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True, help_text="GPS精度メートル")
gps_timestamp = models.DateTimeField(null=True, blank=True)
# カメラメタデータ
camera_capture_time = models.DateTimeField(null=True, blank=True)
device_info = models.TextField(blank=True, null=True)
# 審査・検証情報
validation_status = models.CharField(
max_length=20,
choices=VALIDATION_STATUS_CHOICES,
default='pending'
)
validation_comment = models.TextField(blank=True, null=True)
validated_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True)
validated_at = models.DateTimeField(null=True, blank=True)
# スコア情報
bonus_points = models.IntegerField(default=0)
scoring_breakdown = JSONField(default=dict, blank=True)
# システム情報
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'rog_checkin_extended'
indexes = [
models.Index(fields=['validation_status'], name='idx_checkin_ext_valid'),
models.Index(fields=['created_at'], name='idx_checkin_ext_created'),
]
def __str__(self):
return f"CheckinExtended {self.gpslog_id} - {self.validation_status}"
class UploadedImage(models.Model):
"""画像アップロード管理モデル - マルチアップロード対応"""
UPLOAD_SOURCE_CHOICES = [
('direct', 'Direct'),
('sharing_intent', 'Sharing Intent'),
('bulk_upload', 'Bulk Upload'),
]
PLATFORM_CHOICES = [
('ios', 'iOS'),
('android', 'Android'),
('web', 'Web'),
]
PROCESSING_STATUS_CHOICES = [
('uploaded', 'Uploaded'),
('processing', 'Processing'),
('processed', 'Processed'),
('failed', 'Failed'),
]
MIME_TYPE_CHOICES = [
('image/jpeg', 'JPEG'),
('image/png', 'PNG'),
('image/heic', 'HEIC'),
('image/webp', 'WebP'),
]
# 基本情報
original_filename = models.CharField(max_length=255)
server_filename = models.CharField(max_length=255, unique=True)
file_url = models.URLField()
file_size = models.BigIntegerField()
mime_type = models.CharField(max_length=50, choices=MIME_TYPE_CHOICES)
# 関連情報
event_code = models.CharField(max_length=50, blank=True, null=True)
team_name = models.CharField(max_length=255, blank=True, null=True)
cp_number = models.IntegerField(blank=True, null=True)
# アップロード情報
upload_source = models.CharField(max_length=50, choices=UPLOAD_SOURCE_CHOICES, default='direct')
device_platform = models.CharField(max_length=20, choices=PLATFORM_CHOICES, blank=True, null=True)
# メタデータ
capture_timestamp = models.DateTimeField(blank=True, null=True)
upload_timestamp = models.DateTimeField(auto_now_add=True)
device_info = models.TextField(blank=True, null=True)
# 処理状況
processing_status = models.CharField(max_length=20, choices=PROCESSING_STATUS_CHOICES, default='uploaded')
thumbnail_url = models.URLField(blank=True, null=True)
# 外部キー
gpslog = models.ForeignKey('GpsCheckin', on_delete=models.SET_NULL, null=True, blank=True)
entry = models.ForeignKey('Entry', on_delete=models.SET_NULL, null=True, blank=True)
# システム情報
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'rog_uploaded_images'
indexes = [
models.Index(fields=['event_code', 'team_name'], name='idx_uploaded_event_team'),
models.Index(fields=['cp_number'], name='idx_uploaded_cp_number'),
models.Index(fields=['upload_timestamp'], name='idx_uploaded_timestamp'),
models.Index(fields=['processing_status'], name='idx_uploaded_status'),
]
def __str__(self):
return f"{self.original_filename} - {self.event_code} - CP{self.cp_number}"
def clean(self):
"""バリデーション"""
if self.file_size and (self.file_size <= 0 or self.file_size > 10485760): # 10MB
raise ValidationError("ファイルサイズは10MB以下である必要があります")
@property
def file_size_mb(self):
"""ファイルサイズをMB単位で取得"""
return round(self.file_size / 1024 / 1024, 2) if self.file_size else 0
class NewEvent2(models.Model):
# 既存フィールド
event_name = models.CharField(max_length=255, unique=True)
@ -318,6 +527,21 @@ class NewEvent2(models.Model):
#// Added @2024-10-21
public = models.BooleanField(default=False)
# Status field for enhanced event management (2025-08-27)
STATUS_CHOICES = [
('public', 'Public'),
('private', 'Private'),
('draft', 'Draft'),
('closed', 'Closed'),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft',
help_text="イベントステータス"
)
hour_3 = models.BooleanField(default=False)
hour_5 = models.BooleanField(default=True)
class_general = models.BooleanField(default=True)
@ -344,7 +568,32 @@ class NewEvent2(models.Model):
def save(self, *args, **kwargs):
if not self.deadlineDateTime:
self.deadlineDateTime = self.end_datetime #- timedelta(days=7)
# publicフィールドからstatusフィールドへの自動移行
if self.pk is None and self.status == 'draft': # 新規作成時
if self.public:
self.status = 'public'
super().save(*args, **kwargs)
@property
def deadline_datetime(self):
"""API応答用のフィールド名統一"""
return self.deadlineDateTime
def is_accessible_by_user(self, user):
"""ユーザーがこのイベントにアクセス可能かチェック"""
if self.status == 'public':
return True
elif self.status == 'private':
# スタッフ権限チェック(後で実装)
return hasattr(user, 'staff_privileges') and user.staff_privileges
elif self.status == 'draft':
# ドラフトは管理者のみ
return user.is_staff or user.is_superuser
elif self.status == 'closed':
return False
return False
class NewEvent(models.Model):
event_name = models.CharField(max_length=255, primary_key=True)
@ -460,6 +709,22 @@ class Entry(models.Model):
is_active = models.BooleanField(default=True) # 新しく追加
hasParticipated = models.BooleanField(default=False) # 新しく追加
hasGoaled = models.BooleanField(default=False) # 新しく追加
# API変更要求書対応: スタッフ権限管理 (2025-08-27)
staff_privileges = models.BooleanField(default=False, help_text="スタッフ権限フラグ")
can_access_private_events = models.BooleanField(default=False, help_text="非公開イベント参加権限")
VALIDATION_STATUS_CHOICES = [
('approved', 'Approved'),
('pending', 'Pending'),
('rejected', 'Rejected'),
]
team_validation_status = models.CharField(
max_length=20,
choices=VALIDATION_STATUS_CHOICES,
default='approved',
help_text="チーム承認状況"
)
class Meta: