I have a model MyModel which contains a PK - locid, that is an AutoField.
I want to construct a model formset from this, with some caveats:
The queryset for the formset should be a custom one (say, order_by('field')) rather than all()
Since locid for MyModel is an AutoField and thus hidden by default, I want to be able to show it to the user.
I'm not sure how to do this. I've tried multiple approaches,
MyModelFormSet = modelformset_factory(MyModel, fields=('locid', 'name', 'dupof'))
The above gives me the 3 fields, but locid is hidden.
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['locid'].widget.attrs["type"] = 'visible'
locid = forms.IntegerField(min_value = 1, required=True)
class Meta:
model = MyModel
fields = ('locid', 'name', 'dupof')
The above gives me a ManyToMany error.
Has anyone done something like this before?
Edit 2
I can now use a custom query when I instantiate the formset - but I still need to show the locid field to the user, because the id is important for the application's use. How would I do this? Is there a way to override the default behavior of hiding a PK if its an autofield?
It makes no sense to show an autofield to the user, as it's an autoincremented primary key -- the user can not change it and it will not be available before saving the record to the database (where the DBMS selectes the next available id).
This is how you set a custom queryset for a formset:
from django.forms.models import BaseModelFormSet
class OrderedFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
self.queryset = MyModel.objects.order_by("field")
super(OrderedFormSet, self).__init__(*args, **kwargs)
and then you use that formset in the factory function:
MyModelFormSet = modelformset_factory(MyModel, formset=OrderedFormSet)
I ended up using a template side variable to do this, as I mentioned here:
How to show hidden autofield in django formset
If you like cheap workarounds, why not mangle the locid into the __unicode__ method? The user is guaranteed to see it, and no special knowledge of django-admin is required.
But, to be fair, all my answers to django-admin related questions tend along the lines of "don't strain to hard to make django-admin into an all-purpose CRUD interface".
Related
I'm having issue with Django admin. I want one of my foreign key fields to be searchable and I achieved that making it autocomplete.
class CollectionAdmin(VersionAdmin, admin.ModelAdmin):
form = CollectionForm
autocomplete_fields = ["task"]
I also filter that foreign key in ModelForm.
class CollectionForm(forms.ModelForm):
class Meta:
model = Collection
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.fields.get("task"):
self.fields["task"].queryset = self.fields["task"].queryset.filter(
status=TaskStatus.ASSIGNED
)
When task is not autocomplete field in Collection my filter works as expected.
However, when task is autocomplete field in Collection, filter does not work.
Instead of my filter in form, task admin get_queryset method is called which is not what I want.
TaskAdmin.get_queryset method just filters by user, however I want more filter as you see above, for TaskStatus as well.
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.groups.filter(name=settings.COPYWRITER_GROUP).exists():
return qs.filter(assigned_to=request.user)
return qs
Repeating, form init filter works if field is not autocomplete.
I tried removing task from autocomplete of Collection and it worked.
I want my form filter not to be overridden if the field is autocomplete.
Answering my own question.
When you make field autocomplete, it directly triggers get_queryset method of that model admin.
So, in my case, TaskAdmin get_queryset is called each time when I try to select value from dropdown.
That is why, form filter became useless, we can actually totally remove that filter.
Workaround is to conditionally filtering objects in TaskAdmin get_queryset.
I am providing example,
path = request.path
if "autocomplete" in path:
return super().get_queryset(request).filter()
else:
return super().get_queryset(request)
This will filter queryset if it is called from dropdown.
Thansk!
I'm making a Django app with custom users. I've outlined the key components of my problem below, missing code is denoted by '...'. My custom user model has a foreign key relationship as follows:
class MyCustomUser(models.AbstractBaseUser, models.PermissionsMixin)
...
location = models.ForeignKey(Location)
class Location(models.Model)
name = models.CharField(max_length=50, blank=True, null=True)
I've written a custom user form that includes this field as follows:
class MyCustomUserCreationForm(models.ModelForm)
...
location = forms.ModelChoiceField(Location.objects.all())
This all appears to be working correctly, however, there is no plus button to the right of the select field for location. I want to be able to add a location when I create a user, in the same way that you can add polls when creating choices in the Django tutorial. According to this question, I might not see the green plus if I don't have permission to change the model, but I am logged in as a superuser with all permissions. Any idea what I'm doing wrong?
You need to set a RelatedFieldWidgetWrapper wrapper in your model form:
The RelatedFieldWidgetWrapper (found in django.contrib.admin.widgets)
is used in the Admin pages to include the capability on a Foreign Key
control to add a new related record. (In English: puts the little green plus sign to the right of the control.)
class MyCustomUserCreationForm(models.ModelForm)
...
location = forms.ModelChoiceField(queryset=Location.objects.all())
def __init__(self, *args, **kwargs):
super(MyCustomUserCreationForm, self).__init__(*args, **kwargs)
rel = ManyToOneRel(self.instance.location.model, 'id')
self.fields['location'].widget = RelatedFieldWidgetWrapper(self.fields['location'].widget, rel, self.admin_site)
I could make a mistake in the example code, so see these posts and examples:
RelatedFieldWidgetWrapper
More RelatedFieldWidgetWrapper – My Very Own Popup
Django admin - How can I add the green plus sign for Many-to-many Field in custom admin form
How can I manually use RelatedFieldWidgetWrapper around a custom widget?
Django: override RelatedFieldWidgetWrapper
I have created method based on the answers above:
def add_related_field_wrapper(form, col_name):
rel_model = form.Meta.model
rel = rel_model._meta.get_field(col_name).rel
form.fields[col_name].widget =
RelatedFieldWidgetWrapper(form.fields[col_name].widget, rel,
admin.site, can_add_related=True, can_change_related=True)
And then calling this method from my form:
class FeatureForm(forms.ModelForm):
offer = forms.ModelChoiceField(queryset=Offer.objects.all(), required=False)
package = forms.ModelChoiceField(queryset=Package.objects.all(), required=False)
def __init__(self, *args, **kwargs):
super(FeatureForm, self).__init__(*args, **kwargs)
add_related_field_wrapper(self, 'offer')
add_related_field_wrapper(self, 'package')
That works fine on Django 1.8.2.
Google pointed me to this page when searching how to get a "+" icon next to fields in a custom form with ForeignKey relationship, so I thought I'd add.
For me, using django-autocomplete-light did the trick very well, using the "add another" functionality.
You don't even need to go that far, and besides, these answers are probably outdated as NONE of them worked for me in any capacity.
What I did to solve this is, as long as you have the ForeignKey field already in your model, then you can just create your custom ModelChoiceField:
class LocationModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return "%" % (obj.name)
The key next is NOT to create a custom field for the ModelChoiceField in your ModelForm (ie location = forms.ModelChoiceField(Location.objects.all()))
In other words, leave that out and in your ModelForm have something like this:
class UserAdminForm(forms.ModelForm):
class Meta:
model = User
fields = '__all__'
Lastly, in your ModelAdmin:
class UserAdmin(admin.ModelAdmin):
model = User
form = UserAdminForm
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'location':
return LocationModelChoiceField(queryset=Location.objects.order_by('name')) # if you want to alphabetize your query
return super().formfield_for_foreignkey(db_field, request, **kwargs)
Alternative Method : Using .remote_field instead of rel
def add_related_field_wrapper(self,form, col_name):
rel_model = form.Meta.model
rel = rel_model._meta.get_field(col_name).remote_field
form.fields[col_name].widget = RelatedFieldWidgetWrapper(form.fields[col_name].widget, rel, admin.site, can_add_related=True, can_change_related=True)
def __init__(self, *args, **kwargs):
super(CustomerAdminForm, self).__init__(*args, **kwargs)
self.add_related_field_wrapper(self, 'offer')
self.add_related_field_wrapper(self, 'package')
Thankyou,
I'm having trouble getting ModelMultipleChoiceField to display the initial values of a model instance. I haven't been able to find any documentation about the field, and the examples I've been reading are too confusing. Django: ModelMultipleChoiceField doesn't select initial choices seems to be similar, but the solution that was given there is not dynamic to the model instance.
Here is my case (each database user is connected to one or more projects):
models.py
from django.contrib.auth.models import User
class Project(Model):
users = ManyToManyField(User, related_name='projects', blank=True)
forms.py
from django.contrib.admin.widgets import FilteredSelectMultiple
class AssignProjectForm(ModelForm):
class Meta:
model = User
fields = ('projects',)
projects = ModelMultipleChoiceField(
queryset=Project.objects.all(),
required=False,
widget=FilteredSelectMultiple('projects', False),
)
views.py
def assign(request):
if request.method == 'POST':
form = AssignProjectForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
return HttpResponseRedirect('/index/')
else:
form = AssignProjectForm(instance=request.user)
return render_to_response('assign.html', {'form': form})
The form that it returns is not selecting the instance's linked projects (it looks like: Django multi-select widget?). In addition, it doesn't update the user with any selections made when the form is saved.
Edit: Managed to solve this using the approach here: http://code-blasphemies.blogspot.com/2009/04/dynamically-created-modelmultiplechoice.html
Here's a solution that is better than the older ones, which really don't work.
You have to both load the existing related values from the database when creating the form, and save them back when saving the form. I use the set() method on the related name (manager) which does all the work for you: taking away existing relations that are not selected anymore, and adding new ones which have become selected. So you don't have to do any looping or checking.
class AssignProjectForm(ModelForm):
def __init__(self, *args, **kwargs):
super(AssignProjectForm, self).__init__(*args, **kwargs)
# Here we fetch your currently related projects into the field,
# so that they will display in the form.
self.fields['projects'].initial = self.instance.projects.all(
).values_list('id', flat=True)
def save(self, *args, **kwargs):
instance = super(AssignProjectForm, self).save(*args, **kwargs)
# Here we save the modified project selection back into the database
instance.projects.set(self.cleaned_data['projects'])
return instance
Aside from simplicity, using the set() method has another advantage that comes into play if you use Django signals (eg. post_save etc) on your m2m relation: If you add and remove entries one at a time in a loop, you'll get signals for each object. But if you do it in one operation using set(), you'll get just one signal with a list of objects. If the code in your signal handler does significant work, this is a big deal.
ModelForm's don't automatically work for reverse relationships.
Nothing is happening on save() because a ModelForm only knows what to do with its own fields - projects is not a field on the User model, it's just a field on your form.
You'll have to tell your form how to save itself with this new field of yours.
def save(self, *args, **kwargs):
for project in self.cleaned_data.get('projects'):
project.users.add(self.instance)
return super(AssignProjectForm, self).save(*args, **kwargs)
I have a model, Foo. It has several database properties, and several properties that are calculated based on a combination of factors. I would like to present these calculated properties to the user as if they were database properties. (The backing factors would be changed to reflect user input.) Is there a way to do this with the Django admin interface?
I would suggest you subclass a modelform for Foo (FooAdminForm) to add your own fields not backed by the database. Your custom validation can reside in the clean_* methods of ModelForm.
Inside the save_model method of FooAdmin you get the request, an instance of Foo and the form data, so you could do all processing of the data before/after saving the instance.
Here is an example for a model with a custom form registered with django admin:
from django import forms
from django.db import models
from django.contrib import admin
class Foo(models.Model):
name = models.CharField(max_length=30)
class FooAdminForm(forms.ModelForm):
# custom field not backed by database
calculated = forms.IntegerField()
class Meta:
model = Foo
class FooAdmin(admin.ModelAdmin):
# use the custom form instead of a generic modelform
form = FooAdminForm
# your own processing
def save_model(self, request, obj, form, change):
# for example:
obj.name = 'Foo #%d' % form.cleaned_data['calculated']
obj.save()
admin.site.register(Foo, FooAdmin)
Providing initial values for custom fields based on instance data
(I'm not sure if this is the best solution, but it should work.)
When a modelform for a existing model instance in the database is constructed, it gets passed this instance. So in FooAdminForm's __init__ one can change the fields attributes based on instance data.
def __init__(self, *args, **kwargs):
super(FooAdminForm, self).__init__(*args, **kwargs)
# only change attributes if an instance is passed
instance = kwargs.get('instance')
if instance:
self.fields['calculated'].initial = (instance.bar == 42)
It's easy enough to get arbitrary data to show up in change list or make a field show up in the form: list_display arbitrarily takes either actual model properties, or methods defined on the model or the modeladmin, and you can subclass forms.ModelForm to add any field type you'd like to the change form.
What's far more difficult/impossible is combining the two, i.e. having an arbitrary piece of data on the change list that you can edit in-place by specifying list_editable. Django seems to only accept a true model property that corresponds to a database field. (even using #property on the method in the model definition is not enough).
Has anyone found a way to edit a field not actually present on the model right from the change list page?
In the edit form, put the property name into readonly_fields (1.2 upwards only).
In the changelist, put it into list_display.
You can use the #property decorator in your model (Python >= 2.4):
class Product(models.Model):
#property
def ranking(self):
return 1
"ranking" can then be used in list_display:
class ProductAdmin(admin.ModelAdmin):
list_display = ('ranking', 'asin', 'title')
A Django autofield when displayed using a formset is hidden by default. What would be the best way to show it?
At the moment, the model is declared as,
class MyModel:
locid = models.AutoField(primary_key=True)
...
When this is rendered using Django formsets,
class MyModelForm(ModelForm):
class Meta:
model = MyModel
fields = ('locid', 'name')
it shows up on the page as,
<input id="id_form-0-locid" type="hidden" value="707" name="form-0-locid"/>
Thanks.
Edit
I create the formset like this -
LocFormSet = modelformset_factory(MyModel)
pformset = LocFormSet(request.POST, request.FILES, queryset=MyModel.objects.order_by('name'))
Second Edit
Looks like I'm not using the custom form class I defined there, so the question needs slight modification..
How would I create a formset from a custom form (which will show a hidden field), as well as use a custom queryset?
At the moment, I can either inherit from a BaseModelFormSet class and use a custom query set, or I can use the ModelForm class to add a custom field to a form. Is there a way to do both with a formset?
Third Edit
I'm now using,
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
locid = forms.IntegerField(min_value = 1, required=True)
self.fields['locid'].widget.attrs["type"] = 'visible'
self.queryset = MyModel.objects.order_by('name')
class Meta:
model = MyModel
fields = ('locid', 'name')
LocFormSet = modelformset_factory(MyModel, form = MyModelForm)
pformset = LocFormSet()
But this still doesn't
Show locid
Use the custom query that was specified.
Try changing the default field type:
from django import forms
class MyModelForm(ModelForm):
locid = forms.IntegerField(min_value=1, required=True)
class Meta:
model = MyModel
fields = ('locid', 'name')
EDIT: Tested and works...
As you say, you are not using the custom form you have defined. This is because you aren't passing it in anywhere, so Django can't know about it.
The solution is simple - just pass the custom form class into modelformset_factory:
LocFormSet = modelformset_factory(MyModel, form=MyModelForm)
Edit in response to update 3:
Firstly, you have the redefinition for locid in the wrong place - it needs to be at the class level, not inside the __init__.
Secondly, putting the queryset inside the form won't do anything at all - forms don't know about querysets. You should go back to what you were doing before, passing it in as a parameter when you instantiate the formset. (Alternatively, you could define a custom formset, but that seems like overkill.)
class MyModelForm(ModelForm):
locid = forms.IntegerField(min_value=1, required=True)
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['locid'].widget.attrs["type"] = 'visible'
class Meta:
model = MyModel
fields = ('locid', 'name')
LocFormSet = modelformset_factory(MyModel, form = MyModelForm)
pformset = LocFormSet(request.POST, request.FILES,
queryset=MyModel.objects.order_by('name')))
Okay, none of the approaches above worked for me. I solved this issue from the template side, finally.
There is a ticket filed (http://code.djangoproject.com/ticket/10427), which adds a "value" option to a template variable for a form. For instance, it allows,
{{form.locid.value}}
to be shown. This is available as a patch, which can be installed in the SVN version of django using "patch -p0 file.patch"
Remember, the {{form.locid.value}} variable will be used in conjunction with the invisible form - otherwise, the submit and save operations for the formset will crash.
This is Not the same as {{form.locid.data}} - as is explained in the ticket referred to above.
The reason that the autofield is hidden, is that both BaseModelFormSet and BaseInlineFormSet override that field in add_field. The way to fix it is to create your own formset and override add_field without calling super. Also you don't have to explicitly define the primary key.
you have to pass the formset to modelformset_factory:
LocFormSet = modelformset_factory(MyModel,
formset=VisiblePrimaryKeyFormSet)
This is in the formset class:
from django.forms.models import BaseInlineFormSet, BaseModelFormSet, IntegerField
from django.forms.formsets import BaseFormSet
class VisiblePrimaryKeyFormset(BaseModelFormSet):
def add_fields(self, form, index):
self._pk_field = pk = self.model._meta.pk
if form.is_bound:
pk_value = form.instance.pk
else:
try:
pk_value = self.get_queryset()[index].pk
except IndexError:
pk_value = None
form.fields[self._pk_field.name] = IntegerField( initial=pk_value,
required=True) #or any other field you would like to display the pk in
BaseFormSet.add_fields(self, form, index) # call baseformset which does not modify your primary key field