Pseudo-random ordering in django queryset - django

Suppose I have a QuerySet that returns 10 objects, 3 of which will be displayed in the following positions:
[ display 1 position ] [ display 2 position ] [ display 3 position ]
The model representing it is as follows:
class FeaturedContent(models.Model):
image = models.URLField()
position = models.PositiveSmallIntegerField(blank=True, null=True)
where position can be either 1, 2, 3, or unspecified (Null).
I want to be able to order the QuerySet randomly EXCEPT FOR the objects with a specified position. However, I can't order it by doing:
featured_content = FeaturedContent.objects.order_by('-position', '?')
Because if I had one item that had position = 2, and all the other items were Null, then the item would appear in position 1 instead of position 2.
How would I do this ordering?
Thinking about this, perhaps it would be best to have the data as a dict instead of a list, something like:
`{'1': item or null, '2': item or null, '3': item or null, '?': [list of other items]}`

If you use a db backend that does random ordering efficiently you could do it like this:
# This will hold the result
featured_dict = {}
featured_pos = FeaturedContent.objects.filter(position__isnull=False).order_by('-position')
featured_rand = FeaturedContent.objects.filter(position__isnull=True).order_by('?')
pos_index = 0
rand_index = 0
for pos in range(1, 4):
content = None
if pos_index < len(featured_pos) and featured_pos[pos_index].position == pos:
content = featured_pos[pos_index]
pos_index += 1
elif rand_index < len(featured_rand):
content = featured_rand[rand_index]
rand_index += 1
featured_dict[str(pos)] = content
# I'm not sure if you have to check for a valid index first before slicing
featured_dict['?'] = featured_rand[rand_index:]

If you just want to iterate over the queryset you can have two querysets, order them and chain them.
import itertools
qs1 = FeaturedContent.objects.filter(position__isnull=False).order_by('-position')
qs2 = FeaturedContent.objects.filter(position__isnull=True).order_by('?')
featured_content = itertools.chain(qs1, qs2)
for item in featured_content:
#do something with qs item
print item
Upadate:
Since you are asking to make sure position determines the order and the "blank" spaces are replaced randomly by elements with null positions. If the featured list you want to obtain is not too large, 20 in this case
featured = []
rands = []
for i in xrange(1, 20):
try:
x = FeaturedContent.objects.get(position=i) # assuming position is unique
except FeaturedContentDoesNotExist:
if not rands:
rands = list(FeaturedContent.objects.filter(position__isnull=True).order_by('?')[:20]
x = rands[0]
rands = rands[1:]
featured.append(x)

I would post process it, doing a merge sort between the ordered and unordered records.
EDIT:
The beginnings of a generator for this:
def posgen(posseq, arbseq, posattr='position', startpos=1):
posel = next(posseq)
for cur in itertools.count(startpos):
if getattr(posel, posattr) == cur:
yield posel
posel = next(posseq)
else:
yield next(arbseq)
Note that there are lots of error conditions possible in this code (hint: StopIteration).

Related

Django filter using Q and multiple fields with different values

I am trying to generate a result that satisfies with the filter query below:
indicators = request.GET.getlist('indicators[]')
fmrprofiles = FMRPriority.objects.all()
q_objects = Q()
obj_filters = []
for indicator in indicators:
split_i = indicator.split('_')
if len(split_i) == 5:
if not any(d['indicator'] == split_i[1] for d in obj_filters):
obj_filters.append({
'indicator': split_i[1],
'scores': []
})
for o in obj_filters:
if split_i[1] == o['indicator']:
o['scores'].append(int(split_i[4]))
for obj in obj_filters:
print (obj['scores'])
q_objects.add(Q(pcindicator__id = int(obj['indicator'])) & Q(score__in=obj['scores']), Q.AND)
print (q_objects)
fmrprofiles = fmrprofiles.values('fmr__id','fmr__road_name').filter(q_objects).order_by('-fmr__date_validated')
print (fmrprofiles.query)
Basically, indicators is a list e.g. ['indicator_1_scoring_1_5', 'indicator_1_scoring_1_4', 'indicator_2_scoring_2_5']
I wanted to filter FMRPriority with these following fields:
pcindicator
score
e.g. pcindicator is equal 1 and scores selected are 5,4..another selection pcindicator is equal to 2 and scores selected are 3.
The query q_objects.add(Q(pcindicator__id = int(obj['indicator'])) & Q(score__in=obj['scores']), Q.AND) returns empty set..i have tried also the raw sql, same result.
Model:
class FMRPriority(models.Model):
fmr = models.ForeignKey(FMRProfile, verbose_name=_("FMR Project"), on_delete=models.CASCADE)
pcindicator = models.ForeignKey(PCIndicator, verbose_name=_("Priority Indicator"), on_delete=models.PROTECT)
score = models.FloatField(_("Score"))
I solve this by using OR and count the occurrence of id then exclude those are not equal to the length of filters:
for obj in obj_filters:
print (obj['scores'])
q_objects.add(
(Q(fmrpriority__pcindicator__id = int(obj['indicator'])) & Q(fmrpriority__score__in=obj['scores'])), Q.OR
)
fmrprofiles = fmrprofiles.values(*vals_to_display).filter(q_objects).annotate(
num_ins=Count('id'),
...
)).exclude(
~Q(num_ins = len(obj_filters))
).order_by('rank','road_name')

