Django - Add Nearest Monday to Queryset - django

I have an Order model like so:
class Order(models.Model):
created_at = models.DateTimeField(...)
An order can be created at any time, but all orders get shipped out on the following Monday.
How can I add an extra field to my orders queryset called assembly_date that reflects the next Monday (date the order should be shipped)?
I tried creating a custom OrderManager like so, but am not sure how to correctly set the assembly_date:
from django.db.models import F, ExpressionWrapper, DateField
from django.db.models.functions import ExtractWeekDay
class OrderManager(models.Manager):
def get_queryset():
# need help with logic here:
return self.super().get_queryset().annotate(
assembly_date = ExpressionWrapper(
F("created_at") - ExtractWeekDay("created_at"),
output_field = DateField(),
)
)
But this operation results in the following error:
django.db.utils.ProgrammingError: operator does not exist: timestamp with time zone - double precision
LINE 1: ...E NULL END) * 2628000.0) * INTERVAL '1 seconds')) - EXTRACT(...
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
Keep in mind, I want to be able to filter all orders based on their assembly_date.

Basically you need to dynamically generate timedelta inside of annotate. But as far as I know, there is no way you can apply isoweekday() to a datetimefield inside of annotate.
You can have another field as assembly_date in your model, and use it directly to query.
from datetime import timedelta, date
class Order(models.Model):
created_at = models.DateTimeField(...)
assembly_date = models.DateTimeField(...)
def save(self, *args, **kwargs):
weekday = self.created_at.isoweekday() # 1 is Monday, 2 is Tuesday.
daysdiff = 8 - weekday
self.assembly_date = self.created_at + timedelta(days= daysdiff)
super(Order, self).save(*args, **kwargs)

Related

Django ORM: Text aggregator on subquery

I banged my head on this one:
I have 2 models, and I am trying to design a custom manager/queryset which will allow me to annotate to each Series the id of the linked puzzles satisfying certain conditions in the format '2,13,26'.
The simplified models:
class Series(models.Model):
puzzles = models.ManyToManyField(
Puzzle, through='SeriesElement', related_name='series')
is_public = models.BooleanField(null=False, blank=False, default=False)
class Puzzle(models.Model):
pass
my custom aggregator:
from django.db.models.aggregates import Aggregate
from django.db.models.functions import Coalesce
from django.db.models.fields import CharField
from django.db.models.expressions import Value
class GroupConcat(Aggregate):
"""
according to https://stackoverflow.com/questions/10340684/group-concat-equivalent-in-django
according to https://stackoverflow.com/a/55216659
would be compatible with MySQL and SQLite
"""
function = 'GROUP_CONCAT'
def __init__(self, expression, distinct=False, ordering=None, **extra):
super(GroupConcat, self).__init__(expression,
distinct='DISTINCT ' if distinct else '',
ordering=' ORDER BY %s' % ordering if ordering is not None else '',
output_field=CharField(),
**extra)
def as_sqlite(self, compiler, connection, **extra):
return super().as_sql(compiler,
connection,
template='%(function)s(%(distinct)s%(expressions)s%(ordering)s)',
**extra)
one tentative to achieve my goal:
pzl_sub = apps.get_model('puzzles', 'Puzzle').objects.filter(series__id= OuterRef('id'))
pzl_sub = pzl_sub.filter(series_elements__isnull=False).add_nb_public_series().filter(nb_public_series=5)
pzl_ids= pzl_sub.order_by().values('id')
qs = Series.objects.annotate(id_str_pzl = GroupConcat(pzl_ids))
I obtain only one puzzle.id that fit the specified conditions, instead of the concat of all of the puzzle.ids that fit the conditions
Any clue on what I'm doing wrong?
After days of fighting, and testing everything, I finally figured it out:
pzl_sub = apps.get_model('puzzles', 'Puzzle').objects.filter(series__id= OuterRef('id'))
pzl_sub = pzl_sub.filter(series_elements__isnull=False).add_nb_public_series().filter(nb_public_series=5)
pzl_ids= pzl_sub.order_by().values('series__id')
qs = Series.objects.annotate(id_str_pzl = Subquery(pzl_ids.annotate(result=GroupConcat('id')).order_by().values('result')))
in pzl_ids, we need to single out values('series__id')
and only in a second step, we need to annotate with the aggregator, and single-out again the resulting value ...

Django: cannot annotate using prefetch calculated attribute

Target is to sum and annotate workingtimes for each employee on a given time range.
models:
class Employee(models.Model):
first_name = models.CharField(max_length=64)
class WorkTime(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name="work_times")
work_start = models.DateTimeField()
work_end = models.DateTimeField()
work_delta = models.IntegerField(default=0)
def save(self, *args, **kwargs):
self.work_delta = (self.work_end - self.work_start).seconds
super().save(*args, **kwargs)
getting work times for each employee at a given date range:
queryset = Employee.objects.prefetch_related(
Prefetch(
'work_times',
queryset=WorkTime.objects.filter(work_start__date__range=("2021-03-01", "2021-03-15"]))
.order_by("work_start"),
to_attr="filtered_work_times"
)).all()
trying to annotate sum of work_delta to each employee:
queryset.annotate(work_sum=Sum("filtered_work_times__work_delta"))
This causes a FieldError:
Cannot resolve keyword 'filtered_work_times' into field. Choices are: first_name, id, work_times
How would one proceed from here? Using Django 3.1 btw.
You should use filtering on annotations.
I haven't tried, but I think the following code might help you:
from django.db.models import Sum, Q
Employee.objects.annotate(
work_sum=Sum(
'work_times__work_delta',
filter=Q(work_times__work_start__date__range=["2021-03-01", "2021-03-15"])
)
)
You cannot use the prefetch_related values in the query because simply the prefetching is done separately, Django would first fetch the current objects and then make queries to fetch the related objects so the field you try to refer is not even part of the query you want to add it to.
Instead of doing this simply add a filter [Django docs] keyword argument to your aggregation function:
from django.db.models import Q
start_date = datetime.date(2021, 3, 1)
end_date = datetime.date(2021, 3, 15)
result = queryset.annotate(work_sum=Sum("work_times__work_delta", filter=Q(work_times__work_start__date__range=(start_date, end_date))))

