Let user add field to Django form - python

I am building a form that allows employees to enter the city, state of trip legs and calculate their mileage reimbursement automatically. Right now I have the following code in forms.py:
leg1a = forms.CharField(max_length=20, required=False)
leg1b = forms.CharField(max_length=20, required=False)
leg2a = forms.CharField(max_length=20, required=False)
leg2b = forms.CharField(max_length=20, required=False)
leg3a = forms.CharField(max_length=20, required=False)
leg3b = forms.CharField(max_length=20, required=False)
leg4a = forms.CharField(max_length=20, required=False)
leg4b = forms.CharField(max_length=20, required=False)
leg5a = forms.CharField(max_length=20, required=False)
leg5b = forms.CharField(max_length=20, required=False)
leg6a = forms.CharField(max_length=20, required=False)
leg6b = forms.CharField(max_length=20, required=False)
I then use a separate script to tally the miles in each leg and multiply the total by our mileage rate. It works, but it is ugly on the form; The form has six pairs of fields that are all optional. I would like to have 1 pair to start, and an option the user can click to add another leg.
I need to keep the data in tuple form, like so: ('city, ST','city2, ST') for the distance calculation function.
Here is the code I currently use in my form views for cleaning and sending the data to the mileage handler:
Mileage distance calculation
leg_list = [(leg1a, leg1b), (leg2a, leg2b), (leg3a, leg3b), (leg4a, leg4b), (leg5a, leg5b), (leg6a, leg6b)]
cleaned_leg_list = []
#get rid of empty variables
for leg in leg_list:
if leg == ('',''):
pass
else:
cleaned_leg_list.append(leg)
leg_distance = []
#sends each leg of mileage claim to distance handler
for leg in cleaned_leg_list:
pre_leg = main(leg)
leg_distance.append(pre_leg)
#sums legs of mileage claim
sum_distance = sum(leg_distance)

Use one form and a formset. A formset handles dealing with multiple instances of one form.
https://docs.djangoproject.com/en/dev/topics/forms/formsets/
Optionally, you can look into cloning formsets via javascript to dynamically add as many areas as you want without reloading the view.
This is also a great opportunity to clean up your code - put a method on the form definition that calculates leg distance and calls your external script. Your views can do the summation in a few lines of code!
You can even build the formset class yourself and add the summation function there, so your view is as simple as: if formset.is_valid(): print formset.sum_leg_distances()
class MyForm(forms.Form):
start_city = forms.CharField()
start_state = forms.CharField()
end_city = forms.CharField()
end_state = forms.CharField()
MyFormSet = formset_factory(form=MyForm, extra=6)
def my_view(request):
formset = MyFormSet(request.POST or None)
if request.POST:
if formset.is_valid():
for form in formset.forms:
form.cleaned_data['start_city'] # here's your leg data.
# form.calculate_leg_distance()
# sum([form.calculat_leg_distance() for form in formset])
return render(request, 'my_template', {'formset': formset})
<form method="post">
{{ formset.as_p }}
{{ formset.management_form }}
<input type="submit" />
</form>

Related

Django edit form won't validate on form.save() with user field

