Passing URL variables to a class based view - django

I have just started messing with class based views and I would like to be able to access variables from the URL inside my class. But I am having difficulties getting this to work. I saw some answers but they were all so short I found them to be of no help.
Basically I have a url
url(r'^(?P<journal_id>[0-9]+)/$',
views.Journal_Article_List.as_view(),
name='Journal_Page'),
Then I would like to use ListView to display all articles in the particular journal. My article table however is linked to the journal table via a journal_id. So I end up doing the following
class Journal_Article_List(ListView):
template_name = "journal_article_list.html"
model = Articles
queryset = Articles.objects.filter(JOURNAL_ID = journal_id)
paginate_by = 12
def get_context_data(self, **kwargs):
context = super(Journal_Article_List, self).get_context_data(**kwargs)
context['range'] = range(context["paginator"].num_pages)
return context
The journal_id however is not passed on like it is in functional views. From what I could find on the topic I read I can access the variable using
self.kwargs['journal_id']
But I’m kind of lost on how I am supposed to do that. I have tried it directly within the class which lets me know that self does not exist or by overwriting get_queryset, in which case it tells me as_view() only accepts arguments that are already attributes of the class.

If you override get_queryset, you can access journal_id from the URL in self.kwargs:
def get_queryset(self):
return Articles.objects.filter(JOURNAL_ID=self.kwargs['journal_id'])
You can read more about django’s dynamic filtering in the docs.

Related

Getting fields from extra manager methods using django-rest-framework

I have the following custom model manager in Django that is meant to count the number of related comments and add them to the objects query set:
class PublicationManager(models.Manager):
def with_counts(self):
return self.annotate(
count_comments=Coalesce(models.Count('comment'), 0)
)
Adding this manager to the model does not automatically add the extra field in DRF. In my API view, I found a way to retrieve the count_comments field by overriding the get function such as:
class PublicationDetails(generics.RetrieveUpdateAPIView):
queryset = Publication.objects.with_counts()
...
def get(self, request, pk):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset.get(id=pk))
data = {**serializer.data}
data['count_comments'] = queryset.get(id=pk).count_comments
return Response(data)
This works for a single instance, but when I try to apply this to a paginated list view using pagination_class, overriding the get method seems to remove pagination functionality (i.e. I get a list of results instead of the usual page object with previous, next, etc.). This leads me to believe I'm doing something wrong: should I be adding the custom manager's extra field to the serializer instead? I'm not sure how to proceed given that I'm using a model serializer. Should I be using a basic serializer?
Update
As it turns out, I was using the model manager all wrong. I didn't understand the idea of table-level functionality when what I really wanted was row-level functionality to count the number of comments related to a single instance. I am now using a custom get_paginated_response method with Comment.objects.filter(publication=publication).count().
Original answer
I ended up solving this problem by creating a custom pagination class and overriding the get_paginated_response method.
class PaginationPublication(pagination.PageNumberPagination):
def get_paginated_response(self, data):
for item in data:
publication = Publication.objects.with_counts().get(id=item['id'])
item['count_comments'] = publication.count_comments
return super().get_paginated_response(data)
Not sure it's the most efficient solution, but it works!

Django restrict access to user objects

