Properly update Django model's foreign key property - django

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

Related

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()

How to test if an instance of a django model was created?

I am testing an api for a game and have some issues with a model.
This is my model:
models.py
class Gamesession(models.Model):
gamemode = models.ForeignKey(Gamemode, on_delete=models.CASCADE, null=True)
user = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True)
gametype = models.ForeignKey(Gametype, on_delete=models.CASCADE)
created = models.DateTimeField(editable=False)
objects = models.Manager()
This is the test suite:
test_models.py
def setUp(self):
self.user = CustomUser.objects.create(id=1, username="carina")
self.gametype = Gametype.objects.create(id=1, name="NewGame", rounds=5, round_duration=60, enabled=True)
self.gamesession_user = self.user
self.gamesession_gametype = self.gametype
self.gamesession_created = datetime.utcnow().replace(tzinfo=pytz.UTC)
self.gamesession = Gamesession.objects.create(id=1,
user=self.gamesession_user,
gametype=self.gamesession_gametype,
created=self.gamesession_created)
def test_create_gamesession(self):
gamesession = Gamesession.objects.create(gametype=self.gametype)
assert gamesession.gametype == self.gamesession_gametype
Running my tests keeps retrieving the error: GamesessionTests::test_create_gamesession - django.db.utils.IntegrityError: null value in column "created" of relation "frontend_games
How can I solve this? Is there a better way to test if an instance of the model is created?
This happens because you defined created as a field that is not null=True and without any default=…, so that means that it does not assign a proper value to it. You thus can construct the object with:
def test_create_gamesession(self):
gamesession = Gamesession.objects.create(
gametype=self.gametype,
gamesession_created=self.gamesession_created
)
assert gamesession.gametype == self.gamesession_gametype
You probably do not want to specify a value anyway, you can make use of auto_now_add=True [Django-doc] to specify that Django should fill in the timestamp when creating the object:
class Gamesession(models.Model):
# …
created = models.DateTimeField(auto_now_add=True)
this will also make the field non-editable.

Django: How to update an Integerfield and decrement its value

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()

Using UniqueConstraint to solve "get() returned more than one" error in Django admin

I'm trying to access an (unmanaged) model via Django administration dashboard. The model doesn't have a primary key, but instead is unique across three fields.
class MyObjectView(models.Model):
solution_id = models.IntegerField(blank=True, null=True)
scenario_id = models.IntegerField(blank=True, null=True)
object_id = models.CharField(max_length=256, blank=True, null=True)
description= models.CharField(max_length=1000, blank=True, null=True)
year_of_creation = models.DateField(blank=True, null=True)
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'myobject_view'
While I am able to access the list of all items in the admin dashboard, as soon as I try to view one specific item I get the error:
get() returned more than one MyObjectView -- it returned 4!
As per the documentation, I tried adding a UniqueConstraint in the Meta class, but the constraint doesn't seem to have any effect, and the error above persists:
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'myobject_view'
constraints = [
models.UniqueConstraint(fields=['solution_id', 'scenario_id', 'object_id '], name='unique_object'),
]
Is this the proper way to solve the get() returned more than one error? Should the constraint work even on an unmanaged model?
I think if the object is unmanaged adding an UniqueConstraint in the Meta won't insert any constraint in the database.
You need to catch the exception:
try:
the_object = MyObjectView.objects.get(
object_id=object_id, scenario_id=scenario_id, solution_id=solution_id
)
return the_object # or do something different
except MyObjectView.MultipleObjectsReturned:
# multiple objects have the three same values
# manage that case
return None # just as example
Take a look to the reference of MultipleObjectsReturned exception
Another approach is to add .first() to your get(). That guarantees you will get a single result (or None), and will avoid errors at that point.
If the duplicates are a problem it sounds like you need to investigate how they are being created, fix that and then do some clear up.

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