Use generic UpdateView to always edit a single configuration object - django

What I Need
I want to have a global configuration for my app and I want to reuse a generic UpdateView.
What I Tried
For this purpose I created a model (example fields):
class Configuration(models.Model):
admin = models.ForeignKey('User', on_delete=models.CASCADE)
hostname = models.CharField(max_length=23)
A generic Updateview:
class ConfigurationView(UpdateView):
model = Configuration
fields = ['admin','hostname']
And urls.py entry
path(
'configuration/',
views.ConfigurationView.as_view(
queryset=Configuration.objects.all().first()
),
name='configuration'
),
As you can see I want the configuration/ path to link to this configuration and always only edit this one object.
Problem
I get the error
AttributeError: 'Configuration' object has no attribute 'all'
Questions
How can I hardcode the object into the path in urls.py so that always the first Configuration object is used for the UpdateView?
Is there a better way to do this? I simply want to have a global configuration object and want it to be editable and displayable with a template of my choice.

You're trying to provide a single object to a class expecting a queryset. The view calls get_queryset which does this;
def get_queryset(self):
"""
Return the `QuerySet` that will be used to look up the object.
This method is called by the default implementation of get_object() and
may not be called if get_object() is overridden.
"""
if self.queryset is None:
if self.model:
return self.model._default_manager.all()
else:
raise ImproperlyConfigured(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {
'cls': self.__class__.__name__
}
)
return self.queryset.all()
You've provided a queryset so that lands on self.queryset.all() which for your example is calling all() on an instance of your class.
To use the queryset kwarg of as_view() you'd do something like MyView.as_view(queryset=MyModel.objects.filter(enabled=True))
So you need to change the way the view looks for the object;
class ConfigurationView(UpdateView):
def get_object(self):
return Configuration.objects.first()
By default UpdateView does this to get an object; https://ccbv.co.uk/projects/Django/2.0/django.views.generic.edit/UpdateView/
If you're limiting the config to 1 object you will also want to implement a Singleton design. Essentially this is a way to ensure only 1 object can exist. Read more here; https://steelkiwi.com/blog/practical-application-singleton-design-pattern/
There is a really helpful package for singletons called django-solo

Related

How to read query string values in ModelViewSet logic?

I need to get at query string values in some viewset logic (in this case, derived from ModelViewSet). Everything I've read, including the Django REST Framework doc, says that request is an attribute of the viewset. But when I actually try to refer to it in code, no matter how I do so, I am shown the runtime error 'AddressViewSet' object has no attribute 'request' . Here's one simplified version of the class definition that triggers the error:
class AddressViewSet(viewsets.ModelViewSet):
def __init__(self, suffix, basename, detail):
attendee = ""
if self.request.query_params.get('attendee'):
attendee = self.request.query_params.get('attendee')
self.serializer_class = AddressSerializer
self.queryset = Address.objects.all()
How does one read request properties in viewset logic in DRF?
You are overriding the incorrect method. The ViewSet like all class based views in Django (All DRF views inherit from django.views.generic.View down the line) is instantiated (The pattern usually seen as View.as_view() internally creates an instance of the class) before the request is even received. This instance is used in the url patterns and when a request matching the url pattern is found a dynamically created function is called for the view which then calls dispatch.
Coming back to the point __init__ is not the correct method to override, if you want to filter the queryset you should instead be overriding get_queryset:
class AddressViewSet(viewsets.ModelViewSet):
queryset = Address.objects.all()
serializer_class = AddressSerializer
def get_queryset(self):
queryset = super().get_queryset()
attendee = ""
if self.request.query_params.get('attendee'):
attendee = self.request.query_params.get('attendee')
# Filter queryset here
return queryset
What I did not understand is that you can override a function get_queryset() but if you do that you do not assign queryset directly - that happens automatically. Once that overridden function is called the request is available as a property of the viewset, so it can be referred to there.

Add dynamic attribute to serializer

