Django: OneToOne dropdown in the admin interface and unique associations - python

Referring to Django - one-to-one modelAdmin i am still searching for a solution to my problem with the admin interface of Django and my OneToOne relationship.
I have the following model which extends the standard User model with an additional attribute is_thing_staff:
class ThingStaff(models.Model):
""" Extends the django user model by a separate model relationship which holds additional user
attributes
"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
# by default a new user is not a staff member which take care of the thing administration
is_thing_staff = models.BooleanField(default=False)
def __str__(self):
return u"{}".format(self.user.username)
class Meta:
verbose_name = "Thing Staff"
verbose_name_plural = "Thing Staff"
If i create a new ThingStaff object in the django admin interface, i can select all users, even if there is already a relationship for a user. Saving a new object with a duplicate association to a user results in an error, that there is already an ThingStaff object associated with that User. So far this is more or less ok.
But why show up possible selections if they would result in an error in the next step? So i excluded them via
from django import forms
from django.contrib import admin
from .models import ThingStaff
class ThingStaffForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ThingStaffForm, self).__init__(*args, **kwargs)
self.fields['user'].queryset = User.objects.exclude(
id__in=ThingStaff.objects.values_list('user_id', flat=True)
)
#admin.register(ThingStaff)
class ThingStaffAdmin(admin.ModelAdmin):
form = ThingStaffForm
Great so far: The already associated users will not show up in the dropdown during the creation of a new ThingStaff object.
But if i want to change an existing association, the related user will also not show up in the dropdown which makes it impossible to reset the is_thing_staff flag.
So my question is: How can i enable this specific user again for the change view in the django admin interface?

Django's ModelForm distinguishes between add and change views (each one has it's on own method). This means that you can override it:
class ThingStaffAdmin(ModelAdmin):
def add_view(self, *args, **kwargs):
self.form = ThingStaffAddForm
return super().add_view(*args, **kwargs)
def change_view(self, *args, **kwargs):
self.form = ThingStaffChangeForm
return super().change_view(*args, **kwargs)
More in the docs:
https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.change_view

your exclution list must be updated and selected user for this ThingStaff must not excluded
update your form like this
class ThingStaffForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ThingStaffForm, self).__init__(*args, **kwargs)
exclude_user = ThingStaff.objects.all()
if self.instance:
exclude_user = exclude_user.exclude(pk=self.instance.pk)
self.fields['user'].queryset = User.objects.exclude(id__in=exclude_user.values('user_id'))
this code check if current form is edit form and have an instance exclude that from exclude list.

Related

Accessing inline fields within inline get_formset in Django Admin

I have the following example where I have buildings(address, location,...) and apartments(name, size, type, building). One building containing multiple apartments.
class BuildingAdmin(admin.ModelAdmin):
inlines = [ApartmentInline,]
class ApartmentInline(admin.StackedInline):
def get_formset(self, request, obj=None, **kwargs):
formset = super(ApartmentInline, self).get_formset(request, obj=None, **kwargs)
#Here i'd like to see the values of inline fields, for example size or building that.
#Similar to how one can access ModelAdmin fields with obj.location within get_form
formset.form.base_fields["type"].widget = SelectMultiple(choices=custom_choices)
return formset
I'd like to be able to get the current apartments instance and field values when editing the object (for example size), so that I can create custom choices (querying other DB's or API's) for another field (type).
To modify the inline form widget you can override ModelInlineForm which gives each inline instance access after initializing.
from django.contrib import admin
from django.urls import resolve
class ApartmentInlineForm(forms.ModelForm):
class Meta:
model = A
fields = (
'...',
'...',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# calculate widget choices
self.fields['type'].widget = SelectMultiple(choices=custom_choices)
class ApartmentInline(admin.StackedInline):
model = Apartment
form = ApartmentInlineForm

django Admin - Filter foreign key select depending on other choice in edit form (without jQuery)

I am working on a project which is administered by a super admin who puts in data for different companies.
Lets say, I have these models:
class Company(models.Model):
name = models.CharField(max_length=100)
class ContactPerson(models.Model):
name = models.CharField(max_length=100)
company = models.ForeignKey(Company)
class Item(models.Model):
company = models.ForeignKey(Company)
contact_person = models.ForeignKey(ContactPerson)
I need to ensure that I (in django admin) in the edit mode I only see contact persons which belong to the selected company.
Being not in the year 2005 anymore I want to avoid writing loads of super ugly jQuery code.
I guess I could overwrite the admin form for Item. But still I had to make the contact_person optional, so when I create a new Item, the list of contact persons need to be empty. Then I'd select a company, save it and go back to edit. Now the contact_person list would be filled and I could add somebody. But if I now change the comany, I'd have to remove all selected contact persons. Sure, I could to this in the form... but it looks SO hacky and not like a nice django solution.
Anybody got some fancy ideas?
Actually, django provided me with a neat solution.
When you look at the UserAdmin class within the django code, you'll find a built-in way to handle a two-step creation process.
#admin.register(User)
class UserAdmin(admin.ModelAdmin):
...
add_form = UserCreationForm
...
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during user creation
"""
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
When the attribute add_form is set and the object has no id yet (= we are creating it), it takes a different form than usual.
I wrapped this idea in an admin mixin like this:
class AdminCreateFormMixin:
"""
Mixin to easily use a different form for the create case (in comparison to "edit") in the django admin
Logic copied from `django.contrib.auth.admin.UserAdmin`
"""
add_form = None
def get_form(self, request, obj=None, **kwargs):
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
Now, when I have dependent fields, I create a small form, containing all values independent of - in my case - company and a regular form containing everything.
#admin.register(Item)
class ItemAdmin(AdminCreateFormMixin, admin.ModelAdmin):
form = ItemEditForm
add_form = ItemAddForm
...
Now I can customise the querysets of the dependent field in my edit form:
class ItemEditForm(forms.ModelForm):
class Meta:
model = Item
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['contact_person'].queryset = ContactPerson.objects.filter(company=self.instance.company)
The only drawback is, that all dependent fields need to be nullable for the database. Otherwise you wouldn't be able to save it in the creation process.
Luckily, you can tell django that a field is required in the form but not on database level with blank=False, null=True in the model declaration.
Hope this helps somebody else as well!

