django-flex 26.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-flex might be problematic. Click here for more details.
- django_flex-26.1.2/CHANGELOG.md +42 -0
- django_flex-26.1.2/LICENSE +21 -0
- django_flex-26.1.2/MANIFEST.in +7 -0
- django_flex-26.1.2/PKG-INFO +300 -0
- django_flex-26.1.2/README.md +253 -0
- django_flex-26.1.2/django_flex/__init__.py +92 -0
- django_flex-26.1.2/django_flex/conf.py +92 -0
- django_flex-26.1.2/django_flex/decorators.py +182 -0
- django_flex-26.1.2/django_flex/fields.py +234 -0
- django_flex-26.1.2/django_flex/filters.py +207 -0
- django_flex-26.1.2/django_flex/middleware.py +143 -0
- django_flex-26.1.2/django_flex/permissions.py +365 -0
- django_flex-26.1.2/django_flex/query.py +290 -0
- django_flex-26.1.2/django_flex/response.py +212 -0
- django_flex-26.1.2/django_flex/views.py +236 -0
- django_flex-26.1.2/django_flex.egg-info/PKG-INFO +300 -0
- django_flex-26.1.2/django_flex.egg-info/SOURCES.txt +30 -0
- django_flex-26.1.2/django_flex.egg-info/dependency_links.txt +1 -0
- django_flex-26.1.2/django_flex.egg-info/requires.txt +11 -0
- django_flex-26.1.2/django_flex.egg-info/top_level.txt +1 -0
- django_flex-26.1.2/docs/api_reference.md +328 -0
- django_flex-26.1.2/docs/examples/basic_usage.md +392 -0
- django_flex-26.1.2/docs/examples/filtering.md +377 -0
- django_flex-26.1.2/docs/installation.md +86 -0
- django_flex-26.1.2/docs/permissions.md +278 -0
- django_flex-26.1.2/docs/quickstart.md +268 -0
- django_flex-26.1.2/pyproject.toml +111 -0
- django_flex-26.1.2/setup.cfg +4 -0
- django_flex-26.1.2/tests/test_fields.py +79 -0
- django_flex-26.1.2/tests/test_filters.py +168 -0
- django_flex-26.1.2/tests/test_permissions.py +77 -0
- django_flex-26.1.2/tests/test_response.py +97 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Calendar Versioning](https://calver.org/) (YY.MM.MICRO).
|
|
7
|
+
|
|
8
|
+
## [26.1.0] - 2026-01-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of django-flex
|
|
13
|
+
- Core query engine with field selection and filtering
|
|
14
|
+
- Permission system with row-level, field-level, and operation-level access control
|
|
15
|
+
- `FlexQueryView` class-based view for easy integration
|
|
16
|
+
- `flex_query` decorator for function-based views
|
|
17
|
+
- Optional middleware for centralized endpoint
|
|
18
|
+
- Comprehensive documentation and examples
|
|
19
|
+
- Support for Django 3.2, 4.0, 4.1, 4.2, 5.0, and 6.0
|
|
20
|
+
- Support for Python 3.8, 3.9, 3.10, 3.11, 3.12, and 3.14
|
|
21
|
+
|
|
22
|
+
### CRUD Action Names
|
|
23
|
+
|
|
24
|
+
- `get` - Retrieve single object by ID
|
|
25
|
+
- `query` - Query multiple objects with filters/pagination
|
|
26
|
+
- `create` - Create new objects
|
|
27
|
+
- `update` - Update existing objects
|
|
28
|
+
- `delete` - Delete objects
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
- **Field Selection**: Use comma-separated field strings with dot notation for relations
|
|
33
|
+
- Wildcards: `*` for all fields, `customer.*` for all customer fields
|
|
34
|
+
- Nested relations: `customer.address.city`
|
|
35
|
+
- **Filtering**: Full Django ORM operator support
|
|
36
|
+
- Comparison: `lt`, `lte`, `gt`, `gte`, `exact`, `in`, `isnull`, `range`
|
|
37
|
+
- Text: `contains`, `icontains`, `startswith`, `endswith`, `regex`
|
|
38
|
+
- Date/Time: `date`, `year`, `month`, `day`, `hour`, `minute`, `second`
|
|
39
|
+
- Composable: `and`, `or`, `not` for complex conditions
|
|
40
|
+
- **Pagination**: Limit/offset with smart cursor-based continuation
|
|
41
|
+
- **Security**: Principle of least privilege with deny-by-default
|
|
42
|
+
- **Performance**: Automatic `select_related` for N+1 prevention
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nehemiah Jacob
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-flex
|
|
3
|
+
Version: 26.1.2
|
|
4
|
+
Summary: A flexible query language for Django - enable frontends to dynamically construct database queries
|
|
5
|
+
Author: Nehemiah Jacob
|
|
6
|
+
Maintainer: Nehemiah Jacob
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/n3h3m/django-flex
|
|
9
|
+
Project-URL: Documentation, https://github.com/n3h3m/django-flex#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/n3h3m/django-flex.git
|
|
11
|
+
Project-URL: Issues, https://github.com/n3h3m/django-flex/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/n3h3m/django-flex/blob/main/CHANGELOG.md
|
|
13
|
+
Keywords: django,query,api,flexible,dynamic,graphql-alternative,rest,orm
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: Django :: 3.2
|
|
18
|
+
Classifier: Framework :: Django :: 4.0
|
|
19
|
+
Classifier: Framework :: Django :: 4.1
|
|
20
|
+
Classifier: Framework :: Django :: 4.2
|
|
21
|
+
Classifier: Framework :: Django :: 5.0
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
31
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
32
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
33
|
+
Requires-Python: >=3.8
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: django>=3.2
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
41
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
42
|
+
Requires-Dist: isort>=5.12; extra == "dev"
|
|
43
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
45
|
+
Requires-Dist: django-stubs>=4.0; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# Django-Flex
|
|
49
|
+
|
|
50
|
+
[](https://pypi.org/project/django-flex/)
|
|
51
|
+
[](https://pypi.org/project/django-flex/)
|
|
52
|
+
[](https://www.djangoproject.com/)
|
|
53
|
+
|
|
54
|
+
**A flexible query language for Django** — Enable frontends to dynamically construct database queries with built-in security.
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- 🔍 **Dynamic Field Selection** — Request only the fields you need
|
|
59
|
+
- 🔬 **Rich Filtering** — Full Django ORM operator support
|
|
60
|
+
- 📄 **Built-in Pagination** — Limit/offset with smart cursor support
|
|
61
|
+
- 🔐 **Layered Security** — Row, field, filter, and operation-level access control
|
|
62
|
+
- ⚡ **N+1 Prevention** — Automatic `select_related` optimization
|
|
63
|
+
- 🎯 **Django Native** — Uses Django's built-in auth (groups, permissions)
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install django-flex
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# settings.py
|
|
73
|
+
INSTALLED_APPS = [
|
|
74
|
+
...
|
|
75
|
+
'django_flex',
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
Using Django's official documentation models: Blog, Author, Entry.
|
|
82
|
+
|
|
83
|
+
### 1. Create a View
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# views.py
|
|
87
|
+
from datetime import date
|
|
88
|
+
from django.db.models import Q
|
|
89
|
+
from django_flex import FlexQueryView
|
|
90
|
+
from .models import Entry
|
|
91
|
+
|
|
92
|
+
class EntryQueryView(FlexQueryView):
|
|
93
|
+
model = Entry
|
|
94
|
+
|
|
95
|
+
flex_permissions = {
|
|
96
|
+
'staff': {
|
|
97
|
+
'rows': lambda user: Q(), # All entries
|
|
98
|
+
'fields': ['*', 'blog.*'],
|
|
99
|
+
'filters': ['id', 'rating.gte', 'blog.id', 'headline.icontains'],
|
|
100
|
+
'order_by': ['-pub_date', 'rating'],
|
|
101
|
+
'ops': ['get', 'query'],
|
|
102
|
+
},
|
|
103
|
+
'authenticated': {
|
|
104
|
+
'rows': lambda user: Q(pub_date__lte=date.today()),
|
|
105
|
+
'fields': ['id', 'headline', 'pub_date', 'blog.name'],
|
|
106
|
+
'filters': ['id', 'headline.icontains'],
|
|
107
|
+
'order_by': ['-pub_date'],
|
|
108
|
+
'ops': ['get', 'query'],
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2. Add URL Route
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# urls.py
|
|
117
|
+
from .views import EntryQueryView
|
|
118
|
+
|
|
119
|
+
urlpatterns = [
|
|
120
|
+
path('api/entries/', EntryQueryView.as_view()),
|
|
121
|
+
]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 3. Query from Frontend
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// Query entries
|
|
128
|
+
const response = await fetch('/api/entries/', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {'Content-Type': 'application/json'},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
fields: 'id, headline, pub_date, blog.name',
|
|
133
|
+
filters: { 'rating.gte': 4 },
|
|
134
|
+
order_by: '-pub_date',
|
|
135
|
+
limit: 20
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Response
|
|
140
|
+
{
|
|
141
|
+
"success": true,
|
|
142
|
+
"code": "FLEX_OK_QUERY",
|
|
143
|
+
"pagination": {"offset": 0, "limit": 20, "has_more": false},
|
|
144
|
+
"results": {
|
|
145
|
+
"1": {"id": 1, "headline": "Django 5.0 Released", "pub_date": "2024-01-15", "blog": {"name": "Django News"}},
|
|
146
|
+
"2": {"id": 2, "headline": "Getting Started Guide", "pub_date": "2024-01-14", "blog": {"name": "Tutorials"}}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Query Language
|
|
152
|
+
|
|
153
|
+
### Field Selection
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
fields: 'id, headline' // Specific fields
|
|
157
|
+
fields: '*' // All model fields
|
|
158
|
+
fields: '*, blog.*' // All fields + related
|
|
159
|
+
fields: 'id, blog.name, blog.tagline' // Nested fields
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Filtering
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
// Simple equality
|
|
166
|
+
filters: { rating: 5 }
|
|
167
|
+
|
|
168
|
+
// Operators
|
|
169
|
+
filters: { 'rating.gte': 4 } // Greater than or equal
|
|
170
|
+
filters: { 'rating.in': [4, 5] } // In list
|
|
171
|
+
filters: { 'headline.icontains': 'django' } // Case-insensitive search
|
|
172
|
+
filters: { 'pub_date.gte': '2024-01-01' } // Date comparison
|
|
173
|
+
|
|
174
|
+
// Composition
|
|
175
|
+
filters: { or: { rating: 5, 'number_of_comments.gte': 100 } }
|
|
176
|
+
filters: { not: { 'rating.lt': 3 } }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Pagination
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
limit: 20 // Max results
|
|
183
|
+
offset: 40 // Skip first 40
|
|
184
|
+
|
|
185
|
+
// Response includes next cursor
|
|
186
|
+
pagination: {
|
|
187
|
+
offset: 40,
|
|
188
|
+
limit: 20,
|
|
189
|
+
has_more: true,
|
|
190
|
+
next: { fields: '...', limit: 20, offset: 60 }
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Security Configuration
|
|
195
|
+
|
|
196
|
+
Django-Flex uses Django's built-in auth for role resolution:
|
|
197
|
+
|
|
198
|
+
1. `superuser` → bypasses all checks
|
|
199
|
+
2. `staff` → `user.is_staff`
|
|
200
|
+
3. `<group_name>` → first Django group
|
|
201
|
+
4. `authenticated` → logged in, no group
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
# settings.py
|
|
205
|
+
DJANGO_FLEX = {
|
|
206
|
+
'DEFAULT_LIMIT': 50,
|
|
207
|
+
'MAX_LIMIT': 200,
|
|
208
|
+
'MAX_RELATION_DEPTH': 2,
|
|
209
|
+
|
|
210
|
+
'PERMISSIONS': {
|
|
211
|
+
'entry': {
|
|
212
|
+
'exclude': ['internal_notes'],
|
|
213
|
+
|
|
214
|
+
'staff': {
|
|
215
|
+
'rows': lambda user: Q(),
|
|
216
|
+
'fields': ['*', 'blog.*'],
|
|
217
|
+
'filters': ['id', 'rating.gte', 'pub_date.gte', 'blog.id'],
|
|
218
|
+
'order_by': ['-pub_date', '-rating'],
|
|
219
|
+
'ops': ['get', 'query', 'create', 'update', 'delete'],
|
|
220
|
+
},
|
|
221
|
+
'authenticated': {
|
|
222
|
+
'rows': lambda user: Q(pub_date__lte=date.today()),
|
|
223
|
+
'fields': ['id', 'headline', 'pub_date', 'rating'],
|
|
224
|
+
'filters': ['id'],
|
|
225
|
+
'order_by': ['-pub_date'],
|
|
226
|
+
'ops': ['get', 'query'],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Usage Patterns
|
|
234
|
+
|
|
235
|
+
### Class-Based View
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from django_flex import FlexQueryView
|
|
239
|
+
|
|
240
|
+
class EntryQueryView(FlexQueryView):
|
|
241
|
+
model = Entry
|
|
242
|
+
flex_permissions = {...}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Decorator
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from django_flex import flex_query
|
|
249
|
+
|
|
250
|
+
@flex_query(
|
|
251
|
+
model=Entry,
|
|
252
|
+
allowed_fields=['id', 'headline', 'blog.name'],
|
|
253
|
+
allowed_filters=['id', 'headline.icontains'],
|
|
254
|
+
allowed_actions=['get', 'query'],
|
|
255
|
+
)
|
|
256
|
+
def entry_query(request, result, query_spec):
|
|
257
|
+
return JsonResponse(result.to_dict())
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Programmatic
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from django_flex import FlexQuery
|
|
264
|
+
|
|
265
|
+
result = FlexQuery(Entry).execute({
|
|
266
|
+
'fields': 'id, headline, blog.name',
|
|
267
|
+
'filters': {'rating.gte': 4},
|
|
268
|
+
}, user=request.user)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Supported Operators
|
|
272
|
+
|
|
273
|
+
| Category | Operators |
|
|
274
|
+
|----------|-----------|
|
|
275
|
+
| Comparison | `lt`, `lte`, `gt`, `gte`, `exact`, `in`, `isnull`, `range` |
|
|
276
|
+
| Text | `contains`, `icontains`, `startswith`, `endswith`, `regex` |
|
|
277
|
+
| Date/Time | `date`, `year`, `month`, `day`, `hour`, `minute`, `second` |
|
|
278
|
+
| Composition | `and`, `or`, `not` |
|
|
279
|
+
|
|
280
|
+
## Response Codes
|
|
281
|
+
|
|
282
|
+
| Code | Description |
|
|
283
|
+
|------|-------------|
|
|
284
|
+
| `FLEX_OK` | Single object retrieved |
|
|
285
|
+
| `FLEX_OK_QUERY` | Query results returned |
|
|
286
|
+
| `FLEX_LIMIT_CLAMPED` | Results returned, limit was reduced |
|
|
287
|
+
| `FLEX_NOT_FOUND` | Object not found |
|
|
288
|
+
| `FLEX_PERMISSION_DENIED` | Access denied |
|
|
289
|
+
| `FLEX_INVALID_FILTER` | Invalid filter syntax |
|
|
290
|
+
|
|
291
|
+
## Documentation
|
|
292
|
+
|
|
293
|
+
- [Installation Guide](https://github.com/n3h3m/django-flex/blob/main/docs/installation.md)
|
|
294
|
+
- [Quick Start](https://github.com/n3h3m/django-flex/blob/main/docs/quickstart.md)
|
|
295
|
+
- [Permissions Guide](https://github.com/n3h3m/django-flex/blob/main/docs/permissions.md)
|
|
296
|
+
- [API Reference](https://github.com/n3h3m/django-flex/blob/main/docs/api_reference.md)
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT License - see [LICENSE](https://github.com/n3h3m/django-flex/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Django-Flex
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/django-flex/)
|
|
4
|
+
[](https://pypi.org/project/django-flex/)
|
|
5
|
+
[](https://www.djangoproject.com/)
|
|
6
|
+
|
|
7
|
+
**A flexible query language for Django** — Enable frontends to dynamically construct database queries with built-in security.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔍 **Dynamic Field Selection** — Request only the fields you need
|
|
12
|
+
- 🔬 **Rich Filtering** — Full Django ORM operator support
|
|
13
|
+
- 📄 **Built-in Pagination** — Limit/offset with smart cursor support
|
|
14
|
+
- 🔐 **Layered Security** — Row, field, filter, and operation-level access control
|
|
15
|
+
- ⚡ **N+1 Prevention** — Automatic `select_related` optimization
|
|
16
|
+
- 🎯 **Django Native** — Uses Django's built-in auth (groups, permissions)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install django-flex
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# settings.py
|
|
26
|
+
INSTALLED_APPS = [
|
|
27
|
+
...
|
|
28
|
+
'django_flex',
|
|
29
|
+
]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
Using Django's official documentation models: Blog, Author, Entry.
|
|
35
|
+
|
|
36
|
+
### 1. Create a View
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# views.py
|
|
40
|
+
from datetime import date
|
|
41
|
+
from django.db.models import Q
|
|
42
|
+
from django_flex import FlexQueryView
|
|
43
|
+
from .models import Entry
|
|
44
|
+
|
|
45
|
+
class EntryQueryView(FlexQueryView):
|
|
46
|
+
model = Entry
|
|
47
|
+
|
|
48
|
+
flex_permissions = {
|
|
49
|
+
'staff': {
|
|
50
|
+
'rows': lambda user: Q(), # All entries
|
|
51
|
+
'fields': ['*', 'blog.*'],
|
|
52
|
+
'filters': ['id', 'rating.gte', 'blog.id', 'headline.icontains'],
|
|
53
|
+
'order_by': ['-pub_date', 'rating'],
|
|
54
|
+
'ops': ['get', 'query'],
|
|
55
|
+
},
|
|
56
|
+
'authenticated': {
|
|
57
|
+
'rows': lambda user: Q(pub_date__lte=date.today()),
|
|
58
|
+
'fields': ['id', 'headline', 'pub_date', 'blog.name'],
|
|
59
|
+
'filters': ['id', 'headline.icontains'],
|
|
60
|
+
'order_by': ['-pub_date'],
|
|
61
|
+
'ops': ['get', 'query'],
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Add URL Route
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# urls.py
|
|
70
|
+
from .views import EntryQueryView
|
|
71
|
+
|
|
72
|
+
urlpatterns = [
|
|
73
|
+
path('api/entries/', EntryQueryView.as_view()),
|
|
74
|
+
]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Query from Frontend
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
// Query entries
|
|
81
|
+
const response = await fetch('/api/entries/', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {'Content-Type': 'application/json'},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
fields: 'id, headline, pub_date, blog.name',
|
|
86
|
+
filters: { 'rating.gte': 4 },
|
|
87
|
+
order_by: '-pub_date',
|
|
88
|
+
limit: 20
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Response
|
|
93
|
+
{
|
|
94
|
+
"success": true,
|
|
95
|
+
"code": "FLEX_OK_QUERY",
|
|
96
|
+
"pagination": {"offset": 0, "limit": 20, "has_more": false},
|
|
97
|
+
"results": {
|
|
98
|
+
"1": {"id": 1, "headline": "Django 5.0 Released", "pub_date": "2024-01-15", "blog": {"name": "Django News"}},
|
|
99
|
+
"2": {"id": 2, "headline": "Getting Started Guide", "pub_date": "2024-01-14", "blog": {"name": "Tutorials"}}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Query Language
|
|
105
|
+
|
|
106
|
+
### Field Selection
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
fields: 'id, headline' // Specific fields
|
|
110
|
+
fields: '*' // All model fields
|
|
111
|
+
fields: '*, blog.*' // All fields + related
|
|
112
|
+
fields: 'id, blog.name, blog.tagline' // Nested fields
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Filtering
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
// Simple equality
|
|
119
|
+
filters: { rating: 5 }
|
|
120
|
+
|
|
121
|
+
// Operators
|
|
122
|
+
filters: { 'rating.gte': 4 } // Greater than or equal
|
|
123
|
+
filters: { 'rating.in': [4, 5] } // In list
|
|
124
|
+
filters: { 'headline.icontains': 'django' } // Case-insensitive search
|
|
125
|
+
filters: { 'pub_date.gte': '2024-01-01' } // Date comparison
|
|
126
|
+
|
|
127
|
+
// Composition
|
|
128
|
+
filters: { or: { rating: 5, 'number_of_comments.gte': 100 } }
|
|
129
|
+
filters: { not: { 'rating.lt': 3 } }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Pagination
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
limit: 20 // Max results
|
|
136
|
+
offset: 40 // Skip first 40
|
|
137
|
+
|
|
138
|
+
// Response includes next cursor
|
|
139
|
+
pagination: {
|
|
140
|
+
offset: 40,
|
|
141
|
+
limit: 20,
|
|
142
|
+
has_more: true,
|
|
143
|
+
next: { fields: '...', limit: 20, offset: 60 }
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Security Configuration
|
|
148
|
+
|
|
149
|
+
Django-Flex uses Django's built-in auth for role resolution:
|
|
150
|
+
|
|
151
|
+
1. `superuser` → bypasses all checks
|
|
152
|
+
2. `staff` → `user.is_staff`
|
|
153
|
+
3. `<group_name>` → first Django group
|
|
154
|
+
4. `authenticated` → logged in, no group
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# settings.py
|
|
158
|
+
DJANGO_FLEX = {
|
|
159
|
+
'DEFAULT_LIMIT': 50,
|
|
160
|
+
'MAX_LIMIT': 200,
|
|
161
|
+
'MAX_RELATION_DEPTH': 2,
|
|
162
|
+
|
|
163
|
+
'PERMISSIONS': {
|
|
164
|
+
'entry': {
|
|
165
|
+
'exclude': ['internal_notes'],
|
|
166
|
+
|
|
167
|
+
'staff': {
|
|
168
|
+
'rows': lambda user: Q(),
|
|
169
|
+
'fields': ['*', 'blog.*'],
|
|
170
|
+
'filters': ['id', 'rating.gte', 'pub_date.gte', 'blog.id'],
|
|
171
|
+
'order_by': ['-pub_date', '-rating'],
|
|
172
|
+
'ops': ['get', 'query', 'create', 'update', 'delete'],
|
|
173
|
+
},
|
|
174
|
+
'authenticated': {
|
|
175
|
+
'rows': lambda user: Q(pub_date__lte=date.today()),
|
|
176
|
+
'fields': ['id', 'headline', 'pub_date', 'rating'],
|
|
177
|
+
'filters': ['id'],
|
|
178
|
+
'order_by': ['-pub_date'],
|
|
179
|
+
'ops': ['get', 'query'],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Usage Patterns
|
|
187
|
+
|
|
188
|
+
### Class-Based View
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from django_flex import FlexQueryView
|
|
192
|
+
|
|
193
|
+
class EntryQueryView(FlexQueryView):
|
|
194
|
+
model = Entry
|
|
195
|
+
flex_permissions = {...}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Decorator
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from django_flex import flex_query
|
|
202
|
+
|
|
203
|
+
@flex_query(
|
|
204
|
+
model=Entry,
|
|
205
|
+
allowed_fields=['id', 'headline', 'blog.name'],
|
|
206
|
+
allowed_filters=['id', 'headline.icontains'],
|
|
207
|
+
allowed_actions=['get', 'query'],
|
|
208
|
+
)
|
|
209
|
+
def entry_query(request, result, query_spec):
|
|
210
|
+
return JsonResponse(result.to_dict())
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Programmatic
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from django_flex import FlexQuery
|
|
217
|
+
|
|
218
|
+
result = FlexQuery(Entry).execute({
|
|
219
|
+
'fields': 'id, headline, blog.name',
|
|
220
|
+
'filters': {'rating.gte': 4},
|
|
221
|
+
}, user=request.user)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Supported Operators
|
|
225
|
+
|
|
226
|
+
| Category | Operators |
|
|
227
|
+
|----------|-----------|
|
|
228
|
+
| Comparison | `lt`, `lte`, `gt`, `gte`, `exact`, `in`, `isnull`, `range` |
|
|
229
|
+
| Text | `contains`, `icontains`, `startswith`, `endswith`, `regex` |
|
|
230
|
+
| Date/Time | `date`, `year`, `month`, `day`, `hour`, `minute`, `second` |
|
|
231
|
+
| Composition | `and`, `or`, `not` |
|
|
232
|
+
|
|
233
|
+
## Response Codes
|
|
234
|
+
|
|
235
|
+
| Code | Description |
|
|
236
|
+
|------|-------------|
|
|
237
|
+
| `FLEX_OK` | Single object retrieved |
|
|
238
|
+
| `FLEX_OK_QUERY` | Query results returned |
|
|
239
|
+
| `FLEX_LIMIT_CLAMPED` | Results returned, limit was reduced |
|
|
240
|
+
| `FLEX_NOT_FOUND` | Object not found |
|
|
241
|
+
| `FLEX_PERMISSION_DENIED` | Access denied |
|
|
242
|
+
| `FLEX_INVALID_FILTER` | Invalid filter syntax |
|
|
243
|
+
|
|
244
|
+
## Documentation
|
|
245
|
+
|
|
246
|
+
- [Installation Guide](https://github.com/n3h3m/django-flex/blob/main/docs/installation.md)
|
|
247
|
+
- [Quick Start](https://github.com/n3h3m/django-flex/blob/main/docs/quickstart.md)
|
|
248
|
+
- [Permissions Guide](https://github.com/n3h3m/django-flex/blob/main/docs/permissions.md)
|
|
249
|
+
- [API Reference](https://github.com/n3h3m/django-flex/blob/main/docs/api_reference.md)
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
MIT License - see [LICENSE](https://github.com/n3h3m/django-flex/blob/main/LICENSE) for details.
|