Django: limit models.ForeignKey results - django

I have an order model:
class Order(models.Model):
profile = models.ForeignKey(Profile, null=True, blank=True)
Which returns all possible profiles for an order, which is not necessary and slowing down the loading of the admin order page.
I want the profile returned to simply be the profile of the user who placed the order. I've tried changing it to:
class Order(models.Model):
profile = models.ForeignKey(Profile, null=True, blank=True, limit_choices_to={'order': 99999})
which returns the correct profile for order number 99999, but how can I get this dynamically. The Order model is not aware of the 'self', but the order number is contained in the URL.
What is the best way to do this?

If you are using the Django Admin, you can override the method formfield_for_foreignkey on your ModelAdmin class to modify the queryset for the profile field dinamically based on a GET parameter for instance (as you have access to request inside the method.
Look at this example from the docs (adapted for your case):
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "profile":
# You can get the order number from the GET parameter:
# order = request.GET.get('order')
# or a fixed number:
order = '12345'
kwargs["queryset"] = Profile.objects.filter(order=order)
return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
Reference in the docs: https://docs.djangoproject.com/en/1.9/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey

I took another look at this and it seems to be working, although it seems like a bit of hack! The problem was that the order number doesn't seem to exist in the request so I am parsing the URL requested. I put this in my order admin:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "profile":
try:
order = int(filter(str.isdigit, str(request.path_info)))
except:
order = request.GET.get('order')
kwargs["queryset"] = Profile.objects.filter(order=order)
return super(OrderAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
The line that filters the url string for an integer works for the change order page of the admin, but didn't work for the order page overview, so I added the try/except. Any suggestions/improvements welcome!

I assume from the context you're referring to the display on the Django Admin page. If you set
raw_id_fields = {'my_foreign_key'}
you will get a number, text description of related model (from str) and a nice popup box to open an instance of the admin page for your related models.
You could alternatively use
list_select_related = True
to get the same behaivour you have now but with a couple of order of magnitudes lower number of queries.

Related

How to not set image field as null when PUT/Patch in Django rest framework

I have a website that has users each user has his own profile, the problem is when the user wants for example edit his email or username and save this appear profile_image: The submitted data was not a file. Check the encoding type on the form. I thought that the frontend problem and I tried to edit the user username field without touch the image field in the Django rest framework API screen but it shows the same problem the user image field has a path to his image in getting but in put the image input is empty, how I can get edit the user other fields without loss the user image
my view
class UserProfileRetrieveUpdate(generics.GenericAPIView):
serializer_class = UserProfileChangeSerializer
def get(self, request, *args, **kwargs):
serializer = UserProfileChangeSerializer(
instance=request.user)
return Response(serializer.data)
def put(self, request, *args, **kwargs):
user_profile_serializer = UserProfileChangeSerializer(
instance=request.user,
data=request.data,
)
if user_profile_serializer.is_valid():
user_profile_serializer.save()
print(user_profile_serializer.data)
return Response(user_profile_serializer.data, status=status.HTTP_200_OK)
errors = dict()
errors.update(user_profile_serializer.errors)
return Response(errors, status=status.HTTP_400_BAD_REQUEST)
My serializer
class UserProfileChangeSerializer(serializers.ModelSerializer):
profile_image = serializers.ImageField()
class Meta:
model = Account
fields = ('pk', 'email', 'username', 'UserFullName', 'bio', 'profile_image')
There are multiple ways to approach this problem.
A. Add a PATCH request to the class UserProfileRetrieveUpdate in your view.
The slight difference between the PUT and PATCH request can solve the problem.
The difference between PUT and PATCH request.
With PUT request, the client is requesting a full update to the resource and hence must send all the values for the attributes of a model class. Thus, the new instruction replaces the previous one completely. Missing out on certain key value pairs in the request, would lead to setting the default values assigned in the model class.
With PATCH request, the client is requesting a partial update to the resource and hence shall send only the values for the attributes of a model class that need updating. Thus, the new instruction updates the previous instruction with the received key value pairs in the request.
Hence, if we use the PATCH request, then there would be no need to send the profile_image data and still it will remain preserved.
Suggested Code
A new method patch needs to be added to the class UserProfileRetrieveUpdate in your view, and the user_profile_serializer shall be assigned with attribute partial=True to the UserProfileChangeSerializer as it indicates that there will be partial updating of data instead of the default, which is full updating.
def patch(self, request, *args, **kwargs):
user_profile_serializer = UserProfileChangeSerializer(
instance=request.user,
data=request.data,
partial=True
)
if user_profile_serializer.is_valid():
user_profile_serializer.save()
return Response(user_profile_serializer.data, status=status.HTTP_200_OK)
return Response(user_profile_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
B. Modify models.py
This section entails additional changes that can help improve the code.
To accommodate these solutions, edit the class UserProfile in the models.py file.
1. Keep in mind
DON'T ever set ImageField with null=True as Django stores the path from MEDIA_ROOT to file/image in a CharField.
2. Setting imagefield as optional
If the ImageField can be optional, then set the attribute blank=True as it will allow empty values in the database.
profile_image = models.ImageField(upload_to='image_directory', blank=True)
Please Note: image_directory is the path directly in MEDIA_ROOT.
3. Setting a default image (only beneficial for POST request)
If there is a default image that can be set when no image path is passed to the ImageField, then set the attribute default=default.jpg. This is beneficial for POST requests with no value passed to the profile_image as it will set its path to default.jpg.
profile_image = models.ImageField(upload_to='image_directory', default=default.jpg)
Please Note: default.jpg path must exist in the MEDIA_ROOT.
I Solved this qustion by 1- remove the profile_image = serializers.ImageField() from the serializer 2- I added this function to the UserProfileChangeSerializer this can get the user profile image url
def get_profile_image_url(self, obj):
return obj.profile_image.url

Django conditional field display on form

I am trying to make a simple form, that conditionally shows the website input field based on the value of another database field (that is not on the form) status. For the sake of this process the status field is not editable by the user, just by the admin. Both fields are in the same table: profile.
After working at this for a while I copped-out and just did the conditional hiding and showing on the template. But, I realise this is the unsophisticated method, and would like to improve it.
What I tried so far in forms.py:
class WebsiteForm(forms.ModelForm):
class Meta:
model = Profile
fields = (
'e-mail',
'website',
)
if Profile.status == 'personal' :
exclude = ('website',)
This method in forms.py works effectively, in that I can conditionally show and hide the field if I use test comparitors in the if statement like:
if 1 == 1:
or
if 1 != 1:
But, I cannot get an effective test using the field Profile.status, the value in the field seems to be unavailable at the point the if test in forms.py is performed.
If I use print(Profile.status) I get the following output in the terminal: user__profile__status__isnull, so I think this means that I am at least testing the correct field in the database. Although I am also noting that this output only shows at initialisation of runserver, not when the form page is accessed.
One final point, the user is authenticated and editing their own record.
Any help very much appreciated.
After a lot of trial and even more error, and some wide-ranging searching, I found the answer via the documentation at https://ccbv.co.uk/.
Essentially the path I decided to take was to use a different form for the respective fields that I wanted to use (I'm sure there are other solutions out there that add or subtract fields from the views). This involved changing the form_class with get_form_class:
# views.py
class telephone_view(UpdateView):
template_name = 'account/telephone.html'
#no need to define "form_class" here
#form_class = TelephoneForm
success_url = '/accounts/telephone/'
def get_form_class(self):
if self.request.user.profile.status == 'managed':
messages.success(self.request, _('you got the managed form'))
return TelephoneFormExtended
else:
messages.success(self.request, _('you got the other form'))
return TelephoneFormLight
def get_object(self, queryset=None):
return Profile.get_or_create_for_user(self.request.user)
def form_valid(self, form):
messages.success(self.request, _('Your telephone setting was updated'))
return super(telephone_view, self).form_valid(form)
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(telephone_view, self).dispatch(*args, **kwargs)
After working it out for myself I also found this answer which does the same thing:
Updateview with dynamic form_class

Validate Django model data by calculating the sum of multiple rows

I have two models:
class User(models.Model):
name = models.CharField(max_length=32)
class Referral(models.Model):
referring_user = models.ForeignKey(User, related_name="referrals")
referred_user = models.ForeignKey(User, related_name="referrers")
percentage = models.PositiveIntegerField()
The idea is that every user has n referrers, and should have at least one. Each referrer has a percentage value which should add up to 100% when added to the other referrers.
So User "Alice" might have referrers "Bob" (50%) and "Cynthia" (50%), and User "Donald" might have one referrer: "Erin" (100%).
The problem I have is with validation. Is there a way (preferably one that plays nice with the Django admin using admin.TabularInline) that I can have validation reject the saving of a User if the sum of Refferrals != 100%?
Ideally I want this to happen at the form/admin level and not by overriding User.save(), but at this point I don't know where to start. Most of Django's validation code appears to be atomic, and validation across multiple rows is not something I've done in Django before.
After Jerry Meng suggested I look into the data property and not cleaned_data, I started poking around admin.ModelAdmin to see how I might access that method. I found get_form which appears to return a form class, so I overrode that method to capture the returning class, subclass it, and override .clean() in there.
Once inside, I looped over self.data, using a regex to find the relevant fields and then literally did the math.
import re
from django import forms
from django.contrib import admin
class UserAdmin(admin.ModelAdmin):
# ...
def get_form(self, request, obj=None, **kwargs):
parent = admin.ModelAdmin.get_form(self, request, obj=None, **kwargs)
class PercentageSummingForm(parent):
def clean(self):
cleaned_data = parent.clean(self)
total_percentage = 0
regex = re.compile(r"^referrers-(\d+)-percentage$")
for k, v in self.data.items():
match = re.match(regex, k)
if match:
try:
total_percentage += int(v)
except ValueError:
raise forms.ValidationError(
"Percentage values must be integers"
)
if not total_percentage == 100:
raise forms.ValidationError(
"Percentage values must add up to 100"
)
return cleaned_data
return PercentageSummingForm
As per the Django docs, clean() is the official function to implement for your purposes. You could imagine a function that looks like this:
from django.core.exceptions import ValidationError
def clean(self):
total_percentage = 0
for referrer in self.referrers.all():
total_percentage += referrer.percentage
if total_percentage !== 100:
raise ValidationError("Referrer percentage does not equal 100")

Django form validation with authenticated user as a field

Model:
class ProjectType(models.Model):
project_type_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=45, help_text='Type of project', verbose_name='Project Type')
slug = models.SlugField(max_length=45, blank=True)
description = models.CharField(max_length=400, help_text='Description of the main purpose of the project', verbose_name='Project Type Description')
default = models.BooleanField(default=False)
owner = models.ForeignKey(User)
class Meta:
...
unique_together = (('slug', 'owner'),('name', 'owner'))
I need a form to create/update ProjectType's. Please note the owner field - it is supposed to be current logged-in user. The question is how to ensure that constraints in the unique_together are validated correctly.
I do not want to show owner field on the form - it's the current user, so it should be set automatically by the system. But no matter how I try to do this, either validation does not work, or there are other errors.
Among approaches I tried (individually or in combination):
Creating a hidden field in the related ModelField
Defining init in ProjectTypeForm (in various ways), for example:
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(ProjectTypeForm, self).__init__(*args, **kwargs)
self.fields['owner'].initial = self.user
Setting values in the view like:
...
if request.method == 'POST':
project_type = ProjectType(owner=request.user)
form = ProjectTypeForm(request.POST, instance=project_type, user = request.user.pk) # also tries w/o pk
...
Overriding clean() method of the form in various ways, along these lines:
def clean(self):
cleaned_data = super(ProjectTypeForm, self).clean()
slug=cleaned_data.get('slug')
owner = cleaned_data.get('owner')
if slug:
user = User.objects.get(pk=owner)
...
Many of these approaches are based on various answers found on stackoverflow.com. However, no matter what I try, I cannot find a way to accomplish what I need: (1) auto-setting of the owner field and (2) validation for uniqueness: owner/type_name and owner/type_slug. Typical errors I have is that (a) owner is not recognized as a User (it's treated as a PK), (b) incorrect validation (like lack of it or it misses the fact that it's the same record being edited, etc.), (c) owner is a required field.
For the record - if the owner is a regular field in the form, everything works as expected, but I cannot allow users to set the owner value.
Is there any, hopefully elegant, solution to this?
Thanks!
Exclude the owner field from your form, and save the user in your form's init method - then you can use it to validate the form, eg
class ProjectTypeForm(...):
...
def __init__(self, user, *args, **kwargs):
super(ProjectTypeForm, self).__init__(*args, **kwargs)
self.user = user
def clean(self):
user_projects = ProjectType.objects.filter(owner=self.user)
if user_projects.filter(slug=self.cleaned_data['slug']):
raise forms.ValidationError('...')
elif user_projects.filter(name=self.cleaned_data['name']):
raise forms.ValidationError('...')
else:
return self.cleaned_data
Then in your view, do something like this when creating a new ProjectType:
if request.method == 'POST':
form = ProjectTypeForm(request.user, request.POST)
if form.is_valid():
ptype = form.save(commit=False)
ptype.owner = request.user
ptype.save()
You shouldn't need that to save existing ProjectType objects though.
As I mentioned in my comment, one possible solution is essentially to go along with Django forms and use the owner field on the form. So, what I've done is modified init in this way:
def __init__(self, user, *args, **kwargs):
super(ProjectTypeForm, self).__init__(*args, **kwargs)
self.fields['owner'] = forms.ModelChoiceField(
label='Owner*',
queryset=User.objects.filter(username=user.username),
help_text="Project types are unique to logged-in users who are set as their owners.",
required=True,
empty_label=None)
Basically, what it does it is still using ChoiceField but sets it to one option - current user. In addition, empty_label=None ensures that there is no "empty" choice. The effect is (since username is unique) that current user name appears visible and is the only choice in the otherwise dropdown list with more choices.
In the view I follow this approach:
...
if request.method == 'POST':
project_type = ProjectType()
form = ProjectTypeForm(request.user,request.POST, instance=project_type,)
if form.is_valid():
project_type.save()
return HttpResponseRedirect(reverse('project_types'))
else:
form = ProjectTypeForm(request.user)
...
Basically, that's it - validation of unique constraints (and the whole thing) works like a charm.
Do I like this solution? No. I consider it a hack (ironically, even if it goes along with standard Django approaches). But it requires something that is totally unnecessary. One benefit of this approach is that it clearly communicates to the current user that s/he is set as the project type owner. But even with this in mind I would rather show a message (instead of a field) that Current user X will be set as the owner of the project type being created. So, if someone has a better solution, please submit it to illustrate the full power and flexibility of Django.

How to limit fields in django-admin depending on user?

I suppose similar problem would have been discussed here, but I couldn't find it.
Let's suppose I have an Editor and a Supervisor. I want the Editor to be able to add new content (eg. a news post) but before publication it has to be acknowledged by Supervisor.
When Editor lists all items, I want to set some fields on the models (like an 'ack' field) as read-only (so he could know what had been ack'ed and what's still waiting approval) but the Supervisor should be able to change everything (list_editable would be perfect)
What are the possible solutions to this problem?
I think there is a more easy way to do that:
Guest we have the same problem of Blog-Post
blog/models.py:
Class Blog(models.Model):
...
#fields like autor, title, stuff..
...
class Post(models.Model):
...
#fields like blog, title, stuff..
...
approved = models.BooleanField(default=False)
approved_by = models.ForeignKey(User)
class Meta:
permissions = (
("can_approve_post", "Can approve post"),
)
And the magic is in the admin:
blog/admin.py:
...
from django.views.decorators.csrf import csrf_protect
...
def has_approval_permission(request, obj=None):
if request.user.has_perm('blog.can_approve_post'):
return True
return False
Class PostAdmin(admin.ModelAdmin):
#csrf_protect
def changelist_view(self, request, extra_context=None):
if not has_approval_permission(request):
self.list_display = [...] # list of fields to show if user can't approve the post
self.editable = [...]
else:
self.list_display = [...] # list of fields to show if user can approve the post
return super(PostAdmin, self).changelist_view(request, extra_context)
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # same thing
else:
self.fields = ['approved']
return super(PostAdmin, self).get_form(request, obj, **kwargs)
In this way you can use the api of custom permission in django, and you can override the methods for save the model or get the queryset if you have to. In the methid has_approval_permission you can define the logic of when the user can or can't to do something.
Starting Django 1.7, you can now use the get_fields hook which makes it so much simpler to implement conditional fields.
class MyModelAdmin(admin.ModelAdmin):
...
def get_fields(self, request, obj=None):
fields = super(MyModelAdmin, self).get_fields(request, obj)
if request.user.is_superuser:
fields += ('approve',)
return fields
I have a system kind of like this on a project that I'm just finishing up. There will be a lot of work to put this together, but here are some of the components that I had to make my system work:
You need a way to define an Editor and a Supervisor. The three ways this could be done are 1.) by having an M2M field that defines the Supervisor [and assuming that everyone else with permission to read/write is an Editor], 2.) make 2 new User models that inherit from User [probably more work than necessary] or 3.) use the django.auth ability to have a UserProfile class. Method #1 is probably the most reasonable.
Once you can identify what type the user is, you need a way to generically enforce the authorization you're looking for. I think the best route here is probably a generic admin model.
Lastly you'll need some type of "parent" model that will hold the permissions for whatever needs to be moderated. For example, if you had a Blog model and BlogPost model (assuming multiple blogs within the same site), then Blog is the parent model (it can hold the permissions of who approves what). However, if you have a single blog and there is no parent model for BlogPost, we'll need some place to store the permissions. I've found the ContentType works out well here.
Here's some ideas in code (untested and more conceptual than actual).
Make a new app called 'moderated' which will hold our generic stuff.
moderated.models.py
class ModeratedModelParent(models.Model):
"""Class to govern rules for a given model"""
content_type = models.OneToOneField(ContentType)
can_approve = models.ManyToManyField(User)
class ModeratedModel(models.Model):
"""Class to implement a model that is moderated by a supervisor"""
is_approved = models.BooleanField(default=False)
def get_parent_instance(self):
"""
If the model already has a parent, override to return the parent's type
For example, for a BlogPost model it could return self.parent_blog
"""
# Get self's ContentType then return ModeratedModelParent for that type
self_content_type = ContentType.objects.get_for_model(self)
try:
return ModeratedModelParent.objects.get(content_type=self_content_type)
except:
# Create it if it doesn't already exist...
return ModeratedModelParent.objects.create(content_type=self_content_type).save()
class Meta:
abstract = True
So now we should have a generic, re-usable bit of code that we can identify the permission for a given model (which we'll identify the model by it's Content Type).
Next, we can implement our policies in the admin, again through a generic model:
moderated.admin.py
class ModeratedModelAdmin(admin.ModelAdmin):
# Save our request object for later
def __call__(self, request, url):
self.request = request
return super(ModeratedModelAdmin, self).__call__(request, url)
# Adjust our 'is_approved' widget based on the parent permissions
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'is_approved':
if not self.request.user in self.get_parent_instance().can_approve.all():
kwargs['widget'] = forms.CheckboxInput(attrs={ 'disabled':'disabled' })
# Enforce our "unapproved" policy on saves
def save_model(self, *args, **kwargs):
if not self.request.user in self.get_parent_instance().can_approve.all():
self.is_approved = False
return super(ModeratedModelAdmin, self).save_model(*args, **kwargs)
Once these are setup and working, we can re-use them across many models as I've found once you add structured permissions for something like this, you easily want it for many other things.
Say for instance you have a news model, you would simply need to make it inherit off of the model we just made and you're good.
# in your app's models.py
class NewsItem(ModeratedModel):
title = models.CharField(max_length=200)
text = models.TextField()
# in your app's admin.py
class NewsItemAdmin(ModeratedModelAdmin):
pass
admin.site.register(NewsItem, NewsItemAdmin)
I'm sure I made some code errors and mistakes in there, but hopefully this can give you some ideas to act as a launching pad for whatever you decide to implement.
The last thing you have to do, which I'll leave up to you, is to implement filtering for the is_approved items. (ie. you don't want un-approved items being listed on the news section, right?)
The problem using the approach outlined by #diegueus9 is that the ModelAdmin acts liked a singleton and is not instanced for each request. This means that each request is modifying the same ModelAdmin object that is being accessed by other requests, which isn't ideal. Below is the proposed solutions by #diegueus9:
# For example, get_form() modifies the single PostAdmin's fields on each request
...
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # list of fields to show if user can't approve the post
else:
self.fields = ['approved', ...] # add 'approved' to the list of fields if the user can approve the post
...
An alternative approach would be to pass fields as a keyword arg to the parent's get_form() method like so:
...
from django.contrib.admin.util import flatten_fieldsets
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if has_approval_permission(request, obj):
fields = ['approved']
if self.declared_fieldsets:
fields += flatten_fieldsets(self.declared_fieldsets)
# Update the keyword args as needed to allow the parent to build
# and return the ModelForm instance you require for the user given their perms
kwargs.update({'fields': fields})
return super(PostAdmin, self).get_form(request, obj=None, **kwargs)
...
This way, you are not modifying the PostAdmin singleton on every request; you are simply passing the appropriate keyword args needed to build and return the ModelForm from the parent.
It is probably worth looking at the get_form() method on the base ModelAdmin for more info: https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L431