How to run a function in Django after a certain time passed by

Having the model of Station with the below fields:
class Station(models.Model):
is_available = models.BooleanField(default=True)
unavailable_until = models.DateTimeField(null=True,blank=True)
I can define until when my station is unavailable by giving a DateTime value , so when that action happens the is_available value turns to False.
I want to turn the is_available value to True every time the unavailable_until value passed by(comparing with the current time based on the timezone).
How can I achieve an automation like this?
Imagine that I have a lot of Station records which belong to station owners which can update the availability (assign new unavailable_until value if passed by) whenever they want.
I think the logic could be something like:
def turn_availability_to_true(station):
if (station.unavailable_until < current_time):
station.is_available = True
But how can I implement a function like this to be called by its own when the unavailable_until value passed by?
Instead of using a field, you can .annotate(…) [Django-doc] to calculate dynamically if the Station is available:
The model thus then looks like
class Station(models.Model):
# no is_available
unavailable_until = models.DateTimeField(null=True,blank=True)
and we annotate this with:
from django.db.models import BooleanField, ExpressionWrapper, Q
from django.db.models.functions import Now
Station.objects.annotate(
is_available=ExpressionWrapper(
~Q(unavailable_until__gte=Now()),
output_field=BooleanField()
)
)
If you need this often, you can implement a manager [Django-doc] that will automatically add this to the Stations:
from django.db.models import BooleanField, ExpressionWrapper, Q
from django.db.models.functions import Now
class StationManager(models.Manager):
def get_queryset(self, *args, **kwargs):
return super().get_queryset(*args, **kwargs).annotate(
is_available=ExpressionWrapper(
~Q(unavailable_until__gte=Now()),
output_field=BooleanField()
)
)
class Station(models.Model):
# no is_available
unavailable_until = models.DateTimeField(null=True,blank=True)
objects = StationManager()
The advantage of not running a scheduled task is that it is hard to alter that task if later the unavailable_until field is modified, for example to a later date. In that case, one has to remove the scheduled task, and schedule a new one. By annotating, we avoid synchronization issues.

