Django ORM: Conditional filtering - django

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.

Related

query to django model to compare daily sale over previous day (compare two row of database model)

I have a django model that is "DailyReport" of the companies sale I want to find out company sale change over the previous day.
the model that i define is like that:
class DailyReport(models.Model):
company = models.CharField(max_length=50)
sale = models.IntegerField()
date = models.DateField()
How can i figure out this issue to add new column for every report that represent change rate over the previous day
Use the Lag window function to annotate each row with the previous sale amount for that company.
Then use another annotation to calculate the difference between the current and previous sale
from django.db.models import Window, F
from django.db.models.functions import Lag
DailyReport.objects.annotate(
prev_val=Window(
expression=Lag('sale', default=0),
partition_by=['company'],
order_by=F('date').asc(),
)
).annotate(
diff=F('sale') - F('prev_val')
)
Assuming you can have only one record of a company for each day, you can create a model property:
#property
def previous_day_sale(self):
date = self.date
dr = DailyReport.objects.filter(company=self.company, date=date-timedelta(days=1)
if dr:
return dr.first().sale - self.sale
You may need to override the save method, but you will have to cover all edge cases.
class DailyReport(models.Model):
company = models.CharField(max_length=50)
sale = models.IntegerField()
date = models.DateField()
sale_over_previous_day = models.IntegerField()
def save(self, *args, **kwargs):
previous_day_sale_object = DailyReport.objects.filter(company=self.company, date=date-timedelta(days=1))
if previous_day_sale_object:
previous_day_sale = previous_day_sale_object[0].sale
else:
previous_day_sale = 0
self.sale_over_previous_day = self.sale - previous_day_sale
super(DailyReport, self).save(*args, **kwargs)

How do I limit requests per user in Django rest framework using throttling?

I'm working on a Quiz app and one question gets uploaded every week in a month (4 ques/month). Now I want to make sure that a logged in user can only attempt the question twice per week and not more than that. How can I do this using throttling or any other way?
Here's my Quiz model:
Days = (
("Mon", "Monday"),
("Tue", "Tuesday"),
("Wed", "Wednesday"),
("Thu", "Thursday"),
("Fri", "Friday"),
("Sat", "Saturday"),
("Sun", "Sunday")
)
class QuizDetail(models.Model):
name = models.CharField(max_lenght=255, blank=False, null=False)
start_date = models.DateTimeField()
end_date = models.DateTimeField()
publisehd_week_day = models.CharField(max_length=255, choices=Days)
The published_week_day can change every month, so basically one month it can be Tuesday and next month it can be Thursday.
Note: If in a month published_week_day is Tuesday and a user attempts last week's quiz on Monday and exhausts his two attempts then on Tuesday he should be able to attempt as it will be a fresh quiz.
Store it in another field
I am assuming end_date only stores the last time the quiz ended.
Create a field, say count. Now increase count by 1 everytime a test is attempted. If it reaches above 2, dont allow the quiz to be taken.
What about resetting it?
You could calculate it every single time you make the request. However this is clearly extremely ineffiecient. Therefore, I would do something like so:
Create another process.(Have a look at Simple approach to launching background task in Django)
That process constantly searches through the database at fixed intervals.
If it finds that a week has passed since the last time the user took the test, it resets the count to 0. To do this simple subtract current datetime(datetime.now()) with end_time and compare it to 2 weeks.
You have to create one model which basically would have a relationship with your QuizDetail model
class UserQuizAttempt(models.Model)
quiz = models.ForeignKey(QuizDetail, on_delete=models.CASCADE)
user = models.ForeginKey(User, on_delete=models.CASCADE)
attempt_made_on = models.DateTimeField(auto_now_add=True)
so in your views.py file, where the user will make an API call to attempt the quiz you have to check whether the user has made any attempt for that particular quiz in that particular week. If it is exceeding 2 then just return a response saying that he's exceeding the limit of the attempt for the week.
a basic example could look like this
from datetime import date
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
class QuizAPI(APIView):
authentication_classes = [SessionAuthentication]
permission_classes = [IsAuthenticated]
def check_user_attempts(self, request, quiz_instance):
current_week = date.today().isocalendar()[1]
attempts = UserQuizAttempt.objects.filter(attempt_made_on__week=current_week=current_week, quiz=quiz_instance, user=request.user)
if attempts.count() > 2:
return False
return True
def post(self, request):
quiz_id = request.data.get('quiz_id')
quiz_instance = QuizDetail.objects.get(id=quiz_id)
if self.check_user_attempts(request, quiz_instance):
UserQuizAttempt.objects.create(quiz=quiz_instance, user=request.user)
# your logic goes here
...
else:
return Response("Your attempts exceeding for the week! Please try next week", status=status.HTTP_406_NOT_ACCEPTABLE)
So with this, you will have the history of the user's attempt made on the quiz which can be used for reporting or something.

Django - Add Nearest Monday to Queryset

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)

Filtering queryset if one value is greater than another value

