Use a different form based on variable - django

I have a "product" field that I want to use to determine which form to display. I am trying to do this in the view but wondering if I should do it in the template instead. I have tried the following but "form" does not get assigned by my if statements. What am I doing wrong?
#login_required
def update_message(request, pk):
message = get_object_or_404(Submission, pk=pk)
author = message.author
date_posted = message.date_posted
product = message.product
message_obj = Submission.objects.get(pk=pk)
program_type = message.program_type
if author == request.user:
if request.method == 'POST':
if product == 'Apple':
form = AppleForm(request.user, request.POST, instance=message)
if product == 'Orange':
form = OrangeForm(request.user, request.POST, instance=message)
if form.is_valid():
message_sub = form.save(commit=False)
message_sub.author = request.user
message_sub.date_posted = timezone.now()
message_sub.save()
form.save_m2m()
messages.success(request, 'Message updated')
return redirect('submission-list')
else:
if product == 'Apple':
form = AppleForm(request.user, instance=message)
if product == 'Orange':
form = OrangeForm(request.user, instance=message)
else:
messages.warning(request, 'You can't do that.')
return redirect('message-submission-list')
return render(request, 'programs/submission_create_form.html', {'product':product,'form': form, 'message_obj': message_obj,'program_type':program_type})
class MessageSubmission(models.Model):
message = models.CharField(max_length=5000)
author = models.ForeignKey(User, on_delete=models.CASCADE)
date_posted = models.DateTimeField(default=timezone.now)
program_code = models.ManyToManyField(Program)
program_type = models.CharField(max_length=30, blank=True)
product = models.ForeignKey('Product', on_delete=models.SET_NULL, null=True)
production_cycle = models.ManyToManyField('ProductionCycle', null=True)
def get_absolute_url(self):
return reverse('submission-list')
def __str__(self):
return self.message

As I mentioned in the comment, the issue is that product is a ForeignKey to another model. In the template, the FK will display using the __str__ method of that model, but that doesn't make it equal to that display value. You should compare explicitly with the relevant field on the target model:
if product.fruit_type == 'Orange' # or whatever the field is
(Alternatively you could do if str(product) == 'Orange' but that's more brittle and is coupling display logic in a way that's not very nice.)

There's nothing wrong with doing this in the views. If the form is not defined after those if statements then it means that the value of product is not Apple or Orange, but something else. I would double check the value of product to fix the issue.
Since Product is a class, you should reference a field. You didn't post the code for it, but for example
if form == product.name
If there is a name field.

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.

With Django, how do I reference an existing model in a form save method instead of creating a new instance?

I'm trying to use a ModelChoiceField to display options populated from model, and when a user selects a choice, store that method in a different model.
I'm using a standard form instead of a ModelForm, because I wasn't able to get the form to display how I wanted to when using a Modelform.
My issue is that in my form save method, a new instance is created, which is not what I want.
Here are the relevant models:
class Client(models.Model):
client_email = models.EmailField(max_length = 254)
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
phone = PhoneField(blank=True)
assigned_manager = models.ForeignKey(Manager, on_delete=models.CASCADE, blank=True, null=True)
created_date = models.DateTimeField(default=timezone.now)
#property
def full_name(self):
return '{0} {1}'.format(self.first_name, self.last_name)
class Manager(models.Model):
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
manager_email = models.EmailField(max_length = 254)
username = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True)
#property
def full_name(self):
return '{0} {1}'.format(self.first_name, self.last_name)
My view:
def manageclient(request, urlid):
client = Client.objects.get(id=urlid)
form = AssignManagerForm()
if request.method == "POST":
form = AssignManagerForm(request.POST)
if form.is_valid():
form.save()
return render(request, 'mysite/manageclient.html', {})
else:
form = AssignManagerForm()
context = {
'client': client,
'urlid': urlid,
'form': form,
}
return render(request, 'mysite/manageclient.html', context)
And my forms.py
class AssignManagerForm(forms.Form):
full_name = forms.ModelChoiceField(queryset=Manager.objects.all())
def save(self):
data = self.cleaned_data
client = Client(assigned_manager=data['full_name'])
client.save()
What I need to do is pass the urlid in my view to my save method in my forms.py, but I am unsure how to do that. Even if i could do that, I'm not sure how to modify form save to use urlid to refer to a specific record and set only the assigned_manager record.
Additionally, while I want the meta field to be used to display the form, I know it isn't what should be being passed to the assigned_manager field. How would I pass a Manager of instance to establish the foreign key relationship?
edit: edited to correct queryset in forms.py as per comments
Here is a solution using a ModelForm, by using a ModelForm you no longer have to manually set attributes on save or provide initial values when updating an existing instance.
The field assigned_manager will still be named assigned_manager but it's label can be overridden to be whatever you want it to be by passing labels in the ModelForm.Meta
class AssignManagerForm(forms.ModelForm):
class Meta:
model = Client
fields = ['assigned_manager']
labels = {'assigned_manager': 'Full name'}
def manageclient(request, urlid):
client = Client.objects.get(id=urlid)
if request.method == "POST":
form = AssignManagerForm(request.POST, instance=client)
if form.is_valid():
client = form.save()
# The general convention is to redirect after a successful POST
else:
form = AssignManagerForm(instance=client)
context = {
'client': client,
'urlid': urlid,
'form': form,
}
return render(request, 'mysite/manageclient.html', context)
Instead of saving it in form, you can directly do this operation in view. For example:
def manageclient(request, urlid):
client = Client.objects.get(id=urlid)
if request.method == "POST":
form = AssignManagerForm(request.POST)
if form.is_valid():
client.assigned_manager = form.cleaned_data['full_name']
client.save()
return render(request, 'mysite/manageclient.html', {})

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})

