new supervisor step2
This commit is contained in:
@ -53,6 +53,7 @@ RUN pip install gunicorn
|
|||||||
|
|
||||||
# xlsxwriterを追加
|
# xlsxwriterを追加
|
||||||
RUN pip install -r requirements.txt \
|
RUN pip install -r requirements.txt \
|
||||||
|
&& pip install django-cors-headers \
|
||||||
&& pip install xlsxwriter gunicorn
|
&& pip install xlsxwriter gunicorn
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|||||||
@ -53,10 +53,14 @@ INSTALLED_APPS = [
|
|||||||
'leaflet',
|
'leaflet',
|
||||||
'leaflet_admin_list',
|
'leaflet_admin_list',
|
||||||
'rog.apps.RogConfig',
|
'rog.apps.RogConfig',
|
||||||
|
'corsheaders', # added
|
||||||
'django_filters'
|
'django_filters'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware', # できるだけ上部に
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@ -68,6 +72,43 @@ MIDDLEWARE = [
|
|||||||
|
|
||||||
ROOT_URLCONF = 'config.urls'
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True # 開発環境のみ
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|
||||||
|
CORS_ALLOWED_METHODS = [
|
||||||
|
'GET',
|
||||||
|
'POST',
|
||||||
|
'PUT',
|
||||||
|
'PATCH',
|
||||||
|
'DELETE',
|
||||||
|
'OPTIONS'
|
||||||
|
]
|
||||||
|
CORS_ALLOWED_HEADERS = [
|
||||||
|
'accept',
|
||||||
|
'accept-encoding',
|
||||||
|
'authorization',
|
||||||
|
'content-type',
|
||||||
|
'dnt',
|
||||||
|
'origin',
|
||||||
|
'user-agent',
|
||||||
|
'x-csrftoken',
|
||||||
|
'x-requested-with',
|
||||||
|
]
|
||||||
|
|
||||||
|
# 本番環境では以下のように制限する
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"https://rogaining.sumasen.net",
|
||||||
|
"http://rogaining.sumasen.net",
|
||||||
|
]
|
||||||
|
|
||||||
|
# CSRFの設定
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
"http://rogaining.sumasen.net",
|
||||||
|
"https://rogaining.sumasen.net",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
@ -231,11 +272,15 @@ LOGGING = {
|
|||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
#'django': {
|
'django': {
|
||||||
# 'handlers': ['console'],
|
'handlers': ['console'],
|
||||||
# 'level': 'INFO',
|
'level': 'INFO',
|
||||||
# 'propagate': False,
|
'propagate': False,
|
||||||
#},
|
},
|
||||||
|
'django.request': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
},
|
||||||
'rog': {
|
'rog': {
|
||||||
#'handlers': ['file','console'],
|
#'handlers': ['file','console'],
|
||||||
'handlers': ['console'],
|
'handlers': ['console'],
|
||||||
|
|||||||
@ -18,6 +18,21 @@ from django.urls import path, include
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
|
||||||
|
# debug_urlsビューをrogアプリケーションのviewsからインポート
|
||||||
|
from rog import views as rog_views
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
ALLOWED_HOSTS = ['rogaining.sumasen.net', 'localhost', '127.0.0.1']
|
||||||
|
|
||||||
|
# CORSの設定
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"http://rogaining.sumasen.net",
|
||||||
|
"http://localhost",
|
||||||
|
"http://127.0.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('auth/', include('knox.urls')),
|
path('auth/', include('knox.urls')),
|
||||||
@ -27,3 +42,8 @@ urlpatterns = [
|
|||||||
admin.site.site_header = "ROGANING"
|
admin.site.site_header = "ROGANING"
|
||||||
admin.site.site_title = "Roganing Admin Portal"
|
admin.site.site_title = "Roganing Admin Portal"
|
||||||
admin.site.index_title = "Welcome to Roganing Portal"
|
admin.site.index_title = "Welcome to Roganing Portal"
|
||||||
|
|
||||||
|
# 開発環境での静的ファイル配信
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
@ -30,40 +30,37 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
restart: "on-failure"
|
restart: "on-failure"
|
||||||
# depends_on:
|
|
||||||
# - postgres-db
|
|
||||||
networks:
|
networks:
|
||||||
- rog-api
|
- rog-api
|
||||||
#entrypoint: ["/app/wait-for.sh", "postgres-db:5432", "--", ""]
|
|
||||||
#command: python3 manage.py runserver 0.0.0.0:8100
|
|
||||||
|
|
||||||
supervisor-web:
|
supervisor-web:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.supervisor
|
dockerfile: Dockerfile.supervisor
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: ./supervisor/html
|
source: ./supervisor/html
|
||||||
target: /usr/share/nginx/html
|
target: /usr/share/nginx/html/supervisor
|
||||||
read_only: true
|
read_only: true
|
||||||
- type: bind
|
- type: bind
|
||||||
source: ./supervisor/nginx/default.conf
|
source: ./supervisor/nginx/default.conf
|
||||||
target: /etc/nginx/conf.d/default.conf
|
target: /etc/nginx/conf.d/default.conf
|
||||||
read_only: true
|
read_only: true
|
||||||
- type: volume
|
- type: volume
|
||||||
source: static_volume
|
source: static_volume
|
||||||
target: /app/static
|
target: /app/static
|
||||||
read_only: true
|
read_only: true
|
||||||
- type: volume
|
- type: volume
|
||||||
source: nginx_logs
|
source: nginx_logs
|
||||||
target: /var/log/nginx
|
target: /var/log/nginx
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- api
|
||||||
networks:
|
networks:
|
||||||
- rog-api
|
- rog-api
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
81
docker-compose.yaml.ssl
Normal file
81
docker-compose.yaml.ssl
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# postgres-db:
|
||||||
|
# image: kartoza/postgis:12.0
|
||||||
|
# ports:
|
||||||
|
# - 5432:5432
|
||||||
|
# volumes:
|
||||||
|
# - postgres_data:/var/lib/postgresql
|
||||||
|
# - ./custom-postgresql.conf:/etc/postgresql/12/main/postgresql.conf
|
||||||
|
# environment:
|
||||||
|
# - POSTGRES_USER=${POSTGRES_USER}
|
||||||
|
# - POSTGRES_PASS=${POSTGRES_PASS}
|
||||||
|
# - POSTGRES_DBNAME=${POSTGRES_DBNAME}
|
||||||
|
# - POSTGRES_MAX_CONNECTIONS=600
|
||||||
|
|
||||||
|
# restart: "on-failure"
|
||||||
|
# networks:
|
||||||
|
# - rog-api
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.gdal
|
||||||
|
command: python3 manage.py runserver 0.0.0.0:8100
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
ports:
|
||||||
|
- 8100:8100
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: "on-failure"
|
||||||
|
# depends_on:
|
||||||
|
# - postgres-db
|
||||||
|
networks:
|
||||||
|
- rog-api
|
||||||
|
#entrypoint: ["/app/wait-for.sh", "postgres-db:5432", "--", ""]
|
||||||
|
#command: python3 manage.py runserver 0.0.0.0:8100
|
||||||
|
|
||||||
|
supervisor-web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.supervisor
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /etc/letsencrypt
|
||||||
|
target: /etc/nginx/ssl
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./supervisor/html
|
||||||
|
target: /usr/share/nginx/html
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./supervisor/nginx/default.conf
|
||||||
|
target: /etc/nginx/conf.d/default.conf
|
||||||
|
read_only: true
|
||||||
|
- type: volume
|
||||||
|
source: static_volume
|
||||||
|
target: /app/static
|
||||||
|
read_only: true
|
||||||
|
- type: volume
|
||||||
|
source: nginx_logs
|
||||||
|
target: /var/log/nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
networks:
|
||||||
|
- rog-api
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rog-api:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
geoserver-data:
|
||||||
|
static_volume:
|
||||||
|
nginx_logs:
|
||||||
@ -65,3 +65,4 @@ django-extra-fields==3.0.2
|
|||||||
django-phonenumber-field==6.1.0
|
django-phonenumber-field==6.1.0
|
||||||
django-rest-knox==4.2.0
|
django-rest-knox==4.2.0
|
||||||
dj-database-url==2.0.0
|
dj-database-url==2.0.0
|
||||||
|
django-cors-headers==4.3.0
|
||||||
|
|||||||
19
rog/urls.py
19
rog/urls.py
@ -1,7 +1,8 @@
|
|||||||
from sys import prefix
|
from sys import prefix
|
||||||
from rest_framework import urlpatterns
|
from rest_framework import urlpatterns
|
||||||
from rest_framework.routers import DefaultRouter
|
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, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView, NewCategoryViewSet,LocationInBound2,UserLastGoalTimeView,TeamEntriesView,update_entry_status,get_events,get_zekken_numbers,get_team_info,get_checkins,update_checkins,export_excel
|
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, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView, NewCategoryViewSet,LocationInBound2,UserLastGoalTimeView,TeamEntriesView,update_entry_status,get_events,get_zekken_numbers,get_team_info,get_checkins,update_checkins,export_excel,debug_urls
|
||||||
|
|
||||||
|
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from knox import views as knox_views
|
from knox import views as knox_views
|
||||||
@ -51,7 +52,7 @@ router.register(r'newevent2', views.NewEvent2ViewSet)
|
|||||||
# GET /api/teams/<team_id>/members-with-user/: 特定のチームの全メンバーとそのユーザー情報を取得
|
# GET /api/teams/<team_id>/members-with-user/: 特定のチームの全メンバーとそのユーザー情報を取得
|
||||||
|
|
||||||
|
|
||||||
|
app_name = 'rog' # 名前空間を追加
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
|
||||||
@ -113,11 +114,13 @@ urlpatterns += [
|
|||||||
|
|
||||||
|
|
||||||
# for Supervisor Web app
|
# for Supervisor Web app
|
||||||
path('api/events/', get_events, name='get_events'),
|
path('events/', views.get_events, name='get_events'),
|
||||||
path('api/zekken_numbers/<str:event_code>/', get_zekken_numbers, name='get_zekken_numbers'),
|
path('debug/urls/', views.debug_urls, name='debug_urls'),
|
||||||
path('api/team_info/<int:zekken_number>/', get_team_info, name='get_team_info'),
|
path('zekken_numbers/<str:event_code>/', views.get_zekken_numbers, name='get_zekken_numbers'),
|
||||||
path('api/checkins/<int:zekken_number>/', get_checkins, name='get_checkins'),
|
path('team_info/<int:zekken_number>/', views.get_team_info, name='get_team_info'),
|
||||||
path('api/update_checkins/', update_checkins, name='update_checkins'),
|
path('checkins/<int:zekken_number>/<str:event_code>/', views.get_checkins, name='get_checkins'),
|
||||||
path('api/export_excel/<int:zekken_number>/', export_excel, name='export_excel'),
|
path('update_checkins/', views.update_checkins, name='update_checkins'),
|
||||||
|
path('export_excel/<int:zekken_number>/<str:event_code>/', views.export_excel, name='export_excel'),
|
||||||
# for Supervisor Web app
|
# for Supervisor Web app
|
||||||
|
path('test/', views.test_api, name='test_api'),
|
||||||
]
|
]
|
||||||
|
|||||||
151
rog/views.py
151
rog/views.py
@ -30,7 +30,7 @@ from .permissions import IsTeamOwner,IsTeamOwnerOrMember
|
|||||||
|
|
||||||
from curses.ascii import NUL
|
from curses.ascii import NUL
|
||||||
from django.core.serializers import serialize
|
from django.core.serializers import serialize
|
||||||
from .models import GoalImages, Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, CustomUser, UserTracks, GoalImages, CheckinImages, NewEvent,NewEvent2, Team, Category, NewCategory,Entry, Member, TempUser,EntryMember
|
from .models import GoalImages, Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, CustomUser, UserTracks, GoalImages, CheckinImages, NewEvent,NewEvent2, Team, Category, NewCategory,Entry, Member, TempUser, EntryMember, GpsCheckin
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from .serializers import LocationSerializer, Location_lineSerializer, Location_polygonSerializer, JPN_main_perfSerializer, LocationCatSerializer, UserSerializer, LoginUserSerializer, UseractionsSerializer, UserDestinationSerializer, GifuAreaSerializer, LocationEventNameSerializer, RogUserSerializer, UserTracksSerializer, ChangePasswordSerializer, GolaImageSerializer, CheckinImageSerializer, RegistrationSerializer, MemberWithUserSerializer,TempUserRegistrationSerializer, PasswordResetRequestSerializer, PasswordResetConfirmSerializer
|
from .serializers import LocationSerializer, Location_lineSerializer, Location_polygonSerializer, JPN_main_perfSerializer, LocationCatSerializer, UserSerializer, LoginUserSerializer, UseractionsSerializer, UserDestinationSerializer, GifuAreaSerializer, LocationEventNameSerializer, RogUserSerializer, UserTracksSerializer, ChangePasswordSerializer, GolaImageSerializer, CheckinImageSerializer, RegistrationSerializer, MemberWithUserSerializer,TempUserRegistrationSerializer, PasswordResetRequestSerializer, PasswordResetConfirmSerializer
|
||||||
from knox.models import AuthToken
|
from knox.models import AuthToken
|
||||||
@ -52,7 +52,6 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.decorators import api_view
|
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.parsers import JSONParser, MultiPartParser
|
from rest_framework.parsers import JSONParser, MultiPartParser
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
@ -63,7 +62,7 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime,timedelta
|
||||||
|
|
||||||
from django.contrib.gis.measure import D
|
from django.contrib.gis.measure import D
|
||||||
from django.contrib.gis.geos import Point
|
from django.contrib.gis.geos import Point
|
||||||
@ -88,6 +87,8 @@ from django.core.exceptions import ValidationError
|
|||||||
import xlsxwriter
|
import xlsxwriter
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
from django.urls import get_resolver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@api_view(['PATCH'])
|
@api_view(['PATCH'])
|
||||||
@ -2316,14 +2317,38 @@ class UserLastGoalTimeView(APIView):
|
|||||||
# ----- for Supervisor -----
|
# ----- for Supervisor -----
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
def get_events(request):
|
def debug_urls(request):
|
||||||
events = NewEvent2.objects.filter(public=True)
|
"""デバッグ用:利用可能なURLパターンを表示"""
|
||||||
return Response([{
|
resolver = get_resolver()
|
||||||
'code': event.event_name,
|
urls = []
|
||||||
'name': event.event_name
|
for pattern in resolver.url_patterns:
|
||||||
} for event in events])
|
try:
|
||||||
|
urls.append(str(pattern.pattern))
|
||||||
|
except:
|
||||||
|
urls.append(str(pattern))
|
||||||
|
return JsonResponse({'urls': urls})
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
|
def get_events(request):
|
||||||
|
logger.debug(f"get_events was called. Path: {request.path}")
|
||||||
|
try:
|
||||||
|
events = NewEvent2.objects.filter(public=True)
|
||||||
|
logger.debug(f"Found {events.count()} events") # デバッグ用ログ
|
||||||
|
return Response([{
|
||||||
|
'code': event.event_name,
|
||||||
|
'name': event.event_name,
|
||||||
|
'start_datetime': event.start_datetime,
|
||||||
|
'end_datetime': event.end_datetime,
|
||||||
|
} for event in events])
|
||||||
|
logger.debug(f"Returning data: {data}") # デバッグ用ログ
|
||||||
|
return JsonResponse(data, safe=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in get_events: {str(e)}") # デバッグ用
|
||||||
|
return Response(
|
||||||
|
{"error": "Failed to retrieve events"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
@api_view(['GET'])
|
||||||
def get_zekken_numbers(request, event_code):
|
def get_zekken_numbers(request, event_code):
|
||||||
entries = Entry.objects.filter(
|
entries = Entry.objects.filter(
|
||||||
event__event_name=event_code,
|
event__event_name=event_code,
|
||||||
@ -2333,31 +2358,101 @@ def get_zekken_numbers(request, event_code):
|
|||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
def get_team_info(request, zekken_number):
|
def get_team_info(request, zekken_number):
|
||||||
entry = Entry.objects.select_related('team').get(zekken_number=zekken_number)
|
entry = Entry.objects.select_related('team','event').get(zekken_number=zekken_number)
|
||||||
members = Member.objects.filter(team=entry.team)
|
members = Member.objects.filter(team=entry.team)
|
||||||
|
|
||||||
|
# チームのゴール時間を取得
|
||||||
|
goal_record = GoalImages.objects.filter(
|
||||||
|
team_name=entry.team.team_name,
|
||||||
|
event_code=entry.event.event_name
|
||||||
|
).order_by('-goaltime').first()
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'team_name': entry.team.team_name,
|
'team_name': entry.team.team_name,
|
||||||
'members': ', '.join([f"{m.lastname} {m.firstname}" for m in members]),
|
'members': ', '.join([f"{m.lastname} {m.firstname}" for m in members]),
|
||||||
'start_time': entry.start_time.strftime('%Y-%m-%d %H:%M:%S') if entry.start_time else None,
|
'event_code': entry.event.event_name,
|
||||||
'goal_time': entry.goal_time.strftime('%Y-%m-%d %H:%M:%S') if entry.goal_time else None,
|
'start_datetime': entry.event.start_datetime,
|
||||||
'late_points': entry.late_point or 0
|
'end_datetime': goal_record.goaltime if goal_record else None
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
logger.info(f"Attempting to create new member for team: {self.kwargs['team_id']}")
|
||||||
|
logger.debug(f"Received data: {request.data}")
|
||||||
|
|
||||||
|
team = Team.objects.get(id=self.kwargs['team_id'])
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
def get_checkins(request, zekken_number):
|
def get_checkins(request, *args, **kwargs):
|
||||||
checkins = GpsCheckin.objects.filter(
|
#def get_checkins(request, zekken_number, event_code):
|
||||||
zekken_number=zekken_number
|
try:
|
||||||
).order_by('path_order')
|
# イベントコードをクエリパラメータから取得
|
||||||
return Response([{
|
|
||||||
'id': c.id,
|
zekken_number = kwargs['zekken_number']
|
||||||
'path_order': c.path_order,
|
if not zekken_number:
|
||||||
'cp_number': c.cp_number,
|
logger.debug(f"=== Zekken_number is required.")
|
||||||
'create_at': c.create_at,
|
return Response({"error": "zekken_number is required"}, status=400)
|
||||||
'validate_location': c.validate_location,
|
|
||||||
'points': c.points,
|
event_code = kwargs['event_code']
|
||||||
'buy_flag': c.buy_flag
|
if not event_code:
|
||||||
} for c in checkins])
|
logger.debug(f"=== event_code is required.")
|
||||||
|
return Response({"error": "event_code is required"}, status=400)
|
||||||
|
|
||||||
|
# チェックインデータの取得とCP情報の結合
|
||||||
|
checkins = GpsCheckin.objects.filter(
|
||||||
|
zekken_number=zekken_number,
|
||||||
|
event_code=event_code
|
||||||
|
).order_by('path_order')
|
||||||
|
|
||||||
|
# すべてのフィールドを確実に取得できるようにデバッグログを追加
|
||||||
|
logger.debug(f"Found {checkins.count()} checkins for zekken_number {zekken_number} and event_code {event_code}")
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for c in checkins:
|
||||||
|
location = Location.objects.filter(cp=c.cp_number,group=event_code).first()
|
||||||
|
if location:
|
||||||
|
formatted_time = None
|
||||||
|
if c.create_at:
|
||||||
|
try:
|
||||||
|
# create_atが文字列の場合はdatetimeに変換
|
||||||
|
if isinstance(c.create_at, str):
|
||||||
|
c.create_at = datetime.strptime(c.create_at, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# JST(+9時間)に変換して時刻フォーマット
|
||||||
|
jst_time = c.create_at + timedelta(hours=9)
|
||||||
|
formatted_time = jst_time.strftime('%H:%M:%S')
|
||||||
|
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error formatting create_at for checkin {c.id}: {str(e)}")
|
||||||
|
logger.error(f"Raw create_at value: {c.create_at}")
|
||||||
|
formatted_time = None
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
'id': c.id,
|
||||||
|
'path_order': c.path_order,
|
||||||
|
'cp_number': c.cp_number,
|
||||||
|
'sub_loc_id': location.sub_loc_id if location else f"#{c.cp_number}",
|
||||||
|
'location_name': location.location_name if location else None,
|
||||||
|
'create_at': formatted_time, #(c.create_at + timedelta(hours=9)).strftime('%H:%M:%S') if c.create_at else None,
|
||||||
|
'validate_location': c.validate_location,
|
||||||
|
'points': c.points or 0,
|
||||||
|
'buy_flag': c.buy_flag,
|
||||||
|
'photos': location.photos if location else None,
|
||||||
|
'image_address': c.image_address,
|
||||||
|
'receipt_address': c.image_receipt,
|
||||||
|
'location_name': location.location_name if location else None,
|
||||||
|
'checkin_point': location.checkin_point if location else None,
|
||||||
|
'buy_point': location.buy_point
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug(f"data={data}")
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_checkins: {str(e)}", exc_info=True)
|
||||||
|
return Response(
|
||||||
|
{"error": f"Failed to retrieve checkins: {str(e)}"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def update_checkins(request):
|
def update_checkins(request):
|
||||||
@ -2418,4 +2513,8 @@ def export_excel(request, zekken_number):
|
|||||||
|
|
||||||
# ----- for Supervisor -----
|
# ----- for Supervisor -----
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
def test_api(request):
|
||||||
|
logger.debug("Test API endpoint called")
|
||||||
|
return JsonResponse({"status": "API is working"})
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
<h1 class="text-2xl font-bold mb-6">スーパーバイザーパネル</h1>
|
<h1 class="text-2xl font-bold mb-6">スーパーバイザーパネル</h1>
|
||||||
|
|
||||||
<!-- 選択フォーム -->
|
<!-- 選択フォーム -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">イベントコード</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">イベントコード</label>
|
||||||
<select id="eventCode" class="w-full border border-gray-300 rounded-md px-3 py-2">
|
<select id="eventCode" class="w-full border border-gray-300 rounded-md px-3 py-2">
|
||||||
@ -26,10 +26,6 @@
|
|||||||
<option value="">ゼッケン番号を選択してください</option>
|
<option value="">ゼッケン番号を選択してください</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- チーム情報サマリー -->
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
<div class="text-sm text-gray-500">チーム名</div>
|
<div class="text-sm text-gray-500">チーム名</div>
|
||||||
<div id="teamName" class="font-semibold"></div>
|
<div id="teamName" class="font-semibold"></div>
|
||||||
@ -38,6 +34,10 @@
|
|||||||
<div class="text-sm text-gray-500">メンバー</div>
|
<div class="text-sm text-gray-500">メンバー</div>
|
||||||
<div id="members" class="font-semibold"></div>
|
<div id="members" class="font-semibold"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- チーム情報サマリー -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
<div class="text-sm text-gray-500">スタート時刻</div>
|
<div class="text-sm text-gray-500">スタート時刻</div>
|
||||||
<div id="startTime" class="font-semibold"></div>
|
<div id="startTime" class="font-semibold"></div>
|
||||||
@ -46,15 +46,23 @@
|
|||||||
<div class="text-sm text-gray-500">ゴール時刻</div>
|
<div class="text-sm text-gray-500">ゴール時刻</div>
|
||||||
<div id="goalTime" class="font-semibold"></div>
|
<div id="goalTime" class="font-semibold"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div class="text-sm text-gray-500">ゴール時計</div>
|
||||||
|
<div id="goalTime" class="font-semibold"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div class="text-sm text-gray-500">判定</div>
|
||||||
|
<div id="validate" class="font-semibold text-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
<div class="text-sm text-gray-500">チェックインポイント合計</div>
|
<div class="text-sm text-gray-500">CP合計</div>
|
||||||
<div id="totalPoints" class="font-semibold"></div>
|
<div id="totalPoints" class="font-semibold"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
<div class="text-sm text-gray-500">買い物ポイント合計</div>
|
<div class="text-sm text-gray-500">買物合計</div>
|
||||||
<div id="buyPoints" class="font-semibold"></div>
|
<div id="buyPoints" class="font-semibold"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
@ -62,7 +70,7 @@
|
|||||||
<div id="latePoints" class="font-semibold text-red-600"></div>
|
<div id="latePoints" class="font-semibold text-red-600"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
<div class="text-sm text-gray-500">総合計ポイント</div>
|
<div class="text-sm text-gray-500">総合計</div>
|
||||||
<div id="finalPoints" class="font-semibold text-blue-600"></div>
|
<div id="finalPoints" class="font-semibold text-blue-600"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -72,11 +80,14 @@
|
|||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">順序</th>
|
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">走行順</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">CP番号</th>
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">規定写真</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">チェックイン時刻</th>
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">撮影写真</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">検証</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">CP名称</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ポイント</th>
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">通過時刻</th>
|
||||||
|
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">通過審査</th>
|
||||||
|
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">買物審査</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">獲得点数</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="checkinList" class="bg-white divide-y divide-gray-200">
|
<tbody id="checkinList" class="bg-white divide-y divide-gray-200">
|
||||||
@ -87,6 +98,9 @@
|
|||||||
|
|
||||||
<!-- アクションボタン -->
|
<!-- アクションボタン -->
|
||||||
<div class="mt-6 flex justify-end space-x-4">
|
<div class="mt-6 flex justify-end space-x-4">
|
||||||
|
<button onclick="showAddCPModal()" class="px-4 py-2 bg-blue-500 text-white rounded">
|
||||||
|
新規CP追加
|
||||||
|
</button>
|
||||||
<button id="saveButton" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
<button id="saveButton" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||||
保存
|
保存
|
||||||
</button>
|
</button>
|
||||||
@ -98,6 +112,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// APIのベースURLを環境に応じて設定
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Sortable初期化
|
// Sortable初期化
|
||||||
const checkinList = document.getElementById('checkinList');
|
const checkinList = document.getElementById('checkinList');
|
||||||
@ -108,16 +126,25 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// イベントコードのドロップダウン要素を取得
|
||||||
|
const eventCodeSelect = document.getElementById('eventCode');
|
||||||
|
const zekkenNumberSelect = document.getElementById('zekkenNumber');
|
||||||
|
|
||||||
|
// ゼッケン番号変更時の処理
|
||||||
|
zekkenNumberSelect.addEventListener('change', function(e) {
|
||||||
|
const eventCode = eventCodeSelect.value;
|
||||||
|
if (!eventCode) {
|
||||||
|
alert('イベントコードを選択してください');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadTeamData(e.target.value, eventCode);
|
||||||
|
});
|
||||||
|
|
||||||
// イベントコード変更時の処理
|
// イベントコード変更時の処理
|
||||||
document.getElementById('eventCode').addEventListener('change', function(e) {
|
document.getElementById('eventCode').addEventListener('change', function(e) {
|
||||||
loadZekkenNumbers(e.target.value);
|
loadZekkenNumbers(e.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ゼッケン番号変更時の処理
|
|
||||||
document.getElementById('zekkenNumber').addEventListener('change', function(e) {
|
|
||||||
loadTeamData(e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// チェックボックス変更時の処理
|
// チェックボックス変更時の処理
|
||||||
checkinList.addEventListener('change', function(e) {
|
checkinList.addEventListener('change', function(e) {
|
||||||
if (e.target.type === 'checkbox') {
|
if (e.target.type === 'checkbox') {
|
||||||
@ -131,28 +158,46 @@
|
|||||||
// Excel出力ボタンの処理
|
// Excel出力ボタンの処理
|
||||||
document.getElementById('exportButton').addEventListener('click', exportExcel);
|
document.getElementById('exportButton').addEventListener('click', exportExcel);
|
||||||
|
|
||||||
|
console.log('Page loaded, attempting to load events...');
|
||||||
// 初期データ読み込み
|
// 初期データ読み込み
|
||||||
loadEventCodes();
|
loadEventCodes();
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadEventCodes() {
|
async function loadEventCodes() {
|
||||||
// APIからイベントコードを取得して選択肢を設定
|
try {
|
||||||
fetch('/api/events')
|
const response = await fetch(`${API_BASE_URL}/events/`);
|
||||||
.then(response => response.json())
|
if (!response.ok) {
|
||||||
.then(data => {
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
const select = document.getElementById('eventCode');
|
}
|
||||||
data.forEach(event => {
|
const contentType = response.headers.get("content-type");
|
||||||
const option = document.createElement('option');
|
if (!contentType || !contentType.includes("application/json")) {
|
||||||
option.value = event.code;
|
throw new TypeError("Response is not JSON");
|
||||||
option.textContent = event.name;
|
}
|
||||||
select.appendChild(option);
|
|
||||||
});
|
const data = await response.json();
|
||||||
|
const select = document.getElementById('eventCode');
|
||||||
|
// 既存のオプションをクリア
|
||||||
|
select.innerHTML = '<option value="">イベントを選択してください</option>';
|
||||||
|
|
||||||
|
data.forEach(event => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = event.code;
|
||||||
|
option.textContent = event.name;
|
||||||
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading events:', error);
|
||||||
|
// ユーザーにエラーを表示
|
||||||
|
const select = document.getElementById('eventCode');
|
||||||
|
select.innerHTML = '<option value="">エラー: イベントの読み込みに失敗しました</option>';
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadZekkenNumbers(eventCode) {
|
function loadZekkenNumbers(eventCode) {
|
||||||
// APIからゼッケン番号を取得して選択肢を設定
|
// APIからゼッケン番号を取得して選択肢を設定
|
||||||
fetch(`/api/zekken-numbers/${eventCode}`)
|
fetch(`${API_BASE_URL}/zekken_numbers/${eventCode}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const select = document.getElementById('zekkenNumber');
|
const select = document.getElementById('zekkenNumber');
|
||||||
@ -166,11 +211,129 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTeamData(zekkenNumber) {
|
|
||||||
|
async function loadTeamData(zekkenNumber,event_code) {
|
||||||
|
try {
|
||||||
|
const [teamResponse, checkinsResponse] = await Promise.all([
|
||||||
|
fetch(`${API_BASE_URL}/team_info/${zekkenNumber}/`),
|
||||||
|
fetch(`${API_BASE_URL}/checkins/${zekkenNumber}/${event_code}/`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 各レスポンスのステータスを個別にチェック
|
||||||
|
if (!teamResponse.ok)
|
||||||
|
throw new Error(`Team info fetch failed with status ${teamResponse.status}`);
|
||||||
|
if (!checkinsResponse.ok)
|
||||||
|
throw new Error(`Checkins fetch failed with status ${checkinsResponse.status}`);
|
||||||
|
|
||||||
|
|
||||||
|
const teamData = await teamResponse.json();
|
||||||
|
const checkinsData = await checkinsResponse.json();
|
||||||
|
|
||||||
|
// イベントコードに対応するイベントを検索
|
||||||
|
//const event = eventData.find(e => e.code === document.getElementById('eventCode').value);
|
||||||
|
|
||||||
|
document.getElementById('teamName').textContent = teamData.team_name || '-';
|
||||||
|
document.getElementById('members').textContent = teamData.members || '-';
|
||||||
|
document.getElementById('startTime').textContent =
|
||||||
|
teamData.start_datetime ? new Date(teamData.start_datetime).toLocaleString() : '-';
|
||||||
|
document.getElementById('goalTime').textContent =
|
||||||
|
teamData.end_datetime ? new Date(teamData.end_datetime).toLocaleString() : '-';
|
||||||
|
//'(未ゴール)';
|
||||||
|
|
||||||
|
// チェックインリストの更新
|
||||||
|
const tbody = document.getElementById('checkinList');
|
||||||
|
tbody.innerHTML = ''; // 既存のデータをクリア
|
||||||
|
|
||||||
|
let totalPoints = 0;
|
||||||
|
let buyPoints = 0;
|
||||||
|
|
||||||
|
checkinsData.forEach((checkin, index) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.dataset.id = checkin.id;
|
||||||
|
tr.dataset.cpNumber = checkin.cp_number;
|
||||||
|
|
||||||
|
const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : 'bg-red-100';
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td class="px-6 py-4">${checkin.path_order}</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
${location.photos ?
|
||||||
|
`<img src="${checkin.photos}" class="h-20 w-20 object-cover rounded">` : ''}
|
||||||
|
<div class="text-sm">${checkin.photos}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
${checkin.image_address ?
|
||||||
|
`<img src="${checkin.image_address}" class="h-20 w-20 object-cover rounded">` : ''}
|
||||||
|
${checkin.receipt_address && checkin.buy_flag ?
|
||||||
|
`<img src="${checkin.receipt_address}" class="h-20 w-20 object-cover rounded ml-2">` : ''}
|
||||||
|
<div class="text-sm">${checkin.image_address}</div>
|
||||||
|
<div class="text-sm">${checkin.receipt_address}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 ${bgColor}">
|
||||||
|
<div class="font-bold">${checkin.sub_loc_id}</div>
|
||||||
|
<div class="text-sm">${checkin.location_name}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">${checkin.create_at || '不明'}</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<input type="checkbox"
|
||||||
|
${checkin.validate_location ? 'checked' : ''}
|
||||||
|
class="h-4 w-4 text-blue-600 rounded validate-checkbox"
|
||||||
|
onchange="updatePoints(this)">
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
${checkin.buy_point > 0 ? `
|
||||||
|
<input type="checkbox"
|
||||||
|
${checkin.buy_flag ? 'checked' : ''}
|
||||||
|
class="h-4 w-4 text-green-600 rounded buy-checkbox"
|
||||||
|
onchange="updateBuyPoints(this)">
|
||||||
|
` : ''}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 point-value">${checkin.points}</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
|
||||||
|
if (checkin.points) {
|
||||||
|
if (checkin.buy_flag) {
|
||||||
|
buyPoints += checkin.points;
|
||||||
|
} else {
|
||||||
|
totalPoints += checkin.points;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 合計ポイントの更新
|
||||||
|
document.getElementById('totalPoints').textContent = totalPoints;
|
||||||
|
document.getElementById('buyPoints').textContent = buyPoints;
|
||||||
|
document.getElementById('latePoints').textContent = teamData.late_points || 0;
|
||||||
|
document.getElementById('finalPoints').textContent =
|
||||||
|
totalPoints + buyPoints - (teamData.late_points || 0);
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading team info:', error);
|
||||||
|
|
||||||
|
// エラーメッセージをユーザーに表示
|
||||||
|
alert(`データの読み込みに失敗しました: ${error.message}`);
|
||||||
|
|
||||||
|
// UIをクリア
|
||||||
|
document.getElementById('teamName').textContent = '-';
|
||||||
|
document.getElementById('members').textContent = '-';
|
||||||
|
document.getElementById('startTime').textContent = '-';
|
||||||
|
document.getElementById('goalTime').textContent = '-';
|
||||||
|
document.getElementById('checkinList').innerHTML = '';
|
||||||
|
document.getElementById('totalPoints').textContent = '0';
|
||||||
|
document.getElementById('buyPoints').textContent = '0';
|
||||||
|
document.getElementById('latePoints').textContent = '0';
|
||||||
|
document.getElementById('finalPoints').textContent = '0';
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTeamData_old(zekkenNumber) {
|
||||||
// チーム情報とチェックインデータを取得
|
// チーム情報とチェックインデータを取得
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch(`/api/team-info/${zekkenNumber}`),
|
fetch(`${API_BASE_URL}/team_info/${zekkenNumber}`),
|
||||||
fetch(`/api/checkins/${zekkenNumber}`)
|
fetch(`${API_BASE_URL}/checkins/${zekkenNumber}`)
|
||||||
]).then(responses => Promise.all(responses.map(r => r.json())))
|
]).then(responses => Promise.all(responses.map(r => r.json())))
|
||||||
.then(([teamInfo, checkins]) => {
|
.then(([teamInfo, checkins]) => {
|
||||||
updateTeamInfo(teamInfo);
|
updateTeamInfo(teamInfo);
|
||||||
@ -194,20 +357,137 @@
|
|||||||
checkins.forEach((checkin, index) => {
|
checkins.forEach((checkin, index) => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.dataset.id = checkin.id;
|
tr.dataset.id = checkin.id;
|
||||||
|
tr.dataset.cpNumber = checkin.cp_number;
|
||||||
|
|
||||||
|
const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : 'bg-red-100';
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td class="px-6 py-4">${index + 1}</td>
|
<td class="px-6 py-4">${checkin.path_order}</td>
|
||||||
<td class="px-6 py-4">${checkin.cp_number}</td>
|
<td class="px-6 py-4 ${bgColor}">
|
||||||
<td class="px-6 py-4">${formatDateTime(checkin.create_at)}</td>
|
<div class="font-bold">${checkin.sub_loc_id}</div>
|
||||||
<td class="px-6 py-4">
|
<div class="text-sm">${checkin.location_name}</div>
|
||||||
<input type="checkbox" ${checkin.validate_location ? 'checked' : ''}
|
|
||||||
class="h-4 w-4 text-blue-600 rounded validate-checkbox">
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">${checkin.points}</td>
|
<td class="px-6 py-4">
|
||||||
|
${checkin.image_address ?
|
||||||
|
`<img src="${checkin.image_address}" class="h-20 w-20 object-cover rounded">` : ''}
|
||||||
|
${checkin.receipt_address && checkin.buy_flag ?
|
||||||
|
`<img src="${checkin.receipt_address}" class="h-20 w-20 object-cover rounded ml-2">` : ''}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">${checkin.create_at || '不明'}</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<input type="checkbox"
|
||||||
|
${checkin.validate_location ? 'checked' : ''}
|
||||||
|
class="h-4 w-4 text-blue-600 rounded validate-checkbox"
|
||||||
|
onchange="updatePoints(this)">
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
${checkin.buy_point > 0 ? `
|
||||||
|
<input type="checkbox"
|
||||||
|
${checkin.buy_flag ? 'checked' : ''}
|
||||||
|
class="h-4 w-4 text-green-600 rounded buy-checkbox"
|
||||||
|
onchange="updateBuyPoints(this)">
|
||||||
|
` : ''}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 point-value">${checkin.points}</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
calculateTotalPoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ポイント更新関数
|
||||||
|
function updatePoints(checkbox) {
|
||||||
|
const tr = checkbox.closest('tr');
|
||||||
|
const pointCell = tr.querySelector('.point-value');
|
||||||
|
const cpNumber = tr.dataset.cpNumber;
|
||||||
|
|
||||||
|
fetch(`${API_BASE_URL}/location/${cpNumber}/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(location => {
|
||||||
|
const points = checkbox.checked ? location.checkin_point : 0;
|
||||||
|
pointCell.textContent = points;
|
||||||
|
calculateTotalPoints();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 買い物ポイント更新関数
|
||||||
|
function updateBuyPoints(checkbox) {
|
||||||
|
const tr = checkbox.closest('tr');
|
||||||
|
const pointCell = tr.querySelector('.point-value');
|
||||||
|
const cpNumber = tr.dataset.cpNumber;
|
||||||
|
|
||||||
|
fetch(`${API_BASE_URL}/location/${cpNumber}/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(location => {
|
||||||
|
const points = checkbox.checked ? location.buy_point : 0;
|
||||||
|
pointCell.textContent = points;
|
||||||
|
calculateTotalPoints();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新規CP追加用のモーダル
|
||||||
|
function showAddCPModal() {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="bg-white p-6 rounded-lg w-96">
|
||||||
|
<h3 class="text-lg font-bold mb-4">新規CPを追加</h3>
|
||||||
|
<div class="space-y-4" id="cpInputs">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="number" class="cp-input border rounded px-2 py-1 w-full" placeholder="CP番号">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-4 space-x-2">
|
||||||
|
<button onclick="addCPInput()" class="px-4 py-2 bg-blue-500 text-white rounded">
|
||||||
|
追加
|
||||||
|
</button>
|
||||||
|
<button onclick="saveCPs()" class="px-4 py-2 bg-green-500 text-white rounded">
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
<button onclick="closeModal()" class="px-4 py-2 bg-gray-500 text-white rounded">
|
||||||
|
キャンセル
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCPs() {
|
||||||
|
const inputs = document.querySelectorAll('.cp-input');
|
||||||
|
const eventCode = eventCodeSelect.value;
|
||||||
|
const newCheckins = Array.from(inputs)
|
||||||
|
.map(input => ({
|
||||||
|
cp_number: input.value,
|
||||||
|
create_at: null,
|
||||||
|
validate_location: false,
|
||||||
|
buy_flag: false,
|
||||||
|
points: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
// APIを呼び出して保存
|
||||||
|
fetch(`${API_BASE_URL}/checkins/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
zekken_number: currentZekkenNumber,
|
||||||
|
checkins: newCheckins
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
loadTeamData(currentZekkenNumber,eventCode);
|
||||||
|
closeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.querySelector('.fixed').remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function updatePathOrders() {
|
function updatePathOrders() {
|
||||||
const rows = Array.from(document.getElementById('checkinList').children);
|
const rows = Array.from(document.getElementById('checkinList').children);
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row, index) => {
|
||||||
@ -250,7 +530,7 @@
|
|||||||
validate_location: row.querySelector('.validate-checkbox').checked
|
validate_location: row.querySelector('.validate-checkbox').checked
|
||||||
}));
|
}));
|
||||||
|
|
||||||
fetch('/api/update-checkins', {
|
fetch('${API_BASE_URL}/update_checkins', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@ -1,33 +1,38 @@
|
|||||||
# HTTPS server
|
# HTTPS server
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
server_name rogaining.sumasen.net localhost;
|
||||||
server_name rogaining.sumasen.net;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/api_access.log;
|
||||||
error_log /var/log/nginx/error.log debug;
|
error_log /var/log/nginx/api_error.log debug;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
location / {
|
# Django admin
|
||||||
try_files $uri $uri/ /index.html;
|
location ~ ^/(admin|api)/ {
|
||||||
add_header 'Access-Control-Allow-Origin' '*';
|
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
|
||||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://api:8100;
|
proxy_pass http://api:8100;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header X-CSRFToken $http_x_csrf_token;
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
|
|
||||||
|
# タイムアウト設定
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
proxy_send_timeout 300;
|
||||||
|
proxy_read_timeout 300;
|
||||||
|
send_timeout 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Supervisor webapp
|
||||||
|
location /supervisor/ {
|
||||||
|
alias /usr/share/nginx/html/supervisor/;
|
||||||
|
try_files $uri $uri/ /supervisor/index.html;
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
|
||||||
}
|
}
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
@ -36,6 +41,10 @@ server {
|
|||||||
add_header Cache-Control "public, no-transform";
|
add_header Cache-Control "public, no-transform";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
}
|
||||||
|
|
||||||
error_page 404 /404.html;
|
error_page 404 /404.html;
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|||||||
45
supervisor/nginx/default.conf.80
Normal file
45
supervisor/nginx/default.conf.80
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# HTTPS server
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name rogaining.sumasen.net;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log debug;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api:8100;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /app/static/;
|
||||||
|
expires 1h;
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,29 +7,38 @@ server {
|
|||||||
|
|
||||||
# HTTPS server
|
# HTTPS server
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
listen [::]:80;
|
listen [::]:443 ssl http2;
|
||||||
server_name rogaining.sumasen.net;
|
server_name rogaining.sumasen.net;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/rogaining.sumasen.net/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/rogaining.sumasen.net/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/rogaining.sumasen.net/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/rogaining.sumasen.net/privkey.pem;
|
||||||
|
|
||||||
# SSL configuration
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_prefer_server_ciphers on;
|
|
||||||
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
|
|
||||||
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
error_log /var/log/nginx/error.log debug;
|
error_log /var/log/nginx/error.log debug;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
location / {
|
# Django admin
|
||||||
try_files $uri $uri/ /index.html;
|
location /admin/ {
|
||||||
|
proxy_pass http://api:8100;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Supervisor webapp
|
||||||
|
location /supervisor/ {
|
||||||
|
alias /usr/share/nginx/html/supervisor/;
|
||||||
|
try_files $uri $uri/ /supervisor/index.html;
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
|
||||||
|
}
|
||||||
|
|
||||||
|
# API endpoints
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://api:8100;
|
proxy_pass http://api:8100;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@ -37,10 +46,15 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /app/static/;
|
alias /app/static/;
|
||||||
|
expires 1h;
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
}
|
}
|
||||||
|
|
||||||
error_page 404 /404.html;
|
error_page 404 /404.html;
|
||||||
|
|||||||
Reference in New Issue
Block a user