I am trying to filter in view my queryset based on relation between 2 fields .
however always getting the error that my field is not defined .
My Model has several calculated columns and I want to get only the records where values of field A are greater than field B.
So this is my model
class Material(models.Model):
version = IntegerVersionField( )
code = models.CharField(max_length=30)
name = models.CharField(max_length=30)
min_quantity = models.DecimalField(max_digits=19, decimal_places=10)
max_quantity = models.DecimalField(max_digits=19, decimal_places=10)
is_active = models.BooleanField(default=True)
def _get_totalinventory(self):
from inventory.models import Inventory
return Inventory.objects.filter(warehouse_Bin__material_UOM__UOM__material=self.id, is_active = true ).aggregate(Sum('quantity'))
totalinventory = property(_get_totalinventory)
def _get_totalpo(self):
from purchase.models import POmaterial
return POmaterial.objects.filter(material=self.id, is_active = true).aggregate(Sum('quantity'))
totalpo = property(_get_totalpo)
def _get_totalso(self):
from sales.models import SOproduct
return SOproduct.objects.filter(product__material=self.id , is_active=true ).aggregate(Sum('quantity'))
totalso = property(_get_totalpo)
#property
def _get_total(self):
return (totalinventory + totalpo - totalso)
total = property(_get_total)
And this is line in my view where I try to get the conditional queryset
po_list = MaterialFilter(request.GET, queryset=Material.objects.filter( total__lte = min_quantity ))
But I am getting the error that min_quantity is not defined
What could be the problem ?
EDIT:
My problem got solved thank you #Moses Koledoye but in the same code I have different issue now
Cannot resolve keyword 'total' into field.Choices are: am, author, author_id, bom, bomversion, code, creation_time, description, id, inventory, is_active, is_production, itemgroup, itemgroup_id, keywords, materialuom, max_quantity, min_quantity, name, pomaterial, produce, product, slug, trigger_quantity, uom, updated_by, updated_by_id, valid_from, valid_to, version, warehousebin
Basically it doesn't show any of my calculated fields I have in my model.
Django cannot write a query which is conditioned on a field whose value is unknown. You need to use a F expression for this:
from django.db.models import F
queryset = Material.objects.filter(total__lte = F('min_quantity'))
And your FilterSet becomes:
po_list = MaterialFilter(request.GET, queryset = Material.objects.filter(total__lte=F('min_quantity')))
From the docs:
An F() object represents the value of a model field or annotated
column. It makes it possible to refer to model field values and
perform database operations using them without actually having to pull
them out of the database into Python memory

Custom model method in Django

I'm developing a web app in Django that manages chores on a reoccurring weekly basis. These are the models I've come up with so far. Chores need to be able to be assigned multiple weekdays and times. So the chore of laundry could be Sunday # 8:00 am and Wednesday # 5:30 pm. I first want to confirm the models below are the best way to represent this. Secondly, I'm a little confused about model relationships and custom model methods. Since these chores are on a reoccurring basis, I need to be able to check if there has been a CompletedEvent in this week. Since this is row level functionality, that would be a model method correct? Based on the models below, how would I check for this? It has me scratching my head.
models.py:
from django.db import models
from datetime import date
class ChoreManager(models.Manager):
def by_day(self, day_name):
return self.filter(scheduledday__day_name = day_name)
def today(self):
todays_day_name = date.today().strftime('%A')
return self.filter(scheduledday__day_name = todays_day_name)
class Chore(models.Model):
objects = ChoreManager()
name = models.CharField(max_length=50)
notes = models.TextField()
class Meta:
ordering = ['scheduledday__time']
class ScheduledDay(models.Model):
day_name = models.CharField(max_length=8)
time = models.TimeField()
chore = models.ForeignKey('Chore')
class CompletedEvent(models.Model):
date_completed = DateTimeField(auto_now_add=True)
chore = models.ForeignKey('Chore')
Then all you need to do is:
monday_of_week = some_date - datetime.timedetla(days=some_date.weekday())
end_of_week = date + datetime.timedelta(days=7)
chore = Chore.objects.get(name='The chore your looking for')
ScheduledDay.objects.filter(completed_date__gte=monday_of_week,
completed_date__lt=end_of_week,
chore=chore)
A neater (and faster) option is to use Bitmasks!
Think of the days of the week you want a chore to be repeated on as a binary number—a bit for each day. For example, if you wanted a chore repeated every Tuesday, Friday and Sunday then you would get the binary number 1010010 (or 82 in decimal):
S S F T W T M
1 0 1 0 0 1 0 = 1010010
Days are reversed for sake of illustration
And to check if a chore should be done today, simply get the number of that day and do an &:
from datetime import datetime as dt
if dt.today().weekday() & 0b1010100:
print("Do chores!")
Models
Your models.py would look a bit like this:
from django.contrib.auth.models import User
from django.db import models
from django.utils.functional import cached_property
class Chore(models.Model):
name = models.CharField(max_length=128)
notes = models.TextField()
class ChoreUser(models.Model):
chore_detail = models.ForeignKey('ChoreDetail')
user = models.ForeignKey('ChoreDetail')
completed_time = models.DateTimeField(null=True, blank=True)
class ChoreDetail(models.Model):
chore = models.ForeignKey('Chore')
chore_users = models.ManyToManyField('User', through=ChoreUser)
time = models.DateTimeField()
date_begin = models.DateField()
date_end = models.DateField()
schedule = models.IntegerField(help_text="Bitmask of Weekdays")
#cached_property
def happens_today(self):
return bool(dt.today().weekday() & self.weekly_schedule)
This schema has a M2M relationship between a User and a Chore's Schedule. So you can extend your idea, like record the duration of the chore (if you want to), or even have many users participating in the same chore.
And to answer your question, if you'd like to get the list of completed events this week, you could could put this in a Model Manager for ChoreUser:
from datetime import datetime as dt, timedelta
week_start = dt.today() - timedelta(days=dt.weekday())
week_end = week_start + timedelta(days=6)
chore_users = ChoreUser.objects.filter(completed_time__range=(week_start, week_end))
Now you have all the information you need in a single DB call:
user = chore_users[0].user
time = chore_users[0].chore_detail.time
name = chore_users[0].chore_detail.chore.name
happens_today = chore_users[0].chore_detail.happens_today
You could also get all the completed chores for a user easily:
some_user.choreuser_set.filter(completed_time__range=(week_start, week_end))