ManyToManyField is not saving in DB

I have searched the existing answers on this platform related to this but it isn't helping me.
I save the Form but it is not saved in the DB.
views.py:
def student_list(request, pk) :
course = get_object_or_404(Course, pk=pk)
teacher = Instructor.objects.get(course_name=course)
attendance = Attendance.objects.create(course_name=course, instructor_name=teacher)
if request.method == "POST" :
form = PostForm(request.POST, instance=attendance)
if form.is_valid() :
attendance = form.save(commit=False)
attendance.published_date = timezone.now()
attendance.save()
return redirect('post_list')
else :
form = PostForm(instance=attendance)
return render(request, 'blog/student_list.html', {'course' : course, 'form' : form})
forms.py:
class PostForm(forms.ModelForm) :
class Meta :
model = Attendance
fields = ('student_name',)
def __init__(self, *args, **kwargs) :
super().__init__(*args, **kwargs)
if self.instance.pk :
self.fields['student_name'].queryset = Student.objects.filter(course_name=self.instance.course_name)
models.py:
class Attendance(models.Model) :
course_name = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True)
student_name = models.ManyToManyField(Student)
instructor_name = models.ForeignKey(Instructor, on_delete=models.SET_NULL, null=True)
published_date = models.DateTimeField(blank=True, null=True)
student_list.html:
<form method="POST" class="post-form">{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default">Save</button>
</form>
Also, the fields course_name, instructor_name and published_date are saved in the DB. It is only the student_name that is not saved.
Well this is one of the reasons why using .save(commit=False) is indeed not a good idea, because a ModelForm will indeed save the ManyToManyField. This is done after the object itself is saved, since it first needs a primary key.
You can alter the instance wrapped in the form with:
def student_list(request, pk):
course = get_object_or_404(Course, pk=pk)
teacher = Instructor.objects.get(course_name=course)
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
form.instance.course_name = course
form.instance.instructor_name = teacher
form.instance.published_date = timezone.now()
form.save()
return redirect('post_list')
else:
form = PostForm()
return render(request, 'blog/student_list.html', {'course': course, 'form': form})
That being said, if this is the standard behavior, you can just pass the timezone.now as default in your model:
from django.utils import timezone
class Attendance(models.Model):
course_name = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True)
student_name = models.ManyToManyField(Student)
instructor_name = models.ForeignKey(Instructor, on_delete=models.SET_NULL, null=True)
published_date = models.DateTimeField(blank=True, null=True, default=timezone.now)
or even better, you can just use auto_now_add=True [Django-doc]:
class Attendance(models.Model):
course_name = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True)
student_name = models.ManyToManyField(Student)
instructor_name = models.ForeignKey(Instructor, on_delete=models.SET_NULL, null=True)
published_date = models.DateTimeField(auto_now_add=True)
Note: A ForeignKey does not store the string representation (or name) of the
referenced object in the column, it stores the primary key of the record it
references in a column with an _id suffix to a ForeignKey field. Therefore
ForeignKeys usually do not end with a _name suffix. You might want to
consider renaming the course_name field to course.
Note: A GET request is not supposed to have side-effects, hence constructing
objects when a user makes a GET request, is not compliant with the HTTP
standard. Therefore it might be better to remove the creation of the Attendance
object at the database side.
EDIT: You can pass the course object to the PostForm to filter the queryset. For example:
class PostForm(forms.ModelForm) :
class Meta :
model = Attendance
fields = ('student_name',)
def __init__(self, *args, course=None, **kwargs) :
super().__init__(*args, **kwargs)
if self.instance.pk and not course:
course = self.instance.course_name
if course:
self.fields['student_name'].queryset = Student.objects.filter(course_name=course)
Then in the view, you can pass the Course object:
def student_list(request, pk) :
course = get_object_or_404(Course, pk=pk)
teacher = Instructor.objects.get(course_name=course)
if request.method == 'POST':
form = PostForm(request.POST, course=course)
if form.is_valid() :
form.instance.course_name = course
form.instance.instructor_name = teacher
form.instance.published_date = timezone.now()
form.save()
return redirect('post_list')
else :
form = PostForm(course=course)
return render(request, 'blog/student_list.html', {'course' : course, 'form' : form})

