Django: How to update an Integerfield and decrement its value - django

This seemingly innocuous problem has turned out to be quite difficult to find any information on. I just want to decrement the value of an Integerfield column by 1 in my database, by calling a function.
views.py function call
StudentProfile.objects.add_lesson(student_id)
managers.py
class StudentQuerySet(models.QuerySet):
def add_lesson(self, sid):
self.filter(student_id=sid).update(remaining_lessons=remaining - 1)
class StudentProfileManager(models.Manager):
def add_lesson(self, sid):
self.get_queryset().add_lesson(sid)
Full StudentProfile model
class StudentProfile(models.Model):
student = models.OneToOneField(
User, related_name='student', primary_key=True, parent_link=True, on_delete=models.CASCADE)
portrait = models.ImageField(
upload_to='studentphotos', verbose_name=_('Student Photo'))
about_me = models.TextField(verbose_name=_("About Me"))
spoken_languages = models.CharField(max_length=255)
teacher_default = models.OneToOneField(
'teachers.TeacherProfile', related_name='teacher_default', parent_link=True,
on_delete=models.CASCADE, default=None, blank=True, null=True)
membership_start = models.DateTimeField(
verbose_name="Membership Start Date", default=now, editable=False)
membership_end = models.DateTimeField(
verbose_name="Membership End Date", default=now, editable=False)
remaining_lessons = models.IntegerField(
verbose_name="Membership remaining lessons", default=0)
objects = StudentProfileManager()
def __str__(self):
return User.objects.get_student_name(self.student_id)
I know this is totally wrong, any help is appreciated.

If you want to keep your current setup and be able to add_lesson() to decrement "remaining_lessons", the smallest change you can do to achieve it is by using F() expression:
from django.db.models import F
class StudentQuerySet(models.QuerySet):
def add_lesson(self, sid):
self.filter(student_id=sid).update(remaining_lessons=F('remaining_lessons') - 1)
Ref: https://docs.djangoproject.com/en/3.0/ref/models/expressions/
Although I personally think that if your goal is only to have a method that decrement "remaining_lessons" by 1, you should probably just make it a model method. Like this:
class StudentProfile(models.Model):
# ... your model field ...
def add_lesson(self):
self.remaining_lesson -= 1
self.save()
# and in your Views.py
StudentProfile.objects.get(student_id=sid).add_lesson()
Hope this helps.

Django provides F expressions for exactly the kind of task you have. It makes the update relative to the original field value in the database.
You would need to change your managers.py as follows (plus the return statements :) )
from django.db.models import F
class StudentQuerySet(models.QuerySet):
def add_lesson(self, sid):
return self.filter(student_id=sid).update(remaining_lessons=F('remaining_lessons')-1)
class StudentProfileManager(models.Manager):
def add_lesson(self, sid):
return self.get_queryset().add_lesson(sid)
You could go even further, and for the sake of DRY approach, use QuerySet.as_manager() to create an instance of Manager with a copy of a custom QuerySet’s methods instead of repeating the method twice in your custom Manager and QuerySet. E.g.:
class StudentProfile(models.Model):
...
objects = StudentQuerySet().as_manager()
Hope it helps!

I tried using the F expression, and I have no clue why, but it was decrementing by 3 instead of by 1. Maybe Django runs that code 3 times when it is called in the view.
I found a solution that accomplishes this without a function, in the view, it does exactly what I expect, a decrement of 1:
student_id = request.user.id
student_queryset = StudentProfile.objects.get(student_id=student_id)
student_queryset.remaining_lessons = student_queryset.remaining_lessons - 1
student_queryset.save()

Related

Properly update Django model's foreign key property

I have a Django Model named EmailSendingTask. This is the whole model-
class EmailSendingTask(models.Model):
order = models.OneToOneField(Order, on_delete=models.CASCADE, related_name='order')
status = EnumChoiceField(SetupStatus, default=SetupStatus.active)
time_interval = EnumChoiceField(TimeInterval, default=TimeInterval.five_mins)
immediate_email = models.OneToOneField(PeriodicTask, on_delete=models.CASCADE, null=True, blank=True, related_name='immediate_email')
scheduled_email = models.OneToOneField(PeriodicTask, on_delete=models.CASCADE, null=True, blank=True, related_name='scheduled_email')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = 'EmailSendingTask'
verbose_name_plural = 'EmailSendingTasks'
def __str__(self) -> str:
return f'EmailSendingTask: Order = {self.order}'
The immediate_email and scheduled_email fields are responsible for holding two PeriodicTask objects.
I have created a function called disable_scheduled_email, which is responsible for disabling the scheduled_email's periodic task. The detail of the function is here-
def disable_scheduled_email(self):
print(f'Disabling scheduled email...')
self.scheduled_email.enabled = False
self.save(update_fields=['scheduled_email'])
Now, whenever I call this function and print the value of the self.scheduled_email.enabled, I find it False. But, when I try to look at the Django Admin site, the periodic task's enabled value remains as True. Why is it happening?
After some experiments into the Django Shell I have found out that, I was not specifically calling save() to the foreign key (scheduled_email). I have just added self.scheduled_email.save() into the disable_scheduled_email function. So, the whole function became like:
def disable_scheduled_email(self):
print(f'Disabling scheduled email...')
self.scheduled_email.enabled = False
# self.save(update_fields=['scheduled_email'])
self.scheduled_email.save() #instead of self.save(...), wrote this

