Django way for dynamic forms with dependencies? - django

I'm looking for a django way to handle some complex forms with a lot of business logic. The issue is many of my forms have dependencies in them.
Some examples:
1. two "select" (choice) fields that are dependent on each other. For example consider two dropdowns one for Country and one for City.
2. A "required-if" rule, i.e set field required if something else in the form was selected. Say if the user select "Other" option in a select field, he need to add an explanation in a textarea.
3. Some way to handle date/datetime fields, i.e rules like max/min date?
What I'm doing now is implementing all of these in the form clean(), __init__(), and write some (tedious) client-side JS.
I wonder if there is a better approach? like defining these rules in a something similar to django Meta classes.

I'm going to necro this thread, because I don't see a good answer yet. If you are trying to validate a field and you want that field's validation to depend on another field in that same form, use the clean(self) method.
Here's an example: Say you have two fields, a "main_image" and "image_2". You want to make sure that if a user uploads a second image, that they also uploaded a main image as well. If they don't upload an image, the default image will be called 'default_ad.jpg'.
In forms.py:
class AdForm(forms.ModelForm):
class Meta:
model = Ad
fields = [
'title',
'main_image',
'image_2',
'item_or_model_names',
'category',
'buying_or_selling',
'condition',
'asking_price',
'location',
]
def clean(self):
# "Call the cleaned form"
cleaned_data = super().clean()
main_image = cleaned_data.get("main_image")
image_2 = cleaned_data.get("image_2")
if "default_ad" not in image_2:
# Check to see if image_2's name contains "default_ad"
if "default_ad" in main_image:
raise forms.ValidationError(
"Oops, you didn't upload a main image."
)
If you want more info, read: https://docs.djangoproject.com/en/2.2/ref/forms/validation/#cleaning-and-validating-fields-that-depend-on-each-other
Good luck!

1.This task is souly related the the html building of the form, not involving django/jinga.
2.Here, you go to dynamic forms. the best and most used way nowdays to do this, is through JS.
3.try building a DB with a "time" type field and then through "admin" watch how they handle it. all of special fields useage is covered here: https://docs.djangoproject.com/en/1.9/ref/forms/fields/

Related

Django: Check at model level if anything in ManyToMany field before saving

There's a lot of questions worded similarly, but every single one I've seen is somebody trying to get some kind of data through a ManyToMany relationship before saving it. I'm not trying to use the relationship at all before saving, I just want to see if the user put anything there or not.
My model has a ForeignKey field pointing to a parent model, and two ManyToMany fields pointing to other models, but I only want users to be able to use one M2M field or the other, not both. This model is being edited through the admin as an inline on its parent.
models.py
class ProductSpecial(models.Model):
# name, slug, image, etc
class ProductSpecialAmount(models.Model):
special = models.ForeignKey(ProductSpecial, related_name='amounts', on_delete=models.CASCADE)
amount = models.IntegerField()
brands = models.ManyToManyField(ProductBrand, related_name='specials', blank=True)
lines = models.ManyToManyField(ProductLine, related_name='specials', blank=True)
admin.py
class ProductSpecialAmountInline(admin.StackedInline):
model = ProductSpecialAmount
# fieldsets, etc
#admin.register(ProductSpecial)
class ProductSpecialAdmin(admin.ModelAdmin):
inlines = [ProductSpecialAmountInline]
# fieldsets, etc
I only want users to be able to choose from brands or lines, but not both, and I would like to validate this before save and throw a validation error if necessary. My initial attempt was to just do...
class ProductSpecialAmount(models.Model):
# ...
def clean(self):
if self.brands and self.lines:
raise ValidationError('Please choose either brands or lines, not both', code='invalid')
...but that throws ValueError: "<ProductSpecialAmount: ProductSpecialAmount object (None)>" needs to have a value for field "id" before this many-to-many relationship can be used.
I get that I can't actually query the related ProductBrand or ProductModel objects before this object is saved, but I don't actually want any data from those objects right now, I just want to know if the user left either of the fields blank or not, and am wondering if that's possible to see at the model level.
Whether you actually want to use the data from a field or just see if it is blank, the problem is caused by referencing the m2m field in any way before saving the object. I had a similar problem which I fixed using a custom form as per: https://stackoverflow.com/a/7986937/19837155
This might be more difficult when you're using inlines, but it may be the easiest way to solve your problem.

How to query the database in Django forms?

