Make a change to date fields automatically in Django - django

I have 2 date fields and I want to update the date automatically.
The code won't work until I update the updated field by myself.
How can I make it update itself (updated fields) automatically?
STATUS_CHOICES = (
('P', 'Paid'),
('U', 'UNPAID'),
)
status = models.CharField(
max_length=1, choices=STATUS_CHOICES)
updated = models.DateTimeField(default=datetime.now() )
expiry = models.DateTimeField(default=datetime.now() + timedelta(days=30) )
def save(self):
if(self.expiry >= self.updated):
self.status = default = "P"
else:
self.status = default = "U"
self.expiry = default=self.updated+timedelta(days=1)
super(Users, self).save()

The DateTimeField have a auto_now property, that will set the field to the current date every time the object is saved.
The main question is what event should trigger the save action. For example you can improve your view, so that every time someone visit the page, your model would update as well as desired DateTimeField. On the other hand you may want this happen on the schedule basis, so you should use something like Cron, Celery or Huey.

In your case you have two options.
Make some code that will be executed periodically that will update the status based on current date.
Do not store status in the database at all. If it can depend only from expiry field it can be a computed property
P.S.
It's not related to what you are asking but I have a suggestion for you. Never ever use the direct result from datetime.now() as default value for DateTimeField. It's misleading for you and it's not the same as what you want to achieve. datetime.now will be executed at import time (when the server starts) and will stay the same until next restart.
If you want the default value to be the exact time when the record was created then default value need to be a function (which will be executed every time) and not the result of the function.
This means that updated need to be DateTimeFiled(default=datetime.now) (without the brackets).
This logic is more like for a field called created_at or date_created and not updated but this is your logic.

Related

Uploaded and Deadline fields in Django models

I am making a to do app and in models I have a Task model and dateTimeFields "uploaded", which has auto_now_add=True. Now I want to add deadline field, where users can add the task deadlines. I tried adding second DateTimeField but there's a problem when I make migrations. here's the code and the powershell warning:
models:
class Task(models.Model):
title = models.CharField(max_length=50)
completed = models.BooleanField(default=False)
uploaded = models.DateTimeField(auto_now_add=True)
deadline = models.DateTimeField()
def __str__(self):
return self.title
powershell:
*You are trying to add a non-nullable field 'deadline' to task without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
Provide a one-off default now (will be set on all existing rows with a null value for this column)
Quit, and let me add a default in models.py
Select an option:*
But there's a problem when I make migrations.
The problem is that the database might already have records, so what will you do to the Tasks in the database that now need a value for the existing records.
You can for example decide to work with the current timestamp. You can do this by selecting 1, and then work with timezone.now:
You are trying to add a non-nullable field 'deadline' to task without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> timezone.now
This will thus set the current timestamp to all the Tasks already in the database.
You can also decide to set it to a datetime in the future, for example 1-1-2100:
You are trying to add a non-nullable field 'deadline' to task without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> datetime.datetime(2100, 1, 1)
Another alternative is to provide the default in the model itself, but then it also will take that default for new tasks and thus not only the tasks already recorded.
Often a field is made nullable in case one does not have to specify the current timestamp. In that case you thus specify null=True, and by default it will use NULL/None as default for the exiting records:
class Task(models.Model):
# …
deadline = models.DateTimeField(null=True)
# …
If deadline is added afterwards you create the model, you need to tell django that its a null able field, such that deadline can have no value. just add null=True in deadline field
class Task(models.Model):
title = models.CharField(max_length=50)
completed = models.BooleanField(default=False)
uploaded = models.DateTimeField(auto_now_add=True)
deadline = modles.DateTimeField(null=True)
def __str__(self):
return self.title

django annotate with queryset

