Django: Fix order issue with list of objects - python

I have a function that gets queryset results from the database and does some manipulations.
When I get the results of that list, somehow the order is changed.
And exactly this is the function that makes the order change: schedules = list(set(schedule_list) - set(excluded_schedules))
So I will explain exactly:
I want to display the availability of a professional for booking an appointment. This professional has a list of available slots.
When the visitor loads the professional profile page, Django makes a query to get all time slots of the professional, and then gets all the existing appointments, then proceeds to remove the booked schedules from the total schedules, to display the rest (the available schedules). So far so good?
So, the code is the following (edited for sensitive information):
def get_home_schedules(date, professional):
day = get_day_by_date(date)
try:
schedules = Schedule.objects.filter(professional=professional, day=day, stype="Home").order_by('timefrom')
excluded_schedules = []
schedule_list = []
for s in schedules:
new_appointments = s.appointments.filter(schedule=s, date=date, status='New')
confirmed_appointments = s.appointments.filter(schedule=s, date=date, status='Confirmed')
appointments = chain(new_appointments,confirmed_appointments)
schedule_list.append(s)
if appointments:
for a in appointments:
excluded_schedules.append(a.schedule)
schedules = list(set(schedule_list) - set(excluded_schedules))
return schedules
except:
return None
The schedule model is:
class Schedule(models.Model):
professional = models.ForeignKey(d, on_delete=models.CASCADE)
timefrom = models.CharField(max_length=5, choices=HOURTIME, default="00:00")
timeto = models.CharField(max_length=5, choices=HOURTIME, default="00:00")
day = models.CharField(max_length=8, choices=DAYS, default="Monday")
stype = models.CharField(max_length=10, choices=APPOINTMENT_TYPE, default='Office')
posted = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Availability"
verbose_name_plural = "Availabilities"
constraints = [
models.UniqueConstraint(fields=['professional', 'timefrom', 'day'], name='unique schedule')
]
def __str__(self):
return '%s (%s - %s - %s)' % (self.professional, self.day, self.timefrom, self.timeto)
The appointment types are: Office, Online, Home
The issue I have is that I have 3 different functions, that get the available schedules, one for each appointment type, and the function works without any problems in the Office and Online types, but has the issue of order in the Home type.
The order might be because of the hour, please tell me if it's true or not (even thought I do not think it's the case because the order doesn't get messed up in the other cases).