I'd like to add a dynamic attribute to my serializers whenever they are called. Note that I'm talking about an attribute, not a field. Here's the idea:
User call the API
Viewset calls the serializer with the data
I manage to set self.my_key = my_dynamic_value
Checks and returns the serializer data (normal process)
To set my dynamic value, I need to use self.context meaning I can't override the __init__ method of the serializers (they are called on server start and context is empty)
Any idea on how I could do it?
Using another topic (Pass extra arguments to Serializer Class in Django Rest Framework), I managed to find a suitable solution.
I simply pass my dynamic value within the context by overriding the get_serializer_context method in the view, like that:
class MyViewSet(viewsets.GenericViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
context["my_key"] = my_value
return context
Then in my serializer, I can get it using self.context["my_key"]

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)

Django detailview get_queryset and get_object

I am using Django detailview. initially, I used the URL pattern
url(r'^todo/details/(?P<pk>[\d]+)', views.todoDetailView.as_view(), name='detail_todo'),
my view is
class todoDetailView(DetailView):
model = models.todo
It worked fine.
In the second case, my URL is
url(r'^todo/details/(?P<id>[\d]+)', views.todoDetailView.as_view(), name='detail_todo'),
this time, I modified my view to
class todoDetailView(DetailView):
model = models.todo
# context_object_name = 'todo_detail'
def get_object(self, **kwargs):
print(kwargs)
return models.todo.objects.get(id=self.kwargs['id'])
It worked fine, I modified the second case to
class todoDetailView(DetailView):
model = models.todo
# context_object_name = 'todo_detail'
def get_queryset(self):
return models.todo.objects.get(id=self.kwargs['id'])
then I get an error,
Generic detail view todoDetailView must be called with either an object pk or a slug.
I know that there is no proper slug or pk provided. So, initially I added get_object() (it worked) but get_queryset() is not working. What is the difference in their working ??
And also if a user is getting details only based on the slug, I read on StackOverflow that
this can be used
slug_field = 'param_name'
slug_url_kwarg = 'param_name'
link - Generic detail view ProfileView must be called with either an object pk or a slug
Can anyone explain me actual working of get_object() and get_queryset() (also get_slug_field() if possible)
Along with the terms slug_field and slug_url_kwarg
Thanks in advance
get_object returns an object (an instance of your model), while get_queryset returns a QuerySet object mapping to a set of potentially multiple instances of your model. In the case of the DetailView (or in fact any class that inherits from the SingleObjectMixin, the purpose of the get_queryset is to restrict the set of objects from which you'll try to fetch your instance.
If you want to show details of an instance, you have to somehow tell Django how to fetch that instance. By default, as the error message indicates, Django calls the get_object method which looks for a pk or slug parameter in the URL. In your first example, where you had pk in the URL, Django managed to fetch your instance automatically, so everything worked fine. In your second example, you overrode the get_object method and manually used the id passed as parameter to fetch the object, which also worked. In the third example, however, you didn't provide a get_object method, so Django executed the default one. SingleObjectMixin's default get_object method didn't find either a pk or a slug, so it failed.
There are multiple ways to fix it:
1. Use pk in the URL
The simplest one is to simply use the code you provided in your first example. I don't know why you were unsatisfied with that, it is perfectly fine. If you're not happy, please explain why in more detail.
2. Override get_object
This is the second solution you provided. It is overkill because if you properly configured your view with the correct options (as you will see in the following alternatives), Django would take care of fetching the object for you.
3. Provide the pk_url_kwarg option
If you really want to use id in the URL for some reason, you can indicate that in your view by specifying the pk_url_kwarg option:
class todoDetailView(DetailView):
model = models.todo
pk_url_kwarg = 'id'
4. Provide the slug_field and slug_url_kwarg options [DON'T DO THIS]
This is a terrible solution because you are not really using a slug, but an id, but it should in theory work. You will basically "fool" Django into using the id field as if it was a slug. I am only mentioning it because you explicitly asked about these options in your question.
class todoDetailView(DetailView):
model = models.todo
slug_field = 'id'
slug_url_kwarg = 'id'
Regarding your get_queryset method: in your example, it doesn't even get to be executed, but in any case it is broken because it returns an individual object instead of a queryset (that's what objects.get does). My guess is you probably don't need a custom get_queryset method at all. This would be useful for example if you had a complex permission system in which different users can only access a different subset of todo objects, which I assume is not your case. Currently, if you provide this get_queryset method, even if everything else is configured properly, you will get an error. Probably an AttributeError saying that the queryset object has no attribute filter (because it will actually be a todo object and not a QuerySet object as Django expects).
The default get_object for DetailView tries to fetch the object using pk or slug from the URL. The simplest thing for you to do is to use (?P<pk>[\d]+) in the URL pattern.
When you override get_object, you are replacing this default behaviour, so you don't get any errors.
When you override get_queryset, Django first runs your get_queryset method an fetches the queryset. It then tries to fetch the object from that queryset using pk or slug, and you get an error because you are not using either of them.
The slug_field and slug_url_kwarg parameters are both defined in the docs. The slug_fields is the name of the field in the model used to fetch the item, and slug_url_kwarg is the name of the parameter in the URL pattern. In your case, you are fetching the object using the primary key (pk/id), so you shouldn't use either of these options.
For your URL pattern with (?P<id>[\d]+), you could use pk_url_kwarg = 'id'. That would tell Django to fetch the object using id from the URL. However, it's much simpler to use your first URL pattern with (?P<pk>[\d]+), then you don't have to override any of the methods/attributes above.
get_object() majorly uses with the generic views which takes pk or id
like: DetailView, UpdateView, DeleteView
where get_queryset() uses with the ListView where we are expecting more objects
Also, get_object() or self.get_object() uses pk as default lookup field or can use slug Field
a little peek at get_object()
if queryset is None:
queryset = self.get_queryset()
# Next, try looking up by primary key.
pk = self.kwargs.get(self.pk_url_kwarg)
slug = self.kwargs.get(self.slug_url_kwarg)
if pk is not None:
queryset = queryset.filter(pk=pk)
# Next, try looking up by slug.
if slug is not None and (pk is None or self.query_pk_and_slug):
slug_field = self.get_slug_field()
queryset = queryset.filter(**{slug_field: slug})
I can't help you with what the error message explicitly means, but get_queryset is used in listviews by get multiple objects, while get_object is used to get a single object (ie DetailView).
If you have a pk you can use to get an object, you don't need to specify a slug field. Slug field are used to filter out objects when you don't have or can't show the primary key publicly. This gives a better explanation of a slug field.

Override default queryset with using as_manager

Django documents state we can override default queryset using manager's get_queryset method.
What if I user models.Queryset and as.manager method to avoid duplicate methods. How can I override default query?
It seems get_queryset method of models.Queryset doesnt seems to work in this case.
Thanks.
You can use a combination of a Manager and a QuerySet to achieve this, like so:
# model_managers.py
class CustomManager(models.Manager):
""" Enables changing the default queryset function. """
def get_queryset(self):
# Here you can change the default queryset function
return super(CustomManager, self).get_queryset(example_field=True)
class CustomQuerySet(models.QuerySet):
""" Include additional queryset functions. """
def example_filter(self):
return self.filter(other_field=False)
# models.py
class YourModel(models.Model):
# Override the default manager
objects = CustomManager.from_queryset(CustomQuerySet)() # The pair of empty parenthesis is required!
And then you can use it anywhere like:
# Will filter `example_field`
YourModel.objects.all()
# Will filter `example_field` and `other_field`
YourModel.objects.example_filter()
You should show what you tried that didn't work.
But nevertheless, you just need to use that call to define your own manager in the model:
class MyModel(models.Model):
...
objects = MyQuerySet.as_manager()
You can create manage class by inheriting models.Manager and then you can define your own query set method.
in model.py, we should assign class as property as given below:
example = ExampleManager()
Now we can use example instead of objects and can call respective ExampleManager method.