Counting rows per record

I'm trying count the total number of invoices where record is concurent.
I am unsure about what it is you exactly want. I'm going to base my expectations on your code.
There are a three errors in the code:
Function arguments should a variable name.
def count_consecutive_invoice (df, invoiceNumber):
retlist = []
for i in range(len(df[InvoiceNumber]) - 1):
Notice the removal of quotes around invoiceNumber. You set, what it's equal to later, when calling the function.
You are trying to call the function instead of accessing a variable:
if count_consecutive_invoice[i] + 1 == count_consecutive_invoice[i
+ 1]:
Should be
if df[InvoiceNumber][i] + 1 == df[InvoiceNumber][i + 1]:
You need to declare all variables, including count.
To do this, just add count = 1 after retlist = [].
This code should work:
df = pd.read_excel(r'MYPath\Book1.xlsx')
def count_consecutive_invoice (df, invoiceNumber):
retlist = []
count = 1
for i in range(len(df[ivoiceNumber]) - 1):
# Check if the next number is consecutive
if df[invoiceNumber][i] + 1 == df[invoiceNumber][i+1]:
count += 1
elif count > 1:
# If it is not and count > 1 append the count and restart counting
retlist.append(count)
count = 1
# Since we stopped the loop one early append the last count
retlist.append(count)
return retlist
output = count_consecutive_invoice(df, 'Invoice Number')
print(output)
output:
[4]
Here is my commented solution.
It does recreate a panda frame, you need to pass the rows name for the id and the one on which we count the invoicing.
def count_consecutive_invoice(table, invoice_row_name, id_row_name):
invoiced_table = {} # the output
for row in table:
if row != invoice_row_name:
invoiced_table[row] = []
invoiced_table['Cont of Consecutive Invoices'] = []
streak = False # keep track of streaking invoices cause on first invoice we need to add 2, not 1
for line in range(len(table[invoice_row_name]) - 1):
id = table[id_row_name][line]
if not id in invoiced_table[id_row_name]:
for row in table:
if row != invoice_row_name:
invoiced_table[row].append(table[row][line])
invoiced_table['Cont of Consecutive Invoices'].append(0)
if id == table[id_row_name][line+1]: #check the vendor id so if you get the invoicing for each
if table[invoice_row_name][line]+1 == table[invoice_row_name][line+1]: # check the actual invoicing
itable_line = invoiced_table[id_row_name].index(id)
invoiced_table['Cont of Consecutive Invoices'][itable_line] += 1 + int(not streak) #otherwise we add 1 or 2 depending on the streak status
streak = True
continue
streak = False
return invoiced_table
invoiced = count_consecutive_invoice(df, "Invoice ID", "Vendor ID")
print(pd.DataFrame.from_dict(invoiced))

