Джанго: как мне проверить unique_together внутри модели

У меня есть следующее:

class AccountAdmin(models.Model):

    account = models.ForeignKey(Account)
    is_master = models.BooleanField()
    name = models.CharField(max_length=255)
    email = models.EmailField()

    class Meta:
        unique_together = (('Account', 'is_master'), ('Account', 'username'),)

Если я затем создам нового AccountAdmin с тем же именем пользователя, что и другой в той же учетной записи, вместо того, чтобы выдавать мне ошибку для отображения в шаблоне, он прерывается с IntegrityError, и страница умирает. Я бы хотел, чтобы, на мой взгляд, я мог просто пойти:

if new_accountadmin_form.is_valid():
    new_accountadmin_form.save()

Как мне победить эту проблему. Есть ли второй метод типа is_valid(), который проверяет БД на нарушение части unique_together = (('Account', 'is_master'), ('Account', 'username'),)?

На мой взгляд, я бы не хотел ловить IntegrityError. Это доменная логика, смешанная с логикой представления. Это нарушает DRY, потому что если я покажу одну и ту же форму на 2 страницах, мне придется повторять один и тот же блок. Это также нарушает DRY, потому что если у меня есть две формы для одного и того же, я должен написать одно и то же, кроме: снова.


person orokusaki    schedule 17.12.2009    source источник


Ответы (3)


Есть два варианта:

а) Создайте блок try, в котором вы сохраните свою модель, перехватите IntegrityError и обработайте ее. Что-то типа:

try:
    new_accountadmin_form.save()
except IntegrityError:
    new_accountadmin_form._errors["account"] = ["some message"]
    new_accountadmin_form._errors["is_master"] = ["some message"]

    del new_accountadmin_form.cleaned_data["account"]
    del new_accountadmin_form.cleaned_data["is_master"]

б) В методе clean() вашей формы проверьте, существует ли строка, и поднимите forms.ValidationError с соответствующим сообщением. Пример здесь.


Итак, б) это... Вот почему я сослался на документацию; там есть все, что вам нужно.

Но это будет что-то вроде:

class YouForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
       """ This is the form's clean method, not a particular field's clean method """
       cleaned_data = self.cleaned_data

       account = cleaned_data.get("account")
       is_master = cleaned_data.get("is_master")
       username = cleaned_data.get("username")

       if AccountAdmin.objects.filter(account=account, is_master=is_master).count() > 0:
           del cleaned_data["account"]
           del cleaned_data["is_master"]
           raise forms.ValidationError("Account and is_master combination already exists.")

       if AccountAdmin.objects.filter(account=account, username=username).count() > 0:
           del cleaned_data["account"]
           del cleaned_data["username"]
           raise forms.ValidationError("Account and username combination already exists.")

    # Always return the full collection of cleaned data.
    return cleaned_data

Для чего это стоит - я только что понял, что ваш unique_together выше ссылается на поле с именем username, которое не представлено в модели.

Приведенный выше чистый метод вызывается после вызова всех чистых методов для отдельных полей.

person cethegeek    schedule 17.12.2009
comment
Как я могу сделать это в чистом методе и присоединить его к обычному чистому методу. Я знаю о super(MyForm, self).clean(), но как я могу связать их вместе, чтобы все проверки происходили одновременно, чтобы я мог отобразить это на странице: - Извините, ваш адрес электронной почты недействителен - Извините, ваш имя пользователя уже существует для этой учетной записи и т. д. - person orokusaki; 17.12.2009
comment
К сожалению, ваш второй метод зависит от гонок, вы все равно будете иногда получать IntegrityError. - person Suor; 23.11.2020

Причём совершенно универсальным образом. В модели есть следующие две вспомогательные фнс:

def getField(self,fieldName):
  # return the actual field (not the db representation of the field)
  try:
    return self._meta.get_field_by_name(fieldName)[0]
  except models.fields.FieldDoesNotExist:
    return None

а также

def getUniqueTogether(self):
  # returns the set of fields (their names) that must be unique_together
  # otherwise returns None
  unique_together = self._meta.unique_together
  for field_set in unique_together:
    return field_set
  return None

А в форме иметь следующие фн:

def clean(self):
  cleaned_data = self.cleaned_data
  instance = self.instance

  # work out which fields are unique_together
  unique_filter = {}
  unique_fields = instance.getUniqueTogether()
  if unique_fields:
    for unique_field in unique_fields:
      field = instance.getField(unique_field)
      if field.editable: 
        # this field shows up in the form,
        # so get the value from the form
        unique_filter[unique_field] = cleaned_data[unique_field]
      else: 
        # this field is excluded from the form,
        # so get the value from the model
        unique_filter[unique_field] = getattr(instance,unique_field)

    # try to find if any models already exist in the db;
    # I find all models and then exlude those matching the current model.
    existing_instances = type(instance).objects.filter(**unique_filter).exclude(pk=instance.pk)

    if existing_instances:
      # if we've gotten to this point, 
      # then there is a pre-existing model matching the unique filter
      # so record the relevant errors
      for unique_field in unique_fields:
        self.errors[unique_field] = "This value must be unique."
person trubliphone    schedule 04.01.2013

Model.Meta.unique_together создает ограничение, ограниченное базой данных, в то время как ModelForm.is_valid() в первую очередь основан на правильных типах. Событие, если бы оно проверяло ограничения, у вас было бы состояние гонки, которое все еще могло вызвать IntegrityError в вызове save().

Вы, вероятно, хотите поймать IntegrityError:

if new_accountadmin_form.is_valid():
    try:
        newaccountadmin_form.save()
    except IntegrityError, error:
        # here's your error handling code
person teepark    schedule 17.12.2009
comment
1) Одна проблема заключается в том, что мне нужно импортировать IntegrityError из правильной базы данных, что требует дополнительной настройки. 2) Другая проблема заключается в том, что я не хочу использовать логику проверки в своих представлениях. 3) (риторика, направленная на сам Django) Какой смысл накладывать ограничения unique_together на что-то, если нет логического способа справиться с этим во время проверки. - person orokusaki; 17.12.2009
comment
На ваш третий пункт отвечает документация: она предназначена для обеспечения создания ограничения базы данных и для интерфейса администратора. - person cethegeek; 17.12.2009
comment
1) Вы можете импортировать IntegrityError из django.db, и он получит правильный IntegrityError бэкенда на основе ваших настроек. 2) Я тоже нет. Вероятно, вам следует написать это в методе вашего подкласса ModelForm. 3) Потому что единственная альтернатива — не предлагать способ определения ограничений уникальности вместе. - person teepark; 20.12.2009