From 1a8e97e945df1ac1854870e977389df7559bc7d1 Mon Sep 17 00:00:00 2001 From: Akira Date: Sat, 20 Jul 2024 14:24:52 +0900 Subject: [PATCH] updated for team, member and email registration --- rog/management/commands/cleanup_temp_users.py | 16 ++ rog/models.py | 119 ++++++++++++- rog/serializers.py | 39 ++++- rog/urls.py | 18 +- rog/views.py | 162 +++++++++++++++++- 5 files changed, 340 insertions(+), 14 deletions(-) create mode 100644 rog/management/commands/cleanup_temp_users.py diff --git a/rog/management/commands/cleanup_temp_users.py b/rog/management/commands/cleanup_temp_users.py new file mode 100644 index 0000000..8f33ec4 --- /dev/null +++ b/rog/management/commands/cleanup_temp_users.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from rog.models import TempUser # アプリ名 'rog' を適切に変更してください + +class Command(BaseCommand): + help = 'Deletes expired temporary user records' + + def handle(self, *args, **options): + expired_users = TempUser.objects.filter(expires_at__lt=timezone.now()) + count = expired_users.count() + expired_users.delete() + self.stdout.write(self.style.SUCCESS(f'Successfully deleted {count} expired temporary user records')) + + +# cron job の設定 +# 0 3 * * * /path/to/your/python /path/to/your/manage.py cleanup_temp_users diff --git a/rog/models.py b/rog/models.py index 5937134..108f932 100644 --- a/rog/models.py +++ b/rog/models.py @@ -24,6 +24,12 @@ from django.db import transaction from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager from django.contrib.postgres.indexes import GistIndex +from django.db import models +from django.contrib.auth.hashers import make_password + +from django.utils import timezone +from datetime import timedelta + import csv import codecs import sys @@ -61,20 +67,108 @@ def remove_bom_inplace(path): fp.seek(-bom_length, os.SEEK_CUR) fp.truncate() +#========== Akira ここから + +class TempUser(models.Model): + email = models.EmailField(unique=True) + password = models.CharField(max_length=128) + is_rogaining = models.BooleanField(default=False) + zekken_number = models.CharField(max_length=255, blank=True, null=True) + event_code = models.CharField(max_length=255, blank=True, null=True) + team_name = models.CharField(max_length=255, blank=True, null=True) + group = models.CharField(max_length=255) + verification_code = models.UUIDField(default=uuid.uuid4, editable=False) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + + def __str__(self): + return self.email + + def save(self, *args, **kwargs): + if not self.expires_at: + self.expires_at = timezone.now() + timedelta(hours=24) # 24時間の有効期限 + super().save(*args, **kwargs) + + def is_valid(self): + return timezone.now() <= self.expires_at + + + +class Team(models.Model): + zekken_number = models.CharField(max_length=255, primary_key=True) + team_name = models.CharField(max_length=255) + password = models.CharField(max_length=128) + + def __str__(self): + return f"{self.zekken_number} - {self.team_name}" + + +class Member(models.Model): + zekken_number = models.ForeignKey(Team, on_delete=models.CASCADE) + userid = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + + class Meta: + unique_together = ('zekken_number', 'userid') + + def __str__(self): + return f"{self.zekken_number} - {self.userid}" + + + + +class Entry(models.Model): + zekken_number = models.ForeignKey(Team, on_delete=models.CASCADE) + event_code = models.CharField(max_length=255) + date = models.DateField() + + class Meta: + unique_together = ('zekken_number', 'event_code', 'date') + + def __str__(self): + return f"{self.zekken_number} - {self.event_code} - {self.date}" + + + +#============= Akira ここまで + class CustomUserManager(BaseUserManager): def create_user(self, email, password, group, event_code, team_name, **other_fields): if not email: raise ValueError(_("You must provide an email address")) - - user = self.model(email=email, group=group, event_code=event_code, team_name=team_name, zekken_number=email, is_rogaining=True, **other_fields) + + # ユニークなuseridを生成 + userid = str(uuid.uuid4()) + + user = self.model( + email=self.normalize_email(email), + firstname=firstname, + lastname=lastname, + userid=userid, + date_of_birth=date_of_birth, + ) user.set_password(password) user.save() return user def create_superuser(self, email, password, group, event_code=None, team_name=None, **other_fields): + user = self.create_user( + email, + firstname=firstname, + lastname=lastname, + date_of_birth=date_of_birth, + password=password, + ) + user.is_staff = True + user.is_superuser = True + user.is_active = True + user.save(using=self._db) + return user + + +/* # Providing default values for event_code and team_name if they are not provided if event_code is None: event_code = 'test' # replace this with some default value @@ -91,8 +185,27 @@ class CustomUserManager(BaseUserManager): raise ValueError(_('Superuser must have is_superuser set to True')) return self.create_user(email, password, group, event_code, team_name, **other_fields) +*/ +class CustomUser(AbstractBaseUser, PermissionsMixin): + email = models.EmailField(unique=True) + firstname = models.CharField(max_length=255) + lastname = models.CharField(max_length=255) + userid = models.CharField(max_length=255, unique=True) + date_of_birth = models.DateField() + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + event_code = models.CharField(max_length=255, blank=True, null=True) + team_name = models.CharField(max_length=255, blank=True, null=True) + zekken_number = models.CharField(max_length=255, blank=True, null=True) + objects = CustomUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['firstname', 'lastname', 'userid', 'date_of_birth'] + + def __str__(self): + return self.email class JpnAdminMainPerf(models.Model): geom = models.MultiPolygonField(blank=True, null=True) @@ -197,7 +310,7 @@ class UserUploadUser(models.Model): email=models.CharField(_('User Email'), max_length=255) -class CustomUser(AbstractBaseUser, PermissionsMixin): +class CustomUser_old(AbstractBaseUser, PermissionsMixin): class Groups(models.TextChoices): GB1 = '大垣-初心者', '大垣-初心者' GB2 = '大垣-3時間', '大垣-3時間' diff --git a/rog/serializers.py b/rog/serializers.py index 05ce6dc..78eb266 100644 --- a/rog/serializers.py +++ b/rog/serializers.py @@ -1,14 +1,16 @@ from rest_framework import serializers from rest_framework_gis.serializers import GeoFeatureModelSerializer from sqlalchemy.sql.functions import mode -from .models import Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, UserTracks, GoalImages, CheckinImages,CustomUser +from .models import Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, UserTracks, GoalImages, CheckinImages,CustomUser, Team, Member, Entry from drf_extra_fields.fields import Base64ImageField +from django.contrib.auth.hashers import make_password #from django.contrib.auth.models import User from .models import CustomUser from django.contrib.auth import authenticate from .models import TestModel +from .models import TempUser class LocationCatSerializer(serializers.ModelSerializer): @@ -53,6 +55,39 @@ class JPN_main_perfSerializer(serializers.ModelSerializer): # model=JpnAdminPerf # fields=['id','et_id', 'et_right', 'et_left', 'adm2_l', 'adm1_l', 'adm0_l', 'adm0_r', 'adm1_r', 'adm2_r', 'admlevel'] +#============= Akira ここから +class TeamSerializer(serializers.ModelSerializer): + class Meta: + model = Team + fields = ['zekken_number', 'team_name', 'password'] + extra_kwargs = {'password': {'write_only': True}} + +class MemberSerializer(serializers.ModelSerializer): + class Meta: + model = Member + fields = ['zekken_number', 'userid'] + +class EntrySerializer(serializers.ModelSerializer): + class Meta: + model = Entry + fields = ['zekken_number', 'event_code', 'date'] + +class CustomUserSerializer(serializers.ModelSerializer): + class Meta: + model = CustomUser + fields = ['email', 'firstname', 'lastname', 'userid', 'date_of_birth', 'password'] + extra_kwargs = { + 'password': {'write_only': True}, + 'userid': {'read_only': True} + } + + def create(self, validated_data): + validated_data['password'] = make_password(validated_data.get('password')) + return super(CustomUserSerializer, self).create(validated_data) + + +#============= Akira ここまで + class GifuAreaSerializer(serializers.ModelSerializer): class Meta: @@ -192,4 +227,4 @@ class RegistrationSerializer(serializers.ModelSerializer): raise serializers.ValidationError({'password': 'Passwords must match.'}) user.set_password(password) user.save() - return user \ No newline at end of file + return user diff --git a/rog/urls.py b/rog/urls.py index a3301b0..3076707 100644 --- a/rog/urls.py +++ b/rog/urls.py @@ -1,7 +1,8 @@ from sys import prefix from rest_framework import urlpatterns from rest_framework.routers import DefaultRouter -from .views import LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView +from .views import LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, VerifyEmailView, TeamViewSet, MemberViewSet, EntryViewSet + from django.urls import path, include from knox import views as knox_views @@ -18,6 +19,17 @@ router.register(prefix='track', viewset=UserTracksViewSet, basename='track') router.register(prefix='goalimage', viewset=GoalImageViewSet, basename='goalimage') router.register(prefix='checkinimage', viewset=CheckinImageViewSet, basename='checkinimage') +#Akira 追加 +# /api/teams/ - チームの一覧取得と作成 +# /api/teams// - 特定のチームの取得、更新、削除 +# /api/members/ - メンバーの一覧取得と作成 +# /api/members// - 特定のメンバーの取得、更新、削除 +# /api/entries/ - エントリーの一覧取得と作成 +# /api/entries// - 特定のエントリーの取得、更新、削除 +# +router.register(r'teams', TeamViewSet) +router.register(r'members', MemberViewSet) +router.register(r'entries', EntryViewSet) urlpatterns = router.urls @@ -48,5 +60,7 @@ urlpatterns += [ path('delete-account/', DeleteAccount, name="delete-account"), path('privacy/', PrivacyView, name='privacy-view'), path('register', RegistrationView.as_view(), name='register'), + path('verify-email//', VerifyEmailView.as_view(), name='verify_email'), + # path('goal-image/', GoalImageViewSet.as_view(), name='goal-image') -] \ No newline at end of file +] diff --git a/rog/views.py b/rog/views.py index fadeda5..df95fe6 100644 --- a/rog/views.py +++ b/rog/views.py @@ -1,15 +1,16 @@ from curses.ascii import NUL from django.core.serializers import serialize -from .models import GoalImages, Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, CustomUser, UserTracks, GoalImages, CheckinImages -from rest_framework import viewsets -from .serializers import LocationSerializer, Location_lineSerializer, Location_polygonSerializer, JPN_main_perfSerializer, LocationCatSerializer, CreateUserSerializer, UserSerializer, LoginUserSerializer, UseractionsSerializer, UserDestinationSerializer, GifuAreaSerializer, LocationEventNameSerializer, RogUserSerializer, UserTracksSerializer, ChangePasswordSerializer, GolaImageSerializer, CheckinImageSerializer, RegistrationSerializer +from .models import GoalImages, Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, CustomUser, UserTracks, GoalImages, CheckinImages, TempUser +from rest_framework import viewsets,status +from .serializers import LocationSerializer, Location_lineSerializer, Location_polygonSerializer, JPN_main_perfSerializer, LocationCatSerializer, CreateUserSerializer, UserSerializer, LoginUserSerializer, UseractionsSerializer, UserDestinationSerializer, GifuAreaSerializer, LocationEventNameSerializer, RogUserSerializer, UserTracksSerializer, ChangePasswordSerializer, GolaImageSerializer, CheckinImageSerializer, RegistrationSerializer, Team, Member, Entry, CustomUserSerializer + from knox.models import AuthToken from rest_framework import viewsets, generics, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.parsers import JSONParser, MultiPartParser -from .serializers import LocationSerializer +from .serializers import LocationSerializer, TeamSerializer, MemberSerializer, EntrySerializer from django.http import JsonResponse from rest_framework.permissions import IsAuthenticated from django.contrib.gis.db.models import Extent, Union @@ -29,8 +30,9 @@ from rest_framework.parsers import JSONParser, MultiPartParser from django.views.decorators.csrf import csrf_exempt import uuid from django.shortcuts import render +from django.utils import timezone - +from django.db import transaction class LocationViewSet(viewsets.ModelViewSet): queryset=Location.objects.all() @@ -53,6 +55,113 @@ class Jpn_Main_PerfViewSet(viewsets.ModelViewSet): serializer_class=JPN_main_perfSerializer filter_fields = ["adm1_ja"] +#===== AKira ここから + +class CustomUserViewSet(viewsets.ModelViewSet): + queryset = CustomUser.objects.all() + serializer_class = CustomUserSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return CustomUser.objects.filter(id=self.request.user.id) + +class TeamViewSet(viewsets.ModelViewSet): + queryset = Team.objects.all() + serializer_class = TeamSerializer + + @transaction.atomic + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + + # チーム登録後にエントリー登録と外部APIコールを行う + self.register_entry_and_call_external_api(serializer.data) + + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def register_entry_and_call_external_api(self, team_data): + # エントリーの登録 + entry_data = { + 'zekken_number': team_data['zekken_number'], + 'event_code': request.data.get('event_code'), # エントリー用のevent_codeを取得 + 'date': request.data.get('date') # エントリー用の日付を取得 + } + entry_serializer = EntrySerializer(data=entry_data) + if entry_serializer.is_valid(): + entry_serializer.save() + + # 外部APIへのコール + self.call_external_api(team_data, entry_data) + else: + # エントリー登録に失敗した場合のエラーハンドリング + raise serializers.ValidationError(entry_serializer.errors) + + def call_external_api(self, team_data, entry_data): + external_api_url = "https://rogaining.sumasen.net/gifuroge/register_team" + + payload = { + 'zekken_number': team_data['zekken_number'], + 'event_code': entry_data['event_code'], + 'team_name': team_data['team_name'], + 'class_name': team_data.get('class_name', 'Default'), # class_nameがない場合はデフォルト値を設定 + 'password': team_data['password'] + } + + try: + response = requests.post(external_api_url, data=payload) + response.raise_for_status() # エラーレスポンスの場合は例外を発生させる + + # レスポンスの処理(必要に応じて) + print(f"External API response: {response.json()}") + except requests.RequestException as e: + # 外部APIコールに失敗した場合のエラーハンドリング + print(f"Failed to call external API: {str(e)}") + # ここでエラーをログに記録したり、管理者に通知したりすることができます + + + def get_queryset(self): + user = self.request.user + return Team.objects.filter(member__userid=user) + +class MemberViewSet(viewsets.ModelViewSet): + queryset = Member.objects.all() + serializer_class = MemberSerializer + permission_classes = [IsAuthenticated] + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def get_queryset(self): + user = self.request.user + return Member.objects.filter(userid=user) + + + +class EntryViewSet(viewsets.ModelViewSet): + queryset = Entry.objects.all() + serializer_class = EntrySerializer + permission_classes = [IsAuthenticated] + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def get_queryset(self): + user = self.request.user + return Entry.objects.filter(zekken_number__member__userid=user) + + + +#===== AKira ここまで class UserTracksViewSet(viewsets.ModelViewSet): @@ -550,10 +659,49 @@ class TestActionViewSet(viewsets.ModelViewSet): def PrivacyView(request): return render(request, "rog/privacy.html") -class RegistrationView(APIView): +class RegistrationView_old(APIView): def post(self, request): serializer = RegistrationSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class RegistrationView(APIView): + def post(self, request): + serializer = RegistrationSerializer(data=request.data) + if serializer.is_valid(): + temp_user = serializer.save() + verification_url = request.build_absolute_uri( + reverse('verify_email', kwargs={'verification_code': temp_user.verification_code}) + ) + send_mail( + 'Verify your email', + f'Please click the link to verify your email: {verification_url}', + settings.DEFAULT_FROM_EMAIL, + [temp_user.email], + fail_silently=False, + ) + return Response({"message": "Please check your email to complete registration."}, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class VerifyEmailView(APIView): + def get(self, request, verification_code): + try: + temp_user = TempUser.objects.get(verification_code=verification_code) + if not temp_user.is_valid(): + temp_user.delete() + return Response({"error": "Verification link has expired. Please register again."}, status=status.HTTP_400_BAD_REQUEST) + + user = CustomUser.objects.create_user( + email=temp_user.email, + password=temp_user.password, + is_rogaining=temp_user.is_rogaining, + zekken_number=temp_user.zekken_number, + event_code=temp_user.event_code, + team_name=temp_user.team_name, + group=temp_user.group + ) + +