Django ModelChoiceField has no plus button

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,

Access current user in custom django widget

I have a custom widget used to replace the dropdown for the ForeignKey field with a list of images with radiobuttons. In my render method I need to access the current user (the user who is currently logged in), like I would in a normal view with using request.user.
I have read a lot of solutions to do this with Forms, that you should pop the user object from **kwargs in your __init__ method.
However widgets doesn't have **kwargs in their __init__ method:
def __init__(self, attrs=None):
if attrs is not None:
self.attrs = attrs.copy()
else:
self.attrs = {}
How do I access the current user within a Widget sub-class?
I found a solution by reading through the Django source: Pass the user object when setting the formfield_overrides in the custom admin.
I have 2 models: News and Image. Image contains a name field and an ImageField. The News model contains a ForeignKey which points to image:
class News(models.Model):
... bunch of news related fields
image = models.ForeignKey(Image)
Then in my admin.py I have a custom admin class for news:
class NewsAdmin(admin.ModelAdmin):
model = News
def get_form(self, request, obj=None, **kwargs):
self.formfield_overrides = {
models.ForeignKey : {'widget' : SelectForeign(user = request.user)}
}
return super(NewsAdmin, self).get_form(request, obj, **kwargs)
Then in my widget.py I have a custom widget class:
class SelectForeign(widgets.Widget):
current_user = None
def __init__(self, attrs=None, choices=(), user = None):
self.current_user = user
super(SelectForegin, self).__init__(attrs, choices)
And that's it, now my widget contains the current logged in user. It's not pretty imo, but it works.
Note:
This replaces all ForeignKey fields inside the News model. To fix this, there should be a custom ForeignKey sub-class used so that we can override that one only.
If anyone has a better solution which is cleaner, please share and I'll accept.

ModelForm with a reverse ManytoMany field

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)

Categories

Resources