Manually set model fields in ModelForm - python

I have a model with a foreign key and a unique constraint as follows:
class Menu(models.Model):
tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE)
name = models.CharField(max_length=128)
date_menu = models.DateField()
class Meta:
constraints = [
models.UniqueConstraint(fields=['tournament', 'name', 'date_menu'], name="unique_name_menu")
]
I would like to create a form to add instance of Menu. However the value of tournament is set by the URL of the page. I do not want the user to be able to set it.
For this I use a modelForm, excluding the tournament field :
class MenuForm(forms.ModelForm):
date_menu = forms.DateField(initial=datetime.datetime.now())
class Meta:
model = Menu
exclude = ['tournament']
Here is my view :
def add_menu(request, tournament_slug):
tournament = get_object_or_404(Tournament, slug=tournament_slug)
form = MenuForm(request.POST or None)
if form.is_valid():
menu_id = form.save(commit=False)
menu_id.tournament = Tournament.objects.get(pk=1)
menu_id.save() # I get the integrity error only here
return HttpResponseRedirect(reverse('admin'))
return render(request, "view.html", {'form': form, 'formset': formset, "tournament": tournament})
My problem is that when I call the .is_valid() function on this form the uniqueness condition cannot be checked as the tournament field is not set. As a result I get an integrity error when calling the save function in the view.
The question is : how can link the Menu instance created by the form to add the tournament field before checking if it's valid? If it's not the right way of doing it, how can I check the uniqueness of the model instance and return the corresponding errors to the template when needed?
I tried including the tournament field as hidden field in the view, it works but I don't know if that's the best way of doing it...

You should simply instantiate the form with an unsaved instance of Menu so your view should be like:
def add_menu(request, tournament_slug):
tournament = get_object_or_404(Tournament, slug=tournament_slug)
if request.method == 'POST':
form = MenuForm(request.POST, instance=Menu(tournament=tournament))
if form.is_valid():
menu_id = form.save()
return HttpResponseRedirect(reverse('admin'))
else:
form = MenuForm(instance=Menu(tournament=tournament))
return render(request, "view.html", {'form': form, "tournament": tournament})
Also the form calls _get_validation_exclusions() and excludes fields not present in the form from validation. You can try to override validate_unique to overcome this:
class MenuForm(forms.ModelForm):
date_menu = forms.DateField(initial=datetime.datetime.now())
class Meta:
model = Menu
exclude = ['tournament']
def validate_unique(self):
exclude = self._get_validation_exclusions()
if 'tournament' in exclude:
exclude.remove('tournament') # Make sure `tournament` gets validated
try:
self.instance.validate_unique(exclude=exclude)
except ValidationError as e:
self._update_errors(e)
Note: I changed your view structure to avoid using MenuForm(request.POST or None) which is an antipattern. (Forms
can be valid even if nothing is sent in the POST data, with the way
you write such forms would be considered invalid).
Edit: As discussed in the comments perhaps the option of a hidden and disabled field is much better than overriding the forms validate_unique method:
class MenuForm(forms.ModelForm):
tournament = forms.ModelChoiceField(
queryset=Tournament.objects.all(),
widget=forms.HiddenInput(),
disabled=True
)
date_menu = forms.DateField(initial=datetime.datetime.now())
class Meta:
model = Menu
fields = ['tournament', 'name', 'date_menu']

Related

Setting an initial value in a Django Form

I have to setup an initial value in a form and somehow is not working, it is extremely strange as I have exactly the same code in another view, but in this case my approach is not working:
views.py
#login_required
def add_lead(request):
if request.method == 'POST':
lead_form = LeadsForm(request.POST)
if lead_form.is_valid():
lead_form.save()
messages.success(request, 'You have successfully added a new lead')
return HttpResponseRedirect(reverse('add_lead'))
else:
messages.error(request, 'Error updating your Form')
else:
user = {"agent":request.user}
lead_form = LeadsForm(request.POST or None, initial = user)
return render(request,
'account/add_lead.html',
{'lead_form': lead_form}
)
forms.py
class LeadsForm(forms.ModelForm):
class Meta:
model = Leads
fields = ('project_id','company','agent','point_of_contact','services','expected_licenses',
'expected_revenue','country', 'status', 'estimated_closing_date'
)
widgets = {'estimated_closing_date': DateInput(),
}
Essentially, the agent is the logged user, so I'm passing request.user as a variable, but I have not succeeded, which is very strange because I have that same logic in another form
Any help will be appreciated
If you want to make a form with a foreign key you can use ModelChoiceField. In your case you can use:
class LeadsForm(forms.ModelForm):
agent = forms.ModelChoiceField(queryset=User.objects.all())
class Meta:
model = Leads
fields = ('project_id','company','agent','point_of_contact','services','expected_licenses',
'expected_revenue','country', 'status', 'estimated_closing_date'
)
widgets = {'estimated_closing_date': DateInput(),
}
Then you can assign data with user_id in your form initial.

