django forms with ModelChoiceField - python

After researching for days I'm still confused with creating a form involving 4 tables which are all connected via ForiegnKey. I'm not even sure if I'm using the right approach.
Should I use forms.Form or ModelForm? What I'm looking for I'm sure is common but all of my attempts are in vain. It seems like I'm making this more complicated than it should be since all 4 tables are related.
To the Point: To create a workorder, pick a Building, then pick a Unit within that Building, then pick a caller from Tenant(s) within that Unit. Then gather all data in view and save WorkOrder.
Using Django 1.6
# models.py
class Building(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=200)
...
def __unicode__(self):
return self.name
class Unit(models.Model):
name = models.ForeignKey(Building)
unit = models.CharField(max_length=30)
...
def __unicode__(self):
return self.unit
class Tenant(models.Model):
unit = models.ForeignKey(Unit)
name = models.ForeignKey(Building)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
...
def __unicode__(self):
return u'%s %s' % (self.first_name, self.last_name)
class WorkOrder(models.Model):
name = models.ForeignKey(Building)
unit = models.ForeignKey(Unit)
ordernum = models.AutoField(primary_key=True)
...
def __unicode__(self):
return self.unit
forms.py
# forms.py
class WorkOrderForm(forms.Form):
building_choice = forms.ModelChoiceField(queryset=Building.objects.all(),
empty_label='Pick a building',
)
def __init__(self, *args, **kwargs):
super(WorkOrderForm, self).__init__(*args, **kwargs)
self.fields['building_choice'].label = 'Building'
unit_choice = forms.ModelChoiceField(queryset=Unit.objects.(????),
empty_label='Pick a unit',
)
def __init__(self, *args, **kwargs):
super(WorkOrderForm, self).__init__(*args, **kwargs)
self.fields['unit_choice'].label = 'Unit'
caller_choice = forms.ModelChoiceField(queryset=Tenant.objects.(????),
empty_label='Who called',
)
def __init__(self, *args, **kwargs):
super(WorkOrderForm, self).__init__(*args, **kwargs)
self.fields['caller_choice'].label = 'Tenant'
views.py (incomplete)
#views.py
def create(request):
if request.method == 'POST':
form = WorkOrderForm(request.POST)
if form.is_valid():
workorder = WorkOrder(name=form.cleaned_data['name'],
unit=form.cleaned_data['unit'],
call_date=form.cleaned_data['call_date'],
caller=form.cleaned_data['caller'],
problem_desc=form.cleaned_data['problem_desc'])
workorder.save()
return HttpResponseRedirect('/workorder/')
else:
form = WorkOrderForm()
return render(request, 'workorder_form.html', {'form': form})
If someone could let me know the correct way to go about this I'd be forever grateful.

It's been a while since I posted this question but I found a django package django-clever-selects that met my needs. The docs and example project were well commented. Thanks to all that helped mr earlier.

Related

How to modify values in m2m field in form? (Django)

