How to change a model after creating a ModelViewset? - django

I am creating an API with django rest framework.
But I have a problem every time I want to modify a model when I have already created an associated ModelViewset.
For example, I have this model:
class Machine(models.Model):
name = models.CharField(_('Name'), max_length=150, unique=True)
provider = models.CharField(_("provider"),max_length=150)
build_date = models.DateField(_('build date'))
category = models.ForeignKey("machine.CategoryMachine",
related_name="machine_category",
verbose_name=_('category'),
on_delete=models.CASCADE
)
site = models.ForeignKey(
"client.Site",
verbose_name=_("site"),
related_name="machine_site",
on_delete=models.CASCADE
)
class Meta:
verbose_name = _("Machine")
verbose_name_plural = _("Machines")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("Fridge_detail", kwargs={"pk": self.pk})
And this viewset:
class MachineViewSet(viewsets.ModelViewSet):
"""
A simple ViewSet for listing or retrieving machine.
"""
permission_classes = [permissions.IsAuthenticated]
serializer_class = MachineSerializer
queryset = cache.get_or_set(
'machine_machines_list',
Machine.objects.all(),
60*60*24*7
)
#swagger_auto_schema(responses={200: MachineSerializer})
def list(self, request):
serializer_context = {
'request': request,
}
queryset = cache.get_or_set(
'machine_machines_list',
Machine.objects.all(),
60*60*24*7
)
serializer = MachineSerializer(queryset, many=True, context=serializer_context)
return Response(serializer.data)
#swagger_auto_schema(responses={404: 'data not found', 200: MachineSerializer})
def retrieve(self, request, pk=None):
serializer_context = {
'request': request,
}
queryset = Machine.objects.all()
machine = get_object_or_404(queryset, pk=pk)
serializer = MachineSerializer(machine, context=serializer_context)
return Response(serializer.data)
If I want to add a field to my model, like for example a description. When I run the command python manage.py makemigrations
I get the error :
django.db.utils.ProgrammingError: column machine_machine.description does not exist
I have to comment my viewset to be able to run the makemigrations and migrate.
How can I avoid having to comment each time the viewsets when I want to modify a model?

In your view you have the following line in your class:
queryset = cache.get_or_set(
'machine_machines_list',
Machine.objects.all(),
60*60*24*7
)
Since this line is present in the class declaration and not in a method of the class it is executed when the class is created / interpreted. This causes it to fire a query, since a queryset will need to be evaluated (pickling Querysets will force evaluation) to cache it. The problem here is that your models haven't been migrated yet and your queries fail. You can write that code in the get_queryset method by overriding it if you want:
class MachineViewSet(viewsets.ModelViewSet):
"""
A simple ViewSet for listing or retrieving machine.
"""
permission_classes = [permissions.IsAuthenticated]
serializer_class = MachineSerializer
# Remove below lines
# queryset = cache.get_or_set(
# 'machine_machines_list',
# Machine.objects.all(),
# 60*60*24*7
# )
def get_queryset(self):
return cache.get_or_set(
'machine_machines_list',
Machine.objects.all(),
60*60*24*7
)
...
But this implementation is critically flawed! You are caching the queryset for a week. Lot's of new data can come in within that time period and you would be essentially serving stale data to your users. I would advise you to forego this sort of caching. Also you seem to be returning all the data from your view, consider some sort of filtering on this so that less memory is needed for such stuff.

Related

Add a field to DRF generic list view

I need to create a DRF list view that shows each course along with a boolean field signifying whether the user requesting the view is subscribed to the course.
Course subscriptions are stored in the following model:
class Subscription(models.Model):
user = models.ForeignKey(
CustomUser, related_name='subscriptions', null=False,
on_delete=CASCADE)
course = models.ForeignKey(
Course, related_name='subscriptions', null=False,
on_delete=CASCADE)
class Meta:
ordering = ['course', 'user']
unique_together = [['user', 'course']]
This is the view I am writing:
class CourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated, ]
queryset = Course.objects.all()
serializer_class = CourseSerializer
def isSubscribed(self, request, course):
sub = Subscription.objects.filter(
user=request.user, course=course).first()
return True if sub else False
def list(self, request, format=None):
queryset = Course.objects.all()
serializer = CourseSerializer(queryset, many=True)
return Response(serializer.data)
I am looking for a way to modify the list method, so as to add to the response the information about whether request.user is subscribed to each of the courses.
The best solution I have for now is to construct serializer manually, which would look (at the level of pseudo-code) something like this:
serializer = []
for course in querySet:
course['sub'] = self.isSubscribed(request, course)
serializer.append(CourseSerializer(course))
I suspect there should be a better (standard, idiomatic, less convoluted) way for adding a custom field in a list view, but could not find it. In addition, I am wondering whether it is possible to avoid a database hit for every course.
You can do that easily with Exists:
just change your queryset in your view:
from django.db.models import Exists, OuterRef
class CourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated, ]
serializer_class = CourseSerializer
def get_queryset(self):
subquery = Subscription.objects.filter(user=request.user, course=OuterRef('id'))
return Course.objects.annotate(sub=Exists(subquery))
and add a field for it in your serializer:
class CourseSerializer(serializers.ModelSerializer):
sub = serializers.BooleanField(read_only=True)
class Meta:
model = Course
fields = '__all__'

