Django: How to limit TimeField to hours and minutes only (comparison issue) - django

I'm developping a web api using django, I have a TimeField that stores hours:minutes:seconds, since it will be compared to a hours:minutes (without seconds) coming from the mobile app, the comparison will always fail.
here is a comparison without seconds which is returnning an empty QuerySet:
>>> journey = Journey \
... .objects \
... .filter((Q(route__departStation=1) | Q(route__stop__station_id=1)),
... (Q(route__arrivalStation=1) | Q(route__stop__station_id=1)),
... route__departDate="2019-07-31", route__departTime="10:57")
>>> journey
<QuerySet []>
and here is when I add seconds to the comparison:
>>> journey = Journey \
... .objects \
... .filter((Q(route__departStation=1) | Q(route__stop__station_id=1)),
... (Q(route__arrivalStation=1) | Q(route__stop__station_id=1)),
route__departDate="2019-07-31", route__departTime="10:57:05")
>>> journey
<QuerySet [<Journey: Journey object (1)>]>
so please, how can I prevent the seconds from being saved to the database from this TimeField(), or at least how can I limit the comparison to hours and minutes only.

You can use a TruncMinute expression [Django-doc] here:
from django.db.models.functions import TruncMinute
journeys = Journey.objects.annotate(
depart_minute=TruncMinute('route__departTime')
).filter(
(Q(route__departStation=1) | Q(route__stop__station_id=1)),
(Q(route__arrivalStation=1) | Q(route__stop__station_id=1))
route__departDate='2019-07-31', depart_minute='10:57'
)
That being said, I suggest you use a DateTimeField over a DateField and TimeField. Time actually only makes sense in combination with a date (and timezone). Many countries have daylight saving time (DST), or have changed their timezone throughout history. Therefore it is better to combine these.

In the model you can basically override the save method, take the timefield attribute and override it with a new datetime.time object, but without storing the seconds in it.
Here is an example:
from django.db import models
from datetime import time
class Route(models.Model):
departTime = models.TimeField(null=True, blank=True)
def save(self, *args, **kwargs):
self.departTime = time(hour=self.departTime.hour, minute=self.departTime.minute)
return super().save(*args, **kwargs)
However, bare in mind, that the save method is not going to be called when you are using bulk operations to create Route objects or update on querysets.
In those cases you can make use of manager classes to override for instance the bulk_create or bulk_update methods or QuerySet to override the update method and only store hours and minutes there.

Related

In Django how can I get result from two tables in a single queryset without having relationship? [duplicate]

