I have a Foo table and a Tags table - foo and tags are a many to many relationship
When I create a Foo - I would like to use a CreateView form view. But I would like the entry for the tags to be a text widget.
I thought I could set tags to be a TextInput widget and add a “clean_tags” method on the FooCreateViewForm (where I could create new tags if I needed to - but also convert the tag names into tags to be used by the CreateView) - but that clean method doesn’t seem to be called and the form says “Enter a list of values” for the Tags field.
class FooCreateViewForm(forms.ModelForm):
class Meta:
model = Foo
fields = ["name", "description", "tags"]
widgets = {
"description": Textarea(attrs={"cols": 80, "rows": 10}),
"tags": TextInput()
}
def clean_tags(self):
data = self.cleaned_data['tags']
form_tags = []
with transaction.atomic():
for tag_string in data:
tag = Tag.objects.filter(name=tag_string).first()
if tag:
form_tags.append(tag)
else:
tag_id = uuid.uuid4()
new_tag = Tag(id=tag_id, name=tag_string)
new_tag.save()
form_tags.append(new_tag)
return form_tags
class FooCreateView(
SuccessMessageMixin, LoginRequiredMixin, CreateView
):
template_name = "foos/foo_create.html"
model = Foo
form_class = FooCreateViewForm
success_message = "%(name)s foo was created successfully"
def get_success_url(self):
return Foo.get_absolute_url(self.object.id)
def get_success_message(self, cleaned_data):
return self.success_message % dict(cleaned_data)
def form_valid(self, form):
# not really worried about this yet - as the clean function isn't called
clean_data = form.cleaned_data
return super().form_valid(form)
can we see what you get as a result and your model as well?
SInce your tags field is not a direct association with the tags field in the model, you don’t want to make that a model form field. You want to create an extra field in the form for the tags, and then parse/process those tags as individual entries within the form.
Once you’ve validated what has been submitted, you would then want to save the form for Foo such that you get a valid Foo object, and then add the individual instances of Tags to Foo.
1 Like
Thanks Ken - exactly what I needed. It takes a little while to understand where the logic should go - the form or the view.
class FooCreateViewForm(forms.ModelForm):
tag_string = CharField(help_text="Use single words separated with spaces. "
"Use dashes for multi-word tags.")
class Meta:
model = Foo
fields = ["name", "description"]
widgets = {
"description": Textarea(attrs={"cols": 80, "rows": 10}),
}
def save(self, commit=True):
cleaned_data = self.cleaned_data
data = cleaned_data['tag_string'].split(" ")
form_tags = []
with transaction.atomic():
for tag_string in data:
tag = Tag.objects.filter(name=tag_string).first()
if tag:
form_tags.append(tag)
else:
tag_id = uuid.uuid4()
new_tag = Tag(id=tag_id, name=tag_string)
new_tag.save()
form_tags.append(new_tag)
m = super().save(commit=commit)
m.tags.set([t.id for t in form_tags])
m.save()
return m
Creating forms from models | Django documentation | Django - shows that fields excluded from the form are not saved - and can either be passed in as an instance - or mutated after the save (just set commit to false to prevent double mutations).
This works locally.
1 Like
Side note: The set
method on the objects should accept an iterable of the objects themselves. I don’t believe you need to iterate over your list to produce a list of IDs to add.
I believe you should be able to do this: m.tags.set(form_tags)
. (See Related objects reference | Django documentation | Django)
Also, you could probably replace this:
With something like this:
tag, created = Tag.objects.get_or_create(name=tag_string)
form_tags.append(tag)
… assuming you set the right default
for the id
field in the Tag model. (Aside from actually creating the tag, I don’t see where you “care” in this code whether a new Tag was created or an existing one used.)
Side note #2: There’s a race condition here where you could end up with more than one instance of a Tag with the same name - possibly creating a situation where a search for a tag doesn’t retrieve all the matching instances.
1 Like
Just to be clear - the race condition would be cleared up if you use the get_or_create method and the transaction atomic? Or is there more to think about.
Yes, my reading of the get_or_create
method looks to me like it resolves the race condition. You can see the source for it in django.db.models.query.QuerySet - you might find it to be an interesting read.
However, you still might want to either:
- Remove the
id
field from Tag
and make name
the primary key
- Add a Unique Constraint on
name