Django model form with field not existing in model - django

I am using Django 3.2
I have a model like this:
class BannedUser(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="ban_info")
reason = models.PositiveSmallIntegerField(choices=BANN_REASON_CHOICES)
banned_at = models.DateTimeField(auto_now_add=True)
expiry_date = models.DateField(null=True, blank=True, help_text=_('Date on which ban expires'))
I want to create a form that instead of asking user to select a date, simply asks the user to select the Ban duration. The form will then calculate the expiration_date in the clean() method.
BAN_DURATION_3_DAYS=3
# ...
BAN_DURATION_CHOICES = (
(BAN_DURATION_3_DAYS, _('3 Days')),
# ...
)
class BannedUserForm(forms.ModelForm):
class Meta:
model = BannedUser
fields = ['reason', 'ban_till']
The form field ban_till is a PositiveInteger that maps to the number of days. The intention is then to calculate the expiry_date from today by offsetting the integer amount.
I suppose one way would be to:
create a dynamic field ban_till
add field expiry_date to the form field list (but somehow prevent it from being rendered)
in the form's clean() method calculate the expiry_date and update that field
How to create a form to display field that does not exist in Model?
My solution:
class BannedUserForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.fields['ban_till'] = forms.IntegerField(widget=forms.ChoiceField())
super().__init__(*args, **kwargs)
class Meta:
model = BannedUser
fields = ['reason']
Is this the correct way to do this - or are there any gotchas I need to be aware of?

Related

display only some fields in get api response django serializer

