Django's formset to edit partly predefined data - django

Here's my domain model:
class Seat(models.Model):
name = models.CharField(max_length=8)
class Event(models.Model):
...
class Occupation(models.Model):
event = models.ForeignKey(Event, related_name='occupations')
seat = models.ForeignKey(Seat)
number = models.CharField(max_length=20)
(Seat is a small table, like 5-10 records.)
We have an UI requirements for a event edit page to look like this:
[1-01] [enter number]
[1-02] [enter number]
[1-03] [enter number]
[2-01] [enter number]
[2-02] [enter number]
[2-03] [enter number]
User navigates to event's occupation page where they see a list of all seats from system and prompted to fill numbers from the external source into the system.
Since the seats table is pretty small and to prevent errors like choosing same seat twice, we're required to display all seats pre-fill into the form and locked, so the user can't change seat selection and only limited to enter corresponding numbers.
Also, seats can be added or removed, so we can't make a "static" form with 6 predefined rows.
I suppose it should be a Django's inline model formset with a form like
class OccupationForm(forms.ModelForm):
class Meta:
model = Occupation
fields = ('seat', 'number')
...
But I'm not sure how should I display a prefilled form, prevent an user from changing seats (and not just a client-side locking via disabled or javascript)

First set seat widget to HiddenInput and add the seat_name property to the form. This property will be used later in the HTML template:
class OccupationForm(forms.ModelForm):
class Meta:
model = Occupation
fields = ('seat', 'number')
widgets = {'seat': forms.HiddenInput}
def __init__(self, *args, **kwargs):
super(OccupationForm, self).__init__(*args, **kwargs)
self.fields['number'].required = False
self.seat_name = self.initial['seat'].name
Then populate initial data with the seats and numbers for the event. Pass this initial to the formset. Validate and save formset as usual:
from django.forms.formsets import formset_factory
def update_seats(request, event_id):
event = Event.objects.get(pk=event_id)
numbers = dict((o.seat, o.number) for o in event.occupations.all())
initial = [{'seat': seat, 'number': numbers.get(seat, '')}
for seat in Seat.objects.all()]
OccupationFormSet = formset_factory(OccupationForm,
min_num=len(initial), validate_min=True,
max_num=len(initial), validate_max=True,
extra=0)
if request.method == 'POST':
formset = OccupationFormSet(request.POST, initial=initial)
if formset.is_valid():
for form in formset:
seat = form.initial['seat']
number = form.cleaned_data.get('number', '').strip()
if number:
Occupation.objects.update_or_create(
event=event, seat=seat,
defaults={'number': number})
else:
Occupation.objects.filter(event=event, seat=seat).delete()
return redirect('.')
else:
formset = OccupationFormSet(initial=initial)
return render(request, 'update_seats.html', {'formset': formset})
And update_seats.html template in which we show seat name as {{ form.seat_name }}:
<form action="" method="post">
{% csrf_token %}
<table>
{{ formset.management_form }}
<tr>
<th>Seat</th>
<th>Number</th>
</tr>
{% for form in formset %}
<tr>
<td>[{{ form.seat_name }}]{{ form.seat }}</td>
<td>{{ form.number }}</td>
</tr>
{% endfor %}
</table>
<button>Update</button>
</form>

Related

How to overide django modelform to achieve custom behaviour

