Django update number of items on each save or delete - django

I am new to Django and still learning. I am looking to keep track of how many events I have under a test. My current model looks like
class Test(models.Model):
name = models.CharField(max_length=255)
description = models.CharField(max_length=255, blank=True)
num_of_events = models.IntegerField(default=0)
class Meta:
verbose_name = 'Test'
verbose_name_plural = 'Tests'
def __str__(self):
return self.name
class Event(models.Model):
name = models.CharField(max_length=255)
test = models.ForeignKey(Test,on_delete=models.CASCADE)
class Meta:
verbose_name = 'Event'
verbose_name_plural = 'Events'
def __str__(self):
return self.name
def save(self):
obj, created = Test.objects.update_or_create(name=self.test)
obj.num_of_events += 1
super().save()
def delete(self):
self.test.num_of_events -= 1
super().delete()
I thought I could just override the save() function but it does not update on the admin panel and still shows 0.
I am trying to figure out what I am doing wrong.
EDIT: admin.py
class TestAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'num_of_events')
fieldsets = [
(None, {'fields': ('name', 'description')})
]
class EventsAdmin(admin.ModelAdmin):
pass
class PropertyAdmin(admin.ModelAdmin):
list_display = ('name', 'property_type', 'expected_value')
admin.site.register(Test, TestAdmin)
admin.site.register(Event, EventsAdmin)
admin.site.register(Property, PropertyAdmin)

You forget to save the Test object. For example with:
class Event(models.Model):
# …
def save(self):
if self.test_id is not None:
obj = self.test
obj.num_of_events += 1
obj.save()
super().save()
def delete(self):
if self.test_id is not None:
self.test.num_of_events -= 1
self.test.save()
super().delete()
But regardless, storing the number of items is usually not a good idea. Say that you change the .test of a given Event, then you need to subtract from the old Test and add to the new Test. Furthermore ORM operations in bulk, like .update(..) circumvent .save() and signals, so it will be hard or impossible to keep this correct.
The point is that you do not need to store the number of Events. Indeed, you can simply obtain these with:
from django.db.models import Count
Test.objects.annotate(number_of_events=Count('event'))
The Test objects that arise from this queryset will have an extra attribute .number_of_events that contains the number of related Event ojbects.

Related

How can I add my custom model function's value in my API