Django ORM: Conditional filtering

In a Django app, I keep daily scores of users in such a model:
class Score(models.Model):
user = models.ForeignKey(User)
score = models.PositiveIntegerField(default=0)
date = models.DateField(auto_now_add=True)
I want to find out the days when a user's score has changed drastically compared to a consequent day. That is, if for example, the user scores 10 times higher than the previous day.
How can I include such a condition in a query filter using Django ORM? Is it possible with a single query using conditional expressions as described here: https://docs.djangoproject.com/en/1.9/ref/models/conditional-expressions/
Thanks.
If you change your Score class slightly to include the previous day's score (which is admittedly pretty wasteful), you can pack the query into one line using F expressions.
Your new class:
class Score(models.Model):
user = models.ForeignKey(User)
score = models.PositiveIntegerField(default=0)
lastscore = models.PositiveIntegerField(default=0)
date = models.DateField(auto_now_add=True)
Then the filter becomes:
from django.db.models import F
daily_chg = 10
big_changes = Score.objects.filter(score__gt=daily_chg*F('lastscore'))
Instead of using timedeltas to search for and set the previous day's score field, I'd look into establishing an ordering via a Meta class and calling latest() when saving the current day's score.
Using timedelta we can test for the last week's days for a given user as such:
from my_app.models import Score
import datetime
def some_view(request):
days_with_score_boost = []
today = datetime.date.today()
for each_day in xrange(0,7):
day_to_test, day_before_to_test = today - datetime.timedelta(days=each_day), today - datetime.timedelta(days=each_day + 1)
day_before_score = Score.objects.get(user=request.user,date=today - datetime.timedelta(days=each_day)).score # will need to catch the exception raised if .get return None and process appropriately
if Score.objects.filter(user=request.user,score__gte=days_before_score * 10,date=day_before_to_test).count() > 0:
days_with_score_boost.append(day_to_test)
days_with_score_boost will be a list of datetime.date objects where the score increased by 10 or more from the day before.
In reponse to your comment, I'd make a measure that checks at save time whether the score boost has occured. However, I would get rid of auto_now_add in favor of writing in the save method.
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist
class Score(models.Model):
user = models.ForeignKey(User)
score = models.PositiveIntegerField(default=0)
date = models.DateField(null=True,blank=True)
increased_ten_over_previous_day = models.BooleanField(null=True,blank=True)
def save(self, *args, **kwargs):
self.date = timezone.now().today()
try:
yesterday_score = Score.objects.get(date=self.date-timezone.timedelta(days=1)).score
self.increased_ten_over_previous_day = (yesterday_score * 10) <= self.score
except ObjectDoesNotExist: # called if Score.object.get returns no object; requires you only have one score per user per date
self.increased_ten_over_previous_day = False
super(self, Score).save(*args, **kwargs)
Then you could filter objects for a date_range where increased_ten_over_previous_day is True.

How to set a Django model field's default value to a function call / callable (e.g., a date relative to the time of model object creation)