I have an example model which has a fk relation with user model and Blog model. Now I have a get api which only requires certain fields of user to be displayed.
My model:
class Example(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
related_name="user_examples",
)
blog = models.ForeignKey(
Blog,
on_delete=models.CASCADE,
null=True,
related_name="blog_examples",
)
/................./
Now my view:
class ExampleView(viewsets.ModelViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
def list(self, request, *args, **kwargs):
id = self.kwargs.get('pk')
queryset = Example.objects.filter(blog=id)
serializer = self.serializer_class(queryset,many=True)
return Response(serializer.data,status=200)
My serializer:
class ExampleSerializer(serializers.ModelSerializer):
class Meta:
model = Example
fields = ['user','blog','status']
depth = 1
Now when I call with this get api, I get all example objects that is required but all the unnecessary fields of user like password, group etc . What I want is only user's email and full name. Same goes with blog, I only want certain fields not all of them. Now how to achieve this in a best way??
You will have to specify the required fields in nested serializers. e.g.
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = ['title', 'author']
class ExampleSerializer(serializers.ModelSerializer):
blog = BlogSerializer()
class Meta:
model = Example
fields = ['user','blog','status']
are you setting depth in serializer's init method or anywhere else? beacause ideally it should only display id's and not anything else. if yes then set depth to zero and use serializer's method field to return data that you need on frontend. I can provide you with example code samples

Initial value for dropdown menu

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.

Django model ForeignKey with subset of data

Is it possible to define a foreign key or OneToOne relation in django model with only subset of data?
For example :
I have 2 models.
#with_author
class Product(models.Model):
GTIN = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
UOM = models.OneToOneField(MaterialUOM)
defaultPrice = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
and
#with_author
class UOM(models.Model):
uomname = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
so I want in my Product model only to allow UOM values that have same material value as in product.
Is it possible on model level or any other place and not to display non relevant values in the dropdown?
You can enforce this constraint by adding some validation to the model's clean() method. Something like:
from django.core.exceptions import ValidationError
class Product(models.Model):
GTIN = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
UOM = models.OneToOneField(MaterialUOM)
defaultPrice = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
def clean(self):
if not self.material == self.UOM.material:
# This will cause the model not to be saved and report an error
raise ValidationError('Material does not match UOM material')
If you are using a ModelForm to handle edits to your models, then clean() will be called automatically as part of the form validation. If you are modifying models directly in your code, then you need to call it yourself before saving the model. The documentation explains this in detail.
If you want to be doubly sure, you can also override the save() method:
def save(self, *args, **kwargs):
if not self.material == self.UOM.material:
return # Model is not saved
super(Product, self).save(*args, **kwargs)
This will not report any errors - it will just not save the model. Hence you should also use the clean() method above.

how to create a form with a checkbox for each record for a model in django

I have the following models:
class Activity(models.Model):
name = models.CharField(max_length=128)
calories = models.IntegerField(default=0)
type = models.IntegerField(choices=ACTIVITY_TYPES, default=0, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add = True)
updated_at = models.DateTimeField(auto_now = True)
def __unicode__(self):
return self.name
class CustomerActivity(models.Model):
customer = models.ForeignKey(Customer)
activity = models.ForeignKey(Activity)
created_at = models.DateTimeField(auto_now_add = True)
updated_at = models.DateTimeField(auto_now = True)
def __unicode__(self):
return self.activity.name
I want to create a form that allows the user to add an activity to their own list, thus making a CustomerActivity entry. I am thinking the best way to do this is create a form with a check box for every Activity record and then when the form is published save a CustomerActivity.
How can I accomplish this?
What makes this form different is the fact that you do not know the form fields upfront. Basically you want to generate a form based on database data. I coded the same thing once. The trick is that the Form object holds the individual fields in an instance variable called fields which is a sorted dictionary. The order in which you define the entries in the dict will be the order in which the form fields will appear in the form.
The code
#coding: utf-8
from django import forms
from models import Activity
class ActivitiesForm(forms.Form):
def __init__(self, survey, *args, **kwargs):
super(ActivitiesForm, self).__init__(*args, **kwargs)
self.add_fields()
def add_fields(self):
# generate a queryset of the activities to show
# could be sorted if a special order is needed
activities = Activities.objects.all()
if not activities:
return
for a in activities:
# required false is important to allow the field NOT to be checked
self.fields[a.id] = forms.BooleanField(label=a.name, required=False)
What it does
When a form is created it fetches all the activities from the database and adds a BooleanField for each activity to the form. The field name of this field will be the primary key of the activity and the label of the checkbox the activity's name. The order of how the checkboxes appear in the form will be the order of the primary key. To change that simply sort the queryset activities in the example to your needs.
When you now evaluate the form you get a list of numbers, which corresponds to the primary keys of the selected activities. From the list of the primary keys it is easy to retrieve the corresponding activity object from the database and add it to the m2m relationship on the customer object. Make sure to not use the standard django add function to add something to a m2m relationship as this would not work on m2m relationships with extra fields. Refer to the django docs for how it is done.

How to hide model field in Django Admin?

I generate field automaticly, so I want to hide it from user. I've tried editable = False and hide it from exclude = ('field',). All this things hide this field from me, but made it empty so I've got error: null value in column "date" violates not-null constraint.
models.py:
class Message(models.Model):
title = models.CharField(max_length = 100)
text = models.TextField()
date = models.DateTimeField(editable=False)
user = models.ForeignKey(User, null = True, blank = True)
main_category = models.ForeignKey(MainCategory)
sub_category = models.ForeignKey(SubCategory)
groups = models.ManyToManyField(Group)`
admin.py:
class MessageAdminForm(forms.ModelForm):
def __init__(self, *arg, **kwargs):
super(MessageAdminForm, self).__init__(*arg, **kwargs)
self.initial['date'] = datetime.now()
class MessageAdmin(admin.ModelAdmin):
form = MessageAdminForm
list_display = ('title','user',)
list_filter = ('date',)
Based on your model setup, I think the easiest thing to do would change your date field to:
date = models.DateTimeField(auto_now=True)
that should accomplish what you're after and you don't even need to exclude it from the admin, it's excluded by default. If you have auto_now=True it will act as a 'last update time'. If you have auto_now_add=True it will act as a creation time stamp.
There are several other ways you could accomplish your goal if your use case is more complex than a simple auto date field.
Override the model's save method to put the value in.
class Message(models.Model):
title=models.CharField(max_length=100)
date = models.DateTimeField(editable=False)
def save(*args, **kwargs):
self.date = datetime.datetime.now()
super(Message, self).save(*args, **kwargs)
What you are trying to do with the Model Admin isn't quite working because by default django only transfers the form fields back to a model instance if the fields are included. I think this might be so the model form doesn't try to assign arbitrary attributes to the model. The correct way to accomplish this would be to set the value on the instance in your form's save method.
class MessageAdminForm(forms.ModelForm):
def save(*args, **kwargs):
self.instance.date = datetime.now()
return super(MessageAdminForm, self).save(*args, **kwargs)