Django Admin list_filter and ordering on annotated fields

I'm trying to filter in Django Admin on an annotated field, but getting a FieldDoesNotExist error.
class Event(models.Model):
name = models.CharField(max_length=50, blank=True)
class EventSession(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
start_date = models.DateTimeField()
end_date = models.DateTimeField()
#admin.register(Event)
class EventAdmin(admin.ModelAdmin):
ordering = ["event_start_date"]
list_filter = ["event_start_date", "event_end_date"]
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.annotate(
event_start_date=Min("eventsession_set__start_date"), # start of first day
event_end_date=Max("eventsession_set__start_date"), # start of last day
)
return qs
The resulting error in Django Admin is:
FieldDoesNotExist at /admin/events/event/
Event has no field named 'event_start_date'
I need to filter on event_start_date rather than eventsession_set__start_date because filtering ordering (edit) on the latter causes multiples rows per event (one for each session) to show up in the list view.
The error comes from the get_field method of django/db/models/options.py:
try:
# Retrieve field instance by name from cached or just-computed
# field map.
return self.fields_map[field_name]
except KeyError:
raise FieldDoesNotExist("%s has no field named '%s'" % (self.object_name, field_name))
I'm on Django 3.2. Any ideas?
This is an example how to add ordering and filter by annotated fields at django admin list page
class EventStartDateListFilter(admin.SimpleListFilter):
title = "start_date"
parameter_name = "start_date"
def lookups(self, request, model_admin):
return (
("week", "week"),
# add other filters
)
def queryset(self, request, queryset):
value = self.value()
if value == "week":
return queryset.filter(_event_start_date__gt=now() - timedelta(weeks=1))
return queryset
#admin.register(Event)
class EventAdmin(admin.ModelAdmin):
list_display = ["__str__", "event_start_date", "event_end_date"]
ordering = ["event_start_date", "event_end_date"]
list_filter = [EventStartDateListFilter]
def event_start_date(self, obj):
return obj._event_start_date
event_start_date.admin_order_field = '_event_start_date'
def event_end_date(self, obj):
return obj._event_end_date
event_end_date.admin_order_field = '_event_end_date'
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.annotate(
_event_start_date=Min("eventsession_set__start_date"), # start of first day
_event_end_date=Max("eventsession_set__start_date"), # start of last day
)
return qs
I believe I've found a solution to the duplicate records - overriding get_changelist as described here.
def get_changelist(self, request, **kwargs):
from django.contrib.admin.views.main import ChangeList
class SortedChangeList(ChangeList):
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.order_by("event_start_date")
if request.GET.get("o"):
return ChangeList
return SortedChangeList

KeyError 'request' in DRF

I am checking in serializer if product exists in cart or not and I am using this
class ProductSerializer(serializers.ModelSerializer):
in_cart = serializers.SerializerMethodField()
class Meta:
model = Product
fields = ['id', 'in_cart']
def get_in_cart(self, obj):
user = self.context['request'].user
if user.is_authenticated:
added_to_cart = Cart.objects.filter(user=user, product_id=obj.id).exists()
return added_to_cart
else:
return False
It works fine but I cannot add product to the cart because of that request
my cart model like this
class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True)
quantity = models.PositiveIntegerField()
def __str__(self):
return f'{self.user} cart item'
class ItemsListView(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
When I post product id to add cart it throws this error
user = self.context['request'].user KeyError: 'request'
I need to make both work but adding item to cart is being problem.
How can I solve this? Thank you beforehand!
You need to pass the request to the context before usage. So the calling of serializer should look like this:
ProductSerializer(product, context={'request': request})
With ListAPIView class you don't even need this, because by default it would be available in the serializer due to the default implementation of get_serializer_context method:
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
Although you could override it if needed. Also, take a note that serializer_class accepts a callable object, it should be serializer_class = ProductSerializer.
I have had the same problem while using nested serializers. As mentioned above, you just can pass self.context['request'] to the context of nested serializer:
'author': GETUserSerializer(
recipe.author,
context={'request': self.context['request']}
).data,

List of records not fetching updated records in Django REST framework..?

In Django REST Framework API, list of database table records are not getting updated until the API restart or any code change in python files like model, serializer or view. I've tried the transaction commit but it didn't worked. Below is my view :
class ServiceViewSet(viewsets.ModelViewSet):
#authentication_classes = APIAuthentication,
queryset = Service.objects.all()
serializer_class = ServiceSerializer
def get_queryset(self):
queryset = self.queryset
parent_id = self.request.QUERY_PARAMS.get('parent_id', None)
if parent_id is not None:
queryset = queryset.filter(parent_id=parent_id)
return queryset
# Make Service readable only
def update(self, request, *args, **kwargs):
return Response(status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, *args, **kwargs):
return Response(status=status.HTTP_400_BAD_REQUEST)
Serializer looks like this :
class ServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Service
fields = ('id', 'category_name', 'parent_id')
read_only_fields = ('category_name', 'parent_id')
and model looks like this :
class Service(models.Model):
class Meta:
db_table = 'service_category'
app_label = 'api'
category_name = models.CharField(max_length=100)
parent_id = models.IntegerField(default=0)
def __unicode__(self):
return '{"id":%d,"category_name":"%s"}' %(self.id,self.category_name)
This problem is occuring only with this service, rest of the APIs working perfectly fine. Any help will be appreciated.
Because you are setting up the queryset on self.queryset, which is a class attribute, it is being cached. This is why you are not getting an updated queryset for each request, and it's also why Django REST Framework calls .all() on querysets in the default get_queryset. By calling .all() on the queryset, it will no longer use the cached results and will force a new evaluation, which is what you are looking for.
class ServiceViewSet(viewsets.ModelViewSet):
queryset = Service.objects.all()
def get_queryset(self):
queryset = self.queryset.all()
parent_id = self.request.QUERY_PARAMS.get('parent_id', None)
if parent_id is not None:
queryset = queryset.filter(parent_id=parent_id)
return queryset

Django Rest Framework return nested object using PrimaryKeyRelatedField

I am using DRF to expose some API endpoints.
# models.py
class Project(models.Model):
...
assigned_to = models.ManyToManyField(
User, default=None, blank=True, null=True
)
# serializers.py
class ProjectSerializer(serializers.ModelSerializer):
assigned_to = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), required=False, many=True)
class Meta:
model = Project
fields = ('id', 'title', 'created_by', 'assigned_to')
# view.py
class ProjectList(generics.ListCreateAPIView):
mode = Project
serializer_class = ProjectSerializer
filter_fields = ('title',)
def post(self, request, format=None):
# get a list of user.id of assigned_to users
assigned_to = [x.get('id') for x in request.DATA.get('assigned_to')]
# create a new project serilaizer
serializer = ProjectSerializer(data={
"title": request.DATA.get('title'),
"created_by": request.user.pk,
"assigned_to": assigned_to,
})
if serializer.is_valid():
serializer.save()
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.data, status=status.HTTP_201_CREATED)
This all works fine, and I can POST a list of ids for the assigned to field. However, to make this function I had to use PrimaryKeyRelatedField instead of RelatedField. This means that when I do a GET then I only receive the primary keys of the user in the assigned_to field. Is there some way to maintain the current behavior for POST but return the serialized User details for the assigned_to field?
I recently solved this with a subclassed PrimaryKeyRelatedField() which uses the id for input to set the value, but returns a nested value using serializers. Now this may not be 100% what was requested here. The POST, PUT, and PATCH responses will also include the nested representation whereas the question does specify that POST behave exactly as it does with a PrimaryKeyRelatedField.
https://gist.github.com/jmichalicek/f841110a9aa6dbb6f781
class PrimaryKeyInObjectOutRelatedField(PrimaryKeyRelatedField):
"""
Django Rest Framework RelatedField which takes the primary key as input to allow setting relations,
but takes an optional `output_serializer_class` parameter, which if specified, will be used to
serialize the data in responses.
Usage:
class MyModelSerializer(serializers.ModelSerializer):
related_model = PrimaryKeyInObjectOutRelatedField(
queryset=MyOtherModel.objects.all(), output_serializer_class=MyOtherModelSerializer)
class Meta:
model = MyModel
fields = ('related_model', 'id', 'foo', 'bar')
"""
def __init__(self, **kwargs):
self._output_serializer_class = kwargs.pop('output_serializer_class', None)
super(PrimaryKeyInObjectOutRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return not bool(self._output_serializer_class)
def to_representation(self, obj):
if self._output_serializer_class:
data = self._output_serializer_class(obj).data
else:
data = super(PrimaryKeyInObjectOutRelatedField, self).to_representation(obj)
return data
You'll need to use a different serializer for POST and GET in that case.
Take a look into overriding the get_serializer_class() method on the view, and switching the serializer that's returned depending on self.request.method.