Django: How to take my query out of my for loop

I got the following code, that contains N queries:
for qty in total_qty_bought:
product_id = qty["product"]
quantity = int(qty["quantity__sum"])
try:
method_title = (
self.shipment_model.get(order_id=qty["order_id"])
.method_title.replace("Hent-selv", "")
.strip()
)
To solve the issue I tried to take the method_title query out of the for loop like this:
quantity = 0
for qty in total_qty_bought:
quantity = int(qty["quantity__sum"])
method_title = (
self.shipment_model.get(order_id=total_qty_bought[0]['order_id'])
.method_title.replace("Hent-selv", "")
.strip()
)
Note! There will be a full refrence further down, to understand the bigger picture
The issue in my solution is, that I am hard choosing which dict to enter , as I select [0] before order_id, and not in a for loop like before, would be selecting every individual item in the loop.
Is there a more sufficient way to do this? I do not see a solution without the for loop, but django debugger tool tells me it creates 2k+ queries.
CODE FOR REFRENCE
class InventoryStatusView(LoginRequiredMixin, View):
template_name = "lager-status.html"
cinnamon_form = CinnamonForm(prefix="cinnamon_form")
peber_form = PeberForm(prefix="peber_form")
pc_model = InventoryStatus
product_model = Product.objects.all()
order_item_model = WCOrderItem.objects.all()
shipment_model = WCOrderShipment.objects.all()
def get(self, request):
# Get all added objects that hasn't been deleted
objects = self.pc_model.objects.filter(is_deleted=False)
# Get all added objects that has been deleted
deleted_objects = self.pc_model.objects.filter(is_deleted=True)
# Sum all cinnamon that isn't deleted
total_cinnamon = (
self.pc_model.objects.filter(is_deleted=False)
.aggregate(Sum("cinnamon"))
.get("cinnamon__sum", 0.00)
)
# Sum all peber that isn't deleted
total_peber = (
self.pc_model.objects.filter(is_deleted=False)
.aggregate(Sum("peber"))
.get("peber__sum", 0.00)
)
# Get the amount of kilo attached to products
product_data = {}
queryset = ProductSpy.objects.select_related('product')
for productSpy in queryset:
product_data[productSpy.product.product_id] = productSpy.kilo
# Get quantity bought of each product
total_qty_bought = self.order_item_model.values(
"order_id", "product"
).annotate(Sum("quantity"))
# Get the cities from the inventory model
cities = dict(self.pc_model.CITIES)
# Set our total dict for later reference
our_total = {}
product = Product.objects.filter(
product_id__in={qty['product'] for qty in total_qty_bought}
).first()
# Check if we deal with kanel or peber as a product based on slug
index = 0
if product.slug.startswith("kanel-"):
index = 0
elif product.slug.startswith("peber-"):
index = 1
else:
pass
try:
# Sum the total quantity bought
quantity = 0
for qty in total_qty_bought:
quantity = int(qty["quantity__sum"])
# Get the inventory the order is picked from based on shipment method title
method_title = (
self.shipment_model.get(order_id=total_qty_bought[0]['order_id']) # The error
.method_title.replace("Hent-selv", "")
.strip()
)
# If the order isn't picked, but sent, use this inventory
if method_title not in cities.values():
method_title = "Hovedlager"
try:
# Get the total of kanel and peber bought
kilos = quantity * product_data[product.id]
# If there is no inventory, set it to 0 peber and 0 kanel
if method_title not in our_total:
our_total[method_title] = [0, 0]
# Combine place and kilos
our_total[method_title][index] += kilos
except KeyError as ex:
print(ex)
pass
except WCOrderShipment.DoesNotExist as ed:
print(ed)
pass
# Quantities BOUGHT! (in orders!)
print(our_total)
context = {
"cinnamon_form": self.cinnamon_form,
"peber_form": self.peber_form,
"objects": objects,
"deleted_objects": deleted_objects,
"total_cinnamon": total_cinnamon,
"total_peber": total_peber,
"our_total": our_total,
}
return render(request, self.template_name, context)
You can only do one query by using the __in operator:
shipments = self.shipment_model.get(order_id__in=list_containing_order_ids)
Then you can do a normal for loop in which you verify that condition.

