Django, adding excluded properties to the submitted modelform - django

I've a modelform and I excluded two fields, the create_date and the created_by fields. Now I get the "Not Null" error when using the save() method because the created_by is empty.
I've tried to add the user id to the form before the save() method like this: form.cleaned_data['created_by'] = 1 and form.cleaned_data['created_by_id'] = 1. But none of this works.
Can someone explain to me how I can 'add' additional stuff to the submitted modelform so that it will save?
class Location(models.Model):
name = models.CharField(max_length = 100)
created_by = models.ForeignKey(User)
create_date = models.DateTimeField(auto_now=True)
class LocationForm(forms.ModelForm):
class Meta:
model = Location
exclude = ('created_by', 'create_date', )

Since you have excluded the fields created_by and create_date in your form, trying to assign them through form.cleaned_data does not make any sense.
Here is what you can do:
If you have a view, you can simply use form.save(commit=False) and then set the value of created_by
def my_view(request):
if request.method == "POST":
form = LocationForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
obj.created_by = request.user
obj.save()
...
...
`
If you are using the Admin, you can override the save_model() method to get the desired result.
class LocationAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.created_by = request.user
obj.save()

Pass a user as a parameter to form constructor, then use it to set created_by field of a model instance:
def add_location(request):
...
form = LocationForm(user=request.user)
...
class LocationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(forms.ModelForm, self).__init__(*args, **kwargs)
self.instance.created_by = user

The correct solution is to pass an instance of the object with pre-filled fields to the model form's constructor. That way the fields will be populated at validation time. Assigning values after form.save() may result in validation errors if fields are required.
LocationForm(request.POST or None, instance=Location(
created_by=request.user,
create_date=datetime.now(),
))
Notice that instance is an unsaved object, so the id will not be assigned until form saves it.

One way to do this is by using form.save(commit=False) (doc)
That will return an object instance of the model class without committing it to the database.
So, your processing might look something like this:
form = some_form(request.POST)
location = form.save(commit=False)
user = User(pk=1)
location.created_by = user
location.create_date = datetime.now()
location.save()

Related

Setting user kwargs as field vales in Django Form with Class-based View

I have a Django application (1.11) to track referrals (referred by a user). I want to pass the id of the authenticated user to the ModelForm 'referrer' field (and since it's from the current logged in user the field shouldn't be editable).
class Referral(models.Model):
name = models.CharField(max_length=100)
referrer = models.ForeignKey('users.User', on_delete=models.SET_NULL, related_name='referrals', null=True, blank=True)
View:
class ReferralFormView(FormView):
form_class = ReferralForm
template_name = "refer.html"
success_url = reverse_lazy('thanks')
def get(self, request):
if request.user.is_authenticated():
return super(ReferralFormView, self).get(request)
else:
return redirect('login')
def get_form_kwargs(self):
user = self.request.user
form_kwargs = super(ReferralFormView, self).get_form_kwargs()
form_kwargs['referrer'] = user.id
return form_kwargs
def form_valid(self,form):
...
form.save()
return super(ReferralFormView, self).form_valid(form)
I override get_form_kwargs in the view, then modify form init
class ReferralForm(forms.ModelForm):
class Meta:
model = Referral
def __init__(self, *args, **kwargs):
referrer = kwargs.pop('referrer', None)
super(ReferralForm, self).__init__(*args, **kwargs)
self.fields['referrer'].disabled = True
self.fields['referrer'].queryset = User.objects.filter(id=referrer)
However all I see is a blank referrer field, what am I missing to make the user the value of that field (which can't be edited)? I also tried self.fields['referrer'].initial = User.objects.filter(id=referrer). I don't want the user to have to select their own username from a queryset of one.
I can print a <QuerySet [<User: username>]> for user = User.objects.filter(id=referrer), so why isn't it setting that user as the field value?
Update: I can assign the user value with
self.fields['referrer'].initial = User.objects.filter(id=referrer).first()
self.fields['referrer'].disabled = True
However, on form submit Referral obj is not saving with the referrer field value (that value's still blank)
thanks
what I needed to do was select the user obj in the queryset:
self.fields['referrer'].initial = User.objects.filter(id=referrer).first()
but using self.fields['referrer'].disabled = True disabled the field so I was still getting a blank value on submit (disabled doesn't do what the docs say it does). Using self.fields['referrer'].initial = User.objects.filter(id=referrer).first() with that field set as readonly allows the form to submit with the initial value

How to pass current logged user in a form class in django

I am trying to create a form where one field is a ModelChoicefield. Im trying to populate that field with objects from a different model. I have ran into a problem as i need to get the current logged user within the form to filter the queryset. Here are the 2 models
class UserExercises(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
Muscle = models.ForeignKey(Muscle, on_delete=models.CASCADE)
class Exercise(models.Model):
exercise = models.ForeignKey(UserExercises, on_delete=models.CASCADE)
weight = models.DecimalField(max_digits=6, decimal_places=3)
reps = models.PositiveIntegerField(validators=[MaxValueValidator(100)])
difficulty = models.CharField(max_length=30)
date = models.DateTimeField(default=timezone.now)
user = models.ForeignKey(User, on_delete=models.CASCADE)
And here is my form
class AddExerciseForm(forms.Form):
exercise = forms.ModelChoiceField(queryset=UserExercises.objects.filter(user=1))
class Meta:
model = Exercise
fields = ['exercise', 'weight', 'reps', 'difficulty']
As you can see i am currently hard coding a filter in the ModelChoiceField, but want to replace that with the current users Id. Is there anyway of Going about this. Im new to django so any help would be Appreciated.
My View
#login_required
def add_exercise_view(request):
if request.method == 'POST':
user_id = request.user.id
form = AddExerciseForm(user_id=user_id)
if form.is_valid():
form.save()
return redirect('myfit-home')
else:
form = AddExerciseForm()
return render(request, 'users/register.html', {'form': form})
Firstly, AddExerciseForm should extend forms.ModelForm.
To initialize form data based on some paramater, you can override __init_ method of ModelForm to update form fields (that field is exercise in this case) based on some argument/parameter (which is user_id in this case).
class AddExerciseForm(forms.ModelForm):
class Meta:
model = Exercise
fields = ['exercise', 'weight', 'reps', 'difficulty']
def __init__(self, *args, **kwargs):
user_id = kwargs.pop('user_id', None)
super(AddExerciseForm, self).__init__(*args, **kwargs)
if user_id is not None:
# update queryset for exercise field
self.fields['exercise'].queryset = UserExercises.objects.filter(user=user_id)
else:
# UserExercises.objects.none() will return an empty queryset
self.fields['exercise'].queryset = UserExercises.objects.none()
And pass the user_id while initializing the form in view:
if request.user.is_authenticated():
# get user id
user_id = request.user
form = AddExerciseForm(user_id=user_id)
override __init__ method of the Form, and pass the user as argument
def __init__(self,user,*args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
self.fields['exercise'].queryset=
UserExercises.objects.filter(user=self.user))
self.fields['exercise'].widget = forms.CheckboxSelectMultiple
class Meta:
model = Exercise
fields = ['exercise', 'weight', 'reps', 'difficulty']

How to get Django admin's "Save as new" to work with read-only fields?

I want to implement the "Save as new" feature in Django's admin for a model such as this one:
class Plasmid (models.Model):
name = models.CharField("Name", max_length = 255, blank=False)
other_name = models.CharField("Other Name", max_length = 255, blank=True)
selection = models.CharField("Selection", max_length = 50, blank=False)
created_by = models.ForeignKey(User)
In the admin, if the user who requests a Plasmid object is NOT the same as the one who created it, some of the above-shown fields are set as read-only. If the user is the same, they are all editable. For example:
class PlasmidPage(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if obj:
if not request.user == obj.created_by:
return ['name', 'created_by',]
else:
return ['created_by',]
else:
return []
def change_view(self,request,object_id,extra_context=None):
self.fields = ('name', 'other_name', 'selection', 'created_by',)
return super(PlasmidPage,self).change_view(request,object_id)
The issue I have is that when a field is read-only and a user hits the "Save as new" button, the value of that field is not 'transferred' to the new object. On the other hand, the values of fields that are not read-only are transferred.
Does anybody why, or how I could solve this problem? I want to transfer the values of both read-only and non-read-only fields to the new object.
Did you try Field.disabled attribute?
The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be editable by users. Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data.
I did a quick test in my project. When I added a new entry the disabled fields were sent to the server.
So something like this should work for you:
class PlasmidPage(admin.ModelAdmin):
def get_form(self, request, *args, **kwargs):
form = super(PlasmidPage, self).get_form(request, *args, **kwargs)
if not request.user == self.cleaned_data['created_by'].:
form.base_fields['created_by'].disabled = True
form.base_fields['name'].disabled = True
def change_view(self,request,object_id,extra_context=None):
self.fields = ('name', 'other_name', 'selection', 'created_by',)
return super(PlasmidPage,self).change_view(request,object_id)
It happens because Django uses request.POST data to build a new object, but readonly fields are not sent with the request body. You can overcome this by making widget readonly, not the field itself, like this:
form.fields['name'].widget.attrs = {'readonly': True}
This has a drawback: it's still possible to change field values by tampering the form (e.g if you remove this readonly attribute from the widget using devtools console). You could protect from that by checking that values haven't actually changed in clean() method.
So full solution will be:
class PlasmidForm(models.ModelForm):
class Meta:
model = Plasmid
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and not self.instance.created_by == request.user:
self.fields['name'].widget.attrs = {'readonly': True}
def clean(self):
cleaned_data = super().clean()
if self.instance and not self.instance.created_by == request.user:
self.cleaned_data['name'] = instance.name # just in case user tampered with the form
return cleaned_data
class PlasmidAdmin(admin.ModelAdmin):
form = PlasmidForm
readonly_fields = ('created_by',)
def save_model(self, request, obj, form, change):
if obj.created_by is None:
obj.created_by = request.user
super().save_model(request, obj, form, change)
Notice I left created_by to be readonly, and instead populate it with the current user whenever object is saved. I don't think you really want to transfer this property from another object.

Django: validating unique_together constraints in a ModelForm with excluded fields

I have a form:
class CourseStudentForm(forms.ModelForm):
class Meta:
model = CourseStudent
exclude = ['user']
for a model with some complicated requirements:
class CourseStudent(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
semester = models.ForeignKey(Semester)
block = models.ForeignKey(Block)
course = models.ForeignKey(Course)
grade = models.PositiveIntegerField()
class Meta:
unique_together = (
('semester', 'block', 'user'),
('user','course','grade'),
)
I want the new object to use the current logged in user for CourseStudent.user:
class CourseStudentCreate(CreateView):
model = CourseStudent
form_class = CourseStudentForm
success_url = reverse_lazy('quests:quests')
def form_valid(self, form):
form.instance.user = self.request.user
return super(CourseStudentCreate, self).form_valid(form)
This works, however, because the user is not part of the form, it misses the validation that Django would otherwise do with the unique_together constraints.
How can I get my form and view to use Django's validation on these constraints rather than having to write my own?
I though of passing the user in a hidden field in the form (rather than exclude it), but that appears to be unsafe (i.e. the user value could be changed)?
Setting form.instance.user in form_valid is too late, because the form has already been validated by then. Since that's the only custom thing your form_valid method does, you should remove it.
You could override get_form_kwargs, and pass in a CourseStudent instance with the user already set:
class CourseStudentCreate(CreateView):
model = CourseStudent
form_class = CourseStudentForm
success_url = reverse_lazy('quests:quests')
def get_form_kwargs(self):
kwargs = super(CreateView, self).get_form_kwargs()
kwargs['instance'] = CourseStudent(user=self.request.user)
return kwargs
That isn't enough to make it work, because the form validation skips the unique together constraints that refer to the user field. The solution is to override the model form's full_clean() method, and explicitly call validate_unique() on the model. Overriding the clean method (as you would normally do) doesn't work, because the instance hasn't been populated with values from the form at that point.
class CourseStudentForm(forms.ModelForm):
class Meta:
model = CourseStudent
exclude = ['user']
def full_clean(self):
super(CourseStudentForm, self).full_clean()
try:
self.instance.validate_unique()
except forms.ValidationError as e:
self._update_errors(e)
This worked for me, please check. Requesting feedback/suggestions.
(Based on this SO post.)
1) Modify POST request to send the excluded_field.
def post(self, request, *args, **kwargs):
obj = get_object_or_404(Model, id=id)
request.POST = request.POST.copy()
request.POST['excluded_field'] = obj
return super(Model, self).post(request, *args, **kwargs)
2) Update form's clean method with the required validation
def clean(self):
cleaned_data = self.cleaned_data
product = cleaned_data.get('included_field')
component = self.data['excluded_field']
if Model.objects.filter(included_field=included_field, excluded_field=excluded_field).count() > 0:
del cleaned_data['included_field']
self.add_error('included_field', 'Combination already exists.')
return cleaned_data

Initialize foreign key drop downs

I want to populate two foreign key fields in one of my forms. The relevant bit of code is as below:
if request.method == 'POST':
form = IssuesForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
form.save()
else:
form = IssuesForm(initial={'vehicle': stock_number, 'addedBy': request.user, })
vehicle points to the Vehicle class. addedBy is to contain the currently logged in user.
However the drop downs aren't initialized as I want...I still have to select the vehicle and user. From this I have two questions:
What could be the problem?
What is the best way to make these forms read-only?
EDIT 1
The IssueForm class looks like this so far:
class Issues(models.Model):
vehicle = models.ForeignKey(Vehicle)
description = models.CharField('Issue Description', max_length=30,)
type = models.CharField(max_length=10, default='Other', choices=ISSUE_CHOICES)
status = models.CharField(max_length=12, default='Pending',
choices=ISSUE_STATUS_CHOICES)
priority = models.IntegerField(default='8', editable=False)
addedBy = models.ForeignKey(User, related_name='added_by')
assignedTo = models.CharField(max_length=30, default='Unassigned')
dateTimeAdded = models.DateTimeField('Added On', default=datetime.today,
editable=False)
def __unicode__(self):
return self.description
Form Class
class IssuesForm(ModelForm):
class Meta:
model = Issues
exclude = ('assignedTo')
For your second question, are you wanting to make the addedBy field read-only? If so, don't add it to your form (it'll never be read-only if you present it to the user, e.g. Firebug). You can instead populate it inside your save method.
if request.method == 'POST':
form = IssuesForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
issue = form.save(commit=False)
issue.addedBy = request.user
# any other read only data goes here
issue.save()
else:
form = IssuesForm(initial={'vehicle': stock_number}) # this is related to your first question, which I'm not sure about until seeing the form code
To make a form read only: on your form class, overwrite the __init__method to disable html fields:
def __init__(self, *args, **kwargs):
super(IssuesForm, self).__init__(*args, **kwargs)
for key in self.fields.keys():
self.fields[key].widget.attrs = {'disabled': 'disabled'}
Makes sure you also don't listen for POST requests, if so, don't save the form.
You can further customize the __init__method to take some arguments and set fields to these values after the super method has been called.