I recently added a "user" field to my Game model. I can create a new game that works fine; it's when I want to allow a user to edit the instance of a game where I am running into problems. My view is calling the form = GameForm(request.POST, instance=game) where game = Game.objects.get(pk=id). The form is pre-populated with the correct data, but when it's submitted, whether there are updates or not, the form is not validating. It sees it as a POST, but cannot get inside the if form.is_valid() conditional. And this is ever since I added the user field. I am using the default Django User model, and the field name is called "owner." It is set up as a ManyToManyField(User, blank=True) as users can own many games, and games can be owned by many users. Django forms the Many-To-Many "through" table, but I don't want the user to be able to change who owns what. I have it as a hidden field in my forms.py so a user can't change it.
Model
class Game(models.Model):
game_title = models.CharField(max_length=100,
verbose_name='Game Title',
db_column='game',
blank=False,
null=False,
unique=True)
game_developer = models.CharField(max_length=100,
verbose_name='Developer',
db_column='developer',
blank=True,
null=True)
game_release = models.DateField(max_length=50,
verbose_name='Release Date',
db_column='release_date',
blank=False,
null=True)
rating = models.IntegerField(verbose_name='Game Rating',
db_column='rating',
choices=INT_CHOICES,
blank=True,
null=True)
game_genre = models.CharField(max_length=100,
verbose_name='Genre',
db_column='genre',
blank=False,
null=True,
choices=GENRE_CHOICES)
game_platform = models.CharField(max_length=100,
verbose_name='Game Platform',
db_column='platform',
blank=True,
choices=PLATFORM_CHOICES)
game_esrb = models.CharField(max_length=100,
verbose_name='ESRB Rating',
db_column='esrb',
blank=False,
null=True)
owner = models.ManyToManyField(User, blank=True)
objects = models.Manager()
def __str__(self):
return self.game_title
class Meta:
db_table = 'tbl_games'
verbose_name = 'Game'
View
# Allows the user to update game information
def editGame(request, id):
# Finds the user selected game by game id
game = Game.objects.get(pk=id)
user = request.user.id
if request.method == 'POST':
print("Seen as POST")
# Create game instance pre-populated into a form
form = GameForm(request.POST, instance=game)
if form.is_valid():
print("Form is valid!")
# Saves the edits without saving to the dB
form.save()
messages.success(request, 'Game successfully updated!')
return redirect('library')
else:
print('Seen as GET')
form = GameForm(instance=game)
print("Page loaded")
context = {'form': form}
return render(request, 'library/editGame.html', context)
Form
class GameForm(ModelForm):
game_esrb = forms.CharField(required=False, widget=HiddenInput)
owner = forms.HiddenInput()
def __init__(self, *args, **kwargs):
super(GameForm, self).__init__(*args, **kwargs)
class Meta:
model = Game
fields = [
'game_title', 'game_developer', 'rating', 'game_release',
'game_genre', 'game_platform', 'game_esrb', 'owner'
]
widgets = {
'rating': Select(attrs={'choices': INT_CHOICES}),
'game_genre': Select(attrs={'choices': GENRE_CHOICES}),
'game_platform': Select(attrs={'choices': PLATFORM_CHOICES}),
'esrb_rating': HiddenInput(),
'owner': HiddenInput
}
help_texts = {
'rating':
'Key: 1 - Bad | 2 - Okay | 3 - Average | 4 - Good | 5 - Great'
}
Template
{% block appcontent %}
<div class="height">
<form method="POST">
{% csrf_token %}
{{ form|crispy }}
<div class="form-btn">
<a class=" btn btn-secondary cancel" type="button" href="{% url 'library' %}">Cancel</a>
<button class="update btn btn-primary" type="submit">Update</button>
</div>
</form>
</div>
{% endblock %}
First, remove 'owner' from the list assigned to fields in your GameForm class:
class Meta:
model = Game
fields = [
'game_title', 'game_developer', 'rating', 'game_release',
'game_genre', 'game_platform', 'game_esrb', # 'owner' removed
]
Here's why: inside your ModelForm, you only need to name the Game fields that you want to use as input fields in the form. In your case, you don't actually want the 'owner' field to be an input field, so just leave it out of this list. This will accomplish your stated goal of disallowing a user from modifying the owner field.
Now let's look at, and modify, your view:
You do this in your view: "user = request.user.id" This will assign an int to your attribute 'user'. I'm guessing you wanted a user object, not an int, but it's not clear, since you never actually use this attribute anywhere in your view.
Here's what needs to happen: Your view needs to connect the user object to the game object, then save the game object. We've already made sure that 'owner' doesn't appear as an input field in your form. Now, we need to manually provide a value for that field, then save the object. Below, we do this following the is_valid() call in your view:
def editGame(request, id):
# Finds the user selected game by game id
game = Game.objects.get(pk=id)
if request.method == 'POST':
print("Seen as POST")
# Create game instance pre-populated into a form
form = GameForm(request.POST, instance=game)
if form.is_valid():
print("Form is valid!")
form.save()
game.owner.add(request.user) # update the game's owner field, assigning the current user
game.save()
messages.success(request, 'Game successfully updated!')
return redirect('library')
To summarize and clarify: The end result of all this work is an updated Game object. The user updates values via the ModelForm. The view validates those changes, then saves the form, which saves the corresponding game object. Then, we manually assign the user to the 'owner' field of the game, because we intentionally left that field out of the form. Then we save the game again so this change is included in the 'final' state of the object.