I have an Item object that has a manytomany relation to another object Option. I create a modelform from the Item object like so;
class Item(models.Model):
category = models.ForeignKey(Category)
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True)
options = models.ManyToManyField(Option)
class OptionForm(ModelForm):
options = forms.ChoiceField(widget=forms.RadioSelect())
class Meta:
model = Item
fields = ( 'options', )
When i render the form in the template it renders all available options for the Item object(the expected behavior) even those not created by a specific item. I want to be able to load options defined by the specific Item that will be chosen by the user. How do i override the form to achieve such behavior.for example without a form i can render an Items own Options through its id. item = Item.objects.get(pk=id)
Its tough to make ModelForm's defined on the fly because they are intimately tied to the structure in your model. Nevertheless you could use some clever template control flow and view rendering to get your desired effect. This is untested so you millage with this might vary.
<form method="post" action="">
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
<ul>
{% if user_option.category %}
<li>{{ form.caregory }}</li>
{% endif %}
{% if user_option.name %}
<li>{{ form.name }}</li>
{% endif %}
{% if user_option.p_opt %}
<li>{{ form.price }}</li>
<li>{{ form.options }}</li>
{% endif %}
</ul>
{% endfor %}
</form>
From the Djano docs here.
Try overriding the form's init method and passing in the Item pk as an additional argument. The trick here is to pop the argument before calling the parent init.
class ItemOptionsForm(forms.ModelForm):
class Meta:
model = Item
def __init__(self, *args, **kwargs):
# pop 'item_id' as parent's init is not expecting it
item_id = kwargs.pop('item_id', None)
# now it's safe to call the parent init
super(ItemOptionsForm, self).__init__(*args, **kwargs)
# Limit options to only the item's options
if item_id:
try:
item = Item.objects.get(id=item_id)
except:
raise ValidationError('No item found!')
self.fields['options'] = forms.ChoiceField(item.options)
Then, in your view, create the form like:
form = ItemOptionsForm(item_id=item_id)
The nice thing about this is that you can raise ValidationErrors that will show up in the form.
Be aware that this doesn't prevent someone from POSTing option IDs to your form which do not belong to the Item, so you'll likely want to override the ModelForm.clean() to validate the options as well.
learning from link django: How to limit field choices in formset? provided by #jingo, i solved the problem by first of all creating dynamic form like so;
def partial_order_item_form(item):
"""dynamic form limiting optional_items to their items"""
class PartialOrderItemform(forms.Form):
quantity = forms.IntegerField(widget=forms.TextInput(attrs={'size':'2', 'class':'quantity','maxlength':'5'}))
option = forms.ModelChoiceField(queryset=OptionalItems.objects.filter(item=item),widget= forms.RadioSelect())
return PartialOrderItemform
then validating form like so;
def show_item(request,id):
option = get_object_or_404(Item,pk=id)
if request.method == 'POST':
form = partial_order_item_form(option)
#bound form to POST data,
final_form = form(request.POST)
# check validation of posted data
if final_form.is_valid():
order.add_to_order(request)
url =urlresolvers.reverse('order_index',kwargs={'id':a.id})
# redirect to order page
return HttpResponseRedirect(url)
else:
form = partial_order_item_form(item=id)
context={
'form':form,
}
return render_to_response('item.html',context,context_instance=RequestContext(request))

modelformset_factory does not honor extra parameter

