Scenario: Showing all voucher that a user can apply.
I have 2 tables Voucher (with all information of a voucher) and VoucherCustomer (listing number of vouchers that a user has used)
A validation voucher that can show to user on the application should be
Within use-able duration
Do not exceed number of times used within a day
Do not exceed number of times used per user
Do not exceed number of times used for a voucher
Must active
Here is my model:
class Voucher(models.Model):
code = models.ForeignKey('VoucherCustomer', related_name= 'voucher_code', on_delete = models.CASCADE) (1)
start_at = models.DateTimeField() (2)
end_at = models.DateTimeField() (3)
usage_limit_per_customer = models.BigIntegerField() (4)
times_used = models.BigIntegerField() (5)
usage_limit_daily = models.BigIntegerField() (6)
times_used_daily = models.BigIntegerField() (7)
is_global = models.BooleanField(blank=True, null=True) (8)
is_active = models.BooleanField() (9)
class VoucherCustomer(models.Model):
voucher_code = models.CharField(max_length = 255, primary_key=True) (1)
customer_id = models.IntegerField() (2)
times_used = models.BigIntegerField(blank=True, null=True) (3)
created_at = models.DateTimeField(blank=True, null=True) (4)
updated_at = models.DateTimeField(blank=True, null=True) (5)
Here is the sample data:
+++++++ Voucher ++++++++
(1) (2) (3) (4) (5) (6) (7) (8) (9)
TEST01 | 2020-11-30 17:00:00 | 2021-03-01 16:59:59 | 100 | 1124 | 5000 | 6 | true | true
+++++++ VoucherCustomer ++++++++
(1) (2) (3) (4) (5)
TEST01 10878 9 2020-12-03 02:17:32.012722 2020-12-08 10:32:03.877349
TEST01 12577 1 2020-12-02 07:17:34.005964 2020-12-02 07:17:34.005964
TEST01 8324 18 2020-12-02 07:49:37.385682 2021-02-01 14:35:38.096381
TEST01 7638 2 2020-12-02 08:17:46.532566 2020-12-02 08:17:46.532566
TEST01 3589 1 2020-12-02 14:57:01.356616 2020-12-02 14:57:01.356616
My expected query:
SELECT v.*
FROM leadtime.voucher v
LEFT JOIN leadtime.voucher_customer vc ON v.code = vc.voucher_code AND vc.customer_id in ({input_customer_id})
WHERE 1=1
AND v.start_at <= CURRENT_TIMESTAMP
AND v.end_at >= CURRENT_TIMESTAMP
AND v.is_active = TRUE
AND v.times_used < v.usage_limit
AND v.times_used_daily < v.usage_limit_daily
AND (v.customer_id = {input_customer_id} OR v.is_global)
AND COALESCE(vc.times_used,0) < usage_limit_per_customer
ORDER BY created_at
Here is my code on Django:
from django.db.models import Q, F, Value
from django.db.models.functions import Coalesce
now = datetime.now()
customer_input = customer_id = request.GET.get('customer_id')
query = Voucher.objects.filter(
start_at__lte = now,
end_at__gte = now,
is_active = True,
times_used__lt = F('usage_limit'),
times_used_daily__lt = F('usage_limit_daily'),
Q(customer_id = customer_id_input ) | Q(is_global = True),
VoucherCustomer__customer_id = customer_id_input ,
Coalesce(VoucherCustomer__times_used, Value(0)) <= F('usage_limit_per_customer')
).order_by('created_at').values()
Obviously, it does not work.
I got this error:
File "/code/app_test/views.py", line 467
).order_by('created_at').values()
^
SyntaxError: positional argument follows keyword argument
Anyway, since I am just a beginner, if there are parts that I could improve in my code please feel free to tell me.
------------ Updated ----------------
The current code is not working as I received this err
Cannot resolve keyword 'VoucherCustomer' into field. Choices are: airport, arrival_airport, code, code_id, content, created_at, customer_id, delivery_type, departure_airport, description, discount_amount, discount_type, end_at, from_time, id, is_active, is_global, max_discount_amount, start_at, times_used, times_used_daily, tittle, to_time, updated_at, usage_limit, usage_limit_daily, usage_limit_per_customer
I tried to change the model of VoucherCustomer into this one but still not working.
class VoucherCustomer(models.Model):
voucher_code = models.ManyToOneRel(field = "voucher_code", field_name = "voucher_code", to = "")
......................
class Meta:
managed = False
db_table = 'voucher_customer'
The code is not syntetically correct, you can't use <= inside a method signature, ie use that in filter(), also you need to pass the arguments before passing the keyword arguments through the function, ie some_function(a, b, x=y).
But, you can use Coalesce to annotate the value with Voucher queryset then run the filter again, like this:
query = Voucher.objects.filter(
Q(customer_id = customer_id_input ) | Q(is_global = True),
start_at__lte = now,
end_at__gte = now,
is_active = True,
times_used__lt = F('usage_limit'),
times_used_daily__lt = F('usage_limit_daily'),
code__customer_id = customer_id_input
).annotate(
usage_so_far=Coalesce('code__times_used', Value(0))
).filter(
usage_so_far__gte=F('usage_limit_per_customer')
).order_by('created_at').values()
Related
Models:
class Regions(models.Model):
name = models.CharField(max_length=255, unique=True)
class Owners(models.Model):
name = models.CharField(max_length=255, null=False, unique=True)
url = models.URLField(null=True)
class Lands(models.Model):
region = models.ForeignKey(Regions, on_delete=models.CASCADE)
owner = models.ForeignKey(Owners, on_delete=models.PROTECT, null=True)
description = models.TextField(max_length=2000, null=True)
class LandChangeHistory(models.Model):
land = models.ForeignKey(Lands, on_delete=models.CASCADE, null=False, related_name='lands')
price = models.IntegerField()
size = models.IntegerField()
date_added = models.DateField(auto_now_add=True)
Queryset that works but i need it to be annotated in another queryset somehow:
lands_in_region = Lands.objects.values('region__name').annotate(count=Count('region_id'))
returns for example:
{'region__name': 'New York', 'count': 3}, {'region__name':
'Chicago', 'count': 2}
In the 2nd queryset i need count of lands available in region. But instead of real count, i always get count = 1. How to combine them? Im pretty sure i could do it in raw sql by joining two tables on field "region__id", but dont know how to do it in django orm.
f = LandFilter(request.GET, queryset=LandChangeHistory.objects.all()
.select_related('land', 'land__region', 'land__owner')
.annotate(usd_per_size=ExpressionWrapper(F('price') * 1.0 / F('size'), output_field=FloatField(max_length=3)))
.annotate(count=Count('land__region_id'))
)
For example. If it returns:
land1 | 100$ | 100m2 | New York
land2 | 105$ | 105m2 | New York
land3 | 102$ | 102m2 | Chicago
i need 1 more column, that counts for each land how many NewYork's and Chicago's are there
land1 | 100$ | 100m2 | New York | 2
land2 | 105$ | 105m2 | New York | 2
land3 | 102$ | 102m2 | Chicago | 1
This worked for me. Hope helps somebody.
First i tried to simply call Count() method right after filter, but that breaks query, since it tries to get data from DB immediately. But that was the correct way to think, so i added count annotate and selected it and it worked.
f = LandFilter(request.GET, queryset=LandChangeHistory.objects.all()
.select_related('land', 'land__region', 'land__owner')
.annotate(usd_per_size=ExpressionWrapper(F('price') * 1.0 / F('size'), output_field=FloatField(max_length=3)))
.annotate(count=Subquery(
Lands.objects.filter(region_id=OuterRef('land__region_id'))
.values('region_id')
.annotate(count=Count('pk'))
.values('count')))
)
I am working on a payment system that is registering transactions and timestamps. I would like to make a test to ensure that transactions are only made on a past date - it should not be possible to have a transaction with a future date.
models.py
20 class Ledger(models.Model):
19 account = models.ForeignKey(Account, on_delete=models.PROTECT)
18 transaction = models.ForeignKey(UID, on_delete=models.PROTECT)
17 amount = models.DecimalField(max_digits=10, decimal_places=2)
16 timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
15 text = models.TextField()
14
13 #classmethod
12 def transfer(cls, amount, debit_account, debit_text, credit_account, credit_text, is_loan=False) -> int:
11 assert amount >= 0, 'Negative amount not allowed for transfer.'
10 with transaction.atomic():
9 if debit_account.balance >= amount or is_loan:
8 uid = UID.uid
7 cls(amount=-amount, transaction=uid, account=debit_account, text=debit_text).save()
6 cls(amount=amount, transaction=uid, account=credit_account, text=credit_text).save()
5 else:
4 raise InsufficientFunds
3 return uid
2
1 def __str__(self):
122 return f'{self.amount} :: {self.transaction} :: {self.timestamp} :: {self.account} :: {self. text}'
I honestly don't even know where to start testing this class because Testing is very new to me. I have tested some things I saw online close to the below, but cannot see anything happening. Does it make any sense? Maybe there is something that makes more sense testing here instead... Suggestions are more than welcome!
tests.py
class LedgerModelTestCase(TestCase):
# def setUp(self):
1 # no data to test yet
2 def transfer_completed(self):
3 time = timezone.now() + datetime.timedelta(days=30)
4 print(time)
5 future_transfer = Ledger(timestamp=time)
6 print(future_transfer)
7 self.assertIs(future_transfer.was_published_recently(), False)
8
I'm working with django and mysql.
Say there's a table as below:
class OrderInfo(models.Model):
gg_account_id = models.CharField(max_length=45, blank=True, null=True)
order_status = models.IntegerField(blank=True, null=True)
gg_status = models.IntegerField(blank=True, null=True)
uid = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'order_info'
And data saved in it are:
id gg_account_id order_status gg_status uid
1 6270491342 2 0 1
2 12321323 2 0 34
3 12321323 2 0 34
4 55551233 1 0 54
5 55551233 2 0 54
6 55551233 2 0 54
7 55551233 2 0 54
If there are more than one data with same gg_account_id I want to get only one of them. My expected output should be like :
1 6270491342 1
2 12321323 34
5 55551233 54
and here's my trial with orm query:
recharge_account_list = OrderInfo.objects.\
filter(order_status=2, gg_status=0).\
distinct("gg_account_id").\
values_list("gg_account_id", "uid", "id")
print(recharge_account_list)
But I got error always
File "D:\virtual\Envs\smb_middle_server\lib\site-packages\django\db\backends\base\operations.py", line 171, in distinct
_sql
raise NotSupportedError('DISTINCT ON fields is not supported by this database backend')
django.db.utils.NotSupportedError: DISTINCT ON fields is not supported by this database backend
How can I get expected result?
Thanks
Solution 1: use forloop
gg_account_ids = set()
recharge_accounts = []
for i in recharge_account_list:
if i[0] not in gg_account_ids:
gg_account_ids.add(i[0])
recharge_accounts.append(i)
recharge_account_list = recharge_accounts
Solution 2: use raw SQL
recharge_account_list = OrderInfo.objects.raw('SELECT ... FROM Order_info GROUP BY ...')
It can be observed that you are using mysql as a database.
However, MySQL does not support distinct on the field. I mean by distinct('field_name') it only supports generic distinct So you can do only distinct() operation but not on specific fields.
distinct documentation Django
Similar question
Furthermore, you can achieve that by using group by specific fields.
Distinct with groupby
I have a table with cryptocurrency prices:
id | price | pair_id | exchange_id | date
---+--------+---------+-------------+---------------------------
1 | 8232.7 | 1 | 1 | 2018-02-09 09:31:00.160837
2 | 8523.8 | 1 | 2 | 2018-02-09 09:31:01.353998
3 | 240.45 | 2 | 1 | 2018-02-09 09:31:02.524333
I want to get the latest prices of a single pair from different exchanges. In raw SQL, I do it like this:
SELECT b.price, b.date, k.price, k.date, AVG((b.price + k.price) / 2)
FROM converter_price b JOIN converter_price k
WHERE b.exchange_id=1 AND k.exchange_id=2 AND b.pair_id=1 AND k.pair_id=1
ORDER BY b.date DESC, k.date DESC LIMIT 1;
8320.1|2018-02-09 11:23:00.369810|8318.2|2018-02-09 11:23:06.467424|8245.05199328066
How to do such query in the Django ORM?
EDIT: Adding models.py. I understand that it's quite likely that I'll need to update it.
from django.db import models
# Create your models here.
class Pair(models.Model):
identifier = models.CharField('Pair identifier', max_length=6)
def __str__(self):
return self.identifier
class Exchange(models.Model):
name = models.CharField('Exchange name', max_length=20)
def __str__(self):
return self.name
class Price(models.Model):
pair = models.ForeignKey(Pair, on_delete=models.CASCADE)
exchange = models.ForeignKey(Exchange, on_delete=models.CASCADE)
price = models.FloatField(default=0)
date = models.DateTimeField(auto_now=True, db_index=True)
def __str__(self):
return '{} - {}: {} ({})'.format(
self.date, self.pair, self.price, self.exchange)
EDIT2: To clarify what I'm really after.
I want the latest price with pair_id=1 and exchange_id=1, and the latest price with pair_id=1 and exchange_id=2. With a single query, and without any subsequent processing in Python - of course I can get Price.objects.all() and then search for it myself, but that's not the right way to use the ORM.
The way to do this with raw SQL is joining the table to itself. Showing how to join a table to itself using the Django ORM would (probably) answer the question.
I want to create an error message for following form:
class ExaminationCreateForm(forms.ModelForm):
class Meta:
model = Examination
fields = ['patient', 'number_of_examination', 'date_of_examination']
Models:
class Patient(models.Model):
patientID = models.CharField(max_length=200, unique=True, help_text='Insert PatientID')
birth_date = models.DateField(auto_now=False, auto_now_add=False, help_text='YYYY-MM-DD')
gender = models.CharField(max_length=200,choices=Gender_Choice, default='UNDEFINED')
class Examination(models.Model):
number_of_examination = models.IntegerField(choices=EXA_Choices)
patient = models.ForeignKey(Patient, on_delete=models.CASCADE)
date_of_examination = models.DateField(auto_now=False, auto_now_add=False, help_text='YYYY-MM-DD')
Every Patient has 2 Examinations (number of examination = Choices 1 or 2) and the error message should be activated when the date of the second examination < date of the first examination. Something like this:
Solution: `
def clean_date_of_examination(self):
new_exam = self.cleaned_data.get('date_of_examination')
try:
old_exam = Examination.objects.get(patient=self.cleaned_data.get('patient'))
except Examination.DoesNotExist:
return new_exam
if old_exam:
if old_exam.date_of_examination > new_exam:
raise forms.ValidationError("Second examination should take place after first examination")
return new_exam`
def clean_date_of_examination(self):
new_exam = self.cleaned_data.get('date_of_examination')
old_exam = Examination.objects.get(patient = self.cleaned_data.get('Patient'))
if old_exam:
if old_exam.date_of_examination > new_exam.date_of_examination:
raise forms.ValidationError("Second examination should take place after first examination")
return data
def clean_date_of_examination(self):
# Where 'data' is used?
date_of_exam = self.cleaned_data['date_of_examination']
try:
pat1 = Patient.object.get(examination__number_of_examination=1, date_of_examination=date_of_exam)
except Patiens.DoesNotExist:
# Patient 1 with given query doesn't exist. Handle it!
try:
pat2 = Patient.object.get(examination__number_of_examination=2, date_of_examination=date_of_exam)
except Patiens.DoesNotExist:
# Patient 2 with given query doesn't exist. Handle it!
if pat2.date_of_examination < pat1.date_of_examination:
raise forms.ValidationError("Second examination should take place after first examination")`
return data`