Setting the field value of a form in view.py

My django website would like to allow logged in users to post a recipe. The recipe model is linked to the user model and as such requires a valid user instance. I would like the Recipe form to automatically assign the postedby field to the instance of the logged in user.
So far I have attempted to pass in a dictionary, storing the user's name, to the form instance. as shown in the view
However, the constructor method is not receiving the data and is rendering the form but with a failed attempt to submit.
I cannot store the data without postedby field having a valid instance as the model throws the following error:
Exception Value:UserProfile matching query does not exist.
I have also tried to do the following in views.py;
#login_required
def add_recipe(request):
form = RecipeForm()
form.fields['postedby'] = UserProfile.objects.get(user=User.objects.get(username=request.user.__str__()))
context_dict = {'form':form}
if request.method == 'POST':
...
However this overwrites the postedby form view to be rendered and raises and error.
views.py
#login_required
def add_recipe(request):
form = RecipeForm({'user':request.user})
context_dict = {}
#print(form.fields['postedby'].queryset)
if request.method == 'POST':
form = RecipeForm(request.POST)
if form.is_valid():
form.save(commit=True)
return redirect(reverse('spatula:index'))
else:
print(form.errors)
context_dict['form'] = form
return render(request, 'spatula/add_recipe.html', context=context_dict)
The RecipeForm is as follows:
class RecipeForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
print(kwargs)
super(RecipeForm, self).__init__(*args, **kwargs)
#input fields for recipe form
method = forms.CharField(max_length=512, widget=forms.Textarea(attrs={'placeholder':'Method'}))
name = forms.CharField(max_length=128, widget=forms.TextInput(attrs={'placeholder':'Recipe Name'}))
ingredients = forms.CharField(max_length=512, widget=forms.Textarea(attrs={'placeholder':'Ingredients'}))
category = NameChoiceField(widget=forms.Select(), queryset =Category.objects.all(), initial = 0)
toolsreq = forms.CharField(max_length=512, widget=forms.TextInput(attrs={'placeholder':'Tools Required'}))
difficulty = forms.IntegerField(widget=forms.NumberInput(attrs={'type':'range', 'step':'1', 'min':'1','max':'3'}), help_text = 'Difficulty: ')
cost = forms.IntegerField(widget=forms.NumberInput(attrs={'type':'range', 'step':'1', 'min':'1','max':'3'}), help_text = 'Cost: ')
diettype = forms.IntegerField(widget=forms.RadioSelect(choices=DIET_CHOICES))
# not required as its not stored in DB
#description = forms.CharField(widget=forms.Textarea(attrs={'placeholder':'Description'}))
#hidden fields
rating = forms.FloatField(widget=forms.HiddenInput(), initial=0, required=False)
slug = forms.SlugField(widget=forms.HiddenInput(),required=False)
postedby = forms.SlugField(widget=forms.HiddenInput(),required=False)
#Order in which inputs get rendered
field_order = ['name', 'category', 'toolsreq', 'difficulty', 'cost', 'diettype', 'ingredients', 'method']
class Meta:
model = Recipe
exclude = ('id',)
finally here is the recipe model:
class Recipe(models.Model):
DIET_CHOICES = (
(1,"Meat"),
(2,"Vegetarian"),
(3,"Vegan"),
)
DIFFICULTY_CHOICES = (
(1,1),
(2,2),
(3,3),
)
COST_CHOICES = (
(1,1),
(2,2),
(3,3),
)
name = models.CharField(max_length=NAME_MAX_LENGTH)
ingredients = models.TextField(max_length=MAX_TEXT_LENGTH)
toolsreq = models.TextField(max_length=MAX_TEXT_LENGTH)
method = models.TextField()
# make sure the form views for difficulty,
# cost and diettype datatypes restrict the
# users selection to the CHOICES above
difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES)
cost = models.PositiveSmallIntegerField(choices=COST_CHOICES)
diettype = models.PositiveSmallIntegerField(choices=DIET_CHOICES)
postedby = models.ForeignKey(UserProfile, on_delete=models.CASCADE, default=0)
# - Following fields are hidden when creating a new recipe
# ratings are out of 5, to 1 decimal place.
# - Need a script to update rating everytime
# a new rating for the recipe is posted.
rating = models.DecimalField(decimal_places=1,max_digits=3, default=0)
category = models.ForeignKey(Category,to_field="name", on_delete=models.CASCADE)
# recipes rating is calculated when the recipe is requested, no value to be stored
def __str__(self):
return self.name
# used for recipe mappings
def save(self,*args, **kwargs):
self.slug = slugify(str(self.name)+str(self.postedby))
super(Recipe,self).save(*args, **kwargs)
Since you don't want your user go edit this field, remove it entirely from the form:
exclude = ['id', 'postedby']
Then in your view, set the value on the instance before saving:
# ...
if request.method == 'POST':
form = RecipeForm(request.POST)
if form.is_valid():
recipe = form.save(commit=False)
recipe.postedby = UserProfile.objects.get(user=request.user)
recipe.save()
return redirect(reverse('spatula:index'))
# ...
The error says: Exception Value:UserProfile matching query does not exist. It means there is no UserProfile object for that.
You probably want:
try:
profile = UserProfile.objects.get(user=request.user)
except UserProfile.DoesNotExist
# your logic
Or if you want to automatically create UserProfile object for requested user. You can use get_or_create:
p, created = UserProfile.objects.get_or_create(user=request.user)
Explanation: Any keyword arguments passed to get_or_create() — except an optional one called defaults — will be used in a get() call. If an object is found, get_or_create() returns a tuple of that object and False.

