如何在我的 Django 模型上强制执行 ManyToMany blank=False 约束?

How do I enforce a ManyToMany blank=False constraint on my Django model?

我正在使用 Django 3 和 Python 3.8。我有以下模型,请注意“类型”ManyToMany 字段,其中我将“空白”设置为 False。

class Coop(models.Model):
    objects = CoopManager()
    name = models.CharField(max_length=250, null=False)
    types = models.ManyToManyField(CoopType, blank=False)
    addresses = models.ManyToManyField(Address)
    enabled = models.BooleanField(default=True, null=False)
    phone = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_phone')
    email = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_email')
    web_site = models.TextField()

我想验证如果将该字段留空是否会发生验证错误,所以我有

  @pytest.mark.django_db
    def test_coop_create_with_no_types(self):
        """ Verify can't create coop if no  """    
        coop = CoopFactory.create(types=[])
        self.assertIsNotNone(coop)
        self.assertNone( coop.id )

并使用以下工厂(与 FactoryBoy)构建模型

class CoopFactory(factory.DjangoModelFactory):
    """
        Define Coop Factory
    """
    class Meta:
        model = Coop

    name = "test model"
    enabled = True
    phone = factory.SubFactory(PhoneContactMethodFactory)
    email = factory.SubFactory(EmailContactMethodFactory)
    web_site = "http://www.hello.com"

    @factory.post_generation
    def addresses(self, create, extracted, **kwargs):
        if not create:
            # Simple build, do nothing.
            return

        if extracted:
            # A list of types were passed in, use them
            for address in extracted:
                self.addresses.add(address)
        else:
            address = AddressFactory()
            self.addresses.add( address )

    @factory.post_generation
    def types(self, create, extracted, **kwargs):
        if not create:
            # Simple build, do nothing.
            return

        if extracted:
            # A list of types were passed in, use them
            for _ in range(extracted):
                self.types.add(CoopTypeFactory())

然而,“self.assertNone( coop.id )”断言失败(生成一个 ID)。我希望这不会发生,因为我没有指定任何类型。我还需要做什么来强制执行我的约束,或者我应该使用不同的约束?

编辑:响应@Melvyn的建议,尝试将测试修改为以下

@pytest.mark.django_db
def test_coop_create_with_no_types(self):
    """ Test customer model """    # create customer model instance
    coop = CoopFactory.build(types=[])
    coop.full_clean()
    self.assertIsNotNone(coop)
    self.assertIsNone( coop.id )

但不仅没有收到“类型”字段的验证错误,还收到了电子邮件和 phone 字段的验证错误,这显然是在工厂中填充的。

  File "/Users/davea/Documents/workspace/chicommons/maps/web/tests/test_models.py", line 76, in test_coop_create_with_no_types
    coop.full_clean()
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/django/db/models/base.py", line 1221, in full_clean
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'phone': ['This field cannot be blank.'], 'email': ['This field cannot be blank.']}

编辑: 根据@ArakkalAbu 给出的答案,我实施了建议 (https://github.com/chicommons/maps/blob/master/web/directory/serializers.py) 但此测试继续通过

@pytest.mark.django_db
def test_coop_create_no_coop_types(self):
    """ Test coop serizlizer model """
    name = "Test 8899"
    street = "222 W. Merchandise Mart Plaza, Suite 1212"
    city = "Chicago"
    postal_code = "60654"
    enabled = True
    postal_code = "60654"
    email = "test@example.com"
    phone = "7732441468"
    web_site = "http://www.1871.com"
    state = StateFactory()
    serializer_data = {
        "name": name,
        "types": [
        ],
        "addresses": [{
            "formatted": street,
            "locality": {
                "name": city,
                "postal_code": postal_code,
                "state": state.id
            }
        }],
        "enabled": enabled,
        "phone": {
          "phone": phone
        },
        "email": {
          "email": email
        },
        "web_site": web_site
    }

    serializer = CoopSerializer(data=serializer_data)
    assert serializer.is_valid(True), serializer.errors
ManyToManyField 上的

blank=True 不会转换为 DBMS 约束,但会(例如)在表单验证时进行检查。

在您的单元测试中,您使用的 CoopFactory.create 似乎不检查此逻辑(和非 dbms)约束。

https://docs.djangoproject.com/en/3.0/ref/models/fields/#blank

Note that this is different than null. null is purely database-related, whereas blank is validation-related. If a field has blank=True, form validation will allow entry of an empty value. If a field has blank=False, the field will be required.

您不能强制执行此约束,blank=False 既不 数据库级别 模型级别 。因为每个 m2m 关系都有一条记录,该记录带有指向 m2m 关系(through--(Django Doc) 关系)两侧的外键。

同样在 m2m 关系中,m2m 项目通过单独的操作链接

  1. 创建CoopFactory个实例
  2. 使用 .add()--(django doc) or .set()--(django doc) 方法将 CoopType 添加到 Coop.types

即不能通过

直接创建M2M关系
Coop.objects.create(name='foo', types=[1, 2, 3]) # 1,2 & 3 are PKs of `CoopType`

这条语句触发异常,

TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use types.set() instead.


最好的选择是什么?

as per your ,

I'm not using this in a form though. I'm using the model by a serializer as part of the Django rest framework.

由于您使用的是 DRF,因此您可以验证 传入的有效载荷.

class CoopSerializer(serializers.ModelSerializer):
    class Meta:
        model = Coop
        fields = '__all__'
        <b>extra_kwargs = {
            'types': {
                'allow_empty': False
            }
        }</b>
# execution
s = CoopSerializer(data={'name': 'foo coop', 'types': []})
s.is_valid(True)
s.save()

# excption
rest_framework.exceptions.ValidationError: {'types': [ErrorDetail(string='This list may not be empty.', code='empty')]}

这将帮助您强制获得所需的 M2M 数据。