Django choice field populated by queryset - save id but show value - django

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

Related

Display a calculated field value in a ModelForm

I have an object Performance model in my Django project.
class Performance(models.Model):
performance_id = models.AutoField(primary_key=True)
name = models.CharField(_('Nom'), max_length=64)
code = models.CharField(_('Code'), max_length=16, db_index=True, default='')
type = models.InterField(...)
value = models.CharField(...)
In the Admin interface, I have a dedicated PerformanceInlineAdmin and a PerformanceInlineForm class.
In a Performance object, if the value field starts with "$" then the field contains kind of Reverse Polish Notation expression (such as "100 450 +"...).
In this case, the value displayed shall be the calculated expression instead of the field plaintext value.
But I still haven't found a reliable solution. Not even sure that it should be handled in the Form object.
Any suggestion is welcome.
Z.
You can define a method inside your InlineAdmin class that returns a read-only field:
class PerformanceInlineAdmin(admin.TabularInline):
model = Performance
fields = ['name', 'code', 'type', 'calculated_value']
readonly_fields = ['calculated_value', ]
def calculated_value(self, instance):
if instance.value.startswith("$"):
return polish_calc(instance.value.strip('$')
else:
return instance.value
calculated_value.short_description = "Calculated value"
Note that if you define a calculated_value() method on Performance itself, you can skip the whole method in the inline admin class and just declare it in readonly_fields.

Model form - How to change default ManyToMany widget?

I use Django Model Form:
class Fruit(models.Model):
name = models.CharField(max_length=40)
class Box(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=199)
fruit = models.ManyToManyField(Fruit)
and forms.py:
class BoxModelForm(ModelForm):
class Meta:
model = Box
I have default django ManyToMany widget in form:
http://nov.imghost.us/ly5M.png
How can I change this to input (text type) and if I type into this input:
apple,banana,lemon - comma separated
this Fruit will be created?
As stated here in the documentation :https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#overriding-the-default-fields
You can add a widgets attribute to the Meta of your Modelform to change the default widgets used in the form.
In your case it would be something like this :
class BoxModelForm(ModelForm):
class Meta:
model = Box
widgets = {
'fruit': TheWidgetYouWantToUse(),
}
But actually for the behavior you want to achieve, you could proceed another way.
You should add an extra text field, and write the addition/removal of fruits in the save step, while checking the validity of the differents tags in the clean step.
class BoxModelForm(ModelForm):
fruit_selector = forms.TextField(
max_length=255,
tag = 'Whatever'
)
class Meta:
model = Box
fields = ['user','name']
def clean_fruit_selector(self):
data = self.cleaned_data['fruit_selector']
# Check that data are corrects ie the string is correctly formatted
# If not raise validation error
....
fruit_tags = data.split(",")
#Check that all tags are fruit or raise a validation error
...
return data #or only the list of correct tags
def save(self, commit=True):
instance = super(MyForm, self).save(commit=False)
# Compare the list of tags fruit_tags with self.instance.fruit.all()
....
# Take the right actions
if commit:
instance.save()
return instance
Look into this page for more details on how to change the field validation https://docs.djangoproject.com/en/dev/ref/forms/validation/
This is just a schematic.
django-taggit is a perfect app for this use case.
Define your models like this:
from taggit.managers import TaggableManager
from taggit.models import TagBase, GenericTaggedItemBase
class Fruit(TagBase):
class Meta:
verbose_name = "Fruit"
verbose_name_plural = "Fruits"
class TaggedFruit(GenericTaggedItemBase):
tag = models.ForeignKey(Fruit,
related_name="%(app_label)s_%(class)s_items")
class Box(models.Model):
name = models.CharField(max_length=199)
fruits = TaggableManager(through=TaggedFruit)
Then create basic model form:
class BoxModelForm(ModelForm):
class Meta:
model = Box
And that's it! You can now add fruit tags into your box, separated by comma. In case the fruit doesn't exist, it will be added into Fruit table. Read the docs for more details on how to use django-taggit.
You can use it together with jquery based Selectize.js.

Using an instance's fields to filter the choices of a manytomany selection in a Django admin view

I have a Django model with a ManyToManyField.
1) When adding a new instance of this model via admin view, I would like to not see the M2M field at all.
2) When editing an existing instance I would like to be able to select multiple options for the M2M field, but display only a subset of the M2M options, depending on another field in the model. Because of the dependence on another field's actual value, I can't just use formfield_for_manytomany
I can do both of the things using a custom ModelForm, but I can't reliably tell whether that form is being used to edit an existing model instance, or if it's being used to create a new instance. Even MyModel.objects.filter(pk=self.instance.pk).exists() in the custom ModelForm doesn't cut it. How can I accomplish this, or just tell whether the form is being displayed in an "add" or an "edit" context?
EDIT: my relevant code is as follows:
models.py
class LimitedClassForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LimitedClassForm, self).__init__(*args, **kwargs)
if not self.instance._adding:
# Edit form
clas = self.instance
sheets_in_course = Sheet.objects.filter(course__pk=clas.course.pk)
self.Meta.exclude = ['course']
widget = self.fields['active_sheets'].widget
sheet_choices = []
for sheet in sheets_in_course:
sheet_choices.append((sheet.id, sheet.name))
widget.choices = sheet_choices
else:
# Add form
self.Meta.exclude = ['active_sheets']
class Meta:
exclude = []
admin.py
class ClassAdmin(admin.ModelAdmin):
formfield_overrides = {models.ManyToManyField: {
'widget': CheckboxSelectMultiple}, }
form = LimitedClassForm
admin.site.register(Class, ClassAdmin)
models.py
class Course(models.Model):
name = models.CharField(max_length=255)
class Sheet(models.Model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
file = models.FileField(upload_to=getSheetLocation)
class Class(models.model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
active_sheets = models.ManyToManyField(Sheet)
You can see that both Sheets and Classes have course fields. You shouldn't be able to put a sheet into active_sheets if the sheet's course doesn't match the class's course.

Django Inline Formset to edit one value via ManyToMany

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).

Intermediate model formset validation

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?