I have Node and User models which both belong to an Organisation. I want to ensure that a User will only ever see Node instances belonging to their Organisation.
For this I want to override the Node objects Manager with one that returns a query_set of User owned filtered results.
Based on https://docs.djangoproject.com/en/2.1/topics/db/managers/#modifying-a-manager-s-initial-queryset
the relevant models.py code I have is below:
class Organisation(models.Model):
users = models.ManyToManyField(User, related_name='organisation')
...
class UserNodeManager(models.Manager):
def get_queryset(self, request):
return super().get_queryset().filter(organisation=self.request.user.organisation.first())
class Node(models.Model):
organisation = models.ForeignKey(
Organisation, related_name='nodes', on_delete=models.CASCADE)
uuid = models.UUIDField(primary_key=True, verbose_name="UUID")
...
objects = UserNodeManager
views.py
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
EDIT
I can add custom query_set to individual views and this does work as below:
views.py
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
def get_queryset(self):
return Node.objects.filter(organisation__users__id=self.request.user.pk)
However, my intention is to be DRY and override a 'master' query_set method at a single point so that any view (e.g. form dropdown list, API endpoint) will perform the user restricted query without additional code.
For example, I am using django's generic list views have a form for adding a Scan object which requires a user to select a Node the Scan belongs to. The form currently shows Nodes from other Organisations, which is against the permissions logic I need.
Unfortunately, the overridden Node.objects property does not seem to have any effect and any User can see all Nodes. Am I taking the right approach?
I think the problem is here:
objects = UserNodeManager
You need to initiate UserNodeManager instance like this:
objects = UserNodeManager()
Also, it should throw error when you calling YourModel.objects.all() method(which is called from get_queryset method in view), because when it calls get_queryset() method, it does not pass request. So I think it would be a better approach:
class UserNodeManager(models.Manager):
def all(self, request=None):
qs = super(UserNodeManager, self).all()
if request:
return qs.filter(...)
return qs
Or you can create a new manager method like this(optional):
class UserNodeManager(models.Manager):
def user_specific_nodes(self, request):
return self.get_queryset().filter(...)
Also update in the view:
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
def get_queryset(self):
return Node.objects.all(self.request) # where you can obviously use filter(...) or Model.objects.user_specific_nodes(self.request)
Update
from comments
Thing is that, you need to pass request with filter() or all(). In Generic views, the get_queryset method does not pass that information to all(). So you need to pass that either way. There is another way, to use a middleware like this django-crequest. You can use it like this:
from crequest.middleware import CrequestMiddleware
class UserNodeManager(models.Manager):
def all(self):
qs = super(UserNodeManager, self).all()
request = CrequestMiddleware.get_request()
return qs.filter(...)
The best way of achieving this is by using groups and custom permissions. You might add a group for every organization and set the correct permissions for those groups over your Nodes.
Take a look to this article, it might help: User Groups with Custom Permissions in Django
#ruddra thanks again for your guidance.
While your middleware example did not have effect for me (as user could still see others' objects), I was able to use that with the django documentation to finally implement the Manager similar to:
class UserDeviceManager(models.Manager):
def get_queryset(self):
request = CrequestMiddleware.get_request()
return super().get_queryset().filter(organisation=request.user.organisation)

How to get related objects from one to many relationship for display in ListView and be able to filter?

I'm looking at this tutorial from the Mozilla library. I want to create a list view in admin based on a database relationship. For example I have a Vehicle model and a statusUpdate model. Vehicle is a single instance with many statusUpdates. What I want to do is select the most recent statusUpdate (based on the dateTime field I have created) and have that data available to me in the list view.
The tutorial mentions:
class Vehicle(models.Model):
class statusUpdate(models.Model):
vehicle = models.ForeignKey(Vehicle, on_delete=models.CASCADE)
Question: How could I do a list view with model relationships and be able to filter by fields on the child relationship and pass to the view?
Here's what I wanted in a Class Based View (CBV), my explanation of my issue was not very clear.
def get_context_data(self, **kwargs):
get_context_data is a way to get data that is not normally apart of a generic view. Vehicle is already provided to the View because its the model defined for it, if you wanted to pass objects from a different model you would need to provide a new context, get_context_data is the way to do this. statusUpdate is a model with a foreign key to Vehicle. Full example below.
class VehicleDetail(generic.DetailView):
model = Vehicle
template_name = 'fleetdb/detail.html'
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super(VehicleDetail, self).get_context_data(**kwargs)
context['updates'] = statusUpdate.objects.filter(vehicle_id=1).order_by('-dateTime')[:5]
return context
I don't think that solves your problem entirely. You used this:
context['updates'] = statusUpdate.objects.filter(vehicle_id=1).order_by('-dateTime')[:5]
This will only result in a list of statusUpdates where vehicle_id is set to 1. The part I was struggling with is how to get the primary key (in your case the actual vehicle_id). I found this solution:
vehicle_id = context['vehicle'].pk # <- this is the important part
context['updates'] = statusUpdate.objects.filter(vehicle_id=vehicle_id).order_by('-dateTime')[:5]
I discovered the context object and it contains the data which has already been added (thus you need to call super before using it). Now that I write it down it seems so obvious, but it took me hours to realize.
Btw. I am pretty new to Django and Python, so this might be obvious to others but it wasn't to me.

Django Generic Createview : Add another object to template

I'm trying to add additional data to my template named "equipment_form" linked with my view CreateEquipment (CreateView generic django)
So, my model Equipment possess a subcategory. And my model subcategory possess a category.
For UX reasons, I want my user to chose the category of the equipment first, then the subcategory. In order to do this, I need to get in the view the whole content of the
category table and give it to the template. And I have some trouble to figure out how I can do it.
I will really appreciate the help of the community! Thank you.
So atm my view look like this :
class EquipmentCreate(CreateView):
#category list not passed to the template
category_list = Category.objects.all()
model = Equipment
success_url = reverse_lazy('stock:equipment-list')
EDIT : I found the answer here :
Django - CreateView - How to declare variable and use it in templates
Thank you anyway :)
Found* the answer here:
Override get_context_data and set context_data['place_slug'] = your_slug
Something like this:
def get_context_data(self, **kwargs):
context = super(PictureCreateView, self).get_context_data(**kwargs)
context['place_slug'] = self.place.slug
return context
Some more info on this in the Django docs.
*Posting OP's comment and edit as answer

Query multiple models with class-based views

I would like to solve the following situation.
I have a side panel containing information of the active user. For this an instance of UserInfo model needs to be passed to the views.
Additionally, I would like to pass a number of other model instances to the pages (eg. Purchases, Favourites, etc.).
I know this is pretty easy to do by overriding the get_context_data.
def get_context_data(self, **kwargs):
kwargs['purchases'] = Purchases.objects.get(id=1)
kwargs['favourites'] = Favourites.objects.get(id=1)
.... etc
return super(UploadFileView, self).get_context_data(**kwargs)
So my question is - what would be the best/most appropriate CBV to use for this?
This isn't quite a DetailView as you have multiple objects, but it isn't a ListView either, nor does it look like a FormView or its children.
Since you gain nothing from those, a simple TemplateView is probably the way to go.
If you are querying the same UserInfo, Purchases, Favorites, etc in multiple views, create a Mixin that you can re-use.
class CommonUserInfoMixin (object):
def get_context_data(self, **kwargs):
context = super(OrgContextMixin, self).get_context_data(**kwargs)
... # Add more to context object
Then you can use this in your normal List, Detail, Update, etc CBV's
class ItemList(CommonUserInfoMixin, ListView):
....