Automatically filled form in class-based view

I want to automatically fill some form in class-based view and save in database.
post function in my view
def post(self, request, *args, **kwargs):
form_2 = self.form_class_2(self.request.POST)
if form_2.is_valid():
keyword = form_2.cleaned_data['keyword']
books = self.search(keyword)
if books:
for book in books:
title = self.add_title(book)
form = self.form_class(initial={"title": title})
if form.is_valid():
form.save()
else:
return HttpResponseRedirect(reverse_lazy('add_books'))
return HttpResponseRedirect(reverse_lazy('import_books'))
else:
return HttpResponseRedirect(reverse_lazy('index_books'))
return reverse_lazy('import_books')
my form
class BookForm(forms.ModelForm):
class Meta:
model = Book
exclude = ()
my form_2
class SearchBookForm(forms.Form):
keyword = forms.CharField(max_length=100)
my model
class Book(models.Model):
title = models.CharField(
max_length=75,
verbose_name='Book title')
published_date = models.CharField(
max_length=10,
validators=[check_if_value_is_date, max_year_validator],
blank=True,
null=True,
verbose_name='Publishing date')
pages = models.IntegerField(
validators=[check_if_value_is_negative],
blank=True,
null=True,
verbose_name='Number of pages')
language = models.CharField(
max_length=2,
blank=True,
null=True,
verbose_name='Language')
And this is how my form looks before validation:
<tr><th><label for="id_title">Book title:</label></th><td><input type="text" name="title" value="Harry Potter i Kamień F
ilozoficzny" maxlength="75" required id="id_title"></td></tr>
<tr><th><label for="id_published_date">Publishing date:</label></th><td><input type="text" name="published_date" maxleng
th="10" id="id_published_date"></td></tr>
<tr><th><label for="id_pages">Number of pages:</label></th><td><input type="number" name="pages" id="id_pages"></td></tr
>
<tr><th><label for="id_language">Language:</label></th><td><input type="text" name="language" maxlength="2" id="id_langu
age"></td></tr>
Basically I have 2 forms. form 2 is used to input value which is used as argument in my search function. then this search function return .json, then i took some value from this .json and assign to "title" then this title is my initial data for form. And everything works fine until part with validation. My form isn't valid but when I print my form before validation part I see that my initial data is in form as expected.
Django by default requires Your fields in the form to be filled out. So BookForm requires that you have title, published_date, pages, and language filled out in the form. You instantiate form without passing actual input. Yes you pass in the initial, but you don't pass in request.POST to it (according to what's here). So your form instantiation should look like
form = self.form_class({'title': title, 'published_date': book.published_date, 'pages': book.pages, 'language': book.language})

