Django change BooleanField value after DateField expires - django

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

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: 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 computed fields in admin

I'm trying to use a runtime-computed field in my admin page. This works fine, but I'd like to allow sorting based for that field. Using Django 1.5 (dev), is this possible? I've been scouring the interweb but can't find anything indicating that it is possible.
class Guest(models.Model)
email = models.CharField(max_length=255)
class Invitation(models.Model)
guest = models.ForeignKey(Guest)
created_on = models.DateTimeField(auto_now_add=True)
class GuestAdmin(admin.ModelAdmin):
list_display = ["email", "latest_invitation_sent_on",]
def latest_invitation_sent_on(self, o):
try:
return o.invitation_set.all().order_by(
"-created_on")[0].created_on.strftime("%B %d, %Y")
except IndexError:
return "N/A"
I'd like to be able to enable sorting by latest_invitation_sent_on. Are there any methods of doing this nicely that I'm unaware of?
You should be able to annotate Guests with their latest invitation time and then order_by it (order_by uses the DB to sort and as long as you can provide a valid DB field, table or virtual it should work).
class GuestManager(models.Manager):
def get_query_set(self):
return super(GuestManager, self).get_query_set().annotate(latest_invite=Max("invitation_set__created_on"))
class Guest(models.Model)
email = models.CharField(max_length=255)
objects = GuestManager()
class Invitation(models.Model)
guest = models.ForeignKey(Guest)
created_on = models.DateTimeField(auto_now_add=True)
class GuestAdmin(admin.ModelAdmin):
list_display = ["email", "latest_invite",]
If you only need latest_invite annotation once in a while it makes sense to move it to a separate method or even manager.
class GuestManager(models.Manager):
def by_invitations(self):
return super(GuestManager, self).get_query_set().annotate(latest_invite=Max("invitation_set__created_on")).order_by('-latest_invite')
>>> Guest.objects.by_invitations()

How do you set the initial value for a ManyToMany field in django?

I am using a ModelForm to create a form, and I have gotten the initial values set for every field in the form except for the one that is a ManyToMany field.
I understand that I need to give it a list, but I can't get it to work. My code in my view right now is:
userProfile = request.user.get_profile()
employer = userProfile.employer
bar_memberships = userProfile.barmembership.all()
profileForm = ProfileForm(
initial = {'employer': employer, 'barmembership' : bar_memberships})
But that doesn't work. Am I missing something here?
Per request in the comments, here's the relevant parts of my model:
# a class where bar memberships are held and handled.
class BarMembership(models.Model):
barMembershipUUID = models.AutoField("a unique ID for each bar membership",
primary_key=True)
barMembership = USStateField("the two letter state abbreviation of a bar membership")
def __unicode__(self):
return self.get_barMembership_display()
class Meta:
verbose_name = "bar membership"
db_table = "BarMembership"
ordering = ["barMembership"]
And the user profile that's being extended:
# a class to extend the User class with the fields we need.
class UserProfile(models.Model):
userProfileUUID = models.AutoField("a unique ID for each user profile",
primary_key=True)
user = models.ForeignKey(User,
verbose_name="the user this model extends",
unique=True)
employer = models.CharField("the user's employer",
max_length=100,
blank=True)
barmembership = models.ManyToManyField(BarMembership,
verbose_name="the bar memberships held by the user",
blank=True,
null=True)
Hope this helps.
OK, I finally figured this out. Good lord, sometimes the solutions are way too easy.
I need to be doing:
profileForm = ProfileForm(instance = userProfile)
I made that change, and now everything works.
Although the answer by mlissner might work in some cases, I do not think it is what you want. The keyword "instance" is meant for updating an existing record.
Referring to your attempt to use the keyword "initial", just change the line to:
bar_memberships = userProfile.barmembership.all().values_list('pk', flat=True)
I have not tested this with your code, but I use something similar in my code and it works.