When you do
schedules = list(set(schedule_list) - set(excluded_schedules))
the order is lost (since sets are unordered).
You can just re-sort afterwards (and with that, get rid of the .order_by() since it doesn't matter):
schedules = sorted(
set(schedule_list) - set(excluded_schedules),
key=lambda s: s.timefrom,
)
You can also optimize this to exactly 2 queries with
day = get_day_by_date(date)
schedules = Schedule.objects.filter(professional=professional, day=day, stype="Home").order_by('timefrom')
conflicting_appointment_schedule_ids = set(Appointment.objects.filter(
schedule__in=schedules,
status__in=["New", "Confirmed"],
date=date,
).values_list("schedule_id", flat=True))
return [sched for sched in schedules if sched.id not in conflicting_appointment_schedule_ids]
and I'm sure a single query would be possible too.

A python set is an unordered collection of objects, meaning that the order might change when you create the list of schedules like below:
schedules = list(set(schedule_list) - set(excluded_schedules))
You should delete the .order_by('timefrom') when retrieving the initial queryset (as the order will not be guaranteed anyways), and add it in the return statement. For example:
def get_home_schedules(date, professional):
day = get_day_by_date(date)
try:
schedules = Schedule.objects.filter(professional=professional, day=day, stype="Home")
excluded_schedules = []
schedule_list = []
for s in schedules:
new_appointments = s.appointments.filter(schedule=s, date=date, status='New')
confirmed_appointments = s.appointments.filter(schedule=s, date=date, status='Confirmed')
appointments = chain(new_appointments,confirmed_appointments)
schedule_list.append(s.id)
if appointments:
for a in appointments:
excluded_schedules.append(a.schedule.id)
schedule_ids = list(set(schedule_list) - set(excluded_schedules))
return Schedule.objects.filter(id__in=schedule_ids).order_by('timefrom')
except:
return None

Related

Order DateTimeField by rounded time using only the ORM?

I came up with a solution but it's using a model method ( as far as i understand it cannot be used with X.objects.filter() ) and then use the python method sorted. I've read that it's way faster to use django ORM than direct python so I'm searching for a solution. I precise that adding fields to my model is not possible as the database is already well populated.
Basically I've an Articles model :
class Articles(models.Model):
title = models.CharField(max_length=200, null=False, blank=False)
image = models.URLField(null=False, blank=False)
summary = models.TextField()
link = models.URLField(null=False, blank=False)
pub_date = models.DateTimeField(default=timezone.now)
source = models.ForeignKey(
Sources, on_delete=models.CASCADE, related_name="souurce")
category = models.ManyToManyField(Categories)
and what I want to do is ordering result by approximate publication date ( for example an article published at 5:34 and an another one published a 5:31 are both considered published at the same time ), then I can perform other orderings like per category, source or even by title.
Here is my class method to do that (by closest 10 minutes ):
def get_approximate_date(self):
pub_date = self.pub_date
def timeround10(dt):
"""timertound closets 10 minutes"""
a, b = divmod(round(dt.minute, -1), 60)
h = (dt.hour + a) % 24
m = b
new = dt.replace(hour=h, minute=m, second=0, microsecond=0)
return new
return timeround10(pub_date)
Then in my view I can do the following ( I chose to order by approximate date then by reverse alphabetical order ) :
articles_ = Articles.objects.all()
articles_list = sorted(articles_, key=lambda i: (i.get_approximate_date(), i.summary), reverse=True)
The closest thing I came up with using only django ORM is :
Articles.objects.order_by("-pub_date__year","-pub_date__month","-pub_date__day","-summary")
Apart from being ugly it only round the pub date by hour, so 1:59 PM = 1:01PM.
I'm aware of Trunc https://docs.djangoproject.com/en/3.1/ref/models/database-functions/#trunc but it doesn't only implement a way to order by hour minutes etc, maybe i should expand it if it's the only option.
Thanks in advance !

Django: Transactions and how to avoid wrong counting?

I am currently struggling with a topic connected to transactions. I implemented a discount functionality. Whenever a sale is made with a discount code, the counter redeemed_quantity is increased by + 1.
Now I thought about the case. What if one or more users redeem a discount at the same time? Assuming redeemed_quantity is 10. User 1 buys the product and redeemed_quantity increases by +1 = 11. Now User 2 clicked on 'Pay' at the same time and again redeemed_quantity increases by +1 = 11. Even so, it should be 12. I learned about #transaction.atomic but I think the way I implemented them here will not help me with what I am actually trying to prevent. Can anyone help me with that?
view.py
class IndexView(TemplateView):
template_name = 'website/index.html'
initial_price_of_course = 100000 # TODO: Move to settings
def check_discount_and_get_price(self):
discount_code_get = self.request.GET.get('discount')
discount_code = Discount.objects.filter(code=discount_code_get).first()
if discount_code:
discount_available = discount_code.available()
if not discount_available:
messages.add_message(
self.request,
messages.WARNING,
'Discount not available anymore.'
)
if discount_code and discount_available:
return discount_code, self.initial_price_of_course - discount_code.value
else:
return discount_code, self.initial_price_of_course
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['stripe_pub_key'] = settings.STRIPE_PUB_KEY
discount_object, course_price = self.check_discount_and_get_price()
context['course_price'] = course_price
return context
#transaction.atomic
def post(self, request, *args, **kwargs):
stripe.api_key = settings.STRIPE_SECRET_KEY
token = request.POST.get('stripeToken')
email = request.POST.get('stripeEmail')
discount_object, course_price = self.check_discount_and_get_price()
charge = stripe.Charge.create(
amount=course_price,
currency='EUR',
description='My Description',
source=token,
receipt_email=email,
)
if charge.paid:
if discount_object:
discount_object.redeemed_quantity += 1
discount_object.save()
order = Order(
total_gross=course_price,
discount=discount_object
)
order.save()
return redirect('website:index')
models.py
class Discount(TimeStampedModel):
code = models.CharField(max_length=20)
value = models.IntegerField() # Smallest currency unit, as amount charged
max_quantity = models.IntegerField()
redeemed_quantity = models.IntegerField(default=0)
def available(self):
available_quantity = self.max_quantity - self.redeemed_quantity
if available_quantity > 0:
return True
class Order(TimeStampedModel):
total_gross = models.IntegerField()
discount = models.ForeignKey(
Discount,
on_delete=models.PROTECT, # Can't delete discount if used.
related_name='orders',
null=True,
You can pass the handling of the incrementation to the database in order to avoid the race condition in your code by using django's F expression:
from django.db.models import F
# ...
discount_object.redeemed_quantity = F('redeemed_quantity') + 1
discount_object.save()
From the docs with a completely analogous example:
Although reporter.stories_filed = F('stories_filed') + 1 looks like a normal Python assignment of value to an instance attribute, in fact it’s an SQL construct describing an operation on the database.
When Django encounters an instance of F(), it overrides the standard Python operators to create an encapsulated SQL expression; in this case, one which instructs the database to increment the database field represented by reporter.stories_filed.
Django is a piece of a synchronous code. It means that every request you make to the server is processed individually. This problem could arise, when there are multiple server-workers (for example uwsgi workers), but again - it's practically impossible to do this. We run a webshop application with multiple workers and something like this never happend.
But back to the question - if you want to query the database to increase a value by one, see schwobaseggl's answer.
The last thing is that I think you misunderstand what transaction.atomic() does. Simply put it rolls back any queries made to the database in a function if function exits with an error to the state when function was called. See this answer and this piece of documentation. Maybe it will clear some things up.

Django view objects filter with timezone.now().date or timezone.now().time-> expected string or bytes-like object

Hi i have some Django 11 project, my model look like
class Event(models.Model):
name = models.CharField(max_length=100, unique=True)
title = models.CharField(max_length=100)
info = models.CharField(max_length=100)
image = models.ImageField(upload_to='events/%Y/%m/%d')
start_date = models.DateField(default=timezone.now)
start_time = models.TimeField(default=timezone.now)
stop_date = models.DateField(default=timezone.now)
stop_time = models.TimeField(default=timezone.now)
place = models.ForeignKey('places.Place', on_delete=models.CASCADE)
company = models.ForeignKey('companies.Company', on_delete=models.CASCADE)
and my view look like
def place_website(request, place_id):
place_template = get_template('room.html')
place_obj = Place.objects.filter(id=place_id)
# filter for event obejts only for requested place, filtered for now and next events
place_event_now = Event.objects.filter(place=place_id, start_date=timezone.now().date, stop_date__gte=timezone.now().date)
place_events_next = Event.objects.filter(place=place_id, start_date=timezone.now(), stop_date__gte=timezone.now()).order_by('start_time')
place_context = {
'place_obj': place_obj,
'place_event_now': place_event_now,
'place_events_next': place_events_next,
}
return HttpResponse(place_template.render(place_context))
the thing i want to manage is to pass to template the list of filtered Event objects based on time.
Lets pick this line
place_event_now = Event.objects.filter(place=place_id, start_date=timezone.now().date, stop_date__gte=timezone.now().date)
it couse error "expected string or bytes-like object" but when i remove ".date" from "timezone.now()" error disappear (then filter do nothing) but i want to compare date to date and time to time.
How to do this properly ?
This approach to filter objects in view rather than in template is proper?
###### UPDATE ########
Its werid because after correction now i have no error but queryset is not filtered properly, look like only two first parameter is filtering ok and the another two is causing empty queryset.
place_event_now = Event.objects.filter(place=place_id, start_date=timezone.now().strftime('%Y-%m-%d'), start_time__lte=timezone.now().strftime('%H:%M:%S'), stop_time__gte=timezone.now().strftime('%H:%M:%S'))
I my database time is saved in format H:M:S and timezone.now().time() has different format so i modified filter with .strftime this didnt help, what i wont is to limit "place_event_now" queryset to particular object/objects that come true with condition start_time < currenttime < stop_time.
Another case is with place_event_next
place_events_next = Event.objects.filter(place=place_id, start_date=timezone.now().strftime('%Y-%m-%d'), stop_date__gte=timezone.now().strftime('%Y-%m-%d'), start_time__gt=timezone.now().strftime('%H:%M:%S')).order_by('start_time')
Event when i filter objects that start_time is greater than timezone.now() they still are in queryset.
Am I doing something wrong ?
I figured it out that timezone.now() return time not in my current timezone, i change it to timezone.localtime() and everything working perfect !!!
May be you need call date to date()
replace
filter(place=place_id, start_date=timezone.now().date, stop_date__gte=timezone.now().date)
# ^^^ ^^^
to
filter(place=place_id, start_date=timezone.now().date(), stop_date__gte=timezone.now().date())
# ^^^ ^^^

Django filter against ForeignKey and by result of manytomany sub query

I've looked at doing a query using an extra and/or annotate but have not been able to get the result I want.
I want to get a list of Products, which has active licenses and also the total number of available licenses. An active license is defined as being not obsolete, in date, and the number of licenses less the number of assigned licenses (as defined by a count on the manytomany field).
The models I have defined are:
class Vendor(models.Model):
name = models.CharField(max_length=200)
url = models.URLField(blank=True)
class Product(models.Model):
name = models.CharField(max_length=200)
vendor = models.ForeignKey(Vendor)
product_url = models.URLField(blank=True)
is_obsolete = models.BooleanField(default=False, help_text="Is this product obsolete?")
class License(models.Model):
product = models.ForeignKey(Product)
num_licenses = models.IntegerField(default=1, help_text="The number of assignable licenses.")
licensee_name = models.CharField(max_length=200, blank=True)
license_key = models.TextField(blank=True)
license_startdate = models.DateField(default=date.today())
license_enddate = models.DateField(null=True, blank=True)
is_obsolete = models.BooleanField(default=False, help_text="Is this licenses obsolete?")
licensees = models.ManyToManyField(User, blank=True)
I have tried filtering by the License model. Which works, but I don't know how to then collate / GROUP BY / aggregate the returned data into a single queryset that is returned.
When trying to filter by procuct, I can quite figure out the query I need to do. I can get bits and pieces, and have tried using a .extra() select= query to return the number of available licenses (which is all I really need at this point) of which there will be multiple licenses associated with a product.
So, the ultimate answer I am after is, how can I retrieve a list of available products with the number of available licenses in Django. I'd rather not resort to using raw as much as possible.
An example queryset that gets all the License details I want, I just can't get the product:
License.objects.annotate(
used_licenses=Count('licensees')
).extra(
select={
'avail_licenses': 'licenses_license.num_licenses - (SELECT count(*) FROM licenses_license_licensees WHERE licenses_license_licensees.license_id = licenses_license.id)'
}
).filter(
is_obsolete=False,
num_licenses__gt=F('used_licenses')
).exclude(
license_enddate__lte=date.today()
)
Thank you in advance.
EDIT (2014-02-11):
I think I've solved it in possibly an ugly way. I didn't want to make too many DB calls if I can, so I get all the information using a License query, then filter it in Python and return it all from inside a manager class. Maybe an overuse of Dict and list. Anyway, it works, and I can expand it with additional info later on without a huge amount of risk or custom SQL. And it also uses some of the models parameters that I have defined in the model class.
class LicenseManager(models.Manager):
def get_available_products(self):
licenses = self.get_queryset().annotate(
used_licenses=Count('licensees')
).extra(
select={
'avail_licenses': 'licenses_license.num_licenses - (SELECT count(*) FROM licenses_license_licensees WHERE licenses_license_licensees.license_id = licenses_license.id)'
}
).filter(
is_obsolete=False,
num_licenses__gt=F('used_licenses')
).exclude(
license_enddate__lte=date.today()
).prefetch_related('product')
products = {}
for lic in licenses:
if lic.product not in products:
products[lic.product] = lic.product
products[lic.product].avail_licenses = lic.avail_licenses
else:
products[lic.product].avail_licenses += lic.avail_licenses
avail_products = []
for prod in products.values():
if prod.avail_licenses > 0:
avail_products.append(prod)
return avail_products
EDIT (2014-02-12):
Okay, this is the final solution I have decided to go with. Uses Python to filter the results. Reduces cache calls, and has a constant number of SQL queries.
The lesson here is that for something with many levels of filtering, it's best to get as much as needed, and filter in Python when returned.
class ProductManager(models.Manager):
def get_all_available(self, curruser):
"""
Gets all available Products that are available to the current user
"""
q = self.get_queryset().select_related().prefetch_related('license', 'license__licensees').filter(
is_obsolete=False,
license__is_obsolete=False
).exclude(
license__enddate__lte=date.today()
).distinct()
# return a curated list. Need further information first
products = []
for x in q:
x.avail_licenses = 0
x.user_assigned = False
# checks licenses. Does this on the model level as it's cached so as to save SQL queries
for y in x.license.all():
if not y.is_active:
break
x.avail_licenses += y.available_licenses
if curruser in y.licensees.all():
x.user_assigned = True
products.append(x)
return q
One strategy would be to get all the product ids from your License queryset:
productIDList = list(License.objects.filter(...).values_list(
'product_id', flat=True))
and then query the products using that list of ids:
Product.objects.filter(id__in=productIDList)

Django-date incrementation in a list with ManyToManyField

New to django/programming, any help is greatly appreciated. I need help moving through a history of doctor appointments and selecting what immunizations were performed at each appointment, then creating a date when the immunization is due in the future (based on an immunization information table, which has the proper interval of immunizations and will increment from the visit date)
models.py
class Immunizations(models.Model):
immunization = models.CharField(max_length=100, null=True)
interval = models.CharField(max_length=5, null=True)**This should probably be an integer field, will change later
class Visit(models.Model):
patient = models.ForeignKey(Patients)
date_of_visit = models.DateField(null=True)
weight = models.CharField(max_length=5, null=True)
immunization = models.ManyToManyField(Immunizations)
timestamp = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now())
active = models.BooleanField(default=True)
I have been reading the documentation and questions on SO all weekend, but am still very conflicted about what way to go through this.
What I want is:
Visit.date_of_visit1
Visit.immunization1, Visit.date_of_visit1 + Immunization.interval1
Visit.immunization2, Visit.date_of_visit1 + Immunization.interval2
Visit.date_of_visit2
Visit.immunization1, Visit.date_of_visit2 + Immunization.interval1
ETC
This could go on for years with each visit having different immunizations performed. I want to maintain a record of which immunization was performed and record the due date, even if that due date has passed.
views.py
def visit_profile(request, slug):
patient = Patients.objects.get(slug=slug)
try:
visit = Visit.objects.filter(patient_id=patient.id)
except:
return HttpResponseRedirect('/')
#Immunization Due Dates
visitdate = Visit.objects.get(patient_id=patient.id, active=1).date_of_visit
imm = Immunizations.objects.all()
visitimm = []
for immunization in imm:
due = Immunizations.objects.get(id= immunization.pk)
duedate = visitdate + timedelta(days=int(due.interval))
visitimm.append((due, duedate))
return render_to_response('patient.html',locals(), context_instance=RequestContext(request))
Need help with my views.py. The above works, but only at showing the active=1 visit information. I can't figure out how to modify/re-do to achieve what I want and be able to access the data in my template file. I've experimented with __in method, itertools, looping, etc. Can anyone provide the proper method/direction for doing this? I will go back and properly setup error catching once I can get the code to work. Thanks!
Yep, make interval an IntegerField or maybe rather a PositiveSmallIntegerField since it will never get a negative value nor a very huge number.
Careful, better don't mix plural and singular in model names, they affect the related names when you traverse your foreign keys which makes it a pain to debug, see here. I prefer to use only singulars.
Instead of:
visit = Visit.objects.filter(patient_id=patient.id)
You can simply type:
visit = Visit.objects.filter(patient=patient)
Try something like this
def visit_profile(request, slug):
patient = Patients.objects.get(slug=slug)
visitimm = []
# Looping over all active visit records of the patient in date order
for v in patient.visit_set
.filter(active=True).order_by('date_of_visit'):
# Looping over each visit's immunizations
for i in v.immunizations_set.all():
duedate = v.date_of_visit + timedelta(days=int(i.interval))
visitimm.append((i, duedate))
...

Categories

Resources