在Django REST Framework中按嵌套模型日期字段分组(多对多关联)
问题描述
在Django REST Framework(DRF)项目中,需要对带有多对多through关联的嵌套模型里的schedule_date日期字段进行分组,实现按日期归类医生的可用时段,具体需求及现有代码如下:
models.py
class AvailableTimeSlotType(models.Model): id = models.UUIDField(primary_key = True,default = uuid.uuid4,editable = False) name = models.CharField(max_length=255, blank=True,null=True) image = models.ImageField(upload_to ='api/uploads/available_time_slot_types/%Y/%m/%d/',blank=True,null=True) description = models.TextField(blank=True,null=True) def __str__(self): return self.name class AvailableTimeSlot(models.Model): id = models.UUIDField(primary_key = True,default = uuid.uuid4,editable = False) name = models.CharField(max_length=255, blank=True,null=True) time_field = models.TimeField(default=timezone.localtime) slot_type = models.ForeignKey(AvailableTimeSlotType,on_delete=models.CASCADE,blank=True,null=True) image = models.ImageField(upload_to ='api/uploads/available_time_slots/%Y/%m/%d/',blank=True,null=True) description = models.TextField(blank=True,null=True) def __str__(self): return self.name class Doctor(models.Model): id = models.UUIDField(primary_key = True,default = uuid.uuid4,editable = False) user = models.OneToOneField(User, on_delete=models.CASCADE,related_name='doctor',blank=True,null=True) public_name = models.CharField(max_length=255, blank=True,null=True) biography = models.TextField(blank=True,null=True) image = models.ImageField(upload_to='api/uploads/doctors/%Y/%m/%d/', null=True, blank=True) slug = AutoSlugField(populate_from='public_name',blank=True,null=True) available_timeslots = models.ManyToManyField(AvailableTimeSlot,through="DoctorAvailableTimeSlot",blank=True) verification_date = models.DateTimeField(default=datetime.now, blank=True) years_of_experience = models.IntegerField(default=1,blank=True) rating = models.DecimalField(max_digits=10,decimal_places=2,default=1,blank=True,null=True) def __str__(self): return self.public_name class DoctorAvailableTimeSlot(models.Model): id = models.UUIDField(primary_key = True,default = uuid.uuid4,editable = False) doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE,blank=True,null=True,related_name='doctor_available_timeslots') available_timeslot = models.ForeignKey(AvailableTimeSlot, on_delete=models.CASCADE,blank=True,null=True) schedule_date = models.DateField(default=datetime.now, blank=True) def __str__(self): return self.doctor.public_name +' - '+self.available_timeslot.name
serializer.py
class AvailableTimeSlotTypeSerializer(serializers.ModelSerializer): class Meta: model = AvailableTimeSlotType fields = ("__all__") class AvailableTimeSlotSerializer(serializers.ModelSerializer): slot_type = AvailableTimeSlotTypeSerializer() class Meta: model = AvailableTimeSlot fields = ("__all__") class DoctorAvailableTimeSlotSerializerSerializer(serializers.ModelSerializer): available_timeslot = AvailableTimeSlotSerializer() class Meta: model = DoctorAvailableTimeSlot exclude = ("doctor",) class DoctorSerializer(serializers.ModelSerializer): image = serializers.SerializerMethodField('get_image_url') user = UserSerializer() doctor_available_timeslots = DoctorAvailableTimeSlotSerializerSerializer(many=True) class Meta: model = Doctor fields = ("__all__")
当前JSON响应
{"doctor_available_timeslots": [ { "id": "7bdc5b57-6fd0-430e-b526-7f4697430fda", "available_timeslot": { "id": "569985d5-62e2-4ca2-845b-e919242c3869", "slot_type": { "id": "4f56e64f-f087-44ef-9865-2cc5356a59e6", "name": "Dawn", "image": null, "description": null, }, "name": "12:00 AM", "time_field": "00:00:00", "image": null, "description": "", "created_at": "2023-02-23T14:51:39Z", "updated_at": "2023-03-14T13:17:05.636795Z" }, "schedule_date": "2023-03-16" } ] }
期望JSON响应
{ "doctor_available_timeslots": [{ "2023-03-16": [{ "id": "7bdc5b57-6fd0-430e-b526-7f4697430fda", "available_timeslot": { "id": "569985d5-62e2-4ca2-845b-e919242c3869", "slot_type": { "id": "4f56e64f-f087-44ef-9865-2cc5356a59e6", "name": "Dawn", "image": null, "description": null, }, "name": "12:00 AM", "time_field": "00:00:00", "image": null, "description": "" } } ] }] }
解决方案
通过SerializerMethodField自定义doctor_available_timeslots的序列化逻辑,即可实现按schedule_date分组的效果,具体修改如下:
- 保留原有的
DoctorAvailableTimeSlotSerializerSerializer(建议重命名为DoctorAvailableTimeSlotSerializer更规范),用于序列化单个时段条目。 - 修改
DoctorSerializer,将原有的doctor_available_timeslots字段替换为SerializerMethodField,在自定义方法中完成分组:
class DoctorSerializer(serializers.ModelSerializer): image = serializers.SerializerMethodField('get_image_url') user = UserSerializer() # 替换为自定义方法字段 doctor_available_timeslots = serializers.SerializerMethodField() class Meta: model = Doctor fields = ("__all__") def get_doctor_available_timeslots(self, obj): # 获取当前医生的所有可用时段并按日期排序 slots = obj.doctor_available_timeslots.all().order_by('schedule_date') # 初始化分组字典 grouped_slots = {} for slot in slots: # 将日期转为字符串作为分组键 date_key = slot.schedule_date.strftime('%Y-%m-%d') # 序列化单个时段并移除schedule_date字段 serialized_slot = DoctorAvailableTimeSlotSerializerSerializer(slot).data del serialized_slot['schedule_date'] if date_key not in grouped_slots: grouped_slots[date_key] = [] grouped_slots[date_key].append(serialized_slot) # 转换为期望的列表格式 return [grouped_slots]
说明
- 先按
schedule_date对医生的可用时段排序,保证同日期的条目归类在一起。 - 遍历每个时段时,将日期转为字符串作为分组键,同时移除序列化结果中的
schedule_date字段(已作为分组键使用)。 - 最后将分组后的字典包装成列表,完全匹配期望的响应格式;若不需要外层列表,直接返回
grouped_slots即可。
内容的提问来源于stack exchange,提问作者TEX
相关产品推荐
相关产品推荐

