I am wondering how to specify some constraints on intermediate model formset.
I have 3 classes in model:
Attribute, Product and AttributeValuation, which is intermediate for Attribute and Product:
class Attribute(models.Model):
type = models.CharField(max_length = 200)
pass
class Product(models.Model):
attribute_values = models.ManyToManyField(Attribute, through='AttributeValuation')
class AttributeValuation(models.Model):
attribute = models.ForeignKey(Attribute)
product = models.ForeignKey(Product)
On top of that, I have built AttributeValuationInline with AttributeFormset, and registered it to ProductAdmin:
class AttributeValuationInline(admin.TabularInline):
model = AttributeValuation
extra = 0
formset = AttributeFormset
class ProductAdmin(admin.ModelAdmin):
inlines = (AttributeValuationInline,)
class AttributeFormset(BaseInlineFormSet):
def clean(self):
pass
My question is: how can I check in the clean method the contents of each inline row (form)? I've tried through each form of self.forms in Formset, but I could not access the specific fields of Attribute model (imagine that there are some, I don't want to obfuscate my question with irrelevant data)?
In my example, I would like to have maximum of one Attribute of each type per Product (so that no one puts two or more attributes with the same type associated with one Product).
self.forms[0].cleaned_data
wont work?
I went through
for form in self.forms:
form.instance
And it's ok. Why should cleaned_data be better?
Related
I have a (horrible) database table that will be imported from a huge spreadsheet. The data in the fields is for human consumption and is full of "special cases" so its all stored as text. Going forwards, I'd like to impose a bit of discipline on what users are allowed to put into some of the fields. It's easy enough with custom form validators in most cases.
However, there are a couple of fields for which the human interface ought to be a ChoiceField. Can I override the default form field type (CharField)? (To clarify, the model field is not and cannot be constrained by choices, because the historical data must be stored. I only want to constrain future additions to the table through the create view).
class HorribleTable( models.Model):
...
foo = models.CharField( max_length=16, blank=True, ... )
...
class AddHorribleTableEntryForm( models.Model)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
FOO_CHOICES = (('square', 'Square'), ('rect', 'Rectangular'), ('circle', 'Circular') )
...?
Perhaps you could render the forms manually, passing the options through the context and make the fields in html.
Take a look at here:https://docs.djangoproject.com/en/4.0/topics/forms/#rendering-fields-manually
I think you can easily set your custom form field as long it will match the data type with the one set in your model (e.g. do not set choices longer than max_length of CharField etc.). Do the following where foo is the same name of the field in your model:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
...
I think this is perfectly fine for a creation form. It's will not work for updates as the values in the DB will most probably not match your choices. For that, I suggest adding a second form handling data updates (maybe with custom permission to restrict it).
UPDATE
Another approach will be to override the forms init method. That way you can handle both actions (create and update) within the same form. Let the user select from a choice field when creating an object. And display as a normal model field for existing objects:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get("instance", None)
if instance is None:
self.fields["foo"].widget = forms.widgets.Select(choices=self.FOO_CHOICES)
I have two models, Tag and TagGroup.
class TagGroup(models.Model):
tag_group_name = models.CharField(max_length=100)
class Tag(models.Model):
tag_name = models.CharField(max_length=100)
tag_group = models.ForeignKey(TagGroup, blank=True, null=True)
I've put a TagGroup form as a choice field into a template so that I can assign a TagGroup to a Tag. I created this form to populate from a TagGroup queryset.
class TagGroupForm(ModelForm):
tag_group_name = forms.ModelChoiceField(queryset=TagGroup.objects.values_list('id', 'tag_group_name'), required=False)
class Meta:
model = TagGroup
fields = [
'tag_group_name'
]
I haven't seen any obvious instructions how I can assign the Id to the Tag table while showing the user only the Tag value in the choice field in the template.
Currently the above shows:
Couple of questions:
is the queryset correct? I have tried without "values_list" but it then just shows an "Object" in the form field in template?
how do i 'hide' the Id so i can save on it, while only showing the user the actual string value in the form field?
Edited to add updated form:
class TagGroupForm(ModelForm):
tag_group_name = forms.ModelChoiceField(queryset=TagGroup.objects.all(), to_field_name = 'tag_group_name', required=False)
class Meta:
model = TagGroup
fields = [
'tag_group_name'
]
this now produces the following .. looks close .. the form value has a nice string but the actual value displayed to user is still "TagGroup object". How to get this to show?
From the docs,
The str (unicode on Python 2) method of the model will be called to generate string representations of the objects for use
So simply just assign this to the objects name and all will be ok! (Also, you don't need to use values_list) The reason it shows the Object by default is because this is what the default string representation is.
class TagGroup(models.Model):
tag_group_name = models.CharField(max_length=100)
def __str__(self):
return self.tag_group_name
tag_group_name = forms.ModelChoiceField(queryset=TagGroup.objects.all(), required=False)
Alternatively, if you don't wish to modify this and wish to reserve it for other uses.
To provide customized representations, subclass ModelChoiceField and override label_from_instance. This method will receive a model object, and should return a string suitable for representing it
class TagChoiceField(ModelChoiceField):
queryset = TagGroup.objects.all()
def label_from_instance(self, obj):
return obj.tag_group_name # or similar
I'm struggling to figure out how best to approach this.
I have two models that need to be represented on one page within a form wizard:
class BookingItem(models.Model):
assignedChildren = models.ManyToManyField('PlatformUserChildren', related_name = "childIDs", null=True)
quantity = models.PositiveSmallIntegerField(max_length=2,blank=True, null=True)
class PlatformUserChildren(models.Model):
child_firstname = models.CharField('Childs first name', max_length=30,blank=True, null=True)
The relationship between the models when presenting them on the page is NOT one-to-one. It is governed by quantity attribute and therefore there may be more BookingItems than PlatformUserChildren objects presented on the page (e.g. 4 BookingItems and 2 PlatformUserChildren objects). I loop through each object multiple times based on quantity.
I also need to bind to a queryset of the PlatformChildUser model based on the current logged in user.
My question: how do I best present these two models on the first page of my form wizard?
I have looked at inline_formsets, but they rely on foreign key relationships only.
I have tried a modelformset_factory for one model, and an additional identifier for the other model with some backend reconciliation later, but I'm stuck on how to get a queryset based on user in there
i have attempted the get_form_instance method but I'm not 100% sure if it supports querysets
finally, I have attempted overloading init however most of the examples are not based on form wizard, and supply external arguments.
My current (rather vanilla) code is below:
forms.py
class checkout_PlatformUserChildren(forms.ModelForm):
#activity_id = forms.IntegerField()
class Meta:
model = PlatformUserChildren
fields = ('child_age','child_firstname')
class Meta:
model = PlatformUserChildren
fields = ('child_age','child_firstname')
widgets = {
'child_firstname': SelectMultiple(attrs={'class': 'form-control',}),
'child_age' : TextInput(attrs={'class': 'form-control',}),
}
checkout_PlatformUserChildrenFormSet = modelformset_factory(
PlatformUserChildren,
form = checkout_PlatformUserChildren,
fields=('child_firstname', 'child_age'),
extra=1, max_num=5, can_delete=True)
views.py (done method not shown)
note: getUser is an external function that is currently working
checkoutForms = [
("assign_child", checkout_PlatformUserChildrenFormSet),
("address_information", addressInfo),
]
checkoutTemplates = {
"assign_child": "checkout/assign_child.html",
"address_information": "checkout/address_information.html",
}
class checkout(SessionWizardView):
def get_form_instance(self, step):
currentUser = getUser(self.request.user.id)
if step == 'assign_child':
self.instance = currentUser
return self.instance
I'm trying to make a form to edit the value of a ManyToMany field from its parent model. As an example, I have something similar to these three models:
class Language(models.Model):
label = models.CharField()
class Word(models.Model):
language = models.ForeignKey(Language)
word = models.CharField()
entries = models.ManyToManyField(Entries, null=True, blank=True)
class Entries(models.Model):
entry = models.CharField()
Each Language will have about 50 words. Each Word will have one or two entries each.
I'm generating the formset to edit the entries for a given language like this:
class WordForm(forms.ModelForm):
class Meta:
model = Word
hidden = ('language', )
PronounFormSet = inlineformset_factory(Language, Word,
can_delete=False, extra=0, form=WordForm)
This gives me a <select> which allows me to add/remove an Entry into Word.entries. However, I want to allow the user to edit the field Entries.entry directly (i.e. the field Entries.entry should be in a CharField(). How do I modify my WordForm to allow this?
I know there are probably better ways to do this (e.g. a different database schema), but I'm heavily constrained by a legacy implementation.
If you want the entry to be a text field, you can add a custom field to your ModelForm, and make sure the default field is not shown by explicitly identifying which fields should be shown:
class WordForm(forms.ModelForm):
class Meta:
model = Word
hidden = ('language', )
fields = ('word',)
entries = forms.CharField()
Your form validation logic should be like this:
for form in formset:
obj = form.save(commit=False)
obj.language = # select the language here
obj.save()
entry = Entries.objects.get_or_create(entry=form.cleaned_fields['entries'])
obj.entries.add(entry)
obj.save()
Keep in mind with this implementation, you can't edit fields using this form (since the character field will always be empty when the form is rendered).
I'm trying to dynamically generate a new Model, based on fields from an existing Model. Both are defined in /apps/main/models.py. The existing model looks something like this:
from django.db import models
class People(models.Model):
name = models.CharField(max_length=32)
age = models.IntegerField()
height = models.IntegerField()
I have a list containing the names of fields that I would like to copy:
target_fields = ["name", "age"]
I want to generate a new model the has all of the Fields named in target_fields, but in this case they should be indexed (db_index = True).
I originally hoped that I would just be able to iterate over the class properties of People and use copy.copy to copy the field descriptions that are defined on it. Like this:
from copy import copy
d = {}
for field_name in target_fields:
old_field = getattr(People, field_name) # alas, AttributeError
new_field = copy(old_field)
new_field.db_index = True
d[field_name] = new_field
IndexedPeople = type("IndexedPeople", (models.Model,), d)
I wasn't sure if copy.copy()ing Fields would work, but I didn't get far enough to find out: the fields listed in the class definition don't aren't actually included as properties on the class object. I assume they're used for some metaclass shenanigans instead.
After poking around in the debugger, I found some type of Field objects listed in People._meta.local_fields. However, these aren't just simple description that can be copy.copy()ed and used to describe another model. For example, they include a .model property referring to People.
How can I create a field description for a new model based on a field of an existing model?
From poking around in the debugger and the source: all Django models use the ModelBase metaclass defined in /db/models/base.py. For each field in a model's class definition, ModelBase's .add_to_class method will call the field's .contribute_to_class method.
Field.contribute_to_class is defined in /db/models/fields/__init__.py and it is what's responsible for associating a field definition with a particular model. The field is modified by adding the .model property and by calling the .set_attributes_from_name method with the name used in the model's class definition. This in turn adds adds the .attname and .column properties and sets .name and .verbose_name if necessary.
When I inspect the __dict__ property of a newly-defined CharField and compare it with that of a CharField that was already associated with a model, I also see that these are the only differences:
The .creation_counter property is unique for each instance.
The .attrname, .column and .model properties do not exist on the new instance.
The .name and .verbose_name properties is None on the new instance.
It doesn't seem possible to distinguish between .name/.verbose_name properties that were manually specified to the constructor and ones that were automatically generated. You'll need to chose either to always reset them, ignoring any manually-specified values, or never clear them, which would cause them to always ignore any new name they were given in the new model. I want to use the same name as the original fields, so I am not going to touch them.
Knowing what differences exist, I am using copy.copy() to clone the existing instance, then apply these changes to make it behave like a new instance.
import copy
from django.db import models
def copy_field(f):
fp = copy.copy(f)
fp.creation_counter = models.Field.creation_counter
models.Field.creation_counter += 1
if hasattr(f, "model"):
del fp.attname
del fp.column
del fp.model
# you may set .name and .verbose_name to None here
return fp
Given this function, I create the new Model with the following:
target_field_name = "name"
target_field = People._meta.get_field_by_name(target_field_name)[0]
model_fields = {}
model_fields["value"] = copy_field(target_field)
model_fields["value"].db_index = True
model_fields["__module__"] = People.__module__
NewModel = type("People_index_" + field_name, (models.Model,), model_fields)
It works!
Solution
There is build in way for fields copying Field.clone() - method which deconstructs field removing any model dependent references:
def clone(self):
"""
Uses deconstruct() to clone a new copy of this Field.
Will not preserve any class attachments/attribute names.
"""
name, path, args, kwargs = self.deconstruct()
return self.__class__(*args, **kwargs)
So you can use following util to copy fields ensuring that you'll not accidentally affect source fields of model you're copying from:
def get_field(model, name, **kwargs):
field = model._meta.get_field(name)
field_copy = field.clone()
field_copy.__dict__.update(kwargs)
return field_copy
Also can pass some regular kwargs like verbose_name and etc:
def get_field_as_nullable(*args, **kwargs):
return get_field(*args, null=True, blank=True, **kwargs)
Does not work for m2m fields inside of model definition. (m2m.clone() on model definition raises AppRegistryNotReady: Models aren't loaded yet)
Why this instead of abstract models?
Well, depends on case. Some times you don't need inheristance but actuall fields copying. When? For example:
I have a User model and model which represents an application (document for user data update request) for user data update:
class User(models.Model):
first_name = ...
last_name = ...
email = ...
phone_number = ...
birth_address = ...
sex = ...
age = ...
representative = ...
identity_document = ...
class UserDataUpdateApplication(models.Model):
# This application must ONLY update these fields.
# These fiends must be absolute copies from User model fields.
user_first_name = ...
user_last_name = ...
user_email = ...
user_phone_number = ...
So, i shouldn't carry out duplicated fields from my User model to abstract class due to the fact that some other non-user-logic-extending model wants to have exact same fields. Why? Because it's not directly related to User model - User model shouldn't care what depends on it (excluding cases when you want to extend User model), so it shouldn't be separated due to fact that some other model with it's own non User related logic want's to have exact same fields.
Instead you can do this:
class UserDataUpdateApplication(models.Model):
# This application must ONLY update these fields.
user_first_name = get_field(User, 'first_name')
user_last_name = get_field(User, 'last_name')
user_email = get_field(User, 'user_email')
user_phone_number = get_field(User, 'phone_number')
You also would make som util which would generate some abc class "on fly" to avoid code duplication:
class UserDataUpdateApplication(
generate_abc_for_model(
User,
fields=['first_name', 'last_name', 'email', 'phone_number'],
prefix_fields_with='user_'),
models.Model,
):
pass