Compare commits
29 Commits
66dd4916d1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| dfa757c0d9 | |||
| 7dd59de036 | |||
| a9de2d1f68 | |||
| ab6c0a9b49 | |||
| 0f43e10c5b | |||
| c2b681a03a | |||
| 6108dd3346 | |||
| ec38651899 | |||
| 8161a27cf5 | |||
| 2ed7904ada | |||
| 5670ca57e6 | |||
| b1e7488970 | |||
| b7cacbcf6a | |||
| 3ca5b4a18d | |||
| 5588bbaac8 | |||
| fdc10692af | |||
| d25b099658 | |||
| 8dc2ce7780 | |||
| ae80234f65 | |||
| 53e91fa458 | |||
| 402f7e512e | |||
| 60366b7eb1 | |||
| 5174407da5 | |||
| 55a0b3b905 | |||
| 2666cbcb97 | |||
| 36e468f41c | |||
| 2595294116 | |||
| 89ac81e07d | |||
| 784c8bbb6e |
@@ -0,0 +1,12 @@
|
||||
# 启用PGSQL数据库
|
||||
DB_REMOTE=True
|
||||
# 数据库名称
|
||||
DB_NAME=quasar
|
||||
# 用户名
|
||||
DB_USER=quasar
|
||||
# 密码
|
||||
DB_PASSWORD=123456
|
||||
# 数据库地址
|
||||
DB_HOST=192.168.1.2
|
||||
# 数据库端口
|
||||
DB_PORT=5432
|
||||
@@ -0,0 +1,63 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Generic, T
|
||||
|
||||
from ninja import Schema
|
||||
|
||||
|
||||
# --- 假设的关联模型 Schema ---
|
||||
class AuthorSchema(Schema):
|
||||
id: int
|
||||
username: str
|
||||
|
||||
|
||||
class SectionSchema(Schema):
|
||||
id: int
|
||||
title: str # 假设 MainMenu 有 name 字段
|
||||
|
||||
|
||||
class ArticleAttachmentOut(Schema):
|
||||
id: int
|
||||
file: str
|
||||
|
||||
|
||||
class ArticleListOut(Schema):
|
||||
id: int
|
||||
title: str
|
||||
|
||||
section: SectionSchema
|
||||
|
||||
author: AuthorSchema
|
||||
|
||||
cover: Optional[str] = None
|
||||
is_contribution: bool
|
||||
is_published: bool
|
||||
|
||||
|
||||
class ArticleDetailOut(Schema):
|
||||
id: int
|
||||
title: str
|
||||
|
||||
section: SectionSchema
|
||||
|
||||
author: AuthorSchema
|
||||
|
||||
content: str
|
||||
cover: Optional[str] = None
|
||||
|
||||
is_contribution: bool
|
||||
is_published: bool
|
||||
|
||||
attachments: List[ArticleAttachmentOut] = []
|
||||
|
||||
|
||||
class ArticleCreate(Schema):
|
||||
title: str
|
||||
section_id: Optional[int] = None
|
||||
author_id: Optional[int] = None
|
||||
content: str
|
||||
is_contribution: bool = False
|
||||
is_published: bool = True
|
||||
|
||||
|
||||
class ArticleUpdate(ArticleCreate):
|
||||
pass
|
||||
+1
-1
@@ -121,4 +121,4 @@ def seed_articles(count=20):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_articles(120)
|
||||
seed_articles(1200)
|
||||
|
||||
+42
-12
@@ -1,20 +1,50 @@
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import render
|
||||
from typing import List
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from ninja import Router
|
||||
from ninja.pagination import paginate, PageNumberPagination
|
||||
|
||||
from article.models import Article
|
||||
from utils.permission import user_has_permission
|
||||
|
||||
from ninja import Router
|
||||
from article.schema import ArticleUpdate, ArticleCreate, ArticleListOut, ArticleDetailOut
|
||||
|
||||
# Create your views here.
|
||||
|
||||
# Create your views here.
|
||||
router = Router(tags=['content'])
|
||||
router = Router(tags=['Article'])
|
||||
|
||||
|
||||
@router.get('/article', description="获取文章列表")
|
||||
def article_details(request):
|
||||
if not user_has_permission(request.user, 'article', 'view'):
|
||||
return HttpResponseForbidden("无权访问")
|
||||
articles = Article.objects.all()
|
||||
return render(request, 'articles/list.html', {'articles': articles})
|
||||
@router.get("/article/", response=List[ArticleListOut], description="获取文章列表")
|
||||
@paginate(PageNumberPagination, page_size=20)
|
||||
def list_articles(request, title: str = '', section_id: int = 0):
|
||||
qs = (Article.objects.filter(title__contains=title).prefetch_related('attachments')
|
||||
.select_related('author', 'section').order_by("-id"))
|
||||
if section_id > 0:
|
||||
return qs.filter(section_id=section_id)
|
||||
return qs
|
||||
|
||||
|
||||
@router.get("/article/{id}", response=ArticleDetailOut)
|
||||
def get_article(request, id: int):
|
||||
return get_object_or_404(Article.objects.prefetch_related("attachments"), id=id)
|
||||
|
||||
|
||||
@router.post("/article/", response=ArticleDetailOut)
|
||||
def create_article(request, data: ArticleCreate):
|
||||
article = Article.objects.create(**data.dict())
|
||||
return article
|
||||
|
||||
|
||||
@router.put("/article/{id}", response=ArticleDetailOut)
|
||||
def update_article(request, id: int, data: ArticleUpdate):
|
||||
article = get_object_or_404(Article, id=id)
|
||||
for attr, value in data.dict().items():
|
||||
setattr(article, attr, value)
|
||||
article.save()
|
||||
return article
|
||||
|
||||
|
||||
@router.delete("/article/{id}")
|
||||
def delete_article(request, id: int):
|
||||
article = get_object_or_404(Article, id=id)
|
||||
article.delete()
|
||||
return {"success": True}
|
||||
|
||||
+22
-2
@@ -1,7 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib import admin, messages
|
||||
from django.core.cache import cache
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from base.models import MainMenu, BaseInfo, LinkCategory, FriendLink
|
||||
from base.models import MainMenu, BaseInfo, LinkCategory, FriendLink, PictureLink
|
||||
|
||||
|
||||
# —————— 主菜单 ——————
|
||||
@@ -60,6 +61,25 @@ class FriendLinkAdmin(ImportExportModelAdmin):
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
|
||||
@admin.register(PictureLink)
|
||||
class PictureLinkAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'order', 'is_active', 'created_at']
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
# 1. 先执行 Model 的 save 方法(这里会触发逻辑判断和消息暂存)
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
# 2. 【关键】检查有没有暂存的消息
|
||||
# 获取 key(如果是新建对象,obj.pk 此时已经有了,如果是内存中的临时对象则用 id)
|
||||
msg_key = obj.pk if obj.pk else id(obj)
|
||||
key = f"picture_count_limit_{msg_key}"
|
||||
msg = cache.get(key) # 缓存 30 秒
|
||||
if msg:
|
||||
# 取出消息并弹窗
|
||||
cache.delete(key)
|
||||
self.message_user(request, msg, level=messages.WARNING)
|
||||
|
||||
|
||||
# —————— 页脚链接 ——————
|
||||
@admin.register(BaseInfo)
|
||||
class BaseInfoAdmin(ImportExportModelAdmin):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from base.models import BaseInfo, LinkCategory, MainMenu
|
||||
from base.models import BaseInfo, LinkCategory, MainMenu, PictureLink
|
||||
|
||||
|
||||
def site_info(request):
|
||||
@@ -8,9 +8,10 @@ def site_info(request):
|
||||
site_info_obj = BaseInfo.objects.filter(is_active=True).first()
|
||||
link_categories = LinkCategory.objects.filter(is_active=True).prefetch_related('links')
|
||||
menus = MainMenu.objects.filter(visible=True, parent__isnull=True).order_by('order')
|
||||
|
||||
picture_links = PictureLink.objects.filter(is_active=True).order_by('order')
|
||||
return {
|
||||
'site_info': site_info_obj,
|
||||
'link_categories': link_categories,
|
||||
'menus': menus,
|
||||
'picture_links': picture_links,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 6.0.3 on 2026-04-02 01:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0003_baseinfo_delete_footerlink'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PictureLink',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('name', models.CharField(max_length=100, verbose_name='网站名称')),
|
||||
('picture', models.ImageField(help_text='建议尺寸:280x120', upload_to='picture/%Y/%m/%d/', verbose_name='图片链接')),
|
||||
('url', models.URLField(help_text='点击图片要访问的站点地址', verbose_name='链接地址')),
|
||||
('order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '图片链接',
|
||||
'verbose_name_plural': '图链管理',
|
||||
'ordering': ['order', '-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
||||
@@ -49,6 +50,42 @@ class FriendLink(BaseEntity):
|
||||
return f"[{self.category.name}] {self.name}"
|
||||
|
||||
|
||||
class PictureLink(BaseEntity):
|
||||
name = models.CharField("网站名称", max_length=100)
|
||||
picture = models.ImageField("图片链接", upload_to='picture/%Y/%m/%d/', help_text="建议尺寸:280x120")
|
||||
url = models.URLField("链接地址", help_text="点击图片要访问的站点地址")
|
||||
|
||||
# 2. 排序和状态
|
||||
order = models.IntegerField("排序", default=0)
|
||||
is_active = models.BooleanField("是否启用", default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "图片链接"
|
||||
verbose_name_plural = "图链管理"
|
||||
ordering = ['order', '-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 1. 逻辑判断:如果当前想要启用
|
||||
if self.is_active:
|
||||
# 统计现有的启用数量(排除自己)
|
||||
active_count = PictureLink.objects.filter(is_active=True).exclude(pk=self.pk).count()
|
||||
|
||||
# 2. 如果已满 4 个
|
||||
if active_count >= 4:
|
||||
self.is_active = False # 强制设为不生效
|
||||
|
||||
# 3. 【关键】把消息暂存起来,key 用 pk(如果是新建还没 pk,用 id(self) 临时顶替)
|
||||
# 注意:这里不能直接弹窗,因为没有 request
|
||||
msg_key = self.pk if self.pk else id(self)
|
||||
msg = "限制生效:最多只能有 4 个启用的链接,当前对象已被自动设置为【不启用】。"
|
||||
cache.set(f"picture_count_limit_{msg_key}", msg) # 缓存 30 秒
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class MainMenu(BaseEntity):
|
||||
# --- 基础信息 ---
|
||||
title = models.CharField("标题", max_length=100)
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
from ninja import Schema
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class LinkCategorySchema(Schema):
|
||||
id: int
|
||||
name: str
|
||||
code: str
|
||||
order: int
|
||||
is_active: bool
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class FriendLinkSchema(Schema):
|
||||
id: int
|
||||
category_id: int
|
||||
name: str
|
||||
url: str
|
||||
order: int
|
||||
is_active: bool
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class LinkCategoryCreateSchema(Schema):
|
||||
name: str
|
||||
code: str
|
||||
order: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class FriendLinkCreateSchema(Schema):
|
||||
category_id: int
|
||||
name: str
|
||||
url: str
|
||||
order: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
# ================= 响应模式 (Output) =================
|
||||
class PictureLinkSchema(Schema):
|
||||
id: int
|
||||
name: str
|
||||
picture: str # 这里返回的是图片路径字符串
|
||||
url: str
|
||||
order: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
# 如果你的 Model 字段和 Schema 字段名一致,通常不需要 orm_mode
|
||||
# 但为了保险,或者如果你直接返回 Model 实例,建议开启
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ================= 请求模式 (Input) =================
|
||||
class PictureLinkCreateSchema(Schema):
|
||||
name: str
|
||||
picture: str
|
||||
url: str
|
||||
order: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class PictureLinkUpdateSchema(Schema):
|
||||
name: Optional[str] = None
|
||||
picture: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
order: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class MainMenuBase(Schema):
|
||||
"""基础菜单字段"""
|
||||
title: str
|
||||
code: str
|
||||
url: str = ""
|
||||
is_external: bool = False
|
||||
is_carousel: bool = False
|
||||
order: int = 0
|
||||
visible: bool = True
|
||||
|
||||
|
||||
class MainMenuCreate(MainMenuBase):
|
||||
"""创建时的 Schema"""
|
||||
parent_id: Optional[int] = None
|
||||
|
||||
|
||||
class MainMenuUpdate(MainMenuBase):
|
||||
"""更新时的 Schema"""
|
||||
parent_id: Optional[int] = None
|
||||
|
||||
|
||||
class MainMenuOut(MainMenuBase):
|
||||
"""输出用的 Schema,包含 ID 和递归的子菜单"""
|
||||
id: int
|
||||
parent_id: Optional[int] = None
|
||||
children: List['MainMenuOut'] = [] # 递归引用自身
|
||||
|
||||
|
||||
# 告诉 Pydantic 如何处理递归模型
|
||||
MainMenuOut.model_rebuild()
|
||||
|
||||
|
||||
class BaseInfoIn(Schema):
|
||||
title: str
|
||||
e_title: str
|
||||
address: str
|
||||
postal: Optional[str] = ""
|
||||
telephone: Optional[str] = ""
|
||||
fax: Optional[str] = ""
|
||||
unit: Optional[str] = ""
|
||||
filing: Optional[str] = ""
|
||||
is_active: bool = False
|
||||
|
||||
|
||||
class BaseInfoOut(BaseInfoIn):
|
||||
id: int
|
||||
official_account: Optional[str] = None
|
||||
weibo: Optional[str] = None
|
||||
+321
-7
@@ -1,15 +1,24 @@
|
||||
from django.shortcuts import render
|
||||
from ninja import Router
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.conf import settings
|
||||
import hashlib
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from loguru import logger
|
||||
from ninja import Router, UploadedFile, File
|
||||
from ninja.pagination import paginate, PageNumberPagination
|
||||
|
||||
from base.models import FriendLink, LinkCategory, PictureLink, MainMenu, BaseInfo
|
||||
from base.schema import *
|
||||
|
||||
# Create your views here.
|
||||
router = Router(tags=['content'])
|
||||
router = Router(tags=['base'])
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@@ -43,3 +52,308 @@ def custom_upload_file(request):
|
||||
"url": file_url,
|
||||
"default": file_url
|
||||
})
|
||||
|
||||
|
||||
UPLOAD_DIR = Path("media/uploads")
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@router.post("/upload/{category}")
|
||||
def upload_file(request, category: str, file: UploadedFile = File(...), ):
|
||||
try:
|
||||
# 计算 md5
|
||||
md5 = hashlib.md5(file.read()).hexdigest()
|
||||
file.seek(0) # 重置文件指针
|
||||
|
||||
ext = file.name.split('.')[-1].lower()
|
||||
filename = f"{md5}.{ext}"
|
||||
|
||||
upload_dir = os.path.join(settings.MEDIA_ROOT, f"uploads/{category}/")
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
file_path = os.path.join(upload_dir, filename)
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(file.read())
|
||||
|
||||
url = f"{settings.MEDIA_URL}uploads/{category}/{filename}"
|
||||
return {"url": url}
|
||||
except Exception as e:
|
||||
logger.exception("Upload failed")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# =========================
|
||||
# 分类 CRUD
|
||||
# =========================
|
||||
@router.get("/link-categories", response=List[LinkCategorySchema])
|
||||
def list_categories(request):
|
||||
return LinkCategory.objects.all()
|
||||
|
||||
|
||||
@router.post("/link-categories", response=LinkCategorySchema)
|
||||
def create_category(request, data: LinkCategoryCreateSchema):
|
||||
return LinkCategory.objects.create(**data.dict())
|
||||
|
||||
|
||||
@router.put("/link-categories/{id}", response=LinkCategorySchema)
|
||||
def update_category(request, id: int, data: LinkCategoryCreateSchema):
|
||||
obj = LinkCategory.objects.get(id=id)
|
||||
for k, v in data.dict().items():
|
||||
setattr(obj, k, v)
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/link-categories/{id}")
|
||||
def delete_category(request, id: int):
|
||||
LinkCategory.objects.get(id=id).delete()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# =========================
|
||||
# 友情链接 CRUD
|
||||
# =========================
|
||||
@router.get("/friend-links", response=List[FriendLinkSchema])
|
||||
def list_links(request, category_id: Optional[int] = None):
|
||||
query = FriendLink.objects.all()
|
||||
if category_id:
|
||||
query = query.filter(category_id=category_id)
|
||||
return query
|
||||
|
||||
|
||||
@router.post("/friend-links", response=FriendLinkSchema)
|
||||
def create_link(request, data: FriendLinkCreateSchema):
|
||||
return FriendLink.objects.create(**data.dict())
|
||||
|
||||
|
||||
@router.put("/friend-links/{id}", response=FriendLinkSchema)
|
||||
def update_link(request, id: int, data: FriendLinkCreateSchema):
|
||||
obj = FriendLink.objects.get(id=id)
|
||||
for k, v in data.dict().items():
|
||||
setattr(obj, k, v)
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/friend-links/{id}")
|
||||
def delete_link(request, id: int):
|
||||
FriendLink.objects.get(id=id).delete()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ================= 列表与创建 =================
|
||||
@router.get("/picture-link/", response=List[PictureLinkSchema])
|
||||
def get_picture_links(request):
|
||||
"""
|
||||
获取所有图片链接
|
||||
GET /api/picture-link/
|
||||
"""
|
||||
# 这里的排序与 models.py 中的 Meta.ordering 保持一致
|
||||
return PictureLink.objects.all().order_by('order', '-created_at')
|
||||
|
||||
|
||||
@router.post("/picture-link/", response=PictureLinkSchema)
|
||||
def create_picture_link(request, payload: PictureLinkCreateSchema):
|
||||
"""
|
||||
新增图片链接
|
||||
POST /api/picture-link/
|
||||
"""
|
||||
# 直接利用 payload 创建对象
|
||||
# 注意:这里调用 save() 会触发你 models.py 中定义的 "最多4个启用" 的逻辑
|
||||
link = PictureLink.objects.create(
|
||||
name=payload.name,
|
||||
picture=payload.picture,
|
||||
url=payload.url,
|
||||
order=payload.order,
|
||||
is_active=payload.is_active
|
||||
)
|
||||
return link
|
||||
|
||||
|
||||
# ================= 单个对象操作 =================
|
||||
@router.get("/picture-link/{link_id}", response=PictureLinkSchema)
|
||||
def get_picture_link(request, link_id: int):
|
||||
"""
|
||||
获取单个图片链接详情
|
||||
GET /api/picture-link/{id}/
|
||||
"""
|
||||
return get_object_or_404(PictureLink, id=link_id)
|
||||
|
||||
|
||||
@router.put("/picture-link/{link_id}", response=PictureLinkSchema)
|
||||
def update_picture_link(request, link_id: int, payload: PictureLinkUpdateSchema):
|
||||
"""
|
||||
更新图片链接
|
||||
PUT /api/picture-link/{id}/
|
||||
"""
|
||||
link = get_object_or_404(PictureLink, id=link_id)
|
||||
|
||||
# 更新字段
|
||||
if payload.name is not None: link.name = payload.name
|
||||
if payload.picture is not None: link.picture = payload.picture
|
||||
if payload.url is not None: link.url = payload.url
|
||||
if payload.order is not None: link.order = payload.order
|
||||
if payload.is_active is not None: link.is_active = payload.is_active
|
||||
|
||||
# 保存,同样会触发 models.py 中的限制逻辑
|
||||
link.save()
|
||||
return link
|
||||
|
||||
|
||||
@router.delete("/picture-link/{link_id}")
|
||||
def delete_picture_link(request, link_id: int):
|
||||
"""
|
||||
删除图片链接
|
||||
DELETE /api/picture-link/{id}/
|
||||
"""
|
||||
link = get_object_or_404(PictureLink, id=link_id)
|
||||
link.delete()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
def build_menu_tree(menus: List[MainMenu]) -> List[MainMenuOut]:
|
||||
"""
|
||||
将平铺的菜单列表转换为嵌套的树形结构。
|
||||
这是一个高效的 O(n) 算法。
|
||||
"""
|
||||
menu_map = {}
|
||||
root_menus = []
|
||||
|
||||
# 第一步:将所有菜单放入字典,并初始化 children 列表
|
||||
for menu in menus:
|
||||
menu_data = MainMenuOut(
|
||||
id=menu.id,
|
||||
parent_id=menu.parent_id,
|
||||
title=menu.title,
|
||||
code=menu.code,
|
||||
url=menu.url,
|
||||
is_external=menu.is_external,
|
||||
is_carousel=menu.is_carousel,
|
||||
order=menu.order,
|
||||
visible=menu.visible,
|
||||
children=[]
|
||||
)
|
||||
menu_map[menu.id] = menu_data
|
||||
if menu.parent_id is None:
|
||||
root_menus.append(menu_data)
|
||||
|
||||
# 第二步:遍历字典,将子菜单挂载到父菜单上
|
||||
for menu_id, menu_obj in menu_map.items():
|
||||
if menu_obj.parent_id is not None and menu_obj.parent_id in menu_map:
|
||||
parent = menu_map[menu_obj.parent_id]
|
||||
parent.children.append(menu_obj)
|
||||
|
||||
# 第三步:对每个层级的菜单按 'order' 排序
|
||||
def sort_children(menu_list):
|
||||
menu_list.sort(key=lambda x: (x.order, x.title))
|
||||
for menu in menu_list:
|
||||
sort_children(menu.children)
|
||||
|
||||
sort_children(root_menus)
|
||||
return root_menus
|
||||
|
||||
|
||||
@router.get("main-menu/", response=List[MainMenuOut])
|
||||
def get_menu_tree(request):
|
||||
"""
|
||||
获取完整的导航菜单树。
|
||||
GET /api/main-menu/
|
||||
"""
|
||||
all_menus = MainMenu.objects.all().select_related('parent')
|
||||
return build_menu_tree(all_menus)
|
||||
|
||||
|
||||
@router.post("/main-menu/", response=MainMenuOut)
|
||||
def create_menu(request, payload: MainMenuCreate):
|
||||
"""
|
||||
创建一个新的菜单项。
|
||||
POST /api/main-menu/
|
||||
"""
|
||||
parent = None
|
||||
if payload.parent_id:
|
||||
parent = get_object_or_404(MainMenu, id=payload.parent_id)
|
||||
|
||||
menu = MainMenu.objects.create(
|
||||
parent=parent,
|
||||
**payload.dict(exclude={'parent_id'})
|
||||
)
|
||||
return menu
|
||||
|
||||
|
||||
@router.put("/main-menu/{menu_id}", response=MainMenuOut)
|
||||
def update_menu(request, menu_id: int, payload: MainMenuUpdate):
|
||||
"""
|
||||
更新一个菜单项。
|
||||
PUT /api/main-menu/{menu_id}/
|
||||
"""
|
||||
menu = get_object_or_404(MainMenu, id=menu_id)
|
||||
|
||||
# 处理 parent_id
|
||||
if payload.parent_id is not None:
|
||||
if payload.parent_id != menu.parent_id:
|
||||
new_parent = get_object_or_404(MainMenu, id=payload.parent_id)
|
||||
menu.parent = new_parent
|
||||
else:
|
||||
menu.parent = None
|
||||
|
||||
# 更新其他字段
|
||||
for attr, value in payload.dict(exclude={'parent_id'}).items():
|
||||
setattr(menu, attr, value)
|
||||
|
||||
menu.save()
|
||||
return menu
|
||||
|
||||
|
||||
@router.delete("/main-menu/{menu_id}")
|
||||
def delete_menu(request, menu_id: int):
|
||||
"""
|
||||
删除一个菜单项(及其所有子项)。
|
||||
DELETE /api/main-menu/{menu_id}/
|
||||
"""
|
||||
menu = get_object_or_404(MainMenu, id=menu_id)
|
||||
menu.delete() # Django 的级联删除会自动处理子项
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# 查询列表
|
||||
@router.get("/base-info/", response=List[BaseInfoOut])
|
||||
@paginate(PageNumberPagination, page_size=20)
|
||||
def list_base_info(request):
|
||||
return BaseInfo.objects.all().order_by("-id")
|
||||
|
||||
|
||||
# 获取详情
|
||||
@router.get("/base-info/{pk}", response=BaseInfoOut)
|
||||
def get_base_info(request, pk: int):
|
||||
return get_object_or_404(BaseInfo, pk=pk)
|
||||
|
||||
|
||||
# 创建
|
||||
@router.post("/base-info/", response=BaseInfoOut)
|
||||
def create_base_info(request, data: BaseInfoIn, ):
|
||||
obj = BaseInfo.objects.create(**data.dict())
|
||||
return obj
|
||||
|
||||
|
||||
# 更新
|
||||
@router.put("/base-info/{pk}", response=BaseInfoOut)
|
||||
def update_base_info(
|
||||
request,
|
||||
pk: int,
|
||||
data: BaseInfoIn,
|
||||
):
|
||||
obj = get_object_or_404(BaseInfo, pk=pk)
|
||||
|
||||
for attr, value in data.dict().items():
|
||||
setattr(obj, attr, value)
|
||||
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
|
||||
# 删除
|
||||
@router.delete("/base-info/{pk}")
|
||||
def delete_base_info(request, pk: int):
|
||||
obj = get_object_or_404(BaseInfo, pk=pk)
|
||||
obj.delete()
|
||||
return {"success": True}
|
||||
|
||||
@@ -7,6 +7,8 @@ app_name = 'front' # 可选,用于命名空间(如 {% url 'main:index' %}
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('<str:main>.html', views.section, name='section'),
|
||||
path('list/search.html', views.search_list, name='search_list'),
|
||||
path('list/search/<str:sec>.html', views.search_section_list, name='search_section_list'),
|
||||
path('list/news.html', views.main_news_list, name='news_main'),
|
||||
path('list/<str:main>.html', views.main_article_list, name='main_list'),
|
||||
path('list/<str:main>/<str:sub>.html', views.sub_article_list, name='sub_list'),
|
||||
|
||||
+56
-2
@@ -206,7 +206,7 @@ def sub_news_list(request, sub: str):
|
||||
logger.info(f"主板块 {main_section} 子板块 {sub_section}")
|
||||
if not sub_section or not main_section:
|
||||
raise Http404("不存在的板块")
|
||||
news_list = sub_section.news_set.filter(is_published=True).order_by('-created_at')[:21]
|
||||
news_list = sub_section.news_set.filter(is_published=True).order_by('-created_at')
|
||||
news_list = custom_pagination(news_list, request)
|
||||
|
||||
context = {
|
||||
@@ -219,6 +219,54 @@ def sub_news_list(request, sub: str):
|
||||
return render(request, 'front/list.html', context=context)
|
||||
|
||||
|
||||
def search_list(request):
|
||||
sections = MainMenu.objects.filter(parent__visible=True, visible=True, parent__isnull=False, ).all()
|
||||
articles_qs = Article.objects.filter(is_published=True).all()
|
||||
q = request.GET.get('q')
|
||||
if q:
|
||||
articles_qs = articles_qs.filter(
|
||||
Q(title__icontains=q) | Q(content__icontains=q)
|
||||
)
|
||||
articles = custom_pagination(articles_qs, request)
|
||||
main_section = {
|
||||
'title': '搜索中心',
|
||||
'code': 'search'
|
||||
}
|
||||
context = {
|
||||
'main_section': main_section,
|
||||
'sections': sections,
|
||||
'articles': articles,
|
||||
"q": q,
|
||||
}
|
||||
return render(request, 'front/list.html', context=context)
|
||||
|
||||
|
||||
def search_section_list(request, sec):
|
||||
sections = MainMenu.objects.filter(parent__visible=True, visible=True, parent__isnull=False, ).all()
|
||||
articles_qs = Article.objects.filter(is_published=True).all()
|
||||
q = request.GET.get('q')
|
||||
sec = sections.filter(code=sec).first()
|
||||
if q:
|
||||
articles_qs = articles_qs.filter(
|
||||
Q(title__icontains=q) | Q(content__icontains=q)
|
||||
)
|
||||
if sec:
|
||||
articles_qs = articles_qs.filter(section=sec)
|
||||
articles = custom_pagination(articles_qs, request)
|
||||
main_section = {
|
||||
'title': '搜索中心',
|
||||
'code': 'search'
|
||||
}
|
||||
context = {
|
||||
'main_section': main_section,
|
||||
'sections': sections,
|
||||
'articles': articles,
|
||||
"q": q,
|
||||
"current_sec": sec,
|
||||
}
|
||||
return render(request, 'front/list.html', context=context)
|
||||
|
||||
|
||||
def main_article_list(request, main):
|
||||
"""信息公开"""
|
||||
logger.info(f"正在访问 {main}")
|
||||
@@ -251,11 +299,17 @@ def sub_article_list(request, main: str, sub: str):
|
||||
if not sub_section or not main_section:
|
||||
raise Http404("不存在的板块")
|
||||
articles = sub_section.article_set.filter(is_published=True).order_by('-created_at')
|
||||
q = request.GET.get('q')
|
||||
if q:
|
||||
articles = articles.filter(
|
||||
Q(title__icontains=q) | Q(content__icontains=q)
|
||||
)
|
||||
articles = custom_pagination(articles, request)
|
||||
context = {
|
||||
'main_section': main_section,
|
||||
'sub_section': sub_section,
|
||||
'sections': sections.filter(parent__code=main),
|
||||
'articles': articles
|
||||
'articles': articles,
|
||||
"q": q,
|
||||
}
|
||||
return render(request, 'front/list.html', context=context)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,49 @@
|
||||
from ninja import Schema
|
||||
from typing import Optional
|
||||
from pydantic import constr
|
||||
|
||||
# 新闻模块
|
||||
class NewsSectionSchema(Schema):
|
||||
id: int
|
||||
title: str
|
||||
code: str
|
||||
order: int
|
||||
visible: bool
|
||||
|
||||
class NewsSectionCreateSchema(Schema):
|
||||
title: constr(max_length=100)
|
||||
code: constr(max_length=50)
|
||||
order: Optional[int] = 0
|
||||
visible: Optional[bool] = True
|
||||
|
||||
class NewsSectionUpdateSchema(Schema):
|
||||
title: Optional[constr(max_length=100)]
|
||||
code: Optional[constr(max_length=50)]
|
||||
order: Optional[int]
|
||||
visible: Optional[bool]
|
||||
|
||||
# 新闻内容
|
||||
class NewsSchema(Schema):
|
||||
id: int
|
||||
title: str
|
||||
section_id: int
|
||||
author_id: Optional[int]
|
||||
content: str
|
||||
cover: Optional[str]
|
||||
is_published: bool
|
||||
|
||||
class NewsCreateSchema(Schema):
|
||||
title: constr(max_length=200)
|
||||
section_id: int
|
||||
author_id: Optional[int]
|
||||
content: str
|
||||
cover: Optional[str]
|
||||
is_published: Optional[bool] = True
|
||||
|
||||
class NewsUpdateSchema(Schema):
|
||||
title: Optional[constr(max_length=200)]
|
||||
section_id: Optional[int]
|
||||
author_id: Optional[int]
|
||||
content: Optional[str]
|
||||
cover: Optional[str]
|
||||
is_published: Optional[bool]
|
||||
+22
-7
@@ -3,6 +3,8 @@ import random
|
||||
import sys
|
||||
|
||||
import django
|
||||
from loguru import logger
|
||||
from pypinyin import lazy_pinyin, Style
|
||||
|
||||
# 配置 Django 环境
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -17,18 +19,31 @@ from users.models import User
|
||||
# 初始化 Faker (zh_CN 表示生成中文数据)
|
||||
fake = Faker('zh_CN')
|
||||
|
||||
NEWS_SECTIONS = [
|
||||
{"title": "消防要闻", "order": 1, "visible": True, },
|
||||
{"title": "基层动态", "order": 2, "visible": True, },
|
||||
{"title": "国务院新闻", "order": 3, "visible": True, },
|
||||
]
|
||||
|
||||
|
||||
def seed_news(count=20):
|
||||
print(f"🚀 开始生成 {count} 条富文本新闻数据...")
|
||||
logger.info(f"🚀 开始生成 {count} 条富文本新闻数据...")
|
||||
|
||||
# --- 1. 获取关联数据 ---
|
||||
sections = list(NewsSection.objects.all())
|
||||
users = list(User.objects.all())
|
||||
|
||||
if not sections:
|
||||
print("❌ 错误:数据库中没有找到任何板块 (NewsSection)!")
|
||||
return
|
||||
|
||||
logger.warning("❌ 错误:数据库中没有找到任何板块 (NewsSection)!")
|
||||
for sec in NEWS_SECTIONS:
|
||||
title = sec["title"]
|
||||
NewsSection.objects.create(
|
||||
title=title,
|
||||
code="-".join(lazy_pinyin(title, style=Style.NORMAL)),
|
||||
order=sec["order"],
|
||||
visible=sec["visible"],
|
||||
)
|
||||
sections = list(NewsSection.objects.all())
|
||||
created_count = 0
|
||||
|
||||
for i in range(count):
|
||||
@@ -90,10 +105,10 @@ def seed_news(count=20):
|
||||
)
|
||||
|
||||
created_count += 1
|
||||
print(f" - [{section.title}] {news.title}")
|
||||
logger.info(f" - [{section.title}] {news.title}")
|
||||
|
||||
print(f"✅ 成功生成 {created_count} 条富文本新闻数据!")
|
||||
logger.info(f"✅ 成功生成 {created_count} 条富文本新闻数据!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_news(120)
|
||||
seed_news(600)
|
||||
|
||||
+113
@@ -1,3 +1,116 @@
|
||||
from django.shortcuts import render
|
||||
from loguru import logger
|
||||
|
||||
# Create your views here.
|
||||
from ninja import Router
|
||||
from django.shortcuts import get_object_or_404
|
||||
from typing import List
|
||||
|
||||
from ninja.errors import HttpError
|
||||
from ninja.pagination import paginate, PageNumberPagination
|
||||
|
||||
from users.permission import has_permission, require_permissions
|
||||
from utils.dept_utils import get_dept_children_ids
|
||||
from .models import NewsSection, News
|
||||
from .schema import (
|
||||
NewsSectionSchema, NewsSectionCreateSchema, NewsSectionUpdateSchema,
|
||||
NewsSchema, NewsCreateSchema, NewsUpdateSchema
|
||||
)
|
||||
|
||||
router = Router(tags=['news'])
|
||||
|
||||
|
||||
# ================= 新闻模块接口 =================
|
||||
|
||||
@router.get("/news_section/", response=List[NewsSectionSchema])
|
||||
@paginate(PageNumberPagination, page_size=20)
|
||||
def list_sections(request):
|
||||
return NewsSection.objects.all()
|
||||
|
||||
|
||||
@router.get("/news_section/{section_id}", response=NewsSectionSchema)
|
||||
def retrieve_section(request, section_id: int):
|
||||
return get_object_or_404(NewsSection, id=section_id)
|
||||
|
||||
|
||||
@router.post("/news_section/", response=NewsSectionSchema)
|
||||
def create_section(request, data: NewsSectionCreateSchema):
|
||||
section = NewsSection.objects.create(**data.dict())
|
||||
return section
|
||||
|
||||
|
||||
@router.put("/news_section/{section_id}", response=NewsSectionSchema)
|
||||
def update_section(request, section_id: int, data: NewsSectionUpdateSchema):
|
||||
section = get_object_or_404(NewsSection, id=section_id)
|
||||
for attr, value in data.dict(exclude_unset=True).items():
|
||||
setattr(section, attr, value)
|
||||
section.save()
|
||||
return section
|
||||
|
||||
|
||||
@router.delete("/news_section/{section_id}", response={"success": bool})
|
||||
def delete_section(request, section_id: int):
|
||||
section = get_object_or_404(NewsSection, id=section_id)
|
||||
section.delete()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ================= 新闻接口 =================
|
||||
|
||||
@router.get("/news/", response=List[NewsSchema])
|
||||
@paginate(PageNumberPagination, page_size=20)
|
||||
@require_permissions("news:list")
|
||||
def list_news(request, section_id: int = 0, title: str = ''):
|
||||
user = request.auth
|
||||
if not has_permission(user.id, "news:list"):
|
||||
raise
|
||||
news = News.objects.all()
|
||||
|
||||
# 板块筛选
|
||||
if section_id > 0:
|
||||
return news.filter(section_id=section_id).all()
|
||||
|
||||
# 根据用户权限筛选
|
||||
if has_permission(user.id, 'news:manage'):
|
||||
dept_ids = get_dept_children_ids(user.dept_id)
|
||||
# 筛选:作者属于这些部门中的任意一个
|
||||
news = news.filter(author__dept_id__in=dept_ids)
|
||||
else:
|
||||
logger.info(user)
|
||||
news = news.filter(author=user)
|
||||
|
||||
# 根据新闻标题筛选
|
||||
if title:
|
||||
news = news.filter(title__contains=title)
|
||||
return news
|
||||
|
||||
|
||||
@router.get("/news/{news_id}", response=NewsSchema)
|
||||
@require_permissions("news:view")
|
||||
def retrieve_news(request, news_id: int):
|
||||
return get_object_or_404(News, id=news_id)
|
||||
|
||||
|
||||
@router.post("/news/", response=NewsSchema)
|
||||
@require_permissions("news:create")
|
||||
def create_news(request, data: NewsCreateSchema):
|
||||
news = News.objects.create(**data.dict())
|
||||
return news
|
||||
|
||||
|
||||
@router.put("/news/{news_id}", response=NewsSchema)
|
||||
@require_permissions("news:update")
|
||||
def update_news(request, news_id: int, data: NewsUpdateSchema):
|
||||
news = get_object_or_404(News, id=news_id)
|
||||
for attr, value in data.dict(exclude_unset=True).items():
|
||||
setattr(news, attr, value)
|
||||
news.save()
|
||||
return news
|
||||
|
||||
|
||||
@router.delete("/news/{news_id}", response={"success": bool})
|
||||
@require_permissions("news:delete")
|
||||
def delete_news(request, news_id: int):
|
||||
news = get_object_or_404(News, id=news_id)
|
||||
news.delete()
|
||||
return {"success": True}
|
||||
|
||||
Generated
+425
-105
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -19,21 +19,21 @@ reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.12.1"
|
||||
version = "4.13.0"
|
||||
description = "High-level concurrency and networking framework on top of asyncio or Trio"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"},
|
||||
{file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"},
|
||||
{file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"},
|
||||
{file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
|
||||
[package.extras]
|
||||
trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""]
|
||||
trio = ["trio (>=0.32.0)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -60,6 +60,109 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
|
||||
{file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
|
||||
{file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
|
||||
{file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
|
||||
{file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
|
||||
{file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
|
||||
{file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
|
||||
{file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
@@ -98,6 +201,100 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "contextlib2"
|
||||
version = "21.6.0"
|
||||
description = "Backports and enhancements for the contextlib module"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"},
|
||||
{file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.6"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.8, !=3.9.0, !=3.9.1"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"},
|
||||
{file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
||||
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||
nox = ["nox[uv] (>=2024.4.15)"]
|
||||
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
|
||||
sdist = ["build (>=1.0.0)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "diff-match-patch"
|
||||
version = "20241021"
|
||||
@@ -280,6 +477,59 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "django-ninja-extra"
|
||||
version = "0.31.4"
|
||||
description = "Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "django_ninja_extra-0.31.4-py3-none-any.whl", hash = "sha256:fb40879364808344e16b690990bbd669dccd0a0f4a2844294bf07ec3eb1b5375"},
|
||||
{file = "django_ninja_extra-0.31.4.tar.gz", hash = "sha256:c5efc13bead5e49ac90b83f8739689345c260e412f530d78825740a67a8ca99f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
asgiref = "*"
|
||||
contextlib2 = "*"
|
||||
Django = ">=2.2"
|
||||
django-ninja = ">=1.6.0"
|
||||
injector = ">=0.19.0"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "django-ninja-jwt"
|
||||
version = "5.4.4"
|
||||
description = "Django Ninja JWT - JSON Web Token for Django-Ninja"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "django_ninja_jwt-5.4.4-py3-none-any.whl", hash = "sha256:cc27b86542b4255fa21af0dc652aea15b59066ad5f46296f0c6faa193c5e5924"},
|
||||
{file = "django_ninja_jwt-5.4.4.tar.gz", hash = "sha256:14b145b89955ac5c89ed23a430dac388118f7bd9479cbb562fc076f1c3e13f6c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=2.1"
|
||||
django-ninja-extra = ">=0.30.5"
|
||||
pydantic-settings = ">=2.0.0"
|
||||
pyjwt = [
|
||||
{version = ">=1.7.1,<3"},
|
||||
{version = "*", extras = ["crypto"]},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
crypto = ["cryptography (>=3.3.1)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "django-redis"
|
||||
version = "6.0.0"
|
||||
@@ -346,14 +596,14 @@ reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "40.11.1"
|
||||
version = "40.12.0"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "faker-40.11.1-py3-none-any.whl", hash = "sha256:3af3a213ba8fb33ce6ba2af7aef2ac91363dae35d0cec0b2b0337d189e5bee2a"},
|
||||
{file = "faker-40.11.1.tar.gz", hash = "sha256:61965046e79e8cfde4337d243eac04c0d31481a7c010033141103b43f603100c"},
|
||||
{file = "faker-40.12.0-py3-none-any.whl", hash = "sha256:6238a4058a8b581892e3d78fe5fdfa7568739e1c8283e4ede83f1dde0bfc1a3b"},
|
||||
{file = "faker-40.12.0.tar.gz", hash = "sha256:58b5a9054c367bd5fb2e948634105364cc570e78a98a8e5161a74691c45f158f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -535,6 +785,26 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "injector"
|
||||
version = "0.24.0"
|
||||
description = "Injector - Python dependency injection framework, inspired by Guice"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "injector-0.24.0-py3-none-any.whl", hash = "sha256:47294c7a7fdb811f0d1b442a1e0152bb2fc28b2ccaaba4cba44e5e125e0da2d0"},
|
||||
{file = "injector-0.24.0.tar.gz", hash = "sha256:e85a75d1516cff2f03170f3fd1219f56acb25c9a05e307819ae0dcde3dad3d3f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black (==24.3.0) ; implementation_name == \"cpython\"", "build (==1.0.3)", "check-manifest (==0.49)", "click (==8.1.7)", "coverage[toml] (==7.3.2)", "exceptiongroup (==1.2.0)", "importlib-metadata (==7.0.0)", "iniconfig (==2.0.0)", "mypy (==1.7.1) ; implementation_name == \"cpython\"", "mypy-extensions (==1.0.0)", "packaging (==25.0)", "pathspec (==0.12.1)", "platformdirs (==4.1.0)", "pluggy (==1.3.0)", "pyproject-hooks (==1.0.0)", "pytest (==7.4.3)", "pytest-cov (==4.1.0)", "tomli (==2.0.1)", "typing-extensions (==4.9.0) ; python_version < \"3.9\"", "zipp (==3.19.1)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
@@ -582,103 +852,103 @@ reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.1.1"
|
||||
version = "12.2.0"
|
||||
description = "Python Imaging Library (fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"},
|
||||
{file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e"},
|
||||
{file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -776,6 +1046,24 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
|
||||
files = [
|
||||
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
|
||||
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
@@ -942,6 +1230,35 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.13.1"
|
||||
description = "Settings management using Pydantic"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237"},
|
||||
{file = "pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=2.7.0"
|
||||
python-dotenv = ">=0.21.0"
|
||||
typing-inspection = ">=0.4.0"
|
||||
|
||||
[package.extras]
|
||||
aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"]
|
||||
azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
|
||||
gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
|
||||
toml = ["tomli (>=2.0.1)"]
|
||||
yaml = ["pyyaml (>=6.0.1)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.12.1"
|
||||
@@ -954,6 +1271,9 @@ files = [
|
||||
{file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
|
||||
|
||||
[package.extras]
|
||||
crypto = ["cryptography (>=3.4.0)"]
|
||||
dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
@@ -1112,14 +1432,14 @@ reference = "mirrors"
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "7.3.0"
|
||||
version = "7.4.0"
|
||||
description = "Python client for Redis database and key-value store"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364"},
|
||||
{file = "redis-7.3.0.tar.gz", hash = "sha256:4d1b768aafcf41b01022410b3cc4f15a07d9b3d6fe0c66fc967da2c88e551034"},
|
||||
{file = "redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec"},
|
||||
{file = "redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -1740,4 +2060,4 @@ reference = "mirrors"
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.13,<3.14"
|
||||
content-hash = "3e611ebdd242413bf20b6f4f63f2db3238f46614acb7d4e54d315075c7ba0d7a"
|
||||
content-hash = "2f9ee1d8efbabe771fcdf5216ea88c072e441498ffa4c5391b14a4d09bd7267f"
|
||||
|
||||
+3
-2
@@ -3,7 +3,7 @@ name = "server"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
{name = "ngfchl",email = "ngfchl@126.com"}
|
||||
{ name = "ngfchl", email = "ngfchl@126.com" }
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
@@ -13,7 +13,6 @@ python = ">=3.13,<3.14"
|
||||
django = ">=6.0.3,<7.0.0"
|
||||
django-ninja = "^1.6"
|
||||
django-redis = "^6.0.0"
|
||||
pyjwt = "^2.8.0"
|
||||
python-dateutil = "^2.8.2"
|
||||
toml = "^0.10.2"
|
||||
uvicorn = { version = "^0.42.0", extras = ["standard"] }
|
||||
@@ -26,6 +25,8 @@ pillow = "^12.1.1"
|
||||
django-ckeditor-5 = "^0.2.20"
|
||||
loguru = "^0.7.3"
|
||||
django-import-export = "^4.4.0"
|
||||
django-ninja-jwt = "^5.4.4"
|
||||
django-ninja-extra = "^0.31.4"
|
||||
|
||||
|
||||
|
||||
|
||||
+69
-61
@@ -10,9 +10,11 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
|
||||
import environ
|
||||
from loguru import logger
|
||||
|
||||
env = environ.Env()
|
||||
|
||||
@@ -76,6 +78,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'ninja_extra',
|
||||
'import_export',
|
||||
'django_ckeditor_5',
|
||||
'base.apps.BaseConfig',
|
||||
@@ -90,6 +93,7 @@ MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'ninja.compatibility.files.fix_request_files_middleware',
|
||||
# 'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
@@ -123,7 +127,7 @@ ASGI_APPLICATION = 'server.asgi.application'
|
||||
|
||||
if env('DB_REMOTE', default=False):
|
||||
# mysql_connect = dj_database_url.parse(os.getenv('MYSQL_CONNECTION'), conn_max_age=0)
|
||||
# print("数据库连接信息", mysql_connect)
|
||||
logger.info("正在使用 PGSQL 数据库连接信息")
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': env('DB_ENGINE', default='dj_db_conn_pool.backends.postgresql'),
|
||||
@@ -145,6 +149,7 @@ if env('DB_REMOTE', default=False):
|
||||
},
|
||||
}
|
||||
else:
|
||||
logger.info("正在使用本地 SQLite 数据库", )
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
@@ -156,6 +161,25 @@ else:
|
||||
},
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
# 指定使用本地内存缓存后端
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
|
||||
# 缓存实例的名称,用于区分不同的内存存储
|
||||
# 如果只有一个 locmem 缓存,可以忽略此项,但建议设置
|
||||
'LOCATION': 'unique-snowflake',
|
||||
|
||||
# (可选) 默认超时时间,单位:秒
|
||||
'TIMEOUT': 300,
|
||||
|
||||
# (可选) 最大缓存条目数,超过后会根据 LRU 策略淘汰旧数据
|
||||
'OPTIONS': {
|
||||
'MAX_ENTRIES': 1000,
|
||||
'CULL_FREQUENCY': 3, # 到达 MAX_ENTRIES 时,剔除 1/CULL_FREQUENCY 的数据
|
||||
}
|
||||
}
|
||||
}
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
@@ -195,64 +219,51 @@ STATICFILES_DIRS = (
|
||||
os.path.join(os.path.join(BASE_DIR, 'static')),
|
||||
)
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
# 日志配置
|
||||
LOGGING = {
|
||||
'version': 1, # 版本
|
||||
'disable_existing_loggers': False, # 是否禁用已经存在的日志器
|
||||
'formatters': { # 日志信息显示的格式
|
||||
'verbose': {
|
||||
'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
|
||||
},
|
||||
'simple': {
|
||||
'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
|
||||
},
|
||||
},
|
||||
'filters': { # 过滤器
|
||||
'require_debug_true': { # django在debug模式下才输出日志
|
||||
'()': 'django.utils.log.RequireDebugTrue',
|
||||
},
|
||||
},
|
||||
'handlers': { # 日志处理方法
|
||||
'console': { # 向终端中输出日志
|
||||
'level': 'DEBUG', # 输出等级为“INFO”
|
||||
'filters': ['require_debug_true'],
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'simple'
|
||||
},
|
||||
'file': { # 向文件中输出日志
|
||||
'level': os.getenv('LOGGER_LEVEL') if os.getenv('LOGGER_LEVEL') else 'INFO', # 输出等级为“INFO”
|
||||
# 新增内容
|
||||
# 'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||
# 'filename': os.path.join(BASE_DIR, 'logs/logs.log'),
|
||||
# 'when': 'm',
|
||||
# 'interval': 10,
|
||||
# 'backupCount': 10,
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': BASE_DIR / "db/logs/django_server.log", # 日志文件的位置
|
||||
'maxBytes': 5 * 1024 * 1024, # 日志文件的大小(300*1024*1024为300MB)
|
||||
'backupCount': 10, # 日志文件的数量(超过设定的最大值会自动备份,备份数量最大值为10)
|
||||
'formatter': 'verbose', # 日志输出格式:使用了在之前定义的'verbose'
|
||||
'encoding': 'utf-8' # 新增此行,指定文件编码为UTF-8
|
||||
},
|
||||
},
|
||||
'loggers': { # 日志器
|
||||
'ptools': { # 定义了一个名为django的日志器
|
||||
'handlers': ['console', 'file'], # 可以同时向终端与文件中输出日志
|
||||
'propagate': True, # 是否继续传递日志信息
|
||||
'level': os.getenv('LOGGER_LEVEL') if os.getenv('LOGGER_LEVEL') else 'INFO', # 日志器接收的最低日志级别
|
||||
},
|
||||
'requests': {
|
||||
'level': 'WARNING',
|
||||
'handlers': ['console'], # 或你定义的其他 handler
|
||||
'propagate': False,
|
||||
},
|
||||
'urllib3': {
|
||||
'level': 'WARNING',
|
||||
'handlers': ['console'],
|
||||
'propagate': False,
|
||||
},
|
||||
}
|
||||
NINJA_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15), # 访问令牌有效期
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=3), # 刷新令牌有效期
|
||||
'ROTATE_REFRESH_TOKENS': False,
|
||||
'BLACKLIST_AFTER_ROTATION': False, # 旋转后列入黑名单
|
||||
'UPDATE_LAST_LOGIN': True, # 更新最后登录
|
||||
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
'VERIFYING_KEY': None,
|
||||
'AUDIENCE': None,
|
||||
'ISSUER': None,
|
||||
'JWK_URL': None,
|
||||
'LEEWAY': 0,
|
||||
|
||||
'USER_ID_FIELD': 'id',
|
||||
'USER_ID_CLAIM': 'user_id',
|
||||
'USER_AUTHENTICATION_RULE': 'ninja_jwt.authentication.default_user_authentication_rule',
|
||||
|
||||
# 'AUTH_TOKEN_CLASSES': ('ninja_jwt.tokens.AccessToken',),
|
||||
# 'TOKEN_TYPE_CLAIM': 'token_type',
|
||||
# 'TOKEN_USER_CLASS': 'ninja_jwt.models.TokenUser',
|
||||
|
||||
# 'JTI_CLAIM': 'jti',
|
||||
#
|
||||
# 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
|
||||
# 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
|
||||
# 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
|
||||
|
||||
# For Controller Schemas
|
||||
# FOR OBTAIN PAIR
|
||||
# 'TOKEN_OBTAIN_PAIR_INPUT_SCHEMA': "ninja_jwt.schema.TokenObtainPairInputSchema",
|
||||
# 'TOKEN_OBTAIN_PAIR_REFRESH_INPUT_SCHEMA': "ninja_jwt.schema.TokenRefreshInputSchema",
|
||||
# FOR SLIDING TOKEN
|
||||
# 'TOKEN_OBTAIN_SLIDING_INPUT_SCHEMA': "ninja_jwt.schema.TokenObtainSlidingInputSchema",
|
||||
# 'TOKEN_OBTAIN_SLIDING_REFRESH_INPUT_SCHEMA': "ninja_jwt.schema.TokenRefreshSlidingInputSchema",
|
||||
#
|
||||
# 'TOKEN_BLACKLIST_INPUT_SCHEMA': "ninja_jwt.schema.TokenBlacklistInputSchema",
|
||||
# 'TOKEN_VERIFY_INPUT_SCHEMA': "ninja_jwt.schema.TokenVerifyInputSchema",
|
||||
}
|
||||
|
||||
# 调整POST传输数据文件大小限制
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 25 * 1024 * 1024 * 1024
|
||||
FILE_UPLOAD_MAX_MEMORY_SIZE = 25 * 1024 * 1024 * 1024
|
||||
@@ -274,9 +285,6 @@ CKEDITOR_CONFIGS = {
|
||||
'width': '100%',
|
||||
},
|
||||
}
|
||||
STATIC_URL = '/static/'
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
customColorPalette = [
|
||||
{
|
||||
|
||||
+78
-76
@@ -14,83 +14,41 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
import importlib
|
||||
import pkgutil
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.urls import path, include, re_path
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import traceback
|
||||
import importlib
|
||||
|
||||
import jwt
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import JsonResponse
|
||||
from jwt import exceptions
|
||||
from ninja import NinjaAPI
|
||||
from django.urls import path, include
|
||||
from loguru import logger
|
||||
from ninja.errors import ValidationError
|
||||
from ninja.security import HttpBearer
|
||||
from ninja_extra import NinjaExtraAPI
|
||||
from ninja_jwt.authentication import JWTAuth
|
||||
from ninja_jwt.controller import NinjaJWTDefaultController
|
||||
|
||||
from base.views import custom_upload_file
|
||||
from common.common_response import CommonResponse
|
||||
from .settings import SECRET_KEY
|
||||
|
||||
logger = logging.getLogger('ptools')
|
||||
|
||||
|
||||
class GlobalAuth(HttpBearer):
|
||||
def authenticate(self, request, token):
|
||||
try:
|
||||
logger.debug('用户认证中')
|
||||
logger.debug(f'解析 Auth Token:{token}')
|
||||
res = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
||||
user = User.objects.filter(id=res.get('id'), username=res.get('username')).first()
|
||||
logger.debug(user)
|
||||
if not user:
|
||||
raise exceptions.PyJWKError('用户不存在')
|
||||
request.user = user
|
||||
return token
|
||||
except exceptions.PyJWKError:
|
||||
msg = 'Token 认证失败,请重新登陆!'
|
||||
logger.error(msg)
|
||||
raise
|
||||
except exceptions.ExpiredSignatureError:
|
||||
msg = '认证令牌已过期,请重新登陆!'
|
||||
logger.error(msg)
|
||||
raise
|
||||
except Exception as e:
|
||||
msg = '认证失败,请检查用户是否存在'
|
||||
logger.error(msg)
|
||||
raise
|
||||
|
||||
|
||||
# 2. 获取当前文件所在的目录路径 (假设此文件与各个 app 文件夹同级,或者在特定配置目录下)
|
||||
# 如果你的此文件在项目根目录,而 app 文件夹也在根目录:
|
||||
from django.conf import settings
|
||||
|
||||
project_root = settings.BASE_DIR
|
||||
|
||||
|
||||
# 如果 app 文件夹是 Django 的已安装应用,可能需要通过 settings.INSTALLED_APPS 获取,
|
||||
# 但通常直接扫描物理目录更灵活。这里假设扫描当前目录下的所有子文件夹。
|
||||
|
||||
# 3. 自动发现并注册路由
|
||||
def auto_load_routers(base_path, package_name_prefix=None):
|
||||
def auto_load_routers(package_name_prefix=None):
|
||||
"""
|
||||
base_path: 物理路径,例如 /path/to/project
|
||||
package_name_prefix: 包名前缀,例如 'myproject.' (如果这些文件夹是包的一部分)
|
||||
"""
|
||||
|
||||
base_path = settings.BASE_DIR
|
||||
# 获取该路径下所有子模块/包
|
||||
# 注意:这需要这些文件夹是合法的 Python 包 (即包含 __init__.py),或者在 Python 路径中
|
||||
for importer, modname, ispkg in pkgutil.iter_modules([base_path]):
|
||||
for importer, modname, is_pkg in pkgutil.iter_modules([base_path]):
|
||||
# 过滤条件:
|
||||
# 1. 必须是包 (文件夹)
|
||||
# 2. 不以 '_' 开头 (排除 __pycache__, _private 等)
|
||||
# 3. 排除当前文件所在的脚本名 (防止递归或错误)
|
||||
if ispkg and not modname.startswith('_'):
|
||||
if is_pkg and not modname.startswith('_'):
|
||||
|
||||
# 构造完整的模块路径:例如 "authorize.views"
|
||||
# 如果这些文件夹直接位于根路径且不是大包的一部分,package_name_prefix 可能为空
|
||||
@@ -106,28 +64,84 @@ def auto_load_routers(base_path, package_name_prefix=None):
|
||||
# 检查是否存在 'router' 变量
|
||||
if hasattr(module, 'router'):
|
||||
router = getattr(module, 'router')
|
||||
|
||||
# 使用文件夹名称 (modname) 作为 URL 前缀
|
||||
# 例如: 文件夹 'authorize' -> 前缀 '/authorize'
|
||||
url_prefix = f"/{modname}"
|
||||
|
||||
api_v1.add_router(url_prefix, router)
|
||||
print(f"✅ 自动注册: {url_prefix} (来自 {full_module_path})")
|
||||
logger.info(f"✅ 自动注册: {url_prefix} (来自 {full_module_path})")
|
||||
else:
|
||||
print(f"⚠️ 跳过: {modname} (在 {full_module_path} 中未找到 'router' 变量)")
|
||||
logger.warning(f"⚠️ 跳过: {modname} (在 {full_module_path} 中未找到 'router' 变量)")
|
||||
|
||||
except ImportError as e:
|
||||
# 如果文件夹里没有 views.py,会报 ImportError,这是正常的,跳过即可
|
||||
if "No module named" in str(e) and "views" in str(e):
|
||||
print(f"ℹ️ 跳过: {modname} (没有 views.py 模块)")
|
||||
logger.warning(f"ℹ️ 跳过: {modname} (没有 views.py 模块)")
|
||||
else:
|
||||
print(f"❌ 导入错误 {modname}: {e}")
|
||||
logger.error(f"❌ 导入错误 {modname}: {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ 注册失败 {modname}: {e}")
|
||||
logger.error(f"❌ 注册失败 {modname}: {e}")
|
||||
|
||||
|
||||
api_v1 = NinjaAPI(version='1.0.0', auth=GlobalAuth())
|
||||
auto_load_routers(project_root)
|
||||
class UnifiedNinjaAPI(NinjaExtraAPI):
|
||||
def create_response(self, request, data, *, status=None, headers=None, temporal_response=None):
|
||||
# 1. 如果已经是 CommonResponse,直接放行(避免递归)
|
||||
if isinstance(data, CommonResponse):
|
||||
return super().create_response(request, data, status=status, temporal_response=temporal_response)
|
||||
|
||||
# 2. 处理 (data, status) 元组
|
||||
if isinstance(data, tuple) and len(data) == 2:
|
||||
data, status = data
|
||||
|
||||
# 3. 确保 status 有值,默认为 200
|
||||
if status is None:
|
||||
status = 200
|
||||
|
||||
# ================= 核心修改开始 =================
|
||||
# 4. 嗅探数据内容:判断是否是框架抛出的异常数据
|
||||
# Django Ninja / DRF 的异常通常格式为 {"detail": "错误信息"}
|
||||
# 或者 {"errors": [...]}
|
||||
is_framework_error = False
|
||||
error_message = ""
|
||||
|
||||
if isinstance(data, dict):
|
||||
# 情况 A: 捕获到 {"detail": "权限不足"}
|
||||
if "detail" in data:
|
||||
is_framework_error = True
|
||||
error_message = data.get("detail")
|
||||
# 情况 B: 捕获到其他标准错误格式 (可选)
|
||||
elif "message" in data and status >= 400:
|
||||
is_framework_error = True
|
||||
error_message = data.get("message")
|
||||
|
||||
# 5. 根据嗅探结果决定返回 success 还是 error
|
||||
if is_framework_error:
|
||||
# 强制返回 error 结构,succeed=False
|
||||
# 注意:这里通常使用 403 或 500 作为 code,或者保留原始 status
|
||||
return super().create_response(
|
||||
request,
|
||||
CommonResponse.error(code=status, data=data, msg=error_message),
|
||||
status=status,
|
||||
temporal_response=temporal_response
|
||||
)
|
||||
|
||||
# 如果不是错误,继续正常的成功逻辑
|
||||
if 200 <= status < 300:
|
||||
wrapped_data = CommonResponse.success(data=data)
|
||||
else:
|
||||
# 其他非 200 状态码视为业务错误
|
||||
wrapped_data = CommonResponse.error(code=status, data=data)
|
||||
|
||||
return super().create_response(request, wrapped_data, status=status, temporal_response=temporal_response)
|
||||
|
||||
|
||||
api_v1 = UnifiedNinjaAPI(version='1.0.0', auth=JWTAuth())
|
||||
api_v1.register_controllers(NinjaJWTDefaultController)
|
||||
|
||||
auto_load_routers()
|
||||
|
||||
|
||||
# logger.debug(api_v1.urls)
|
||||
|
||||
|
||||
@api_v1.exception_handler(ValidationError)
|
||||
@@ -138,18 +152,6 @@ def validation_errors(request, exc):
|
||||
return JsonResponse(data=CommonResponse.error(msg=f"输入参数验证失败!").to_dict(), status=422, safe=False)
|
||||
|
||||
|
||||
@api_v1.exception_handler(exceptions.PyJWKError) # 捕获 HttpBearer 认证异常
|
||||
def custom_http_bearer_exception_handler(request, exc):
|
||||
logger.error(exc)
|
||||
return JsonResponse(data=CommonResponse.error(msg=f"用户Token认证失败:{exc}").to_dict(), status=401)
|
||||
|
||||
|
||||
@api_v1.exception_handler(exceptions.ExpiredSignatureError) # 捕获 HttpBearer 认证异常
|
||||
def custom_http_bearer_exception_handler(request, exc):
|
||||
logger.error(exc)
|
||||
return JsonResponse(data=CommonResponse.error(msg=f"用户登录信息已过期,请重新登录!!").to_dict(), status=401)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', include("front.urls")),
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
+16
-13
@@ -169,13 +169,16 @@ body {
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
background: #1e3a8a; /* 悬停时变为主题色 */
|
||||
background: #c62828; /* 悬停时变为主题色 */
|
||||
color: #fff; /* 图标变白 */
|
||||
width: 30px; /* 悬停时稍微变大 */
|
||||
height: 30px; /* 悬停时稍微变大 */
|
||||
}
|
||||
|
||||
/* 如果使用的是 FontAwesome 等图标库,确保图标大小合适 */
|
||||
.search-btn i {
|
||||
font-size: 18px;
|
||||
.search-btn img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.user-icons {
|
||||
@@ -241,7 +244,7 @@ body {
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item {
|
||||
margin-bottom: -3px; /* 负边距,让 Tab 沉下去盖住强调线 */
|
||||
/*margin-bottom: -3px; !* 负边距,让 Tab 沉下去盖住强调线 *!*/
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@@ -251,7 +254,7 @@ body {
|
||||
border-top-right-radius: 4px;
|
||||
|
||||
/* 4. 字体大小修正 */
|
||||
font-size: 15px;
|
||||
/*font-size: 15px;*/
|
||||
font-weight: 600; /* 加粗 */
|
||||
|
||||
color: #555; /* 未选中时的文字颜色 */
|
||||
@@ -364,16 +367,16 @@ body {
|
||||
}
|
||||
|
||||
/* --- 自定义样式结束 --- */
|
||||
|
||||
.ad-banner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 30px 0 15px 0;
|
||||
/* 可选:给图片加点样式,让它更好看 */
|
||||
.link-image {
|
||||
border-radius: 8px; /* 圆角 */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 阴影 */
|
||||
transition: transform 0.2s; /* 悬停动画 */
|
||||
}
|
||||
|
||||
.ad-item {
|
||||
width: 280px;
|
||||
height: 120px;
|
||||
/* 鼠标悬停时图片微微放大 */
|
||||
.link-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.grid {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1775128526851" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5418" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M644.096 251.904a277.333333 277.333333 0 1 0-392.192 392.192 277.333333 277.333333 0 0 0 392.192-392.192zM191.573333 191.573333a362.666667 362.666667 0 0 1 541.269334 480.938667l228.053333 228.053333-60.330667 60.330667-228.053333-228.053333A362.709333 362.709333 0 0 1 191.573333 191.573333z" fill="#23418A" p-id="5419"></path></svg>
|
||||
|
After Width: | Height: | Size: 665 B |
+51
-1
@@ -7,6 +7,7 @@
|
||||
<title>{% block title %}{% endblock %}{{ site_info.title }}</title>
|
||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'admin/simpleui-x/fontawesome-free-6.2.0-web/css/fontawesome.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top-header">
|
||||
@@ -16,7 +17,7 @@
|
||||
<div>
|
||||
<h1>{{ site_info.title }}</h1>
|
||||
<p>{{ site_info.e_title }}</p>
|
||||
<p>CHANGBAI MOUNTAIN FIRE AND RESCUE BRIGADE</p>
|
||||
{# <p>CHANGBAI MOUNTAIN FIRE AND RESCUE BRIGADE</p>#}
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
@@ -159,10 +160,59 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<button id="backToTop"
|
||||
class="btn btn-primary rounded-circle shadow d-none"
|
||||
style="position: fixed; bottom: 30px; right: 30px; width: 50px; height: 50px; z-index: 1000; font-size: 20px;"
|
||||
title="回到顶部">
|
||||
↑
|
||||
</button>
|
||||
|
||||
|
||||
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
|
||||
<!-- 2. JavaScript 逻辑 -->
|
||||
<script>
|
||||
// 获取按钮元素
|
||||
const backToTopBtn = document.getElementById("backToTop");
|
||||
|
||||
// 监听页面滚动事件
|
||||
window.addEventListener("scroll", function () {
|
||||
// 当页面向下滚动超过 300px 时显示按钮,否则隐藏
|
||||
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
|
||||
backToTopBtn.classList.remove("d-none");
|
||||
// 可选:添加淡入动画类
|
||||
backToTopBtn.classList.add("fade-in");
|
||||
} else {
|
||||
backToTopBtn.classList.add("d-none");
|
||||
backToTopBtn.classList.remove("fade-in");
|
||||
}
|
||||
});
|
||||
|
||||
// 监听按钮点击事件
|
||||
backToTopBtn.addEventListener("click", function () {
|
||||
// 平滑滚动到顶部
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 3. 可选:添加简单的淡入淡出 CSS 动画 -->
|
||||
<style>
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- 页面底部脚本 -->
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ article.section.title }} - 长白山消防救援支队{% endblock %}
|
||||
{% block title %}{{ article.section.title }} - {{ site_info.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
@@ -9,8 +9,12 @@
|
||||
<ol class="breadcrumb" style="background-color: transparent !important; padding-bottom: 0;">
|
||||
<li class="breadcrumb-item"><a href="/">网站首页</a></li>
|
||||
<!-- 动态显示所属板块 -->
|
||||
<li class="breadcrumb-item"><a href="#">{{ article.section.parent.title }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="#">{{ article.section.title }}</a></li>
|
||||
<li class="breadcrumb-item"><a
|
||||
href="{% url "front:main_list" main=article.section.parent.code %}">{{ article.section.parent.title }}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item"><a
|
||||
href="{% url "front:sub_list" main=article.section.parent.code sub=article.section.code %}">{{ article.section.title }}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">文章详情</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ article.section.title }} - 长白山消防救援支队{% endblock %}
|
||||
{% block title %}{{ article.section.title }} - {{ site_info.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
|
||||
+69
-67
@@ -5,80 +5,82 @@
|
||||
{% block title %}首页 - {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- 顶部横幅 -->
|
||||
<div class="row d-none d-md-block">
|
||||
<div class="col-12">
|
||||
<img src="{% static 'images/slogan_bg.png' %}" alt="标语背景"
|
||||
style="object-fit: fill;max-width: 1160px;width: 100%;">
|
||||
{# <h2>对党忠诚 纪律严明 赴汤蹈火 竭诚为民</h2>#}
|
||||
<div class="container">
|
||||
<!-- 顶部横幅 -->
|
||||
<div class="row d-none d-md-block">
|
||||
<div class="col-12">
|
||||
<img src="{% static 'images/slogan_bg.png' %}" alt="标语背景"
|
||||
style="object-fit: fill;max-width: 1160px;width: 100%;">
|
||||
{# <h2>对党忠诚 纪律严明 赴汤蹈火 竭诚为民</h2>#}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row d-none d-md-block" style="padding:0 5px;">
|
||||
<div class="col-12 date-bar">
|
||||
<span style="font-size: 14px;padding-left: 25px;">{{ time_str }}</span>
|
||||
<div class="user-icons">
|
||||
<form class="search-form" action="{% url 'front:search_list' %}">
|
||||
<div class="search-wrapper">
|
||||
<input type="text" class="search-input" name="q">
|
||||
<button type="submit" class="search-btn">
|
||||
<img src="{% static "icons/search.svg" %}" alt="">
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="row d-none d-md-block" style="padding:0 5px;">
|
||||
<div class="col-12 date-bar">
|
||||
<span style="font-size: 14px;padding-left: 25px;">{{ time_str }}</span>
|
||||
<div class="user-icons">
|
||||
<form class="search-form">
|
||||
<div class="search-wrapper">
|
||||
<input type="text" class="search-input">
|
||||
<button type="submit" class="search-btn">
|
||||
<img src="{% static "icons/search.svg" %}" alt="">
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<img src="{% static "icons/user.svg" %}" alt="">
|
||||
<img src="{% static "icons/edit.svg" %}" alt="">
|
||||
<img src="{% static "icons/sina.svg" %}" alt="">
|
||||
<img src="{% static "icons/wechat.svg" %}" alt="">
|
||||
<img src="{% static "icons/douyin.svg" %}" alt="">
|
||||
<img src="{% static "icons/user.svg" %}" alt="">
|
||||
<img src="{% static "icons/edit.svg" %}" alt="">
|
||||
<img src="{% static "icons/sina.svg" %}" alt="">
|
||||
<img src="{% static "icons/wechat.svg" %}" alt="">
|
||||
<img src="{% static "icons/douyin.svg" %}" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主要内容区 -->
|
||||
<!-- 左侧大图 -->
|
||||
<div class="row" style="height: 300px;">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
{% include 'components/_carousel.html' with carousel_id=carousel.key slides=carousel.data height="300px" %}
|
||||
<!-- 主要内容区 -->
|
||||
<!-- 左侧大图 -->
|
||||
<div class="row" style="height: 300px;">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
{% include 'components/_carousel.html' with carousel_id=carousel.key slides=carousel.data height="300px" %}
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
{% url "front:news_main" as more_url %}
|
||||
{% include 'components/_news_card.html' with data=news_data.data unique_id=news_data.key more_url=more_url route="front:news_detail" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
{% url "front:news_main" as more_url %}
|
||||
{% include 'components/_news_card.html' with data=news_data.data unique_id=news_data.key more_url=more_url route="front:news_detail" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 广告横幅 -->
|
||||
<div class="ad-banner row">
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
<img class="ad-item" src="/media/upload/images/2026/03/25/img.png" alt="我为十五五规划献策">
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
<img class="ad-item" src="/media/upload/images/2026/03/25/img_1.png" alt="精彩火焰蓝">
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
<img class="ad-item" src="/media/upload/images/2026/03/25/img_2.png" alt="电动自行车安全">
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
<img class="ad-item" src="/media/upload/images/2026/03/25/img_3.png" alt="消防安全培训">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
{% for section in section_data %}
|
||||
{% if forloop.revcounter == 1 and forloop.counter|add:"0"|divisibleby:"2" == False %}
|
||||
<!-- 最后一项,且总数为奇数,则独占一行 -->
|
||||
<div class="col-12" style="margin: 15px auto">
|
||||
{% url "front:main_list" main=section.key as card_more_url %}
|
||||
{% include 'components/_news_card.html' with data=section.data unique_id=section.key more_url=card_more_url %}
|
||||
<div class="row g-4 justify-content-start mt-3">
|
||||
{% for link in picture_links %}
|
||||
<!--
|
||||
col-6: 手机端一行2个
|
||||
col-md-4: 平板一行3个
|
||||
col-lg-3: 电脑端一行4个 (25%)
|
||||
-->
|
||||
<div class="col-6 col-md-4 col-lg-3">
|
||||
<a href="{{ link.url }}" class="d-block" target="_blank">
|
||||
<!-- img-fluid 让图片宽度 100% 自适应父容器 -->
|
||||
<img class="img-fluid" src="{{ link.picture.url }}" alt="{{ link.name }}">
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-md-6 col-sm-12" style="margin: 15px auto">
|
||||
{% url "front:main_list" main=section.key as card_more_url %}
|
||||
{% include 'components/_news_card.html' with data=section.data unique_id=section.key more_url=card_more_url %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
{% for section in section_data %}
|
||||
{% if forloop.revcounter == 1 and forloop.counter|add:"0"|divisibleby:"2" == False %}
|
||||
<!-- 最后一项,且总数为奇数,则独占一行 -->
|
||||
<div class="col-12" style="margin: 15px auto">
|
||||
{% url "front:main_list" main=section.key as card_more_url %}
|
||||
{% include 'components/_news_card.html' with data=section.data unique_id=section.key more_url=card_more_url %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-md-6 col-sm-12" style="margin: 15px auto">
|
||||
{% url "front:main_list" main=section.key as card_more_url %}
|
||||
{% include 'components/_news_card.html' with data=section.data unique_id=section.key more_url=card_more_url %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+60
-22
@@ -1,23 +1,34 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% block title %}{{ main_section.title }} - {% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<nav aria-label="breadcrumb" style="margin-left: -10px;">
|
||||
<ol class="breadcrumb" style="background-color: transparent !important;padding-bottom: 0;">
|
||||
<li class="breadcrumb-item"><a href="/" style="color: #23408A; text-decoration: none;">网站首页</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
<a href="{% url "front:main_list" main=main_section.code %}"
|
||||
style="color: #23408A; text-decoration: none;"
|
||||
>
|
||||
{{ main_section.title }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
{{ sub_section.title }}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="container row align-items-center justify-content-between">
|
||||
<nav aria-label="breadcrumb" style="margin-left: -10px;">
|
||||
<ol class="breadcrumb" style="background-color: transparent !important;padding-bottom: 0;">
|
||||
<li class="breadcrumb-item"><a href="/" style="color: #23408A; text-decoration: none;">网站首页</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
<a href="{% url "front:main_list" main=main_section.code %}"
|
||||
style="color: #23408A; text-decoration: none;"
|
||||
>
|
||||
{{ main_section.title }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
{{ sub_section.title }}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<form class="search-form" action="{% url 'front:search_list' %}">
|
||||
<div class="search-wrapper" style="border: solid 1px #23408a85">
|
||||
<input type="text" class="search-input" name="q">
|
||||
<button type="submit" class="search-btn">
|
||||
<img src="{% static "icons/search_23418A.svg" %}" alt="" style="border-radius: 15px;">
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row" style="height: 100%;margin: 15px auto;margin-right: 0 !important;">
|
||||
<div class="col-3">
|
||||
<ul class="list-group">
|
||||
@@ -42,6 +53,27 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% elif '/search' in request.path %}
|
||||
<li class="list-group-item" style="background: #23418A; color: #f5f5f5">
|
||||
<a href="{% url "front:main_list" main=main_section.code %}"
|
||||
style="color: #F5F5F5;text-decoration: none;">
|
||||
<h5 style="margin: 0; padding: 0; line-height: 1.2;">{{ main_section.title }}</h5>
|
||||
</a>
|
||||
</li>
|
||||
{% for sec in sections %}
|
||||
{% if sec.code == current_sec.code %}
|
||||
<li class="list-group-item" style="background: #e9ecef;">
|
||||
<a href="{% url "front:search_section_list" sec=current_sec %}?{% if q %}q={{ q }}{% endif %}"
|
||||
style="color: #23418A; text-decoration: none; font-weight: bold;"> {{ sec.title }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="list-group-item">
|
||||
<a href="{% url "front:search_section_list" sec=sec.code %}?{% if q %}q={{ q }}{% endif %}"
|
||||
style="color: #1d2124; text-decoration: none;"> {{ sec.title }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item" style="background: #23418A; color: #f5f5f5">
|
||||
<a href="{% url "front:main_list" main=main_section.code %}"
|
||||
@@ -57,10 +89,11 @@
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="list-group-item">
|
||||
<a href="{% url "front:sub_list" main=main_section.code sub=sec.code %}"
|
||||
<a href="{% url "front:sub_list" main=main_section.code|default:sec.parent.code sub=sec.code %}"
|
||||
style="color: #1d2124; text-decoration: none;"> {{ sec.title }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
@@ -121,7 +154,8 @@
|
||||
<!-- 首页 -->
|
||||
{% if articles.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">首页</a>
|
||||
<a class="page-link"
|
||||
href="?page=1{% if q %}&q={{ q }}{% endif %}">首页</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
@@ -132,7 +166,8 @@
|
||||
<!-- 上一页 -->
|
||||
{% if articles.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ articles.previous_page_number }}"
|
||||
<a class="page-link"
|
||||
href="?page={{ articles.previous_page_number }}{% if q %}&q={{ q }}{% endif %}"
|
||||
aria-label="Previous">
|
||||
<span>上一页</span>
|
||||
</a>
|
||||
@@ -154,7 +189,7 @@
|
||||
{# 显示规则:第1-2页 | 当前页前后各2页 | 最后2页 #}
|
||||
{% if i == 1 or i == 2 or i == articles.paginator.num_pages or i == articles.paginator.num_pages|add:"-1" or i >= articles.number|add:"-2" and i <= articles.number|add:"2" %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ i }}">{{ i }}</a>
|
||||
<a class="page-link" href="?page={{ i }}{% if q %}&q={{ q }}{% endif %}">{{ i }}</a>
|
||||
</li>
|
||||
{% elif i == articles.number|add:"-3" or i == articles.number|add:"3" %}
|
||||
{# 省略号只出现一次 #}
|
||||
@@ -168,7 +203,9 @@
|
||||
<!-- 下一页 -->
|
||||
{% if articles.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ articles.next_page_number }}" aria-label="Next">
|
||||
<a class="page-link"
|
||||
href="?page={{ articles.next_page_number }}{% if q %}&q={{ q }}{% endif %}"
|
||||
aria-label="Next">
|
||||
<span>下一页</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -181,7 +218,8 @@
|
||||
<!-- 尾页 -->
|
||||
{% if articles.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ articles.paginator.num_pages }}"
|
||||
<a class="page-link"
|
||||
href="?page={{ articles.paginator.num_pages }}{% if q %}&q={{ q }}{% endif %}"
|
||||
aria-label="Last">
|
||||
<span>尾页</span>
|
||||
</a>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{% load static %}
|
||||
<header class="site-header">
|
||||
<div class="header-top">
|
||||
<div class="logo-container">
|
||||
<img src="{% static 'images/logo.png' %}" alt="长白山消防救援支队" class="logo">
|
||||
<h1>长白山消防救援支队</h1>
|
||||
<p>CHANGBAI MOUNTAIN FIRE AND RESCUE BRIGADE</p>
|
||||
</div>
|
||||
<div class="top-links">
|
||||
<a href="#" aria-label="搜索"><i class="icon-search"></i></a>
|
||||
<a href="#" aria-label="用户中心"><i class="icon-user"></i></a>
|
||||
<a href="#" aria-label="消息"><i class="icon-message"></i></a>
|
||||
<a href="#" aria-label="登录"><i class="icon-login"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<nav class="main-navigation">
|
||||
<ul class="nav-list">
|
||||
<li><a href="{% url 'index' %}" class="{% if request.resolver_match.url_name == 'index' %}active{% endif %}">网站首页</a></li>
|
||||
<li><a href="{% url 'about' %}" class="{% if request.resolver_match.url_name == 'about' %}active{% endif %}">单位简介</a></li>
|
||||
<li><a href="{% url 'news' %}" class="{% if request.resolver_match.url_name == 'news' %}active{% endif %}">新闻中心</a></li>
|
||||
<li><a href="{% url 'info' %}" class="{% if request.resolver_match.url_name == 'info' %}active{% endif %}">信息公开</a></li>
|
||||
<li><a href="{% url 'service' %}" class="{% if request.resolver_match.url_name == 'service' %}active{% endif %}">办事服务</a></li>
|
||||
<li><a href="{% url 'party' %}" class="{% if request.resolver_match.url_name == 'party' %}active{% endif %}">党建引领</a></li>
|
||||
<li><a href="{% url 'interaction' %}" class="{% if request.resolver_match.url_name == 'interaction' %}active{% endif %}">互动交流</a></li>
|
||||
<li><a href="{% url 'science' %}" class="{% if request.resolver_match.url_name == 'science' %}active{% endif %}">消防科普</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 轮播图/大背景图 -->
|
||||
<div class="hero-banner">
|
||||
<img src="{% static 'images/banner.jpg' %}" alt="雪地消防员" class="banner-img">
|
||||
</div>
|
||||
</header>
|
||||
@@ -1,11 +0,0 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>长白山消防支队</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+147
-24
@@ -1,17 +1,109 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.utils.html import format_html
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from .models import Department, Role, Menu
|
||||
from .models import User
|
||||
from .models import Department
|
||||
from .models import Role, Permission
|
||||
|
||||
|
||||
# ========== 菜单管理 ==========
|
||||
@admin.register(Menu)
|
||||
class MenuAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'name', 'code', 'menu_type_display', 'path', 'permission_code', 'parent_link', 'order_num', 'status')
|
||||
list_filter = ('menu_type', 'status', 'parent')
|
||||
search_fields = ('name', 'code', 'permission_code')
|
||||
ordering = ('order_num', 'id')
|
||||
list_editable = ('order_num', 'status') # 允许在列表页直接编辑
|
||||
list_per_page = 20
|
||||
|
||||
# 自定义字段:显示菜单类型
|
||||
def menu_type_display(self, obj):
|
||||
types = {0: '目录', 1: '菜单', 2: '按钮'}
|
||||
return types.get(obj.menu_type, '未知')
|
||||
|
||||
menu_type_display.short_description = '类型'
|
||||
|
||||
# 自定义字段:父菜单链接(可点击)
|
||||
def parent_link(self, obj):
|
||||
if obj.parent:
|
||||
url = f"/admin/users/menu/{obj.parent.id}/change/"
|
||||
return format_html('<a href="{}">{}</a>', url, obj.parent.name)
|
||||
return '-'
|
||||
|
||||
parent_link.short_description = '父菜单'
|
||||
|
||||
|
||||
# ========== 角色管理 ==========
|
||||
class RoleMenuInline(admin.TabularInline):
|
||||
"""
|
||||
内联管理:在角色编辑页直接分配菜单
|
||||
"""
|
||||
model = Role.menus.through # 使用自动创建的中间表
|
||||
verbose_name = "菜单权限"
|
||||
verbose_name_plural = "菜单权限"
|
||||
extra = 1 # 默认显示1个空行
|
||||
|
||||
# 优化显示:只显示关键字段
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == "menu":
|
||||
# 可以在这里过滤菜单选项,例如只显示状态为True的
|
||||
kwargs["queryset"] = Menu.objects.filter(status=True)
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'code', 'description', 'user_count', 'menu_count', 'status', 'created_at')
|
||||
list_filter = ('status', 'created_at')
|
||||
search_fields = ('name', 'code', 'description')
|
||||
ordering = ('-created_at',)
|
||||
list_per_page = 20
|
||||
|
||||
# 使用内联管理菜单权限
|
||||
inlines = [RoleMenuInline]
|
||||
|
||||
# 排除 menus 字段,因为已经在内联中处理
|
||||
exclude = ('menus',)
|
||||
|
||||
# 自定义字段:关联用户数量
|
||||
def user_count(self, obj):
|
||||
count = obj.users.count()
|
||||
if count:
|
||||
url = f"/admin/users/user/?roles__id__exact={obj.id}"
|
||||
return format_html('<a href="{}">{}</a>', url, count)
|
||||
return 0
|
||||
|
||||
user_count.short_description = '用户数'
|
||||
|
||||
# 自定义字段:关联菜单数量
|
||||
def menu_count(self, obj):
|
||||
count = obj.menus.count()
|
||||
if count:
|
||||
url = f"/admin/users/menu/?roles__id__exact={obj.id}"
|
||||
return format_html('<a href="{}">{}</a>', url, count)
|
||||
return 0
|
||||
|
||||
menu_count.short_description = '菜单数'
|
||||
|
||||
|
||||
# ========== 用户管理 ==========
|
||||
class UserRoleInline(admin.TabularInline):
|
||||
"""
|
||||
内联管理:在用户编辑页直接分配角色
|
||||
"""
|
||||
model = User.roles.through
|
||||
verbose_name = "角色"
|
||||
verbose_name_plural = "角色"
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||
list_display = ('username', 'cname', 'dept', 'is_active', 'is_staff')
|
||||
list_display = ('username', 'cname', 'dept', 'phone', 'is_active', 'is_staff')
|
||||
list_filter = ('is_active', 'is_staff', 'dept')
|
||||
search_fields = ('username', 'cname')
|
||||
search_fields = ('username', 'cname', 'phone',)
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
('个人信息', {'fields': ('cname', 'dept')}),
|
||||
@@ -24,33 +116,64 @@ class UserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||
'fields': ('username', 'cname', 'password1', 'password2', 'dept'),
|
||||
}),
|
||||
)
|
||||
ordering = ('username',)
|
||||
ordering = ('username', '-date_joined',)
|
||||
filter_horizontal = ('groups', 'user_permissions', 'roles') # 如果 roles 是 ManyToMany
|
||||
list_per_page = 20
|
||||
# 使用内联管理角色
|
||||
inlines = [UserRoleInline]
|
||||
|
||||
# 排除 roles 字段,因为已经在内联中处理
|
||||
exclude = ('roles',)
|
||||
|
||||
# 自定义字段:显示所有角色名称
|
||||
def role_names(self, obj):
|
||||
roles = obj.roles.all()
|
||||
if roles:
|
||||
names = ', '.join([role.name for role in roles])
|
||||
return names[:50] + '...' if len(names) > 50 else names
|
||||
return '-'
|
||||
|
||||
role_names.short_description = '角色'
|
||||
|
||||
|
||||
# ========== 部门管理 ==========
|
||||
@admin.register(Department)
|
||||
class DepartmentAdmin(ImportExportModelAdmin):
|
||||
list_display = ('name', 'parent', 'order')
|
||||
list_filter = ('parent',)
|
||||
search_fields = ('name',)
|
||||
ordering = ('order', 'name')
|
||||
class DepartmentAdmin(admin.ModelAdmin):
|
||||
list_display = ('name_with_indent', 'parent_link', 'order', 'user_count')
|
||||
list_editable = ('order',)
|
||||
ordering = ('order', 'id')
|
||||
list_per_page = 30
|
||||
|
||||
def get_queryset(self, request):
|
||||
# 优化查询,避免 N+1
|
||||
return super().get_queryset(request).select_related('parent')
|
||||
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(ImportExportModelAdmin):
|
||||
list_display = ('name', 'permission_count')
|
||||
search_fields = ('name',)
|
||||
filter_horizontal = ('permissions',)
|
||||
def name_with_indent(self, obj):
|
||||
"""根据层级缩进显示部门名称"""
|
||||
level = 0
|
||||
current = obj.parent
|
||||
while current:
|
||||
level += 1
|
||||
current = current.parent
|
||||
indent = "=>" * level
|
||||
return format_html('{}{}', indent, obj.name)
|
||||
|
||||
def permission_count(self, obj):
|
||||
return obj.permissions.count()
|
||||
name_with_indent.short_description = "部门名称"
|
||||
name_with_indent.allow_tags = True
|
||||
|
||||
permission_count.short_description = "权限数量"
|
||||
def parent_link(self, obj):
|
||||
if obj.parent:
|
||||
url = f"/admin/users/department/{obj.parent.id}/change/"
|
||||
return format_html('<a href="{}">{}</a>', url, obj.parent.name)
|
||||
return '-'
|
||||
|
||||
parent_link.short_description = "上级部门"
|
||||
|
||||
@admin.register(Permission)
|
||||
class PermissionAdmin(ImportExportModelAdmin):
|
||||
list_display = ('resource', 'action', 'desc')
|
||||
list_filter = ('resource', 'action')
|
||||
search_fields = ('resource', 'desc')
|
||||
ordering = ('resource', 'action')
|
||||
def user_count(self, obj):
|
||||
count = obj.user_set.count()
|
||||
if count:
|
||||
url = f"/admin/users/user/?department__id__exact={obj.id}"
|
||||
return format_html('<a href="{}">{}</a>', url, count)
|
||||
return 0
|
||||
|
||||
user_count.short_description = "用户数"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# api/departments.py
|
||||
from typing import List
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from ninja import Router
|
||||
from ninja.errors import HttpError
|
||||
|
||||
from users.models import Department
|
||||
from users.permission import has_permission, require_permissions
|
||||
from users.schema import DepartmentOut, DepartmentCreate, DepartmentUpdate
|
||||
|
||||
router = Router(tags=['depts'])
|
||||
|
||||
|
||||
@router.get("/", response=List[DepartmentOut], summary="获取部门列表(扁平)")
|
||||
@require_permissions("dept:list")
|
||||
def list_departments(request):
|
||||
return Department.objects.all()
|
||||
|
||||
|
||||
@router.get("/tree/", response=List[dict], summary="获取部门树形结构")
|
||||
@require_permissions("dept:list")
|
||||
def get_department_tree(request):
|
||||
depts = Department.objects.all().order_by('order')
|
||||
return build_dept_tree(list(depts))
|
||||
|
||||
|
||||
@router.post("/", response=DepartmentOut, summary="创建部门")
|
||||
@require_permissions("dept:create")
|
||||
def create_department(request, payload: DepartmentCreate):
|
||||
if not has_permission(request.auth.id, "dept:create"):
|
||||
raise HttpError(403, "权限不足")
|
||||
|
||||
# 👇 新增:校验根部门唯一性
|
||||
if payload.parent_id is None:
|
||||
if Department.objects.filter(parent__isnull=True).exists():
|
||||
raise HttpError(400, "根部门已存在,无法创建新的根部门")
|
||||
|
||||
data = payload.dict()
|
||||
parent_id = data.pop('parent_id', None)
|
||||
if parent_id:
|
||||
# 可以额外校验 parent_id 是否真实存在
|
||||
if not Department.objects.filter(id=parent_id).exists():
|
||||
raise HttpError(400, "指定的上级部门不存在")
|
||||
data['parent_id'] = parent_id
|
||||
|
||||
try:
|
||||
dept = Department.objects.create(**data)
|
||||
return dept
|
||||
except IntegrityError as e:
|
||||
# 捕获数据库层面的唯一性冲突,提供统一错误信息
|
||||
if "unique_root_department" in str(e):
|
||||
raise HttpError(400, "根部门已存在,无法创建新的根部门")
|
||||
else:
|
||||
raise HttpError(400, "创建部门失败,请检查数据")
|
||||
|
||||
|
||||
@router.get("/{dept_id}/", response=DepartmentOut, summary="获取部门详情")
|
||||
@require_permissions("dept:view")
|
||||
def get_department(request, dept_id: int):
|
||||
dept = get_object_or_404(Department, id=dept_id)
|
||||
return dept
|
||||
|
||||
|
||||
@router.put("/{dept_id}/", response=DepartmentOut, summary="更新部门")
|
||||
@require_permissions("dept:update")
|
||||
def update_department(request, dept_id: int, payload: DepartmentUpdate):
|
||||
dept = get_object_or_404(Department, id=dept_id)
|
||||
data = payload.dict()
|
||||
parent_id = data.pop('parent_id', None)
|
||||
if parent_id:
|
||||
data['parent_id'] = parent_id
|
||||
else:
|
||||
data['parent'] = None
|
||||
|
||||
for attr, value in data.items():
|
||||
setattr(dept, attr, value)
|
||||
dept.save()
|
||||
return dept
|
||||
|
||||
|
||||
@router.delete("/{dept_id}/", summary="删除部门")
|
||||
@require_permissions("dept:delete")
|
||||
def delete_department(request, dept_id: int):
|
||||
dept = get_object_or_404(Department, id=dept_id)
|
||||
# 可选:检查是否有子部门或用户
|
||||
if dept.user_set.exists() or Department.objects.filter(parent=dept).exists():
|
||||
return 400, {"detail": "部门包含用户或子部门,无法删除"}
|
||||
|
||||
dept.delete()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# 工具函数:构建部门树
|
||||
def build_dept_tree(depts, parent_id=None):
|
||||
tree = []
|
||||
for dept in depts:
|
||||
if dept.parent_id == parent_id:
|
||||
node = {
|
||||
'id': dept.id,
|
||||
'name': dept.name,
|
||||
'parent_id': dept.parent_id,
|
||||
'order': dept.order,
|
||||
'children': build_dept_tree(depts, dept.id) or None
|
||||
}
|
||||
tree.append(node)
|
||||
return tree
|
||||
@@ -0,0 +1,526 @@
|
||||
# generate_test_data.py
|
||||
import os
|
||||
|
||||
import django
|
||||
|
||||
# 设置 Django 环境
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
django.setup()
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from users.models import Department, Menu, Role, User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '生成有逻辑的测试数据(可重复运行,自动跳过已存在项)'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write("🚀 开始生成测试数据(幂等模式)...")
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
departments = self._create_departments()
|
||||
menus = self._create_menus()
|
||||
roles = self._create_roles(menus)
|
||||
self._create_users(departments, roles)
|
||||
except Exception as e:
|
||||
self.stderr.write(f"❌ 生成失败: {e}")
|
||||
return
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("✅ 测试数据生成完成!可安全重复运行。")
|
||||
)
|
||||
self.stdout.write("\n📌 登录信息:")
|
||||
self.stdout.write(" - 超级管理员: admin / admin123")
|
||||
self.stdout.write(" - 技术经理: techmgr / tech123")
|
||||
self.stdout.write(" - HR专员: hr1 / hr123")
|
||||
|
||||
def _get_or_create_dept(self, name, parent=None, order=0):
|
||||
"""获取或创建部门(按名称+父部门唯一)"""
|
||||
dept, created = Department.objects.get_or_create(
|
||||
name=name,
|
||||
parent=parent,
|
||||
defaults={'order': order}
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(f" ➕ 部门: {dept.name}")
|
||||
return dept
|
||||
|
||||
def _create_departments(self):
|
||||
self.stdout.write("🔄 同步部门结构...")
|
||||
company = self._get_or_create_dept("总公司", None, 0)
|
||||
tech = self._get_or_create_dept("技术部", company, 1)
|
||||
hr = self._get_or_create_dept("人力资源部", company, 2)
|
||||
finance = self._get_or_create_dept("财务部", company, 3)
|
||||
backend = self._get_or_create_dept("后端开发组", tech, 1)
|
||||
frontend = self._get_or_create_dept("前端开发组", tech, 2)
|
||||
recruitment = self._get_or_create_dept("招聘组", hr, 1)
|
||||
|
||||
return {
|
||||
'company': company,
|
||||
'tech': tech,
|
||||
'hr': hr,
|
||||
'finance': finance,
|
||||
'backend': backend,
|
||||
'frontend': frontend,
|
||||
'recruitment': recruitment,
|
||||
}
|
||||
|
||||
def _get_or_create_menu(self, code, defaults=None):
|
||||
"""按 code 获取或创建菜单"""
|
||||
menu, created = Menu.objects.get_or_create(code=code, defaults=defaults)
|
||||
if created:
|
||||
self.stdout.write(f" ➕ 菜单: {menu.name} ({menu.code})")
|
||||
return menu
|
||||
|
||||
def _create_menus(self):
|
||||
self.stdout.write("🔄 同步菜单结构...")
|
||||
|
||||
# ========================
|
||||
# 1. 系统管理(保持不变)
|
||||
# ========================
|
||||
system = self._get_or_create_menu("system", {
|
||||
"name": "系统管理",
|
||||
"menu_type": 0,
|
||||
"permission_code": "system:settings",
|
||||
"order_num": 1
|
||||
})
|
||||
|
||||
user_menu = self._get_or_create_menu("user", {
|
||||
"name": "用户管理",
|
||||
"parent": system,
|
||||
"path": "/system/user",
|
||||
"component": "system/UserManage",
|
||||
"permission_code": "user:list",
|
||||
"menu_type": 1,
|
||||
"order_num": 1
|
||||
})
|
||||
role_menu = self._get_or_create_menu("role", {
|
||||
"name": "角色管理",
|
||||
"parent": system,
|
||||
"path": "/system/role",
|
||||
"component": "system/RoleManage",
|
||||
"permission_code": "role:list",
|
||||
"menu_type": 1,
|
||||
"order_num": 2
|
||||
})
|
||||
menu_menu = self._get_or_create_menu("menu", {
|
||||
"name": "菜单管理",
|
||||
"parent": system,
|
||||
"path": "/system/menu",
|
||||
"component": "system/Menu",
|
||||
"permission_code": "menu:list",
|
||||
"menu_type": 1,
|
||||
"order_num": 3
|
||||
})
|
||||
dept_menu = self._get_or_create_menu("dept", {
|
||||
"name": "部门管理",
|
||||
"parent": system,
|
||||
"path": "/system/dept",
|
||||
"component": "system/DeptManage",
|
||||
"permission_code": "dept:list",
|
||||
"menu_type": 1,
|
||||
"order_num": 4
|
||||
})
|
||||
|
||||
# --- 系统管理: list 权限(关键补充!)---
|
||||
self._get_or_create_menu("user_list", {
|
||||
"name": "查看用户列表",
|
||||
"parent": user_menu,
|
||||
"permission_code": "user:list",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("role_list", {
|
||||
"name": "查看角色列表",
|
||||
"parent": role_menu,
|
||||
"permission_code": "role:list",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("menu_list", {
|
||||
"name": "查看菜单列表",
|
||||
"parent": menu_menu,
|
||||
"permission_code": "menu:list",
|
||||
"menu_type": 2
|
||||
})
|
||||
# ================= 新增:菜单管理的按钮权限 =================
|
||||
self._get_or_create_menu("menu_list", {
|
||||
"name": "查看菜单列表",
|
||||
"parent": menu_menu,
|
||||
"permission_code": "menu:list",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("menu_create", {
|
||||
"name": "新增菜单",
|
||||
"parent": menu_menu,
|
||||
"permission_code": "menu:create",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("menu_update", {
|
||||
"name": "编辑菜单",
|
||||
"parent": menu_menu,
|
||||
"permission_code": "menu:update",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("menu_delete", {
|
||||
"name": "删除菜单",
|
||||
"parent": menu_menu,
|
||||
"permission_code": "menu:delete",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("dept_list", {
|
||||
"name": "查看部门列表",
|
||||
"parent": dept_menu,
|
||||
"permission_code": "dept:list",
|
||||
"menu_type": 2
|
||||
})
|
||||
# ================= 新增:部门管理的按钮权限 =================
|
||||
self._get_or_create_menu("dept_list", {
|
||||
"name": "查看部门列表",
|
||||
"parent": dept_menu,
|
||||
"permission_code": "dept:list",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("dept_create", {
|
||||
"name": "新增部门",
|
||||
"parent": dept_menu,
|
||||
"permission_code": "dept:create",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("dept_update", {
|
||||
"name": "编辑部门",
|
||||
"parent": dept_menu,
|
||||
"permission_code": "dept:update",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("dept_delete", {
|
||||
"name": "删除部门",
|
||||
"parent": dept_menu,
|
||||
"permission_code": "dept:delete",
|
||||
"menu_type": 2
|
||||
})
|
||||
# --- 系统管理: 其他按钮权限 ---
|
||||
self._get_or_create_menu("user_create", {
|
||||
"name": "新增用户",
|
||||
"parent": user_menu,
|
||||
"permission_code": "user:create",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("user_update", {
|
||||
"name": "编辑用户",
|
||||
"parent": user_menu,
|
||||
"permission_code": "user:update",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("user_delete", {
|
||||
"name": "删除用户",
|
||||
"parent": user_menu,
|
||||
"permission_code": "user:delete",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("user_reset_pwd", {
|
||||
"name": "重置密码",
|
||||
"parent": user_menu,
|
||||
"permission_code": "user:reset_password",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("role_assign", {
|
||||
"name": "分配角色",
|
||||
"parent": role_menu,
|
||||
"permission_code": "user:assign_roles",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("role_create", {
|
||||
"name": "新增角色",
|
||||
"parent": role_menu,
|
||||
"permission_code": "role:create",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("role_update", {
|
||||
"name": "编辑角色",
|
||||
"parent": role_menu,
|
||||
"permission_code": "role:update",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("role_delete", {
|
||||
"name": "删除角色",
|
||||
"parent": role_menu,
|
||||
"permission_code": "role:delete",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("role_set_perms", {
|
||||
"name": "设置权限",
|
||||
"parent": role_menu,
|
||||
"permission_code": "role:set_permissions",
|
||||
"menu_type": 2
|
||||
})
|
||||
# ========================
|
||||
# 2. 内容管理(仅保留动态内容类)
|
||||
# ========================
|
||||
content = self._get_or_create_menu("content", {
|
||||
"name": "内容管理",
|
||||
"menu_type": 0,
|
||||
"permission_code": "system:content",
|
||||
"order_num": 2
|
||||
})
|
||||
|
||||
# 仅保留这些在“内容管理”下
|
||||
dynamic_content_models = [
|
||||
("article", "文章管理", 1, "ArticleList"),
|
||||
("carousel", "轮播图管理", 2, "CarouselList"),
|
||||
("notice", "公告管理", 3, "NoticeList"),
|
||||
("newssection", "新闻栏目", 4, "NewsSection"),
|
||||
("news", "新闻管理", 5, "NewsList"),
|
||||
]
|
||||
|
||||
content_menus = {}
|
||||
for code, name, order, component in dynamic_content_models:
|
||||
menu = self._get_or_create_menu(code, {
|
||||
"name": name,
|
||||
"parent": content,
|
||||
"path": f"/content/{code}",
|
||||
"component": f"content/{component}",
|
||||
"permission_code": f"{code}:list",
|
||||
"menu_type": 1,
|
||||
"order_num": order
|
||||
})
|
||||
content_menus[code] = menu
|
||||
|
||||
# 添加 list + CRUD 权限
|
||||
self._get_or_create_menu(f"{code}_list", {
|
||||
"name": f"查看{name}",
|
||||
"parent": menu,
|
||||
"permission_code": f"{code}:list",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu(f"{code}_create", {
|
||||
"name": f"新增{name}",
|
||||
"parent": menu,
|
||||
"permission_code": f"{code}:create",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu(f"{code}_update", {
|
||||
"name": f"编辑{name}",
|
||||
"parent": menu,
|
||||
"permission_code": f"{code}:update",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu(f"{code}_delete", {
|
||||
"name": f"删除{name}",
|
||||
"parent": menu,
|
||||
"permission_code": f"{code}:delete",
|
||||
"menu_type": 2
|
||||
})
|
||||
|
||||
# --- 公告特殊权限(保持不变)---
|
||||
notice_menu = content_menus["notice"]
|
||||
self._get_or_create_menu("notice_mark_read", {
|
||||
"name": "标记公告已读",
|
||||
"parent": notice_menu,
|
||||
"permission_code": "notice:mark_as_read",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu("notice_view_receipts", {
|
||||
"name": "查看公告回执",
|
||||
"parent": notice_menu,
|
||||
"permission_code": "notice:view_receipts",
|
||||
"menu_type": 2
|
||||
})
|
||||
|
||||
# ========================
|
||||
# 3. 新增:站点配置(原属于内容管理的静态配置类)
|
||||
# ========================
|
||||
site_config = self._get_or_create_menu("site_config", {
|
||||
"name": "站点配置",
|
||||
"menu_type": 0,
|
||||
"order_num": 3 # 排在内容管理之后
|
||||
})
|
||||
|
||||
site_config_models = [
|
||||
("baseinfo", "基础信息", 1, "BaseInfo"),
|
||||
("mainmenu", "主导航菜单", 2, "MainMenu"),
|
||||
("picturelink", "图片链接", 3, "PictureLink"),
|
||||
("friendlink", "友情链接", 4, "FriendLink"),
|
||||
("linkcategory", "链接分类", 5, ""),
|
||||
]
|
||||
|
||||
for code, name, order, component in site_config_models:
|
||||
if not component:
|
||||
menu = self._get_or_create_menu("friendlink")
|
||||
else:
|
||||
menu = self._get_or_create_menu(code, {
|
||||
"name": name,
|
||||
"parent": site_config,
|
||||
"path": f"/config/{code}",
|
||||
"component": f"config/{component}", # 建议组件路径也调整为 config/
|
||||
"permission_code": f"{code}:list",
|
||||
"menu_type": 1,
|
||||
"order_num": order
|
||||
})
|
||||
content_menus[code] = menu # 仍加入字典以便返回
|
||||
|
||||
# 添加 list + CRUD 权限
|
||||
self._get_or_create_menu(f"{code}_list", {
|
||||
"name": f"查看{name}",
|
||||
"parent": menu,
|
||||
"permission_code": f"{code}:list",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu(f"{code}_create", {
|
||||
"name": f"新增{name}",
|
||||
"parent": menu,
|
||||
"permission_code": f"{code}:create",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu(f"{code}_update", {
|
||||
"name": f"编辑{name}",
|
||||
"parent": menu,
|
||||
"permission_code": f"{code}:update",
|
||||
"menu_type": 2
|
||||
})
|
||||
self._get_or_create_menu(f"{code}_delete", {
|
||||
"name": f"删除{name}",
|
||||
"parent": menu,
|
||||
"permission_code": f"{code}:delete",
|
||||
"menu_type": 2
|
||||
})
|
||||
|
||||
return {
|
||||
'system': system,
|
||||
'user': user_menu,
|
||||
'role': role_menu,
|
||||
'menu': menu_menu,
|
||||
'dept': dept_menu,
|
||||
'content': content,
|
||||
'site_config': site_config, # 新增返回
|
||||
**content_menus,
|
||||
}
|
||||
|
||||
def _get_or_create_role(self, code, name, description, status=True):
|
||||
"""按 code 获取或创建角色"""
|
||||
role, created = Role.objects.get_or_create(
|
||||
code=code,
|
||||
defaults={
|
||||
'name': name,
|
||||
'description': description,
|
||||
'status': status
|
||||
}
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(f" ➕ 角色: {role.name}")
|
||||
return role
|
||||
|
||||
def _create_roles(self, menus):
|
||||
self.stdout.write("🔄 同步角色权限...")
|
||||
|
||||
admin_role = self._get_or_create_role(
|
||||
"admin", "超级管理员", "拥有全部权限"
|
||||
)
|
||||
tech_manager_role = self._get_or_create_role(
|
||||
"tech_manager", "技术部经理", "管理技术部用户"
|
||||
)
|
||||
hr_role = self._get_or_create_role(
|
||||
"hr_staff", "HR专员", "查看员工信息"
|
||||
)
|
||||
employee_role = self._get_or_create_role(
|
||||
"employee", "普通员工", "无管理权限"
|
||||
)
|
||||
|
||||
# 分配菜单权限(幂等设置)
|
||||
all_menus = Menu.objects.all()
|
||||
admin_role.menus.set(all_menus)
|
||||
|
||||
tech_manager_role.menus.set([
|
||||
menus['user'], menus['role'], menus['dept'],
|
||||
Menu.objects.get(permission_code="user:create"),
|
||||
Menu.objects.get(permission_code="user:delete"),
|
||||
Menu.objects.get(permission_code="user:assign_roles"),
|
||||
])
|
||||
|
||||
hr_role.menus.set([menus['user']])
|
||||
employee_role.menus.set([])
|
||||
|
||||
return {
|
||||
'admin': admin_role,
|
||||
'tech_manager': tech_manager_role,
|
||||
'hr': hr_role,
|
||||
'employee': employee_role,
|
||||
}
|
||||
|
||||
def _get_or_create_user(self, username, defaults, roles):
|
||||
"""获取或创建用户,并设置角色"""
|
||||
user, created = User.objects.get_or_create(
|
||||
username=username,
|
||||
defaults=defaults
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(f" ➕ 用户: {user.cname} ({user.username})")
|
||||
# 无论是否新建,都确保角色正确(幂等)
|
||||
user.roles.set(roles)
|
||||
return user
|
||||
|
||||
def _create_users(self, departments, roles):
|
||||
self.stdout.write("🔄 同步用户账号...")
|
||||
|
||||
self._get_or_create_user(
|
||||
"admin",
|
||||
{
|
||||
"cname": "系统管理员",
|
||||
"phone": "13800000000",
|
||||
"password": make_password("adminadmin"),
|
||||
"dept": departments['company'],
|
||||
"is_active": True,
|
||||
"is_staff": True,
|
||||
"is_superuser": True,
|
||||
},
|
||||
[roles['admin']]
|
||||
)
|
||||
|
||||
self._get_or_create_user(
|
||||
"techmgr",
|
||||
{
|
||||
"cname": "张经理",
|
||||
"phone": "13800000001",
|
||||
"password": make_password("adminadmin"),
|
||||
"dept": departments['tech'],
|
||||
"is_active": True,
|
||||
},
|
||||
[roles['tech_manager']]
|
||||
)
|
||||
|
||||
self._get_or_create_user(
|
||||
"dev1",
|
||||
{
|
||||
"cname": "李开发",
|
||||
"phone": "13800000002",
|
||||
"password": make_password("adminadmin"),
|
||||
"dept": departments['backend'],
|
||||
"is_active": True,
|
||||
},
|
||||
[roles['employee']]
|
||||
)
|
||||
|
||||
self._get_or_create_user(
|
||||
"hr1",
|
||||
{
|
||||
"cname": "王HR",
|
||||
"phone": "13800000003",
|
||||
"password": make_password("adminadmin"),
|
||||
"dept": departments['hr'],
|
||||
"is_active": True,
|
||||
},
|
||||
[roles['hr']]
|
||||
)
|
||||
|
||||
self._get_or_create_user(
|
||||
"finance1",
|
||||
{
|
||||
"cname": "赵会计",
|
||||
"phone": "13800000004",
|
||||
"password": make_password("adminadmin"),
|
||||
"dept": departments['finance'],
|
||||
"is_active": True,
|
||||
},
|
||||
[roles['employee']]
|
||||
)
|
||||
@@ -0,0 +1,67 @@
|
||||
from typing import List
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from loguru import logger
|
||||
from ninja import Router
|
||||
|
||||
from users.models import Menu
|
||||
from users.permission import require_permissions
|
||||
from users.schema import MenuOut, MenuCreate, MenuUpdate
|
||||
|
||||
router = Router(tags=['menus'])
|
||||
|
||||
|
||||
@router.get("/", response=List[MenuOut], summary="获取菜单列表")
|
||||
@require_permissions("menu:list")
|
||||
def list_menus(request):
|
||||
menus = Menu.objects.all()
|
||||
logger.info(menus)
|
||||
permissions = [m.permission_code for m in menus]
|
||||
logger.info(permissions)
|
||||
return menus
|
||||
|
||||
|
||||
@router.post("/", response=MenuOut, summary="创建菜单")
|
||||
@require_permissions("menu:create")
|
||||
def create_menu(request, payload: MenuCreate):
|
||||
data = payload.dict()
|
||||
parent_id = data.pop('parent_id', None)
|
||||
if parent_id:
|
||||
data['parent_id'] = parent_id
|
||||
|
||||
menu = Menu.objects.create(**data)
|
||||
return menu
|
||||
|
||||
|
||||
@router.get("/{menu_id}/", response=MenuOut, summary="获取菜单详情")
|
||||
@require_permissions("menu:view")
|
||||
def get_menu(request, menu_id: int):
|
||||
menu = get_object_or_404(Menu, id=menu_id)
|
||||
return menu
|
||||
|
||||
|
||||
@router.put("/{menu_id}/", response=MenuOut, summary="更新菜单")
|
||||
@require_permissions("menu:update")
|
||||
def update_menu(request, menu_id: int, payload: MenuUpdate):
|
||||
logger.debug(payload)
|
||||
menu = get_object_or_404(Menu, id=menu_id)
|
||||
data = payload.dict()
|
||||
parent_id = data.pop('parent_id', None)
|
||||
if parent_id:
|
||||
data['parent_id'] = parent_id
|
||||
else:
|
||||
data['parent'] = None
|
||||
logger.info(data)
|
||||
for attr, value in data.items():
|
||||
setattr(menu, attr, value)
|
||||
logger.info(menu.parent_id)
|
||||
menu.save()
|
||||
return menu
|
||||
|
||||
|
||||
@router.delete("/{menu_id}/", summary="删除菜单")
|
||||
@require_permissions("menu:delete")
|
||||
def delete_menu(request, menu_id: int):
|
||||
menu = get_object_or_404(Menu, id=menu_id)
|
||||
menu.delete()
|
||||
return {"success": True}
|
||||
@@ -0,0 +1,122 @@
|
||||
# Generated by Django 6.0.3 on 2026-04-05 12:05
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='role',
|
||||
name='permissions',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='role',
|
||||
options={'verbose_name': '角色', 'verbose_name_plural': '角色'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='department',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='department',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='code',
|
||||
field=models.CharField(default=django.utils.timezone.now, max_length=50, unique=True, verbose_name='角色编码'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='描述'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='status',
|
||||
field=models.BooleanField(default=True, verbose_name='状态'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='avatar',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='头像'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, max_length=20, verbose_name='手机号'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, verbose_name='状态'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='roles',
|
||||
field=models.ManyToManyField(blank=True, related_name='users', to='users.role', verbose_name='角色'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Menu',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('name', models.CharField(max_length=50, verbose_name='菜单名称')),
|
||||
('code', models.CharField(max_length=50, unique=True, verbose_name='菜单编码')),
|
||||
('path', models.CharField(blank=True, max_length=200, verbose_name='路由路径')),
|
||||
('component', models.CharField(blank=True, max_length=200, verbose_name='组件路径')),
|
||||
('icon', models.CharField(blank=True, max_length=50, verbose_name='图标')),
|
||||
('menu_type', models.IntegerField(choices=[(0, '目录'), (1, '菜单'), (2, '按钮')], default=1, verbose_name='菜单类型')),
|
||||
('order_num', models.IntegerField(default=0, verbose_name='显示顺序')),
|
||||
('permission_code', models.CharField(blank=True, max_length=100, verbose_name='权限标识')),
|
||||
('status', models.BooleanField(default=True, verbose_name='状态')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.menu', verbose_name='父菜单')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '菜单',
|
||||
'verbose_name_plural': '菜单',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='menus',
|
||||
field=models.ManyToManyField(blank=True, related_name='roles', to='users.menu', verbose_name='菜单权限'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Permission',
|
||||
),
|
||||
]
|
||||
+67
-27
@@ -1,35 +1,30 @@
|
||||
from django.contrib.auth.models import PermissionsMixin, AbstractUser
|
||||
from django.db import models
|
||||
|
||||
from common.base import BaseEntity
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class Permission(models.Model):
|
||||
CODENAMES = [
|
||||
('view', '查看'),
|
||||
('add', '新增'),
|
||||
('edit', '编辑'),
|
||||
('delete', '删除'),
|
||||
]
|
||||
resource = models.CharField("资源标识", max_length=50) # 如 'article', 'menu', 'carousel'
|
||||
action = models.CharField("操作", max_length=10, choices=CODENAMES)
|
||||
desc = models.CharField("描述", max_length=100)
|
||||
class Role(BaseEntity):
|
||||
"""角色模型"""
|
||||
name = models.CharField(max_length=50, unique=True, verbose_name="角色名称")
|
||||
code = models.CharField(max_length=50, unique=True, verbose_name="角色编码")
|
||||
description = models.CharField(max_length=200, blank=True, verbose_name="描述")
|
||||
status = models.BooleanField(default=True, verbose_name="状态")
|
||||
# 直接关联菜单(多对多)
|
||||
menus = models.ManyToManyField(
|
||||
'Menu',
|
||||
blank=True,
|
||||
related_name='roles',
|
||||
verbose_name="菜单权限"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('resource', 'action')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.resource}:{self.action}"
|
||||
verbose_name = "角色"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField("角色名称", max_length=50, unique=True)
|
||||
permissions = models.ManyToManyField('Permission', blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Department(models.Model):
|
||||
class Department(BaseEntity):
|
||||
name = models.CharField("部门名称", max_length=100, unique=True)
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, verbose_name="上级部门")
|
||||
order = models.IntegerField("排序", default=0)
|
||||
@@ -38,15 +33,37 @@ class Department(models.Model):
|
||||
verbose_name = "部门"
|
||||
verbose_name_plural = "部门管理"
|
||||
|
||||
# 👇 核心:添加部分唯一约束
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['parent'],
|
||||
condition=models.Q(parent__isnull=True),
|
||||
name='unique_root_department'
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_root(self):
|
||||
return self.parent is None
|
||||
|
||||
class User(AbstractUser, PermissionsMixin):
|
||||
|
||||
class User(AbstractUser, PermissionsMixin, BaseEntity):
|
||||
username = models.CharField("英文名/账号", max_length=30, unique=True)
|
||||
cname = models.CharField("中文名", max_length=30, blank=True)
|
||||
phone = models.CharField(max_length=20, blank=True, verbose_name="手机号")
|
||||
avatar = models.CharField(max_length=255, blank=True, verbose_name="头像")
|
||||
dept = models.ForeignKey('Department', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属部门")
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_active = models.BooleanField(default=True, verbose_name="状态")
|
||||
# 直接在这里定义多对多关系 👈
|
||||
roles = models.ManyToManyField(
|
||||
Role,
|
||||
blank=True,
|
||||
related_name='users',
|
||||
verbose_name="角色"
|
||||
)
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
REQUIRED_FIELDS = []
|
||||
@@ -55,5 +72,28 @@ class User(AbstractUser, PermissionsMixin):
|
||||
return f"{self.cname} ({self.username})"
|
||||
|
||||
|
||||
# 给 User 关联 Role(多对多)
|
||||
User.add_to_class('roles', models.ManyToManyField(Role, blank=True))
|
||||
class Menu(BaseEntity):
|
||||
"""菜单模型"""
|
||||
MENU_TYPE_CHOICES = [
|
||||
(0, '目录'),
|
||||
(1, '菜单'),
|
||||
(2, '按钮'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=50, verbose_name="菜单名称")
|
||||
code = models.CharField(max_length=50, unique=True, verbose_name="菜单编码")
|
||||
path = models.CharField(max_length=200, blank=True, verbose_name="路由路径")
|
||||
component = models.CharField(max_length=200, blank=True, verbose_name="组件路径")
|
||||
icon = models.CharField(max_length=50, blank=True, verbose_name="图标")
|
||||
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name="父菜单")
|
||||
menu_type = models.IntegerField(choices=MENU_TYPE_CHOICES, default=1, verbose_name="菜单类型")
|
||||
order_num = models.IntegerField(default=0, verbose_name="显示顺序")
|
||||
permission_code = models.CharField(max_length=100, blank=True, verbose_name="权限标识")
|
||||
status = models.BooleanField(default=True, verbose_name="状态")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "菜单"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
from typing import List
|
||||
|
||||
from django.core.cache import cache
|
||||
from loguru import logger
|
||||
from ninja.errors import HttpError
|
||||
|
||||
from users.models import User, Menu # 替换为你的实际 app 名
|
||||
from functools import wraps
|
||||
import asyncio
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
# 你的权限检查函数(不变)
|
||||
def has_permission(user_id: int, permission: str) -> bool:
|
||||
# 你原来的逻辑
|
||||
return True
|
||||
|
||||
|
||||
# ====================== 多权限通用装饰器 ======================
|
||||
def require_permissions(*perms: str, match_all: bool = True):
|
||||
"""
|
||||
多权限检查装饰器
|
||||
:param perms: 权限字符串,可传多个
|
||||
:param match_all: True = 必须拥有所有权限(AND),False = 拥有任意一个即可(OR)
|
||||
用法:
|
||||
@require_permissions("role:update", "role:delete") # 两个都要有
|
||||
@require_permissions("role:view", "role:list", match_all=False) # 有一个就行
|
||||
"""
|
||||
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
async def async_wrapper(request, *args, **kwargs):
|
||||
user_id = request.auth.id
|
||||
check = sync_to_async(has_permission)
|
||||
|
||||
# 检查所有权限
|
||||
results = [await check(user_id, perm) for perm in perms]
|
||||
allowed = all(results) if match_all else any(results)
|
||||
|
||||
if not allowed:
|
||||
raise HttpError(403, "权限不足")
|
||||
|
||||
return await view_func(request, *args, **kwargs)
|
||||
|
||||
@wraps(view_func)
|
||||
def sync_wrapper(request, *args, **kwargs):
|
||||
user_id = request.auth.id
|
||||
results = [has_permission(user_id, perm) for perm in perms]
|
||||
allowed = all(results) if match_all else any(results)
|
||||
|
||||
if not allowed:
|
||||
raise HttpError(403, "权限不足")
|
||||
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
# 自动识别同步/异步
|
||||
if asyncio.iscoroutinefunction(view_func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def get_user_permissions(user_id: int) -> List[str]:
|
||||
"""
|
||||
获取用户所有按钮级权限标识(permission_code)
|
||||
利用 Django ORM 的多对多关系链式查询
|
||||
"""
|
||||
cache_key = f"user_permissions_{user_id}"
|
||||
permissions = cache.get(cache_key)
|
||||
logger.info(f"当前缓存的权限列表:{permissions}")
|
||||
if permissions is None:
|
||||
try:
|
||||
# 链式查询:用户 → 角色 → 菜单(仅按钮类型)
|
||||
permissions = list(
|
||||
Menu.objects.filter(
|
||||
roles__users__id=user_id, # 通过 roles__users 反向关联到用户
|
||||
menu_type=2, # 2 = 按钮
|
||||
status=True
|
||||
)
|
||||
.values_list('permission_code', flat=True)
|
||||
.distinct()
|
||||
)
|
||||
logger.info(f"当前获取到的权限列表:{permissions}")
|
||||
cache.set(cache_key, permissions, 3600) # 缓存1小时
|
||||
except User.DoesNotExist:
|
||||
return []
|
||||
logger.info(f"当前用户的权限列表:{permissions}")
|
||||
return permissions
|
||||
|
||||
|
||||
def has_permission(user_id: int, permission_code: str) -> bool:
|
||||
"""检查用户是否拥有指定权限"""
|
||||
if not permission_code:
|
||||
return True
|
||||
return permission_code in get_user_permissions(user_id)
|
||||
|
||||
|
||||
def clear_user_permissions_cache(user_id: int):
|
||||
"""清除用户权限缓存(当角色或菜单变更时调用)"""
|
||||
cache.delete(f"user_permissions_{user_id}")
|
||||
|
||||
|
||||
# 批量清除缓存的辅助函数(可选)
|
||||
def clear_all_users_permissions_cache():
|
||||
"""清除所有用户权限缓存(谨慎使用)"""
|
||||
# 实际项目中可通过信号或更精细的方式处理
|
||||
cache.clear() # 或遍历已知用户ID
|
||||
|
||||
|
||||
def build_menu_tree(menus):
|
||||
"""高效构建菜单树(O(n) 时间复杂度)"""
|
||||
# 1. 将菜单列表转为字典,key=id
|
||||
menu_dict = {}
|
||||
for menu in menus:
|
||||
menu_dict[menu.id] = {
|
||||
'id': menu.id,
|
||||
'name': menu.name,
|
||||
'code': menu.code,
|
||||
'path': menu.path,
|
||||
'component': menu.component,
|
||||
'icon': menu.icon,
|
||||
'order_num': menu.order_num,
|
||||
'parent_id': menu.parent_id, # ✅ 只存 ID,避免对象引用
|
||||
'permission_code': menu.permission_code,
|
||||
'menu_type': menu.menu_type,
|
||||
'children': [] # 初始化为空列表
|
||||
}
|
||||
|
||||
# 2. 构建树结构
|
||||
tree = []
|
||||
for menu_id, node in menu_dict.items():
|
||||
parent_id = node['parent_id']
|
||||
if parent_id is None or parent_id not in menu_dict:
|
||||
# 根节点(parent_id 为 None 或指向不存在的父节点)
|
||||
tree.append(node)
|
||||
else:
|
||||
# 添加到父节点的 children
|
||||
menu_dict[parent_id]['children'].append(node)
|
||||
|
||||
# 3. (可选)按 order_num 排序
|
||||
def sort_children(nodes):
|
||||
nodes.sort(key=lambda x: x['order_num'] or 0)
|
||||
for node in nodes:
|
||||
sort_children(node['children'])
|
||||
|
||||
sort_children(tree)
|
||||
return tree
|
||||
@@ -0,0 +1,29 @@
|
||||
# api/permissions.py
|
||||
from loguru import logger
|
||||
from ninja import Router
|
||||
|
||||
from users.models import Menu
|
||||
from users.permission import build_menu_tree, get_user_permissions, require_permissions
|
||||
|
||||
router = Router(tags=['perms'])
|
||||
|
||||
|
||||
# ========== 用户专属接口 ==========
|
||||
@router.get("/menus/", summary="获取当前用户菜单树")
|
||||
@require_permissions("menu:list")
|
||||
def get_user_menu_tree(request):
|
||||
menus = Menu.objects.filter(
|
||||
roles__users__id=request.auth.id,
|
||||
menu_type__in=[0, 1],
|
||||
status=True
|
||||
).order_by('order_num')
|
||||
tree_menus = build_menu_tree(list(menus))
|
||||
logger.info(tree_menus)
|
||||
return tree_menus
|
||||
|
||||
|
||||
@router.get("/permissions/", summary="获取当前用户按钮权限")
|
||||
@require_permissions("menu:list")
|
||||
def get_user_permissions_api(request):
|
||||
perms = get_user_permissions(request.auth.id)
|
||||
return {"permissions": perms}
|
||||
@@ -0,0 +1,63 @@
|
||||
# api/roles.py
|
||||
from typing import List
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from ninja import Router
|
||||
from ninja.errors import HttpError
|
||||
|
||||
from users.models import Role
|
||||
from users.permission import clear_user_permissions_cache, require_permissions
|
||||
from users.schema import RoleOut, RoleCreate, RoleMenuAssign, RoleUpdate
|
||||
|
||||
router = Router(tags=['roles'])
|
||||
|
||||
|
||||
@router.get("/", response=List[RoleOut], summary="获取角色列表")
|
||||
@require_permissions("role:list")
|
||||
def list_roles(request):
|
||||
return Role.objects.all()
|
||||
|
||||
|
||||
@router.post("/", response=RoleOut, summary="创建角色")
|
||||
@require_permissions("role:create")
|
||||
def create_role(request, payload: RoleCreate):
|
||||
role = Role.objects.create(**payload.dict())
|
||||
return role
|
||||
|
||||
|
||||
@router.get("/{role_id}/", response=RoleOut, summary="获取角色详情")
|
||||
@require_permissions("role:view")
|
||||
def get_role(request, role_id: int):
|
||||
role = get_object_or_404(Role, id=role_id)
|
||||
return role
|
||||
|
||||
|
||||
@router.put("/{role_id}/", response=RoleOut, summary="更新角色")
|
||||
@require_permissions("role:update")
|
||||
def update_role(request, role_id: int, payload: RoleUpdate):
|
||||
role = get_object_or_404(Role, id=role_id)
|
||||
for attr, value in payload.dict().items():
|
||||
setattr(role, attr, value)
|
||||
role.save()
|
||||
return role
|
||||
|
||||
|
||||
@router.delete("/{role_id}/", summary="删除角色")
|
||||
@require_permissions("role:delete")
|
||||
def delete_role(request, role_id: int):
|
||||
role = get_object_or_404(Role, id=role_id)
|
||||
role.delete()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/{role_id}/menus/", summary="分配角色菜单权限")
|
||||
@require_permissions("role:assign_permissions")
|
||||
def assign_role_menus(request, role_id: int, payload: RoleMenuAssign):
|
||||
role = get_object_or_404(Role, id=role_id)
|
||||
# 直接使用 ManyToManyField 的 set() 方法
|
||||
role.menus.set(payload.menu_ids)
|
||||
|
||||
# 清除该角色下所有用户的权限缓存
|
||||
clear_user_permissions_cache(role_id)
|
||||
|
||||
return {"success": True}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
# schemas.py
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from ninja import Schema
|
||||
|
||||
|
||||
# ========== 用户 Schema ==========
|
||||
class UserRoleOut(Schema):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class UserDeptOut(Schema):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class UserOut(Schema):
|
||||
id: int
|
||||
username: str
|
||||
cname: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
|
||||
dept: Optional[UserDeptOut] = None
|
||||
roles: List[UserRoleOut] = []
|
||||
|
||||
is_active: bool
|
||||
is_staff: bool
|
||||
is_superuser: bool
|
||||
|
||||
last_login: Optional[datetime] = None
|
||||
date_joined: datetime
|
||||
|
||||
|
||||
class UserCreate(Schema):
|
||||
username: str
|
||||
password: Optional[str] = None
|
||||
cname: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
dept: Optional[int] = None
|
||||
roles: List[int] = []
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class UserResetPassword(Schema):
|
||||
user_id: int
|
||||
new_password: str
|
||||
|
||||
|
||||
class UserUpdate(Schema):
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
cname: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
dept: Optional[int] = None
|
||||
roles: Optional[List[int]] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
# ========== 菜单 Schema ==========
|
||||
class MenuBase(Schema):
|
||||
name: str
|
||||
code: str
|
||||
path: Optional[str] = None
|
||||
component: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
parent_id: Optional[int] = None
|
||||
menu_type: int = 1
|
||||
order_num: int = 0
|
||||
permission_code: Optional[str] = None
|
||||
status: bool = True
|
||||
|
||||
|
||||
class MenuCreate(MenuBase):
|
||||
pass
|
||||
|
||||
|
||||
class MenuUpdate(MenuBase):
|
||||
pass
|
||||
|
||||
|
||||
class MenuOut(MenuBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# ========== 角色 Schema ==========
|
||||
class RoleBase(Schema):
|
||||
name: str
|
||||
code: str
|
||||
description: Optional[str] = None
|
||||
status: bool = True
|
||||
menus: List[MenuOut]
|
||||
|
||||
|
||||
class RoleCreate(RoleBase):
|
||||
pass
|
||||
|
||||
|
||||
class RoleUpdate(RoleBase):
|
||||
pass
|
||||
|
||||
|
||||
class RoleOut(RoleBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# ========== 关联 Schema ==========
|
||||
class RoleMenuAssign(Schema):
|
||||
menu_ids: List[int]
|
||||
|
||||
|
||||
class UserRoleAssign(Schema):
|
||||
role_ids: List[int]
|
||||
|
||||
|
||||
class DepartmentBase(Schema):
|
||||
name: str
|
||||
parent_id: Optional[int] = None
|
||||
order: int = 0
|
||||
|
||||
|
||||
class DepartmentCreate(DepartmentBase):
|
||||
pass
|
||||
|
||||
|
||||
class DepartmentUpdate(DepartmentBase):
|
||||
pass
|
||||
|
||||
|
||||
class DepartmentOut(DepartmentBase):
|
||||
id: int
|
||||
+4
-3
@@ -3,6 +3,7 @@ import sys
|
||||
|
||||
import django
|
||||
from faker import Faker
|
||||
from loguru import logger
|
||||
|
||||
# 配置 Django 环境
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -16,7 +17,7 @@ fake = Faker('zh_CN')
|
||||
|
||||
|
||||
def seed_users(count=5):
|
||||
print(f"正在生成 {count} 个用户...")
|
||||
logger.info(f"正在生成 {count} 个用户...")
|
||||
|
||||
users = []
|
||||
for i in range(count):
|
||||
@@ -37,9 +38,9 @@ def seed_users(count=5):
|
||||
last_name=fake.last_name()
|
||||
)
|
||||
users.append(user)
|
||||
print(f" - 创建用户: {username} (密码: password123)")
|
||||
logger.info(f" - 创建用户: {username} (密码: password123)")
|
||||
|
||||
print("用户生成完毕!")
|
||||
logger.info("用户生成完毕!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import secrets
|
||||
from typing import List
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from loguru import logger
|
||||
from ninja import Router
|
||||
from ninja.errors import HttpError
|
||||
from ninja.pagination import paginate, PageNumberPagination
|
||||
|
||||
from users.models import User, Department
|
||||
from users.permission import require_permissions
|
||||
from users.schema import UserUpdate, UserOut, UserCreate, UserResetPassword
|
||||
from utils.dept_utils import get_dept_children_ids
|
||||
|
||||
router = Router(tags=['users'])
|
||||
|
||||
|
||||
@router.get("/", response=List[UserOut], summary="获取用户列表")
|
||||
@paginate(PageNumberPagination, page_size=20)
|
||||
@require_permissions("user:list")
|
||||
def list_users(request, dept_id: int = None, q: str = None):
|
||||
users = User.objects.all()
|
||||
logger.debug(f"users: {len(users)}")
|
||||
|
||||
logger.debug(f"dept_id: {dept_id}, q: {q}")
|
||||
if dept_id:
|
||||
current_dept = get_object_or_404(Department, id=dept_id)
|
||||
dept_ids = get_dept_children_ids(dept_id)
|
||||
if current_dept.is_root:
|
||||
# 包含未分配用户
|
||||
users = users.filter(Q(dept_id__in=dept_ids) | Q(dept__isnull=True))
|
||||
else:
|
||||
users = users.filter(dept_id__in=dept_ids)
|
||||
logger.debug(f"users: {len(users)}")
|
||||
|
||||
if q:
|
||||
users = users.filter(
|
||||
Q(username__icontains=q) |
|
||||
Q(cname__icontains=q) |
|
||||
Q(phone__icontains=q)
|
||||
)
|
||||
|
||||
logger.debug(f"users: {len(users)}")
|
||||
return users.order_by("dept_id")
|
||||
|
||||
|
||||
@router.post("/")
|
||||
@require_permissions("user:create")
|
||||
def create_user(request, data: UserCreate):
|
||||
try:
|
||||
if not data.password:
|
||||
data.password = secrets.token_urlsafe(8)[:8]
|
||||
|
||||
logger.debug(f"用户名:{data.username}, 密码:{data.password}")
|
||||
user = User.objects.create_user(
|
||||
username=data.username,
|
||||
email=data.email,
|
||||
password=make_password(data.password)
|
||||
)
|
||||
|
||||
for k, v in data.dict(exclude={"password", "roles"}).items():
|
||||
setattr(user, k, v)
|
||||
|
||||
user.save()
|
||||
|
||||
if data.roles:
|
||||
user.roles.set(data.roles)
|
||||
|
||||
return {
|
||||
"detail": f"用户创建成功!密码:{data.password}"
|
||||
}
|
||||
except IntegrityError as e:
|
||||
raise HttpError(500, "用户已存在!")
|
||||
|
||||
|
||||
@router.post("/reset-password/", summary="重置用户密码")
|
||||
@require_permissions("user:reset_password")
|
||||
def reset_password(request, payload: UserResetPassword):
|
||||
user = get_object_or_404(User, id=payload.user_id)
|
||||
|
||||
if payload.new_password:
|
||||
new_pass = payload.new_password
|
||||
else:
|
||||
new_pass = secrets.token_urlsafe(8)[:12] # 自动生成
|
||||
|
||||
user.password = make_password(new_pass)
|
||||
user.save()
|
||||
|
||||
# 返回新密码(仅用于通知,前端可显示)
|
||||
return {"detail": f"密码已重置: {new_pass}"}
|
||||
|
||||
|
||||
@router.get("/{id}", response=UserOut)
|
||||
@require_permissions("user:view")
|
||||
def get_user(request, id: int):
|
||||
obj = get_object_or_404(User, id=id)
|
||||
data = obj.__dict__
|
||||
data["roles"] = list(obj.roles.values_list("id", flat=True))
|
||||
return data
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
@require_permissions("user:update")
|
||||
def update_user(request, id: int, data: UserUpdate):
|
||||
user = get_object_or_404(User, id=id)
|
||||
|
||||
if data.password:
|
||||
user.set_password(data.password)
|
||||
|
||||
for k, v in data.dict(exclude_unset=True, exclude={"password", "roles"}).items():
|
||||
if k == "dept":
|
||||
k = "dept_id"
|
||||
setattr(user, k, v)
|
||||
|
||||
user.save()
|
||||
|
||||
if data.roles is not None:
|
||||
user.roles.set(data.roles)
|
||||
|
||||
if data.password:
|
||||
return {"detail": f"密码已重置: {data.password}"}
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
@require_permissions("user:delete")
|
||||
def delete_user(request, id: int):
|
||||
User.objects.filter(id=id).delete()
|
||||
return {"success": True}
|
||||
+13
-1
@@ -1,3 +1,15 @@
|
||||
from django.shortcuts import render
|
||||
from ninja import Router
|
||||
from .users import router as users_router
|
||||
from .menus import router as menu_router
|
||||
from .roles import router as role_router
|
||||
from .permissions import router as perm_router
|
||||
from .departments import router as dept_router
|
||||
|
||||
# Create your views here.
|
||||
router = Router(tags=['users'])
|
||||
|
||||
router.add_router("/users", users_router) # 用户基础接口
|
||||
router.add_router("/perms", perm_router) # 用户基础接口
|
||||
router.add_router("/menus", menu_router) # /menus/
|
||||
router.add_router("/roles", role_router) # /roles/
|
||||
router.add_router("/dept", dept_router) # /roles/
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import List
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
from users.models import Department
|
||||
|
||||
|
||||
def get_dept_children_ids(dept_id: int) -> List[int]:
|
||||
"""
|
||||
递归获取当前部门及所有子部门的 ID 列表
|
||||
"""
|
||||
# 尝试从缓存获取
|
||||
cache_key = f"dept_tree_{dept_id}"
|
||||
cached_ids = cache.get(cache_key)
|
||||
if cached_ids:
|
||||
return cached_ids
|
||||
|
||||
# 缓存未命中,执行递归逻辑...
|
||||
|
||||
# 1. 初始化列表,包含当前部门 ID
|
||||
dept_ids = [dept_id]
|
||||
|
||||
# 2. 查询直接子部门
|
||||
children = Department.objects.filter(parent_id=dept_id) # 假设外键是 parent_id
|
||||
|
||||
# 3. 递归遍历子部门
|
||||
for child in children:
|
||||
# 递归获取子部门的子部门,并将结果扩展到列表中
|
||||
dept_ids.extend(get_dept_children_ids(child.id))
|
||||
|
||||
# 将结果存入缓存,过期时间设为 12 小时
|
||||
cache.set(cache_key, dept_ids, 12 * 60 * 60)
|
||||
return dept_ids
|
||||
@@ -1,9 +0,0 @@
|
||||
def user_has_permission(user, resource, action):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
if not user.is_active:
|
||||
return False
|
||||
return user.roles.filter(
|
||||
permissions__resource=resource,
|
||||
permissions__action=action
|
||||
).exists()
|
||||
Reference in New Issue
Block a user