ModelForm right approach for editing database record?

Can someone help me with fixing Django ModelForm?
This particular code can add new item to database as expected, but when I'm trying to edit db record - It just add new record, instead of updating old. I'm quite new in Django framework.
views.py:
def manage(request, item_id = None):
t = get_object_or_404(Hardware, id=item_id) if item_id else None
form = Manage(request.POST or None, instance=t)
if t:
if form.is_valid():
#form.save()
hostname = form.cleaned_data['hostname']
cpu = form.cleaned_data['cpu']
os = form.cleaned_data['os']
ram = form.cleaned_data['ram_total']
storage = form.cleaned_data['storage']
hostdata = Hardware(
hostname=hostname,
cpu=cpu,
ram_total=ram,
os=os,
storage=storage,
lock_state=t.lock_state, # because in edit operation we shouldn't change it.
lock_date=t.lock_date, # because in edit operation we shouldn't change it.
locked_by=t.locked_by) # because in edit operation we shouldn't change it.
hostdata.save()
return HttpResponseRedirect(reverse('main:index'))
elif not t:
if form.is_valid():
hostname = form.cleaned_data['hostname']
cpu = form.cleaned_data['cpu']
os = form.cleaned_data['os']
ram = form.cleaned_data['ram_total']
storage = form.cleaned_data['storage']
current_user = request.user
user = User.objects.get(id=current_user.id)
hostdata = Hardware(
hostname=hostname,
cpu=cpu,
ram_total=ram,
os=os,
storage=storage,
lock_state=0,
lock_date=datetime.datetime.now(),
locked_by=user)
hostdata.save()
return HttpResponseRedirect(reverse('main:index'))
return render(request, 'hardware/edit.html', {'form': form})
models.py:
class Hardware(models.Model):
hostname = models.CharField(max_length=255, default=None)
os = models.CharField(max_length=255, default=None)
cpu = models.CharField(max_length=255, default=None)
ram_total = models.CharField(max_length=255, default=None)
storage = models.CharField(max_length=255, default=None)
lock_state = models.BooleanField(default=0)
locked_by = models.ForeignKey(User)
lock_date = models.DateTimeField(default=None)
alive = models.BooleanField(default=0)
class Meta:
db_table = "hardware"
def __str__(self):
return self.hostname
forms.py:
class Manage(forms.ModelForm):
class Meta:
model = Hardware
fields = ['hostname', 'os', 'cpu', 'ram_total', 'storage']
urls.py:
url(r'^manage/new/$', views.manage, name='add'),
url(r'^manage/edit/(?P<item_id>[0-9]+)/$', views.manage, name='edit')
template:
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save!" />
</form>
You already retrieved the instance t in the first line of your view. The code below will always create a new instance (unless you specify the pk parameter):
hostdata = Hardware(...)
hostdata.save()
Simply do this instead:
if t:
if form.is_valid():
t.hostname = form.cleaned_data['hostname']
t.cpu = form.cleaned_data['cpu']
....
t.save()
However, you really should rely on the save method provided by the ModelForm as the other answers suggested. Here's an example:
def manage(request, item_id=None):
t = get_object_or_404(Hardware, id=item_id) if item_id else None
# if t is None, a new object will be created in form.save()
# if t is an instance of Hardware, t will be updated in form.save()
form = Manage(request.POST, instance=t)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('main:index')
return render(request, 'hardware/edit.html', {'form': form})
You also specified fields in your form:
fields = ['hostname', 'os', 'cpu', 'ram_total', 'storage']
These are the fields which will be set or updated when you call form.save().
I think something like this - using update_fields - should work:
def manage(request, item_id = None):
t = get_object_or_404(Hardware, id=item_id)
form = Manage(request.POST or None, instance=t)
if t:
if form.is_valid():
#form.save()
t.hostname = form.cleaned_data['hostname']
t.cpu = form.cleaned_data['cpu']
t.os = form.cleaned_data['os']
t.ram = form.cleaned_data['ram_total']
t.storage = form.cleaned_data['storage']
t.save(update_fields=['hostname', 'cpu', 'os','ram','storage'])
return HttpResponseRedirect(reverse('main:index'))
........
Try Class Based View, which in it's simplest looks like:
from django.views import generic
class HardwareEditView(generic.UpdateView):
template_name = "hardware.html"
form_class = Manage
You will have to add get_absolute_url to the model.
Generic class based views are exactly for this standard create/update/view common tasks.

