Finish basic API implementation
This commit is contained in:
265
rog/models.py
265
rog/models.py
@ -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:
|
||||
|
||||
Reference in New Issue
Block a user