I have been struggling to normalize a few tables/models I have in Django that are to be used in one large form.
Let's say I have 2 forms based off of 2 models, 1 of which is dependant of the other. Is there a way to add form validations in 1 model when a specific selection is made in a different model?
The cleaned_data from 1 form isn't available in the other form, is the primary issue. I have instantiated both forms in the same view though.
So basically, in 1 form I need to reference fields which are in a different model (a different modelForm that's instantiated on the same page)
Form2 based off table/model2 :
def clean(self):
cleaned_data = super().clean()
if 'model1_field' in cleaned_data and not cleaned_data['model2_field']:
self.add_error('model2_field', forms.ValidationError('This field is required.'))
else:
print('No validations are needed')
More details on the dependency (trying to follow concepts of db normalization), and there are a bunch of fields that are required in model2, only when 'selection2'or 'is_option1' from model1 is selected.
--- Models------
MULTI_OPTIONS = (
('selection1', 'selection1'),
('selection2', 'selection2') # Display and make model2 required
)
class Model1(models.Model):
primary_key = models.CharField(db_column='primary_table1_key', max_length=10) # Maps tocommon key for all related tables
is_selected = models.BooleanField(db_column='IsSelected', blank=True, default=None)
multi_select_options = MultiSelectField(db_column='SelectedOptions', max_length=150, choices = MULTI_OPTIONS, blank=True, null=True)
is_option1 = models.BooleanField(db_column='IsOption1', blank=True, null=True, default=None) # No validations needed for Model1SubOptions
is_option2 = models.BooleanField(db_column='IsOption2', blank=True, null=True, default=None)# If option2 or 'selection2' is made in Model1, then make required sub_selections and all other fields in Model1SubOptions
class Meta:
managed = True
# This should become required if Model1 specific option (from a multiselect (Or) a specific boolean field is set to true)
class Model2(models.Model):
primary_key = models.CharField(db_column='primary_table1_key', max_length=10) # Maps tocommon key for all related tables
sub_selections = MultiSelectField(db_column='SubOptions', max_length=150, choices = (('Some', 'Some'), ('Other','Other')), blank=True, null=True)
other_field2 = models.PositiveIntegerField(db_column='OtherField2', blank=True, null=True)
---------- Forms ---------
class Model1Form(forms.ModelForm):
class Meta:
model = models.Model1
fields = ( 'is_selected', 'multi_select_options', 'is_option1', 'is_option2')
def clean(self):
cleaned_data = super().clean()
if('multi_select_options' in cleaned_data):
multi_select_options = cleaned_data['multi_select_options']
if(not multi_select_options):
self.add_error('multi_select_options', forms.ValidationError('This field is required'))
# if(('selection1' in multi_select_options) and check_data_does_not_exist(cleaned_data, 'model2')):
# Validate model2
class Model2Form(forms.ModelForm):
class Meta:
model = models.Model2
fields = ( 'sub_selections', 'other_field2')
def clean(self):
cleaned_data = super().clean()
# if(('selection1' in multi_select_options) and check_data_does_not_exist(cleaned_data, 'multi_select_options')):
# Validate model2
You can create a form that has all the fields from both models, you can then do all validation in one clean method and then your view can handle saving he individual models
class CombinedForm(forms.Form):
# All fields from Model1
# All fields from Model2
def clean(self):
# Do all validation for fields that are dependent on each other
class CombinedView(FormView):
form_class = CombinedForm
def form_valid(self, form):
Model1.objects.create(
param1=form.cleaned_data['param1'],
param2=form.cleaned_data['param2'],
)
Model2.objects.create(
param3=form.cleaned_data['param3'],
param4=form.cleaned_data['param4'],
)
Related
I'm struggling to update records with the writeable nested serializers I've created.
There are many Listing categories for a classifieds app I'm creating that each have a few unique attributes, but they also share many attributes. I have a handful of django models that inherit from a parent Listing model, and one of these models, Battery, contains some nested data. So far I've been able to create Battery records but keep getting AttributeErrors when I try to update them.
I've tried to include only the relevant code. Here are my views:
# views.py
class ListingCreateView(CreateAPIView):
queryset = Listing.objects.all()
def get_serializer_class(self):
category = self.request.data['category']
if category == 1:
return PercussionSerializer
elif category == 6:
return BatterySerializer
return ListingSerializer
class ListingUpdateView(UpdateAPIView):
queryset = Listing.objects.all()
def get_serializer_class(self):
category = self.request.data['category']
if category == 1:
return PercussionSerializer
elif category == 6:
return BatterySerializer
return ListingSerializer
here are my models:
# models.py
## Parent model
class Listing(models.Model):
title = models.CharField(max_length=100)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True, default=0.00)
## One of the child models
class Battery(Listing):
model_name = models.TextField(blank=True, null=True, default="")
color = models.ForeignKey(Color, on_delete=models.CASCADE, blank=True, null=True)
manufacture_year = models.IntegerField(null=True)
## Model for the nested data in Battery model
class Drum(models.Model):
drum_type = models.CharField(max_length=50, blank=True)
size = models.TextField(blank=True, null=True, default="")
battery = models.ForeignKey(Battery, related_name='drums', on_delete=models.CASCADE, null=True)
and here are my serializers:
# serializers.py
class ListingSerializer(serializers.ModelSerializer):
class Meta:
model = Listing
fields = '__all__'
class DrumSerializer(serializers.ModelSerializer):
class Meta:
model = Drum
fields = ['drum_type', 'size', 'carrier', 'stand', 'cover', 'case', 'sold']
class BatterySerializer(serializers.ModelSerializer):
drums = DrumSerializer(many=True)
class Meta:
model = Battery
fields = ['id', 'title', 'description', 'price', 'model_name', 'color', 'manufacture_year', 'drums']
def create(self, validated_data):
drum_data = validated_data.pop('drums')
battery = Battery.objects.create(**validated_data)
for drum_data in drum_data:
Drum.objects.create(battery=battery, **drum_data)
return battery
def update(self, instance, validated_data):
# Update Listing field values
instance.title = validated_data.get('title', instance.title)
instance.description = validated_data.get('description', instance.description)
instance.price = validated_data.get('price', instance.price)
# Grab the Battery record for this Listing and update its values
instance_battery = Battery.objects.get(pk=instance.pk)
instance_battery.model_name = validated_data.get('model_name', instance_battery.model_name)
instance_battery.color = validated_data.get('color', instance_battery.color)
instance_battery.manufacture_year = validated_data.get('manufacture_year', instance_battery.manufacture_year)
# Check for a list of drums
drum_data = validated_data.pop('drums')
# If it exists
if drum_data:
# Clear the existing drums
instance_battery.drums.clear()
# Create new drums
Drum.objects.bulk_create(
[
Drum(**drum)
for drum in drum_data
],
)
# Save the updated Listing & Battery
instance.save()
instance_battery.save()
# Return the updated Battery
return instance
I feel like I've followed the DRF documentation about writeable nested serializers correctly, but I continue to get this AttributeError when I try to post an update to a Battery record:
AttributeError: Got AttributeError when attempting to get a value for field drums on serializer BatterySerializer.
The serializer field might be named incorrectly and not match any attribute or key on the Listing instance.
Original exception text was: 'Listing' object has no attribute 'drums'.
Judging by the error message I think the Django model inheritance requires a more specific solution that what the DRF documentation provides. Can anybody help me understand how to create the serializers I need to create/update a Battery record that inherits from a Listing model with a nested list of Drum records?
Thanks!
I think the problem appears because the view waits for a Listing instance. I can suggest the following: try to redefine def get_qyeryset(self)
For example:
class ListingUpdateView(UpdateAPIView):
def get_queryset(self):
if self.request.data['category'] == 6:
return Battery.objects.all()
else:
return Listing.objects.all()
def get_serializer_class(self):
category = self.request.data['category']
if category == 1:
return PercussionSerializer
elif category == 6:
return BatterySerializer
return ListingSerializer
Maybe it is not the best way, but it can solve your problem
I've got the following Situation, I have a rather large legacy model (which works nonetheless well) and need one of its fields as a distinct dropdown for one of my forms:
Legacy Table:
class SummaryView(models.Model):
...
Period = models.CharField(db_column='Period', max_length=10, blank=True, null=True)
...
def __str__(self):
return self.Period
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'MC_AUT_SummaryView'
Internal Model:
class BillCycle(models.Model):
...
Name = models.CharField(max_length=100, verbose_name='Name')
Period = models.CharField(max_length=10, null=True, blank=True)
Version = models.FloatField(verbose_name='Version', default=1.0)
Type = models.CharField(max_length=100, verbose_name='Type', choices=billcycle_type_choices)
Association = models.ForeignKey(BillCycleAssociation, on_delete=models.DO_NOTHING)
...
def __str__(self):
return self.Name
Since I don't want to connect them via a Foreign Key (as the SummaryView is not managed by Django) I tried a solution which I already used quite a few times. In my forms I create a ModelChoiceField which points to my Legacy Model:
class BillcycleModelForm(forms.ModelForm):
period_tmp = forms.ModelChoiceField(queryset=SummaryView.objects.values_list('Period', flat=True).distinct(),
required=False, label='Period')
....
class Meta:
model = BillCycle
fields = ['Name', 'Type', 'Association', 'period_tmp']
And in my view I try to over-write the Period Field from my internal Model with users form input:
def billcycle_create(request, template_name='XXX'):
form = BillcycleModelForm(request.POST or None)
data = request.POST.copy()
username = request.user
print("Data:")
print(data)
if form.is_valid():
initial_obj = form.save(commit=False)
initial_obj.ModifiedBy = username
initial_obj.Period = form.cleaned_data['period_tmp']
initial_obj.Status = 'Creating...'
print("initial object:")
print(initial_obj)
form.save()
....
So far so good:
Drop Down is rendered correctly
In my print Statement in the View ("data") I see that the desired infos are there:
'Type': ['Create/Delta'], 'Association': ['CP'], 'period_tmp': ['2019-12']
Still I get a Select a valid choice. That choice is not one of the available choices. Error in the forms. Any ideas??
I am trying to update a field in Admin but it raises Validation Error from clean method which I have defined as follows in forms.py:
class BasePhoneFormSet(BaseInlineFormSet):
def clean(self):
super(BasePhoneFormSet, self).clean()
if any(self.errors):
return
phone_numbers = []
for form in self.forms:
if form.cleaned_data.get('number') in phone_numbers:
raise forms.ValidationError(
'Duplicate Entry')
phone_numbers.append(form.cleaned_data.get('number'))
PhoneFormSet = inlineformset_factory(
Post,
Phone,
formset=BasePhoneFormSet,
form=PostForm,
fields = ('number',),
can_delete=False, # admin still shows delete next to the phone number
extra=0,
validate_min=True,
min_num=1,
)
This code works in the views , but in the admin, I can't update or add any phone number since it raises the same ValidationError for duplicate entry.
here is my models.py
class Post(TimeStampedModel, models.Model):
unique_id = models.CharField(max_length=6, unique=True)
user = models.ForeignKey(User, related_name='posts')
city = models.ForeignKey(City, related_name='posts')
class Phone(TimeStampedModel, models.Model):
number = models.CharField(
validators=[phone_regex], max_length=15)
post = models.ForeignKey(Post)
And this is admin.py
class PhoneInline(admin.StackedInline):
model = Phone
formset = PhoneFormSet
class PostAdmin(admin.ModelAdmin):
inlines = [
PhoneInline,
]
I looked into BaseInlineFormSet in models.forms but I got confused more.
class PhoneInline(admin.StackedInline):
model = Phone
formset = BasePhoneFormSet
fields = ('number',)
can_delete = False
extra = 0
min_num = 1
I am trying using django forms and not model forms. While trying to populate data for edit I keep getting keyError (u 'manager'). This does not appear if I remove assignment of a field 'choices'. Choice is a many to many field on my model. to make it less confusing I will paste my model, forms and view.
#model.py
class Choices(models.Model):
choices = models.CharField(max_length=70)
def __unicode__(self):
return self.choices
class UserProfile(models.Model):
user = models.OneToOneField(MyUser)
about_me = models.TextField(max_length=2000, null=True, blank=True)
country = models.CharField(max_length=100,null=True, blank=True)
choices = models.ManyToManyField(Choices, blank=True)
#forms.py
class UserProfileForm(forms.Form):
CHOICES = (
('like to cook', 'like to cook'),
('like only to eat', 'like only to eat')
)
about_me = forms.CharField(widget=forms.Textarea, required=False)
country = forms.CharField(max_length=60,required=False)
choices =forms.MultipleChoiceField(choices=CHOICES,widget=forms.CheckboxSelectMultiple(),required=False)
#views.py
#login_required
def update_profile(request):
userprofile = UserProfile.objects.get(user=request.user)
form = UserProfileForm(initial={'about_me':userprofile.about_me,
'country':userprofile.country,
'choices': userprofile.choices})
return render(request, 'u_profiles/edit_pro.html', {'form':form})
now when I assign the initial value of selected choices I get the keyerror. I would like to know the correct way of doing this.
Thanks.
I am using ManyToMany relationship in my code in a scenario where i have showrooms and their categories. Now a showroom can fall into maximum three categories and i have to validate it while saving. Below is my code:
##models.py
class Showrooms(models.Model):
name = models.CharField(max_length = 100)
contact_person = models.CharField(max_length = 100)
offer = models.TextField()
categories = models.ManyToManyField(Categories, null=True, blank=True, related_name='categories')
class Meta:
db_table = 'showrooms'
verbose_name_plural = "showrooms"
class Categories(models.Model):
category = models.CharField(max_length = 100)
image = models.ImageField(upload_to = showroom_upload_path, null=True, blank=True)
slug = models.SlugField(blank=True)
class Meta:
db_table = 'showroom_categories'
verbose_name_plural = "categories"
def __str__(self):
return self.category
everything is working fine except i am not able to put validation on number of categories per showroom. And i am not using it in views but just wanna to do it in admin.
Please help
Thanks
Edits
Ok.. I have solved my issue. I created a form in forms.py
class ShowroomsForm(forms.ModelForm):
class Meta:
model = Showrooms
def clean(self):
categories = self.cleaned_data.get('categories')
if categories and categories.count() > 3:
raise ValidationError('Maximum three categories are allowed.')
return self.cleaned_data
and added it to admin.py like below:
class ShowroomsAdmin(admin.ModelAdmin):
form = ShowroomsForm
admin.site.register(Showrooms, ShowroomsAdmin)
You could define a clean() method on your model an raise a validation error whenever a showroom gets assigned to more than 3 categories.
https://docs.djangoproject.com/en/1.5/ref/models/instances/#django.db.models.Model.clean
I had the same problem and used #Rakesh Kumar's solution. But then I got an error
django.core.exceptions.ImproperlyConfigured: Creating a ModelForm without either the 'fields' attribute or the 'exclude' attribute is prohibited; form JobsForm needs updating.
The problem was that Rashid's form didn't have any form fields.
So I modified his solution just a little.
forms.py - I added fields
class ShowroomsForm(forms.ModelForm):
class Meta:
model = Showrooms
fields = "__all__"
def clean(self):
categories = self.cleaned_data.get('categories')
if categories and categories.count() > 3:
raise ValidationError('Maximum three categories are allowed.')
return self.cleaned_data
admin.py - this remained the same:
class ShowroomsAdmin(admin.ModelAdmin):
form = ShowroomsForm
admin.site.register(Showrooms, ShowroomsAdmin)
After that, it worked perfectly!
All I needed was to create a model form:
class ShowroomsForm(forms.ModelForm):
class Meta:
model = Showrooms
def clean(self):
categories = self.cleaned_data.get('categories')
if categories and categories.count() > 3:
raise ValidationError('Maximum three categories are allowed.')
return self.cleaned_data
and added it to admin.py like below:
class ShowroomsAdmin(admin.ModelAdmin):
form = ShowroomsForm
admin.site.register(Showrooms, ShowroomsAdmin)