Django selecting a count - django

I'm fairly new to Django and I want to get the total clients a user has (my users will be selling something through my website) so I have a table called orders where I keep the user_id of the user who purchased and the product_id being purchased. Each product id is related to a product that has a user (the one I'm doing the query for):
select COUNT(distinct(o.user_id)) as total_clients from `order` o
inner join product p on p.id=o.product_id where p.user_id=32;
User id 32 is logged in and I want to show him how many clients purchased his products.
I want to do this in a get instead of filter as it makes more sense.
Here's what my logic tells me to write:
clients = Order.objects.get(
status = Order.COMPLETED,
product__user = self.user
).annotate(
total_clients = Count( 'user', distinct = True )
)
return clients.total_clients
.and here's what it returns:
Exception Type: MultipleObjectsReturned
Exception Value: get() returned more than one Order -- it returned 2!
I could probably be running a query instead of using the orm but I don't want that. This is a rather simple query that I'm sure Django is handling very easily and I want to keep away from writing strings in my code.
Here's my model setup:
class UserProfile( models.Model ):
user = models.OneToOneField( User, related_name = 'profile' )
....
def get_total_clients( self ):
clients = Order.objects.get(
status = Order.COMPLETED,
product__user = self.user
).annotate(
total_clients = Count( 'user', distinct = True )
)
return clients.total_clients
class Order( models.Model ):
PENDING = 0
COMPLETED = 1
REFUNDED = 2
STATUS_CHOICES = (
(PENDING, "Pending"),
(COMPLETED, "Completed"),
(REFUNDED, "Refunded"),
)
user = models.ForeignKey( User, related_name = "orders" )
product = models.ForeignKey( Product, related_name = "orders" )
amount = models.DecimalField( max_digits = 6, decimal_places = 2, default = 0.00 )
status = models.SmallIntegerField(
choices = STATUS_CHOICES, default = PENDING, db_index = True
)
class Product( models.Model ):
user = models.ForeignKey( User, related_name = "unlocks" )
name = models.CharField( max_length = 255 )

Django queryset have a count method:
clients = Order.objects.filter(
status = Order.COMPLETED,
product__user = self.user
)
return clients.count()

If i got you right, you are intrested in the amount of consumers who ordered a product from a user. Some docs, that may be helpful.
My suggestion is:
result = Product.objects.distinct().filter(
# or any query:
orders__status=Order.COMPLETED,
user=default_user
).aggregate( # see the difference between aggregate() and annotate()
total_users=Count('orders__user', distinct=True)
)
I expect the result to be: {'total_users': NUM_OF_USERS}
In raw SQL it will be something like:
SELECT DISTINCT COUNT(DISTINCT "main_order"."user_id") AS
"total_users" FROM "main_product" INNER JOIN "main_order" ON (
"main_product"."id" = "main_order"."product_id" ) WHERE
("main_order"."status" = STATUS AND "main_product"."user_id" = USER_ID)
Is that what you wanted?

Related

django bulk_create with Null value

models.py
class control(models.Model):
amount = models.IntegerField()
driver = models.ForeignKey(driver, on_delete=models.CASCADE, null=True, blank=True)
views.py
controlValues = [
(1,1,1),
(2,8,None)
]
control.objects.bulk_create([
control(
id = i[0],
amount = i[1],
driver = driver(id = i[2])
) for i in controlValues], ignore_conflicts=True
)
I got error:
bulk_create() prohibited to prevent data loss due to unsaved related object 'driver'.
How can I set Null for driver? I'm using mysql.
If the value is None, you should not construct a driver model with that object, but thus work with:
control.objects.bulk_create([
control(
id=id,
amount=amount,
driver=driver(id=dr_id) if dr_id is not None else None
) for id, amount, dr_id in controlValues],
ignore_conflicts=True
)
or a lot simpler:
control.objects.bulk_create([
control(
id=id,
amount=amount,
driver_id=dr_id
) for id, amount, dr_id in controlValues],
ignore_conflicts=True
)
Note: Models in Django are written in PascalCase, not snake_case,
so you might want to rename the model from driver to Driver.

Django Annotation Count with Subquery & OuterRef