Django, get objects by multiple values

There is a model
class Fabric(models.Model):
vendor_code = models.CharField(max_length=50)
color = models.CharField(max_length=50)
lot = models.CharField(max_length=50)
I've a list of objects
values = [
{'vendor_code': '123', 'color': 'aodfe', 'lot': 'some lot 1'},
{'vendor_code': '456', 'color': 'adfae', 'lot': 'some lot 2'},
{'vendor_code': '789', 'color': 'dvade', 'lot': 'some lot 3'},
]
There are no ids in dict objects. How can get objects checking for list of field values(for all 3 values per object at same time)?
I know that I can query one by one in loop as:
for item in values:
fabric = Fabric.objects.filter(vendor_code=item['vendor_code'], color=item['color'], lot=item['lot'])
but amount of objects in list can be large. Is there any proper way to get objects at once, if they exists? Or at least to get them with min amount of db hit.
Thanks in advance!
You can use the in (__in) filter like so:
fabrics = Fabric.objects.filter(
vendor_code__in=[value['vendor_code'] for value in values],
color__in=[value['color'] for value in values],
lot__in=[value['lot'] for value in values],
)
This will however iterate the values list 3 times, to only iterate it once use something like this:
vendor_codes = []
colors = []
lots = []
for value in values:
vendor_codes.append(value['vendor_code'])
colors.append(value['color'])
lots.append(value['lot'])
fabrics = Fabric.objects.filter(
vendor_code__in=vendor_codes,
color__in=colors,
lot__in=lots,
)
To filter according to all three values at the same time you will have to use Q objects like this:
q_objects = []
for value in values:
q_objects.append(Q(
vendor_code=value['vendor_code'],
color=value['color'],
lot=value['lot']
)
)
final_q_object = Q()
for q_object in q_objects:
final_q_object.add(q_object, Q.OR)
fabrics = Fabric.objects.filter(final_q_object)
The gist of it is to get this query:
Q(Q(a=i, b=j, c=k)) | Q(Q(a=l, b=m, c=n) | ...)
Final answer after a bit of optimization:
final_query = Q()
for item in values:
final_query.add(
Q(
vendor_code=value['vendor_code'],
color=value['color'],
lot=value['lot']
),
Q.OR
)
fabrics = Fabric.objects.filter(final_query)
You could use the field lookup "in".
# get all the vendor codes in a list
vendor_code_list = []
for v in values:
vendor_code_list.append(v['vendor_code'])
# query all the fabrics
fabrics = Fabric.objects.filter(vendor_code__in=vendor_code_list)
If you want to match exact values and you are sure that your item keys are valid field names you can just:
for item in values:
fabric = Fabric.objects.filter(**item)
otherwise if you want check if your items are contained inside existing items you can:
for item in values:
item_in = {'{}__in'.format(key):val for key, val in item.items()}
fabric = Fabric.objects.filter(**item_in)

Getting next and previous objects in Django