Django: 1.4.1
Model:
class Hoja(models.Model):
nombre = models.CharField(max_length=200) # requerido
class Linea(models.Model):
hoja = models.ForeignKey(Hoja) # requerido
nombre = models.CharField(max_length=200) # requerido
padre = models.ForeignKey('self', null=True, blank=True, related_name='hijo')
View:
lineas = Linea.objects.filter(hoja=alt).order_by('id')
LineaHojaSet = modelformset_factory(Linea, can_delete=True, extra=1 if request.POST.has_key('siguiente') else 0)
formset = LineaHojaSet(request.POST or None, queryset=lineas)
if request.method=='POST':
# process formset
return render_to_response('template.html', {'formset':formset}, context_instance=RequestContext(request))
Template:
<table>
<thead>
<tr><th>Nombre</th><th>Borrar</th></tr>
</thead>
<tbody>
{% for fs in formset %}
<tr>
<td>{{ fs.nombre }}</td>
<td>{{ fs.id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<input type="submit" name="siguiente" value="AƱadir siguiente" />
When I submit the "siguiente" button, I can see than the formset is getting the correct extra field of 1, but in the webpage, the only rows showing are the database ones. It's this a bug, or I'm doing something wrong?
Formset factory finds number of forms either by max_num, extra parameters or form-TOTAL_FORMS parameter in request.POST (or data) from management form.
In your case, request.POST['form-TOTAL_FORMS'] has number which does not include extra form . So it does not add extra form when you create formset.
One solution would be to increment this number by one when your condition is met. e.g.
data = None
if request.POST:
data = request.POST.copy() #required as request.POST is immutable
if request.POST.has_key('siguiente'):
data['form-TOTAL_FORMS'] = int(data['form-TOTAL_FORMS']) + 1
#now use data instead of request.POST
formset = LineaHojaSet(data, queryset=lineas)
....
However, there are some drawbacks of manipulating formset this way. When you validate formset, the extra form will show errors if there are any required fields.
Better solution would be to create formset again before passing it template with one extra form and queryset. Most likely, when formset is valid, you would save any new objects, those will get added by queryset. So your page will show newly added objects and one extra form.
lineas = Linea.objects.filter(hoja=alt).order_by('id')
LineaHojaSet = modelformset_factory(Linea, can_delete=True,)
formset = LineaHojaSet(request.POST or None, queryset=lineas)
if request.method=='POST':
# process formset
if formset.is_valid:
#saved and done with formset.
if request.POST.has_key('siguiente'):
LineaHojaSet = modelformset_factory(Linea, can_delete=True, extra=1)
formset = LineaHojaSet(queryset=lineas)
...
return render_to_response('template.html', {'formset':formset}, context_instance=RequestContext(request))

Django and users entering data

I am building a webapp which will be used by a company to carry out their daily operations. Things like sending invoices, tracking accounts receivable, tracking inventory (and therefore products). I have several models set up in my various apps to handle the different parts of the web-app. I will also be setting up permissions so that managers can edit more fields than, say, an office assistant.
This brings me to my question. How can I show all fields of a model and have some that can be edited and some that cannot be edited, and still save the model instance?
For example, I have a systems model for tracking systems (we install irrigation systems). The system ID is the primary key, and it is important for the user to see. However, they cannot change that ID since it would mess things up. Now, I have a view for displaying my models via a form using the "form.as_table". This is efficient, but merely spits out all the model fields with input fields filling in the values stored for that model instance. This includes the systemID field which should not be editable.
Because I don't want the user to edit the systemID field, I tried making it just a label within the html form, but django complains. Here's some code:
my model (not all of it, but some of it):
class System(models.Model):
systemID = models.CharField(max_length=10, primary_key=True, verbose_name = 'System ID')
systemOwner = models.ForeignKey (System_Owner)
installDate = models.DateField()
projectManager = models.ForeignKey(Employee, blank=True, null=True)
#more fields....
Then, my view for a specific model instance:
def system_details(request, systemID):
if request.method == 'POST':
sysEdit = System.objects.get(pk=systemID)
form = System_Form(request.POST, instance=sysEdit)
if form.is_valid():
form.save()
return HttpResponseRedirect('/systems/')
else:
sysView = System.objects.get(pk=systemID)
form = System_Form(instance=sysView)
return render_to_response('pages/systems/system_details.html', {'form': form}, context_instance=RequestContext(request))
Now the html page which displays the form:
<form action="" method="POST">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Save Changes">
<input type="button" value="Cancel Changes" onclick="window.location.href='/systems/'">
</form>
So, what I am thinking of doing is having two functions for the html. One is a form for displaying only those fields the user can edit, and the other is for just displaying the content of the field (the systemID). Then, in the view, when I want to save the changes the user made, I would do:
sysValues = System.objects.get(pk=SystemID)
form.save(commit = false)
form.pk = sysValues.sysValues.pk (or whatever the code is to assign the sysValues.pk to form.pk)
Is there an easier way to do this or would this be the best?
Thanks
One thing you can do is exclude the field you don't need in your form:
class System_Form(forms.ModelForm):
class Meta:
exclude = ('systemID',)
The other is to use read-only fields: http://docs.djangoproject.com/en/1.3/ref/contrib/admin/#django.contrib.admin.ModelAdmin.readonly_fields as #DTing suggessted
To make a field read only you can set the widget readonly attribute to True.
using your example:
class System_Form(ModelForm):
def __init__(self, *args, **kwargs):
super(System_Form, self).__init__(*args, **kwargs)
self.fields['systemID'].widget.attrs['readonly'] = True
class Meta:
model = System
or exclude the fields using exclude or fields in the class Meta of your form and display it in your template if desired like so:
forms.py
class System_Form(ModelForms):
class Meta:
model = System
exclude = ('systemID',)
views.py
def some_view(request, system_id):
system = System.objects.get(pk=system_id)
if request.method == 'POST':
form = System_Form(request.POST, instance=system)
if form.is_valid():
form.save()
return HttpResponse('Success')
else:
form = System_Form(instance=system)
context = { 'system':system,
'form':form, }
return render_to_response('some_template.html', context,
context_instance=RequestContext(request))
some_template.html
<p>make changes for {{ system }} with ID {{ system.systemID }}</p>
<form method='post'>
{{ form.as_p }}
<input type='submit' value='Submit'>
</form>

Django: Using Radio select box on model formsets

Hey,
I'm using a model formset to let my users edit their photo album. I want to put a Radio select box on every photo saying "Set as cover image" so that I can process all the photos and find the one who should be album cover. The problem is how can I a field with radio select on to the formset and still keep it mutal with the rest of the photos? This is my current code:
class ProjectGalleryForm(forms.ModelForm):
remove_photo = forms.BooleanField()
# set_as_cover_image = .... ?? <-- what to put?
class Meta:
model = Photo
exclude = (
'effect',
'caption',
'title_slug',
'crop_from',
'is_public',
'slug',
'tags'
)
I think the key here is that the radio button is not actually part of the formset: it's part of the parent form. It's the actual Album model that needs to know which of the Photo objects is the cover image. So what you want to do is to display each option from the radio button alongside its corresponding line in the Photo formset - and that's the tricky bit, because Django can't render form fields in that way. You'll need to produce the HTML for each option manually.
So, given these forms, and assuming the Album model has a cover_image which is a OneToOneField to Photo:
class AlbumForm(forms.modelForm):
class Meta:
model = Album
photo_formset = forms.inlineformset_factory(Album, Photo, form=ProjectGalleryForm)
in the template you would do something like:
{% for photo_form in photo_formset %}
<tr><td>
{% if photo_form.instance.pk %}
<input type="radio" id="id_cover_image_{{ forloop.counter }}" name="cover_image" value="{{ photo_form.instance.pk }}">
<label for="id_cover_image_{{ forloop.counter }}">Use as cover image</label>
{% endif %>
</td><td>{{ photo_form.as_p }}</td>
</tr>
{% endfor %}
I like to have the a neat template file and therefore, I made a custom widget for this purpose.
class SingleRadioInput(Input):
input_type = 'radio'
def render(self, value, checked, attrs=None):
output = []
if value:
is_cover = ''
if checked : is_cover = 'checked'
output.append(
('<input type="radio" name="inline" value="%s" %s/>')
% (value, is_cover)
)
return mark_safe(u''.join(output))
Hope it can help someone
Based on #Mikou answer, here is my more comprehensive solution.
In order to keep my template clean and pretty, I used a custom widget
class SingleRadioInput(forms.widgets.Input):
input_type = 'radio'
def render(self, name, value, attrs=None):
final_attrs = self.build_attrs(attrs, type=self.input_type)
output = []
if name:
is_checked = ''
if value:
is_checked = 'checked'
output.append(
('<input id="%s" type="radio" name="%s" value="%s" %s/>')
% (final_attrs['id'], final_attrs['name'], final_attrs['instance_id'], is_checked )
)
return mark_safe(u''.join(output))
My object form looks like that, it will auto select the object if the field default == True
class ObjectForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ObjectForm, self).__init__(*args, **kwargs)
self.fields['default'].widget.attrs.update({'instance_id': self.instance.id, 'name': 'default'})
if self.instance.default:
self.fields['default'].widget.attrs.update({'value': True})
class Meta:
model = MyModel
fields = ['default']
widgets = {
'default': SingleRadioInput(),
}
Here is my formset
ProductReferenceFormset = inlineformset_factory(ParentModel, MyModel,
form=ObjectForm,
extra=0, can_delete=False, can_order=False)
I gave up handling the save part in the form, it is really not worth the complexity I think... So the save part is in the form_valid() in the View
def form_valid(self, form, price_form):
form.save()
# save the default radio
MyModel.objects.filter(parent=self.object).update(default=False)
MyModel.objects.filter(id=self.request.POST.get('default')).update(default=True)
return HttpResponseRedirect(self.get_success_url())
Qualification:
<option value='10th' {% if '10th' in i.qf %} selected='select' {% endif %}>10th</option>
<option value='12th' {% if '12th' in i.qf %} selected='select' {% endif %}>12th</option>
<option value='graduted' {% if 'Graduated' in i.qf %} selected='select' {% endif %}>Graduated</option>
</select>
<br><br>

CharField values disappearing after save (readonly field)

I'm implementing simple "grade book" application where the teacher would be able to update the grades w/o being allowed to change the students' names (at least not on the update grade page). To do this I'm using one of the read-only tricks, the simplest one. The problem is that after the SUBMIT the view is re-displayed with 'blank' values for the students. I'd like the students' names to re-appear.
Below is the simplest example that exhibits this problem. (This is poor DB design, I know, I've extracted just the relevant parts of the code to showcase the problem. In the real example, student is in its own table but the problem still exists there.)
models.py
class Grade1(models.Model):
student = models.CharField(max_length=50, unique=True)
finalGrade = models.CharField(max_length=3)
class Grade1OForm(ModelForm):
student = forms.CharField(max_length=50, required=False)
def __init__(self, *args, **kwargs):
super(Grade1OForm,self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id:
self.fields['student'].widget.attrs['readonly'] = True
self.fields['student'].widget.attrs['disabled'] = 'disabled'
def clean_student(self):
instance = getattr(self,'instance',None)
if instance:
return instance.student
else:
return self.cleaned_data.get('student',None)
class Meta:
model=Grade1
views.py
from django.forms.models import modelformset_factory
def modifyAllGrades1(request):
gradeFormSetFactory = modelformset_factory(Grade1, form=Grade1OForm, extra=0)
studentQueryset = Grade1.objects.all()
if request.method=='POST':
myGradeFormSet = gradeFormSetFactory(request.POST, queryset=studentQueryset)
if myGradeFormSet.is_valid():
myGradeFormSet.save()
info = "successfully modified"
else:
myGradeFormSet = gradeFormSetFactory(queryset=studentQueryset)
return render_to_response('grades/modifyAllGrades.html',locals())
template
<p>{{ info }}</p>
<form method="POST" action="">
<table>
{{ myGradeFormSet.management_form }}
{% for myform in myGradeFormSet.forms %}
{# myform.as_table #}
<tr>
{% for field in myform %}
<td> {{ field }} {{ field.errors }} </td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input type="submit" value="Submit">
</form>
Your way of displaying the readonly field is the problem.
Since the student field is disabled, the form submit will not have that as the input, so the error form that is displayed with validation error messages don't get the initial value.
That is why ReadOnly Widget has to be more complex than just being a html disabled field.
Try using a real ReadOnlyWidget, one that overrides _has_changed.
Following is what I use. For instantiation, it takes the original_value and optionally display_value, if it is different.
class ReadOnlyWidget(forms.Widget):
def __init__(self, original_value, display_value=None):
self.original_value = original_value
if display_value:
self.display_value = display_value
super(ReadOnlyWidget, self).__init__()
def _has_changed(self, initial, data):
return False
def render(self, name, value, attrs=None):
if self.display_value is not None:
return unicode(self.display_value)
return unicode(self.original_value)
def value_from_datadict(self, data, files, name):
return self.original_value
I'm stretching myself a little here, so some thoughts:
% Have you sniffed the traffic to see exactly what's being sent between browser and server?
% Do you need to send the student name as a hidden field (your db update thing may assume you want student blank if you don't)?
% Have you looked at the source of your HTML after Python parses it?