I'm trying to build my own poll app and the models have a lot of relation with each other and I need to count how many objects are in relation with some other objects so I need these custom function for it
Here's the model with the custom functions:
class Poll(models.Model):
title = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_available = models.BooleanField(default=True)
date_created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
#property
def total_votes(self):
votes = 0
for o in self.option_set.all():
votes = votes + o.how_much_vote
return votes
#property
def users_involved(self):
list = []
for o in self.option_set.all():
for v in o.vote:
list.append(v.user)
return list
#property
def users_involved(self):
users = []
for o in self.option_set.all():
for v in o.who_voted:
users.append(v)
return users
#property
def total_votes_each_option(self):
dct = {}
for o in self.option_set.all():
dct[o.title]= o.how_much_vote
return dct
My question is how do you include all of those custom functions total_votes, users_involved, etc to my api? because right now my api looks something like this:
{
"id": 1,
"title": "Which is the best frontend framework?",
"is_active": true,
"is_available": true,
"date_created": "2021-07-13T14:08:17.709054Z"
}
which is expected but I want to know how do I add those extra value to make it look like this
{
"id": 1,
"title": "Which is the best frontend framework?",
"is_active": true,
"is_available": true,
"date_created": "2021-07-13T14:08:17.709054Z",
"total_votes": "some number",
"users_involved": [...users]
}
Edit: here's my serializer class
class PollSerializer(serializers.ModelSerializer):
class Meta:
model = Poll
fields = '__all__'
Thank you very much
Simple way we can use SerializerMethodField
from rest_framework import serializers
class PollSerializer(serializers.ModelSerializer):
total_votes = serializers.SerializerMethodField()
users_involved = serializers.SerializerMethodField()
class Meta:
model = Poll
fields = (
'id',
'title',
'is_active',
'is_available',
'date_created',
'total_votes',
'users_involved',
)
def get_total_votes(self, poll):
return poll.total_votes
def get_users_involved(self, poll)
return poll.users_involved
Please note that SerializerMethodField is read_only, cannot for write.
Also make sure your custom model functions well for loop processing, using select_related or prefetch_related.
This one should be easy, you can add addition fields in Serializers by mentioning the field_name and its type before class Meta: something like this.
Model Upload
class Upload(models.Model):
.
.
total_files = models.IntegerField(default=0,null=True,blank=True)
.
.
def add(self):
return (self.total_files + 2)
Serializer FileSerializer
class FileSerializer(serializers.ModelSerializer):
#This is the line you need to add
extra_add = serializers.IntegerField(source = 'add')
class Meta:
model = Upload
fields= ['id','name','uploaded','total_files','created_on','updated_on','extra_add']
Then mention your extra_add field name in fields[..,..,'extra_add'].
You can change the type of field while mentioning the field thereserializer.<field_type>(source='<method_name>').
Result of the Above field i added:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username')
class PollSerializer(serializers.ModelSerializer):
total_votes = serializers.IntegerField(read_only=True)
users_involved = UserSerializer(read_only=True, many=True)
class Meta:
model = Poll
fields = '__all__'
#staticmethod
def setup_eager_loading(queryset):
return queryset.prefetch_related('option_set')
Also note that you defined users_involved twice in your Pool class.
You can use PoolSerializer.setup_eager_loading function in your view/viewset for better db query performance but it's not necessary (I do it in viewset.get_queryset method like self.queryset = self.serializer_class.setup_eager_loading(self.queryset).

How to skip an existing object instance when creating resources in bulk python