I'm trying to get the next and previous objects of a comic book issue. Simply changing the id number or filtering through date added is not going to work because I don't add the issues sequentially.
This is how my views are setup and it WORKS for prev_issue and does return the previous object, but it returns the last object for next_issue and I do not know why.
def issue(request, issue_id):
issue = get_object_or_404(Issue, pk=issue_id)
title = Title.objects.filter(issue=issue)
prev_issue = Issue.objects.filter(title=title).filter(number__lt=issue.number)[0:1]
next_issue = Issue.objects.filter(title=title).filter(number__gt=issue.number)[0:1]
Add an order_by clause to ensure it orders by number.
next_issue = Issue.objects.filter(title=title, number__gt=issue.number).order_by('number').first()
I know this is a bit late, but for anyone else, django does have a nicer way to do this, see https://docs.djangoproject.com/en/1.7/ref/models/instances/#django.db.models.Model.get_previous_by_FOO
So the answer here would be something something like
next_issue = Issue.get_next_by_number(issue, title=title)
Django managers to do that with a bit of meta class cleaverness.
If it's required to find next and previous objects ordered by field values that can be equal and those fields are not of Date* type, the query gets slightly complex, because:
ordering on objects with same values limiting by [:1] will always produce same result for several objects;
object can itself be included in resulting set.
Here's are querysets that also take into account the primary keys to produce a correct result (assuming that number parameter from OP is not unique and omitting the title parameter as it's irrelevant for the example):
Previous:
prev_issue = (Issue.objects
.filter(number__lte=issue.number, id__lt=instance.id)
.exclude(id=issue.id)
.order_by('-number', '-id')
.first())
Next:
next_issue = (Issue.objects
.filter(number__gte=issue.number, id__gt=instance.id)
.exclude(id=issue.id)
.order_by('number', 'id')
.first())
from functools import partial, reduce
from django.db import models
def next_or_prev_instance(instance, qs=None, prev=False, loop=False):
if not qs:
qs = instance.__class__.objects.all()
if prev:
qs = qs.reverse()
lookup = 'lt'
else:
lookup = 'gt'
q_list = []
prev_fields = []
if qs.query.extra_order_by:
ordering = qs.query.extra_order_by
elif qs.query.order_by:
ordering = qs.query.order_by
elif qs.query.get_meta().ordering:
ordering = qs.query.get_meta().ordering
else:
ordering = []
ordering = list(ordering)
if 'pk' not in ordering and '-pk' not in ordering:
ordering.append('pk')
qs = qs.order_by(*ordering)
for field in ordering:
if field[0] == '-':
this_lookup = (lookup == 'gt' and 'lt' or 'gt')
field = field[1:]
else:
this_lookup = lookup
q_kwargs = dict([(f, get_model_attr(instance, f))
for f in prev_fields])
key = "%s__%s" % (field, this_lookup)
q_kwargs[key] = get_model_attr(instance, field)
q_list.append(models.Q(**q_kwargs))
prev_fields.append(field)
try:
return qs.filter(reduce(models.Q.__or__, q_list))[0]
except IndexError:
length = qs.count()
if loop and length > 1:
return qs[0]
return None
next_instance = partial(next_or_prev_instance, prev=False)
prev_instance = partial(next_or_prev_instance, prev=True)
note that do not use object.get(pk=object.pk + 1) these sorts of things, IntegrityError occurs if object at that pk is deleted, hence always use a query set
for visitors:
''' Useage '''
"""
# Declare our item
store = Store.objects.get(pk=pk)
# Define our models
stores = Store.objects.all()
# Ask for the next item
new_store = get_next_or_prev(stores, store, 'next')
# If there is a next item
if new_store:
# Replace our item with the next one
store = new_store
"""
''' Function '''
def get_next_or_prev(models, item, direction):
'''
Returns the next or previous item of
a query-set for 'item'.
'models' is a query-set containing all
items of which 'item' is a part of.
direction is 'next' or 'prev'
'''
getit = False
if direction == 'prev':
models = models.reverse()
for m in models:
if getit:
return m
if item == m:
getit = True
if getit:
# This would happen when the last
# item made getit True
return models[0]
return False
original author
Usage
# you MUST call order by to pass in an order, otherwise QuerySet.reverse will not work
qs = Model.objects.all().order_by('pk')
q = qs[0]
prev = get_next_or_prev(qs, q, 'prev')
next = get_next_or_prev(qs, q, 'next')
next_obj_id = int(current_obj_id) + 1
next_obj = Model.objects.filter(id=next_obj_id).first()
prev_obj_id= int(current_obj_id) - 1
prev_obj = Model.objects.filter(id=prev_obj_id).first()
#You have nothing to loose here... This works for me