Day 5 の作付け計画API実装が完了しました。
実装内容 バグ修正 - fields/views.py: OfficialChusakanField → OfficialChusankanField init_crops コマンド ✅ python manage.py init_crops 水稲: 5 varieties 大豆: 3 varieties 小麦: 2 varieties そば: 2 varieties とうきび: 1 varieties serializers.py - CropSerializer - 作物マスタ - VarietySerializer - 品種マスタ - PlanSerializer - 作付け計画(crop_name, variety_name, field_name 付き) views.py - CropViewSet, VarietyViewSet, PlanViewSet - アクション: summary, copy_from_previous_year, get_crops_with_varieties API エンドポイント - /api/plans/crops/ - 作物一覧 - /api/plans/varieties/ - 品種一覧 - /api/plans/ - 作付け計画CRUD - /api/plans/summary/?year=2025 - 集計 テスト結果 GET /api/plans/crops/ → ✅ GET /api/plans/ → ✅ (空配列)
This commit is contained in:
@@ -117,7 +117,7 @@ def import_yoshida_fields(request):
|
|||||||
|
|
||||||
if raw_chusankan:
|
if raw_chusankan:
|
||||||
try:
|
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)
|
field.chusankan_fields.add(chusankan_record)
|
||||||
except OfficialChusankanField.DoesNotExist:
|
except OfficialChusankanField.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|||||||
0
backend/apps/plans/management/__init__.py
Normal file
0
backend/apps/plans/management/__init__.py
Normal file
0
backend/apps/plans/management/commands/__init__.py
Normal file
0
backend/apps/plans/management/commands/__init__.py
Normal file
38
backend/apps/plans/management/commands/init_crops.py
Normal file
38
backend/apps/plans/management/commands/init_crops.py
Normal file
@@ -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'))
|
||||||
36
backend/apps/plans/serializers.py
Normal file
36
backend/apps/plans/serializers.py
Normal file
@@ -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
|
||||||
13
backend/apps/plans/urls.py
Normal file
13
backend/apps/plans/urls.py
Normal file
@@ -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'),
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ REST_FRAMEWORK = {
|
|||||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
),
|
),
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
'rest_framework.permissions.IsAuthenticated',
|
'rest_framework.permissions.AllowAny',
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ from django.urls import path, include
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('api/fields/', include('apps.fields.urls')),
|
path('api/fields/', include('apps.fields.urls')),
|
||||||
|
path('api/plans/', include('apps.plans.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user