Allow empty foreign key selection in admin forms - python

I created a model(AnalysisFieldTemplate) with a foreign key to AnalysisFieldRule.
What i want is to have the possibility to leave the field display_analysis_field_rule blank and save my admin form.
class AnalysisFieldRule(models.Model):
action_kind: str = models.CharField(
choices=ActionKind.choices,
default=ActionKind.DISPLAY,
max_length=text_choices_max_length(ActionKind),
verbose_name=_("action kind"),
)
formula: str = formula_field()
name: str = models.CharField(max_length=MAX_NAME_LENGTH, verbose_name=_("name"))
def __str__(self):
return f"{self.name}: {ActionKind(self.action_kind).label}"
class Meta:
constraints = [
UniqueConstraint(fields=("action_kind", "name"), name="%(app_label)s_%(class)s_is_unique"),
]
ordering = ["name", "action_kind"]
verbose_name = _("analysis field rule")
verbose_name_plural = _("analysis field rules")
class AnalysisFieldTemplate(models.Model):
display_analysis_field_rule: AnalysisFieldRule = models.ForeignKey(
AnalysisFieldRule,
blank=True,
limit_choices_to={"action_kind": ActionKind.DISPLAY},
null=True,
on_delete=models.PROTECT,
related_name="display_analysis_field_templates",
verbose_name=_("display rule"),
)
Now here is the problem. If i try to save my admin form without choosing one a value for display_analysis_field_rule it will result in an Validationerror.
It seems that the standard empty value for a foreign key "------" is not a valid choice.
#admin.register(AnalysisFormTemplate)
class AnalysisFieldTemplateAdmin(admin.ModelAdmin):
fieldsets = (
(
None,
{
"fields": (
"name",
"name_for_formula",
"ordering",
"required",
"kind",
"display_analysis_field_rule",
"highlight_analysis_field_rule",
)
},
),
(
_("Text options"),
{"fields": ("max_length",)},
),
(
_("Integer options"),
{"fields": ("min_integer_value", "max_integer_value")},
),
(_("Amount of money options"), {"fields": ("min_amount_of_money", "max_amount_of_money")}),
)
I debugged a little deeper and found that the "to_python" compares the choosen value with pythons standard "empty_values" but of course it contains not the "------" and it will handle it as a normal id which results in an Validation error.
My question is how can i make it possible to save my form without choosing a value for my foreign key? Do i have to override the "to_python" function? What would be a best practice here?
I appreciate all the help :)

You need to create custom modelform for your use case and make the particular field with required=False
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['display_analysis_field_rule'].required = False
self.fields['display_analysis_field_rule'].empty_label = None
class Meta:
model = MyModel # Put your model name here
#admin.register(AnalysisFormTemplate)
class AnalysisFieldTemplateAdmin(admin.ModelAdmin):
form = MyForm
# .... your stuff

The solution which worked for me was to create a custom form and override the empy_values for each of my foreing keys with its empty_label.
from django.core.validators import EMPTY_VALUES # NOQA
def empty_values_list_for_foreign_key(empty_label:str):
empty_values_list = list(EMPTY_VALUES)
empty_values_list.append(empty_label)
return empty_values_list
class AnalysisFieldTemplateAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["display_analysis_field_rule"].empty_values = empty_values_list_for_foreign_key(self.fields["display_analysis_field_rule"].empty_label)
self.fields["highlight_analysis_field_rule"].empty_values = empty_values_list_for_foreign_key(self.fields["highlight_analysis_field_rule"].empty_label)
#admin.register(AnalysisFieldTemplate)
class AnalysisFieldTemplateAdmin(admin.ModelAdmin):
form = AnalysisFieldTemplateAdminForm

Related

null value in column "assigned_facilities_id"