EDITED:
How can I set a Django field's default to a function that gets evaluated each time a new model object gets created?
I want to do something like the following, except that in this code, the code gets evaluated once and sets the default to the same date for each model object created, rather than evaluating the code each time a model object gets created:
from datetime import datetime, timedelta
class MyModel(models.Model):
# default to 1 day from now
my_date = models.DateTimeField(default=datetime.now() + timedelta(days=1))
ORIGINAL:
I want to create a default value for a function parameter such that it is dynamic and gets called and set each time the function is called. How can I do that? e.g.,
from datetime import datetime
def mydate(date=datetime.now()):
print date
mydate()
mydate() # prints the same thing as the previous call; but I want it to be a newer value
Specifically, I want to do it in Django, e.g.,
from datetime import datetime, timedelta
class MyModel(models.Model):
# default to 1 day from now
my_date = models.DateTimeField(default=datetime.now() + timedelta(days=1))
The question is misguided. When creating a model field in Django, you are not defining a function, so function default values are irrelevant:
from datetime import datetime, timedelta
class MyModel(models.Model):
# default to 1 day from now
my_date = models.DateTimeField(default=datetime.now() + timedelta(days=1))
This last line is not defining a function; it is invoking a function to create a field in the class.
In this case datetime.now() + timedelta(days=1) will be evaluated once, and stored as the default value.
PRE Django 1.7
Django [lets you pass a callable as the default][1], and it will invoke it each time, just as you want:
from datetime import datetime, timedelta
class MyModel(models.Model):
# default to 1 day from now
my_date = models.DateTimeField(default=lambda: datetime.now() + timedelta(days=1))
Django 1.7+
Please note that since Django 1.7, usage of lambda as default value is not recommended (c.f. #stvnw comment). The proper way to do this is to declare a function before the field and use it as a callable in default_value named arg:
from datetime import datetime, timedelta
# default to 1 day from now
def get_default_my_date():
return datetime.now() + timedelta(days=1)
class MyModel(models.Model):
my_date = models.DateTimeField(default=get_default_my_date)
More information in the #simanas answer below
[1]: https://docs.djangoproject.com/en/dev/ref/models/fields/#default
Doing this default=datetime.now()+timedelta(days=1) is absolutely wrong!
It gets evaluated when you start your instance of django. If you are under apache it will probably work, because on some configurations apache revokes your django application on every request, but still you can find you self some day looking through out your code and trying to figure out why this get calculated not as you expect.
The right way of doing this is to pass a callable object to default argument. It can be a datetime.today function or your custom function. Then it gets evaluated every time you request a new default value.
def get_deadline():
return datetime.today() + timedelta(days=20)
class Bill(models.Model):
name = models.CharField(max_length=50)
customer = models.ForeignKey(User, related_name='bills')
date = models.DateField(default=datetime.today)
deadline = models.DateField(default=get_deadline)
There's an important distinction between the following two DateTimeField constructors:
my_date = models.DateTimeField(auto_now=True)
my_date = models.DateTimeField(auto_now_add=True)
If you use auto_now_add=True in the constructor, the datetime referenced by my_date is "immutable" (only set once when the row is inserted to the table).
With auto_now=True, however, the datetime value will be updated every time the object is saved.
This was definitely a gotcha for me at one point. For reference, the docs are here:
https://docs.djangoproject.com/en/dev/ref/models/fields/#datetimefield
Sometimes you may need to access model data after creating a new user model.
Here is how I generate a token for each new user profile using the first 4 characters of their username:
from django.dispatch import receiver
class Profile(models.Model):
auth_token = models.CharField(max_length=13, default=None, null=True, blank=True)
#receiver(post_save, sender=User) # this is called after a User model is saved.
def create_user_profile(sender, instance, created, **kwargs):
if created: # only run the following if the profile is new
new_profile = Profile.objects.create(user=instance)
new_profile.create_auth_token()
new_profile.save()
def create_auth_token(self):
import random, string
auth = self.user.username[:4] # get first 4 characters in user name
self.auth_token = auth + ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range(random.randint(3, 5)))
You can't do that directly; the default value is evaluated when the function definition is evaluated. But there are two ways around it.
First, you can create (and then call) a new function each time.
Or, more simply, just use a special value to mark the default. For example:
from datetime import datetime
def mydate(date=None):
if date is None:
date = datetime.now()
print date
If None is a perfectly reasonable parameter value, and there's no other reasonable value you could use in its place, you can just create a new value that's definitely outside the domain of your function:
from datetime import datetime
class _MyDateDummyDefault(object):
pass
def mydate(date=_MyDateDummyDefault):
if date is _MyDateDummyDefault:
date = datetime.now()
print date
del _MyDateDummyDefault
In some rare cases, you're writing meta-code that really does need to be able to take absolutely anything, even, say, mydate.func_defaults[0]. In that case, you have to do something like this:
def mydate(*args, **kw):
if 'date' in kw:
date = kw['date']
elif len(args):
date = args[0]
else:
date = datetime.now()
print date
Pass the function in as a parameter instead of passing in the result of the function call.
That is, instead of this:
def myfunc(date=datetime.now()):
print date
Try this:
def myfunc(date=datetime.now):
print date()