bazis-author 2.2.0__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.
- bazis_author-2.2.0/.dockerignore +3 -0
- bazis_author-2.2.0/.github/workflows/build-publish.yml +16 -0
- bazis_author-2.2.0/.github/workflows/update-licenses.yml +18 -0
- bazis_author-2.2.0/.gitignore +44 -0
- bazis_author-2.2.0/PKG-INFO +589 -0
- bazis_author-2.2.0/README.md +563 -0
- bazis_author-2.2.0/bazis/contrib/author/__init__.py +19 -0
- bazis_author-2.2.0/bazis/contrib/author/_version.py +34 -0
- bazis_author-2.2.0/bazis/contrib/author/admin_abstract.py +53 -0
- bazis_author-2.2.0/bazis/contrib/author/apps.py +22 -0
- bazis_author-2.2.0/bazis/contrib/author/locale/en/LC_MESSAGES/django.mo +0 -0
- bazis_author-2.2.0/bazis/contrib/author/locale/en/LC_MESSAGES/django.po +22 -0
- bazis_author-2.2.0/bazis/contrib/author/locale/ru/LC_MESSAGES/django.mo +0 -0
- bazis_author-2.2.0/bazis/contrib/author/locale/ru/LC_MESSAGES/django.po +24 -0
- bazis_author-2.2.0/bazis/contrib/author/models_abstract.py +49 -0
- bazis_author-2.2.0/bazis/contrib/author/routes_abstract.py +50 -0
- bazis_author-2.2.0/bazis_author.egg-info/PKG-INFO +589 -0
- bazis_author-2.2.0/bazis_author.egg-info/SOURCES.txt +56 -0
- bazis_author-2.2.0/bazis_author.egg-info/dependency_links.txt +1 -0
- bazis_author-2.2.0/bazis_author.egg-info/requires.txt +8 -0
- bazis_author-2.2.0/bazis_author.egg-info/top_level.txt +1 -0
- bazis_author-2.2.0/pyproject.toml +94 -0
- bazis_author-2.2.0/pytest.ini +4 -0
- bazis_author-2.2.0/sample/deploy/admin.sh +4 -0
- bazis_author-2.2.0/sample/deploy/app.sh +5 -0
- bazis_author-2.2.0/sample/deploy/config/admin.py +30 -0
- bazis_author-2.2.0/sample/deploy/config/app.py +32 -0
- bazis_author-2.2.0/sample/deploy/db.sh +78 -0
- bazis_author-2.2.0/sample/deploy/init.sh +40 -0
- bazis_author-2.2.0/sample/entity/__init__.py +14 -0
- bazis_author-2.2.0/sample/entity/admin.py +64 -0
- bazis_author-2.2.0/sample/entity/migrations/0001_initial.py +110 -0
- bazis_author-2.2.0/sample/entity/migrations/0002_initial.py +87 -0
- bazis_author-2.2.0/sample/entity/migrations/0003_alter_childentity_dt_created_and_more.py +67 -0
- bazis_author-2.2.0/sample/entity/migrations/__init__.py +14 -0
- bazis_author-2.2.0/sample/entity/models.py +64 -0
- bazis_author-2.2.0/sample/entity/router.py +24 -0
- bazis_author-2.2.0/sample/entity/routes.py +49 -0
- bazis_author-2.2.0/sample/manage.py +35 -0
- bazis_author-2.2.0/sample/project.env +11 -0
- bazis_author-2.2.0/sample/pyproject.toml +14 -0
- bazis_author-2.2.0/sample/sample/__init__.py +14 -0
- bazis_author-2.2.0/sample/sample/main.py +24 -0
- bazis_author-2.2.0/sample/sample/router.py +19 -0
- bazis_author-2.2.0/sample/sample/settings.py +15 -0
- bazis_author-2.2.0/sample/sample/urls.py +29 -0
- bazis_author-2.2.0/sample/sample/wsgi.py +22 -0
- bazis_author-2.2.0/sample/users/__init__.py +14 -0
- bazis_author-2.2.0/sample/users/migrations/0001_initial.py +63 -0
- bazis_author-2.2.0/sample/users/migrations/0002_user_dt_first_login.py +34 -0
- bazis_author-2.2.0/sample/users/migrations/__init__.py +14 -0
- bazis_author-2.2.0/sample/users/models.py +24 -0
- bazis_author-2.2.0/sample/uv.lock +1329 -0
- bazis_author-2.2.0/setup.cfg +4 -0
- bazis_author-2.2.0/tests/__init__.py +14 -0
- bazis_author-2.2.0/tests/conftest.py +30 -0
- bazis_author-2.2.0/tests/factories.py +49 -0
- bazis_author-2.2.0/tests/test_author.py +159 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Build and Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*.*.*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
uses: ecofuture-tech/.github/.github/workflows/reusable-build-publish.yml@main
|
|
14
|
+
secrets:
|
|
15
|
+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
16
|
+
TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Update Licenses
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- '**.py'
|
|
9
|
+
- 'pyproject.toml'
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
update-licenses:
|
|
16
|
+
uses: ecofuture-tech/.github/.github/workflows/reusable-update-licenses.yml@main
|
|
17
|
+
secrets:
|
|
18
|
+
WORKFLOW_LICENSE_TOKEN: ${{ secrets.WORKFLOW_LICENSE_TOKEN }}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.idea
|
|
2
|
+
.ipynb_checkpoints
|
|
3
|
+
.mypy_cache
|
|
4
|
+
.vscode
|
|
5
|
+
__pycache__
|
|
6
|
+
.pytest_cache
|
|
7
|
+
htmlcov
|
|
8
|
+
dist
|
|
9
|
+
site
|
|
10
|
+
.coverage
|
|
11
|
+
coverage.xml
|
|
12
|
+
.netlify
|
|
13
|
+
test.db
|
|
14
|
+
log.txt
|
|
15
|
+
Pipfile.lock
|
|
16
|
+
env3.*
|
|
17
|
+
env
|
|
18
|
+
docs_build
|
|
19
|
+
venv
|
|
20
|
+
.venv
|
|
21
|
+
docs.zip
|
|
22
|
+
archive.zip
|
|
23
|
+
|
|
24
|
+
.env
|
|
25
|
+
local.env
|
|
26
|
+
*/.env*
|
|
27
|
+
|
|
28
|
+
# vim temporary files
|
|
29
|
+
*~
|
|
30
|
+
.*.sw?
|
|
31
|
+
*.DS_Store
|
|
32
|
+
|
|
33
|
+
*.egg-info
|
|
34
|
+
build
|
|
35
|
+
dist
|
|
36
|
+
|
|
37
|
+
docs/_autosummary
|
|
38
|
+
docs/_build
|
|
39
|
+
*.DS_Store
|
|
40
|
+
*.log
|
|
41
|
+
my-*
|
|
42
|
+
My*
|
|
43
|
+
*/**/_version.py
|
|
44
|
+
/uv.lock
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bazis-author
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Author management module for Bazis framework.
|
|
5
|
+
Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
6
|
+
Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
7
|
+
Project-URL: Home, https://github.com/ecofuture-tech/bazis-author
|
|
8
|
+
Keywords: bazis,django,fastapi,pydantic,framework,jsonapi,author
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: FastAPI
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: bazis
|
|
21
|
+
Requires-Dist: bazis-users
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: bazis-test-utils; extra == "test"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# Bazis Author
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/bazis-author/)
|
|
30
|
+
[](https://pypi.org/project/bazis-author/)
|
|
31
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
32
|
+
|
|
33
|
+
An extension package for Bazis that provides automatic authorship tracking for models (who created and who updated records).
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Install the package
|
|
39
|
+
uv add bazis-author
|
|
40
|
+
|
|
41
|
+
# Create model with authorship tracking
|
|
42
|
+
from bazis.contrib.author.models_abstract import AuthorMixin
|
|
43
|
+
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
|
|
44
|
+
from django.db import models
|
|
45
|
+
|
|
46
|
+
class Article(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
47
|
+
"""Article with author tracking"""
|
|
48
|
+
title = models.CharField(max_length=255)
|
|
49
|
+
content = models.TextField()
|
|
50
|
+
|
|
51
|
+
class Meta:
|
|
52
|
+
verbose_name = 'Article'
|
|
53
|
+
verbose_name_plural = 'Articles'
|
|
54
|
+
|
|
55
|
+
# Create route with automatic author population
|
|
56
|
+
from bazis.contrib.author.routes_abstract import AuthorRouteBase
|
|
57
|
+
from django.apps import apps
|
|
58
|
+
|
|
59
|
+
class ArticleRouteSet(AuthorRouteBase):
|
|
60
|
+
model = apps.get_model('myapp.Article')
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Table of Contents
|
|
64
|
+
|
|
65
|
+
- [Description](#description)
|
|
66
|
+
- [Requirements](#requirements)
|
|
67
|
+
- [Installation](#installation)
|
|
68
|
+
- [Core Components](#core-components)
|
|
69
|
+
- [Model Mixins](#model-mixins)
|
|
70
|
+
- [Base Routes](#base-routes)
|
|
71
|
+
- [Admin Mixins](#admin-mixins)
|
|
72
|
+
- [Usage](#usage)
|
|
73
|
+
- [Creating Model with Authorship](#creating-model-with-authorship)
|
|
74
|
+
- [Creating Route with Authorship](#creating-route-with-authorship)
|
|
75
|
+
- [Admin Setup](#admin-setup)
|
|
76
|
+
- [How It Works](#how-it-works)
|
|
77
|
+
- [Examples](#examples)
|
|
78
|
+
- [License](#license)
|
|
79
|
+
- [Links](#links)
|
|
80
|
+
|
|
81
|
+
## Description
|
|
82
|
+
|
|
83
|
+
**Bazis Author** is an extension package for the Bazis framework that provides automatic authorship tracking for records. The package includes:
|
|
84
|
+
|
|
85
|
+
- **AuthorMixin** — mixin for models adding `author` (who created) and `author_updated` (who updated) fields
|
|
86
|
+
- **AuthorRouteBase** and **AuthorRequiredRouteBase** — base route classes with automatic author field population
|
|
87
|
+
- **AuthorAdminMixin** — admin mixin with convenient filters and readonly fields
|
|
88
|
+
|
|
89
|
+
The main advantage of the package is fully automatic author field population without the need to write additional code.
|
|
90
|
+
|
|
91
|
+
**This package requires `bazis` and `bazis-users` packages to be installed.**
|
|
92
|
+
|
|
93
|
+
## Requirements
|
|
94
|
+
|
|
95
|
+
- **Python**: 3.12+
|
|
96
|
+
- **bazis**: latest version
|
|
97
|
+
- **bazis-users**: latest version
|
|
98
|
+
- **PostgreSQL**: 12+
|
|
99
|
+
- **Redis**: For caching
|
|
100
|
+
|
|
101
|
+
## Installation
|
|
102
|
+
|
|
103
|
+
### Using uv (recommended)
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uv add bazis-author
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Using pip
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install bazis-author
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Core Components
|
|
116
|
+
|
|
117
|
+
### Model Mixins
|
|
118
|
+
|
|
119
|
+
#### AuthorMixin
|
|
120
|
+
|
|
121
|
+
Mixin for models adding authorship tracking fields.
|
|
122
|
+
|
|
123
|
+
**Location**: `bazis.contrib.author.models_abstract.AuthorMixin`
|
|
124
|
+
|
|
125
|
+
**Added fields**:
|
|
126
|
+
|
|
127
|
+
- `author` — Foreign Key to user who created the record (nullable)
|
|
128
|
+
- `author_updated` — Foreign Key to user who last updated the record (nullable)
|
|
129
|
+
|
|
130
|
+
**Features**:
|
|
131
|
+
|
|
132
|
+
- Inherits from `UserMixin`, has access to user context via `CTX_USER_REQUEST`
|
|
133
|
+
- Automatically populates `author` when creating record
|
|
134
|
+
- Automatically updates `author_updated` on every save
|
|
135
|
+
- Fields are optional (nullable), so records can be created without a user
|
|
136
|
+
|
|
137
|
+
**Example usage**:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from bazis.contrib.author.models_abstract import AuthorMixin
|
|
141
|
+
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
|
|
142
|
+
from django.db import models
|
|
143
|
+
|
|
144
|
+
class Document(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
145
|
+
"""Document with authorship tracking"""
|
|
146
|
+
title = models.CharField('Title', max_length=255)
|
|
147
|
+
content = models.TextField('Content')
|
|
148
|
+
|
|
149
|
+
class Meta:
|
|
150
|
+
verbose_name = 'Document'
|
|
151
|
+
verbose_name_plural = 'Documents'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Base Routes
|
|
155
|
+
|
|
156
|
+
#### AuthorRouteMixin
|
|
157
|
+
|
|
158
|
+
Base mixin for routes that automatically populate author fields.
|
|
159
|
+
|
|
160
|
+
**Location**: `bazis.contrib.author.routes_abstract.AuthorRouteMixin`
|
|
161
|
+
|
|
162
|
+
**Features**:
|
|
163
|
+
|
|
164
|
+
- Excludes `author` and `author_updated` fields from CREATE and UPDATE schemas (they are populated automatically)
|
|
165
|
+
- Uses `hook_before_create` and `hook_before_update` hooks to set author
|
|
166
|
+
- Sets author only for authenticated users
|
|
167
|
+
|
|
168
|
+
**Methods**:
|
|
169
|
+
|
|
170
|
+
- `hook_before_create(item)` — sets `author` before creating record
|
|
171
|
+
- `hook_before_update(item)` — sets `author_updated` before updating record
|
|
172
|
+
|
|
173
|
+
#### AuthorRouteBase
|
|
174
|
+
|
|
175
|
+
Base route class with authorship support for optional authentication.
|
|
176
|
+
|
|
177
|
+
**Location**: `bazis.contrib.author.routes_abstract.AuthorRouteBase`
|
|
178
|
+
|
|
179
|
+
**Inheritance**: `AuthorRouteMixin` + `UserRouteBase`
|
|
180
|
+
|
|
181
|
+
Use this class when endpoints are accessible to both authenticated and anonymous users.
|
|
182
|
+
|
|
183
|
+
**Example usage**:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from bazis.contrib.author.routes_abstract import AuthorRouteBase
|
|
187
|
+
from django.apps import apps
|
|
188
|
+
|
|
189
|
+
class ArticleRouteSet(AuthorRouteBase):
|
|
190
|
+
"""Route for articles with automatic authorship"""
|
|
191
|
+
model = apps.get_model('myapp.Article')
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### AuthorRequiredRouteBase
|
|
195
|
+
|
|
196
|
+
Base route class with authorship support for required authentication.
|
|
197
|
+
|
|
198
|
+
**Location**: `bazis.contrib.author.routes_abstract.AuthorRequiredRouteBase`
|
|
199
|
+
|
|
200
|
+
**Inheritance**: `AuthorRouteMixin` + `UserRequiredRouteBase`
|
|
201
|
+
|
|
202
|
+
Use this class when endpoints require mandatory authentication (return 401 for unauthenticated requests).
|
|
203
|
+
|
|
204
|
+
**Example usage**:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from bazis.contrib.author.routes_abstract import AuthorRequiredRouteBase
|
|
208
|
+
from django.apps import apps
|
|
209
|
+
|
|
210
|
+
class PrivateDocumentRouteSet(AuthorRequiredRouteBase):
|
|
211
|
+
"""Route for private documents (authenticated only)"""
|
|
212
|
+
model = apps.get_model('myapp.PrivateDocument')
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Admin Mixins
|
|
216
|
+
|
|
217
|
+
#### AuthorAdminMixin
|
|
218
|
+
|
|
219
|
+
Mixin for Django admin classes adding convenient work with author fields.
|
|
220
|
+
|
|
221
|
+
**Location**: `bazis.contrib.author.admin_abstract.AuthorAdminMixin`
|
|
222
|
+
|
|
223
|
+
**Capabilities**:
|
|
224
|
+
|
|
225
|
+
- Adds `author__username` to search fields
|
|
226
|
+
- Makes `author` and `author_updated` fields readonly
|
|
227
|
+
- Adds autocomplete filter by author to list view
|
|
228
|
+
- Automatically populates author fields when saving through admin
|
|
229
|
+
|
|
230
|
+
**Example usage**:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from django.contrib import admin
|
|
234
|
+
from bazis.contrib.author.admin_abstract import AuthorAdminMixin
|
|
235
|
+
from bazis.core.admin_abstract import DtAdminMixin
|
|
236
|
+
from .models import Article
|
|
237
|
+
|
|
238
|
+
@admin.register(Article)
|
|
239
|
+
class ArticleAdmin(AuthorAdminMixin, DtAdminMixin, admin.ModelAdmin):
|
|
240
|
+
list_display = ('id', 'title', 'author', 'author_updated', 'dt_created')
|
|
241
|
+
search_fields = ('title', 'content')
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Usage
|
|
245
|
+
|
|
246
|
+
### Creating Model with Authorship
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from bazis.contrib.author.models_abstract import AuthorMixin
|
|
250
|
+
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
|
|
251
|
+
from django.db import models
|
|
252
|
+
|
|
253
|
+
class Article(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
254
|
+
"""Article with automatic author tracking"""
|
|
255
|
+
title = models.CharField('Title', max_length=255)
|
|
256
|
+
content = models.TextField('Content')
|
|
257
|
+
is_published = models.BooleanField('Published', default=False)
|
|
258
|
+
|
|
259
|
+
class Meta:
|
|
260
|
+
verbose_name = 'Article'
|
|
261
|
+
verbose_name_plural = 'Articles'
|
|
262
|
+
|
|
263
|
+
def __str__(self):
|
|
264
|
+
return self.title
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Creating Route with Authorship
|
|
268
|
+
|
|
269
|
+
#### For public endpoints (optional authentication)
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from bazis.contrib.author.routes_abstract import AuthorRouteBase
|
|
273
|
+
from django.apps import apps
|
|
274
|
+
|
|
275
|
+
class ArticleRouteSet(AuthorRouteBase):
|
|
276
|
+
"""
|
|
277
|
+
Articles can be created by anonymous users.
|
|
278
|
+
If user is authenticated - author fields will be populated automatically.
|
|
279
|
+
"""
|
|
280
|
+
model = apps.get_model('myapp.Article')
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Behavior**:
|
|
284
|
+
- Anonymous user creates record → `author` and `author_updated` remain `null`
|
|
285
|
+
- Authenticated user creates record → `author` is automatically populated
|
|
286
|
+
- Any user updates record → `author_updated` is populated if user is authenticated
|
|
287
|
+
|
|
288
|
+
#### For private endpoints (required authentication)
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from bazis.contrib.author.routes_abstract import AuthorRequiredRouteBase
|
|
292
|
+
from django.apps import apps
|
|
293
|
+
|
|
294
|
+
class PrivateDocumentRouteSet(AuthorRequiredRouteBase):
|
|
295
|
+
"""
|
|
296
|
+
Documents are accessible only to authenticated users.
|
|
297
|
+
Anonymous users will get 401.
|
|
298
|
+
"""
|
|
299
|
+
model = apps.get_model('myapp.PrivateDocument')
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Behavior**:
|
|
303
|
+
- Anonymous user tries to create/update record → 401 Unauthorized
|
|
304
|
+
- Authenticated user creates record → `author` is automatically populated
|
|
305
|
+
- Authenticated user updates record → `author_updated` is automatically updated
|
|
306
|
+
|
|
307
|
+
### Admin Setup
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from django.contrib import admin
|
|
311
|
+
from bazis.contrib.author.admin_abstract import AuthorAdminMixin
|
|
312
|
+
from bazis.core.admin_abstract import DtAdminMixin
|
|
313
|
+
from .models import Article
|
|
314
|
+
|
|
315
|
+
@admin.register(Article)
|
|
316
|
+
class ArticleAdmin(AuthorAdminMixin, DtAdminMixin, admin.ModelAdmin):
|
|
317
|
+
list_display = ('id', 'title', 'author', 'author_updated', 'is_published', 'dt_created')
|
|
318
|
+
search_fields = ('title', 'content')
|
|
319
|
+
list_filter = ('is_published',)
|
|
320
|
+
readonly_fields = ('dt_created', 'dt_updated')
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**What AuthorAdminMixin provides**:
|
|
324
|
+
|
|
325
|
+
1. **Autocomplete filter by author** in list view
|
|
326
|
+
2. **Search by author username** (`author__username`)
|
|
327
|
+
3. **Readonly fields** `author` and `author_updated` (cannot be edited)
|
|
328
|
+
4. **Automatic population** when saving through admin
|
|
329
|
+
|
|
330
|
+
## How It Works
|
|
331
|
+
|
|
332
|
+
### In Models
|
|
333
|
+
|
|
334
|
+
When calling `save()` on a model with `AuthorMixin`:
|
|
335
|
+
|
|
336
|
+
1. Check for user presence in `CTX_USER_REQUEST` context
|
|
337
|
+
2. If user exists:
|
|
338
|
+
- When creating record (`author` is empty) → set `author`
|
|
339
|
+
- When updating record → update `author_updated`
|
|
340
|
+
3. Call `super().save()` to save
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
def save(self, *args, **kwargs):
|
|
344
|
+
if author := self.CTX_USER_REQUEST.get():
|
|
345
|
+
if not self.author:
|
|
346
|
+
self.author = author
|
|
347
|
+
self.author_updated = author
|
|
348
|
+
super().save(*args, **kwargs)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### In Routes
|
|
352
|
+
|
|
353
|
+
When creating/updating record via API:
|
|
354
|
+
|
|
355
|
+
1. **In CREATE**: `hook_before_create(item)` is triggered
|
|
356
|
+
- If user is NOT anonymous → set `item.author`
|
|
357
|
+
|
|
358
|
+
2. **In UPDATE**: `hook_before_update(item)` is triggered
|
|
359
|
+
- If user is NOT anonymous → set `item.author_updated`
|
|
360
|
+
|
|
361
|
+
3. Fields `author` and `author_updated` are **excluded** from CREATE/UPDATE schemas, so client cannot pass them
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
fields: dict[ApiAction, SchemaFields] = {
|
|
365
|
+
CrudApiAction.CREATE: SchemaFields(
|
|
366
|
+
exclude={'author': None, 'author_updated': None},
|
|
367
|
+
),
|
|
368
|
+
CrudApiAction.UPDATE: SchemaFields(
|
|
369
|
+
exclude={'author': None, 'author_updated': None},
|
|
370
|
+
),
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### In Admin
|
|
375
|
+
|
|
376
|
+
When saving through Django Admin:
|
|
377
|
+
|
|
378
|
+
1. `save_model()` — sets author from `request.user`
|
|
379
|
+
2. `save_formset()` — sets author for inline models
|
|
380
|
+
|
|
381
|
+
## Examples
|
|
382
|
+
|
|
383
|
+
### Complete Application Example
|
|
384
|
+
|
|
385
|
+
**models.py**:
|
|
386
|
+
```python
|
|
387
|
+
from bazis.contrib.author.models_abstract import AuthorMixin
|
|
388
|
+
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
|
|
389
|
+
from django.db import models
|
|
390
|
+
|
|
391
|
+
class Article(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
392
|
+
"""Public article"""
|
|
393
|
+
title = models.CharField('Title', max_length=255)
|
|
394
|
+
content = models.TextField('Content')
|
|
395
|
+
is_published = models.BooleanField('Published', default=False)
|
|
396
|
+
|
|
397
|
+
class Meta:
|
|
398
|
+
verbose_name = 'Article'
|
|
399
|
+
verbose_name_plural = 'Articles'
|
|
400
|
+
|
|
401
|
+
class PrivateNote(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
402
|
+
"""Private note (authenticated only)"""
|
|
403
|
+
title = models.CharField('Title', max_length=255)
|
|
404
|
+
content = models.TextField('Content')
|
|
405
|
+
|
|
406
|
+
class Meta:
|
|
407
|
+
verbose_name = 'Note'
|
|
408
|
+
verbose_name_plural = 'Notes'
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**routes.py**:
|
|
412
|
+
```python
|
|
413
|
+
from bazis.contrib.author.routes_abstract import AuthorRouteBase, AuthorRequiredRouteBase
|
|
414
|
+
from django.apps import apps
|
|
415
|
+
|
|
416
|
+
class ArticleRouteSet(AuthorRouteBase):
|
|
417
|
+
"""Articles accessible to everyone, but author is tracked if user is authenticated"""
|
|
418
|
+
model = apps.get_model('myapp.Article')
|
|
419
|
+
|
|
420
|
+
class PrivateNoteRouteSet(AuthorRequiredRouteBase):
|
|
421
|
+
"""Notes accessible only to authenticated users"""
|
|
422
|
+
model = apps.get_model('myapp.PrivateNote')
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**router.py**:
|
|
426
|
+
```python
|
|
427
|
+
from bazis.core.routing import BazisRouter
|
|
428
|
+
from . import routes
|
|
429
|
+
|
|
430
|
+
router = BazisRouter(tags=['Content'])
|
|
431
|
+
router.register(routes.ArticleRouteSet.as_router())
|
|
432
|
+
router.register(routes.PrivateNoteRouteSet.as_router())
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**admin.py**:
|
|
436
|
+
```python
|
|
437
|
+
from django.contrib import admin
|
|
438
|
+
from bazis.contrib.author.admin_abstract import AuthorAdminMixin
|
|
439
|
+
from bazis.core.admin_abstract import DtAdminMixin
|
|
440
|
+
from .models import Article, PrivateNote
|
|
441
|
+
|
|
442
|
+
@admin.register(Article)
|
|
443
|
+
class ArticleAdmin(AuthorAdminMixin, DtAdminMixin, admin.ModelAdmin):
|
|
444
|
+
list_display = ('id', 'title', 'author', 'is_published', 'dt_created')
|
|
445
|
+
search_fields = ('title', 'content')
|
|
446
|
+
list_filter = ('is_published',)
|
|
447
|
+
|
|
448
|
+
@admin.register(PrivateNote)
|
|
449
|
+
class PrivateNoteAdmin(AuthorAdminMixin, DtAdminMixin, admin.ModelAdmin):
|
|
450
|
+
list_display = ('id', 'title', 'author', 'dt_created')
|
|
451
|
+
search_fields = ('title', 'content')
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### API Usage Examples
|
|
455
|
+
|
|
456
|
+
**Creating article by anonymous user**:
|
|
457
|
+
```bash
|
|
458
|
+
POST /api/v1/content/article/
|
|
459
|
+
Content-Type: application/json
|
|
460
|
+
|
|
461
|
+
{
|
|
462
|
+
"data": {
|
|
463
|
+
"type": "myapp.article",
|
|
464
|
+
"attributes": {
|
|
465
|
+
"title": "My Article",
|
|
466
|
+
"content": "Article content",
|
|
467
|
+
"is_published": true
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# Response: 201 Created
|
|
473
|
+
# author and author_updated will be null
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Creating article by authenticated user**:
|
|
477
|
+
```bash
|
|
478
|
+
POST /api/v1/content/article/
|
|
479
|
+
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
480
|
+
Content-Type: application/json
|
|
481
|
+
|
|
482
|
+
{
|
|
483
|
+
"data": {
|
|
484
|
+
"type": "myapp.article",
|
|
485
|
+
"attributes": {
|
|
486
|
+
"title": "My Article",
|
|
487
|
+
"content": "Article content",
|
|
488
|
+
"is_published": true
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# Response: 201 Created
|
|
494
|
+
# author is automatically set to current user
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Attempting to create private note without authentication**:
|
|
498
|
+
```bash
|
|
499
|
+
POST /api/v1/content/private_note/
|
|
500
|
+
Content-Type: application/json
|
|
501
|
+
|
|
502
|
+
{
|
|
503
|
+
"data": {
|
|
504
|
+
"type": "myapp.privatenote",
|
|
505
|
+
"attributes": {
|
|
506
|
+
"title": "Secret Note",
|
|
507
|
+
"content": "Secret content"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# Response: 401 Unauthorized
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Creating private note with authentication**:
|
|
516
|
+
```bash
|
|
517
|
+
POST /api/v1/content/private_note/
|
|
518
|
+
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
519
|
+
Content-Type: application/json
|
|
520
|
+
|
|
521
|
+
{
|
|
522
|
+
"data": {
|
|
523
|
+
"type": "myapp.privatenote",
|
|
524
|
+
"attributes": {
|
|
525
|
+
"title": "Secret Note",
|
|
526
|
+
"content": "Secret content"
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
# Response: 201 Created
|
|
532
|
+
# author is automatically set to current user
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Authorship Check in Responses
|
|
536
|
+
|
|
537
|
+
Fields `author` and `author_updated` are returned in the `relationships` block:
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
{
|
|
541
|
+
"data": {
|
|
542
|
+
"type": "myapp.article",
|
|
543
|
+
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
544
|
+
"attributes": {
|
|
545
|
+
"title": "My Article",
|
|
546
|
+
"content": "Article content",
|
|
547
|
+
"is_published": true
|
|
548
|
+
},
|
|
549
|
+
"relationships": {
|
|
550
|
+
"author": {
|
|
551
|
+
"data": {
|
|
552
|
+
"type": "users.user",
|
|
553
|
+
"id": "456e7890-e89b-12d3-a456-426614174000"
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
"author_updated": {
|
|
557
|
+
"data": {
|
|
558
|
+
"type": "users.user",
|
|
559
|
+
"id": "456e7890-e89b-12d3-a456-426614174000"
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## License
|
|
568
|
+
|
|
569
|
+
Apache License 2.0
|
|
570
|
+
|
|
571
|
+
See [LICENSE](LICENSE) file for details.
|
|
572
|
+
|
|
573
|
+
## Links
|
|
574
|
+
|
|
575
|
+
- [Bazis Documentation](https://github.com/ecofuture-tech/bazis) — main repository
|
|
576
|
+
- [Bazis Users](https://github.com/ecofuture-tech/bazis-users) — user management package
|
|
577
|
+
- [Bazis Author Repository](https://github.com/ecofuture-tech/bazis-author) — package repository
|
|
578
|
+
- [Issue Tracker](https://github.com/ecofuture-tech/bazis-author/issues) — report bugs or request features
|
|
579
|
+
|
|
580
|
+
## Support
|
|
581
|
+
|
|
582
|
+
If you have questions or issues:
|
|
583
|
+
- Check the [Bazis documentation](https://github.com/ecofuture-tech/bazis)
|
|
584
|
+
- Search [existing issues](https://github.com/ecofuture-tech/bazis-author/issues)
|
|
585
|
+
- Create a [new issue](https://github.com/ecofuture-tech/bazis-author/issues/new) with detailed information
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
Made with ❤️ by the Bazis team
|