Django admin.py: save_model () не вызывается model.save () в методе сохранения ModelForm - PullRequest
2 голосов
/ 10 марта 2012

В модели моего блога есть поле для множества тегов:

tags = models.ManyToManyField(PostTag)

Но редактировать его было неудобно, и я изменил свою модель так:

def _get_tagging(self): # Returns comma separated list of tags
    tagging = []
    for tag in self.tags.all():
        tagging.append(tag.name)
    return ", ".join(tagging)

def _set_tagging (self, tagging): # Saves tags from comma separated list
    tagging = tagging.split(", ")
    self.tags.clear()
    for tag in tagging:
        if len(tag) < 1:
            continue
        try:
            self.tags.add(PostTag.objects.get(name=tag))
        except ObjectDoesNotExist:
            self.tags.create(name=tag)

tagging = property(_get_tagging, _set_tagging)

Затем я изменил свой admin.py:

class BlogAdminForm (forms.ModelForm):
    tagging = forms.CharField(required=False, label="Tags", max_length=200,
                        widget=forms.TextInput(attrs={'class':'vTextField'}))

    class Meta:
        model = BlogPost

    def __init__(self, *args, **kwargs):
        super(BlogAdminForm, self).__init__(*args, **kwargs)

        if kwargs.has_key('instance'):
            instance = kwargs['instance']
            self.initial['tagging'] = instance.tagging

    def save(self, commit=True):
        model = super(BlogAdminForm, self).save(commit=False)
        model.tagging = self.cleaned_data["tagging"]

        if commit:
            model.save()

        return model

И это работало нормально, но только для редактирования объектов.Я получил ошибку, когда попытался создать новый объект.Зачем?Поскольку отношение «многие ко многим» можно использовать с объектом, которого еще нет в базе данных и который не имеет первичного ключа (экземпляр «BlogPost» должен иметь значение первичного ключа, прежде чем отношение «многие ко многим» можетиспользоваться).Я попытался решить ее, отредактировав метод сохранения следующим образом:

def save(self, commit=True):
    model = super(BlogAdminForm, self).save(commit=False)
    try:
        model.tagging = self.cleaned_data["tagging"]
    except ValueError:
        model.save()
        model.tagging = self.cleaned_data["tagging"]

    if commit:
        model.save()

Это решило исходную проблему.Но теперь model.save() не вызывает метод save_model моей модели администратора:

class BlogAdmin (admin.ModelAdmin):
    # ... 
    form = BlogAdminForm

    def save_model(self, request, obj, form, change):
        obj.author = request.user
        obj.save()

В результате я получаю новую ошибку: null value in column "author_id" violates not-null constraint. Что я делаю не так?Можно ли вызвать этот метод вручную?

1 Ответ

3 голосов
/ 10 марта 2012

Вам нужно будет сохранить теги после сохранения экземпляра, а это значит сделать это в вашей функции save_model. Это не имеет ничего общего с вашим кодом манипуляции тегами: если вы посмотрите на документацию для метода Form.save , там написано:

Еще один побочный эффект использования commit=False наблюдается, когда ваша модель имеет отношение многие ко многим с другой моделью. Если ваша модель имеет отношение «многие ко многим» и вы указываете commit=False при сохранении формы, Django не может сразу сохранить данные формы для отношения «многие ко многим». Это связано с тем, что невозможно сохранить данные «многие ко многим» для экземпляра до тех пор, пока этот экземпляр не появится в базе данных.

Чтобы обойти эту проблему, каждый раз, когда вы сохраняете форму, используя commit=False, Django добавляет метод save_m2m() к вашему подклассу ModelForm. После того, как вы вручную сохранили экземпляр, созданный формой, вы можете вызвать save_m2m(), чтобы сохранить данные формы «многие ко многим».

Есть несколько способов решить вашу проблему. Вы могли бы написать widget , который конвертирует туда и обратно между списками идентификаторов тегов и имен тегов через запятую, а затем вызывать form.save_m2m() в вашем методе save_model. Но у этого подхода есть тот недостаток, что вам придется создавать новые теги при декодировании значения из виджета, даже если форма не сохранена (возможно, из-за ошибки проверки в другом месте формы).

Так что я думаю, что лучший подход в этом случае - добавить свой собственный метод save_tags в форму:

class BlogAdminForm(forms.ModelForm):
    tagging = forms.CharField(required=False, label="Tags", max_length=200,
                              widget=forms.TextInput(attrs={'class':'vTextField'}))

    class Meta:
        model = Post

    def __init__(self, *args, **kwargs):
        super(BlogAdminForm, self).__init__(*args, **kwargs)
        if 'instance' in kwargs:
            tags = (t.name for t in kwargs['instance'].tags.all())
            self.initial['tagging'] = ', '.join(tags)

    def save_tags(self, obj):
        obj.tags = (Tag.objects.get_or_create(name = tag.strip())[0]
                    for tag in self.cleaned_data['tagging'].split(','))

class BlogPostAdmin(admin.ModelAdmin):
    form = BlogAdminForm

    def save_model(self, request, obj, form, change):
        obj.author = request.user
        obj.save()
        form.save_tags(obj)

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

  • 'instance' in kwargs проще, чем kwargs.has_key('instance').

  • Генераторное выражение (t.name for t in kwargs['instance'].tags.all()) проще, чем построение списка в цикле for.

  • Метод get_or_create - это удобный ярлык, позволяющий избежать необходимости try: ... except ObjectDoesNotExist: ...

  • Вы можете назначить поле ManyToMany непосредственно вместо вызова clear и затем add (также, это более эффективно, когда теги не меняются).

...