I have Users who take Surveys periodically. The system has multiple surveys which it issues at set intervals from the submitted date of the last issued survey of that particular type.
class Survey(Model):
name = CharField()
description = TextField()
interval = DurationField()
users = ManyToManyField(User, related_name='registered_surveys')
...
class SurveyRun(Model):
''' A users answers for 1 taken survey '''
user = ForeignKey(User, related_name='runs')
survey = ForeignKey(Survey, related_name='runs')
created = models.DateTimeField(auto_now_add=True)
submitted = models.DateTimeField(null=True, blank=True)
# answers = ReverseForeignKey...
So with the models above a user should be alerted to take survey A next on this date:
A.interval + SurveyRun.objects.filter(
user=user,
survey=A
).latest('submitted').submitted
I want to run a daily periodic task which queries all users and creates new runs for all users who have a survey due according to this criteria:
For each survey the user is registered:
if no runs exist for that user-survey combo then create the first run for that user-survey combination and alert the user
if there are runs for that survey and none are open (an open run has been created but not submitted so submitted=None) and the latest one's submitted date plus the survey's interval is <= today, create a new run for that user-survey combo and alert the user
Ideally I could create a manager method which would annotate with a surveys_due field like:
users_with_surveys_due = User.objects.with_surveys_due().filter(surveys_due__isnull=False)
Where the annotated field would be a queryset of Survey objects for which the user needs to submit a new round of answers.
And I could issue alerts like this:
for user in users_with_surveys_due.all():
for survey in user.surveys_due:
new_run = SurveyRun.objects.create(
user=user,
survey=survey
)
alert_user(user, run)
However I would settle for a boolean flag annotation on the User object indicating one of the registered_surveys needs to create a new run.
How would I go about implementing something like this with_surveys_due() manager method so Postgres does all the heavy lifting? Is it possible to annotate with a collection objects, like a reverse FK?
UPDATE:
For clarity here is my current task in python:
def make_new_runs_and_alert_users():
runs = []
Srun = apps.get_model('surveys', 'SurveyRun')
for user in get_user_model().objects.prefetch_related('registered_surveys', 'runs').all():
for srvy in user.registered_surveys.all():
runs_for_srvy = user.runs.filter(survey=srvy)
# no runs exist for this registered survey, create first run
if not runs_for_srvy.exists():
runs.append(Srun(user=user, survey=srvy))
...
# check this survey has no open runs
elif not runs_for_srvy.filter(submitted=None).exists():
latest = runs_for_srvy.latest('submitted')
if (latest.submitted + qnr.interval) <= timezone.now():
runs.append(Srun(user=user, survey=srvy))
Srun.objects.bulk_create(runs)
UPDATE #2:
In attempting to use Dirk's solution I have this simple example:
In [1]: test_user.runs.values_list('survey__name', 'submitted')
Out[1]: <SurveyRunQuerySet [('Test', None)]>
In [2]: test_user.registered_surveys.values_list('name', flat=True)
Out[2]: <SurveyQuerySet ['Test']>
The user has one open run (submitted=None) for the Test survey and is registered to one survey (Test). He/She should not be flagged for a new run seeing as there is an un-submitted run outstanding for the only survey he/she is registered for. So I create a function encapsulating the Dirk's solution called get_users_with_runs_due:
In [10]: get_users_with_runs_due()
Out[10]: <UserQuerySet [<User: test#gmail.com>]> . # <-- should be an empty queryset
In [107]: for user in _:
print(user.email, i.has_survey_due)
test#gmail.com True # <-- should be false
UPDATE #3:
In my previous update I had made some changes to the logic to properly match what I wanted but neglected to mention or show the changes. Here is the query function below with comments by the changes:
def get_users_with_runs_due():
today = timezone.now()
survey_runs = SurveyRun.objects.filter(
survey=OuterRef('pk'),
user=OuterRef(OuterRef('pk'))
).order_by('-submitted')
pending_survey_runs = survey_runs.filter(submitted__isnull=True)
surveys = Survey.objects.filter(
users=OuterRef('pk')
).annotate(
latest_submission_date=Subquery(
survey_runs.filter(submitted__isnull=False).values('submitted')[:1]
)
).annotate(
has_survey_runs=Exists(survey_runs)
).annotate(
has_pending_runs=Exists(pending_survey_runs)
).filter(
Q(has_survey_runs=False) | # either has no runs for this survey or
( # has no pending runs and submission date meets criteria
Q(has_pending_runs=False, latest_submission_date__lte=today - F('interval'))
)
)
return User.objects.annotate(has_survey_due=Exists(surveys)).filter(has_survey_due=True)
UPDATE #4:
I tried to isolate the issue by creating a function which would make most of the annotations on the Surveys by user in an attempt to check the annotation on that level prior to querying the User model with it.
def annotate_surveys_for_user(user):
today = timezone.now()
survey_runs = SurveyRun.objects.filter(
survey=OuterRef('pk'),
user=user
).order_by('-submitted')
pending_survey_runs = survey_runs.filter(submitted=None)
return Survey.objects.filter(
users=user
).annotate(
latest_submission_date=Subquery(
survey_runs.filter(submitted__isnull=False).values('submitted')[:1]
)
).annotate(
has_survey_runs=Exists(survey_runs)
).annotate(
has_pending_runs=Exists(pending_survey_runs)
)
This worked as expected. Where the annotations were accurate and filtering with:
result.filter(
Q(has_survey_runs=False) |
(
Q(has_pending_runs=False) &
Q(latest_submission_date__lte=today - F('interval'))
)
)
produced the desired results: An empty queryset where the user should not have any runs due and vice-versa. Why is this not working when making it the subquery and querying from the User model?
To annotate users with whether or not they have a survey due, I'd suggest to use a Subquery expression:
from django.db.models import Q, F, OuterRef, Subquery, Exists
from django.utils import timezone
today = timezone.now()
survey_runs = SurveyRun.objects.filter(survey=OuterRef('pk'), user=OuterRef(OuterRef('pk'))).order_by('-submitted')
pending_survey_runs = survey_runs.filter(submitted__isnull=True)
surveys = Survey.objects.filter(users=OuterRef('pk'))
.annotate(latest_submission_date=Subquery(survey_runs.filter(submitted__isnull=False).values('submitted')[:1]))
.annotate(has_survey_runs=Exists(survey_runs))
.annotate(has_pending_runs=Exists(pending_survey_runs))
.filter(Q(has_survey_runs=False) | Q(latest_submission_date__lte=today - F('interval')) & Q(has_pending_runs=False))
User.objects.annotate(has_survey_due=Exists(surveys))
.filter(has_survey_due=True)
I'm still trying to figure out how to do the other one. You cannot annotate a queryset with another queryset, values must be field equivalents. Also you cannot use a Subquery as queryset parameter to Prefetch, unfortunately. But since you're using PostgreSQL you could use ArrayField to list the ids of the surveys in a wrapped value, but I haven't found a way to do that, as you can't use aggregate inside a Subquery.

Checking for overlapping TimeField ranges

I have this model:
class Task(models.Model):
class Meta:
unique_together = ("campaign_id", "task_start", "task_end", "task_day")
campaign_id = models.ForeignKey(Campaign, on_delete=models.DO_NOTHING)
playlist_id = models.ForeignKey(PlayList, on_delete=models.DO_NOTHING)
task_id = models.AutoField(primary_key=True, auto_created=True)
task_start = models.TimeField()
task_end = models.TimeField()
task_day = models.TextField()
I need to write a validation test that checks if a newly created task time range overlaps with an existing one in the database.
For example:
A task with and ID 1 already has a starting time at 5:00PM and ends at 5:15PM on a Saturday. A new task cannot be created between the first task's start and end time. Where should I write this test and what is the most efficent way to do this? I also use DjangoRestFramework Serializers.
When you receive the form data from the user, you can:
Check the fields are consistent: user task_start < user task_end, and warn the user if not.
Query (SELECT) the database to retrieve all existing tasks which intercept the user time,
Order the records by task_start (ORDER BY),
Select only records which validate your criterion, a.k.a.:
task_start <= user task_start <= task_end, or,
task_start <= user task_end <= task_end.
warn the user if at least one record is found.
Everything is OK:
Construct a Task instance,
Store it in database.
Return success.
Implementation details:
task_start and task_end could be indexed in your database to improve selection time.
I saw that you also have a task_day field (which is a TEXT).
You should really consider using UTC DATETIME fields instead of TEXT, because you need to compare date AND time (and not only time): consider a task which starts at 23:30 and finish at 00:45 the day after…
This is how I solved it. It's not optimal by far, but I'm limited to python 2.7 and Django 1.11 and I'm also a beginner.
def validate(self, data):
errors = {}
task_start = data.get('task_start')
task_end = data.get('task_end')
time_filter = Q(task_start__range=[task_start, task_end])
| Q(task_end__range=[task_start, task_end])
filter_check = Task.objects.filter(time_filter).exists()
if task_start > task_end:
errors['error'] = u'End time cannot be earlier than start time!'
raise serializers.ValidationError(errors)
elif filter_check:
errors['errors'] = u'Overlapping tasks'
raise serializers.ValidationError(errors)
else:
pass
return data

django event with date available=True, make available=False on event.date is today

I have a event model which works fine, but i need the boolean to switch to false once the event_date == today, so people cannot book old events
class Event(models.Model):
event_date = models.DateField()
start = models.ForeignKey(TimeSlots, on_delete=models.CASCADE, verbose_name='Slot Time', null=True)
available = models.BooleanField(default=True)
class Meta:
verbose_name = u'Event'
verbose_name_plural = u'Event'
def __str__(self):
return str(self.event_date)
def get_absolute_url(self):
url = reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=[self.pk])
return u'%s' % (url, str(self.start))
i was thinking of something like this
def save(day, month, year):
events = Event.objects.filter(date__day=day, date__month=month, date__year=year)
for event in events:
change.available = False
change.save()
but it doesnt work without a self
, what can i use to do make the boolean switch to false when event_date==current_date ?
thanks
i taught that the solution was
#property def available(self):
return self.event_date > timezone.now().date()
the problem is that, i still need to manually put the boolean true or false. But when i put the property def available,
the boolean field disapear in admin and old date as marked as non available as it should BUT, newer date cannot be put as unavailable, but i need to be able to set to future date(event) as unavailable :/
For that specific situation, there are several solutions, the one that you proposed will not work (or not have the desired effect).
Perhaps the simplest one, to mark all events that happen today as not available (available=False) would be to create a management command containing the code you provided, that would be executed daily by a cronjob. This way all events that happen on a given day would be marked as not available, regardless of their "status".
Other solution (that would only work if the availability only depends on the date, which does not seem to be the case) would be to remove the database column (available = models.BooleanField(default=True) ) and implement an object property like this:
#property
def available(self):
return self.event_date > timezone.now().date()
You should use cron jobs for such tasks. You should write a function that deactivates events based on your condition event_date == today and than run this function with cron job at particular hour each day.
One of the simplest Django plugins for cron jobs is django-crontab. It's very easy to set it up and it has good docs.