How to redirect from FormView to a ListView after form validation

In my Django application, I would like to display a form where a user enters a name to search for Persons from an external data source (not model). I am using class-based generic views and have a working application (code attached below) with a minor inconvenience - I would like to see if there is a better way to do this.
First, here's how I have done it:
I have a Form with 3 fields (first, second and last name) and a clean() where I check if at least one field is populated
A FormView which renders this form, and form_valid() method which does nothing at the moment (reasons will become clear shortly)
An entry in urls.py to render this view and display the form in a template. The form is being submitted to a ListView with GET, and not the FormView itself (sad!)
A ListView where I define get_queryset(self) because data comes from an external source, not a Model; and I use self.request.GET.get('first_name', '') etc. to retrieve query string values, make a connection to the external data source and get a Pandas dataframe which I convert to a list of records and render, paginated, in the template.
An entry in urls.py for the List View and render the template of search results.
Everything works but, hopefully, the problem is apparent. The FormView is being used only to display the form, but the form submits to the ListView where data is retrieved to be displayed. This means that my form_valid() in the FormView and consequently clean() from the form aren't even used - I can work around it by using Javascript based validation but I would like to be able to use the FormView to its full potential.
So, how do I redirect to ListView with the form input, after form validation?
Here's my simple, and working, code:
urls.py
...
path('search/name', form_views.NameSearchView.as_view(), name='search-name'),
path('search/results', list_views.SearchResultsList.as_view(), name='search-results'),
...
forms.py
class NameSearchForm(forms.Form):
last_name = forms.CharField(label='Last Name', required=False)
first_name = forms.CharField(label='First Name', required=False)
second_name = forms.CharField(label='Second Name', required=False)
def clean(self):
cleaned_data = super().clean()
first_name = cleaned_data['first_name'].strip()
second_name = cleaned_data['second_name'].strip()
last_name = cleaned_data['last_name'].strip()
# At least one field should be filled to search
if not (first_name or second_name or last_name):
raise ValidationError(
_('Fill in a name to search!'),
code='invalid',
params={
'first_name': first_name,
'second_name': second_name,
'last_name': last_name,
})
form_views.py
class NameSearchView(FormView):
template_name = 'app/search_name.html'
form_class = NameSearchForm
# This is clearly wrong as it does nothing
success_url = '.'
def form_valid(self, form):
self.form = form
return HttpResponseRedirect(self.get_success_url())
list_views.py
class SearchResultsList(ListView):
template_name = 'app/search_results.html'
paginate_by = 10
def get_queryset(self):
first_name = self.request.GET.get('first_name', '').strip()
second_name = self.request.GET.get('second_name', '').strip()
last_name = self.request.GET.get('last_name', '').strip()
conn = create_connection() # This abstraction creates a database connection
query = '''
SELECT i.first_name first_name,
i.second_name second_name,
i.last_name last_name,
i.info
FROM db.person_tbl i
WHERE i.name_type = 'primary'
'''
params = []
terms = [first_name, second_name, last_name]
term_keys = ['first', 'sec', 'last']
for i, term in enumerate(terms):
if term:
query += f' AND i.srch_{term_keys[i]}_name = ?'
params.append(term)
# this abstraction gets the pandas dataframe
object_list = conn.get_data(query, params=params)\
.sort_values(by='last_name')\
.to_dict('records')
return object_list
I would like to be able to submit POST to the FormView itself, validate the form and then display the search results, paginated, through a ListView.
Any help would be greatly appreciated!

Django using a modelform to update an instance of model

