Django form validation and action on two models - django

I haven't got much experience with building custom forms (only default model forms) and I'm looking for some help here. I'm trying to build a form that will get "feed_url" from user and if it already exist, just add reference into UserFeed model. In case it doesn't exist, it should add it to the Feed model and also reference it in UserFeed model as well.
models
class Category(models.Model):
name = models.CharField(unique=False, max_length=64)
user = models.ForeignKey(User)
slug = AutoSlugField(populate_from='name', always_update='True', unique_with='user')
def __unicode__(self):
return self.name
class Meta:
ordering = ('name',)
class Feed(models.Model):
feed_url = models.URLField(unique=True)
def __unicode__(self):
return self.feed_url
class UserFeed(models.Model):
feed = models.ForeignKey(Feed)
title = models.CharField(max_length=64)
category = models.ForeignKey(Category)
user = models.ForeignKey(User)
slug = AutoSlugField(populate_from='title', always_update='True', unique_with='user')
def __unicode__(self):
return self.title
class Meta:
ordering = ('title',)
forms
class UserFeedForm(forms.Form):
feed_url = forms.URLField()
title = forms.CharField(max_length=64)
category = forms.ModelChoiceField(Category)
user = forms.HiddenInput()
views
def addfeed(request):
categories = Category.objects.filter(user=request.user)
feeds = Feed.objects.all()
if request.method == 'POST':
form = UserFeedForm(request.POST)
form.fields['category'].queryset = categories
if form.is_valid():
feed = form.save(commit=False)
if form.fields['feed_url'] in feeds:
##### add to Feed object and add reference to UserFeed object
feed.user = request.user
feed.save()
else:
##### get id from Feed object and add reference to UserFeed object
feed.user = request.user
feed.save()
return HttpResponseRedirect("/reader/manage")
else:
form = UserFeedForm()
form.fields['category'].queryset = categories
context = {'form': form,}
return expand_context_and_render(request, context, 'reader/form.html')
Could anybody please point me in the right direction?
thanks
UPDATE
Now I have updated my view and form as follows:
forms
class UserFeedForm(forms.ModelForm):
feed = forms.URLField()
title = forms.CharField(max_length=64)
category = forms.ModelChoiceField(Category)
user = forms.HiddenInput()
class Meta:
model = UserFeed
fields = ['feed', 'title', 'category']
views
def addfeed(request):
categories = Category.objects.filter(user=request.user)
feeds = Feed.objects.all()
if request.method == 'POST':
form = UserFeedForm(request.POST)
form.fields['category'].queryset = categories
if form.is_valid():
feed = form.cleaned_data.get('feed')
if feed in feeds:
##### get id from Feed and add reference to UserFeed
existing_feed = Feed.objects.get(feed_url=feed)
form.feed = existing_feed.id
form.user = request.user
form.save(commit=True)
else:
##### add to Feed object and then add reference to UserFeed object
Feed.object.create(feed_url=feed)
existing_feed = Feed.objects.get(feed_url=feed)
form.feed = existing_feed.id
form.user = request.user
form.save(commit=True)
return HttpResponseRedirect("/reader/manage")
else:
form = UserFeedForm()
form.fields['category'].queryset = categories
context = {'page_title': page_title,
'form': form,
}
return expand_context_and_render(request, context, 'reader/form.html')
I think I'm getting closer but it still won't work, giving me this error:
"Cannot assign "u'http://feeds.bbci.co.uk/news/rss.xml'": "UserFeed.feed" must be a "Feed" instance."
Any idea how can I fix this?
thanks

Try taking a look at this question, and also inline formsets in the docs.
Additionally in your UserFieldForm, feed is defined as a URLField. So in your view, in this line:
feed = form.get_cleaned_data.get('feed')
your feed variable is simply a string, and not actually a Feed object. This is where your error might be occurring? That's where inline formsets could help you. Also, this line also makes your your if feed in feeds: statement moot, because feed is just a string, whereas feeds is a Queryset of all your Feed instances. I think this will cause your view to always end up in the else block?
I'm new to Django myself but I hope this helps you.

