Django REST Framework更新用户时出现KeyError: 'centros'问题求助
问题描述
使用Django REST Framework开发应用时,更新用户时抛出KeyError: 'centros',错误触发点为centros = validated_data.pop('centros')代码行。
模型代码
class Usuario(AbstractBaseUser): organizacion = models.CharField(verbose_name='Organización', blank=True, null=True, max_length=250) departamento = models.CharField(verbose_name='Departamento', blank=True, null=True, max_length=250) centros = models.ManyToManyField(Centro, verbose_name='Centro o Unidad', blank=True, related_name='centros_usuario') telefono = models.CharField(max_length=100, verbose_name='Teléfono', blank=True, null=True) rut = models.CharField(max_length=100, verbose_name='RUT', blank=True, null=True) profesion = models.CharField(max_length=150, verbose_name='Profesión', blank=True, null=True) cargo = models.CharField(max_length=150, verbose_name='Cargo', blank=True, null=True) nacionalidad = models.CharField(max_length=100, verbose_name='Nacionalidad', blank=True, null=True) sexo = models.CharField(max_length=10, choices=SEXO_CHOICES ,verbose_name='Sexo', blank=True, null=True) fecha_nacimiento = models.DateField(auto_now=False, auto_now_add=False,verbose_name='Fecha de nacimiento', blank=True, null=True) imagen_perfil = models.ImageField(upload_to='perfil/', verbose_name='Imágen de Perfil', max_length=255, blank=True, null=True) cv = models.FileField(verbose_name='CV', upload_to='cv_usuarios/', blank=True, null=True) direccion = models.CharField(max_length=250, blank=True, null=True, verbose_name='Dirección') perfil = models.PositiveSmallIntegerField( null=False, blank=False, default=2, choices=PERFIL_CHOICES, verbose_name='Perfil') is_active = models.BooleanField( verbose_name='active', default=True, help_text=( 'Designates whether this user shoud be treated as active. ' 'Unselect this instead of deleting accounts. ' ) ) estado = models.CharField(verbose_name='Estado usuario', default='INHABILITADO', choices=ESTADO_CHOICES, max_length=13) is_staff = models.BooleanField( verbose_name='staff status', default=False, help_text=( 'Designates whether the user can log into this admin site. ' ) ) is_superuser = models.BooleanField( verbose_name='superuser status', default=False, help_text=( 'Designates whether this user is a Superuser. ' ) ) terminos_y_condiciones = models.BooleanField(verbose_name='Términos y Condiciones', default=False) linkedin = models.CharField(verbose_name='Linkedin', max_length=100, blank=True, null=True) email = models.EmailField(max_length=250, unique=True, verbose_name='Correo') first_name = models.CharField(max_length=250, verbose_name='Nombre') last_name = models.CharField(max_length=250, verbose_name='Apellido', blank=True, null=True) objects = UsuarioManager() USERNAME_FIELD = 'email' def __str__(self): return self.first_name def get_email_user(self): return self.email.lower() def save(self, *args, **kwargs): self.email = self.email.lower() return super(Usuario, self).save(*args, **kwargs)
更新用户序列化器代码
class UpdateUsuarioSerializer(serializers.ModelSerializer): email = serializers.CharField(max_length=250, required=False) first_name = serializers.CharField(max_length=250, required=False) class Meta: model = Usuario fields = ['organizacion', 'departamento', 'centros', 'telefono', 'rut', 'profesion', 'cargo', 'nacionalidad', 'sexo', 'fecha_nacimiento', 'imagen_perfil', 'cv', 'email', 'first_name', 'last_name', 'linkedin' ] def update(self, instance, validated_data): with transaction.atomic(): centros = validated_data.pop('centros') instance = super(UpdateUsuarioSerializer, self).update(instance,validated_data) instance.save() instance.centros.clear() for centro in centros: instance.centros.add(Centro.objects.get(id=centro)) return instance
视图集代码
class UsuarioViewSet(viewsets.ModelViewSet): queryset = Usuario.objects.filter(is_active = True) permission_classes = [permissions.AllowAny] serializer_class = UsuarioSerializer pagination_class = StandardResultsSetPagination filter_backends = [DjangoFilterBackend, filters.SearchFilter] filterset_fields = ['perfil'] search_fields = ['first_name', 'last_name', 'email', 'rut', 'telefono', 'profesion', 'cargo', 'centros__nombre'] def create(self, request): imagen_perfil = request.data.get('imagen_perfil', None) or None if imagen_perfil is not None: try: request.data['imagen_perfil'] = file64bits(imagen_perfil) except ValueError as error: raise serializers.ValidationError({'imagen_perfil': 'Error en formato de archivo'}) cv = request.data.get('cv', None) or None if cv is not None: try: request.data['cv'] = file64bits(cv) except ValueError as error: raise serializers.ValidationError({'cv': 'Error en formato de archivo'}) serializer = UsuarioSerializer(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) def update(self, request, pk=None): imagen_perfil = request.data.get('imagen_perfil', None) or None if imagen_perfil is not None: try: request.data['imagen_perfil'] = file64bits(imagen_perfil) except ValueError as error: raise serializers.ValidationError({'imagen_perfil': 'Error en formato de archivo'}) cv = request.data.get('cv', None) or None if cv is not None: try: request.data['cv'] = file64bits(cv) except ValueError as error: raise serializers.ValidationError({'cv': 'Error en formato de archivo'}) instance = Usuario.objects.get(id=pk) serializer = UpdateUsuarioSerializer(instance, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_202_ACCEPTED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
解决方案
错误核心原因是更新请求未传入centros字段,或该字段未通过序列化器验证,导致validated_data字典中无centros键,直接调用pop()触发KeyError。可通过以下方式修复:
1. 兼容centros不存在的场景
在update方法中,给pop()设置默认值,避免触发异常:
def update(self, instance, validated_data): with transaction.atomic(): # 若validated_data无centros,默认返回空列表 centros = validated_data.pop('centros', []) instance = super(UpdateUsuarioSerializer, self).update(instance, validated_data) instance.centros.clear() for centro in centros: instance.centros.add(Centro.objects.get(id=centro)) return instance
若需保留用户原有关联的Centro,删除instance.centros.clear()即可。
2. 显式定义ManyToMany字段序列化规则
默认ModelSerializer对ManyToMany字段的处理可能不符合预期,显式指定字段类型并允许为空:
class UpdateUsuarioSerializer(serializers.ModelSerializer): email = serializers.CharField(max_length=250, required=False) first_name = serializers.CharField(max_length=250, required=False) # 显式定义centros字段,自动转换ID为实例 centros = serializers.PrimaryKeyRelatedField( queryset=Centro.objects.all(), many=True, required=False ) class Meta: model = Usuario fields = ['organizacion', 'departamento', 'centros', 'telefono', 'rut', 'profesion', 'cargo', 'nacionalidad', 'sexo', 'fecha_nacimiento', 'imagen_perfil', 'cv', 'email', 'first_name', 'last_name', 'linkedin'] def update(self, instance, validated_data): with transaction.atomic(): centros = validated_data.pop('centros', []) instance = super().update(instance, validated_data) instance.centros.clear() for centro in centros: instance.centros.add(centro) return instance
此方式无需再手动查询Centro实例,效率更高。
3. 支持部分字段更新
若接口允许仅更新部分字段(即PATCH请求),在实例化序列化器时添加partial=True:
def update(self, request, pk=None): # ... 其他代码 ... instance = Usuario.objects.get(id=pk) # 允许部分字段更新 serializer = UpdateUsuarioSerializer(instance, data=request.data, partial=True) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_202_ACCEPTED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
配合pop('centros', []),即使请求未传centros字段,代码也能正常执行。
内容的提问来源于stack exchange,提问作者Angel Daniel Ascanio Salazar
相关产品推荐
相关产品推荐