I have a model:
class Registration(models.Model):
student_name = models.CharField(max_length=50)
season = models.ForeignKey(Season, on_delete=models.CASCADE)
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
address = models.TextField()
class Meta:
unique_together = (('student_name', 'selected_season', 'selected_subject'),)
I want to create a form for this model where the user will be given the option to select the subject based on the season that they selected. I have models for them as well:
class Subject(models.Model):
subject_name = models.CharField(max_length=50)
...
class Season(models.Model):
season = models.CharField(max_length=2, primary_key=True)
subject = models.ManyToManyField(Subject)
...
I dont know how to query the Form. Should It be a ModelForm or a regular Form? How to query the database in the form?
You can only know which season was selected when the form is submitted, so there's no simple direct way to implement this (note that this is a HTTP limitation, not a django one). IOW you'll need either a "wizard" process or front-end scripting.
The "wizard" solution is: one first form where the user selects the season, user submits the form, your code selects the related subjects and displays a second form with subjects choices, user selects subjects and submits for final validation (nb: this is usually done within a single view, using a form hidden field to keep track of the current step and which season was selected in first step). This is garanteed to work (if correctly implemented of course xD), but not really user friendly.
Second solution is to use front-end scripting.
In it's simplest form, when the user selects the season, you use js to hide other seasons subjects (or you first hide all subjects and only display relevant ones when the season is selected). This can be done rather simply by grouping all subjects for a given season in a same fieldset (or whatever other container tag) with an id matching the season's one, or by having a distinct html "select" (with same name but different ids) per season. Of course you can also have all subjects in one single html select (or whatever), keep a front-side (js) mapping of seasons=>subjects, and update your select or whatever from this mapping.
This solution can be made to work (in "degraded" mode) without JS (you just have to make sure the subjects selector(s) are visible and active by default). You'll have to implement a custom validation backend-side (cf django's forms doc for this) to make sure the subject matches the season anyway (never trust front-side validation, it's only user-friendly sugar), so in the worst case the form will just not validate.
Now if you have a LOT of seasons and subjects, you may want to prefer doing an ajax query when the user selects the season and populate the subjects selector from this query's result (to avoid initially rendering a huge list of options), but then you can't make it work without JS anymore (whether this is an issue or not depends on your requirements).
EDIT
If i do follow the form wizard option, I need to create 2 forms, the first consisting of just the Season.
Well, you could do it with one single form (passing an argument to specify what should be done) but using two forms is much simpler indeed.
The 2nd form will consist of the rest of the options (except for seasons), am I right? So should the 2nd form be a modelform?
Well, that's the point of modelforms, isn't it ?
How do I put a queryset in the ModelChoiceField in modelform? I googled but could't find anything –
In your form's __init__, you update self.fields["subject"].queryset with your filtered queryset.
IMPORTANT: do NOT try to touch self.fieldname - this is the class-level field definition, so changing it would totally unexpected results in production (been here, done that, and that was quite some fun to debug xD).
Now this being said, for your use case, I reckon you'd be better with the simple js filtering solution - it's the easiest to set up - on the backend it's totally transparent, and the front-end part is rather simple using jQuery -, it's much more user-friendly, and it's garanteed to work.

Django dynamic form & ordering of fields

I have a form for logging a 'ticket' to a department.
It is a dynamic form which has additional custom fields depending on the form category/department.
Each ticket has standard fields such as title, date, content. Some have fields called custom_acbdef which allows department to ask additional questions on their forms.
These additional fields always appear at the bottom of the form which is OK at the moment. (I add the model form then just loop trough additional fields and add them to self.fields)
Now, I want to add an additional standard field called 'PDF attachment' but I want this to always appear at the bottom of the form. The problem at the moment is all standard fields appear at the top and custom fields appear at the bottom.
class Meta:
model = Ticket
fields = ('ticket_category','ticket_branch','ticket_content', 'ticket_attachment1')
So in the above, I'd want all to insert all my custom fields inbetween ticket_content and ticket_attachment. Any ideas how I can do this? All the custom form fields have dynamic field names but always start with 'custom_'
When things start to become unmanageable inside my forms __init__, I generally take one of the following approaches:
Create a Factory
Leveraging closures, write a function to build out the fields dynamically then return that class.
def TicketForm():
fields = ['title', 'date', 'content']
for custom_field in custom_fields:
fields.append(custom_field)
fields.append('ticket_content')
fields.append('ticket_attachment1')
class _TicketForm(forms.ModelForm):
class Meta:
model = Ticket
fields = fields
return _TicketForm
Multiple forms
I'll create several different forms based on the use case, then within my view determine which one should be returned. I posted an example of this yesterday.
For further reading, check out a post by James Bennett (django core dev) regarding dynamic forms.
I know it's an old post but recently has been published a
package that fulfills this purpose.
It allows to define new fields on-the-fly and populate them in forms and database in a very simple way.
Here is the link
dinadata by undersat package

Django - Customizeable UserProfile

So I've got a UserProfile in Django that has certain fields that are required by the entire project - birthday, residence, etc. - and it also contains a lot of information that doesn't actually have any importance as far as logic goes - hometown, about me, etc. I'm trying to make my project a bit more flexible and applicable to more situations than my own, and I'd like to make it so that administrators of a project instance can add any fields they like to a UserProfile without having to directly modify the model. That is, I'd like an administrator of a new instance to be able to create new attributes of a user on the fly based on their specific needs. Due to the nature of the ORM, is this possible?
Well a simple solution is to create a new model called UserAttribute that has a key and a value, and link it to the UserProfile. Then you can use it as an inline in the django-admin. This would allow you to add as many new attributes to a UserProfile as you like, all through the admin:
models.py
class UserAttribute(models.Model):
key = models.CharField(max_length=100, help_text="i.e. Age, Name etc")
value = models.TextField(max_length=1000)
profile = models.ForeignKey(UserProfile)
admin.py
class UserAttributeInline(admin.StackedInline):
model = UserAttribute
class UserProfile(admin.ModelAdmin):
inlines = [UserAttibuteInline,]
This would allow an administrator to add a long list of attributes. The limitations are that you cant's do any validation on the input(outside of making sure that it's valid text), you are also limited to attributes that can be described in plain english (i.e. you won't be able to perform much login on them) and you won't really be able to compare attributes between UserProfiles (without a lot of Database hits anyway)
You can store additional data in serialized state. This can save you some DB hits and simplify your database structure a bit. May be the best option if you plan to use the data just for display purposes.
Example implementation (not tested)::
import yaml
from django.db import models
class UserProfile(models.Model):
user = models.OneToOneField('auth.User', related_name='profile')
_additional_info = models.TextField(default="", blank=True)
#property
def additional_info(self):
return yaml.load(self._additional_info)
#additional_info.setter
def additional_info(self, user_info_dict):
self._additional_info = yaml.dump(user_info_dict)
When you assign to profile.additional_info, say, a dictionary, it gets serialized and stored in _additional_info instead (don't forget to save the instance later). And then, when you access additional_info, you get that python dictionary.
I guess, you can also write a custom field to deal with this.
UPDATE (based on your comment):
So it appears that the actual problem here is how to automatically create and validate forms for user profiles. (It remains regardless on whether you go with serialized options or complex data structure.)
And since you can create dynamic forms without much trouble[1], then the main question is how to validate them.
Thinking about it... Administrator will have to specify validators (or field type) for each custom field anyway, right? So you'll need some kind of a configuration option—say,
CUSTOM_PROFILE_FIELDS = (
{
'name': 'user_ip',
'validators': ['django.core.validators.validate_ipv4_address'],
},
)
And then, when you're initializing the form, you define fields with their validators according to this setting.
[1] See also this post by Jacob Kaplan-Moss on dynamic form generation. It doesn't deal with validation, though.

Limiting Django ModelChoiceField queryset to selected items

Here is what I've been struggling for a day...
I have a Message model in which recipients is a ManyToManyField to the User model.
Then there is a form for composing messages. As there are thousands of users, it is not convenient to display the options in a multiple select widget in the form, which is the default behavior. Instead, using FcbkComplete jquery plugin, I made the recipients field look like an input field where the user types the recipients, and it WORKS.
But...
Although not visible on the form page, all the user list is rendered into the page in the select field, which is something I don't want for obvious reasons.
I tried overriding the ModelChoiceField's behavior manipulating validation and queryset, I played with the MultipleChoice widget, etc. But none of them worked and felt natural.
So, what is the (best) way to avoid having the whole list of options on the client side, but still be able to validate against a queryset?
Have you seen django-ajax-selects? I've never used it, but it's in my mental grab bag for when I come across a problem like what it sounds like you're trying to solve...
I would be trying one of two ways (both of which might be bad! I'm really just thinking out aloud here):
Setting the field's queryset to be empty (queryset = Model.objects.none()) and having the jquery tool use ajax views for selecting/searching users. Use a clean_field function to manually validate the users are valid.
This would be my preferred choice: edit the template to not loop through the field's queryset - so the html would have 0 options inside the select tags. That is, not using form.as_p() method or anything.
One thing I'm not sure about is whether #2 would still hit the database, pulling out the 5k+ objects, just not displaying them in the html. I don't think it should, but... not sure, at all!
If you don't care about suggestions, and is OK to use the ID, Django Admin comes with a raw_id_field attribute for these situations.
You could also make a widget, that uses the username instead of the ID and returns a valid user. Something among the lines of:
# I haven't tested this code. It's just for illustration purposes
class RawUsernameField(forms.CharField):
def clean(self, value):
try:
return User.objects.get(username=value)
except User.DoesNotExist:
rause forms.ValidationError(u'Invalid Username')
I solve this by overriding the forms.ModelMultipleChoiceField's default widget. The new widget returns only the selected fields, not the entire list of options:
class SelectMultipleUserWidget(forms.SelectMultiple):
def render_options(self, choices, selected_choices):
choices = [c for c in self.choices if str(c[0]) in selected_choices]
self.choices = choices
return super(SelectMultipleUserWidget,
self).render_options([], selected_choices)
class ComposeForm(forms.Form):
recipients = forms.ModelMultipleChoiceField(queryset=User.objects.all(),
widget=SelectMultipleUserWidget)
...