Modifying a Django ModelForms fields - python

I have a Django model, which has a foreign key owner, referring to the user who owns this object.
To let other users edit these objects, I currently use a forms.ModelForm, which works fine so far.
But I know want to let the owner and only the owner change the owner of the object he owns (what an ownage! :). Thus I tried the following:
class FolderForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
if kwargs.get("instance", False):
if user == kwargs["instance"].owner:
self._meta.fields += ("owner",)
super(FolderForm, self).__init__(*args, **kwargs)
class Meta:
model = Folder
fields = (
"name",
"description",
)
But this doesn't work, since Django uses some metaclass-magic to set the fields on the model, which seems to be done before my subclassed __init__ is called.
Anyone ever did something like this?

I like to use closure normally
def make_form(exclude_user=True):
class Form(forms.ModelForm):
class Meta:
model = Folder
exclude = ['user'] if exclude_user else None
return Form
form_cls = make_form(request.user != folder.owner)

Why don't you create two Forms:
One that excludes owner for users that don't own the data (mouthful) and do a simple if statement in your view:
if request.user == Model.owner:
form = OwnerForm
else:
form = OthersForm
Keep it as simple as possible has HUGE wins down the line.

Related

How to auto populate a read-only serializer field in django rest framework?

I have a question regarding django rest framework.
Most of the time, I have a serializer which has some read-only fields. For example, consider this simple model below:
class PersonalMessage(models.Model):
sender = models.ForeignKey(User, related_name="sent_messages", ...)
recipient = models.ForeignKey(User, related_name="recieved_messages", ...)
text = models.CharField(...)
def __str__(self) -> str:
return f"{self.text} (sender={self.sender})"
In this model, the value of sender and recipient should be automatically provided by the application itself and the user shouldn't be able to edit those fields. Alright, now take a look at this serializer:
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
It perfectly prevents users from setting an arbitrary value on the sender and recipient fields. But the problem is, when these fields are marked as read-only in the serializer, the serializer will completely ignore all the values that are passed into the constructor for these fields. So when I try to create a model, no values would be set for these fields:
PersonalMessageSerializer(data={**request.data, 'sender': ..., 'recipient': ...) # Won't work
What's the best way to prevent users from setting an arbitrary value and at the same time auto-populate those restricted fields in django rest framework?
Depending on how you get those two objects, you can use the serializer's save method to pass them, and they will automatically be applied to the object you are saving:
sender = User.objects.first()
recipient = User.objects.last()
serializer = PersonalMessageSerializer(data=request.data)
message = serializer.save(sender=sender, recipient=recipient)
The kwargs should match the field names in your model for this to work. For reference, have a look here
You able to override the serializer context like this;
PersonalMessageSerializer(data={**request.data, context={'sender': sender, 'recipent': recipent})
and catch the context inside serializer.
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
def validate(self, attrs):
attrs = super().validate(attrs)
attrs['sender'] = self.context['sender']
attrs['recipent'] = self.context['recipent']
return attrs
now serializer.validated_data it must returns sender and recipent.
From the question it is not possible to understand what field(s) of the relationship with sender and recipient you want to interact with, but a general answer can be found in the Serializer relations section of Django REST documentation.
Long story short, if you want to interact with one field only, you can use SlugRelatedField, which lets you interact with the target of the relationship using only one of its fields.
If it just the id, you can use PrimaryKeyRelatedField.
If you want to interact with more than one field, the way to go is Nested Relationships. Here you can specify a custom serializer for the target relationship, but you will have to override the create() method in your PersonalMessageSerializer to create the object from your relationship, as nested serializers are read-only by default.
So this is how you can make set a default on create but read only after in DRF. Although in this solution it wont actually be readonly, it's writable, but you now have explicit control on what the logged in user can write, which is the ultimate goal
Given the model
class PersonalMessage(models.Model):
sender = models.ForeignKey(User,...)
recipient = models.ForeignKey(User,..)
text = models.CharField(...)
You would first create your own custom default (I will show an example for only one field)
# Note DRF already has a CurrentUserDefault you can also use
class CurrentSenderDefault:
requires_context = True
def __call__(self, serializer_field):
return serializer_field.context['request'].user
def __repr__(self):
return '%s()' % self.__class__.__name__
Next you make your own field, that knows whats up with the filter.
This queryset prevents people from setting a value they are not allowed to. which is exactly what you want
class SenderField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
if user:
queryset = User.objects.filter(id=user.id)
else:
queryset = User.objects.none()
return queryset
Finally on the serialiser you go
class PersonalMessageSerializer(serializers.ModelSerializer):
sender = SenderField(default=CurrentSenderDefault())
recipient = ...
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')

django Admin - Filter foreign key select depending on other choice in edit form (without jQuery)

I am working on a project which is administered by a super admin who puts in data for different companies.
Lets say, I have these models:
class Company(models.Model):
name = models.CharField(max_length=100)
class ContactPerson(models.Model):
name = models.CharField(max_length=100)
company = models.ForeignKey(Company)
class Item(models.Model):
company = models.ForeignKey(Company)
contact_person = models.ForeignKey(ContactPerson)
I need to ensure that I (in django admin) in the edit mode I only see contact persons which belong to the selected company.
Being not in the year 2005 anymore I want to avoid writing loads of super ugly jQuery code.
I guess I could overwrite the admin form for Item. But still I had to make the contact_person optional, so when I create a new Item, the list of contact persons need to be empty. Then I'd select a company, save it and go back to edit. Now the contact_person list would be filled and I could add somebody. But if I now change the comany, I'd have to remove all selected contact persons. Sure, I could to this in the form... but it looks SO hacky and not like a nice django solution.
Anybody got some fancy ideas?
Actually, django provided me with a neat solution.
When you look at the UserAdmin class within the django code, you'll find a built-in way to handle a two-step creation process.
#admin.register(User)
class UserAdmin(admin.ModelAdmin):
...
add_form = UserCreationForm
...
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during user creation
"""
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
When the attribute add_form is set and the object has no id yet (= we are creating it), it takes a different form than usual.
I wrapped this idea in an admin mixin like this:
class AdminCreateFormMixin:
"""
Mixin to easily use a different form for the create case (in comparison to "edit") in the django admin
Logic copied from `django.contrib.auth.admin.UserAdmin`
"""
add_form = None
def get_form(self, request, obj=None, **kwargs):
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
Now, when I have dependent fields, I create a small form, containing all values independent of - in my case - company and a regular form containing everything.
#admin.register(Item)
class ItemAdmin(AdminCreateFormMixin, admin.ModelAdmin):
form = ItemEditForm
add_form = ItemAddForm
...
Now I can customise the querysets of the dependent field in my edit form:
class ItemEditForm(forms.ModelForm):
class Meta:
model = Item
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['contact_person'].queryset = ContactPerson.objects.filter(company=self.instance.company)
The only drawback is, that all dependent fields need to be nullable for the database. Otherwise you wouldn't be able to save it in the creation process.
Luckily, you can tell django that a field is required in the form but not on database level with blank=False, null=True in the model declaration.
Hope this helps somebody else as well!

Validate count() constraint in many to many relation before saving an instance in django

I would like to prevent a save in a django model when a certain constraint is not met and give a validation error so that a django staff user knows what went wrong.
The constraint is the count() from an intermediate table specified using the through parameter.
models.py:
class Goal(models.Model):
name = models.CharField(max_length=128)
class UserProfile(models.Model):
goals = models.ManyToManyField(Goal, through=UserProfileGoals, blank=True)
class UserProfileGoal(models.Model):
goal = models.ForeignKey(Goals)
user_profile = models.ForeignKey(UserProfile)
class UserGoalConstraint(models.Model):
user_profile = models.OneToOneField(UserProfile)
max_goals = models.PositiveIntegerField()
So the UserGoalConstraint.max_goals gives me the number of the maximum definable UserProfile.goal which are stored in the UserProfileGoal model (same UserGoal can be stored more often to the UserProfile)
I have read and tried solutions from several posts, which are using ModelForm's clean(), Model's clean() and pre_save signal events,
but the actual problem I have is, how do I know if it is just an update or a new database entry, because
class UserProfileGoal(models.Model):
goal = models.ForeignKey(Goals)
user_profile = models.ForeignKey(UserProfile)
def clean(self):
goal_counter = self.user_profile.goals.count() + 1
try:
qs = UserGoalConstraint.objects.get(user_profile=self.user_profile)
except UserGoalConstraint.DoesNotExist:
raise ObjectDoesNotExist('Goal Constraint does not exist')
if goal_counter > qs.max_goals:
raise ValidationError('There are more goals than allowed goals')
does not really work, as clean() can also be an update and the +1 gives me a wrong result which leads to the ValidationError.
My client should use the django-admin interface to add goals to the user profile directly via an Inline:
admin.py:
class UserProfileGoalInline(admin.TabularInline):
model=UserProfileGoal
class UserProfileAdmin(admin.ModelAdmin)
...
inlines = [UserProfileGoalInline, ]
So he needs to be nicely informed when he adds to many goals to a user profile.
Maybe I am missing something obvious on how to solve this problem...?
I am looking for a working and somehow user friendly solution (= get informed in admin interface).
[UPDATE]:
I tried know to check wether it is created or not with the self.pk is None trick at the beginning of the clean()
if self.pk is not None:
return # it is not a create
...
I thought that would deal with the issue...
However, in the admin inline, when the staff user adds more than one goal at the same time, the clean() does not recognize these. Debug output shows for 2 goals added, that the goal counter holds the same number even the second entry should have one more and should give an validation error
Thanks to #zaidfazil for a starting solution:
class UserProfileGoalForm(forms.ModelForm):
class Meta:
model = UserProfileGoal
...
def clean(self):
cleaned_data = super(UserProfileGoalForm, self).clean()
if self.instance.pk is not None:
return cleaned_data
user_profile = self.cleaned_data.get('user_profile')
goal_count = user_profile.goals.count()
goal_limit = UserGoalConstraint.objects.get(user_profile=user_profile).max_goals # removed try catch for get for easier reading
if goal_count >= goal_limit:
raise ValidationError('Maximum limit reached for goals')
return cleaned_data
However, this does not handle the inline in the UserProfile admin interface: clean() won't handle correctly if you add more than one Goal at the same time and press save.
So I applied the UserProfileGoalForm to the inline and defined max_num :
class UserProfileGoalInline(admin.TabularInline):
model=UserProfileGoal
form = UserProfileGoalForm
def get_max_num(self, request, obj=None, **kwargs):
if obj is None:
return
goal_limit = UserGoalConstraint.objects.get(training_profile=obj).max_goals
return goal_limit # which will overwrite the inline's max_num attribute
Now my client can only add at maximum the max_goals value from the UserGoalConstraint, and also a possible admin form for UserProfileGoal will handle the constraint:
class UserProfileGoalAdmin(admin.ModelAdmin):
form = UserProfileGoalForm
You could handle it in ModelForm clean method,
class GoalForm(forms.ModelForm):
class Meta:
model = Goal
.....
def clean(self):
cleaned_data = super(GoalForm, self).clean()
if self.instance.pk is not None:
return cleaned_data
goal_limit = self.user_profile.usergoalconstraint.max_goals
goal_count = self.user_profile.goals.count()
if goal_count >= goal_limit:
raise ValidationError("Maximum limit reached for goals")
return cleaned_data

Django ModelChoiceField has no plus button

I'm making a Django app with custom users. I've outlined the key components of my problem below, missing code is denoted by '...'. My custom user model has a foreign key relationship as follows:
class MyCustomUser(models.AbstractBaseUser, models.PermissionsMixin)
...
location = models.ForeignKey(Location)
class Location(models.Model)
name = models.CharField(max_length=50, blank=True, null=True)
I've written a custom user form that includes this field as follows:
class MyCustomUserCreationForm(models.ModelForm)
...
location = forms.ModelChoiceField(Location.objects.all())
This all appears to be working correctly, however, there is no plus button to the right of the select field for location. I want to be able to add a location when I create a user, in the same way that you can add polls when creating choices in the Django tutorial. According to this question, I might not see the green plus if I don't have permission to change the model, but I am logged in as a superuser with all permissions. Any idea what I'm doing wrong?
You need to set a RelatedFieldWidgetWrapper wrapper in your model form:
The RelatedFieldWidgetWrapper (found in django.contrib.admin.widgets)
is used in the Admin pages to include the capability on a Foreign Key
control to add a new related record. (In English: puts the little green plus sign to the right of the control.)
class MyCustomUserCreationForm(models.ModelForm)
...
location = forms.ModelChoiceField(queryset=Location.objects.all())
def __init__(self, *args, **kwargs):
super(MyCustomUserCreationForm, self).__init__(*args, **kwargs)
rel = ManyToOneRel(self.instance.location.model, 'id')
self.fields['location'].widget = RelatedFieldWidgetWrapper(self.fields['location'].widget, rel, self.admin_site)
I could make a mistake in the example code, so see these posts and examples:
RelatedFieldWidgetWrapper
More RelatedFieldWidgetWrapper – My Very Own Popup
Django admin - How can I add the green plus sign for Many-to-many Field in custom admin form
How can I manually use RelatedFieldWidgetWrapper around a custom widget?
Django: override RelatedFieldWidgetWrapper
I have created method based on the answers above:
def add_related_field_wrapper(form, col_name):
rel_model = form.Meta.model
rel = rel_model._meta.get_field(col_name).rel
form.fields[col_name].widget =
RelatedFieldWidgetWrapper(form.fields[col_name].widget, rel,
admin.site, can_add_related=True, can_change_related=True)
And then calling this method from my form:
class FeatureForm(forms.ModelForm):
offer = forms.ModelChoiceField(queryset=Offer.objects.all(), required=False)
package = forms.ModelChoiceField(queryset=Package.objects.all(), required=False)
def __init__(self, *args, **kwargs):
super(FeatureForm, self).__init__(*args, **kwargs)
add_related_field_wrapper(self, 'offer')
add_related_field_wrapper(self, 'package')
That works fine on Django 1.8.2.
Google pointed me to this page when searching how to get a "+" icon next to fields in a custom form with ForeignKey relationship, so I thought I'd add.
For me, using django-autocomplete-light did the trick very well, using the "add another" functionality.
You don't even need to go that far, and besides, these answers are probably outdated as NONE of them worked for me in any capacity.
What I did to solve this is, as long as you have the ForeignKey field already in your model, then you can just create your custom ModelChoiceField:
class LocationModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return "%" % (obj.name)
The key next is NOT to create a custom field for the ModelChoiceField in your ModelForm (ie location = forms.ModelChoiceField(Location.objects.all()))
In other words, leave that out and in your ModelForm have something like this:
class UserAdminForm(forms.ModelForm):
class Meta:
model = User
fields = '__all__'
Lastly, in your ModelAdmin:
class UserAdmin(admin.ModelAdmin):
model = User
form = UserAdminForm
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'location':
return LocationModelChoiceField(queryset=Location.objects.order_by('name')) # if you want to alphabetize your query
return super().formfield_for_foreignkey(db_field, request, **kwargs)
Alternative Method : Using .remote_field instead of rel
def add_related_field_wrapper(self,form, col_name):
rel_model = form.Meta.model
rel = rel_model._meta.get_field(col_name).remote_field
form.fields[col_name].widget = RelatedFieldWidgetWrapper(form.fields[col_name].widget, rel, admin.site, can_add_related=True, can_change_related=True)
def __init__(self, *args, **kwargs):
super(CustomerAdminForm, self).__init__(*args, **kwargs)
self.add_related_field_wrapper(self, 'offer')
self.add_related_field_wrapper(self, 'package')
Thankyou,

How to pass initial parameter to django's ModelForm instance?

The particular case I have is like this:
I have a Transaction model, with fields: from, to (both are ForeignKeys to auth.User model) and amount. In my form, I'd like to present the user 2 fields to fill in: amount and from (to will be automaticly set to current user in a view function).
Default widget to present a ForeignKey is a select-box. But what I want to get there, is limit the choices to the user.peers queryset members only (so people can only register transactions with their peers and don't get flooded with all system users).
I tried to change the ModelForm to something like this:
class AddTransaction(forms.ModelForm):
from = ModelChoiceField(user.peers)
amount = forms.CharField(label = 'How much?')
class Meta:
model = models.Transaction
But it seems I have to pass the queryset of choices for ModelChoiceField right here - where I don't have an access to the web request.user object.
How can I limit the choices in a form to the user-dependent ones?
Use the following method (hopefully it's clear enough):
class BackupForm(ModelForm):
"""Form for adding and editing backups."""
def __init__(self, *args, **kwargs):
systemid = kwargs.pop('systemid')
super(BackupForm, self).__init__(*args, **kwargs)
self.fields['units'] = forms.ModelMultipleChoiceField(
required=False,
queryset=Unit.objects.filter(system__id=systemid),
widget=forms.SelectMultiple(attrs={'title': _("Add unit")}))
class Meta:
model = Backup
exclude = ('system',)
Create forms like this:
form_backup = BackupForm(request.POST,
instance=Backup,
systemid=system.id)
form_backup = BackupForm(initial=form_backup_defaults,
systemid=system.id)
Hope that helps! Let me know if you need me to explain more in depth.
I ran into this problem as well, and this was my solution:
class ChangeEmailForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
self.user = user
super(ChangeEmailForm, self).__init__(*args, **kwargs)
self.fields['email'].initial = user.email
class Meta:
model = User
fields = ('email',)
def save(self, commit=True):
self.user.email = self.cleaned_data['email']
if commit:
self.user.save()
return self.user
Pass the user into the __init__ of the form, and then call super(…). Then set self.fields['from'].queryset to user.peers

Categories

Resources