I have the following model in Django which I use to store data about medicines.
class Medicine(models.Model):
Medicine_Name = models.CharField(max_length=100)
User_Associated = models.ForeignKey(User, on_delete=models.CASCADE)
Tablets_In_Box = models.IntegerField()
Dose_in_mg = models.IntegerField()
Dose_Tablets = models.IntegerField()
Number_Of_Boxes = models.IntegerField()
Last_Collected = models.DateField()
def __str__(self):
return self.Medicine_Name
def get_absolute_url(self):
return reverse('tracker-home')
I am trying to create a model form where a user can update the last collection of one of their medicines. Here is what I began with.
class CollectionForm(forms.ModelForm):
class Meta:
model = Medicine
fields = ['Medicine_Name', 'Number_Of_Boxes', 'Last_Collected']
I do not understand how I can call an instance of my model based on the 'Medicine_Name' from the field. In other words, I need the user to be able to select the correct medicine from a dropdown menu, and then the form must update the 'Last_Collected', and 'Numer_Of_Boxes' fields on my Medicine model.
https://docs.djangoproject.com/en/2.1/topics/forms/modelforms/#the-save-method
It seems this contains relevant information, but I struggle to see how to use it in this instance. How can I correctly get the instance of the medicine form I need, based on the user input in the form? Furthermore how can I use the save method in my views to make sure the database gets updated correctly?
EDIT Added view for the form:
def update(request, pk):
instance = Medicine.objects.get(id=pk)
if request.method == 'POST':
form = CollectionForm(user=request.user, instance=instance, data=request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.User_Associated = request.user
instance.save()
else:
form = CollectionForm()
context = {'form': form}
return render(request, 'tracker/medicine_collection.html', context )
**EDIT
views:
def update(request, pk):
instance = Medicine.objects.get(id=pk)
if request.method == 'POST':
form = CollectionForm(instance=instance, data=request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.User_Associated = request.user
instance.save()
return redirect ('/')
....
This is based on updating the instance of the specific user. This tutorial helpt me achieve the same thing.
https://youtu.be/EX6Tt-ZW0so
Tried a different approach (class based views - UpdateView) I just learned here on SO. Did not test it but I think its a step in the right direction.
class UpdateMedicine(LoginRequiredMixin, UpdateView):
model = Medicine #call the model you need to update
fields = ['Medicine_Name', 'Number_Of_Boxes', 'Last_Collected'] #specify the fields you need to update
template_name_suffix = 'medicine_update_form' #specify the template where the update form is living
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
user=self.request.user, #get the current logged in user
instance=get_object_or_404(Medicine, pk=self.kwargs['pk']) #get the pk of the instance
)
return context
def form_valid(self, form):
form.instance.medicine = get_object_or_404(Medicine, slug=self.kwargs['pk'])
return super().form_valid(form) #saves the updates to the instance
def get_success_url(self):
return reverse('medicine-collection') #name of the url where your 'tracker/medicine_collection.html is living
Link the appropriate templates and urls to the above example and try some things yourself.
Link to the django docs:
https://docs.djangoproject.com/en/3.0/ref/class-based-views/generic-editing/
Good luck!

Django ModelForm not displaying all fields

I'm trying to build forms linked to a PostgreSQL database using Django ModelForms. The template is rendering two of the fields(the ones with ManyToMany relationships), but it only gives me an empty box for "title".
This is my forms.py:
Forms.py:
class ProgramForm(forms.ModelForm):
class Meta:
model = Program
fields = ['function','task', 'title']
widgets = {
'function' : forms.Select,
'task' : forms.Select,
'title' : forms.Select,
}
This is my Models.py:
class Program(models.Model):
title = models.CharField(max_length=255)
function = models.ManyToManyField(function, related_name='programs')
task = models.ManyToManyField(Task, related_name='programs')
def __unicode__(self):
return self.title
class Task(models.Model):
tasknum = models.CharField(max_length=20)
taskname = models.CharField(max_length=100)
task_num_name = models.CharField(max_length=100)
function = models.ForeignKey(Function, related_name="tasks")
def __unicode__(self):
return self.task_num_name
class Function(models.Model):
function = models.CharField(max_length=50)
function_abrev = models.CharField(max_length = 25)
def __unicode__(self):
return self.function
Views.py:
def main(request):
return render (request, 'assignments/main.html')
def add_program(request):
form = ProgramForm()
return render (request, 'assignments/ad_form.html', {"form":form})
def link(request):
if request.method == 'POST':
form = ProgramForm(request.POST)
if form.is_valid():
return HttpResponse("we maybe getting somewhere")
else:
return HttpResponse("keep working")
I need a couple of things to happen:
I need for the "title" to render in the html page as a scroll down(the same way "function" and "task" appear.
I need to be able to save the relationships. The models are populated with all the information required with the exception of the relationships. The objective is for a staff member to be able to chose a "function", for that choice to act as a filter for the "task" scroll down(function and task have a OneToMany), and then allow them to choose any programs they want to add to their portfolio.
Any help will be much appreciated.
1. Title field in form
For this, I don't quite understand how the title field could be a scroll down the same way function and task are. Function and task are drop downs because they are manytomany fields linked to other models, meaning that the user has to pick which other objects in the Functions model and the Tasks model are to be linked. The title field, on the other hand, is just a CharField and so there is no defined set of things for the user to pick from. To allow the user to enter in the title for the Program, you should change the widget for title to Textarea() as such:
forms.py
from django.forms import ModelForm, Textarea
class ProgramForm(forms.ModelForm):
class Meta:
model = Program
fields = ['function','task', 'title']
widgets = {
'function' : forms.Select,
'task' : forms.Select,
'title' : Textarea(),
}
2. Save the Program from the form
To save the Program created by the user on staff member, simply add form.save() to your link(request) function:
views.py
def link(request):
if request.method == 'POST':
form = ProgramForm(request.POST)
if form.is_valid():
form.save()
return HttpResponse("we maybe getting somewhere")
else:
return HttpResponse("keep working")
Hope this helps!
I was able to do a query from views.py and pass if to the template.
Views.py
def function_page(request, Function_id):
assignments = Function.objects.get(id=Function_id)
programs = assignments.programs.all()
context = {
'assignments': assignments,
'programs' : programs
}
return render (request, 'function.html', context)
HTML
{% for program in programs %}
<option value="{{program.title}}">{{program.title}}</option>
{% endfor %}

submitting 2 forms together, passing primary key from one as foreign key in other

I have created a form using python and django from 2 seperate modelForms in the one html template. Models:
class Action(models.Model):
name = models.CharField("Action name", max_length=50)
keywords = models.CharField("Keywords", max_length=50)
object = models.CharField("Object", max_length=50, blank=True, null=True)
uploadDate = models.DateField("Date", default=get_current_date)
UploadedBy = models.CharField("UploadedBy", max_length=50, default="")
class Image(models.Model):
image = models.FileField(upload_to=get_upload_file_name, default="")
action = models.ForeignKey(Action)
def get_upload_file_name(instance, filename):
return "uploaded_files/%s_%s" % (str(datetime.now().day).replace('.','_'), filename)
forms:
class ActionForm(ModelForm):
#bind form to Action model
class Meta:
model = Action
fields = ['name','keywords', 'object', 'UploadedBy', 'uploadDate']
class ImageForm(ModelForm):
class Meta:
model= Image
fields =['image']
The code which creates the form in views:
def actioncreate(request):
if request.method == "GET":
#create the object - Actionform
form = ActionForm;
form2 = ImageForm;
#pass into it
return render(request,'app/createForm.html', { 'form':form, 'form2':form2})
elif request.method == "POST":
# take all of the user data entered to create a new action instance in the table
form = ActionForm(request.POST, request.FILES)
form2 = ImageForm(request.POST, request.FILES)
if form.is_valid() and form2.is_valid():
act = form.save(commit=False)
img = form2.save(commit=False)
#set the action_id Foreignkey
act.id = img.action_id
act.save()
img.save()
return HttpResponseRedirect('/actions')
else:
form = ActionForm()
form2 = ImageForm;
return render(request,'app/createForm.html', { 'form':form, 'form2':form2 })
The form is created fine but when it is submitted, it trys to save image.id, image.image (filename) and returns null for image.action_id
I am getting the error:
null value in column "action_id" violates not-null constraint
DETAIL: Failing row contains (2, uploaded_files/29_personrunning_Hq8IAQi.jpg, null).
I obviously need to populate the third column with the action.id which django creates itself on submitting the first part 'form'. Is there a way I can get the action.id value and populate the action_id field in the image table in the one form submission?
image.action_id is declared initially as a foreignKey related to action in models.
The first problem is related to act = form.save(commit=False) because it will return an object that hasn’t yet been saved to the database, then act doesn't have an ID. You need to save (and commit) act first.
Also there is another error in following line:
act.id = img.action_id # It should be: img.action_id = act.id
You may want to assign act to img.action. Please note that you are doing it in the wrong way (you are assigning in img.action to act). The best way to do it is:
img.action = act # This is equivalent to img.action_id = act.id
Try swapping these lines:
act.save()
img.action = act

Categories