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.
@@ -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,3 @@
1
+ include README.md LICENSE
2
+ include pyproject.toml
3
+ recursive-include src/fc_django_post_api *.py
@@ -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
+ [![PyPI version](https://badge.fury.io/py/fc-django-post-api.svg)](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
+ [![PyPI version](https://badge.fury.io/py/fc-django-post-api.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ # API models - using tbase_post models from external package
2
+ from django.db import models
@@ -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 []