I'm trying to create a high score statistic table/list for a quiz, where the table/list is supposed to be showing the percentage of (or total) correct guesses on a person which was to be guessed on. To elaborate further, these are the models which are used.
The Quiz model:
class Quiz(models.Model):
participants = models.ManyToManyField(
User,
through="Participant",
through_fields=("quiz", "correct_user"),
blank=True,
related_name="related_quiz",
)
fake_users = models.ManyToManyField(User, related_name="quiz_fakes")
user_quizzed = models.ForeignKey(
User, related_name="user_taking_quiz", on_delete=models.CASCADE, null=True
)
time_started = models.DateTimeField(default=timezone.now)
time_end = models.DateTimeField(blank=True, null=True)
final_score = models.IntegerField(blank=True, default=0)
This model does also have some properties; I deem them to be unrelated to the problem at hand.
The Participant model:
class Participant(models.Model): # QuizAnswer FK -> QUIZ
guessed_user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="clicked_in_quiz", null=True
)
correct_user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="solution_in_quiz", null=True
)
quiz = models.ForeignKey(
Quiz, on_delete=models.CASCADE, related_name="participants_in_quiz"
)
#property
def correct(self):
return self.guessed_user == self.correct_user
To iterate through what I am trying to do, I'll try to explain how I'm thinking this should work:
For a User in User.objects.all(), find all participant objects where the user.id equals correct_user(from participant model)
For each participantobject, evaluate if correct_user==guessed_user
Sum each participant object where the above comparison is True for the User, represented by a field sum_of_correct_guesses
Return a queryset including all users with parameters [User, sum_of_correct_guesses]
^Now ideally this should be percentage_of_correct_guesses, but that is an afterthought which should be easy enough to change by doing sum_of_correct_guesses / sum n times of that person being a guess.
Now I've even made some pseudocode for a single person to illustrate to myself roughly how it should work using python arithmetics
# PYTHON PSEUDO QUERY ---------------------
person = get_object_or_404(User, pk=3) # Example-person
y = Participant.objects.filter(
correct_user=person
) # Find participant-objects where person is used as guess
y_corr = [] # empty list to act as "queryset" in for-loop
for el in y: # for each participant object
if el.correct: # if correct_user == guessed_user
y_corr.append(el) # add to queryset
y_percentage_corr = len(y_corr) / len(y) # do arithmetic division
print("Percentage correct: ", y_percentage_corr) # debug-display
# ---------------------------------------------
What I've tried (with no success so far), is to use an ExtensionWrapper with Count() and Q object:
percentage_correct_guesses = ExpressionWrapper(
Count("pk", filter=Q(clicked_in_quiz=F("id")), distinct=True)
/ Count("solution_in_quiz"),
output_field=fields.DecimalField())
all_users = (
User.objects.all().annotate(score=percentage_correct_guesses).order_by("score"))
Any help or directions to resources on how to do this is greatly appreciated :))
I found an answer while looking around for related problems:
Django 1.11 Annotating a Subquery Aggregate
What I've done is:
Create a filter with an OuterRef() which points to a User and checks if Useris the same as correct_person and also a comparison between guessed_person and correct_person, outputs a value correct_user in a queryset for all elements which the filter accepts.
Do an annotated count for how many occurrences there are of a correct_user in the filtered queryset.
Annotate User based on the annotated-count, this is the annotation that really drives the whole operation. Notice how OuterRef() and Subquery are used to tell the filter which user is supposed to be correct_user.
Below is the code snippet which I made it work with, it looks very similar to the answer-post in the above linked question:
from django.db.models import Count, OuterRef, Subquery, F, Q
crit1 = Q(correct_user=OuterRef('pk'))
crit2 = Q(correct_user=F('guessed_user'))
compare_participants = Participant.objects.filter(crit1 & crit2).order_by().values('correct_user')
count_occurrences = compare_participants.annotate(c=Count('*')).values('c')
most_correctly_guessed_on = (
User.objects.annotate(correct_clicks=Subquery(count_occurrences))
.values('first_name', 'correct_clicks')
.order_by('-correct_clicks')
)
return most_correctly_guessed_on
This works wonderfully, thanks to Oli.

Django get_FOO_Display() for range list choices

STATUS_APPROVE, STATUS_REJECT, STATUS_PROC, STATUS_PEND, STATUS_PAID = range(5)
STATUS_CHOICES = (
(STATUS_APPROVE, 'Approved'),
(STATUS_REJECT, 'Rejected'),
(STATUS_PROC, 'Processing'),
(STATUS_PEND, 'Pending'),
(STATUS_PAID, 'Paid'),
)
class ExpenseClaim(TimeStampedModel):
status = models.PositiveIntegerField(default=STATUS_PEND,
choices=STATUS_CHOICES, blank=False)
class MileageClaimSerializer(serializers.ModelSerializer):
status = serializers.ChoiceField(choices=STATUS_CHOICES)
I cannot work out a way to get foo display for a choice list with range. So the range keys are used to store the point in list. I need the serializer to return the word and not the key.
if this serializer is readonly you can use the source attribute inside a CharField
status = serializers.CharField(source='get_status_display')
https://www.django-rest-framework.org/api-guide/fields/#source

The best way to do an efficient filter query in django

