Django custom user edit form: get_fieldsets() error - python

In Django's admin.py, why, even though this works:
class StudentAdmin(UserAdmin):
add_form = UserCreationForm
form = CustomChangeForm
fieldsets = UserAdmin.fieldsets
these do not?
class StudentAdmin(UserAdmin):
add_form = UserCreationForm
form = CustomChangeForm
def get_fieldsets(self, request, obj = None):
return UserAdmin.fieldsets
or
class StudentAdmin(UserAdmin):
add_form = UserCreationForm
form = CustomChangeForm
def get_fieldsets(self, request, obj = None):
return super(UserAdmin, self).get_fieldsets(request, obj)
Shouldn't they be equivalent?
The second set gives me an exception u"Key 'password' not found in Form", while the first one works fine.
The point, in the long term, is obviously to get more specific things working, but first I'd like to figure out what I got wrong so far.
Similarly, adding:
inlines = (MyInline,)
to the class works. But adding this:
def get_inline_instances(self, request, obj=None):
return (MyInline,)
throws the exception: unbound method get_formset() must be called with MyInline instance as first argument (got WSGIRequest instance instead). To the extent of my understanding, these two should also be equivalent.
Thanks.

Related

Django admin inline - show only when changing object

I have a UserProfile which has many required (not null but blank) fields and I'm trying not to show it when adding a user.
I tried multiple things, for example:
def get_formsets_with_inlines(self, request, obj=None):
if not obj:
return []
return super().get_formsets_with_inlines(request, obj)
But after saving User, django raises error which says that fields from UserProfile can't be null.
Do you know how to make it work?
As of Django 3.0, there is a ModelAdmin.get_inlines() method, which you can override like this:
def get_inlines(self, request, obj=None):
if obj:
return [FirstInline, SecondInline]
else:
return []
See Django Documentation.
ModelAdmin provides a method get_inline_instances for conditional inlines.
from the docs:
The get_inline_instances method is given the HttpRequest and the obj
being edited (or None on an add form) and is expected to return a list
or tuple of InlineModelAdmin objects.
for example:
class MyModelAdmin(admin.ModelAdmin):
inlines = (MyInline,)
def get_inline_instances(self, request, obj=None):
return [inline(self.model, self.admin_site) for inline in self.inlines]
here you can check if obj is present or not.
One important point from the docs:
If you override this method, make sure that the returned inlines are
instances of the classes defined in inlines or you might encounter a
“Bad Request” error when adding related objects.
I use "get_inlines()" added to Django since v3.0 instead of "inlines = ()" as shown below so that "AddressInline" is not displayed when adding a user but it's displayed when changing a user:
# "admin.py"
class AddressInline(admin.TabularInline):
model = Address
#admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
# inlines = (AddressInline,)
# Here
def get_inlines(self, request, obj=None):
if obj:
return (AddressInline,)
else:
return ()
fieldsets = (
# ...
)
add_fieldsets = (
# ...
)

How to use UserPassesTestMixin in generic Updateview