I am trying to create a resources in bulk. While the resources are created I have the matric_no has to be unique. If the value of an existing matric_no is uploaded together with the some new entries, I get an integrity error 500 because the value already exists and it stops the rest of the values from being created. How can I loop through these values and then check if the value exists, and then skip so that the others can be populated? Here is my code:
**models.py**
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
#python_2_unicode_compatible
class Undergraduate(models.Model):
id = models.AutoField(primary_key=True)
surname = models.CharField(max_length=100)
firstname = models.CharField(max_length=100)
other_names = models.CharField(max_length=100, null=True, blank=True)
card = models.CharField(max_length=100, null=True, blank=True)
matric_no = models.CharField(max_length=20, unique=True)
faculty = models.CharField(max_length=250)
department_name = models.CharField(max_length=250)
sex = models.CharField(max_length=8)
graduation_year = models.CharField(max_length=100)
mobile_no = models.CharField(max_length=150, null=True, blank=True)
email_address = models.CharField(max_length=100)
residential_address = models.TextField(null=True, blank=True)
image = models.CharField(max_length=250,
default='media/undergraduate/default.png', null=True, blank=True)
def __str__(self):
return "Request: {}".format(self.matric_no)
***serializers.py***
from .models import Undergraduate
from .models import Undergraduate
class UndergraduateSerializer(serializers.ModelSerializer):
class Meta:
model = Undergraduate
fields ='__all__'
class CreateListMixin:
"""Allows bulk creation of a resource."""
def get_serializer(self, *args, **kwargs):
if isinstance(kwargs.get('data', {}), list):
print(list)
kwargs['many'] = True
return super().get_serializer(*args, **kwargs)
**api.py**
from .models import Undergraduate
from rest_framework.viewsets import ModelViewSet
from .serializers import CreateListMixin,UndergraduateSerializer
class UndergraduateViewSet(CreateListMixin, ModelViewSet):
queryset = Undergraduate.objects.all()
serializer_class = UndergraduateSerializer
permission_classes = (permissions.IsAuthenticated,)
**urls.py**
from rest_framework.routers import DefaultRouter
from .api import UndergradMassViewSet
router=DefaultRouter()
router.register(r'ug', UndergradMassViewSet)
This is the updated serializer.py
class UndergraduateSerializer(serializers.ModelSerializer):
class Meta:
model = Undergraduate
fields = ('id', 'surname', 'firstname', 'other_names', 'card','matric_no', 'faculty', 'department_name', 'sex', 'graduation_year', 'mobile_no', 'email_address', 'residential_address')
def create(self, validated_data):
created_ids = []
for row in validated_data:
try:
created = super().create(row)
created_ids.append(created.pk)
except IntegrityError:
pass
return Undergraduate.objects.filter(pk__in=[created_ids])
This is how i moved it now
Seriliazers.py
class UndergraduateSerializer(serializers.ModelSerializer):
def create(self, validated_data):
created_ids = []
for row in validated_data:
try:
created = super().create(row)
created_ids.append(created.pk)
except IntegrityError:
pass
return Undergraduate.objects.filter(pk__in=[created_ids])
class Meta:
model = Undergraduate
fields = ('id', 'surname', 'firstname', 'other_names', 'card','matric_no', 'faculty', 'department_name', 'sex', 'graduation_year', 'mobile_no', 'email_address', 'residential_address')
read_only_fields = ('id',)
When the list sent has an existing matric_no , it returns 500 ListSeriaizer is not iterable
You definitely have to implement a custom create() method in your serializer to handle such a case as the serializer's default create method expects one object.
Nonetheless, there is a couple of design decisions to consider here:
You can use bulk_create but it has a lot of caveats which are listed in the docs and it would still raise an integrity error since the inserts are done in one single commit. The only advantage here is speed
You can loop over each object and create them singly. This will solve the integrity issue as you can catch the integrity exception and move on. Here you'll lose the speed in 1
You can also check for integrity issues in the validate method before even moving on to create. In this way, you can immediately return an error response to the client, with information about the offending ro. If everything is OK, then you can use 1 or 2 to create the objects.
Personally, I would choose 2(and optionally 3). Assuming you also decide to chose that, this is how your serializer's create method should look like:
def create(self, validated_data):
created_ids = []
for row in validated_data:
try:
created = super().create(row)
created_ids.append(created.pk)
except IntegrityError:
pass
return Undergraduate.objects.filter(pk__in=[created_ids])
So in this case, only the created objects will be returned as response

Nested objects save in DRF