Django: Distinct on forgin key relationship

I'm working on a Ticket/Issue-tracker in django where I need to log the status of each ticket. This is a simplification of my models.
class Ticket(models.Model):
assigned_to = ForeignKey(User)
comment = models.TextField(_('comment'), blank=True)
created = models.DateTimeField(_("created at"), auto_now_add=True)
class TicketStatus(models.Model):
STATUS_CHOICES = (
(10, _('Open'),),
(20, _('Other'),),
(30, _('Closed'),),
)
ticket = models.ForeignKey(Ticket, verbose_name=_('ticket'))
user = models.ForeignKey(User, verbose_name=_('user'))
status = models.IntegerField(_('status'), choices=STATUS_CHOICES)
date = models.DateTimeField(_("created at"), auto_now_add=True)
Now, getting the status of a ticket is easy sorting by date and retrieving the first column like this.
ticket = Ticket.objects.get(pk=1)
ticket.ticketstatus_set.order_by('-date')[0].get_status_display()
But then I also want to be able to filter on status in the Admin, and those have to get the status trough a Ticket-queryset, which makes it suddenly more complex. How would I get a queryset with all Tickets with a certain status?
I guess you are trying to avoid a cycle (asking for each ticket status) to filter manually the queryset. As far as I know you cannot avoid that cycle. Here are ideas:
# select_related avoids a lot of hits in the database when enter the cycle
t_status = TicketStatus.objects.select_related('Ticket').filter(status = ID_STATUS)
# this is an array with the result
ticket_array = [ts.ticket for ts in tickets_status]
Or, since you mention you were looking for a QuerySet, this might be what you are looking for
# select_related avoids a lot of hits in the database when enter the cycle
t_status = TicketStatus.objects.select_related('Ticket').filter(status = ID_STATUS)
# this is a QuerySet with the result
tickets = Tickets.objects.filter(pk__in = [ts.ticket.pk for ts in t_status])
However, the problem might be in the way you are modeling the data. What you called TickedStatus is more like TicketStatusLog because you want to keep track of the user and date who change the status.
Therefore, the reasonable approach is to add a field 'current_status' to the Ticket model that is updated each time a new TicketStatus is created. In this way (1) you don't have to order a table each time you ask for a ticket and (2) you would simply do something like Ticket.objects.filter(current_status = ID_STATUS) for what I think you are asking.