I'm trying to access the dictonary inside the jsonfield serializer "assigned_facilities". But i'm receiving the following error:
django.db.utils.IntegrityError: null value in column "assigned_facilities_id" of relation "users_leadfacilityassign" violates not-null constraint
DETAIL: Failing row contains (78, null, null, 159).
File "/app/users/api/views.py", line 53, in perform_create
serializer.save(agent=self.request.user)
File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 205, in save
self.instance = self.create(validated_data)
File "/app/users/api/serializers.py", line 252, in create
instance.leadfacility.create(assigned_facilities_id=assigned_facilities.get('facility_id'), datetime=assigned_facilities.get('datetime'))
I'm basically trying to create a "LeadFacilityAssign" object for each item inside my json so i can have a "LeadFacilityAssign" object for each facility i want to add to a lead.
Does anyone know what is causing this error? I tried a few different things but nothing worked so far.
json
{
"facilities": [{
"facility_id": "1",
"datetime": "2018-12-19 09:26:03.478039"
},
{
"facility_id": "1",
"datetime": "2018-12-19 09:26:03.478039"
}
]
}
serializers.py
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.JSONField(required=False, allow_null=True, write_only=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"is_owner",
"assigned_facilities",
)
read_only_fields = ("id", "is_owner")
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
instance = Lead.objects.create(**validated_data)
for facility in assigned_facilities:
instance.leadfacility.create(assigned_facilities_id=assigned_facilities.get('facility_id'), datetime=assigned_facilities.get("datetime"))
return instance
models.py
class Facility(models.Model):
name = models.CharField(max_length=150, null=True, blank=False)
def __str__(self):
return self.name
class Lead(models.Model):
first_name = models.CharField(max_length=40, null=True, blank=True)
last_name = models.CharField(max_length=40, null=True, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class LeadFacilityAssign(models.Model):
assigned_facilities = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name='leadfacility')
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='leadfacility')
datetime = models.DateTimeField()
views.py
class LeadCreateView(CreateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def perform_create(self, serializer):
serializer.save(agent=self.request.user)
class LeadUpdateView(UpdateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def get_queryset(self):
return Lead.objects.all()
You have three tables:
Lead table in which lead_id is non nullable since it is primary key
Facility table in which facility_id is non nullable since it is primary key
LeadFacility table in which lead_facility_id is non nullable but its two foreign keys (lead_id and facility_id) are nullable.
And you are assigning these nullable value to your non nullable field.
Maybe you are trying to do :
lead_falility_id (table: LeadFacility talbe) = facility_id (table: Facility)
But by mistake you are doing :
lead_falility_id (table: LeadFacility talbe) = facility_id (table: LeadFacility).
And because of this, your are doing :
lead_facility_id = null for the non nullable field.
Your JSON doesn't match your serializer.
Your serializer fields don't match your model fields.
Your views don't match your models or your serializers.
So, let's take it from the top.
If I understand correctly, you want to create a LeadFacilityAssign object at the same time as creating or updating a Lead object. There are some approaches to solve this, like using a post_save signal right after a Lead create request, but let's follow your drift...
From your Lead serializer, this is "fine":
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.JSONField(required=False, allow_null=True, write_only=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"is_owner",
"assigned_facilities",
)
read_only_fields = ("id", "is_owner")
But this:
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
Has a comparison statement (==) in the last line, meaning it could return a True or False value (?), plus you're not using the "agent" field anywhere else, nor is declared in the serializer or even a field in your models. Just get rid of that function or add the "agent" field in your LeadFacilityAssign model (assuming you'll assign a Lead, a Facility and an Agent to that relationship).
What I'm guessing you're expecting from you JSON call is the "facilities" info. From your declared fields above I guess you should be expecting an "assigned_facilities" field, which doesn't show in your JSON data, but let's assume your API will receive an "assigned_facilities" field instead of the "facilities" sub-dict with many facilities related to a single Lead.
I haven't tested the code below, but according to the REST API docs, you have to define two methods in your serializer now, one for CREATE and one for UPDATE.
Create:
def create(self, validated_data):
lead = Lead.objects.create(first_name=validated_data['first_name'], last_name=validated_data['last_name'] #Here you will create the Lead object that you will reference later in your LeadFacilityAssign relationship with the dictionary information from the received data, so let's save it:
lead.save()
#Now we need to create all facilities relationships to this Lead:
facilities = validated_data['assigned_facilities'] #This will create a "facilities" sub-dict from your received data with a facility_id and a datetime field in key-value pair.
for item in facilities:
facility = Facility.objects.get(id=item['facility_id']) #Get a single facility object for each ID in your JSON. If this fails, try converting it to int().
datetime = item['datetime'] #Again, if it fails because it's taken as string, try converting it to datetime object.
entry = LeadFacilityAssign.objects.create(assigned_facilities=facility, lead=lead, datetime=datetime) #Create the entry.
entry.save() #Save the entry
return #Exit your function
The Update method should look more or less the same.
In the view, if you're not using the "agent" field just parse the user it for safety or just use it later if you want to include it as owner in your model.
class LeadCreateView(CreateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Your JSON sample include "facilities" but your serializer has assigned_facilities = serializers.JSONField(required=False, allow_null=True, write_only=True).
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
facilities = serializers.JSONField(required=False, allow_null=True, write_only=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"PrimaryAddress",
"City",
"PostalCode",
"RegionOrState",
"pc_email",
"Cell",
"secphone",
"client_cell",
"client_secphone",
"birthday",
"curr_client_address",
"curr_client_city",
"curr_client_zip",
"ideal_address",
"ideal_city",
"ideal_zip",
"ideal_state",
"budget",
"client_email",
"client_first_name",
"client_last_name",
"lead_status",
"created_at",
"agent",
"is_owner",
"relationship",
"marital_status",
"gender",
"pets",
"facilities",
)
read_only_fields = ("id", "created_at", "agent", "is_owner")
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
facilities = validated_data.pop("facilities", None)
instance = Lead.objects.create(**validated_data)
for facilities in assigned_facilities:
LeadFacilityAssign.objects.create(assigned_facilities_id=assigned_facilities.get("facility_id"), datetime=assigned_facilities.get("datetime"), lead=instance)
return instance
Also you added required False for facilities, so validated.pop("facilities") might be caused error if there is no facilities in request. You should add another parameter into pop method. validated.pop("facilities", None)
Your Query will become like this
instance.leadfacility.create(assigned_facilities_id__id=assigned_facilities.get('facility_id'), datetime=assigned_facilities.get("datetime"))
NOTE-
because assigned_facilities_id return full object of foreign key & assigned_facilities_id__id return value of id foreign key object

Perfom Django Validation for field that is NOT part of form

I would like to raise a ValidationError based on one of the fields in my Django model, without having the respective filed as part of a ModelForm. What I found after googling a bit is the concept of validators for models. So I tried to do the following:
def minimumDuration(value):
if value == 0:
raise ValidationError("Minimum value accepted is 1 second!")
class PlaylistItem(models.Model):
position = models.IntegerField(null=False)
content = models.ForeignKey(Content, null=True, on_delete=models.SET_NULL)
item_duration = models.IntegerField(validators = [minimumDuration], default = 5, null=True, blank=True)
playlist = models.ForeignKey(Playlist, null=True, on_delete=models.CASCADE)
However, no error appears when I introduce 0 in the respective field. From Django's documentation I found out that validators are not automatically applied when saving a model. It redirected me to this page, but I don't really understand how to apply those. Any idea?
Here is an example of a form with such a custom field outside of the Model:
class ExampleForm(forms.ModelForm):
custom_field = forms.BooleanField(
label='Just non model field, replace with the type you need',
required=False
)
class Meta:
model = YourModel
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# optional: further customize field widget
self.fields['custom_field'].widget.attrs.update({
'id': self.instance.pk + '-custom_field',
'class': 'custom-field-class'
})
self.fields['custom_field'].initial = self._get_custom_initial()
def _get_custom_initial(self):
# compute initial value based on self.instance and other logic
return True
def _valid_custom_field(value):
# validate your value here
# return Boolean
def clean(self):
"""
The important method: override clean to hook your validation
"""
super().clean()
custom_field_val = self.cleaned_data.get('custom_field')
if not self._valid_custom_field(custom_field_val):
raise ValidationError(
'Custom Field is not valid')

Django Include ManyToManyField on "other" model in ModelForm

I would like to have a form with the preselected checkboxes of a ManyToManyField.
models.py
class Store(models.Model):
...
class Brand(models.Model):
stores = models.ManyToManyField(Store, blank=True, related_name="brands")
forms.py
class StoreForm(ModelForm):
class Meta:
model = Store
fields = ('brands',)
I get this exception:
django.core.exceptions.FieldError: Unknown field(s) (brands) specified for Store
I know that I can add the field manually to the class:
brands = forms.ModelMultipleChoiceField(
queryset=Brand.objects.all(),
widget=forms.CheckboxSelectMultiple,
)
If I do this the checkboxes are not preselected.
How is it possible to include the ManyToMany field from "the other side" of the model (from Store)?
#hedgie To change the field in the other model is not a good option for me because I use it already.
But the __init__() was a good hint. I come up with this solution and it seems to work.
class StoreForm(ModelForm):
def __init__(self, *args, **kwargs):
if kwargs.get('instance'):
brand_ids = [t.pk for t in kwargs['instance'].brands.all()]
kwargs['initial'] = {
'brands': brand_ids,
}
super().__init__(*args, **kwargs)
# https://stackoverflow.com/questions/49932426/save-many-to-many-field-django-forms
def save(self, commit=True):
# Get the unsaved Pizza instance
instance = forms.ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
# This is where we actually link the pizza with toppings
instance.brands.clear()
for brand in self.cleaned_data['brands']:
instance.brands.add(brand)
self.save_m2m = save_m2m
# Do we need to save all changes now?
# Just like this
# if commit:
instance.save()
self.save_m2m()
return instance
brands = forms.ModelMultipleChoiceField(
queryset=Brand.objects.all(),
widget=forms.CheckboxSelectMultiple,
)
Though it seems to be not very elegant. I wonder why django does not support a better way.
One possibility is to define the field on the "other" model. So instead of writing this:
class Store(models.Model):
...
class Brand(models.Model):
stores = models.ManyToManyField(Store, blank=True, related_name="brands")
You can write this:
class Brand(models.Model):
...
class Store(models.Model):
brands = models.ManyToManyField(Brand, blank=True, related_name="stores")
Or, if you have manually added the field to the form, you could populate its initial value in the form's __init__() method.

How to write code such that only one of the radio button should be selected in models with django.?

#models.py
from django.db import models
from django.contrib.auth.models import User
CHOICES = (('1','Earned Leave'),('2','Casual Leave'),('3','Sick Leave'),('4','Paid Leave'))
class Leave(models.Model):
employee_ID = models.CharField(max_length = 20)
name = models.CharField(max_length = 50)
user = models.ForeignKey(User, on_delete = models.CASCADE, null =True)
department = models.CharField(max_length = 50)
designation = models.CharField(max_length = 50)
type_of_leave = models.CharField(max_length = 15, choices = CHOICES)
from_date = models.DateField(help_text = 'mm/dd/yy')
to_date = models.DateField(help_text = 'mm/dd/yy')
reporting_manager = models.CharField(max_length = 50, default = None, help_text = '0001_manager, 0002_manager')
reason = models.CharField(max_length= 180)
accepted = models.BooleanField(('accept'), default= False)
rejected = models.BooleanField(('reject'), default = False)
reason_reject = models.CharField(('reason for rejection'),max_length=50) // this didn't help me.
def __str__(self):
return self.name
This is a leave request form, where only of the two fields (accepted, rejected) should be selected and if the field rejected field is not selected then the reason_reject should not be shown in the /admin panel.
#forms.py
from django import forms
from lrequests import models
class LeaveRequestForm(forms.ModelForm):
class Meta:
fields = ("name", "employee_ID", "department", "designation", "type_of_leave", "from_date", "to_date", "reporting_manager", "reason")
model = models.Leave
The user fills the form and submits it. So, now the admin has to either accept it or reject it. The reason_reject field should appear to the admin only after once he selects rejected field. This all should happen only at the admin side.
#admin.py
from django.contrib import admin
from . import models
#admin.register(models.Leave)
class LeaveAdmin(admin.ModelAdmin):
list_display = ["name"]
#"employee_ID", "department", "designation", "type_of_leave", "from_date", "to_date", "reporting_manager", "reason", "accepted", "rejected", "reason_reject"
list_filter = ['department','type_of_leave']
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return (qs.filter(reporting_manager=request.user.username) or qs.filter(employee_ID=request.user.username))
# def get_readonly_fields(self, request, obj=None):
# # the logged in user can be accessed through the request object
# if obj and request.user.is_staff:
# readonly_fields = [f.name for f in self.opts.fields]
# readonly_fields.remove('accepted')
# readonly_fields.remove('rejected')
# return readonly_fields
#the above chunk of get_readonly_fields was written so that the admin could only either 'accept' or 'reject' the form.
def get_fields(self, request, obj=None):
fields = [f.name for f in self.opts.fields]
if obj and obj.rejected:
fields.append('reason_reject')
return fields
By running the above code I get
KeyError at /admin/lrequests/leave/15/change/ and
"Key 'id' not found in 'LeaveForm'. Choices are: accepted, department, designation, employee_ID, from_date, name, reason, reason_reject, rejected, reporting_manager, to_date, type_of_leave, user."
The entire premise is flawed. You cannot do this kind of logic in the model definition, because at that point there is no instance so no values to compare. And even if you could, it still wouldn't make sense because the model definition defines the database columns that the model has; you can't have different instances having different columns in the db. A model definition is for the entire model. You need to have reason_reject in the class definition.
What you might want to do is to change the form depending on the values in the model. You can do this in the admin by overriding the get_fields method.
class LeaveAdmin(admin.ModelAdmin):
...
def get_fields(self, request, obj=None):
fields = [....list of fields for the form...]
if obj and obj.rejected:
fields.append('reason_reject')
return fields
Note, you might want to think about whether you really need separate accepted/rejected fields; it would be better to have a single field for the state of the application: accepted or rejected. You can represent those either as individual radio buttons or as a dropdown.
STATUS_CHOICES = (
(1, 'Accepted'),
(0, 'Rejected'),
)
status = models.IntegerField(choices=STATUS_CHOICES, blank=True, null=True)
Firstly, define it as a drop down menu. That is with choices like
STATUS_CHOICES = (('0', 'Rejected'),('1', 'Accepted'),)
class Leave(models.Model):
...
status = models.CharField(max_length = 15, choices = STATUS_CHOICES)
and then in admin.py
class LeaveAdmin(admin.ModelAdmin):
...
radio_fields = {"status": admin.HORIZONTAL}

Store form fields as key-values / individual rows

I have a simple form in Django that looks like this:
class SettingForm(forms.Form):
theme = forms.CharField(rrequired=True,
initial='multgi'
)
defaultinputmessage = forms.CharField(required=True,
initial='Type here to begin..'
)
...and the model to store it looks like:
class Setting(models.Model):
name = models.CharField(
null=False, max_length=255
)
value= models.CharField(
null=False, max_length=255
)
When the form is submitted, how can i store the form fields as key value pairs and then when the page is rendered, how can I initialize the form with the key's value. I've tried looking for an implementation of this but have been unable to find one.
Any help?
Thanks.
I'm assuming you want to store 'theme' as the name and the value as the value, same for defaultinputmessage. If that's the case, this should work:
form = SettingForm({'theme': 'sometheme', 'defaultinputmessage': 'hello'})
if form.is_valid():
for key in form.fields.keys():
setting = Setting.objects.create(name=key, value=form.cleaned_data[key])
Here's how I did it.
I needed to do this because I had a Model that stored information as key value pairs and I needed to build a ModelForm on that Model but the ModelForm should display the key-value pairs as fields i.e. pivot the rows to columns. By default, the get() method of the Model always returns a Model instance of itself and I needed to use a custom Model. Here's what my key-value pair model looked like:
class Setting(models.Model):
domain = models.ForeignKey(Domain)
name = models.CharField(null=False, max_length=255)
value = models.CharField(null=False, max_length=255)
objects = SettingManager()
I built a custom manager on this to override the get() method:
class SettingManager(models.Manager):
def get(self, *args, **kwargs):
from modules.customer.proxies import *
from modules.customer.models import *
object = type('DomainSettings', (SettingProxy,), {'__module__' : 'modules.customer'})()
for pair in self.filter(*args, **kwargs): setattr(object, pair.name, pair.value)
setattr(object, 'domain', Domain.objects.get(id=int(kwargs['domain__exact'])))
return object
This Manager would instantiate an instance of this abstract model. (Abstract models don't have tables so Django doesn't throw up errors)
class SettingProxy(models.Model):
domain = models.ForeignKey(Domain, null=False, verbose_name="Domain")
theme = models.CharField(null=False, default='mytheme', max_length=16)
message = models.CharField(null=False, default='Waddup', max_length=64)
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super(SettingProxy, self).__init__(*args, **kwargs)
for field in self._meta.fields:
if isinstance(field, models.AutoField):
del field
def save(self, *args, **kwargs):
with transaction.commit_on_success():
Setting.objects.filter(domain=self.domain).delete()
for field in self._meta.fields:
if isinstance(field, models.ForeignKey) or isinstance(field, models.AutoField):
continue
else:
print field.name + ': ' + field.value_to_string(self)
Setting.objects.create(domain=self.domain,
name=field.name, value=field.value_to_string(self)
)
This proxy has all the fields that I'd like display in my ModelFom and store as key-value pairs in my model. Now if I ever needed to add more fields, I could simply modify this abstract model and not have to edit the actual model itself. Now that I have a model, I can simply build a ModelForm on it like so:
class SettingsForm(forms.ModelForm):
class Meta:
model = SettingProxy
exclude = ('domain',)
def save(self, domain, *args, **kwargs):
print self.cleaned_data
commit = kwargs.get('commit', True)
kwargs['commit'] = False
setting = super(SettingsForm, self).save(*args, **kwargs)
setting.domain = domain
if commit:
setting.save()
return setting
I hope this helps. It required a lot of digging through the API docs to figure this out.

Categories

Resources