The logic you are looking for is:
Check if the feed exists.
If it exists, fetch it.
If it doesn't create a new feed.
Associate the feed with a UserFeed object.
The get_or_create method does parts 1-3, so you need the following:
if form.is_valid():
obj = form.save(commit=False)
feed = form.cleaned_data.get('feed')
feed_obj, created = Feed.objects.get_or_create(feed_url=feed)
obj.feed = feed_obj
obj.save()
return HttpResponseRedirect("/reader/manage")

I finally got it working like this:
forms
class UserFeedForm(forms.Form):
feed = forms.URLField()
title = forms.CharField(max_length=64)
category = forms.ModelChoiceField(Category)
user = forms.HiddenInput()
class Meta:
model = UserFeed
fields = ['feed', 'title', 'category']
views
def addfeed(request):
user = request.user
categories = Category.objects.filter(user=request.user)
if request.method == 'POST':
form = UserFeedForm(request.POST)
form.fields['category'].queryset = categories
if form.is_valid():
feed = form.cleaned_data['feed']
category = form.cleaned_data['category']
title = form.cleaned_data['title']
feed_obj, created = Feed.objects.get_or_create(feed_url=feed)
obj = UserFeed(feed=feed_obj, title=title, category=category, user=user)
obj.save()
return HttpResponseRedirect("/reader/manage")
else:
form = UserFeedForm()
form.fields['category'].queryset = categories
context = {'page_title': page_title,
'form': form,
}
return expand_context_and_render(request, context, 'reader/form.html')
Both tips from the answers below pointed me in the right direction. Thanks !

Related

Django - forms trying to clean unique together, CrispyError

