Django模型预先保存为自定义ManyToManyField

时间:2012-05-12 20:48:49

标签: python django django-models django-forms

我有一个多对多关系的模型。在pre_save信号中,我想预处理实例数据:

@receiver(pre_save, sender=Book)
def pre_save_tags(sender, instance, **kwargs):
    # tags is a many-to-many relation
    print instance.tags

tags设置在SelectMultiple字段中。因此,它有多个值。

问题

ValueError: 'Book' instance needs to have a primary key value before a many-to-many relationship can be used.
  1. 如何获取尚未保存的实例的标签?
  2. 如何在保存之前更改标签?
  3. 我不仅限于pre_save。如果有更好的解决方案,我会全力以赴。

2 个答案:

答案 0 :(得分:0)

这是因为对象是新的,并且尚未保存。因为它没有主键。如果保存之前保存的对象,则不应发生此错误。

为什么不使用post_save。在post_save中,对象已保存,因此具有主键。如果必须使用pre_save,则可以检查instance.id是否存在。如果它不存在,则不能有任何标记(因为对象必须具有ManyToMany-fields的主要ID才能工作(因此例外))

要测试主键,您可以执行以下操作:

@receiver(pre_save, sender=Book)
def pre_save_tags(sender, instance, **kwargs):
    # tags is a many-to-many relation
    if instance.id:
        print instance.tags

答案 1 :(得分:0)

知道了。我相信有更优雅的方式。如果您知道如何改进代码,请给我一些指示或提供更好的解决方案:

class ManyToManyTagField(ManyToManyField):

    def formfield(self, **kwargs):
        defaults = {
            'form_class': TagField,
        }
        return super(ManyToManyTagField, self).formfield(**defaults)

    def save_form_data(self, instance, data):
        tags = []
        for tag_id in data:
            autocompletedTag = tag_id.find('___')
            if autocompletedTag > 0:
                parts = tag_id.split('___')
                tags.append(parts[0])
            else:
                newTag = Tag(name=tag_id, category=TagCategory.objects.get(name='New'))
                newTag.save()
                tags.append(newTag.id)

        return super(ManyToManyTagField, self).save_form_data(instance, tags)

相关部分是方法save_form_data

以下是其他课程,仅为了完整起见。

TagWidget:

class TagWidget(SelectMultiple):
    """
    Widget for Tag input fields.
    """
    queryset = []

    def render(self, name, value, attrs=None):
        final_attrs = self.build_attrs(attrs, name=name)
        final_attrs['class'] = 'tag-input'

        selectedTags = []
        if value:
            for tag_id in value:
                if isinstance(tag_id, (int, long)) or tag_id.isdigit():
                    tag_name = Tag.objects.get(pk=tag_id).name
                    selectedTags.append({ 'value': str(tag_id) + '___' + tag_name, 'label': tag_name })
                else:
                    autocompletedTag = tag_id.find('___')
                    if autocompletedTag > 0:
                        parts = tag_id.split('___')
                        selectedTags.append({ 'value': parts[0] + '___' + parts[1], 'label': parts[1] })
                    else:
                        selectedTags.append({ 'value': tag_id, 'label': tag_id })

        allTags = []
        for tag in self.queryset:
            allTags.append({ 'value': str(tag.id) + '___' + str(tag.name), 'label': str(tag.name) });

        allTagsJson = simplejson.dumps(allTags)

        return render_to_string('form/tagwidget.html',
                                {'attrs': flatatt(final_attrs), 'selectedTags': selectedTags, 'allTags': allTagsJson})

TagField:

class TagField(ModelMultipleChoiceField):
    widget = TagWidget

    def __init__(self, queryset, *args, **kwargs):
        self.widget.queryset = queryset
        super(TagField, self).__init__(queryset, *args, **kwargs)

    def clean(self, value):
        if self.required and not value:
            raise ValidationError(self.error_messages['required'])
        elif not self.required and not value:
            return []
        if not isinstance(value, (list, tuple)):
            raise ValidationError(self.error_messages['list'])
        # Since this overrides the inherited ModelChoiceField.clean
        # we run custom validators here
        self.run_validators(value)
        return value

tagwidget.html:

{% autoescape off %}
<ul{{ attrs }}>
{% for tag in selectedTags %}
    <li tagValue="{{ tag.value }}">{{ tag.label }}</li>
{% endfor %}
</ul>
<script type="text/javascript">
var autocompletionData = {{ allTags }};
</script>
{% endautoescape %}