how to build query with several manyTomany relationships - Django

I really don't understand all the ways to build the right query.
I have the following models in the code i'm working on. I can't change models.
models/FollowUp:
class FollowUp(BaseModel):
name = models.CharField(max_length=256)
questions = models.ManyToManyField(Question, blank=True, )
models/Survey:
class Survey(BaseModel):
name = models.CharField(max_length=256)
followup = models.ManyToManyField(
FollowUp, blank=True, help_text='questionnaires')
user = models.ManyToManyField(User, blank=True, through='SurveyStatus')
models/SurveyStatus:
class SurveyStatus(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
survey = models.ForeignKey(Survey, on_delete=models.CASCADE)
survey_status = models.CharField(max_length=10,
blank=True,
null=True,
choices=STATUS_SURVEY_CHOICES,
)
models/UserSurvey:
class UserSurvey(BaseModel):
user = models.ForeignKey(User, null=True, blank=True,
on_delete=models.DO_NOTHING)
followups = models.ManyToManyField(FollowUp, blank=True)
surveys = models.ManyToManyField(Survey, blank=True)
questions = models.ManyToManyField(Question, blank=True)
#classmethod
def create(cls, user_id):
user = User.objects.filter(pk=user_id).first()
cu_quest = cls(user=user)
cu_quest.save()
cu_quest._get_all_active_surveys
cu_quest._get_all_followups()
cu_quest._get_all_questions()
return cu_quest
def _get_all_questions(self):
[[self.questions.add(ques) for ques in qstnr.questions.all()]
for qstnr in self.followups.all()]
return
def _get_all_followups(self):
queryset = FollowUp.objects.filter(survey__user=self.user).filter(survey__user__surveystatus_survey_status='active')
# queryset = self._get_all_active_surveys()
[self.followups.add(quest) for quest in queryset]
return
#property
def _get_all_active_surveys(self):
queryset = Survey.objects.filter(user=self.user,
surveystatus__survey_status='active')
[self.surveys.add(quest) for quest in queryset]
return
Now my questions:
my view sends to the create of the UserSurvey model in order to create a questionary.
I need to get all the questions of the followup of the surveys with a survey_status = 'active' for the user (the one who clicks on a button)...
I tried several things:
I wrote the _get_all_active_surveys() function and there I get all the surveys that are with a survey_status = 'active' and then the _get_all_followups() function needs to call it to use the result to build its own one. I have an issue telling me that
a list is not a callable object.
I tried to write directly the right query in _get_all_followups() with
queryset = FollowUp.objects.filter(survey__user=self.user).filter(survey__user__surveystatus_survey_status='active')
but I don't succeed to manage all the M2M relationships. I wrote the query above but issue also
Related Field got invalid lookup: surveystatus_survey_status
i read that a related_name can help to build reverse query but i don't understand why?
it's the first time i see return empty and what it needs to return above. Why this notation?
If you have clear explanations (more than the doc) I will very appreciate.
thanks
Quite a few things to answer here, I've put them into a list:
Your _get_all_active_surveys has the #property decorator but neither of the other two methods do? It isn't actually a property so I would remove it.
You are using a list comprehension to add your queryset objects to the m2m field, this is unnecessary as you don't actually want a list object and can be rewritten as e.g. self.surveys.add(*queryset)
You can comma-separate filter expressions as .filter(expression1, expression2) rather than .filter(expression1).filter(expression2).
You are missing an underscore in surveystatus_survey_status it should be surveystatus__survey_status.
Related name is just another way of reverse-accessing relationships, it doesn't actually change how the relationship exists - by default Django will do something like ModelA.modelb_set.all() - you can do reverse_name="my_model_bs" and then ModelA.my_model_bs.all()

Django change BooleanField value after DateField expires

I would like to be able to automatically set suspended to False (if is True, of course) when end_suspension_date passes by (and therefore if it exists).
models.py
class Profile(models.Model):
suspended = models.BooleanField(default=False)
start_suspension_date = models.DateField(null=True, blank=True)
end_suspension_date = models.DateField(null=True, blank=True)
# ... other fields
Is there any way to do this without third-party apps? I thought of defining a function inside the model (but I don't see much sense in doing so):
def end_suspension(self):
if date.today() >= self.end_suspension_date:
self.suspended = False
start_suspension_date = None
end_suspension_date = None
else:
# do nothing...
No, you will need something like celery to define a task that filters for end of suspension.
An alternative method I prefer is to replace the suspended field with a property, because having a field that stores "is the user suspended" and a field that stores "when is the user no longer suspended" are redundant because we know the current date.
A more idiomatic would be calling it is_suspended, so:
class Profile(models.Model):
...
#property
def is_suspended(self):
return date.today() < self.end_suspension_date
Then on login views permission checks etc just access profile.is_suspended.
Simple is better then complex :)
Aldi, beware of timezone. Rule of thumb: store UTC date instead of local date.
You can try it, like:
class Profile(models.Model):
start_suspension_date = models.DateField(null=True, blank=True)
end_suspension_date = models.DateField(null=True, blank=True)
# ... other fields
#property
def suspended(self):
return date.today() < self.end_suspension_date

Django substract two fields from related model

With this models:
class Vine(models.Model):
autor = models.ForeignKey(Viner,related_name='autor')
titulo = models.CharField(max_length=450)
estado = models.CharField(choices=ESTADOS_VINE, max_length=30)
objects = models.Manager()
custom_object = managers.VineManager()
and the model for the votes
class Voto(models.Model):
user = models.ForeignKey(MyUser)
submit_date = models.DateTimeField(auto_now_add=True)
vine = models.ForeignKey(Vine)
valoracion = models.BooleanField(default=False)
and the class for the Favorites (This is working fine yet)
class Favorito(models.Model):
date = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='favoritos')
I have this 'query' in Django.
vines = Vine.custom_object.filter(estado=2).\
annotate(favoritosCount=Count('favoritos', distinct=True)).\
filter(voto__valoracion=False).annotate(disLikesCount=Count('voto', distinct=True))\
.annotate(likesCount=Count('voto', distinct=True)).filter(voto__valoracion=True)
But the second filter is not working because of the first.
Basically what I want is to get the sum of 'positive votes' - 'negative votes' as a field and order by it.
Could anyone please help me?
Thank you in advance
AFAIK you can't do that query with the ORM. You might be able to do it with a raw query.
I think It's easier if you add a count field to your Vine model and order by it. Then update that count field every time there's a new Voto.
Something like this:
from django.db.models import F
class Vine(models.Model):
...
votos = models.IntegerField()
class Meta:
ordering = ('votos',)
class Voto(models.Model):
...
def save(self):
"""When saving new Voto instance, update related Vine."""
if not self.pk:
new_vote = 1 if self.valoracion else -1
self.vine.update(votos=F('votos') + new_vote)
return super(Voto, self).save()
PS: If you want to know more about that F expression.