I have a model with a unique together and I want to validate this condition in my modelform. The unique together includes a field that is passed to the form in an init method, the user, and a field that is in the form. I'm having problems with validating a unique together condition.
EDIT
I have modified the code to what you see below
model:
class Objective(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
objective_name = models.CharField(max_length=10)
description = models.CharField(max_length=300)
mode = models.CharField(max_length=2, default='LA')
class Meta:
unique_together = ['user', 'objective_name', 'course']
ordering = ['objective_name']
def __str__(self):
return self.objective_name
The view:
def addobjective(request, course_id):
this_course = get_object_or_404(Course, pk=course_id)
user = request.user
all_courses = Course.objects.filter(user=user)
objective_list = Objective.objects.filter(
course=this_course).order_by('objective_name')
context = {'objective_list': objective_list}
if request.method == 'POST':
form = ObjectiveForm(user, request.POST, my_course=this_course)
if form.is_valid():
obj = form.save(commit=False)
obj.course = this_course
obj.user = user
obj.save()
form = ObjectiveForm(user, my_course=this_course)
context['form'] = form
return redirect('gradebook:addobjective', course_id=this_course.id)
else:
form = ObjectiveForm(user, my_course=this_course)
context['form'] = form
context['this_course'] = this_course
context['all_courses'] = all_courses
return render(request, 'gradebook/objective-form.html', context)
forms.py:
class ObjectiveForm(ModelForm):
def __init__(self, user, *args, **kwargs):
self.request = kwargs.pop('request', None)
my_course = kwargs.pop('my_course')
self.objs = Objective.objects.filter(user=user, course=my_course)
super(ObjectiveForm, self).__init__(*args, **kwargs)
class Meta:
model = Objective
fields = ('objective_name', 'description', 'mode',)
def clean(self):
super(ObjectiveForm, self).clean()
objective_name = self.cleaned_data.get("objective_name")
description = self.cleaned_data.get("description")
mode = self.cleaned_data.get("mode")
if self.objs.filter(objective_name=objective_name).count() > 0:
print("error")
del self.cleaned_data["objective_name"]
del self.cleaned_data["description"]
del self.cleaned_data["mode"]
raise ValidationError(
"This course already has a learning objective with this name.")
return self.cleaned_data
EDIT
The error I know get is |as_crispy_field got passed an invalid or inexistent field. This occurs when I enter in a value for objective_name that is a duplicate. error is printed to the console and then I get the above error. I do not get the ValidationError.
The full traceback can be seen here.
Maybe with the form I do not need the unique together constraint in the model?
Yes, my_course field is not defined in Objective model , so maybe you need to change this line:
form = ObjectiveForm(request.POST, my_course=this_course)
To
form = ObjectiveForm(request.POST, course=this_course)
It turns out that the problem was caused by improper indentation of return redirect('gradebook:addobjective', course_id=this_course.id) after the if form.is_valid():. The return redirect has to be a part of the POST request.

Save two model (One contains foreign key) data from a single template

I'm pretty new to django and working on a blog based project where user can add ratings to specific type of review post.For example giving stars are enabled for restaurant but not for tech stores.
I have created two different form for "review" and "star" model. I want to rate the restaurant using the model named "star" and do it in same template.But I'm having difficulties to do that.
Im getting this error.
"The above exception (NOT NULL constraint failed: reviews_star.post_id_id) was the direct cause of the following exception:"
How do I get the review id that I just save with "review_form.save()".
my review model kinda looks like this (Removed other attributes which aren't related to this problem):
class Review(models.Model):
review_title = models.CharField(verbose_name='Title', max_length=100)
review_body = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE)
restaurant_or_techstore = models.CharField(verbose_name='Foods or Travel',max_length=20)
slug = models.SlugField(null=False,unique=True,max_length = 300)
My rating model looks like this:
class Star(models.Model):
post_id = models.ForeignKey(Review, on_delete = models.CASCADE )
food = models.FloatField(verbose_name='Food',null=False)
service = models.FloatField(verbose_name='Food',null=False)
cleanliness = models.FloatField(verbose_name='Food',null=False)
and my view :
def CreateReview(request):
ImageFormSet = modelformset_factory(Image,form=ImageForm,extra=5)
if request.method == 'POST':
reviewForm = ReviewForm(request.POST)
formset = ImageFormSet(request.POST,request.FILES,queryset=Image.objects.none())
starsForm = StarsrForm(request.POST)
if reviewForm.is_valid() and formset.is_valid() and starsForm.is_valid():
review_form = reviewForm.save(commit=False)
review_form.author = request.user
review_form.post_or_discussion = 1
review_form.food_or_travel = 'Foods'
review_form.save()
reviewForm.save_m2m()
starsForm.save()
for form in formset.cleaned_data:
if form:
image = form['image']
photo = Image(review=review_form,image=image)
photo.save()
messages.success(request,'Image Uploaded Successfully')
return HttpResponseRedirect("/")
else:
print(reviewForm.errors, formset.errors)
else:
reviewForm = ReviewForm()
starsForm = StarsrForm()
formset = ImageFormSet(queryset=Image.objects.none())
return render(request,'reviews/review_form.html',{'reviewForm':reviewForm,'formset':formset,'starsForm':starsForm})
best try when you use ForeighKey is to use related_name attr for this field
class Star(models.Model):
post = models.ForeignKey(Review, related_name='post_review', on_delete = models.CASCADE )
after it you can refer to star from review by some_object_review.post_review.some_field_star
For you error above try reviews_star.post_id.id
Solve The problem.All I had to do is pass the review_form instance to the star form.
new view is:
if request.method == 'POST':
reviewForm = ReviewForm(request.POST)
formset = ImageFormSet(request.POST,request.FILES,queryset=Image.objects.none())
starsForm = StarsrForm(request.POST)
if reviewForm.is_valid() and formset.is_valid() and starsForm.is_valid():
review_form = reviewForm.save(commit=False)
review_form.author = request.user
review_form.post_or_discussion = 1
review_form.food_or_travel = 'Foods'
review_form.save()
reviewForm.save_m2m()
instance = review_form
print(instance.id)
star_form = starsForm.save(commit=False)
star_form.post_id = instance
star_form.save()
for form in formset.cleaned_data:
if form:
image = form['image']
photo = Image(review=review_form,image=image)
photo.save()
messages.success(request,'Image Uploaded Successfully')
return HttpResponseRedirect("/")
else:
print(reviewForm.errors, formset.errors)
else:
reviewForm = ReviewForm()
starsForm = StarsrForm()
formset = ImageFormSet(queryset=Image.objects.none())
return render(request,'reviews/review_form.html',{'reviewForm':reviewForm,'formset':formset,'starsForm':starsForm})

How to retrieve user selections from a previous form

I have several forms that take people through steps and below are the first two and the simplest ones and makes it easy to explain what i am having problem with.
The following two views are login required and contain one form on each. First view is the new_operator where the user fills out a single text input field. Second view is the new_asset where the user fills one text input field as the asset name and selects an operator from the a select/dropdown field. The question is how can i get the form to remember the operator name the user created in the previous form and make it as the default option? To be clear, i still want the user to select any other operator if they choose to do so but i want the option they just created to be the default. Thanks a lot in advance for the help.
First, here are the models:
class OperatorCompany(models.Model):
name = models.CharField(max_length=50, unique=True)
created_at = models.DateTimeField(default=timezone.now)
created_by = models.ForeignKey(User, related_name='operator_added_by', null=True, on_delete=models.SET_NULL)
class Meta:
verbose_name = "Operator Company"
verbose_name_plural = "Operator Companies"
def __str__(self):
return self.name
class AssetName(models.Model):
name = models.CharField(max_length=50, unique=True)
operator = models.ForeignKey(OperatorCompany, related_name='asset', on_delete=models.CASCADE)
created_at = models.DateTimeField(default=timezone.now)
created_by = models.ForeignKey(User, related_name='asset_added_by', null=True,
on_delete=models.SET_NULL)
class Meta:
verbose_name = "Asset"
verbose_name_plural = "Assets"
def __str__(self):
return self.name
views.py
def new_operator(request):
if request.method == 'POST':
form = NewOperatorForm(request.POST)
if form.is_valid():
newoperator = form.save(commit=False)
newoperator.created_by = request.user
newoperator.created_at = timezone.now()
newoperator.save()
return redirect('wellsurfer:new_asset')
else:
form = NewOperatorForm()
return render(request, 'wellsurfer/create_new_operator.html', {'create_operator': form})
def new_asset(request):
if request.method == 'POST':
form = NewAssetForm(request.POST)
if form.is_valid():
newasset = form.save(commit=False)
newasset.created_by = request.user
newasset.created_at = timezone.now()
newasset.save()
return redirect('wellsurfer:new_pad')
else:
form = NewAssetForm()
return render(request, 'wellsurfer/create_new_asset.html', {'create_asset': form})
and following are the forms.py without the init, clean functions and the widgets
class NewOperatorForm(forms.ModelForm):
class Meta:
model = OperatorCompany
fields = ('name',)
class NewAssetForm(forms.ModelForm):
class Meta:
model = AssetName
fields = ('name', 'operator')
To share data between multiple pages, you can use session variables. These are stored on the server and associated to clients according to the session cookie they communicate to the server at every request.
Typically, in the first view, you would add after save():
request.session['latest_created_operator_id'] = newoperator.id
to save in the session the operator id.
And in the second view, after the else,
operator_id = request.session.get('latest_created_operator_id', None)
operator = Operator.objects.filter(id=operator_id).first() # returns None if not found
form = NewAssetForm(initial={'operator': operator})
retrieves the operator and populates the form.
(That's untested code; you may need to edit a bit.)
At a glance, maybe something like this would work.
What you can do is add another URL in urls.py for new_asset which accepts a OperatorCompany id. I don't have your url config but it could be something like:
urls.py
path('wellsurfer/new_asset/<int:operator_id>', new_asset, name='wellsurfer:new_asset_operator')
view.py
def new_operator(request):
if request.method == 'POST':
form = NewOperatorForm(request.POST)
if form.is_valid():
newoperator = form.save(commit=False)
newoperator.created_by = request.user
newoperator.created_at = timezone.now()
newoperator.save()
return redirect('wellsurfer:new_asset', operator_id=newoperator.id)
else:
form = NewOperatorForm()
return render(request, 'wellsurfer/create_new_operator.html', {'create_operator': form})
def new_asset(request, operator_id=None):
if request.method == 'POST':
form = NewAssetForm(request.POST)
if form.is_valid():
newasset = form.save(commit=False)
newasset.created_by = request.user
newasset.created_at = timezone.now()
newasset.save()
return redirect('wellsurfer:new_pad')
else:
form = NewAssetForm()
if operator_id is not None:
operator_company = OperatorCompany.objects.get(pk=operator_id)
form.fields['operator'].initial = operator_company
return render(request, 'wellsurfer/create_new_asset.html', {'create_asset': form})

django forms - how to filter number of available options in a form

I'm trying to limit number of "categories" that user have available when entering new "feed" only to categories that he owns and he created. The way it works now is that user can add "feed" to other users' "categories" as this is what the form displays. How can I fix it ?
thanks!
-M
models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=50)
user = models.ForeignKey(User)
class Feed(models.Model):
url = models.URLField()
name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
description = models.TextField(blank=True)
category = models.ForeignKey(Category)
user = models.ForeignKey(User)
forms.py
class FeedForm(forms.ModelForm):
class Meta:
model = Feed
exclude = ['user']
views.py
def addfeed(request, user):
user = request.user
page_title = "Add feed"
instance = Category.objects.filter(user=request.user)
if request.method == 'POST':
form = FeedForm(request.POST, instance=instance)
if form.is_valid():
feed = form.save(commit=False)
feed.user = request.user
feed.save()
return HttpResponseRedirect("/user/" + user.username + "/manage")
else:
form = FeedForm()
return render(request, "form_manage.html", {
'page_title': page_title,
'form': form,
})
Set the queryset attribute of the field somewhere. Because it depends on your user, it's something you have to set during or after instantiating the form. For instance, here's how to do it in the view:
def addfeed(request, user):
user = request.user # why does this view take user as an arg and then reassign?
page_title = "Add feed"
categories = Category.objects.filter(user=request.user)
if request.method == 'POST':
form = FeedForm(request.POST)
form.fields['category'].queryset = categories
if form.is_valid():
feed = form.save(commit=False)
feed.user = request.user
feed.save()
return HttpResponseRedirect("/user/" + user.username + "/manage")
else:
form = FeedForm()
form.fields['category'].queryset = categories
return render(request, "form_manage.html", {
'page_title': page_title,
'form': form,})
I removed the instance argument to your POST case's form construction because that's meant for passing in an existing Feed instance, not a categories queryset.
You could also do this in the form's __init__ if you pass in the correct categories queryset.
I use javascript to do this. For example, you could pass a list of the relevant categories as extra context in your view then use javascript in your template to empty the pre-populated option field in the form and replace it with your extra context.

Django form is only valid after second request

I have a very strange problem with django forms, I display a form which includes an additional formset so that the user can also submit data for a foreign key relation at the same time.
The template always displays a form for the original model and one form for the second model.
I now want to submit the two forms without filling in anything in the second form.
On the first submission the seond form does not validate and the page is redisplayed, but on the second submission the second form is valid! Even so the POST data is identical.
How can this be possible?
Or maybe I am doing this completely wrong, how can you discern if the user did not fill in anything in the formset or if he filled in something invalid?
Here the models:
class Software(models.Model):
creation_date = models.DateTimeField(default=datetime.now)
creator = models.ForeignKey(User)
version = models.CharField(max_length=300, unique=True, editable=False)
major_version = models.IntegerField()
minor_version = models.IntegerField()
[...]
def save(self, **kwargs):
"""
This updates the version string to the combined representation.
"""
self.version = Software.combine_version_string (self.major_version, self.minor_version)
super(Software, self).save(**kwargs)
class SoftwarePatch(models.Model):
file = models.FileField(upload_to='software_patches')
file_name = models.CharField(max_length=255, editable=False)
file_date = models.DateTimeField(default=datetime.now)
upload_date = models.DateTimeField(default=datetime.now)
software = models.ForeignKey('Software', related_name='patches')
firmware_patch = models.BooleanField(default=True)
target_path = models.CharField(max_length=255, blank=True)
class Meta:
unique_together = ('software', 'file_name')
verbose_name_plural = "software patches"
def __unicode__(self):
return self.file_name
def clean(self):
if self.file and not self.file_name:
self.file_name = self.file.file.name
Here my forms:
SoftwarePatchFormSet = inlineformset_factory(Software,
SoftwarePatch,
extra=1)
class SoftwareForm(forms.ModelForm):
"""
A simple form for creating a new software.
"""
class Meta:
model = Software
And finally my view function:
def software_add(request, software_id=None):
if software_id == None:
software = Software()
else:
software = Software.objects.get(id=software_id)
if request.POST:
form = SoftwareForm(request.POST, instance=software)
if form.is_valid():
software = form.save(commit=False)
softwarepatch_formset = SoftwarePatchFormSet(request.POST, request.FILES, instance=software)
if softwarepatch_formset.is_valid():
software = form.save()
softwarepatch_formset.save()
# Redirect, in case of a popup close it
if request.POST.has_key("_popup"):
pk_value = software._get_pk_val()
return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
# escape() calls force_unicode.
(escape(pk_value), escape(software)))
if 'next' in request.POST:
return HttpResponseRedirect(request.POST['next'])
else:
return HttpResponseRedirect(reverse('index'))
else:
form = SoftwareForm(instance=software)
softwarepatch_formset = SoftwarePatchFormSet(instance=software)
is_popup = request.GET.has_key("_popup") or request.POST.has_key("_popup")
return render_to_response(
'main/software_edit.html',
{'form': form,
'softwarepatch_formset': softwarepatch_formset,
'add': True,
'is_popup': is_popup,
},
context_instance = RequestContext(request)
)
First of all, you should set the instance argument only when creating a form / formset for an existing object i.e. one already in the DB. So for example if software_id = None and it's a GET request, you should only do form = SoftwareForm().
Also, after doing software = form.save(commit=False), you should do software.save() instead of software = form.save(). [I don't think it's really a problem though, just that you're redoing a save]. Remember that if you have a ManyToManyField in the Software model, you need to do form.save_m2m() after software = form.save() as well.
Here's what I think you should have:
def software_add(request, software_id=None):
if request.POST:
if software_id:
software = Software.objects.get(id=software_id)
form = SoftwareForm(request.POST, instance=software)
else:
form = SoftwareForm(request.POST)
if form.is_valid():
software = form.save(commit=False)
softwarepatch_formset = SoftwarePatchFormSet(request.POST, request.FILES, instance=software)
if softwarepatch_formset.is_valid():
software.save()
softwarepatch_formset.save()
# Redirect, in case of a popup close it
if request.POST.has_key("_popup"):
pk_value = software._get_pk_val()
return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
# escape() calls force_unicode.
(escape(pk_value), escape(software)))
if 'next' in request.POST:
return HttpResponseRedirect(request.POST['next'])
else:
return HttpResponseRedirect(reverse('index'))
else:
softwarepatch_formset = SoftwarePatchFormSet(request.POST, request.FILES)
else:
if software_id:
software = Software.objects.get(id=software_id)
form = SoftwareForm(instance=software)
softwarepatch_formset = SoftwarePatchFormSet(instance=software)
else:
form = SoftwareForm()
softwarepatch_formset = SoftwarePatchFormSet()
is_popup = request.GET.has_key("_popup") or request.POST.has_key("_popup")
return render_to_response(
'main/software_edit.html',
{'form': form,
'softwarepatch_formset': softwarepatch_formset,
'add': True,
'is_popup': is_popup,
},
context_instance = RequestContext(request)
)
Ok I finally found my problem!
I have the following model field: file_date = models.DateTimeField(default=datetime.now)
This sets the innital-file-date to a value like this: u'2011-10-18 08:14:30.242000'
After being rendered through the html widget the value will be: u'2011-10-18 08:14:30'
So django will think the form was changed and therefore not save.
On the second load django will automatically set the truncated value as initial-file-date and then nothing is changed and the save works as expected.
So now I only have to figure out what to use instead of datetime.now. I will update this post when I have figured it out.