views.py:
class ProfileView(DetailView):
model = User
template_name ="profile/profile_view.html"
class ProfileEdit(UserPassesTestMixin, UpdateView):
login_url = '/login/'
model = User
form_class = ProfileForm
template_name="profile/profile_new.html"
def test_func(request, user_id):
user = User.objects.get(pk=user_id)
if request.user == user.user:
return True
else:
return HttpResponse("<h1>You will not be able to edit other profiles</h1>")
urls.py
url(r'^(?P<pk>[0-9]+)/edit/$', ProfileEdit.as_view(), name='profile_edit')
I got this error: (test_func() missing 1 required positional argument: 'user_id')
This post addresses the same issue I want to solve. Is it different for class based views?
There are a couple of things wrong with this method.
The first is that, like any method, it needs self as the first argument.
Secondly, as the error states, the code that calls this view is not expecting to pass user_id as a parameter. You should get that value, as you would anywhere else in a class-based view, from self.kwargs.
Thirdly, you try to access the user attribute of the user, but that doesn't exist. You could just compare the user directly, but in fact there is no point in querying the user model at all; you can just compare the ID with the current user's ID.
def test_func(self, request):
if request.user.id == int(self.kwargs['pk']):
...
However, you should also ask yourself whether any of this is the right thing to be doing. Rather than passing the user id in the URL and then checking it's the same as the logged-in user's id, you could just omit the ID completely and just always access the current user. You would do that by defining get_object in the view to return request.user; you could then remove the UserPassesTestMixin, the test_func, and the URL parameter.
I got the answer i needed.
views.py
class ProfileEdit(UserPassesTestMixin, LoginRequiredMixin, UpdateView):
model = User
form_class = ProfileForm
template_name="profile/profile_new.html"
def test_func(self):
x = self.request.user.username
y = self.kwargs['slug']
if x == y:
return True
else:
if self.request.user.is_authenticated():
raise Http404("You are not authenticated to edit this profile")
I replaced 'id' with 'slug' i will be using that to edit

map each post to the user who posted it by foreign key in django?

