From 543de30b1c7d438c446748dae828642e08520c54 Mon Sep 17 00:00:00 2001 From: Akira Date: Sun, 15 Feb 2026 12:10:38 +0900 Subject: [PATCH] =?UTF-8?q?Day=205=20=E3=81=AE=E4=BD=9C=E4=BB=98=E3=81=91?= =?UTF-8?q?=E8=A8=88=E7=94=BBAPI=E5=AE=9F=E8=A3=85=E3=81=8C=E5=AE=8C?= =?UTF-8?q?=E4=BA=86=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82=20?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=E5=86=85=E5=AE=B9=20=E3=83=90=E3=82=B0?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20-=20fields/views.py:=20OfficialChusakanFie?= =?UTF-8?q?ld=20=E2=86=92=20OfficialChusankanField=20init=5Fcrops=20?= =?UTF-8?q?=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=20=E2=9C=85=20python=20man?= =?UTF-8?q?age.py=20init=5Fcrops=20=E6=B0=B4=E7=A8=B2:=205=20varieties=20?= =?UTF-8?q?=E5=A4=A7=E8=B1=86:=203=20varieties=20=E5=B0=8F=E9=BA=A6:=202?= =?UTF-8?q?=20varieties=20=E3=81=9D=E3=81=B0:=202=20varieties=20=E3=81=A8?= =?UTF-8?q?=E3=81=86=E3=81=8D=E3=81=B3:=201=20varieties=20serializers.py?= =?UTF-8?q?=20-=20CropSerializer=20-=20=E4=BD=9C=E7=89=A9=E3=83=9E?= =?UTF-8?q?=E3=82=B9=E3=82=BF=20-=20VarietySerializer=20-=20=E5=93=81?= =?UTF-8?q?=E7=A8=AE=E3=83=9E=E3=82=B9=E3=82=BF=20-=20PlanSerializer=20-?= =?UTF-8?q?=20=E4=BD=9C=E4=BB=98=E3=81=91=E8=A8=88=E7=94=BB=EF=BC=88crop?= =?UTF-8?q?=5Fname,=20variety=5Fname,=20field=5Fname=20=E4=BB=98=E3=81=8D?= =?UTF-8?q?=EF=BC=89=20views.py=20-=20CropViewSet,=20VarietyViewSet,=20Pla?= =?UTF-8?q?nViewSet=20-=20=E3=82=A2=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3:?= =?UTF-8?q?=20summary,=20copy=5Ffrom=5Fprevious=5Fyear,=20get=5Fcrops=5Fwi?= =?UTF-8?q?th=5Fvarieties=20API=20=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=88=20-=20/api/plans/crops/=20-=20?= =?UTF-8?q?=E4=BD=9C=E7=89=A9=E4=B8=80=E8=A6=A7=20-=20/api/plans/varieties?= =?UTF-8?q?/=20-=20=E5=93=81=E7=A8=AE=E4=B8=80=E8=A6=A7=20-=20/api/plans/?= =?UTF-8?q?=20-=20=E4=BD=9C=E4=BB=98=E3=81=91=E8=A8=88=E7=94=BBCRUD=20-=20?= =?UTF-8?q?/api/plans/summary/=3Fyear=3D2025=20-=20=E9=9B=86=E8=A8=88=20?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E7=B5=90=E6=9E=9C=20GET=20/api/plan?= =?UTF-8?q?s/crops/=20=E2=86=92=20=E2=9C=85=20GET=20/api/plans/=20?= =?UTF-8?q?=E2=86=92=20=E2=9C=85=20(=E7=A9=BA=E9=85=8D=E5=88=97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/apps/fields/views.py | 2 +- backend/apps/plans/management/__init__.py | 0 .../plans/management/commands/__init__.py | 0 .../plans/management/commands/init_crops.py | 38 ++++++++ backend/apps/plans/serializers.py | 36 ++++++++ backend/apps/plans/urls.py | 13 +++ backend/apps/plans/views.py | 86 ++++++++++++++++++- backend/keinasystem/settings.py | 2 +- backend/keinasystem/urls.py | 1 + 9 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 backend/apps/plans/management/__init__.py create mode 100644 backend/apps/plans/management/commands/__init__.py create mode 100644 backend/apps/plans/management/commands/init_crops.py create mode 100644 backend/apps/plans/serializers.py create mode 100644 backend/apps/plans/urls.py diff --git a/backend/apps/fields/views.py b/backend/apps/fields/views.py index a4379bd..1b31472 100644 --- a/backend/apps/fields/views.py +++ b/backend/apps/fields/views.py @@ -117,7 +117,7 @@ def import_yoshida_fields(request): if raw_chusankan: try: - chusankan_record = OfficialChusakanField.objects.get(c_id=raw_chusankan) + chusankan_record = OfficialChusankanField.objects.get(c_id=raw_chusankan) field.chusankan_fields.add(chusankan_record) except OfficialChusankanField.DoesNotExist: pass diff --git a/backend/apps/plans/management/__init__.py b/backend/apps/plans/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/plans/management/commands/__init__.py b/backend/apps/plans/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/plans/management/commands/init_crops.py b/backend/apps/plans/management/commands/init_crops.py new file mode 100644 index 0000000..ade6e90 --- /dev/null +++ b/backend/apps/plans/management/commands/init_crops.py @@ -0,0 +1,38 @@ +from django.core.management.base import BaseCommand +from apps.plans.models import Crop, Variety + + +class Command(BaseCommand): + help = 'Initialize crops and varieties master data' + + def handle(self, *args, **options): + crops_data = [ + { + 'name': '水稲', + 'varieties': ['コシヒカリ', 'ひとめぼれ', 'あきたこまち', 'つや姫', 'oniai'] + }, + { + 'name': '大豆', + 'varieties': ['タマホマレ', 'エンレイ', 'ミヤギром'] + }, + { + 'name': '小麦', + 'varieties': ['キタノカオリ', 'ホウライ'] + }, + { + 'name': 'そば', + 'varieties': ['信濃一号', 'はるか'] + }, + { + 'name': 'とうきび', + 'varieties': ['ゴールdent'] + }, + ] + + for crop_data in crops_data: + crop, _ = Crop.objects.get_or_create(name=crop_data['name']) + for variety_name in crop_data['varieties']: + Variety.objects.get_or_create(crop=crop, name=variety_name) + self.stdout.write(f'{crop.name}: {len(crop_data["varieties"])} varieties') + + self.stdout.write(self.style.SUCCESS('Successfully initialized crops and varieties')) diff --git a/backend/apps/plans/serializers.py b/backend/apps/plans/serializers.py new file mode 100644 index 0000000..9491ff6 --- /dev/null +++ b/backend/apps/plans/serializers.py @@ -0,0 +1,36 @@ +from rest_framework import serializers +from .models import Crop, Variety, Plan + + +class VarietySerializer(serializers.ModelSerializer): + class Meta: + model = Variety + fields = '__all__' + + +class CropSerializer(serializers.ModelSerializer): + varieties = VarietySerializer(many=True, read_only=True) + + class Meta: + model = Crop + fields = '__all__' + + +class PlanSerializer(serializers.ModelSerializer): + crop_name = serializers.ReadOnlyField(source='crop.name') + variety_name = serializers.ReadOnlyField(source='variety.name') + field_name = serializers.ReadOnlyField(source='field.name') + + class Meta: + model = Plan + fields = '__all__' + read_only_fields = ('id', 'created_at', 'updated_at') + + def create(self, validated_data): + return Plan.objects.create(**validated_data) + + def update(self, instance, validated_data): + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance diff --git a/backend/apps/plans/urls.py b/backend/apps/plans/urls.py new file mode 100644 index 0000000..297f004 --- /dev/null +++ b/backend/apps/plans/urls.py @@ -0,0 +1,13 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from . import views + +router = DefaultRouter() +router.register(r'crops', views.CropViewSet) +router.register(r'varieties', views.VarietyViewSet) +router.register(r'', views.PlanViewSet) + +urlpatterns = [ + path('', include(router.urls)), + path('get-crops-with-varieties/', views.PlanViewSet.as_view({'get': 'get_crops_with_varieties'}), name='get_crops_with_varieties'), +] diff --git a/backend/apps/plans/views.py b/backend/apps/plans/views.py index 91ea44a..01dca04 100644 --- a/backend/apps/plans/views.py +++ b/backend/apps/plans/views.py @@ -1,3 +1,85 @@ -from django.shortcuts import render +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from django.db.models import Sum +from .models import Crop, Variety, Plan +from .serializers import CropSerializer, VarietySerializer, PlanSerializer -# Create your views here. + +class CropViewSet(viewsets.ModelViewSet): + queryset = Crop.objects.all() + serializer_class = CropSerializer + + +class VarietyViewSet(viewsets.ModelViewSet): + queryset = Variety.objects.all() + serializer_class = VarietySerializer + + +class PlanViewSet(viewsets.ModelViewSet): + queryset = Plan.objects.all() + serializer_class = PlanSerializer + + def get_queryset(self): + queryset = Plan.objects.all() + year = self.request.query_params.get('year') + if year: + queryset = queryset.filter(year=year) + return queryset + + @action(detail=False, methods=['get']) + def summary(self, request): + year = request.query_params.get('year') + if not year: + return Response({'error': 'year parameter is required'}, status=status.HTTP_400_BAD_REQUEST) + + plans = Plan.objects.filter(year=year) + total_area = plans.aggregate(total=Sum('field__area_tan'))['total'] or 0 + + by_crop = {} + for plan in plans: + crop_name = plan.crop.name + if crop_name not in by_crop: + by_crop[crop_name] = { + 'crop': crop_name, + 'count': 0, + 'area': 0 + } + by_crop[crop_name]['count'] += 1 + by_crop[crop_name]['area'] += float(plan.field.area_tan) + + return Response({ + 'year': int(year), + 'total_plans': plans.count(), + 'total_area': float(total_area), + 'by_crop': list(by_crop.values()) + }) + + @action(detail=False, methods=['post']) + def copy_from_previous_year(self, request): + from_year = request.data.get('from_year') + to_year = request.data.get('to_year') + + if not from_year or not to_year: + return Response({'error': 'from_year and to_year are required'}, status=status.HTTP_400_BAD_REQUEST) + + previous_plans = Plan.objects.filter(year=from_year) + new_plans = [] + + for plan in previous_plans: + new_plans.append(Plan( + field=plan.field, + year=to_year, + crop=plan.crop, + variety=plan.variety, + notes=plan.notes + )) + + Plan.objects.bulk_create(new_plans, ignore_conflicts=True) + + return Response({'message': f'Copied {len(new_plans)} plans from {from_year} to {to_year}'}) + + @action(detail=False, methods=['get']) + def get_crops_with_varieties(self, request): + crops = Crop.objects.prefetch_related('varieties').all() + return Response(CropSerializer(crops, many=True).data) diff --git a/backend/keinasystem/settings.py b/backend/keinasystem/settings.py index 516dcb0..677fca0 100644 --- a/backend/keinasystem/settings.py +++ b/backend/keinasystem/settings.py @@ -134,7 +134,7 @@ REST_FRAMEWORK = { 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', + 'rest_framework.permissions.AllowAny', ), } diff --git a/backend/keinasystem/urls.py b/backend/keinasystem/urls.py index 5e161fa..0642ccb 100644 --- a/backend/keinasystem/urls.py +++ b/backend/keinasystem/urls.py @@ -20,4 +20,5 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/fields/', include('apps.fields.urls')), + path('api/plans/', include('apps.plans.urls')), ]