I have a table called Post. A post can have 2 videos or 2 images, but not both. The table schema for a post looks like this:
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
header = models.CharField()
created_at = models.DateTimeField(auto_now_add=True)
I have two tables that look similar to each other:
class PostImage(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
img = models.ImageField()
class PostVideo(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
video = models.FileField()
How do I create and enforce the relationship where a post can have maximum and minimum of 2 images or 2 videos, but it can't have both videos and images at the same time? Or is there a better way to do this?
Probably you can write a Mixin class like this:
class PostMixin(object):
def save(self, *args, **kwargs):
if hasattr(self, 'img') and self.post.images.exists():
raise ValidationError('Already have an image')
elif hasattr(self, 'video') and self.post.videos.exists():
raise ValidationError('Already have a video')
super(PostMixin, self).save(*args, **kwargs)
class PostImage(PostMixin, models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="images")
img = models.ImageField()
class PostVideo(PostMixin, models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="videos")
video = models.FileField()
But rather than handling them in model, you should handle them in forms or serializers(if you are using DRF).
Another method is to add a pre_save signal for both PostImage and PostVideo and check your conditions there:
#receiver(pre_save, sender=PostVideo)
#receiver(pre_save, sender=PostImage)
def post_validator(sender, instance, *args, **kwargs):
images_count = instance.post.postimage_set.count()
videos_count = instance.post.postvideo_set.count()
if not (<your conditions met>):
raise ValidationError('Conditions not met!')
Related
I have an exam model that whenever an instance is created, instances of the Question model to the number that is specified in the Exam are created(using post_save signal). Also, I have a Go code that whenever a request is sent, fills out 3 fields of the Question model. My problem is how can I send this request in the signal part.
The codes are as followed:
models.py:
class Exam(models.Model):
title = models.CharField(max_length=255)
subject = models.CharField(max_length=255, default='')
organizer = models.CharField(max_length=255, default='...')
description = models.TextField(max_length=1000)
created_at = models.DateTimeField(auto_now_add=True)
duration = models.DurationField()
number_of_questions = models.PositiveSmallIntegerField()
order = models.IntegerField(default=0)
def __str__(self):
return self.title
class ExamQuestion(models.Model):
exam = models.ForeignKey('ExamApply', on_delete=models.CASCADE)
question_template = models.ForeignKey(QuestionTemplate, on_delete=models.CASCADE)
text = models.TextField(max_length=5000, null=True, blank=True)
question_params = models.JSONField(null=True, blank=True)
answer_choices = models.JSONField(null=True, blank=True)
answer_given = models.JSONField(default=dict, null=True, blank=True)
correct_answer = models.JSONField(null=True, blank=True)
data = models.JSONField(null=True, blank=True)
is_correct = models.BooleanField(null=True)
order = models.IntegerField(null=True, blank=True)
def __str__(self):
return str(self.id)
class ExamApply(models.Model):
class Status(models.TextChoices):
CREATED = 'CR', 'Created'
STARTED = 'ST', 'Started'
FINISHED = 'FN', 'Finished'
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
exam = models.ForeignKey(Exam, on_delete=models.CASCADE)
start_date = models.DateTimeField()
end_date = models.DateTimeField()
status = models.CharField(max_length=2, choices=Status.choices, default=Status.CREATED)
def get_score(self):
score = ExamQuestion.objects.filter(exam=self, answer_given=F('correct_answer')).count()
return score
signals.py:
#receiver(post_save, sender=ExamApply)
def create_examapply_examquestion(sender, instance, created, **kwargs):
if created:
for _ in range(instance.exam.number_of_questions):
ExamQuestion.objects.create(exam=instance)
id = ExamQuestion.objects.all().last().id
return redirect('/question/' + str(id) + '/') #doesnt work
#receiver(post_save, sender=ExamApply)
def save_examapply_examquestion(sender, instance, created, **kwargs):
instance.exam.save()
urls.py related to the part I want:
urlpatterns = [
path('questions/<int:pk>/', UpdateQuestionAPI.as_view()),
]
views.py:
class UpdateQuestionAPI(generics.RetrieveUpdateDestroyAPIView):
queryset = ExamQuestion.objects.all()
serializer_class = IntegrateQuestionSerializer
lookup_field = 'pk'
def get(self, request, *args, **kwargs):
question = ExamQuestion.objects.filter(pk=kwargs['pk'])
serializer = ExamQuestionSerializer(question, many=True)
return Response(serializer.data)
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response({"message": "updated successfully"})
else:
return Response({"message": "failed", "details": serializer.errors})
serializers.py:
class IntegrateQuestionSerializer(serializers.ModelSerializer):
class Meta:
model = ExamQuestion
fields = ['question_params', 'answer_choices', 'correct_answer',]
class ExamQuestionSerializer(serializers.ModelSerializer):
title = serializers.SerializerMethodField()
class Meta:
model = ExamQuestion
fields = ['title']
def get_title(self, obj):
return obj.exam.exam.title
I had the idea of using redirect(to the update view), but it doesn't work.
First of all, it makes no sense to use request in the signal part.
This code is related to the model and the orm and is not responsible of the request part. That's the view work.
The signal you are using is related to the creation of the ExamApply model. The view you posted doesn't create a ExamApply so it will not get called at all.
Just so you know, usually signals are avoided because they lead to complicated code. To quote the documentation “Signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug. Where possible you should opt for directly calling the handling code, rather than dispatching via a signal.”
Here you have multiple issues in your signals, you have a return in a loop, that will stop the loop after the first iteration. Then you return an HttpResponse (in the form of redirect) but you are not in a view so it doesn't make sense.
It's not clear what you want to do because the view and the serializer you posted are not related to the creation of ExamApply as I understand it what you could do (one of):
Override the save method of ExamApply to create the questions.
Or handle the creation of questions in the view that create the ExamApply
For example:
def save(self, *args, **kwargs):
if not self.pk:
# not self.pk means a creation.
for _ in range(self.exam.number_of_questions):
question = ExamQuestion.objects.create(exam=self.exam)
# You might want to link question to user / exam apply here?
self.exam.question_set.add(question)
super().save(*args, **kwargs)
In your ExamApply creation view or serializer you might want to return the questions id, if you use DRF it's trivial to do so: https://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
But you might want to link questions to ExamApply and/or user and not just Exam.
On a side note, for the future, if you need the created object id you should use
question = ExamQuestion.objects.create(exam=instance)
question.id
I have the following models:
class PlaceMixin(models.Model):
name = models.CharField(max_length=200, null=True, blank=True)
address = models.CharField(max_length=200, null=True, blank=True)
sublocality = models.CharField(max_length=100, null=True, blank=True)
city = models.CharField(max_length=100, null=True, blank=True)
class Meta:
abstract = True
class Bar(PlaceMixin):
pass
class Restaurant(PlaceMixin):
pass
Bar and Restaurant have almost same save() method:
def save(self, *args, **kwargs):
try:
bar = Bar.objects.get(address=self.address)
except Bar.DoesNotExist:
Do something
super().save(*args, **kwargs)
def save(self, *args, **kwargs):
try:
restaurant = Restaurant.objects.get(address=self.address)
except Restaurant.DoesNotExist:
Do something
super().save(*args, **kwargs)
I was wondering if I can put the method in the Abstract model and pass it to the two inherited model?
def save(self, *args, **kwargs):
try:
temp = self.objects.get(address=self.address)
except self.DoesNotExist:
Do something
super().save(*args, **kwargs)
Something like this? But you can query in an abstract model. I basically need to check if an instance exists for executing an action.
You can make a common save method for both Restaurant and Bar model in a Mixin class like this:
from django.apps import apps
class CommonMixin(object):
def save(self, *args, **kwargs):
if self.__class__.__name__ == "Resturant":
model = apps.get_model('app_name', 'Bar')
if model.objects.filter(address=self.address).exists():
...
else:
model = apps.get_model('app_name', 'Restaurant')
if model.objects.filter(address=self.address).exists():
...
super(CommonMixin, self).save(*args, **kwargs)
And import it in both Restaurant and Bar class:
class Restaurant(CommonMixin, PlaceMixin):
...
class Bar(CommonMixin, PlaceMixin):
...
Probably a better approach is to use a separate model for Address information. Then you won't need a new Mixin to override save(the approach given above feels like over engineering). So lets say you have a different address model, there you can simply put unique=True to restrict duplicate entries:
class Address(models.Model):
address = models.CharField(max_length=255, unique=True)
class PlaceMixin(models.Model):
address = models.ForeignKey(Address)
...
You can use abstract metadata to achieve this. And if you want to use any variable inside class model, you just need to use self.__class__ like so:
class PlaceMixin(models.Model):
name = models.CharField(max_length=200, null=True, blank=True)
address = models.CharField(max_length=200, null=True, blank=True)
sublocality = models.CharField(max_length=100, null=True, blank=True)
city = models.CharField(max_length=100, null=True, blank=True)
class Meta:
abstract = True
def save(self, *args, **kwargs):
try:
self.__class__.objects.get(address=self.address)
except self.__class__.DoesNotExist:
# Do something
else:
super().save(*args, **kwargs)
class Bar(PlaceMixin):
pass
class Restaurant(PlaceMixin):
pass
There are a lot of code design like this in Django source code, a lot of good practices in their project so give it a try. E.g: a line of code on Django repo
I have two models (Post and Display). Both have Datetime-auto fields. My problem is that i want to update all display objects related to a post, once a post is updated.
I have read here that you could override one models save method, but all the examples are About updating the model with the foreign key in it and then call the save method of the other model. In my case it's the other way arround. How can i do this ?
class Post(models.Model):
title = models.CharField(max_length=40)
content = models.TextField(max_length=300)
date_posted = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
rooms = models.ManyToManyField(Room, related_name='roomposts', through='Display')
def __str__(self):
return self.title
def get_absolute_url(self):
return "/post/{}/".format(self.pk)
class Display(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
room = models.ForeignKey(Room, on_delete=models.CASCADE)
isdisplayed = models.BooleanField(default=0)
date_posted = models.DateTimeField(auto_now=True)
def __str__(self):
return str(self.isdisplayed)
i want to update the date_posted of all related Display-objects once their related post is changed. I do not know if overriding the save-method works here.
in this case you should have a look at django's reverse foreign key documentation
https://docs.djangoproject.com/en/2.2/topics/db/queries/#following-relationships-backward
in your case you can override the save method on your Post model
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
#either: this excutes many sql statments
for display in self.display_set.all():
display.save()
#or faster: this excute only one sql statements,
#but note that this does not call Display.save
self.display_set.all().update(date_posted=self.date_posted)
The name display_set can be changed using the related_name option
in Display, you can change it:
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='displays')
Then, instead of using self.display_set in your code, you can use self.displays
Overriding save method works, but that's not were you should go, imo.
What you need is signals:
#receiver(post_save, sender=Post)
def update_displays_on_post_save(sender, instance, **kwargs):
if kwargs.get('created') is False: # This means you have updated the post
# do smth with instance.display_set
Usually it goes into signals.py.
Also you need to include this in you AppConfig
def ready(self):
from . import signals # noqa
I want to create detail view for my Playlist model. I followed steps from docs and i keep getting this error:
AttributeError at /root
Generic detail view PlaylistDetail must be called with either an object pk or a slug in the URLconf.
here is my code:
model:
class Playlist(models.Model):
title = models.CharField(max_length=40, null=True)
description = models.CharField(max_length=500, null=True)
author = models.ForeignKey(User, default=None, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
miniature = models.ImageField(upload_to='images/playlist', default="defaults/default.png", validators=[validate_miniature_file_extension])
tracks = models.ManyToManyField(Track)
def __str__(self):
return self.title
url:
path('<slug:author>', PlaylistDetail.as_view(), name='playlist'),
view:
class PlaylistDetail(DetailView):
model = Playlist
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['now'] = timezone.now()
return context
I suppose its caused because of that there are more than one playlist created by the same user, and it will must get User && title. Any suggestions?
The issue is that in the urls, you use <slug:author>
path('<slug:author>', PlaylistDetail.as_view(), name='playlist'),
And by default it looks for slug.
Try updating the view with this:
class PlaylistDetail(DetailView):
model = Playlist
slug_url_kwarg = 'author' # <----
def get_context_data(self, **kwargs):
# . . .
I have a super class for my models as below:
class BaseModel(models.Model):
""" BaseClass vase aksare model ha """
def __init__(self, *args, **kwargs):
super(BaseModel, self).__init__(args, kwargs)
print('******> base model __init__')
status = models.IntegerField(default=1)
is_deleted = models.BooleanField(default=False)
create_user = models.ForeignKey(User, related_name="%(app_label)s_%(class)s_creator_related")
create_date = models.DateTimeField()
update_date = models.DateTimeField()
update_user = models.ForeignKey(User, related_name="%(app_label)s_%(class)s_updater_related")
class Meta:
abstract = True
def validate(self):
print('^^^^^^^^^^^^^^^^^^^^^^^^^^^^base validation')
and I have a profile model as below:
class Profile(BaseModel):
def __init__(self, *args, **kwargs):
super(Profile, self).__init__(args, kwargs)
""" User profile """
user = models.OneToOneField(User, related_name='profile')
mobile = models.CharField(max_length=25, null=True)
firstname_en = models.CharField(max_length=500, null=True)
lastname_en = models.CharField(max_length=500, null=True)
gender = models.IntegerField(default=0)
birth_date = models.DateTimeField(null=True)
edu_bg = models.ForeignKey('Category', related_name="profile__edu_bg", null=True)
region = models.ForeignKey('Category', related_name="profile__region", null=True)
credit = models.DecimalField(default=0, decimal_places=6, max_digits=15)
key = models.TextField(null=True)
secret = models.TextField(null=True)
I have an error when I want to insert a new userProfile as below:
TypeError: int() argument must be a string, a bytes-like object or a number, not 'tuple'.
then print the vars(userprofileObject) and realized that 'id': ((), {}), however, I have not set it. When I removed the __init__ functions or set id to None in insertion code, problem solved.
Any idea?
I need those __init__ and also don't want to set id=None in my code
This is how django's models work. You shouldn't change their __init__ method.
This is why
You may be tempted to customize the model by overriding the __init__ method. If you do so, however, take care not to change the calling signature as any change may prevent the model instance from being saved. Rather than overriding __init__, try using one of these approaches:
# Add a classmethod on the model class:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
#classmethod
def create(cls, title):
book = cls(title=title)
# do something with the book
return book
book = Book.create("Pride and Prejudice")
Source https://docs.djangoproject.com/en/1.10/ref/models/instances/#django.db.models.Model
Also read this Writing a __init__ function to be used in django model