Creating a Django form from a dictionary of objects - python

I feel like this must be really simple, but after a couple of days of trying I'm officially clueless.
I have a dictionary where the keys are objects and the values are lists of objects. Here's how I want to use that info to construct a form:
for object in dictionary:
name_of_field = object.slug
name_of_field = forms.ModelMultipleChoiceField(widgets=forms.CheckboxSelectMultiple, queryset=dictionary[object])
Of course, just putting name_of_field in there twice doesn't work to generate dynamically named fields. What this actually does is create a single field called "name_of_field" using the final object it iterates over. I wish it would create a field for every key in the dictionary, named using the key object's slug and with a choice set of that key's values.
Is there a way to loop through this dictionary and create the form fields I want? I feel like the answer lies in superclassing __init__, but I still can't wrap my head around how to get multiple fields with different names.

You don't say where you are using this code. You should be putting it into the form's __init__ method, from where you can reference self.fields:
class DynamicForm(forms.Form):
def __init__(self, *args, **kwargs):
dynamic_fields = kwargs.pop('dynamic_fields')
super(DynamicForm, self).__init__(*args, **kwargs)
for key, value in dynamic_fields:
self.fields[key.slug] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=value)

I feel like I want to contribute to this question although it is really old, because I could not solve my question with the answer alone.
For a given model with the form:
class Product(models.Model):
data = models.JSONField()
store = models.ForeignKey(Store, on_delete = models.CASCADE)
number = models.PositiveIntegerField()
a dynamically created form can be created (careful, this is a simplified case, where all products of the same store share the same keys in the JSONField):
class ProductForm(forms.Form):
def __init__(self, first_product, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["number"] = forms.IntegerField(required = True)
for key in first_product.data.keys():
self.fields[key] = forms.CharField(required = False)
class Meta:
model = Product
fields = ["number", "data"]
The form must be called by handing it the first_product variable in the view:
class SomeView(TemplateView):
template_name = "appName/sometemplatename.html"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
store = Store.objects.get(user = self.request.user)
tmp = Product.objects.filter(store = store).first()
context["testform"] = ProductForm(first_product = tmp)
return context
This is a vastly general approach to start from.

Related

Django : several models returns in queryset

Yeah I know, it's not possible. Or maybe I didn't see.
But, I'm gonna explain why I need this. Let's do some dummy classes:
class A(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
lvl_struct = GenericForeignKey('content_type', 'object_id')
Let's say A can be attached to a struct (logical struct, like department in jobs). An A instance can be attached to only one struct, but there're 4 disctinct type of struct, and that's where I discovered generic foreign key (instead of a polymorphism on A).
But now, my problem is, in my form, I want to attach the actual struct when I create a A instance :
class CreateAForm(forms.ModelForm)
lvl_struct = forms.ModelChoiceField(
queryset=None, #here is my problem
required=True
)
So here I would like to have a unique select with all possibilities (all instances of first struct type, all instances of second struct type and so on).
So is there any way to do this, or will I have to do like four select with some js to check at least one and only one select has a value?
Or of course a third solution which I didn't see. Tell me.
Thank you in advance for your time.
For this task I am using ChoiceField instead of ModelChoiceField. Convert all the querysets of your objects into combined list inside of form init, then create list with display names and assign it to choices like:
class MyForm(forms.ModelForm):
content_object = forms.ChoiceField(label=_('Object'), widget=forms.Select(required=True)
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
# combine object_type and object_id into a single 'generic_obj' field
# getall the objects that we want the user to be able to choose from
available_objects = list(ModelOne.objects.filter(...))
available_objects += list(ModelTwo.objects.filter(...))
available_objects += list(ModelThree.objects.filter(...))
# now create our list of choices for the <select> field
object_choices = []
for obj in available_objects:
type_id = ContentType.objects.get_for_model(obj.__class__).id
obj_id = obj.id
form_value = "type:%s-id:%s" % (type_id, obj_id) # e.g."type:12-id:3"
display_text = str(obj)
object_choices.append([form_value, display_text])
self.fields['content_object'].choices = object_choices
When you save the form you have to decode the string to get chosen content_type and object_id. After that you can assign them back to instance. You can do it with:
from django.contrib.contenttypes.models import ContentType
def save(self, *args, **kwargs):
# get object_type and object_id values from combined generic_obj field
object_string = self.cleaned_data['content_object']
matches = re.match("type:(\d+)-id:(\d+)", object_string).groups()
object_type_id = matches[0] # get 45 from "type:45-id:38"
object_id = matches[1] # get 38 from "type:45-id:38"
object_type = ContentType.objects.get(id=object_type_id)
self.instance.object_id = object_id
self.instance.content_type = object_type
super(MyForm, self).save(*args, **kwargs)

How to create a dynamic Django choice field

I am trying to generate a list to populate choices for a form. This list will be dynamic and will depend on what choices have been created by other users. Here is an example of the model the list should derive from:
#models.py
class User(models.Model):
brewery_you_work_for = models.CharField(choises=ALL_THE_BREWERIES_IN_THE_WORLD)
username = models.CharField()
I want the form to be something like this:
#forms.py
class BestBrewery(forms.Form):
vote = forms.ChoiceField(choices=BREWERIES_A_USER_WORKS_FOR)
What I want to do is have a list of all the breweries in the world for some users to select as the brewery they work for. Then I want to generate a list for other users to vote as the best brewery.
Lets say I have 3 users that claim they work the Sierra Nevada, 2 users that claim they work for Budweiser, and 5 users that claim they work for Coors.
I want to generate a list of breweries that would look like this:
(
'Budweiser',
'Coors,
'Sierra Nevada',
)
Note the alphabetical order and no repeated brewery listings.
You need to pass an extra user argument when initialising the form. Normally like this if you are using view functions:
form = BestBrewery(request.user)
If you are using Class Based Views you should override the get_form_kwargs method in your view:
def get_form_kwargs(self):
kwargs = super(MyView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
Your form should look like this:
class BestBrewery(forms.Form):
def __init__(self, user, *args, **kwargs):
super(BestBrewery, self).__init__(*args, **kwargs)
self.fields['vote'].choices = (
user.brewery_you_work_for,
user.get_brewery_you_work_for_display()
)
Just note that to override the choices field you need to provide tuples containing the value for the form and the text to be shown in the dropdown.
Well, I think u can set "choices" as result of function.
I tried like that:
def yolo():
# there we will get stats from db and return choices depends on stats
first_query = 1 # there U will use more complicated logic :)
second_query = 3
if first_query > second_query:
return (1, 2, 3)
else:
return (1, 3)
class Event(models.Model):
date = models.DateField(primary_key=True)
count_of_updates = models.SmallIntegerField(default=0)
yolos = models.CharField(max_length=255, choices=yolo())
And, for being sure U can check ur choices with something ugly:
choices = s._meta.get_field_by_name('yolos')[0].choices
print choices
Order and other things u can set in func logic :)
In your Form, you can change the choices of your field in the init.
class BestBrewery(forms.Form):
vote = forms.ChoiceField()
def __init__(self, *args, **kwargs):
super(BestBrewery, self).__init__(*arg, **kwargs)
# get the choices from where you need
choices = (...)
self.fields['vote'].choices = choices

Changing the choices in Django Model according to a field value

If for example, I have a class Summary,
each Instance of the class Summary, can be one subject:
subjects = (
('english', 'אנגלית'),
('bible', 'תנ"ך'),
('history', 'היסטוריה'),
('civics', 'אזרחות'),
('language', 'לשון'),
('literature', 'ספרות'),
)
class Summary(models.Model):
...
subject = models.CharField(max_length=20, choices=subjects)
...
Now I've decided I want to hardcode some topics for each subject, so if Summary.subject = "literature" I want to add a field
subtopic = models.CharField(choices=literature_subtopics)
and make the choices equal to:
literature_subtopics = (
('poems', 'שירה'),
('short_stories', 'סיפורים קצרים'),
('plays', 'מחזות'),
('novels', 'נובלות'),
)
If the subject was English then english_subtopics would be used for the choices field.
I want to hard-code all these divisions because they will not change more than once every few years if at all, storing them in a database makes zero sense.
I need to somehow set up all these divisions for each subject, and make Django set the choices field for the subtopic appropriately.
can I override the init method to accomplish this somehow? I heard that's a bad idea and can break things.
Even if the data doesn't change often, it seems most natural to put data in the database and Python in Python files. Your proposed solution seems like you're fighting the way Django wants to do things.
What do think of a database solution?
class Subject(models.Model):
parent = models.ForeignKey("self")
name = models.CharField(max_length=100)
hebrew_name = models.CharField(max_length=100)
class Summary(models.Model):
...
subject = models.ForeignKey("Subject")
...
class SubjectForm(forms.Form):
subject = forms.ModelChoiceField(queryset=Subject.objects.none())
...
def __init__(self, *args, **kwargs): # see http://stackoverflow.com/a/4880869/1477364
sub = kwargs.pop('parent_subject')
super(SubjectForm, self).__init__(*args, **kwargs)
parent_subject = Subject.objects.get(name=sub)
sub_subjects = Subject.objects.filter(parent=parent_subject)
self.fields['subject'].queryset = forms.ModelChoiceField(queryset=sub_subjects)
Note that the code implies there will always be a parent Subject passed to SubjectForm. (You'll need a "root" Subject.)
You could set the choices dynamically using a function.
# models.py
def get_choices():
choices_tuple_list = []
... # your logic to define choices
return choices_tuple_list
class Summary(models.Model):
... # Summary definition
subject = models.CharField(max_length=20, blank=True)
def __init__(self, *args, **kwargs):
super(Summary, self).__init__(*args, **kwargs)
self._meta.get_field_by_name('subject')[0]._choices = get_choices()
See docs

Over-Ride __init__: Extending Forms (Python/Django)

Struggling to figure out how to Over-Ride the __init__() method in my Django Form to include additional values from the database. I have a group of photographers that I am trying to list as a form option for the user. Afterwards, the user's photographer selection will be added (along with other information) to the database as an instantiation of a new model.
This is a continuation, or elaboration, of my other Current Question. #Rob Osborne has given me some great advice helping me understand how to extend BaseForm, but I still cannot get my code to execute. The linked question lists my models, form, and views, if you are interested. While I understand that using ModelForm is easier and more documented, I must use BaseForm in this instance.
Here is what I have:
class AForm(BaseForm):
def __init__(self, data=None, files=None, instance=None, auto_id='id_%s',
prefix=None, initial=None, error_class=ErrorList,
label_suffix=':', empty_permitted=False):
self.instance = instance
object_data = self.instance.fields_dict()
self.declared_fields = SortedDict()
self.base_fields = fields_for_a(self.instance)
BaseForm.__init__(self, data, files, auto_id, prefix, object_data,
error_class, label_suffix, empty_permitted)
self.fields['photographer'].queryset = Photographer.objects.all()
def save(self, commit=True):
if not commit:
raise NotImplementedError("AForm.save must commit it's changes.")
if self.errors:
raise ValueError(_(u"The Form could not be updated because the data didn't validate."))
cleaned_data = self.cleaned_data
# save fieldvalues for self.instance
fields = field_list(self.instance)
for field in fields:
if field.enable_wysiwyg:
value = unicode(strip(cleaned_data[field.name]))
else:
value = unicode(cleaned_data[field.name])
Using the above code results in a KeyError at 'photographer'.
I appreciate any ideas / comments on how to resolve this KeyError so that I can get the photographer values into my form. Thank you!
EDIT:
Trying to use super, as recommended by #supervacuo, but still getting a KeyError at photographer as before:
class AForm(BaseForm):
def __init__(self, data=None, files=None, instance=None, auto_id='id_%s',
prefix=None, initial=None, error_class=ErrorList,
label_suffix=':', empty_permitted=False):
super(AForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted)
self.fields['photographer'].queryset = Photographer.objects.all()
What could I be missing that is generating the KeyError? Thanks for any advice.
EDIT 2: adding fields_dict()
from models.py
class A(models.Model):
category = models.ForeignKey(Category)
user = models.ForeignKey(User)
def fields_dict(self):
fields_dict = {}
fields_dict['title'] = self.title
for key, value in self.fields():
fields_dict[key.name] = value.value
return fields_dict
Thanks for any advice.
EDIT 3: (edited class AForm above in the initial question as well, to include more information)
def fields_for_a(instance):
fields_dict = SortedDict()
fields = field_list(instance)
for field in fields:
if field.field_type == Field.BOOLEAN_FIELD:
fields_dict[field.name] = forms.BooleanField(label=field.label, required=False, help_text=field.help_text)
elif field.field_type == Field.CHAR_FIELD:
widget = forms.TextInput
fields_dict[field.name] = forms.CharField(label=field.label, required=field.required, max_length=field.max_length, help_text=field.help_text, widget=widget)
fields_dict[field.name] = field_type(label=field.label,
required=field.required,
help_text=field.help_text,
max_length=field.max_length,
widget=widget)
return fields_dict
EDIT 4: def fields(self). from models.py:
def fields(self):
fields_list = []
fields = list(self.category.field_set.all())
fields += list(Field.objects.filter(category=None))
for field in fields:
try:
fields_list.append((field, field.fieldvalue_set.get(ad=self),))
except FieldValue.DoesNotExist:
pass # If no value is associated with that field, skip it.
return fields_list
def field(self, name):
if name == 'title':
return self.title
else:
return FieldValue.objects.get(field__name=name, ad=self).value
That GitHub link should've been the first thing in your question.
The django-classifieds application has an entire system of dynamic fields (based on the Field and FieldValue models) which is why you're having trouble. If you don't fully understand this aspect of django-classifieds, I recommend you base your project on something else instead.
Looking down the list of FIELD_CHOICES in django-classified's models.py, you can't use this database-driven field system to define relationsips — so there's no dynamic per-category ForeignKey field!
The alternative would be to add a photographer field on your A model (any particular reason you've renamed it from Ad?), as it seems you have done based on your other question. To go the rest of the distance, however, you'd need to edit the fields_dict() method like so:
def fields_dict(self):
fields_dict = {}
fields_dict['title'] = self.title
fields_dict['photographer'] = self.photographer
for key, value in self.fields():
fields_dict[key.name] = value.value
return fields_dict
Your call to BaseForm.__init__ seems wrong; you should be using super(), like so
class AForm(BaseForm):
def __init__(self, *args, **kwargs):
super(AForm, self).__init__(*args, **kwargs)
self.fields['photographer'].queryset = Photographer.objects.all()
(as actually recommended in Rob Osbourne's accepted answer to your other question).
Beyond that, I am suspicious of your fields_dict() method, which isn't part of Django and you haven't provided the definition for. Confirm with print self.fields.keys() that, for whatever mysterious reason, photographer is not there, then post the code for fields_dict().

Auto-generate form fields for a Form in django

I have some models and I want to generate a multi-selection form from this data.
So the form would contain an entry for each category and the choices would be the skills in that category.
models.py
class SkillCategory(models.Model):
name = models.CharField(max_length=50)
class Skill(models.Model):
name = models.CharField(max_length=50)
category = models.ForeignKey(SkillCategory)
Is there a way to auto-generate the form fields?
I know I can manually add a 'SkillCategory' entry in the form for each SkillCategory, but the reason to have it as a model is so skills and skillcategories can be edited freely.
I want to do something like this:
(I tried this, but didn't get it to work, don't remember the exact error...)
forms.py
class SkillSelectionForm(forms.Form):
def __init__(*args, **kwargs):
super(SkillSelectionForm, self).__init__(*args, **kwargs)
for c in SkillCategory.objects.all():
category_skills = [(pk, s.name) for s in c.skill_set.all()]
setattr(self, c.name, forms.MultipleChoiceField(choices=category_skills, widget=forms.CheckboxSelectMultiple))
SOLUTION
This creates a form field entry using the SkillCategory.name and assigns choices as those in Skill. field_name/display_name are used to avoid issues with non-ascii category names.
forms.py
def get_categorized_skills():
skills = {}
for s in Skill.objects.values('pk', 'name', 'category__name').order_by('category__name'):
if s['category__name'] not in skills.keys():
skills[s['category__name']] = []
skills[s['category__name']].append((s['pk'], s['name']))
return skills
class SkillSelectionForm(forms.Form):
def __init__(self, *args, **kwargs):
super(SkillSelectionForm, self).__init__(*args, **kwargs)
skills = get_categorized_skills()
for idx, cat in enumerate(skills.keys()):
field_name = u'category-{0}'.format(idx)
display_name = cat
self.fields[field_name] = forms.MultipleChoiceField(choices=skills[cat], widget=forms.CheckboxSelectMultiple, label=display_name)
Okay so you can't set fields like that on forms.Form, for reasons which will become apparent when you see DeclarativeFieldsMetaclass, the metaclass of forms.Form (but not of forms.BaseForm). A solution which may be overkill in your case but an example of how dynamic form construction can be done, is something like this:
base_fields = [
forms.MultipleChoiceField(choices=[
(pk, s.name) for s in c.skill_set.all()
]) for c in SkillCategory.objects.all()
]
SkillSelectionForm = type('SkillSelectionForm', (forms.BaseForm,), {'base_fields': base_fields})
Take a look at creating dynamic forms in Django, from b-list.org and uswaretech.com. I've had success using these examples to dynamically create form content from models.
What you want is a Formset. This will give you a set of rows, each of which maps to a specific Skill.
See the Formset documentation and the page specifically on generating formsets for models.

Categories