Django Q bad query logic

I'm trying to create a manager that has a method 'active_or_users' to retrieve all accounts that are active, or that an user has created. An active account has a start date that is either today, or somewhere in the past, and a end date that is somewhere in the future. Right now the active_or_users method works, however it returns duplicates of the same object. It's returning three copies of a user created active job. This is less than ideal.
from django.db.models import Q
from django.db import models
from django.contrib.auth.models import User
class ActiveJobs(models.Manager):
def active(self):
return super(ActiveJobs, self).get_query_set().\
filter(publications__publish_on__lte=date.today(),
publications__end_on__gt=date.today())
def active_or_users(self, user):
return super(ActiveJobs, self).get_query_set().\
filter((Q(publications__publish_on__lte=date.today()) &
Q(publications__end_on__gt=date.today())) | Q(creator=user))
class Job(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField(blank=True, null=True)
creator = models.ForeignKey(User)
objects = ActiveJobs()
class JobPublicationRecord(models.Model):
job = models.ForeignKey('Job', related_name='publications')
publish_on = models.DateField(auto_now=False)
end_on = models.DateField(auto_now=False, auto_now_add=False,
blank=True, null=True)
To put the comments into an answer
With the OR query, an instance will be returned for every hit of the query. I.e: an instance if a Job is created by user and another instance of the same job if also in the date range specified, etc.
So to fix this, change the method active_or_users to:
def active_or_users(self, user):
return super(ActiveJobs, self).get_query_set().filter(
(Q(publications__publish_on__lte=date.today()) &
Q(publications__end_on__gt=date.today())) | Q(creator=user)).distinct()