How to create a mixin pattern in Python - python

I'm trying to understand the concept of mixins using the following example:
I have a simple serializer using the DRF:
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = Test
fields = ('url', 'name', 'user')
I would like to create a mixin which enhances (overwrites the class get_queryset) for any custom serializer by added a check that the user owns the objects and only shows these items for example...
def get_queryset(self):
"""
This view should return a list of all the items
for the currently authenticated user.
"""
user = self.request.user
return ???????.objects.filter(user=user)
So my TestSerializer would look like this:
class TestSerializer(serializers.ModelSerializer, UserListMixin):
etc
and UserListMixin:
class UserListMixin(object):
"""
Filtering based on the value of request.user.
"""
def get_queryset(self, *args, **kwargs):
"""
This view should return a list of all the purchases
for the currently authenticated user.
"""
user = self.request.user
return super([?????????], self).get_queryset(*args, **kwargs).filter(user=user)
What I'm having difficulty with is creating the UserListMixin class. How can I return the correct object based on what I'm extending return [OBJECT].objects.filter(user=user) and would this approach work?

Filters are chainable, so the best thing to do here is to call the super method to get the default queryset, then add your filter on top:
def get_queryset(self, *args, **kwargs)
user = self.request.user
return super(UserListMixin, self).get_queryset(*args, **kwargs).filter(user=user)

Related

Django: validation of restricted Foreign keys forms in Mixins Class views

