I have the following model and modelform for an Employee:
models.py
class Employee(models.Model):
reports_to = models.ForeignKey(
'self', on_delete=models.SET_NULL,
null=True, blank=True)
forms.py
class EmployeeForm(forms.ModelForm):
class Meta:
model = Employee
The idea is that the boss of an Employee is themselves an Employee.
The problem is that, when I'm updating the instance, the respective form field created is a dropdown with all Employees, including the object I'm updating itself.
Is there an easy way to remove the instance itself from the dropdown options so that no employee has him/herself as their own boss?
PS.: I'm not looking for a solution that validates the form field after submitting a form, but rather removing the option from the form dropdown altogether. Thanks!
Yes, you can modify the queryset of the respective field, and omit the instance, if that instance (already) exists. Like:
class EmployeeForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = self.instance
if instance.pk is not None:
self.fields['reports_to'].queryset = Employee.objects.exclude(pk=instance.pk)
class Meta:
model = Employee
In case the instance has a pk that is not None (that means that you edit the instance, not create a new one), then we thus "patch" the queryset that contains all the Employees, except for that one.
Related
I'd like to set an initial value on my dropdown form of "Select an Industry". Once the user selects a valid value from the dropdown AND saves the form, ideally, this option wouldn't be visible anymore within the list if the user were to go back to the form. If there is no way to do this, that's fine.
Models.py
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phone = models.CharField(blank=True, null=True, max_length=100)
industry = models.IntegerField(default=0)
Forms.py
class EditUserProfileForm (forms.ModelForm):
industry = forms.ChoiceField()
def __init__(self, *args, **kwargs):
super(EditUserProfileForm, self).__init__(*args, **kwargs)
self.fields['industry'].choices = [(t.industry, t.industryname) for t in Industry.objects.all()]
class Meta:
model = UserProfile
fields = (
'phone',
'industry',
)
Is it possible to set a default value without creating an instance of the Industry object whose industryname is "Select and Industry"?
Thanks!
If industry is an integer representing entries in a separate table, then it is a foreign key. Make it an actual ForeignKey field; then Django will automatically output a select box for that related model in your form.
I have a model with a forgein key to itself. For example:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
For my purposes, I never want parent_folder to refer to itself, but the default admin interface for this model does allow the user to choose its own instance. How can I stop that from happening?
Edit: If you're trying to do a hierarchical tree layout, like I was, another thing you need to watch out for is circular parent relationships. (For example, A's parent is B, B's parent is C, and C's parent is A.) Avoiding that is not part of this question, but I thought I would mention it as a tip.
I would personally do it at the model level, so if you reuse the model in another form, you would get an error as well:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
def clean(self):
if self.parent_folder == self:
raise ValidationError("A folder can't be its own parent")
If you use this model in a form, use a queryset so the model itself doesn't appear:
class FolderForm(forms.ModelForm):
class Meta:
model = Folder
fields = ('name','parent_folder')
def __init__(self, *args, **kwargs):
super(FolderForm, self).__init__(*args, **kwargs)
if hasattr(self, 'instance') and hasattr(self.instance, 'id'):
self.fields['parent_folder'].queryset = Folder.objects.exclude(id=self.instance.id)
To make sure the user does not select the same instance when filling in the foreign key field, implement a clean_FIELDNAME method in the admin form that rejects that bad value.
In this example, the model is Folder and the foreign key is parent_folder:
from django import forms
from django.contrib import admin
from .models import Folder
class FolderAdminForm(forms.ModelForm):
def clean_parent_folder(self):
if self.cleaned_data["parent_folder"] is None:
return None
if self.cleaned_data["parent_folder"].id == self.instance.id:
raise forms.ValidationError("Invalid parent folder, cannot be itself", code="invalid_parent_folder")
return self.cleaned_data["parent_folder"]
class FolderAdmin(admin.ModelAdmin):
form = FolderAdminForm
admin.site.register(Folder, FolderAdmin)
Edit: Combine my answer with raphv's answer for maximum effectiveness.
In my Django (1.6+) application, I have many Django models that point to (read only) DB views.
These models also contain foreign key relations.
Now if the Django application tries to delete the related fk-models, this will lead to DB errors ("Cannot delete from view") if I don't set cascade=DO_NOTHING on all foreign key fields.
Example:
class PersonView(models.Model):
person = models.ForeignKey(Person, db_column='fk_person', on_delete=DO_NOTHING)
class Meta:
db_table = 'view_persons'
managed = False
Since all my db-view-model-ForeignKey-fields should have cascade=DO_NOTHING by default, I'd like to create a DB-View model base class which will automatically set all foreign-key-fields to on_delete=DO_NOTHING, so I only need to care for inheriting from this model - otherwise it's easy to forget (and redundant) setting this attribute for all fields.
In the end I want to end up with a code like this:
class ViewModel(models.Model):
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super(ViewModel, self).__init__(*args, **kwargs)
# How to set all foreign-key fields' on_delete attribute to "DO_NOTHING"?
class PersonView(ViewModel):
# no need to set on_delete anymore
person = models.ForeignKey(Person, db_column='fk_person')
class Meta:
db_table = 'view_persons'
managed = False
How can I alter Django model attributes in my base class to set all foreign key fields to on_delete=DO_NOTHING?
Well, you can monkey-patch models.ForeignKey but the more preferred method is to simply subclass ForeignKey:
class MyForeignKey(models.ForeignKey):
def __init__(self, *args, **kwargs):
super(MyForeignKey, self).__init__(*args, **kwargs)
self.on_delete = models.DO_NOTHING
Then you can use MyForeignKey instead of ForeignKey in your models.
I'd like to create a form allowing me to assign services to supplier from these models. There is no M2M relationship defined since I use a DB used by others program, so it seems not possible to change it. I might be wrong with that too.
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
contact = models.ForeignKey(Contact, null=False, blank=False)
class SupplierPrice(models.Model):
service_user = models.ForeignKey('ServiceUser')
price_type = models.IntegerField(choices=PRICE_TYPES)
price = models.DecimalField(max_digits=10, decimal_places=4)
I've created this form:
class SupplierServiceForm(ModelForm):
class Meta:
services = ModelMultipleChoiceField(queryset=Service.objects.all())
model = ServiceUser
widgets = {
'service': CheckboxSelectMultiple(),
'contact': HiddenInput(),
}
Here is the view I started to work on without any success:
class SupplierServiceUpdateView(FormActionMixin, TemplateView):
def get_context_data(self, **kwargs):
supplier = Contact.objects.get(pk=self.kwargs.get('pk'))
service_user = ServiceUser.objects.filter(contact=supplier)
form = SupplierServiceForm(instance=service_user)
return {'form': form}
I have the feeling that something is wrong in the way I'm trying to do it. I have a correct form displayed but it is not instantiated with the contact and checkboxes aren't checked even if a supplier has already some entries in service_user.
You are defining services inside your Meta class. Put it outside, right after the beginning of SupplierServiceForm. At the very least it should show up then.
Edit:
I misunderstood your objective. It seems you want to show a multiple select for a field that can only have 1 value. Your service field will not be able to store the multiple services.
So, by definition, your ServiceUser can have only one Service.
If you don't want to modify the database because of other apps using it, you can create another field with a many to many relationship to Service. That could cause conflicts with other parts of your apps using the old field, but without modifying the relationship i don't see another way.
The solution to my problem was indeed to redefine my models in oder to integrate the m2m relationship that was missing, using the through argument. Then I had to adapt a form with a special init method to have all selected services displayed in checkboxes, and a special save() method to save the form using m2m relationship.
class Supplier(Contact):
services = models.ManyToManyField('Service', through='SupplierPrice')
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
supplier = models.ForeignKey(Supplier, null=False, blank=False)
price = models.Decimal(max_digits=10, decimal_places=2, default=0)
And the form, adapted from the very famous post about toppings and pizza stuff.
class SupplierServiceForm(ModelForm):
class Meta:
model = Supplier
fields = ('services',)
widgets = {
'services': CheckboxSelectMultiple(),
'contact_ptr_id': HiddenInput(),
}
services = ModelMultipleChoiceField(queryset=Service.objects.all(), required=False)
def __init__(self, *args, **kwargs):
# Here kwargs should contain an instance of Supplier
if 'instance' in kwargs:
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault('initial', {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data (checked boxes).
initial['services'] = [s.pk for s in kwargs['instance'].services.all()]
ModelForm.__init__(self, *args, **kwargs)
def save(self, commit=True):
supplier = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
def save_m2m():
new_services = self.cleaned_data['services']
old_services = supplier.services.all()
for service in old_services:
if service not in new_services:
service.delete()
for service in new_services:
if service not in old_services:
SupplierPrice.objects.create(supplier=supplier, service=service)
self.save_m2m = save_m2m
# Do we need to save all changes now?
if commit:
self.save_m2m()
return supplier
This changed my first models and will make a mess in my old DB but at least it works.
I'm writing a simple application with the models Person and Project, which have a many to many relationship through the Membership model. Only one Membership can exist between any two given Person and Project objects. Currently, new Membership objects can be added via inline from both the Person and Project change forms.
This works fine, but there's the problem of a user accidentally creating a redundant Membership. I've set a unique_together constraint for the two foreign keys in the Membership model, however this has two problems:
When saving a new parent object, any redundant instances of the Membership model are saved without raising any kind of error.
When modifying an existing object, adding a redundant Membership triggers the expected error message, but the delete icon of the corresponding inline is not shown and it's not possible to remove one of the objects that have already been persisted because the error prevents any database operations from happening.
I'd like to know what's the right way of iterating through the membership_set collection before the parent object is saved and removing any redundant objects.
this is how I made in my project:
#Models.py
class Person(models.Model):
user = models.OneToOneField(User, primary_key=True)
field = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
The view
#views.py
class GroupDetails(generic.DetailView):
model = Group
template_name = 'app/template.html'
form_class = SomeForm
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
#get group
group = self.get_object(queryset=Group.objects.all())
#get person
person = Person.objects.get(pk=request.user)
#check if objects exists before save
if Membership.objects.filter(person = person, group = group).exists():
messages.error(request, 'Oh this is duplicated')
return HttpResponseRedirect(reverse('view-name', kwargs={'pk': group.pk}))
else:
if form.is_valid():
form.save(commit=False)
#assign to the through table
persontogroup = Membership.objects.create(person = person, group = group)
persontogroup.save()
messages.success(request, 'Success!')
return HttpResponseRedirect('/something/')
Hope this helps. You can also check: Avoid Django def post duplicating on save