fc-django-post-api 2026.6.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.
- fc_django_post_api-2026.6.0/LICENSE +21 -0
- fc_django_post_api-2026.6.0/MANIFEST.in +3 -0
- fc_django_post_api-2026.6.0/PKG-INFO +67 -0
- fc_django_post_api-2026.6.0/README.md +48 -0
- fc_django_post_api-2026.6.0/pyproject.toml +27 -0
- fc_django_post_api-2026.6.0/setup.cfg +4 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/__init__.py +9 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/admin.py +64 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/admin_site.py +192 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/apps.py +17 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/models.py +2 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/permissions.py +24 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/serializers.py +127 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/tests.py +442 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/urls.py +54 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api/views.py +176 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api.egg-info/PKG-INFO +67 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api.egg-info/SOURCES.txt +19 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api.egg-info/dependency_links.txt +1 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api.egg-info/requires.txt +8 -0
- fc_django_post_api-2026.6.0/src/fc_django_post_api.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Terry Chan
|
|
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,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fc-django-post-api
|
|
3
|
+
Version: 2026.6.0
|
|
4
|
+
Summary: Django REST API package extracted from fc_django_crazypowertools
|
|
5
|
+
Author: DBox AI
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: Django<4.0,>=3.2
|
|
11
|
+
Requires-Dist: djangorestframework<3.15,>=3.13
|
|
12
|
+
Requires-Dist: djangorestframework-simplejwt<6.0,>=4.8
|
|
13
|
+
Requires-Dist: django-filter<23.0,>=21.0
|
|
14
|
+
Requires-Dist: djoser>=2.0
|
|
15
|
+
Requires-Dist: django-solo<2.4,>=2.2
|
|
16
|
+
Requires-Dist: django-hitcount<2.0,>=1.3.5
|
|
17
|
+
Requires-Dist: django-tbase-post-product<2027.0.0,>=2026.3.1617736733
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# fc-django-post-api
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/fc-django-post-api/)
|
|
23
|
+
|
|
24
|
+
Django REST API package extracted from [fc_django_crazypowertools](https://github.com/napoler/fc_django_crazypowertools).
|
|
25
|
+
|
|
26
|
+
> **Status:** experimental — first PyPI release (2026.6.0).
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install fc-django-post-api
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configure
|
|
35
|
+
|
|
36
|
+
Add to `INSTALLED_APPS` in your Django settings:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
INSTALLED_APPS = [
|
|
40
|
+
...,
|
|
41
|
+
"fc_django_post_api",
|
|
42
|
+
]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The `AppConfig` declares `label = "api"`, so the Django app label remains `api` even though the Python import is `fc_django_post_api`. Database tables, migrations, and `reverse()` lookups are unaffected.
|
|
46
|
+
|
|
47
|
+
## Mount URLs
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# urls.py
|
|
51
|
+
urlpatterns = [
|
|
52
|
+
path("api/", include("fc_django_post_api.urls")),
|
|
53
|
+
...,
|
|
54
|
+
]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Endpoints
|
|
58
|
+
|
|
59
|
+
- `GET /api/posts/` — PostViewSet (DRF)
|
|
60
|
+
- `GET /api/health/` — HealthView
|
|
61
|
+
- `GET /api/me/` — MeView
|
|
62
|
+
- `POST /api/auth/token/` — JWT obtain
|
|
63
|
+
- `POST /api/auth/token/refresh/` — JWT refresh
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# fc-django-post-api
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fc-django-post-api/)
|
|
4
|
+
|
|
5
|
+
Django REST API package extracted from [fc_django_crazypowertools](https://github.com/napoler/fc_django_crazypowertools).
|
|
6
|
+
|
|
7
|
+
> **Status:** experimental — first PyPI release (2026.6.0).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install fc-django-post-api
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configure
|
|
16
|
+
|
|
17
|
+
Add to `INSTALLED_APPS` in your Django settings:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
INSTALLED_APPS = [
|
|
21
|
+
...,
|
|
22
|
+
"fc_django_post_api",
|
|
23
|
+
]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The `AppConfig` declares `label = "api"`, so the Django app label remains `api` even though the Python import is `fc_django_post_api`. Database tables, migrations, and `reverse()` lookups are unaffected.
|
|
27
|
+
|
|
28
|
+
## Mount URLs
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# urls.py
|
|
32
|
+
urlpatterns = [
|
|
33
|
+
path("api/", include("fc_django_post_api.urls")),
|
|
34
|
+
...,
|
|
35
|
+
]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Endpoints
|
|
39
|
+
|
|
40
|
+
- `GET /api/posts/` — PostViewSet (DRF)
|
|
41
|
+
- `GET /api/health/` — HealthView
|
|
42
|
+
- `GET /api/me/` — MeView
|
|
43
|
+
- `POST /api/auth/token/` — JWT obtain
|
|
44
|
+
- `POST /api/auth/token/refresh/` — JWT refresh
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fc-django-post-api"
|
|
7
|
+
version = "2026.6.0"
|
|
8
|
+
description = "Django REST API package extracted from fc_django_crazypowertools"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "DBox AI"}
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"Django>=3.2,<4.0",
|
|
17
|
+
"djangorestframework>=3.13,<3.15",
|
|
18
|
+
"djangorestframework-simplejwt>=4.8,<6.0",
|
|
19
|
+
"django-filter>=21.0,<23.0",
|
|
20
|
+
"djoser>=2.0",
|
|
21
|
+
"django-solo>=2.2,<2.4", # 兼容 Django 3.2 (2.4+ 要求 Django>=4.2;2.0/2.1 有 model signal bug)
|
|
22
|
+
"django-hitcount>=1.3.5,<2.0", # 兼容 Django 3.2 (1.3.0-1.3.4 有 import path bug)
|
|
23
|
+
"django-tbase-post-product>=2026.3.1617736733,<2027.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
3
|
+
except ImportError: # pragma: no cover
|
|
4
|
+
from importlib_metadata import version, PackageNotFoundError # type: ignore
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("fc-django-post-api")
|
|
8
|
+
except PackageNotFoundError: # 源码 checkout 场景
|
|
9
|
+
__version__ = "2026.6.0"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.utils.html import format_html
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken, BlacklistedToken
|
|
5
|
+
|
|
6
|
+
# Unregister the default admin classes first
|
|
7
|
+
admin.site.unregister(OutstandingToken)
|
|
8
|
+
admin.site.unregister(BlacklistedToken)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@admin.register(OutstandingToken)
|
|
12
|
+
class OutstandingTokenAdmin(admin.ModelAdmin):
|
|
13
|
+
"""Admin interface for viewing and managing JWT tokens."""
|
|
14
|
+
|
|
15
|
+
list_display = [
|
|
16
|
+
"id",
|
|
17
|
+
"user",
|
|
18
|
+
"created_at",
|
|
19
|
+
"expires_at",
|
|
20
|
+
"is_expired",
|
|
21
|
+
"is_blacklisted",
|
|
22
|
+
]
|
|
23
|
+
list_filter = ["created_at", "expires_at"]
|
|
24
|
+
search_fields = ["user__username", "token_id"]
|
|
25
|
+
readonly_fields = [
|
|
26
|
+
"id",
|
|
27
|
+
"user",
|
|
28
|
+
"jti",
|
|
29
|
+
"token",
|
|
30
|
+
"created_at",
|
|
31
|
+
"expires_at",
|
|
32
|
+
]
|
|
33
|
+
actions = ["revoke_tokens"]
|
|
34
|
+
|
|
35
|
+
def is_expired(self, obj):
|
|
36
|
+
"""Check if token is expired."""
|
|
37
|
+
return obj.expires_at < datetime.now(obj.expires_at.tzinfo)
|
|
38
|
+
|
|
39
|
+
is_expired.boolean = True
|
|
40
|
+
|
|
41
|
+
def is_blacklisted(self, obj):
|
|
42
|
+
"""Check if token is blacklisted."""
|
|
43
|
+
return BlacklistedToken.objects.filter(token=obj).exists()
|
|
44
|
+
|
|
45
|
+
is_blacklisted.boolean = True
|
|
46
|
+
|
|
47
|
+
@admin.action(description="Revoke selected tokens")
|
|
48
|
+
def revoke_tokens(self, request, queryset):
|
|
49
|
+
"""Revoke selected OutstandingTokens by blacklisting them."""
|
|
50
|
+
for token in queryset:
|
|
51
|
+
BlacklistedToken.objects.get_or_create(token=token)
|
|
52
|
+
self.message_user(request, f"{queryset.count()} token(s) revoked.")
|
|
53
|
+
|
|
54
|
+
revoke_tokens.allowed_permissions = ("delete",)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@admin.register(BlacklistedToken)
|
|
58
|
+
class BlacklistedTokenAdmin(admin.ModelAdmin):
|
|
59
|
+
"""Admin interface for viewing blacklisted tokens."""
|
|
60
|
+
|
|
61
|
+
list_display = ["id", "token", "blacklisted_at"]
|
|
62
|
+
list_filter = ["blacklisted_at"]
|
|
63
|
+
search_fields = ["token__user__username"]
|
|
64
|
+
readonly_fields = ["token", "blacklisted_at"]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom admin views for API Help and Token Generation.
|
|
3
|
+
Uses monkey-patching to extend the default admin.site without replacing it.
|
|
4
|
+
This preserves all existing model registrations from tbase_admin.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django.contrib import admin
|
|
8
|
+
from django.shortcuts import render
|
|
9
|
+
from django.http import HttpRequest, HttpResponse
|
|
10
|
+
from django.contrib import messages
|
|
11
|
+
from django.urls import path, reverse
|
|
12
|
+
from django.conf import settings
|
|
13
|
+
|
|
14
|
+
from rest_framework_simplejwt.tokens import RefreshToken
|
|
15
|
+
from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_jwt_settings():
|
|
19
|
+
"""Get JWT token lifetime settings."""
|
|
20
|
+
jwt_settings = getattr(settings, "SIMPLE_JWT", {})
|
|
21
|
+
access_lifetime = jwt_settings.get("ACCESS_TOKEN_LIFETIME")
|
|
22
|
+
refresh_lifetime = jwt_settings.get("REFRESH_TOKEN_LIFETIME")
|
|
23
|
+
return {
|
|
24
|
+
"access_lifetime": access_lifetime,
|
|
25
|
+
"refresh_lifetime": refresh_lifetime,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def api_help_view(request: HttpRequest) -> HttpResponse:
|
|
30
|
+
"""
|
|
31
|
+
Display API documentation page in admin.
|
|
32
|
+
"""
|
|
33
|
+
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
|
|
34
|
+
from django.utils import timezone
|
|
35
|
+
|
|
36
|
+
base_url = request.build_absolute_uri("/api/")
|
|
37
|
+
|
|
38
|
+
# Get current user's tokens
|
|
39
|
+
user_tokens = OutstandingToken.objects.filter(user=request.user)
|
|
40
|
+
active_tokens = []
|
|
41
|
+
for token in user_tokens:
|
|
42
|
+
is_expired = token.expires_at < timezone.now()
|
|
43
|
+
is_blacklisted = BlacklistedToken.objects.filter(token=token).exists()
|
|
44
|
+
active_tokens.append(
|
|
45
|
+
{
|
|
46
|
+
"id": token.id,
|
|
47
|
+
"created_at": token.created_at,
|
|
48
|
+
"expires_at": token.expires_at,
|
|
49
|
+
"is_expired": is_expired,
|
|
50
|
+
"is_blacklisted": is_blacklisted,
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
context = {
|
|
55
|
+
"title": "API Token Management",
|
|
56
|
+
"base_url": base_url,
|
|
57
|
+
"active_tokens": active_tokens,
|
|
58
|
+
**admin.site.each_context(request),
|
|
59
|
+
}
|
|
60
|
+
return render(request, "admin/api_help.html", context)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def api_token_view(request: HttpRequest) -> HttpResponse:
|
|
64
|
+
"""
|
|
65
|
+
Generate JWT token for the logged-in user and list existing tokens.
|
|
66
|
+
"""
|
|
67
|
+
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
|
|
68
|
+
from django.utils import timezone
|
|
69
|
+
|
|
70
|
+
jwt_settings = get_jwt_settings()
|
|
71
|
+
|
|
72
|
+
# Get current user's existing tokens
|
|
73
|
+
user_tokens = OutstandingToken.objects.filter(user=request.user).order_by("-created_at")
|
|
74
|
+
active_tokens = []
|
|
75
|
+
for token in user_tokens:
|
|
76
|
+
is_expired = token.expires_at < timezone.now()
|
|
77
|
+
is_blacklisted = BlacklistedToken.objects.filter(token=token).exists()
|
|
78
|
+
active_tokens.append(
|
|
79
|
+
{
|
|
80
|
+
"id": token.id,
|
|
81
|
+
"created_at": token.created_at,
|
|
82
|
+
"expires_at": token.expires_at,
|
|
83
|
+
"is_expired": is_expired,
|
|
84
|
+
"is_blacklisted": is_blacklisted,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
context = {
|
|
89
|
+
"title": "获取访问令牌",
|
|
90
|
+
"user": request.user,
|
|
91
|
+
"access_token": None,
|
|
92
|
+
"refresh_token": None,
|
|
93
|
+
"access_lifetime": jwt_settings["access_lifetime"],
|
|
94
|
+
"refresh_lifetime": jwt_settings["refresh_lifetime"],
|
|
95
|
+
"active_tokens": active_tokens,
|
|
96
|
+
**admin.site.each_context(request),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if request.method == "POST":
|
|
100
|
+
try:
|
|
101
|
+
# Generate tokens for the current user
|
|
102
|
+
refresh = RefreshToken.for_user(request.user)
|
|
103
|
+
context["access_token"] = str(refresh.access_token)
|
|
104
|
+
context["refresh_token"] = str(refresh)
|
|
105
|
+
messages.success(request, "✅ 访问令牌已成功生成!")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
messages.error(request, f"❌ 生成令牌失败: {str(e)}")
|
|
108
|
+
|
|
109
|
+
return render(request, "admin/api_token.html", context)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def revoke_token_view(request: HttpRequest, token_id: int) -> HttpResponse:
|
|
113
|
+
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
|
|
114
|
+
from django.shortcuts import get_object_or_404, redirect
|
|
115
|
+
|
|
116
|
+
token = get_object_or_404(OutstandingToken, id=token_id)
|
|
117
|
+
|
|
118
|
+
# Users can only revoke their own tokens, admins can revoke any
|
|
119
|
+
if request.user != token.user and not request.user.is_superuser:
|
|
120
|
+
messages.error(request, "You can only revoke your own tokens!")
|
|
121
|
+
return redirect("admin:api_help")
|
|
122
|
+
|
|
123
|
+
BlacklistedToken.objects.get_or_create(token=token)
|
|
124
|
+
messages.success(request, f"Token #{token_id} has been revoked successfully!")
|
|
125
|
+
return redirect("admin:api_help")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Monkey-patch admin.site.get_urls() to add custom URLs
|
|
129
|
+
_original_get_urls = admin.site.get_urls
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def custom_get_urls():
|
|
133
|
+
"""Extend admin.site.get_urls() with custom API URLs."""
|
|
134
|
+
urls = _original_get_urls()
|
|
135
|
+
custom_urls = [
|
|
136
|
+
path("api-help/", admin.site.admin_view(api_help_view), name="api_help"),
|
|
137
|
+
path("api-token/", admin.site.admin_view(api_token_view), name="api_token"),
|
|
138
|
+
path(
|
|
139
|
+
"api-token/<int:token_id>/revoke/",
|
|
140
|
+
admin.site.admin_view(revoke_token_view),
|
|
141
|
+
name="api_token_revoke",
|
|
142
|
+
),
|
|
143
|
+
]
|
|
144
|
+
return custom_urls + urls
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Apply the monkey-patch
|
|
148
|
+
admin.site.get_urls = custom_get_urls
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Monkey-patch admin.site.get_app_list() to add API menu to left navigation
|
|
152
|
+
# Django 3.2 AdminSite.get_app_list(self, request)
|
|
153
|
+
_original_get_app_list = admin.site.get_app_list.__func__
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def custom_get_app_list(self, request):
|
|
157
|
+
"""Extend admin.site.get_app_list() to add API menu."""
|
|
158
|
+
app_list = _original_get_app_list(self, request)
|
|
159
|
+
|
|
160
|
+
# Add API application to navigation
|
|
161
|
+
api_app = {
|
|
162
|
+
"name": "API",
|
|
163
|
+
"app_label": "api",
|
|
164
|
+
"app_url": reverse("admin:api_help"),
|
|
165
|
+
"has_module_perms": True,
|
|
166
|
+
"models": [
|
|
167
|
+
{
|
|
168
|
+
"name": "API 使用帮助",
|
|
169
|
+
"object_name": "api_help",
|
|
170
|
+
"admin_url": reverse("admin:api_help"),
|
|
171
|
+
"view_only": True,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "获取访问令牌",
|
|
175
|
+
"object_name": "api_token",
|
|
176
|
+
"admin_url": reverse("admin:api_token"),
|
|
177
|
+
"view_only": True,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Insert after "性能管理" (index 0) or at the end
|
|
183
|
+
if app_list and app_list[0].get("app_label") == "performance":
|
|
184
|
+
app_list.insert(1, api_app)
|
|
185
|
+
else:
|
|
186
|
+
app_list.insert(0, api_app)
|
|
187
|
+
|
|
188
|
+
return app_list
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Apply the monkey-patch (bind to admin.site instance)
|
|
192
|
+
admin.site.get_app_list = custom_get_app_list.__get__(admin.site, type(admin.site))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FcDjangoPostApiConfig(AppConfig):
|
|
5
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
6
|
+
name = "fc_django_post_api"
|
|
7
|
+
label = "api"
|
|
8
|
+
verbose_name = "API"
|
|
9
|
+
|
|
10
|
+
def ready(self):
|
|
11
|
+
"""
|
|
12
|
+
Apply monkey-patch to admin.site when app is ready.
|
|
13
|
+
This adds custom API help and token generation views to Django admin.
|
|
14
|
+
"""
|
|
15
|
+
# Import applies the monkey-patch to admin.site
|
|
16
|
+
from fc_django_post_api import admin_site # noqa: F401
|
|
17
|
+
from . import admin as api_admin # noqa: F401
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom permissions for API endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rest_framework import permissions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IsAuthorOrReadOnly(permissions.BasePermission):
|
|
9
|
+
"""
|
|
10
|
+
Custom permission to only allow authors of an object to edit it.
|
|
11
|
+
Assumes the model instance has an 'author' attribute.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def has_object_permission(self, request, view, obj):
|
|
15
|
+
# Read permissions are allowed for any request,
|
|
16
|
+
# so we'll always allow GET, HEAD or OPTIONS requests.
|
|
17
|
+
if request.method in permissions.SAFE_METHODS:
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
# Write permissions only for the author
|
|
21
|
+
# If obj.author is None, deny write access
|
|
22
|
+
if obj.author is None:
|
|
23
|
+
return False
|
|
24
|
+
return obj.author == request.user
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Serializers for Post CRUD operations.
|
|
3
|
+
Uses tbase_post.models.Post from external package.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from rest_framework import serializers
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PostSerializer(serializers.ModelSerializer):
|
|
10
|
+
"""
|
|
11
|
+
Serializer for tbase_post Post model.
|
|
12
|
+
Uses ModelSerializer for automatic field mapping.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
tag_names = serializers.ListField(
|
|
16
|
+
child=serializers.CharField(max_length=255),
|
|
17
|
+
required=False,
|
|
18
|
+
allow_empty=True,
|
|
19
|
+
help_text="List of tag names to set on the post",
|
|
20
|
+
)
|
|
21
|
+
author = serializers.PrimaryKeyRelatedField(read_only=True)
|
|
22
|
+
author_name = serializers.SerializerMethodField()
|
|
23
|
+
|
|
24
|
+
class Meta:
|
|
25
|
+
model = None # Set dynamically in __init__
|
|
26
|
+
fields = [
|
|
27
|
+
"id",
|
|
28
|
+
"title",
|
|
29
|
+
"content",
|
|
30
|
+
"publish_status",
|
|
31
|
+
"created_on",
|
|
32
|
+
"updated_on",
|
|
33
|
+
"tag_names",
|
|
34
|
+
"meta_description",
|
|
35
|
+
"meta_keywords",
|
|
36
|
+
"article_img",
|
|
37
|
+
"product_name",
|
|
38
|
+
"product_id",
|
|
39
|
+
"youtube_id",
|
|
40
|
+
"data",
|
|
41
|
+
"author",
|
|
42
|
+
"author_name",
|
|
43
|
+
]
|
|
44
|
+
read_only_fields = [
|
|
45
|
+
"id",
|
|
46
|
+
"created_on",
|
|
47
|
+
"updated_on",
|
|
48
|
+
"author",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
def __init__(self, *args, **kwargs):
|
|
52
|
+
super().__init__(*args, **kwargs)
|
|
53
|
+
# Import Post model dynamically to avoid import errors
|
|
54
|
+
from tbase_post.models import Post
|
|
55
|
+
|
|
56
|
+
self.Meta.model = Post
|
|
57
|
+
|
|
58
|
+
def to_representation(self, instance):
|
|
59
|
+
"""Include tag_names in output."""
|
|
60
|
+
data = super().to_representation(instance)
|
|
61
|
+
try:
|
|
62
|
+
data["tag_names"] = list(instance.tags.values_list("name", flat=True))
|
|
63
|
+
except Exception:
|
|
64
|
+
data["tag_names"] = []
|
|
65
|
+
return data
|
|
66
|
+
|
|
67
|
+
def create(self, validated_data):
|
|
68
|
+
"""Create post and set tags."""
|
|
69
|
+
tag_names = validated_data.pop("tag_names", [])
|
|
70
|
+
instance = super().create(validated_data)
|
|
71
|
+
self._set_tags(instance, tag_names)
|
|
72
|
+
return instance
|
|
73
|
+
|
|
74
|
+
def update(self, instance, validated_data):
|
|
75
|
+
"""Update post and set tags."""
|
|
76
|
+
tag_names = validated_data.pop("tag_names", None)
|
|
77
|
+
instance = super().update(instance, validated_data)
|
|
78
|
+
if tag_names is not None:
|
|
79
|
+
self._set_tags(instance, tag_names)
|
|
80
|
+
return instance
|
|
81
|
+
|
|
82
|
+
def _set_tags(self, instance, tag_names):
|
|
83
|
+
"""Set tags on instance using django-taggit."""
|
|
84
|
+
# Clear existing tags and add new ones
|
|
85
|
+
instance.tags.clear()
|
|
86
|
+
for tag_name in tag_names:
|
|
87
|
+
instance.tags.add(tag_name)
|
|
88
|
+
|
|
89
|
+
def get_author_name(self, obj):
|
|
90
|
+
"""Get author username."""
|
|
91
|
+
try:
|
|
92
|
+
return obj.author.username if obj.author else None
|
|
93
|
+
except Exception:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PostListSerializer(serializers.ModelSerializer):
|
|
98
|
+
"""
|
|
99
|
+
Simplified serializer for post listing.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
tag_names = serializers.SerializerMethodField(read_only=True)
|
|
103
|
+
|
|
104
|
+
class Meta:
|
|
105
|
+
model = None # Set dynamically in __init__
|
|
106
|
+
fields = [
|
|
107
|
+
"id",
|
|
108
|
+
"title",
|
|
109
|
+
"publish_status",
|
|
110
|
+
"created_on",
|
|
111
|
+
"updated_on",
|
|
112
|
+
"article_img",
|
|
113
|
+
"tag_names",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def __init__(self, *args, **kwargs):
|
|
117
|
+
super().__init__(*args, **kwargs)
|
|
118
|
+
from tbase_post.models import Post
|
|
119
|
+
|
|
120
|
+
self.Meta.model = Post
|
|
121
|
+
|
|
122
|
+
def get_tag_names(self, obj):
|
|
123
|
+
"""Get tag names from taggit."""
|
|
124
|
+
try:
|
|
125
|
+
return list(obj.tags.values_list("name", flat=True))
|
|
126
|
+
except Exception:
|
|
127
|
+
return []
|