Skip to content

Commit b9f1acc

Browse files
committed
Created generate_rql_class command
* Command `generate_rql_class` is ready and tested * Refactored `NestedAutoRQLFilterClass` optimizations for the convinience of command * Extended docs/README
1 parent d452bb3 commit b9f1acc

File tree

13 files changed

+274
-26
lines changed

13 files changed

+274
-26
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ dist/
1313
tests/reports/
1414
.coverage
1515

16-
docs/_build
16+
docs/_build
17+
18+
_generated_filters*.py

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,55 @@ Notes
180180
3. Support for Choices() fields from [Django Model Utilities](https://django-model-utils.readthedocs.io/en/latest/utilities.html#choices) is added
181181

182182

183+
Helpers
184+
================================
185+
There is a Django command `generate_rql_class` to decrease development and integration efforts for filtering.
186+
This command automatically generates a filter class for a given model with all relations and all optimizations (!) to the specified depth.
187+
188+
Example
189+
-------
190+
```commandline
191+
django-admin generate_rql_class --settings=tests.dj_rf.settings tests.dj_rf.models.Publisher --depth=1 --exclude=authors,fk2
192+
```
193+
This command for the model `Publisher` from tests package will produce the following output to stdout:
194+
```python
195+
from tests.dj_rf.models import Publisher
196+
197+
from dj_rql.filter_cls import RQLFilterClass
198+
from dj_rql.qs import NPR, NSR
199+
200+
201+
class PublisherFilters(RQLFilterClass):
202+
MODEL = Publisher
203+
SELECT = True
204+
EXCLUDE_FILTERS = ['authors', 'fk2']
205+
FILTERS = [
206+
{
207+
"filter": "id",
208+
"ordering": True,
209+
"search": False
210+
},
211+
{
212+
"filter": "name",
213+
"ordering": True,
214+
"search": True
215+
},
216+
{
217+
"namespace": "fk1",
218+
"filters": [
219+
{
220+
"filter": "id",
221+
"ordering": True,
222+
"search": False
223+
}
224+
],
225+
"qs": NSR('fk1')
226+
}
227+
]
228+
229+
```
230+
231+
183232
Django Rest Framework Extensions
184233
================================
185234
1. Pagination (limit, offset)

dj_rql/filter_cls.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,12 +1116,11 @@ def _iter_model_to_get_filters(self, depth, model_data):
11161116
model_related_models.append(relation_data + (rel_f_name,))
11171117
continue
11181118

1119-
if isinstance(field, SUPPORTED_FIELD_TYPES):
1120-
namespace.append({
1121-
'filter': field.name,
1122-
'ordering': True,
1123-
'search': FilterTypes.field_filter_type(field) == FilterTypes.STRING,
1124-
})
1119+
namespace.append({
1120+
'filter': field.name,
1121+
'ordering': True,
1122+
'search': FilterTypes.field_filter_type(field) == FilterTypes.STRING,
1123+
})
11251124

11261125
return [i for i in model_related_models if i[0] not in through_models]
11271126

@@ -1131,19 +1130,12 @@ def _add_relation_to_iterated_models(self, depth, field, namespace):
11311130
else:
11321131
circular_related_name = field.field.name
11331132

1134-
field_name = field.name
1135-
qs = None
1136-
if isinstance(field, (ForeignKey, OneToOneField, OneToOneRel)):
1137-
qs = NSR(field_name)
1138-
elif not self._is_through_field(field):
1139-
qs = NPR(field_name)
1140-
11411133
namespace_filters = []
11421134
if depth < self.DEPTH:
11431135
namespace.append({
1144-
'namespace': field_name,
1136+
'namespace': field.name,
11451137
'filters': namespace_filters,
1146-
'qs': qs,
1138+
'qs': self._get_field_optimization(field),
11471139
})
11481140

11491141
return field.related_model, namespace_filters, circular_related_name
@@ -1154,16 +1146,22 @@ def _get_relative_field_name(self, field, circular_related_name, prefix):
11541146
# This is needed to avoid circular dependencies
11551147
return
11561148

1157-
if prefix:
1158-
rel_f_name = '.'.join((prefix, field_name))
1159-
else:
1160-
rel_f_name = field_name
1161-
1149+
rel_f_name = '.'.join((prefix, field_name)) if prefix else field_name
11621150
if rel_f_name in self.EXCLUDE_FILTERS or rel_f_name in self._described_filters:
11631151
return
11641152

11651153
return rel_f_name
11661154

1155+
def _get_field_optimization(self, field):
1156+
if not self.SELECT:
1157+
return
1158+
1159+
if isinstance(field, (ForeignKey, OneToOneField, OneToOneRel)):
1160+
return NSR(field.name)
1161+
1162+
if not self._is_through_field(field):
1163+
return NPR(field.name)
1164+
11671165
@staticmethod
11681166
def _is_through_field(field):
11691167
return getattr(field, 'through', None)

dj_rql/management/__init__.py

Whitespace-only changes.

dj_rql/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#
2+
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
3+
#
4+
5+
import json
6+
import re
7+
8+
from dj_rql.filter_cls import NestedAutoRQLFilterClass
9+
10+
from django.core.management import BaseCommand
11+
from django.db.models import ForeignKey, OneToOneField, OneToOneRel
12+
from django.utils.module_loading import import_string
13+
14+
15+
TEMPLATE = """from {model_package} import {model_name}
16+
17+
from dj_rql.filter_cls import RQLFilterClass
18+
{optimizations_import}
19+
20+
class {model_name}Filters(RQLFilterClass):
21+
MODEL = {model_name}
22+
SELECT = {select_flag}
23+
EXCLUDE_FILTERS = {exclusions}
24+
FILTERS = {filters}
25+
"""
26+
27+
28+
class Command(BaseCommand):
29+
help = (
30+
'Automatically generates a filter class for a model '
31+
'with all relations to the specified depth.'
32+
)
33+
34+
def add_arguments(self, parser):
35+
parser.add_argument(
36+
'model',
37+
nargs=1,
38+
type=str,
39+
help='Importable model location string.',
40+
)
41+
parser.add_argument(
42+
'-d',
43+
'--depth',
44+
type=int,
45+
default=1,
46+
help='Max depth of traversed model relations.',
47+
)
48+
parser.add_argument(
49+
'-s',
50+
'--select',
51+
action='store_true',
52+
default=True,
53+
help='Flag to include QuerySet optimizations: true by default.',
54+
)
55+
parser.add_argument(
56+
'-e',
57+
'--exclude',
58+
type=str,
59+
help='List of coma separated filter names or namespace to be excluded from generation.',
60+
)
61+
62+
def handle(self, *args, **options):
63+
model_import = options['model'][0]
64+
model = import_string(model_import)
65+
is_select = options['select']
66+
exclusions = options['exclude'].split(',') if options['exclude'] else []
67+
68+
class Cls(NestedAutoRQLFilterClass):
69+
MODEL = model
70+
DEPTH = options['depth']
71+
SELECT = is_select
72+
EXCLUDE_FILTERS = exclusions
73+
74+
def _get_init_filters(self):
75+
self.init_filters = super()._get_init_filters()
76+
77+
self.DEPTH = 0
78+
self.SELECT = False
79+
return super()._get_init_filters()
80+
81+
def _get_field_optimization(self, field):
82+
if not self.SELECT:
83+
return
84+
85+
if isinstance(field, (ForeignKey, OneToOneField, OneToOneRel)):
86+
return "NSR('{0}')".format(field.name)
87+
88+
if not self._is_through_field(field):
89+
return "NPR('{0}')".format(field.name)
90+
91+
filters = Cls(model._default_manager.all()).init_filters
92+
filters_str = json.dumps(filters, sort_keys=False, indent=4).replace(
93+
'"ordering": true', '"ordering": True',
94+
).replace(
95+
'"ordering": false', '"ordering": False',
96+
).replace(
97+
'"search": true', '"search": True',
98+
).replace(
99+
'"search": false', '"search": False',
100+
).replace(
101+
'"qs": null', '"qs": None',
102+
)
103+
104+
filters_str = re.sub(r"\"((NPR|NSR)\('\w+?'\))\"", r'\1', filters_str)
105+
106+
model_package, model_name = model_import.rsplit('.', 1)
107+
code = TEMPLATE.format(
108+
model_package=model_package,
109+
model_name=model_name,
110+
filters=filters_str,
111+
select_flag='True' if is_select else 'False',
112+
optimizations_import='from dj_rql.qs import NPR, NSR\n' if is_select else '',
113+
exclusions=exclusions,
114+
)
115+
116+
return code

docs/reference.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ Filter classes
7575
:members:
7676

7777

78+
.. autoclass:: dj_rql.filter_cls.NestedAutoRQLFilterClass
79+
:members:
80+
81+
7882
DB optimization
7983
---------------
8084

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
test = pytest
33

44
[flake8]
5-
exclude = .idea,.git,venv*/,.eggs/,*.egg-info
5+
exclude = .idea,.git,venv*/,.eggs/,*.egg-info,_generated_filters*.py
66
max-line-length = 100
77
show-source = True
88
ignore = W605
99

1010
[tool:pytest]
11-
addopts = --create-db --nomigrations --junitxml=tests/reports/out.xml --cov=dj_rql --cov-report xml:tests/reports/coverage.xml
11+
addopts = --show-capture=no --create-db --nomigrations --junitxml=tests/reports/out.xml --cov=dj_rql --cov-report xml:tests/reports/coverage.xml
1212
DJANGO_SETTINGS_MODULE = tests.dj_rf.settings

tests/dj_rf/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class RandomFk(models.Model):
1818
class Publisher(models.Model):
1919
name = models.CharField(max_length=20, null=True)
2020

21-
fk1 = models.ForeignKey(RandomFk, on_delete=models.SET_NULL, null=True)
21+
fk1 = models.ForeignKey(RandomFk, on_delete=models.SET_NULL, null=True, related_name='r')
2222
fk2 = models.ForeignKey(RandomFk, on_delete=models.SET_NULL, null=True)
2323

2424

@@ -48,7 +48,7 @@ class Book(models.Model):
4848
blog_rating = models.BigIntegerField(null=True, choices=BLOG_RATING_CHOICES)
4949
github_stars = models.PositiveIntegerField(null=True)
5050
amazon_rating = models.FloatField(null=True)
51-
current_price = models.DecimalField(null=True, decimal_places=4)
51+
current_price = models.DecimalField(null=True, decimal_places=4, max_digits=20)
5252

5353
written = models.DateField(null=True)
5454
published_at = models.DateTimeField(null=True)

tests/dj_rf/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
'django.contrib.staticfiles',
2424
'rest_framework',
2525
'tests.dj_rf',
26+
'dj_rql',
2627
]
2728

2829
MIDDLEWARE = [
@@ -53,3 +54,5 @@
5354
USE_L10N = True
5455

5556
USE_TZ = True
57+
58+
SILENCED_SYSTEM_CHECKS = ['models.W042', 'fields.W903', 'fields.E005']

0 commit comments

Comments
 (0)