Context: how I handled foreign keys restrictions on GET
I have some trouble validating this form:
class RecordConsultationForm(forms.ModelForm):
class Meta:
model = Consultation
fields = ["fk_type", "fk_client", "duration", "date"]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(RecordConsultationForm, self).__init__(*args, **kwargs)
self.fields['fk_client'].queryset = Client.objects.filter(fk_owner=self.user) # <=====HERE
The queryset restricts the available clients to users. Pretty effective, I just had to add the following to get_context_data():
#method_decorator(login_required, name='dispatch')
class BrowseConsultations(BrowseAndCreate):
template_name = "console/browse_consultations.html"
model = Consultation
form_class = RecordConsultationForm
success_url = 'browse_consultations'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = self.form_class(user = self.request.user) #<=====HERE
return context
def post(self, request):
form = self.form_class(user = self.request.user) #<=====HERE
return super().post(request)
Form validation on BrowseConsultations
Despite what I added in get_context_data() and post(), form data on POST does not seem valid, as if user were None (so it seems):
Invalid fk_client: Select a valid choice. 2 is not one of the available choices.
Maybe get_context_data() was not the proper place to set the user? Is there a way I could tweak the form in post()? Is it the proper way to do it?
Additional details
BrowseAndCreate BrowseConsultations inherits from is a Mixing class I created to handle common ordering tasks and messages. Here is a portion of it:
#method_decorator(login_required, name='dispatch')
class BrowseAndCreate(CreateView, TemplateView):
"""display a bunch of items from a model and the form to create new instances"""
def post(self, request):
super().post(request)
return redirect(self.success_url)
def form_valid(self, form):
super().form_valid(form)
messages.add_message(self.request, messages.SUCCESS,
"Recorded in {}".format(self.object.status))
def form_invalid(self, form):
for e in form.errors.items():
messages.add_message(self.request, messages.WARNING,
"Invalid {}: {}".format(e[0], e[1][0]))
Environment
django 3.0.4
python 3.7
First of all, the CreateView (that your BrowseAndCreate inherits from) handles form validation in the post method, where it calls form_valid on success and form_invalid on failure. Both these methods should return an HTTP response.
Furthermore, the get_context_data from FormMixin that you are overriding already takes care of getting the form data.
If you need the user in your form, you could have this in your form:
class RecordConsultationForm(forms.Form):
def __init__(self, user, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
And this in your view:
class BrowseConsultations(BrowseAndCreate):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs

Cant create a record within viewset custom view based on url parameters

Hello I have a django rest framework view set. For the create view I want to create a custom view that will create a new record based on two different parameters that are passed on through the url which are namespace and path. I looked at the documentation but i couldnt find how it should look like. I am noit sure what I need to do in order to create a record based on both url parameters.
I basically tried setting the create to a CreateAPIView but it did not work
class PreferenceViewSet(viewsets.ViewSet):
queryset = Preference.objects.all()
serializer_class = PreferenceSerializer
def get_permissions(self):
if self.action == 'create' or self.action == 'destroy':
permission_classes = [IsAuthenticated]
else:
permission_classes = [IsAdminUser]
return [permission() for permission in permission_classes]
def list(self, request):
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
def create(self, request):
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
I want to setup the create to create a preference with the two parameters that are passe in the url
path('preferences/<str:namespace>/<str:path>', preference_path, name='preference-path'),
I wanted it to create a new object with the namespace and path
You need to do this in 2 steps:
Add the url arguments to serializer context from viewset
Override create method on the serializer and use data passed on the context to create the record
So, at first override get_serializer_context method to add the arguments to context:
class PreferenceViewSet(viewsets.ViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._namespace = None
self._path = None
...
...
def get_serializer_context(self):
context = super().get_serializer_context()
context.update(namespace=self._namespace, path=self._path)
return context
def create(self, request):
self._namespace = self.kwargs['namespace']
self._path = self.kwargs['path']
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
Now, you can access the parameters inside the overriden create method of the serializer and create the record as you want e.g.:
class PreferenceSerializer(serializers.HyperlinkedModelSerializer):
...
...
def create(self, validated_data):
namespace = self.context['namespace']
path = self.context['path']
# Create object here based on the params

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

Integration of custom methods in browsable api

I'm creating an application that has Item and Customer objects. The Customer has a property watchList which is a list of Item.
Now I want to create a REST api for these watch lists. It should list all items in the watch list of the current customer and offer a method to add (already existing) items to the list.
class WatchListViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = ItemSerializer
def get_queryset(self):
return Customer.objects.get(user = self.request.user).watchList
def list(self, request):
queryset = Customer.objects.get(user = self.request.user).watchList
serializer = ItemSerializer(queryset, context={'request': request}, many=True)
return Response(serializer.data)
#list_route(methods=['POST'])
def add(self, request, *args, **kwargs):
#request.data.id contains the id of the item that should be added
# ...
return Response(status=status.HTTP_201_CREATED)
However, when I request localhost:800/api/watchList/add/, I see a form for an item not an input for the id of an existing item (or even better, a dropdown/selection field).
How can I inform the browsable api that the requested input type differs from the rest of the view set? Can this be connected to some kind of automatic validation (the method won't be executed if no id is passed)?
It turned out this solves my problem:
class ItemIdSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.IntegerField()
class Meta:
model = Item
fields = ('id',)
and
class WatchListViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def get_serializer(self):
if self.action == WatchListViewSet.add.__name__:
return ItemIdSerializer()
return super(WatchListViewSet, self).get_serializer()
The method get_serializer returns the special ItemIdSerializer if the add action is executed.
By defining the attribute id as serializers.IntegerField() the default behaviour of hiding the read-only attribute id is overwritten.
However, this approach doesn't provide any automatic verification, it's still possible to execute the add method without providing an id.

Field Level Permission Django

Today i came up with a requirement where i need to implement field level permission so looking for the best possible way.
class ABC(models.Model):
field1 = .....
field2 = .....
field3 = .....
Create two groups(A and B) and assigned permission that both one can add/edit/delete
and the other can only add/edit. But now need some help in this :-
I want if the first group user logs in in the admin he should be able to see all the three fields but if second group user logs in they should only see field1.
I want this in django admin as i need to perform some manipulations after these.My django version is 1.3
Thanks in advance
In your admin.py
class ABCAdmin(admin.ModelAdmin):
fields = [.....] # here comes the fields open to all users
def change_view(self, request, object_id, extra_context=None): # override default admin change behaviour
if request.user in gruop2: # an example
self.fields.append('field2') # add field 2 to your `fields`
self.fields.append('field3') # add field 3 to your `fields`
You can use the docs to see what is available. Above is an example taken from one of my usages. You may also need to define change_view and add_view too.
Just in case someone else stumble about this, I had some issues with the given accepted answer. Every time the view got refreshed, it appended the fields over and over again. As well to the desired restricted view, where it shouldn't appear.
So, according to the docs, I made it working as follows:
Creating a custom ModelForm
class AbcStaffForm(ModelForm):
class Meta:
model = Abc
exclude = ["manager", "foo", "bar",]
Overwrite get_form() in AbcModelAdmin & refered the custom ModelForm
class AbcAdmin(admin.ModelAdmin):
# some other code
# ...
def get_form(self, request, obj=None, **kwargs):
if not request.user.is_superuser:
kwargs['form'] = AbcStaffForm # ModelForm
return super().get_form(request, obj, **kwargs)
You can also override readonly_fields in changeform_view.
Try this in admin.py
class ABCAdmin(admin.ModelAdmin):
def changeform_view(self, request, *args, **kwargs)
self.readonly_fields = list(self.readonly_fields)
if request.user in group: #or another condition
self.readonly_fields.append('field2')
return super(ABCAdmin, self).changeform_view(request, *args, **kwargs)
Overwrite get_fields() in ABCAdmin (group B cannot view "is_on" field):
class ABCAdmin(admin.ModelAdmin):
fields = ['name', 'title', 'price', 'is_on', 'create_time']
def get_fields(self, request, obj=None):
if request.user in groupB:
if 'is_on' not in self.fields:
self.fields.append('is_on')
else:
if 'is_on' in self.fields:
self.fields.remove('is_on')
return super(ABCAdmin, self).get_fields(request, obj)

Categories

Resources