django - prevent form submission if submitted form value is X

I am using Django 1.4.6 & python 2.7.
I have a test form that allows the user to enter data from a select list:
<select name="language_code" id="id_language_code">
<option value="en-CA">English (Canada) - English (Canada)‎</option>
<option value="en-GB" selected="selected">English (UK) - English (UK)‎</option>
<option value="en">English (US)</option>
<option value="fr-CA">French (Canada) - français (Canada)‎</option>
<option value="fr">French (France) - français (France)‎</option>
</select>
The form submission is working OK.
However, I need to make a change so that if the user selects the select list option of fr-CA and then submits the form, then I would like to NOT add the record and also redirect the user to a different form - return redirect(settings.MENU_DETAIL_LINK_NAME_DETAILS).
Essentially, if the boolean field language_code_disabled of LanguageVersion model is true, then the user should not be able to add the language version.
I am not exactly sure how to do this. I have been going around in circles on this so I have confused myself on how to achieve this.
Here is my models code:
class NameDetails(models.Model, FillableModelWithLanguageVersion):
user = models.ForeignKey(User)
language_version = models.ForeignKey('LanguageVersion')
name_details_prefix_title = models.CharField(null=True, blank=True, max_length=25)
name_details_first_name = models.CharField(null=False, blank=False, max_length=50)
name_details_middle_name = models.CharField(null=True, blank=True, max_length=100)
name_details_last_name = models.CharField(null=False, blank=False, max_length=60)
name_details_suffix_title = models.CharField(null=True, blank=True, max_length=25)
class LanguageVersion(models.Model):
"""Language version selection for a user"""
user = models.ForeignKey(User)
language_code = models.CharField(max_length=32)
language_code_disabled = models.BooleanField(default=False)
Here is my views.py
#login_required
def name_details_add(request):
language_versions = LanguageVersion.objects.filter(user=request.user)
available_languages = get_available_language_details(language_versions, request.user.userprofile.language_preference)
name_details_num = request.user.namedetails_set.count()
preview_labels = get_name_details_labels(available_languages)
if name_details_num >= settings.MAX_NAME_DETAILS:
return redirect(settings.MENU_DETAIL_LINK_NAME_DETAILS)
if request.method == 'GET':
form = NameDetailsForm(
available_languages=available_languages,
language_preference=request.user.userprofile.language_preference,)
elif request.method == 'POST':
form = NameDetailsForm(
available_languages=available_languages,
language_preference=request.user.userprofile.language_preference,
data=request.POST)
if form.is_valid() and name_details_num < settings.MAX_NAME_DETAILS:
name_detail = NameDetails(user=request.user)
name_detail.fill(form.cleaned_data)
name_detail.save()
messages.success(request, _('successfully added.'))
return redirect(settings.MENU_DETAIL_LINK_NAME_DETAILS)
I am hoping that somone can help me out on this issue.
I need to find out how to call a list of all language codes from the LanguageVersions model of the user where the language_code_disabled is True.
Do I write a function that will loop through the language versions or can I write a simple call function to return a list of disabled language codes, such as en, fr, fr-CA, de ?
disabled_language_versions = LanguageVersion.objects.filter(user=request.user, language_code_disabled=True)
you can add a field validation for the LanguageVersion fk on the form, and access that information in the view:
class NameDetailsForm(forms.ModelForm):
...
def clean_language_version(self):
lang = self.cleaned_data.get('language_version')
if LanguageVersion.objects.filter(language_code=lang, language_code_disabled=True).exists():
raise forms.ValidationError("Language code disabled")
return lang
then in the view:
if form.is_valid() ... :
...
elif form['language_version'].errors:
return redirect(settings.MENU_DETAIL_LINK_NAME_DETAILS)
Well, you can do this:
if request.POST['language_code'] == 'fr-CA':
return HttpResponseRedirect('/some/other/page/')

Categories