Models:
class Owner(models.Model):
name = models.CharField(max_length=255)
def __unicode__(self):
return self.name
class SomeThing(models.Model):
own_id = models.IntegerField(unique=True)
description = models.CharField(max_length=255, blank=True)
owner = models.ForeignKey(Owner, blank=True, null=True)
def __unicode__(self):
return self.description
Serializers:
class OwnerNameField(serializers.RelatedField):
def to_internal_value(self, data):
pass
def to_representation(self, value):
return value.name
def get_queryset(self):
queryset = self.queryset
if isinstance(queryset, (QuerySet, Manager)):
queryset = queryset.all()
lista = [Owner(name="------------")]
lista.extend(queryset)
return lista
class OwnerSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Owner
fields = ('name', 'id')
class ThingSerializer(serializers.ModelSerializer):
owner = OwnerNameField(queryset=Owner.objects.all())
class Meta:
model = SomeThing
fields = ('own_id', 'description', 'owner')
Basically it works as intended. But when i add some fields to Owner class i would like to see all these fields in output of ThingSerializer (and be able to parse them - string doesn't suit here). I could change field owner to owner = OwnerSerializer() which gives me what i need. But when i want to add SomeThing object (tested in API browser) i also need add new Owner object - and i don't want it, i want use existing Owner object. How can i achieve it?
Finally i got it. This question describes exactly my problem and provided answers work as a charm!

How to limit choices to Foreign keys in Django admin

I run into a problem when using the Django admin. I'm building a small ScrumBoard. It has projects, with statuses, stories and tasks.
Consider the following model:
#python_2_unicode_compatible
class Project(models.Model):
name = models.CharField(max_length=100)
class Meta:
verbose_name = _('Project')
verbose_name_plural = _('Projects')
def __str__(self):
return self.name
#python_2_unicode_compatible
class Status(models.Model):
name = models.CharField(max_length=64) # e.g. Todo, In progress, Testing Done
project = models.ForeignKey(Project)
class Meta:
verbose_name = _('Status')
verbose_name_plural = _('Statuses')
def __str__(self):
return self.name
#python_2_unicode_compatible
class Story(models.Model):
"""Unit of work to be done for the sprint. Can consist out of smaller tasks"""
project = models.ForeignKey(Project)
name=models.CharField(max_length=200)
description=models.TextField()
status = models.ForeignKey(Status)
class Meta:
verbose_name = _('Story')
verbose_name_plural = _('Stories')
# represent a story with it's title
def __str__(self):
return self.name
The problem: when an admin user creates a story he will see statuses from all the projects instead of the status from one project.
To filter statuses by project, you need your story to already exist so django know which project we are talking about. If you set status nullalble, you can do like this (implying, you do save and continue on first save to set status)
class StatusAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(StatusAdmin, self).get_form(request, obj, **kwargs)
if obj and obj.project:
form.base_fields['status'].queryset = \
form.base_fields['status'].queryset.filter(project=obj.project)
elif obj is None and 'status' in form.base_fields: # on creation
del form.base_fields['status']
return form
You will need something like django-smart-selects

Show information of subclass in list_display django

I have two classes in models.py:
class ModelOne(models.Model):
field_one = models.CharField(max_length=100)
field_two = models.CharField(max_length=200)
field_three = models.CharField(max_length=300)
[...] #other fields
def __unicode__(self):
return self.field_one
class ModelTwo(models.Model):
relation_model_one = models.ForeignKey(ModelOne)
other_field = models.CharField(max_length=50)
[...]
def __unicode__(self):
return self.relation_model_one.field_one
And your administration in admin.py is this:
class ModelTwoInline(admin.StackedInline):
model = ModelTwo
extra = 0
class ModelOneAdmin(admin.ModelAdmin):
list_display = ('field_one', 'field_two', 'field_three',)
inlines = [ModelTwoInline]
My question is:
Can I display the fields of the ModelTwo in list_display of the ModelOne? (The same for list_filter and search_fields)
I need this because I have many subclasses related to the main class!
You can display anything you want in list_display by writing an instance method with the #property decorator, which then returns whatever you need, and including it in list_display. I don't think that works for list_filter though.
So, let's return to your ModelOne class:
class ModelOne(models.Model):
[...]
def __unicode__(self):
return self.field_one
#property
def model_two_other_field(self):
return ', '.join([m2.other_field for m2 in self.modeltwo_set.all()])
Then, in your ModelOneAdmin:
class ModelOneAdmin(admin.ModelAdmin):
list_display = ('field_one', 'field_two', 'field_three', 'model_two_other_field')
[...]
I would note, for the record, that when you do this, you'll be requiring a database hit for every ModelOne instance being displayed. So, if you're listing 50 instances, you're incurring the overhead of 50 separate queries (one for each call of self.modeltwo_set.all()). That doesn't necessarily mean you shouldn't do it -- it might be the right answer, depending on your situation. Just be aware that it could be quite an expensive operation.
You should use the related name of the fields or RelatedModel._meta.get_fields
class ModelOneAdmin(admin.ModelAdmin):
list_display = ('field_one', 'field_two', 'field_three')
inlines = [ModelTwoInline]
def get_list_display(self, request):
extra_fields = [
f.name for model in self.inlines
for f in ModelTwo._meta.get_fields(include_hidden=False)
]
return self.list_display + extra_fields