I have 2 models in my models.py. They are Doctor and Specialty:
class Doctor(models.Model):
name = CharField(max_length=255)
specialties = ManyToManyField('Specialty', related_name='doctors')
mc_id = CharField(max_length=255, unique=True)
class Specialty(models.Model):
name = CharField(max_length=255)
mc_id = CharField(max_length=255, unique=True)
def __str__(self):
return self.name
When i'm creating new Doctor instance via admin interface (change_form.html template) I see my Doctor model fields, which I should fill with data. Among these fields there is a field specialties ("Cпециальности"), which is m2m field and contains names (as I set __str__(self) method in Specialty model to return self.name) for specialties which were previously added to Specialty model (see a screen).
All I want is to see in this list not only specialty names, but also values of mc_id field of Specialty model, but it is not my case to make it at the expense of something like this:
class Specialty(models.Model):
name = CharField(max_length=255)
mc_id = CharField(max_length=255, unique=True)
def __str__(self):
return self.name + '(' + self.mc_id + ')'
I thought i could make it through overriding of formfield_for_manytomany() method in my admin model (associated with Doctor model) in admin.py:
#admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "specialties":
queryset = Specialty.objects.all()
for specialty in queryset:
specialty.name += '(' + {specialty.mc_id} + ')'
return super().formfield_for_manytomany(db_field, request, **kwargs)
It didn't work. I'd like to know how can i solve my problem. Thank you in advance.
UPDATE: I'd also like to make a content of specialties field in the form user-dependent:
if superuser tries to add new doctor he will see in the specialties field of the form values like:
Internal Medicine (mc1_id)
Neurology (mc1_id)
Cardiology (mc2_id)
if normal user which name is 'mc1_id' tries to add new doctor he will see in the form values filtered by his name and without '(mc_id)'-part. Like:
Internal Medicine
Neurology
I think that overwrite the label_from_instance method in the form field can work for you.
class SpecialtyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
obj.name + '(' + obj.mc_id + ')'
class DoctorModelForm(forms.ModelForm):
specialties = SpecialtyModelMultipleChoiceField(
Specialty.objecs.all
# ....
)
class Meta:
model = Doctor
fields = '__all__'
#admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
form = DoctorModelForm
# ....
UPDATE:
In order to make the field text user dependent you can return different classes in the get_form method of the Admin Model:
class SpecialtyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
obj.name + '(' + obj.mc_id + ')'
class DoctorModelForm(forms.ModelForm):
specialties = SpecialtyModelMultipleChoiceField(
Specialty.objecs.all
# ....
)
class Meta:
model = Doctor
fields = '__all__'
class SimpleDoctorModelForm(forms.ModelForm):
class Meta:
model = Doctor
fields = '__all__'
#admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kw):
if request.user.is_superuser:
return DoctorModelForm
return SimpleDoctorModelForm
If you need more, you can set properties in the form and work with its in the constructor:
class DoctorModelForm(forms.ModelForm):
class Meta:
model = Doctor
fields = '__all__'
#classmethod
def set_user(cls, user):
cls.__user = user
def __init__(self, *args, **kw):
super(DoctorModelForm, self).__init__(*args, **kw)
if self.__user.condition:
self.fields['specialties'] = MyCustomField
#admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
form = DoctorModelForm
def get_form(self, request, obj=None, **kw):
form_class = super(DoctorAdmin, self).get_form(request, obj, **kw)
form_class.set_user(request.user)
return form_class
Just use format() in models
def __str__(self):
return '{} {}'.format(self.name, self.mc_id)
I'd like to share an altnernative realisation of Tonio's great idea.
# fields.py
class SpecialtyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
obj.name + '(' + obj.mc_id + ')'
# admin.py
class BaseAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
# some logic
return form
#admin.register(Doctor)
class DoctorAdmin(BaseAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "specialties":
queryset = Specialty.objects.all().filter(mc_id=request.user)
kwargs.update({'queryset': queryset})
return super().formfield_for_manytomany(db_field, request, **kwargs)
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if request.user.is_superuser:
form.base_fields['specialties'] = SpecialtyModelMultipleChoiceField(Specialty.objects.all())
return form
I think it can be useful in situations when DoctorAdmin is inherited from some base model like BaseAdmin and this base model also has overriden get_form() method. But here in case of superuser's request queryset is formed twice, therefore Tonio's answer is definetely better.

My form with a ModelMultipleChoiceField is not saving data.

In the admin panel, I can add Persons to my CompleteClass model. There is a M2M relationship between CompleteClass and Person. But, my form doesn't work as it should. The pub_date will update, and I can save the head_count, but not the ModelMultipleChoiceField (persons) -- it will not save.
models.py
class Person(models.Model):
name = models.CharField(max_length=255)
persona_description = models.CharField(max_length=255)
def __str__(self):
return self.name
class CompleteClass(models.Model):
persons = models.ManyToManyField(Person)
class_name = models.CharField(max_length=255)
class_head_count = models.IntegerField()
class_pub_date = models.DateField()
def __str__(self):
return '%s %s' % (self.class_name, self.class_head_count)
def save_complete_class(self):
self.class_pub_date = timezone.now()
self.save()
class Meta:
ordering = ('class_pub_date',)
Here is views.py:
def class_new(request):
if request.method == "POST":
form = CompleteClassForm(request.POST)
if form.is_valid():
complete_class = form.save(commit=False)
complete_class.class_pub_date = timezone.now()
complete_class.save()
form.save_m2m()
return redirect('class_detail', pk=complete_class.pk)
else:
form = CompleteClassForm()
return render(request, 'app/class_edit.html', {'form': form})
and forms.py
class CompleteClassForm(forms.ModelForm):
class Meta:
model = CompleteClass
fields = ('class_name', 'class_head_count',)
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super(CompleteClassForm, self).__init__(*args, **kwargs)
self.fields['class_persons']=forms.ModelMultipleChoiceField(queryset=Person.objects.all())
I've read through the documentation and used the save_m2m since i've set commit=false.
The POST data contains person data, but it's not being written to the database. I'm stumped. Please help!
Only fields named in the fields tuple are saved to the instance. You don't have your m2m field listed there.
You also define your modelchoicefield with a different name - class_persons instead of persons. In fact, there is no reason to define that field separately at all - you haven't changed any of the attributes from the defaults.
And once you've removed that definition, there ​is also no reason to override __init__, seeing as you never pass the user parameter nor do you use it anywhere in the form.

Django ModelForm - Performance issue

I am using ModelForm to allow multiple rows edit at the same time. It is a very simple form that has series of yes_no columns. The model looks like:
models.py
class Yn(models.Model):
yn_id = models.IntegerField(primary_key=True)
description = models.CharField(max_length=30)
def __str__(self):
return ' '.join([
self.description,
])
class Meta:
managed = False
db_table = 'yn'
class Invoice(models.Model):
description = models.CharField(max_length=50)
invoice_date = models.DateTimeField()
...
invoice_sent_yn = models.ForeignKey('Yn', models.DO_NOTHING, db_column='invoice_sent_yn', related_name="invoice_sent_yn")
confirm_receipt_yn = models.ForeignKey('Yn', models.DO_NOTHING, db_column='confirm_receipt_yn', related_name="confirm_receipt_yn")
paid_yn = models.ForeignKey('Yn', models.DO_NOTHING, db_column='paid_yn', related_name="paid_yn")
forms.py
class InvoiceGridEdit(ModelForm):
model = Invoice
fields = ['description','invoice_date','invoice_sent_yn', 'confirm_receipt_yn', 'paid_yn']
def __init__(self, *args, **kwargs):
super(JurisGridEditForm, self).__init__(*args, **kwargs)
...
...
InvoiceFormSet = modelformset_factory(models.Invoice, form=InvoiceGridEdit)
views.py
class InvoiceUpdateGrid(CreateView):
template_name = "/invoice_update_grid.html"
model = Invoice
form_class = InvoviceViewEditForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("account_login")
def get(self, request, *args, *kwargs):
self.object = None
customer_number = self.request.GET.get('customer_number')
invoice_form = InvoiceFormSet(queryset=Invoice.objects.filter(customer_number = customer_number)
return self.render_to_response(self.get_context_data(invoice_form=invoice_form))
def post(self, request, *args, **kwargs):
self.object = None
invoice_form = InvoiceFormSet(self.request.POST)
if (invoice_form.is_valid()):
return self.form_valid(invoice_form)
else:
return self.form_invalid(invoice_form)
def form_valid(self, invoice_form):
...
...
invoice_form.save()
return redirect("customer_list")
def form_invalid(self, invoice_form):
return self.render_to_response(self.get_context_data(invoice_form=invoice_form))
The forms works fine, get and post works, except, it takes a while (~30 sec) to retrieve and update. Using django-debug-toolbar, it looks like the yn columns retrieve separately for each column for each row (~2k rows). The Yn table only has 3 rows -1 - Unknown, 0 - No, 1 - Yes.
I tried to search for work around to stop the craziness of Django hitting the DB 900 times per retrieval. I found something about caching but I have no idea how to do it.
Thanks in advance.

getting the id from a foreignkey relationship django

I want to get the id or pk of a ForeignKey relationship post_comment but I've tried many different ways to catch it and i do not have any good result, please guys give me a hand in this situation
In views.py
class createComment(View):
form_class = CommentForm
template_name = "createComment.html"
def get(self, request):
form = self.form_class(None)
return render(request, self.template_name, {'form':form})
def post(self, request):
obj = self.form_class(None)
obj.title_comment = self.request.POST['title_comment']
obj.body_comment = self.request.POST['body_comment']
obj.post_comment = self.pk
obj.save()
In models.py
class Comment(models.Model):
user_comment = models.ForeignKey("auth.User")
title_comment = models.CharField(max_length=50)
body_comment = models.TextField()
timestamp_comment = models.DateTimeField(auto_now=True)
post_comment = models.ForeignKey("Post", null=True)
status_comment = models.BooleanField(default=True)
def __unicode__(self):
return unicode(self.title_comment)
def __str__(self):
return self.title_comment
You can pass a primary key in the url, and then use it in your class as one way.
kwargs.get(pk name)
You could change post to:
def post(self, request, *args, **kwargs)
You then can't just assign obj.post_comment = kwargs.get(pk) you have to actually get the object.
Post.objects.get(pk = pk)
You might want to also consider renaming fieldname_comment to just fieldname for your models fields. Seems a bit redundant to have _comment on every single field in the Comment model.
I don't know how works class based views but I can tell you that self.pk does not exist in class based view, you would try get form instance and get the I'd field from this instance...

Django - manage error with full_clean() validaton

I use modelformset_factory, and I use full_clean() to validate the form with unique_together=True. I wonder what is the best way to handle error in case the unique_together do not validate in order to return the error message in the template.
Please take a look to my view, and tell me if im correct the way I do it, or if there is a better approach.
model:
class Attribute(models.Model):
shapefile = models.ForeignKey(Shapefile)
name = models.CharField(max_length=255, db_index=True)
type = models.IntegerField()
width = models.IntegerField()
precision = models.IntegerField()
def __unicode__(self):
return self.name
def delete(self):
shapefile = self.shapefile
feature_selected = Feature.objectshstore.filter(shapefile=shapefile)
feature_selected.hremove('attribute_value', self.name)
super(Attribute, self).delete()
class Meta:
unique_together = (('name', 'shapefile'),)
form:
class AttributeForm(ModelForm):
def __init__(self, *args, **kwargs):
super(AttributeForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.pk:
self.fields['type'].widget.attrs['disabled'] = True
self.fields['type'].required = False
self.fields['width'].widget.attrs['readonly'] = True
self.fields['precision'].widget.attrs['readonly'] = True
def clean_type(self):
if self.instance and self.instance.pk:
return self.instance.type
else:
return self.cleaned_data['type']
type = forms.ChoiceField(choices=FIELD_TYPE)
class Meta:
model = Attribute
exclude = 'shapefile'
view:
def editFields(request, shapefile_id):
layer_selected = Shapefile.objects.get(pk=shapefile_id)
attributes_selected= Attribute.objects.filter(shapefile__pk=shapefile_id)
attributesFormset = modelformset_factory(Attribute, form=AttributeForm, extra=1, can_delete=True)
if request.POST:
formset = attributesFormset(request.POST, queryset=attributes_selected)
if formset.is_valid():
instances = formset.save(commit=False)
for instance in instances:
instance.shapefile = layer_selected
try:
instance.full_clean()
except ValidationError as e:
non_field_errors = e.message_dict[NON_FIELD_ERRORS]
print non_field_errors
formset = attributesFormset(queryset=attributes_selected)
return render_to_response("basqui/manage_layer_editFields.html", {'shapefile': layer_selected, 'formset':formset}, context_instance=RequestContext(request))
instance.save()
formset = attributesFormset(queryset=attributes_selected)
return render_to_response("basqui/manage_layer_editFields.html", {'shapefile': layer_selected, 'formset':formset}, context_instance=RequestContext(request))
The disadvantage of your approach is that you have moved the validation from the form to the view.
I had the same problem recently of validating a unique together constraint where one field is excluded from the model form. My solution was to override the model form's clean method, and query the database to check the unique together constraint. This duplicates the code that is called by full_clean, but I like it because it's explicit.
I briefly thought about overriding _get_validation_exclusions which would have been more DRY, but I decided not to rely on a private api.

Categories