models.py file
I am not so good at this aspect in Django. Please can someone help me? I wish to know if there is a more efficient way for the class method already_voted
class Vote(TimeStamped):
voter = models.ForeignKey(get_user_model(), verbose_name=_("Vote"), on_delete=models.CASCADE)
contester = models.ForeignKey(Contester, verbose_name=_("Contester"), on_delete=models.CASCADE,
help_text=("The chosen contester"), related_name="votes")
ip_address = models.GenericIPAddressField(
_("Voter's IP"),
protocol="both",
unpack_ipv4=False,
default="None",
unique=True
)
num_vote = models.PositiveIntegerField(_("Vote"), default=0)
class Meta:
unique_together = ('voter','contester')
verbose_name = _("Vote")
verbose_name_plural = _("Votes")
permissions = (
("vote_multiple_times", "can vote multiple times"),
)
....
....
#classmethod
def already_voted(cls, contester_id, voter_id=None, ip_addr=None):
return cls.objects.filter(contester_id=contester_id).exists() and \
(cls.objects.filter(ip_address=ip_addr).exists() or \
cls.objects.filter(voter_id=voter_id).exists())
The class method may be right, but your model needs one more index:
contester = models.ForeignKey( db_index= True #... )
Notice that:
voter doesn't need index because is on first place on unique_together constraint.
contester needs index because, despite it is on unique_together, doesn't is place on first position of the constraint.
ip_address doesn't need index because has unique constraint.
Also:
unique_together is deprecated and should be a list of tuples (not just a tuple)
Edited
Edited 5 feb 2021 due to OP comment
You can get results in just one hit using Exists but it is less readable, also, I'm not sure if it is more efficient or the best way:
from django.db.models import Exists
q_ip=Vote.objects.filter(ip_address="1")
q_voter=Vote.objects.filter(voter=2)
already_voted=(
Vote
.objects
.filter(contester=3)
.filter(Exists(q_ip)|Exists(q_voter))
.exists())
The underlying sql, you can see this is just one query:
SELECT ( 1 ) AS "a"
FROM "a1_vote"
WHERE ( "a1_vote"."contester" = 3
AND ( EXISTS(SELECT U0."id",
U0."voter",
U0."contester",
U0."ip_address",
U0."num_vote"
FROM "a1_vote" U0
WHERE U0."ip_address" = '1')
OR EXISTS(SELECT U0."id",
U0."voter",
U0."contester",
U0."ip_address",
U0."num_vote"
FROM "a1_vote" U0
WHERE U0."voter" = 2) ) )
LIMIT 1

Filtering X most recent entries in each category of queryset

Question is regarding filtering X most recent entries in each category of queryset.
Goal is like this:
I have a incoming queryset based on the following model.
class UserStatusChoices(models.TextChoices):
CREATOR = 'CREATOR'
SLAVE = 'SLAVE'
MASTER = 'MASTER'
FRIEND = 'FRIEND'
ADMIN = 'ADMIN'
LEGACY = 'LEGACY'
class OperationTypeChoices(models.TextChoices):
CREATE = 'CREATE'
UPDATE = 'UPDATE'
DELETE = 'DELETE'
class EntriesChangeLog(models.Model):
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
)
object_id = models.PositiveIntegerField(
)
content_object = GenericForeignKey(
'content_type',
'object_id',
)
user = models.ForeignKey(
get_user_model(),
verbose_name='user',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='access_logs',
)
access_time = models.DateTimeField(
verbose_name='access_time',
auto_now_add=True,
)
as_who = models.CharField(
verbose_name='Status of the accessed user.',
choices=UserStatusChoices.choices,
max_length=7,
)
operation_type = models.CharField(
verbose_name='Type of the access operation.',
choices=OperationTypeChoices.choices,
max_length=6,
)
And I need to filter this incoming queryset in a such way to keep only 4 most recent objects (defined by access_time field) in each category. Categories are defined by ‘content_type_id’ field and there are 3 possible options.
Lets call it ‘option1’, ‘option2’ and ‘option3’
This incoming queryset might contain different amount of objects of 1,2 or all 3 categories. This is can’t be predicted beforehand.
DISTINCT is not possible to use as after filtering operation this queryset might be ordered.
I managed to get 1 most recent object in a following way:
# get one most recent operation in each category
last_operation_time = Subquery(
EntriesChangeLog.objects.filter(user=OuterRef('user')).values('content_type_id').
annotate(last_access_time=Max(‘access_time’)).values_list('last_access_time', flat=True)
)
queryset.filter(access_time__in=last_operation_time)
But I have a hard time to figure out how to get last 4 most recent objects instead of last one.
This is needed for Django-Filter and need to be done in one query.
DB-Postgres 12
Do you have any ideas how to do such filtration?
Thanks...
pk_to_rank = queryset.annotate(rank=Window(
expression=DenseRank(),
partition_by=('content_type_id',),
order_by=F('access_time').desc(),
)).values_list('pk', 'rank', named=True)
pks_list = sorted(log.pk for log in pk_to_rank if log.rank <= value)
return queryset.filter(pk__in=pks_list)
Managed to do it only this way by spliting queryset in 2 parts. Option with 3 unions is also possible but what if we have 800 options instead 3 - make 800 unions()??? ges not...