I have a models like these
class Campaign(models.Model):
campaign_name= models.CharField(max_length=30)
user=models.ForeignKey(User)
rows=models.IntegerField(default=3)
columns=models.IntegerField(default=1)
def __str__(self): # __unicode__ on Python 2
return self.campaign_name+": "+self.campaign_desc
def save(self, *args, **kwargs):
print("Doing Custom Task ")
super(Campaign, self).save(*args, **kwargs) # Call the "real" save() method.
class Item(models.Model):
campaign=models.ForeignKey(Campaign)
item_name=models.CharField(max_length=70)
item_ID=models.CharField(max_length=400)
def __str__(self):
return self.item_name
I have registered Campaign in admin using admin.site.register(Campaign,CampaignAdmin) and want the number of items in each campaign to be rows X columns of campaign (Foreign key).
Q1) If I validate it using the save override method in the place of print("Doing Custom Task ") , I couldn't save the instance when it's created.
Q2) It would be also nice to have the number of items to be filled to show up appropriately. Now I have
class ItemInline(admin.TabularInline):
model = Item
#extra = 4
in admin.py Basically I want the extra parameter to be rows X columns
If you want to do some custom validation, you'd better do it via forms, or (as a last resort), using the Model.clean* methods family.
class Campaign(models.Model):
def clean(self):
if self.pk:
if self.item_set.count() > 5 # Or whatever number you need
raise ValidationError(_('Too much items for me.'))
Override the get_extra() method for your inline:
class ItemInline(admin.TabularInline):
extra = 4
def get_extra(self, request, obj=None, **kwargs):
if obj:
return obj.rows * obj.columns
return self.extra
Related
I have a model of the category with title and count and I also have another model called Products which has category as one of its foreign-key. How can I auto-update the category count field by the number of products that comes under the same category?
class Category(models.Model):
title = models.CharField(max_length = 20)
count = models.IntegerFIeld()
class Product(models.Model):
title = models.CharField(max_length = 20)
category = models.ForeignKey(Category, on_delete = models.CASCADE)
You can either override save method or write a signal to auto-update the field, i.e.:
class Category(models.Model):
def save(self, *args, **kwargs):
self.count = self.product_set.count()
super().save(*args, **kwargs)
or with signals:
from django.dispatch import receiver
#receiver(post_save, sender=Category)
def category_post_save(sender, instance, *args, **kwargs):
instance.count = instance.product_set.count()
instance.save()
But I would suggest you using a calculated property:
class Category(models.Model):
...
#property
def count(self):
return self.product_set.count()
If you want to keep the field count I recommend you use signals. You should use post_signal and pre_signal (to get the previous category from a query) on the model Product.
Here are the signals:
#receiver(signals.pre_save, sender=Product)
def product_pre_save_signal(sender, instance, *args, **kwargs):
if instance.id:
previous_instance = Product.objects.get(id=instance.id)
if previous_instance.category != instance.category:
instance.previous_category = previous_instance.category
#receiver(signals.post_save, sender=Product)
def product_post_save_signal(sender, instance, created, *args, **kwargs):
if not created:
if hasattr(instance, "previous_category"):
if instance.previous_category:
instance.previous_category.count -= 1
instance.previous_category.save()
if instance.category:
instance.category.count += 1
instance.category.save()
else:
if instance.category:
instance.category.count += 1
instance.category.save()
Another recommendation for this situation is, make it a read-only field(add editable=False to the field declaration), so no one can edit it from Django admin or serializer.
But I highly recommend you remove the field because it's hard to keep this field sync with the table Product, so calculate the value each time.
You can have almost any query that is related field count over Category or Product, so don't worry:)
I have library project. i have book and order model. the user can order many books at once, bu i have error.
this is my code ->
book model ->
class Book(models.Model):
name=models.CharField(max_length=100, verbose_name=_('name'))
Condition = models.IntegerField(default=10,verbose_name=_('condition'))
author=models.CharField(max_length=100, verbose_name=_('author'))
quantity=models.IntegerField(default=100, verbose_name=_('quantity'))
branch = models.ManyToManyField(Branch, verbose_name=_('branch'))
def __str__(self):
return self.name
class Order(models.Model):
book=models.ManyToManyField(Book, verbose_name=_('book'))
user=models.ForeignKey(User,on_delete=models.CASCADE, verbose_name=_('user'))
branch=models.ForeignKey(Branch,on_delete=models.CASCADE, verbose_name=_('branch'))
start_date=models.DateField(verbose_name=_('start'))
end_date=models.DateField(verbose_name=_('finish'))
def __str__(self):
return str(self.user)
def save(self, *args, **kwargs):
today = datetime.datetime.now()
if not self.pk:
# self.book.quantity -= 1
for i in self.book:
i.quantity -= 1
i.save()
self.end_date = today + datetime.timedelta(days=14)
super(Order, self).save(*args, **kwargs)
when i save i have this error ->"<Order: d#gmail.com>" needs to have a value for field "id" before this many-to-many relationship can be used.
Are you using .add() method before you save the Order model?
If so then try saving the model object first before adding m2m objects.
For eg: order = Order(user=<user>, branch=<branch>, start_date=<start_date>, end_date=<end_date>) then order.save() and finally order.book.add(<book1>, <book2>) and so on.
Also i see in save() method you have used for i in self.book but you need to use self.book.all()
I have two models named 'School' and 'Student'. I've created each's serializers and the nested serializer for School having a student serializer as a nested field.
Here I want to apply filters on the fields of the serializers using 'django-filters' and it is almost working, BUT ...the problem is that when I filter the nested field, i-e 'students's field' , It doesn't show me the required result.
My models are :
class School(models.Model):
name = models.CharField(max_length=256)
principal = models.CharField(max_length=256)
location = models.CharField(max_length=256)
is_government = models.BooleanField(default=True)
def __str__(self):
return self.name
class Student(models.Model):
name = models.CharField(max_length=256)
age = models.PositiveIntegerField()
school = models.ForeignKey(School,related_name='students',on_delete = models.CASCADE)
is_adult = models.BooleanField(default=True)
def __str__(self):
return self.name
and my serializers are:
class SchoolSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
# Instantiate the superclass normally
super(SchoolSerializer, self).__init__(*args, **kwargs)
allow_students = self.context.get("allow_students",None)
if allow_students:
self.fields['students'] = StudentSerializer(many=True, context=kwargs['context'], fields=['name','age','is_adult'])
class Meta():
model = School
fields = '__all__'
class StudentSerializer(DynamicFieldsModelSerializer):
class Meta():
model = Student
fields = '__all__'
and these are the filters that i am using in my views:
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import FilterSet
from django_filters import rest_framework as filters
class SchoolStudentAPIView(generics.ListAPIView, mixins.CreateModelMixin):
queryset = School.objects.all()
serializer_class = SchoolSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields = ('is_government','students__is_adult')
Here, the issue is that when i search for "students__is_adult", which is a nested field, It filters out the list of students that are adult ALONG WITH THE students that are not.
Can someone add something extra or give another solutuion? thank you
The problem
First of all, Django Rest Framework is not doing the query you'd expect. Let's see how to check.
One way to debug the actual query is adding a custom list() method to the SchoolStudentAPIView class, as follows:
def list(self, request, *args, **kwargs):
resp = super().list(request, *args, **kwargs)
from django.db import connection
print(connection.queries) # or set a breakpoint here
return resp
This method does nothing more than dumping all the executed queries to the console.
The last element of connection.queries is what we should focus on. It'll be a dict() with its "sql" key looking something like:
SELECT `school`.`id`, `school`.`name`, `school`.`location`, `school`.`is_government`
FROM `school` INNER JOIN `student` ON (`school`.`id` = `student`.`school_id`)
WHERE `student`.`is_adult` = 1
This query means that the SchoolSerializer will be passed all the Schools that have at least one adult Student.
By the way, the same school can appear multiple times, since the above query produces one row per adult student.
The SchoolSerializer, in the end, shows all the Students in the School regardless of any filtering option: this is what this line achieves.
if allow_students:
self.fields['students'] = StudentSerializer(many=True, ...)
Suggested solution
No simple solution is to be found with serializers. Maybe the more straightforward way is to write a custom list() method in the SchoolStudentAPIView class.
The method will:
look for the query string argument student__is_adult: if it's there, the method will create a custom field on each School in the queryset (I named it filtered_students), and make that field point to the correct Student queryset.
pass a context argument to the SchoolSerializer, to tell it that students are filtered
The SchoolSerializer class, in turn, will populate its students field in two different ways, depending on the presence or absence of the context argument. Specifically, the StudentSerializer field will have the source kwarg if the students__is_adult key is present in the passed context.
In code:
class SchoolStudentAPIView(generics.ListAPIView, mixins.CreateModelMixin):
# ...
def list(self, request, *args, **kwargs):
schools = self.get_queryset()
ctx = {}
if 'students__is_adult' in request.query_params:
filter_by_adult = bool(request.query_params['students__is_adult'])
ctx = {
'students__is_adult': filter_by_adult,
'allow_students': True,
}
for s in schools:
s.filtered_students = s.students.filter(is_adult=filter_by_adult)
ser = SchoolSerializer(data=schools, many=True, context=ctx)
ser.is_valid()
return Response(ser.data)
class SchoolSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(SchoolSerializer, self).__init__(*args, **kwargs)
allow_students = self.context.get("allow_students", None)
if allow_students:
# Change 'source' to custom field if students are filtered
filter_active = self.context.get("posts__is_active", None)
if filter_active is not None:
stud = StudentSerializer(
source='filtered_students', many=True,
context=kwargs['context'],
fields=['name', 'age', 'is_adult'])
else:
stud = StudentSerializer(
many=True, context=kwargs['context'],
fields=['name', 'age', 'is_adult'])
self.fields['students'] = stud
Is there some elegant solution to using Django's DeleteView but instead actually deleting the objects, marking them inactive? I like the simplicity of the DeleteView but would want to keep the data at the backend, instead of removing it.
Elegant solution would be overriding Model & Manager to update a field on delete. This is an implementation as Abstract Model, so that it can be extended by any other Model. You can modify it as per your need, if you already have delete fields in your model.
Soft Deletion Abstract Model
class SoftDeletionModel(models.Model):
deleted_at = models.DateTimeField(blank=True, null=True)
objects = SoftDeletionManager()
all_objects = SoftDeletionManager(alive_only=False)
class Meta:
abstract = True
def delete(self):
self.deleted_at = timezone.now()
self.save()
def hard_delete(self):
super(SoftDeletionModel, self).delete()
Object Manager
class SoftDeletionManager(models.Manager):
def __init__(self, *args, **kwargs):
self.alive_only = kwargs.pop('alive_only', True)
super(SoftDeletionManager, self).__init__(*args, **kwargs)
def get_queryset(self):
if self.alive_only:
return SoftDeletionQuerySet(self.model).filter(deleted_at=None)
return SoftDeletionQuerySet(self.model)
def hard_delete(self):
return self.get_queryset().hard_delete()
QuerySet
class SoftDeletionQuerySet(QuerySet):
def delete(self):
return super(SoftDeletionQuerySet, self).update(deleted_at=timezone.now())
def hard_delete(self):
return super(SoftDeletionQuerySet, self).delete()
def alive(self):
return self.filter(deleted_at=None)
def dead(self):
return self.exclude(deleted_at=None)
For explanation, see Soft Deletion in Django
The DeleteView inherits DeletionMixin so you can just predefine the delete method.
DeletionMixin
Override delete method in DeleteView as follows
class Example(DeleteView):
def delete(self, request, *args, **kwargs):
"""
Calls the delete() method on the fetched object and then
redirects to the success URL.
"""
self.object = self.get_object()
self.object.is_deleted = True # Declare a boolean field is_deleted in your model. Default value is Flase.
return HttpResponseRedirect(self.get_success_url())
Many toys have same information(for example description and price) how can i customise my admin.py in order to get last saved object values, everytime when i press add new toy in admin panel and display them in my input fields.
models.py
class Toy(models.Model):
name = models.CharField(max_length=255)
description = TextField()
quantity = models.FloatField(default=0.0)
price = models.FloatField()
def __unicode__(self):
return self.name
admin.py
admin.site.register(Toy)
Try to do it like that:
First create a form in your admin.py:
class ToyForm(forms.ModelForm):
class Meta:
model = Toy
fields = ('__all__')
def __init__(self, *args, **kwargs):
#if not an edit
if 'instance' not in kwargs:
#we get the last object
last_object = Toy.objects.all().order_by("id")[0]
#we add the last object informations to the initial data
initial = {'description': last_object.description, 'price': last_object.price}
kwargs['initial'] = initial
super(ToyForm, self).__init__(*args, **kwargs)
Then add this form to the Admin model:
class ToyAdmin(admin.ModelAdmin):
form = ToyForm
Fianally:
admin.site.register(ToyAdmin, Toy)
In modeladmin class there is a method get_form, you can override the method to return, the values returned from you conditioned queryset results ( Mymodel.objects.last() )
from django.contrib import admin
#admin.register(Toy)
class ToyAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(ToyAdmin, self).get_form(request, obj, **kwargs)
# code logic to check condition & queryset like Toy.objects.last()
form.base_fields['name'].initial = 'value'
return form