I want to connect each post with the logged in user who posted it.
models.py
from django.conf import settings
from django.db import models
# Create your models here.
class Campagin(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
title = models.CharField(max_length=120)
media = models.FileField()
description = models.TextField(max_length=220)
timestamp = models.DateTimeField(auto_now=False, auto_now_add=True)
updated = models.DateTimeField(auto_now=True, auto_now_add=False)
def __str__(self):
return self.title`
As you can see the posts were made by two different users, but the relation shows that it is made by the first user
this image shows the registered users..
Views.py
class NewCampagin(LoginRequiredMixin, CreateView):
template_name = 'campagin/new_campagin.html'
model = Campagin
fields = ['title','media','description']
def get_absolute_url(self):
return reverse('campagin:active_campagin')
Okay so CreateView allows you to specify the model and fields attributes to implicitly create a form for you. It's quite neat for quick form submissions but in your case, you will need to make some customizations before saving the Campaign object into the database (linking up the current logged in user).
As a result, you will need to create your own form first (create a file called forms.py which can be next to your views.py) and enter this code:
class CampaignForm(ModelForm): # Import ModelForm too.
def __init__(self, *args, **kwargs):
# We need to get access the currently logged in user so set it as an instance variable of CampaignForm.
self.user = kwargs.pop('user', None)
super(CampaignForm, self).__init__(*args, **kwargs)
class Meta:
model = models.Campaign # you need to import this from your models.py class
fields = ['title','media','description']
def save(self, commit=True):
# This is where we need to insert the currently logged in user into the Campaign instance.
instance = super(CampaignForm, self).save(commit=False)
# Once the all the other attributes are inserted, we just need to insert the current logged in user
# into the instance.
instance.user = self.user
if commit:
instance.save()
return instance
Now that we have our forms.py all ready to go we just need to modify your views.py:
class NewCampagin(LoginRequiredMixin, CreateView):
template_name = 'campagin/new_campagin.html'
form_class = forms.CampaignForm # Again, you'll need to import this carefully from our newly created forms.py
model = models.Campaign # Import this.
queryset = models.Campaign.objects.all()
def get_absolute_url(self):
return reverse('campagin:active_campagin') # Sending user object to the form, to verify which fields to display/remove (depending on group)
def get_form_kwargs(self):
# In order for us to access the current user in CampaignForm, we need to actually pass it accross.
# As such, we do this as shown below.
kwargs = super(NewCampaign, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
What's actually happening with my POST requests under the bonnet??
Note: This is just extra information for the sake of learning. You do
not need to read this part if you don't care about how your class
based view is actually handling your post request.
Essentially CreateView looks like this:
class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
"""
View for creating a new object instance,
with a response rendered by template.
"""
template_name_suffix = '_form'
Doesn't look that interesting but if we analyse BaseCreateView:
class BaseCreateView(ModelFormMixin, ProcessFormView):
"""
Base view for creating an new object instance.
Using this base class requires subclassing to provide a response mixin.
"""
def post(self, request, *args, **kwargs):
self.object = None
return super(BaseCreateView, self).post(request, *args, **kwargs)
we can see we are inheriting from two very important classes ModelFormMixin and ProcessFormView. Now the line, return super(BaseCreateView, self).post(request, *args, **kwargs), essentially calls the post function in ProcessFormView which looks like this:
def post(self, request, *args, **kwargs):
"""
Handles POST requests, instantiating a form instance with the passed
POST variables and then checked for validity.
"""
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
As you can see, your CreateView really just boils down to this small post function which simply gets a specified form and validates + saves it. There's 2 questions to ask at this point.
1) What does form = self.get_form() do since I didn't even specify my form?
2) What is self.form_valid(form) actually doing?
To answer the first question, self.get_form() essentially calls another function form_class = self.get_form_class() and this function is actually found in ModelFormMixin (the one where inherited from!):
def get_form_class(self):
"""
Returns the form class to use in this view.
"""
if self.fields is not None and self.form_class:
raise ImproperlyConfigured(
"Specifying both 'fields' and 'form_class' is not permitted."
)
if self.form_class:
return self.form_class
else:
if self.model is not None:
# If a model has been explicitly provided, use it
model = self.model
elif hasattr(self, 'object') and self.object is not None:
# If this view is operating on a single object, use
# the class of that object
model = self.object.__class__
else:
# Try to get a queryset and extract the model class
# from that
model = self.get_queryset().model
if self.fields is None:
raise ImproperlyConfigured(
"Using ModelFormMixin (base class of %s) without "
"the 'fields' attribute is prohibited." % self.__class__.__name__
)
# THIS IS WHERE YOUR FORM WAS BEING IMPLICITLY CREATED.
return model_forms.modelform_factory(model, fields=self.fields)
As you can see, this function is where your form was being implicitly created (see very last line). We needed to add more functionality in your case so we created our own forms.py and specified form_class in the views.py as a result.
To answer the second question, we need to look at the function (self.form_valid(form)) call's source code:
def form_valid(self, form):
"""
If the form is valid, save the associated model.
"""
# THIS IS A CRUCIAL LINE.
# This is where your actual Campaign object is created. We OVERRIDE the save() function call in our forms.py so that you could link up your logged in user to the campaign object before saving.
self.object = form.save()
return super(ModelFormMixin, self).form_valid(form)
So here we are simply saving the object.
I hope this helps you!
More information at https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-editing/#createview

Django Rest Framework permissions not being called

I'm currently converting all my views to generics, as I like how cleaner the code gets. I am trying to make my User detail view, like so:
# User.views
from Common import view_mixins, view_filters, view_permissions
class UserDetail(view_mixins.IntOrStrLookupMixin, generics.RetrieveUpdateDestroyAPIView):
queryset = Profile.objects.all()
lookup_fields = ('user__pk', 'user__username')
lookup_url_kwarg = 'userid'
filter_backends = (view_filters.ResourceVisibilityFilter, )
permission_classes = (view_permissions.IsOwnerOrReadOnly, )
serializer_class = ProfileSerializer
def update(self, request, *args, **kwargs):
user = self.get_object()
return Response('whatever')
# Common.view_permissions
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
class IsOwnerOrReadOnly(permissions.BasePermission):
'''
Owner of object can GET, PUT, DELETE. Everyone else can GET.
'''
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
print('did you call me?')
return (
request.method in SAFE_METHODS
or
obj.user == request.user
)
# Common.view_mixins
class IntOrStrLookupMixin(object):
"""
Apply to views that can be looked up by slug or pk
"""
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.kwargs:
argument = self.kwargs[field]
if is_int(argument):
filter[self.lookup_fields[0]] = argument
else:
filter[self.lookup_fields[1]] = argument
return get_object_or_404(queryset, **filter)
So my issue is, the permission is never called. I can get the user just fine, but anyone can do PUT or DELETE on which I am trying to prevent.
In my mixin, I had to call the check_object_permissions method.
class IntOrStrLookupMixin(object):
"""
Apply to views that can be looked up by slug or pk
"""
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.kwargs:
argument = self.kwargs[field]
if is_int(argument):
filter[self.lookup_fields[0]] = argument
else:
filter[self.lookup_fields[1]] = argument
obj = get_object_or_404(queryset, **filter)
self.check_object_permissions(self.request, obj)
return obj
I think I encountered this problem. The object level permissions are only called when you use get_object() method to get the object being operated on. Update your update() with this line and you should see the permissions being called. And update your custom get_object() to either call the super method or call the permissions directly
class UserDetail(view_mixins.IntOrStrLookupMixin, generics.RetrieveUpdateDestroyAPIView):
... blah blah blah
def update(self, request):
user = self.get_object()
return Response('whatever')
class IntOrStrLookupMixin(object):
def get_object(self):
... retrieve the object ...
self.check_object_permissions(self.request, obj)
return obj
Edit: I filed a bug report about this with the DRF team and they updated the docs. On the permission page in the docs it says "Object level permissions are run by REST framework's generic views when .get_object() is called". I agree that this is a rather subtle thing and is easy to miss.
Edit #2: Looks like the problem is not only in the update() not calling get_object(), but also in the IntOrStrLookupMixin mixin redefining get_object() method. Updated the code to reflect

Django hide field when creating new object

I want to hide field name when user is creating new object, but it must be visible if user wants to edit this object.
I tried exclude method but it makes field invisible when i try to edit this field. for example i want to hide status field.
class Toys(BaseModel):
name = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag, related_name='Item_tags')
price = models.CharField(max_length=255)
status = models.BooleanField(default=False)
In the model admin that you register in your admin.py file you can overload the get_form(self, request, obj=None, **kwargs) method. As you can see it takes the obj argument, it is None only on add (not None on change).
From there you could mess around with the form to exclude the form field "name" from it only if the obj is None.
In Django 1.10 that method is in django.contrib.admin.options.ModelAdmin.get_form.
EDIT 1 (this is by far not the best solution)
I can't give you a full solution here, but you can start with something like:
# admin.py
from django.contrib import admin
from models import Toys
class ToysModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
# all the code you have in the original
# django.contrib.admin.options.ModelAdmin.get_form
# up to the last try except
if obj is not None:
defaults['fields'] = ('tags', 'price', 'status', )
try:
return modelform_factory(self.model, **defaults)
except FieldError as e:
raise FieldError(
'%s. Check fields/fieldsets/exclude attributes of class %s.'
% (e, self.__class__.__name__)
)
admin.site.register(Toys, ToysModelAdmin)
EDIT 2 (this is a better example)
# admin.py
from collections import OrderedDict
from django.contrib import admin
from models import Toys
class ToysModelAdmin(admin.ModelAdmin):
# this is not perfect as you'll need to keep track of your
# model changes also here, but you won't accidentally add
# a field that is not supposed to be editable
_add_fields = ('tags', 'price', 'status', )
def get_form(self, request, obj=None, **kwargs):
model_form = super(ToysModelAdmin, self).get_form(
request, obj, **kwargs
)
if obj is None:
model_form._meta.fields = self._add_fields
model_form.base_fields = OrderedDict(**[
(field, model_form.base_fields[field])
for field in self._add_fields
])
return model_form
I had same problem.
I wanted to exclude some columns only in create form.
You can override get_exclude() method
from django.contrib import admin
from . import models
#admin.register(models.Toys)
class ToysAdmin(admin.ModelAdmin):
def get_exclude(self, request, obj=None):
exclude = list(super().get_exclude(request, obj) or [])
if obj==None:
# columns to exclude when it's create form
exclude += ["status","column_to_hide_on_create"]
return tuple(set(exclude))

Categories

Resources