How to input a model instance in a form field

I have a simple model with 2 classes:
class Company(models.Model):
company_name = models.CharField(default='', max_length=128, blank=True, null=True)
class Visitor(models.Model):
visitor_company = models.ForeignKey(Company)
visitor_name = models.CharField(default='', max_length=128, blank=False, null=False)
I also have a simple form:
class VisitorForm(forms.ModelForm):
visitor_company = forms.CharField()
class Meta:
model = Visitor
fields = "__all__"
And here is the view.py code:
def home(request):
form = Visitor()
if request.method == "POST":
form = Visitor(request.POST)
if form.is_valid():
obj, created = Visitor.objects.get_or_create(**form.cleaned_data)
if created:
messages.add_message(request, messages.SUCCESS, 'Visitor added.')
else:
messages.add_message(request, messages.INFO, 'Visitor exists : %s' % obj.visitor_name)
return redirect('visitors')
context = { 'form': form }
return render(request, "visitors/home.html", context)
I have set visitor_company as a CharField as I want to use Typeahead for users to specify the ForeignKey, rather than Django's built in dropdown (which would appear if I did not set the input type).
However, when I use this method, even if I input a valid company_name in the visitor_company field, I get Cannot assign "XXX": "Visitor.visitor_company" must be a "Company" instance.
How do I input a Company instance? Is it also possible to use get_or_create on a ForeignKey like this if the Company record doesn't exist?
This is untested code, so consider this a starting point, no real solution:
forms.py
class VisitorForm(forms.ModelForm):
visitor_company = forms.CharField()
def clean_visitor_company(self):
vc = self.cleanded_data['visitor_company']
try:
vc_object = Company.objects.get(company_name=vc)
except Company.DoesNotExist:
vc_object = Company.objects.create(company_name=vc)
return vc_object
class Meta:
model = Visitor
fields = "__all__"
views.py
def home(request):
form = VisitorForm(request.POST or None)
if form.is_valid():
form.save()
return redirect('visitors')
return render(request, "visitors/home.html", { 'form': form })