I'm trying to build the search for a Django site I am building, and in that search, I am searching in three different models. And to get pagination on the search result list, I would like to use a generic object_list view to display the results. But to do that, I have to merge three querysets into one.
How can I do that? I've tried this:
result_list = []
page_list = Page.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
article_list = Article.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
post_list = Post.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
for x in page_list:
result_list.append(x)
for x in article_list:
result_list.append(x)
for x in post_list:
result_list.append(x)
return object_list(
request,
queryset=result_list,
template_object_name='result',
paginate_by=10,
extra_context={
'search_term': search_term},
template_name="search/result_list.html")
But this doesn't work. I get an error when I try to use that list in the generic view. The list is missing the clone attribute.
How can I merge the three lists, page_list, article_list and post_list?
Concatenating the querysets into a list is the simplest approach. If the database will be hit for all querysets anyway (e.g. because the result needs to be sorted), this won't add further cost.
from itertools import chain
result_list = list(chain(page_list, article_list, post_list))
Using itertools.chain is faster than looping each list and appending elements one by one, since itertools is implemented in C. It also consumes less memory than converting each queryset into a list before concatenating.
Now it's possible to sort the resulting list e.g. by date (as requested in hasen j's comment to another answer). The sorted() function conveniently accepts a generator and returns a list:
result_list = sorted(
chain(page_list, article_list, post_list),
key=lambda instance: instance.date_created)
If you're using Python 2.4 or later, you can use attrgetter instead of a lambda. I remember reading about it being faster, but I didn't see a noticeable speed difference for a million item list.
from operator import attrgetter
result_list = sorted(
chain(page_list, article_list, post_list),
key=attrgetter('date_created'))
Try this:
matches = pages | articles | posts
It retains all the functions of the querysets which is nice if you want to order_by or similar.
Please note: this doesn't work on querysets from two different models.
Related, for mixing querysets from the same model, or for similar fields from a few models, starting with Django 1.11 a QuerySet.union() method is also available:
union()
union(*other_qs, all=False)
New in Django 1.11. Uses SQL’s UNION operator to combine the results of two or more QuerySets. For example:
>>> qs1.union(qs2, qs3)
The UNION operator selects only distinct values by default. To allow duplicate values, use the all=True
argument.
union(), intersection(), and difference() return model instances of
the type of the first QuerySet even if the arguments are QuerySets of
other models. Passing different models works as long as the SELECT
list is the same in all QuerySets (at least the types, the names don’t
matter as long as the types in the same order).
In addition, only LIMIT, OFFSET, and ORDER BY (i.e. slicing and
order_by()) are allowed on the resulting QuerySet. Further, databases
place restrictions on what operations are allowed in the combined
queries. For example, most databases don’t allow LIMIT or OFFSET in
the combined queries.
You can use the QuerySetChain class below. When using it with Django's paginator, it should only hit the database with COUNT(*) queries for all querysets and SELECT() queries only for those querysets whose records are displayed on the current page.
Note that you need to specify template_name= if using a QuerySetChain with generic views, even if the chained querysets all use the same model.
from itertools import islice, chain
class QuerySetChain(object):
"""
Chains multiple subquerysets (possibly of different models) and behaves as
one queryset. Supports minimal methods needed for use with
django.core.paginator.
"""
def __init__(self, *subquerysets):
self.querysets = subquerysets
def count(self):
"""
Performs a .count() for all subquerysets and returns the number of
records as an integer.
"""
return sum(qs.count() for qs in self.querysets)
def _clone(self):
"Returns a clone of this queryset chain"
return self.__class__(*self.querysets)
def _all(self):
"Iterates records in all subquerysets"
return chain(*self.querysets)
def __getitem__(self, ndx):
"""
Retrieves an item or slice from the chained set of results from all
subquerysets.
"""
if type(ndx) is slice:
return list(islice(self._all(), ndx.start, ndx.stop, ndx.step or 1))
else:
return islice(self._all(), ndx, ndx+1).next()
In your example, the usage would be:
pages = Page.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
articles = Article.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
posts = Post.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
matches = QuerySetChain(pages, articles, posts)
Then use matches with the paginator like you used result_list in your example.
The itertools module was introduced in Python 2.3, so it should be available in all Python versions Django runs on.
In case you want to chain a lot of querysets, try this:
from itertools import chain
result = list(chain(*docs))
where: docs is a list of querysets
The big downside of your current approach is its inefficiency with large search result sets, as you have to pull down the entire result set from the database each time, even though you only intend to display one page of results.
In order to only pull down the objects you actually need from the database, you have to use pagination on a QuerySet, not a list. If you do this, Django actually slices the QuerySet before the query is executed, so the SQL query will use OFFSET and LIMIT to only get the records you will actually display. But you can't do this unless you can cram your search into a single query somehow.
Given that all three of your models have title and body fields, why not use model inheritance? Just have all three models inherit from a common ancestor that has title and body, and perform the search as a single query on the ancestor model.
This can be achieved by two ways either.
1st way to do this
Use union operator for queryset | to take union of two queryset. If both queryset belongs to same model / single model than it is possible to combine querysets by using union operator.
For an instance
pagelist1 = Page.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
pagelist2 = Page.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
combined_list = pagelist1 | pagelist2 # this would take union of two querysets
2nd way to do this
One other way to achieve combine operation between two queryset is to use itertools chain function.
from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
You can use Union:
qs = qs1.union(qs2, qs3)
But if you want to apply order_by on the foreign models of the combined queryset... then you need to Select them beforehand this way... otherwise it won't work.
Example
qs = qs1.union(qs2.select_related("foreignModel"), qs3.select_related("foreignModel"))
qs.order_by("foreignModel__prop1")
where prop1 is a property in the foreign model.
DATE_FIELD_MAPPING = {
Model1: 'date',
Model2: 'pubdate',
}
def my_key_func(obj):
return getattr(obj, DATE_FIELD_MAPPING[type(obj)])
And then sorted(chain(Model1.objects.all(), Model2.objects.all()), key=my_key_func)
Quoted from https://groups.google.com/forum/#!topic/django-users/6wUNuJa4jVw. See Alex Gaynor
Requirements:
Django==2.0.2, django-querysetsequence==0.8
In case you want to combine querysets and still come out with a QuerySet, you might want to check out django-queryset-sequence.
But one note about it. It only takes two querysets as it's argument. But with python reduce you can always apply it to multiple querysets.
from functools import reduce
from queryset_sequence import QuerySetSequence
combined_queryset = reduce(QuerySetSequence, list_of_queryset)
And that's it. Below is a situation I ran into and how I employed list comprehension, reduce and django-queryset-sequence
from functools import reduce
from django.shortcuts import render
from queryset_sequence import QuerySetSequence
class People(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
mentor = models.ForeignKey('self', null=True, on_delete=models.SET_NULL, related_name='my_mentees')
class Book(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Student, on_delete=models.CASCADE)
# as a mentor, I want to see all the books owned by all my mentees in one view.
def mentee_books(request):
template = "my_mentee_books.html"
mentor = People.objects.get(user=request.user)
my_mentees = mentor.my_mentees.all() # returns QuerySet of all my mentees
mentee_books = reduce(QuerySetSequence, [each.book_set.all() for each in my_mentees])
return render(request, template, {'mentee_books' : mentee_books})
Here's an idea... just pull down one full page of results from each of the three and then throw out the 20 least useful ones... this eliminates the large querysets and that way you only sacrifice a little performance instead of a lot.
The best option is to use the Django built-in methods:
# Union method
result_list = page_list.union(article_list, post_list)
That will return the union of all the objects in those querysets.
If you want to get just the objects that are in the three querysets, you will love the built-in method of querysets, intersection.
# intersection method
result_list = page_list.intersection(article_list, post_list)
This will do the work without using any other libraries:
result_list = page_list | article_list | post_list
You can use "|"(bitwise or) to combine the querysets of the same model as shown below:
# "store/views.py"
from .models import Food
from django.http import HttpResponse
def test(request):
# ↓ Bitwise or
result = Food.objects.filter(name='Apple') | Food.objects.filter(name='Orange')
print(result)
return HttpResponse("Test")
Output on console:
<QuerySet [<Food: Apple>, <Food: Orange>]>
[22/Jan/2023 12:51:44] "GET /store/test/ HTTP/1.1" 200 9
And, you can use |= to add the queryset of the same model as shown below:
# "store/views.py"
from .models import Food
from django.http import HttpResponse
def test(request):
result = Food.objects.filter(name='Apple')
# ↓↓ Here
result |= Food.objects.filter(name='Orange')
print(result)
return HttpResponse("Test")
Output on console:
<QuerySet [<Food: Apple>, <Food: Orange>]>
[22/Jan/2023 12:51:44] "GET /store/test/ HTTP/1.1" 200 9
Be careful, if adding the queryset of a different model as shown below:
# "store/views.py"
from .models import Food, Drink
from django.http import HttpResponse
def test(request):
# "Food" model # "Drink" model
result = Food.objects.filter(name='Apple') | Drink.objects.filter(name='Milk')
print(result)
return HttpResponse("Test")
There is an error below:
AssertionError: Cannot combine queries on two different base models.
[22/Jan/2023 13:40:54] "GET /store/test/ HTTP/1.1" 500 96025
But, if adding the empty queryset of a different model as shown below:
# "store/views.py"
from .models import Food, Drink
from django.http import HttpResponse
def test(request):
# "Food" model # Empty queryset of "Drink" model
result = Food.objects.filter(name='Apple') | Drink.objects.none()
print(result)
return HttpResponse("Test")
There is no error below:
<QuerySet [<Food: Apple>]>
[22/Jan/2023 13:51:09] "GET /store/test/ HTTP/1.1" 200 9
Again be careful, if adding the object by get() as shown below:
# "store/views.py"
from .models import Food
from django.http import HttpResponse
def test(request):
result = Food.objects.filter(name='Apple')
# ↓↓ Object
result |= Food.objects.get(name='Orange')
print(result)
return HttpResponse("Test")
There is an error below:
AttributeError: 'Food' object has no attribute '_known_related_objects'
[22/Jan/2023 13:55:57] "GET /store/test/ HTTP/1.1" 500 95748
This recursive function concatenates array of querysets into one queryset.
def merge_query(ar):
if len(ar) ==0:
return [ar]
while len(ar)>1:
tmp=ar[0] | ar[1]
ar[0]=tmp
ar.pop(1)
return ar

Queryset to build "pivot table" for the dataset using Django ORM?

I have CurrencyHistory model along with the database table which is populated on every Currency model update for the historical data.
class CurrencyHistory(models.Model):
id = models.AutoField(primary_key=True)
change_rate_date = models.DateTimeField(_("Change Rate Date"),
auto_now=False, auto_now_add=True,
db_column='change_rate_date')
code = models.ForeignKey("core.Currency", verbose_name=_("Code"),
on_delete=models.CASCADE,
related_name='history',
db_column='code')
to_usd_rate = models.DecimalField(_("To USD Rate"),
max_digits=20,
decimal_places=6,
null=True,
db_column='to_usd_rate')
Database structure looks like
id | change_rate_date | code | to_usd_rate
1 | 2021-01-01 | EUR | 0.123456
2 | 2021-01-01 | CAD | 0.987654
3 | 2021-01-02 | EUR | 0.123459
4 | 2021-01-02 | CAD | 0.987651
I need to fetch data using Djnago ORM to have a dictionary to display single row per date with the every currency as columns, like this
Date
EUR
CAD
2021-01-01
0.123456
0.987654
2021-01-02
0.123459
0.987651
But I have no idea how to correctly do it using Django ORM to make it fast.
I suppose for loop over the all unique database dates to get dict for
each data will work in this case but it looks very slow solution that
will generate thousands of requests.
You want to use serailizers to turn a model instance into a dictionary. Even if your not using a RESTFUL api, serailizers are the best way to get show dictionaries.
All types of ways to serialize data
Convert Django Model object to dict with all of the fields intact
For a quick summary of my top favorite methods..
Method 1.
Model to dict
from django.forms import model_to_dict
instance = CurrencyHistory(...)
dict_instance = model_to_dict(instance)
Method 2:
Serailizers (this will show the most detail)
of course this implies that you'll need to install DRF
pip install rest_framework
Serializers.py
# import your model
from rest_framework import serializers
class CurrencySerialzier(serializers.ModelSerializer):
code = serializers.StringRelatedField() # returns the string repr of the model inst
class Meta:
model = CurrencyHistory
fields = "__all__"
views.py
from .serializers import CurrencySerializer
...inside your view
currency_inst = CurrencyHistory.objects.get()
serializer = CurrencySerializer(currency_inst)
serializer.data # this returns a mapping of the instance (dictionary)
# here is where you either return a context object and template, or return a DRF Response object
You can use the django-pivot module. After you pip install django-pivot:
from django_pivot.pivot import pivot
pivot_table_dictionary = pivot(CurrencyHistory,
'change_rate_date',
'code',
'to_usd_rate')
The default aggregation is Sum which will work fine if you only have one entry per date per currency. If the same currency shows up multiple times on a single date, you'll need to choose what number you want to display. The average of to_usd_rate? The max? You can pass the aggregation function to the pivot call.
Alternatively, put unique_together = [('change_rate_date', 'code')] in the Meta class of your model to ensure there really is only one value for each date, code pair.

Django: Combine a date and time field and filter

I have a django model that has a date field and a separate time field. I am trying to use a filter to find a value on the latest record by date/time that is less than the current record's date time.
How do I use annotate/aggregate to combine the date and time fields into one and then do a filter on it?
models.py
class Note(models.model):
note_date = models.DateField(null=True)
note_time = models.TimeField(null=True)
note_value = models.PositiveIntegerField(null=True)
def get_last(n):
"""
n: Note
return: Return the note_value of the most recent Note prior to given Note.
"""
latest = Note.objects.filter(
note_date__lte=n.note_date
).order_by(
'-note_date', '-note_time'
).first()
return latest.note_value if latest else return 0
This will return any notes from a previous date, but if I have a two notes on the same date, one at 3pm and one at 1pm, and I send the 3pm note to the function, I want to get the value of the 1pm note. Is there a way to annotate the two fields into one for comparison, or do I have to perform a raw SQL query? Is there a way to convert the date and time component into one, similar to how you could use Concat for strings?
Note.objects.annotate(
my_dt=Concat('note_date', 'note_time')
).filter(
my_dt__lt=Concat(models.F('note_date'), models.F('note_time')
).first()
I am too late but here is what I did
from django.db.models import DateTimeField, ExpressionWrapper, F
notes = Note.objects.annotate(my_dt=ExpressionWrapper(F('note_date') + F('note_time'), output_field=DateTimeField()))
Now we have added a new field my_dt of datetime type and can add a filter further to do operations
Found an answer using models.Q here: filter combined date and time in django
Note.objects.filter(
models.Q(note_date__lt=n.note_date) | models.Q(
note_date=n.note_date,
note_time__lt=n.note_time
)
).first()
I guess I just wasn't searching by the right criteria.
Here is another Approach which is more authentic
from django.db.models import Value, DateTimeField
from django.db.models.functions import Cast, Concat
notes = Note.objects.annotate(my_dt=Cast(
Concat('note_date', Value(" "), 'note_time', output_field=DateTimeField()),
output_field=DateTimeField()
).filter(my_dt__lte=datetime.now())
Here is another solution following others.
def get_queryset(self):
from django.db import models
datetime_wrapper = models.ExpressionWrapper(models.F('note_date') + models.F('note_time'), output_field=models.DateTimeField())
return Note.objects.annotate(
note_datetime=datetime_wrapper
).filter(note_datetime__gt=timezone.now()).order_by('note_datetime')

How should I use DurationField in my model?

In my model I want to be able to input duration, like 2 years, 5 months, etc.
In version 1.8 DurationField was introduced so I tried using that:
In my model I have
user_validPeriod = models.DurationField()
Trying to add a new User from my admin panel, If I try typing something like 2d or 2 days in the appearing text-field though I get Enter a valid duration.
Can someone provide me with an example of how this field is supposed to be used?
To use a DurationField in django 1.8 you have to use a python datetime.timedelta instance like this:
Considering this model :
from django.db import models
class MyModel(models.Model):
duration = models.DurationField()
You can set a duration this way :
import datetime
my_model = MyModel()
my_model.duration = datetime.timedelta(days=20, hours=10)
And query it this way :
# Equal
durations = MyModel.objects.filter(duration=datetime.timedelta(*args, **kwargs))
# Greater than or equal
durations = MyModel.objects.filter(duration__gte=datetime.timedelta(*args, **kwargs))
# Less than or equal
durations = MyModel.objects.filter(duration__lte=datetime.timedelta(*args, **kwargs))
More info on datetime.timedelta here and on DurationField here.
In your admin panel, you can enter a duration with a string with following format : [DD] [[hh:]mm:]ss

How to obtain and/or save the queryset criteria to the DB?

I would like to save a queryset criteria to the DB for reuse.
So, if I have a queryset like:
Client.objects.filter(state='AL')
# I'm simplifying the problem for readability. In reality I could have
# a very complex queryset, with multiple filters, excludes and even Q() objects.
I would like to save to the DB not the results of the queryset (i.e. the individual client records that have a state field matching 'AL'); but the queryset itself (i.e. the criteria used in filtering the Client model).
The ultimate goal is to have a "saved filter" that can be read from the DB and used by multiple django applications.
At first I thought I could serialize the queryset and save that. But serializing a queryset actually executes the query - and then I end up with a static list of clients in Alabama at the time of serialization. I want the list to be dynamic (i.e. each time I read the queryset from the DB it should execute and retrieve the most current list of clients in Alabama).
Edit: Alternatively, is it possible to obtain a list of filters applied to a queryset?
Something like:
qs = Client.objects.filter(state='AL')
filters = qs.getFilters()
print filters
{ 'state': 'AL' }
You can do as jcd says, storing the sql.
You can also store the conditions.
In [44]: q=Q( Q(content_type__model="User") | Q(content_type__model="Group"),content_type__app_label="auth")
In [45]: c={'name__startswith':'Can add'}
In [46]: Permission.objects.filter(q).filter(**c)
Out[46]: [<Permission: auth | group | Can add group>, <Permission: auth | user | Can add user>]
In [48]: q2=Q( Q(content_type__model="User") | Q(content_type__model="Group"),content_type__app_label="auth", name__startswith='Can add')
In [49]: Permission.objects.filter(q2)
Out[49]: [<Permission: auth | group | Can add group>, <Permission: auth | user | Can add user>]
In that example you see that the conditions are the objects c and q (although they can be joined in one object, q2). You can then serialize these objects and store them on the database as strings.
--edit--
If you need to have all the conditions on a single database record, you can store them in a dictionary
{'filter_conditions': (cond_1, cond_2, cond_3), 'exclude_conditions': (cond_4, cond_5)}
and then serialize the dictionary.
You can store the sql generated by the query using the queryset's _as_sql() method. The method takes a database connection as an argument, so you'd do:
from app.models import MyModel
from django.db import connection
qs = MyModel.filter(pk__gt=56, published_date__lt=datetime.now())
store_query(qs._as_sql(connection))
You can use http://github.com/denz/django-stored-queryset for that
You can pickle the Query object (not the QuerySet):
>>> import pickle
>>> query = pickle.loads(s) # Assuming 's' is the pickled string.
>>> qs = MyModel.objects.all()
>>> qs.query = query # Restore the original 'query'.
Docs: https://docs.djangoproject.com/en/dev/ref/models/querysets/#pickling-querysets
But: You can’t share pickles between versions
you can create your own model to store your queries.
First field can contains fk to ContentTypes
Second field can be just text field with your query etc.
And after that you can use Q object to set queryset for your model.
The current answer was unclear to me as I don't have much experience with pickle. In 2022, I've found that turning a dict into JSON worked well. I'll show you what I did below. I believe pickling still works, so at the end I will show some more thoughts there.
models.py - example database structure
class Transaction(models.Model):
id = models.CharField(max_length=24, primary_key=True)
date = models.DateField(null=False)
amount = models.IntegerField(null=False)
info = models.CharField()
account = models.ForiegnKey(Account, on_delete=models.SET_NULL, null=True)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=False, default=None)
class Account(models.Model):
name = models.CharField()
email = models.EmailField()
class Category(models.Model):
name = models.CharField(unique=True)
class Rule(models.Model):
category = models.ForeignKey(Category, on_delete=models.SET_NULL, blank=False, null=True, default=None)
criteria = models.JSONField(default=dict) # this will hold our query
My models store financial transactions, the category the transaction fits into (e.g., salaried income, 1099 income, office expenses, labor expenses, etc...), and a rule to save a query to automatically categorize future transactions without having to remember the query every year when doing taxes.
I know, for example, that all my transactions with my consulting clients should be marked as 1099 income. So I want to create a rule for clients that will grab each monthly transaction and mark it as 1099 income.
Making the query the old-fashioned way
>>> from transactions.models import Category, Rule, Transaction
>>>
>>> client1_transactions = Transaction.objects.filter(account__name="Client One")
<QuerySet [<Transaction: Transaction object (1111111)>, <Transaction: Transaction object (1111112)>, <Transaction: Transaction object (1111113)...]>
>>> client1_transactions.count()
12
Twelve transactions, one for each month. Beautiful.
But how do we save this to the database?
Save query to database in JSONField
We now have Django 4.0 and a bunch of support for JSONField.
I've been able to grab the filtering values out of a form POST request, then add them in view logic.
urls.py
from transactions import views
app_name = "transactions"
urlpatterns = [
path("categorize", views.categorize, name="categorize"),
path("", views.list, name="list"),
]
transactions/list.html
<form action="{% url 'transactions:categorize' %}" method="POST">
{% csrf_token %}
<label for="info">Info field contains...</label>
<input id="info" type="text" name="info">
<label for="account">Account name contains...</label>
<input id="account" type="text" name="account">
<label for="category">New category should be...</label>
<input id="category" type="text" name="category">
<button type="submit">Make a Rule</button>
</form>
views.py
def categorize(request):
# get POST data from our form
info = request.POST.get("info", "")
account = request.POST.get("account", "")
category = request.POST.get("category", "")
# set up query
query = {}
if info:
query["info__icontains"] = info
if account:
query["account__name__icontains"] = account
# update the database
category_obj, _ = Category.objects.get_or_create(name=category)
transactions = Transaction.objects.filter(**query).order_by("-date")
Rule.objects.get_or_create(category=category_obj, criteria=query)
transactions.update(category=category_obj)
# render the template
return render(
request,
"transactions/list.html",
{
"transactions": transactions.select_related("account"),
},
)
That's pretty much it!
My example here is a little contrived, so please forgive any errors.
How to do it with pickle
I actually lied before. I have a little experience with pickle and I do like it, but I am not sure on how to save it to the database. My guess is that you'd then save the pickled string to a BinaryField.
Perhaps something like this:
>>> # imports
>>> import pickle # standard library
>>> from transactions.models import Category, Rule, Transaction # my own stuff
>>>
>>> # create the query
>>> qs_to_save = Transaction.objects.filter(account__name="Client 1")
>>> qs_to_save.count()
12
>>>
>>> # create the pickle
>>> saved_pickle = pickle.dumps(qs_to_save.query)
>>> type(saved_pickle)
<class 'bytes'>
>>>
>>> # save to database
>>> # make sure `criteria = models.BinaryField()` above in models.py
>>> # I'm unsure about this
>>> test_category, _ = Category.objects.get_or_create(name="Test Category")
>>> test_rule = Rule.objects.create(category=test_category, criteria=saved_pickle)
>>>
>>> # remake queryset at a later date
>>> new_qs = Transaction.objects.all()
>>> new_qs.query = pickle.loads(test_rule.criteria)
>>> new_qs.count()
12
Going even further beyond
I found a way to make this all work with my htmx live search, allowing me to see the results of my query on the front end of my site before saving.
This answer is already too long, so here's a link to a post if you care about that: Saving a Django Query to the Database.