diff --git a/db/logs/django_server.log b/.env
similarity index 100%
rename from db/logs/django_server.log
rename to .env
diff --git a/.gitignore b/.gitignore
index 4e2a696..c60a6b3 100755
--- a/.gitignore
+++ b/.gitignore
@@ -7,8 +7,6 @@ config/supervisord.sock
.idea/
/.idea/
/.idea/*
-/templates/
-/templates/*
**/__pycache__
**/.DS_Store
/venv/
diff --git a/article/admin.py b/article/admin.py
index e6a58c2..fd36a38 100644
--- a/article/admin.py
+++ b/article/admin.py
@@ -1,51 +1,56 @@
+from django import forms
from django.contrib import admin
from django.utils.html import format_html
+from import_export.admin import ImportExportModelAdmin
-from article.models import NoticeReceipt, Notice, Article, Carousel, Section
+from article.models import NoticeReceipt, Notice, Article, Carousel, ArticleAttachment
+from base.models import MainMenu
from users.models import User
# —————— 板块 ——————
-@admin.register(Section)
-class SectionAdmin(admin.ModelAdmin):
- list_display = ('display_name', 'code', 'parent', 'is_enabled', 'order')
- list_filter = ('parent', 'enabled')
- search_fields = ('name', 'code')
- ordering = ('order', 'name')
- list_editable = ('order',)
- fieldsets = (
- ('基本信息', {
- 'fields': ('name', 'code', 'parent', 'enabled', 'order')
- }),
- )
-
- def display_name(self, obj):
- """在列表中显示层级缩进"""
- if obj.is_sub:
- return f" └─ {obj.name}" # 使用全角空格+符号模拟缩进
- return obj.name
- display_name.short_description = '板块名称'
- display_name.admin_order_field = 'name'
-
- def is_enabled(self, obj):
- return obj.enabled
- is_enabled.boolean = True
- is_enabled.short_description = '启用'
-
- def formfield_for_foreignkey(self, db_field, request, **kwargs):
- """限制 parent 只能选择主板块(无父级的板块)"""
- if db_field.name == "parent":
- kwargs["queryset"] = Section.objects.filter(parent__isnull=True)
- return super().formfield_for_foreignkey(db_field, request, **kwargs)
-
- def get_queryset(self, request):
- """优化查询,避免 N+1"""
- return super().get_queryset(request).select_related('parent')
-
+# @admin.register(ArticleSection)
+# class SectionAdmin(admin.ModelAdmin):
+# list_display = ('display_name', 'code', 'parent', 'is_enabled', 'order')
+# list_filter = ('parent', 'enabled')
+# search_fields = ('name', 'code')
+# ordering = ('order', 'name')
+# list_editable = ('order',)
+# fieldsets = (
+# ('基本信息', {
+# 'fields': ('name', 'code', 'parent', 'enabled', 'order')
+# }),
+# )
+#
+# def display_name(self, obj):
+# """在列表中显示层级缩进"""
+# if obj.is_sub:
+# return f" └─ {obj.name}" # 使用全角空格+符号模拟缩进
+# return obj.name
+#
+# display_name.short_description = '板块名称'
+# display_name.admin_order_field = 'name'
+#
+# def is_enabled(self, obj):
+# return obj.enabled
+#
+# is_enabled.boolean = True
+# is_enabled.short_description = '启用'
+#
+# def formfield_for_foreignkey(self, db_field, request, **kwargs):
+# """限制 parent 只能选择主板块(无父级的板块)"""
+# if db_field.name == "parent":
+# kwargs["queryset"] = ArticleSection.objects.filter(parent__isnull=True)
+# return super().formfield_for_foreignkey(db_field, request, **kwargs)
+#
+# def get_queryset(self, request):
+# """优化查询,避免 N+1"""
+# return super().get_queryset(request).select_related('parent')
+#
# —————— 轮播图 ——————
@admin.register(Carousel)
-class CarouselAdmin(admin.ModelAdmin):
+class CarouselAdmin(ImportExportModelAdmin):
list_display = ('title', 'image_thumb', 'link_url', 'order', 'active')
list_editable = ('order', 'active')
ordering = ('order',)
@@ -54,18 +59,49 @@ class CarouselAdmin(admin.ModelAdmin):
if obj.image:
return format_html('
{p}
" for p in paragraphs]) + + article = Article.objects.create( + title=fake.sentence(nb_words=random.randint(5, 10))[:-1], + section=section, + author=author, + content=content_html, + is_published=True, + ) + + # --- 生成真实附件 --- + if random.random() < 0.3: + fake_filename = f"附件资料_{fake.word()}.pdf" + + # 创建临时文件 + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_file: + tmp_file.write(b'%PDF-1.4 Fake PDF Content for Testing') + tmp_file_path = tmp_file.name + + try: + # 打开并保存 + with open(tmp_file_path, 'rb') as f: + # 关键点:使用 file.save 方法,这会触发文件上传机制 + attachment = ArticleAttachment(article=article) + attachment.file.save(fake_filename, File(f), save=True) + + print(f" - [{section}] {article.title} (含真实附件)") + + finally: + # 删除临时文件 + if os.path.exists(tmp_file_path): + os.remove(tmp_file_path) + else: + print(f" - [{section}] {article.title}") + + created_count += 1 + + print(f"✅ 成功生成 {created_count} 篇文章!") + + +if __name__ == "__main__": + seed_articles(30) diff --git a/article/views.py b/article/views.py index 7a65ecc..9eb6c51 100644 --- a/article/views.py +++ b/article/views.py @@ -13,7 +13,7 @@ router = Router(tags=['content']) @router.get('/article', description="获取文章列表") -def article_list(request): +def article_details(request): if not user_has_permission(request.user, 'article', 'view'): return HttpResponseForbidden("无权访问") articles = Article.objects.all() diff --git a/base/admin.py b/base/admin.py index 62105c2..2241a97 100644 --- a/base/admin.py +++ b/base/admin.py @@ -1,24 +1,67 @@ from django.contrib import admin -from django.utils.html import format_html +from import_export.admin import ImportExportModelAdmin -from article.models import Section, Carousel, Article, Notice, NoticeReceipt -from base.models import MainMenu, FooterLink -from users.models import User +from base.models import MainMenu, FooterLink, LinkCategory, FriendLink # —————— 主菜单 —————— @admin.register(MainMenu) -class MainMenuAdmin(admin.ModelAdmin): - list_display = ('title', 'url', 'parent', 'order', 'visible') +class MainMenuAdmin(ImportExportModelAdmin): + list_display = ('display_name', 'code', 'url', 'parent', 'order', 'visible') list_editable = ('order', 'visible') list_filter = ('parent', 'visible') search_fields = ('title', 'url') - ordering = ('order',) + ordering = ('id',) + + def display_name(self, obj): + """在列表中显示层级缩进""" + if obj.is_sub: + return f" └─ {obj.title}" # 使用全角空格+符号模拟缩进 + return obj.title + + display_name.short_description = '板块名称' + display_name.admin_order_field = 'name' + + def is_enabled(self, obj): + return obj.enabled + + is_enabled.boolean = True + is_enabled.short_description = '启用' + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + """限制 parent 只能选择主板块(无父级的板块)""" + if db_field.name == "parent": + kwargs["queryset"] = MainMenu.objects.filter(parent__isnull=True, is_external=False) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + def get_queryset(self, request): + """优化查询,避免 N+1""" + return super().get_queryset(request).select_related('parent') + + +# 1. 注册分类管理 +@admin.register(LinkCategory) +class LinkCategoryAdmin(ImportExportModelAdmin): + list_display = ('name', 'code', 'order', 'is_active') + list_editable = ('order', 'is_active') + + +# 2. 注册链接管理 +@admin.register(FriendLink) +class FriendLinkAdmin(ImportExportModelAdmin): + list_display = ('name', 'category', 'url', 'order', 'is_active') + list_filter = ('category', 'is_active') # 左侧过滤器,方便筛选 + search_fields = ('name', 'url') + + # 优化:在添加链接时,只显示启用的分类 + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == "category": + kwargs["queryset"] = LinkCategory.objects.filter(is_active=True) + return super().formfield_for_foreignkey(db_field, request, **kwargs) # —————— 页脚链接 —————— @admin.register(FooterLink) -class FooterLinkAdmin(admin.ModelAdmin): - list_display = ('title', 'url', 'order') - list_editable = ('order',) - ordering = ('order',) +class FooterLinkAdmin(ImportExportModelAdmin): + list_display = ('title', 'address', 'unit', 'postal', 'telephone', 'fax', 'filing') + list_editable = ('postal', 'telephone', 'fax', 'unit', 'filing',) diff --git a/base/migrations/0001_initial.py b/base/migrations/0001_initial.py index 303ea49..51e657f 100644 --- a/base/migrations/0001_initial.py +++ b/base/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-24 10:31 +# Generated by Django 6.0.3 on 2026-04-01 00:55 import django.db.models.deletion from django.db import migrations, models @@ -16,29 +16,77 @@ class Migration(migrations.Migration): name='FooterLink', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=50, verbose_name='链接文字')), - ('url', models.URLField(verbose_name='链接地址')), - ('order', models.IntegerField(default=0, verbose_name='排序')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('title', models.CharField(max_length=50, verbose_name='页脚信息')), + ('address', models.CharField(max_length=200, verbose_name='单位地址')), + ('postal', models.CharField(default='', verbose_name='邮编')), + ('telephone', models.CharField(default='', verbose_name='服务热线')), + ('fax', models.CharField(default='', verbose_name='传真')), + ('unit', models.CharField(default='', verbose_name='版权单位')), + ('filing', models.CharField(default='', verbose_name='备案号')), + ('official_account', models.ImageField(default='', upload_to='carousel/', verbose_name='公众号二维码')), + ('weibo', models.ImageField(default='', upload_to='carousel/', verbose_name='微博二维码')), + ('is_active', models.BooleanField(default=False, verbose_name='生效')), ], options={ - 'verbose_name': '页脚链接', + 'verbose_name': '页脚信息', 'verbose_name_plural': '页脚管理', }, ), + migrations.CreateModel( + name='LinkCategory', + 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=50, unique=True, verbose_name='分类名称')), + ('code', models.SlugField(help_text="用于前端模板调用的标识,如 'gov', 'fire'", unique=True, 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'], + }, + ), + migrations.CreateModel( + name='FriendLink', + 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='网站名称')), + ('url', models.URLField(verbose_name='链接地址')), + ('order', models.IntegerField(default=0, verbose_name='排序')), + ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='base.linkcategory', verbose_name='所属分类')), + ], + options={ + 'verbose_name': '友情链接', + 'verbose_name_plural': '友情链接管理', + 'ordering': ['order', '-created_at'], + }, + ), migrations.CreateModel( name='MainMenu', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=50, verbose_name='菜单标题')), - ('url', models.CharField(help_text='如 /news/ 或 外链 https://xxx', max_length=200, verbose_name='链接地址')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('title', models.CharField(max_length=100, verbose_name='标题')), + ('code', models.SlugField(help_text='用于URL或模板识别,建议英文', unique=True, verbose_name='唯一标识')), + ('url', models.CharField(blank=True, help_text='如 /menu/news.html 或 外链', max_length=200, verbose_name='链接地址')), ('is_external', models.BooleanField(default=False, verbose_name='是否外链')), - ('order', models.IntegerField(default=0, verbose_name='排序')), - ('visible', models.BooleanField(default=True, verbose_name='是否显示')), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.mainmenu', verbose_name='父菜单')), + ('order', models.PositiveSmallIntegerField(default=0, verbose_name='排序权重')), + ('visible', models.BooleanField(default=True, verbose_name='显示状态')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='base.mainmenu', verbose_name='父级节点')), ], options={ - 'verbose_name': '主菜单', - 'verbose_name_plural': '主菜单管理', + 'verbose_name': '站点导航/板块', + 'verbose_name_plural': '站点结构管理', + 'ordering': ('order', 'title'), }, ), ] diff --git a/base/migrations/0002_alter_friendlink_options_alter_linkcategory_options_and_more.py b/base/migrations/0002_alter_friendlink_options_alter_linkcategory_options_and_more.py new file mode 100644 index 0000000..bdc0f4e --- /dev/null +++ b/base/migrations/0002_alter_friendlink_options_alter_linkcategory_options_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0.3 on 2026-04-01 01:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='friendlink', + options={'ordering': ['order', '-created_at'], 'verbose_name': '友情链接', 'verbose_name_plural': '友链管理'}, + ), + migrations.AlterModelOptions( + name='linkcategory', + options={'ordering': ['order'], 'verbose_name': '友链分类', 'verbose_name_plural': '友链分类'}, + ), + migrations.AlterModelOptions( + name='mainmenu', + options={'ordering': ('order', 'title'), 'verbose_name': '站点导航', 'verbose_name_plural': '站点导航'}, + ), + migrations.AddField( + model_name='mainmenu', + name='is_carousel', + field=models.BooleanField(default=False, verbose_name='轮播图'), + ), + ] diff --git a/base/models.py b/base/models.py index 8cbc48b..1ae3b32 100644 --- a/base/models.py +++ b/base/models.py @@ -1,27 +1,144 @@ +from django.core.exceptions import ValidationError from django.db import models +from common.base import BaseEntity + + # Create your models here. -class MainMenu(models.Model): - title = models.CharField("菜单标题", max_length=50) - url = models.CharField("链接地址", max_length=200, help_text="如 /news/ 或 外链 https://xxx") - is_external = models.BooleanField("是否外链", default=False) - parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, verbose_name="父菜单") - order = models.IntegerField("排序", default=0) - visible = models.BooleanField("是否显示", default=True) + + +# --- 新增:友情链接分类模型 --- +class LinkCategory(BaseEntity): + name = models.CharField("分类名称", max_length=50, unique=True) # 分类名,如"政府网站" + code = models.SlugField("分类代码", max_length=50, unique=True, help_text="用于前端模板调用的标识,如 'gov', 'fire'") + order = models.IntegerField("显示排序", default=0) # 分类本身的排序 + is_active = models.BooleanField("是否启用", default=True) class Meta: - verbose_name = "主菜单" - verbose_name_plural = "主菜单管理" + verbose_name = "友链分类" + verbose_name_plural = "友链分类" + ordering = ['order'] def __str__(self): - return self.title + return self.name -class FooterLink(models.Model): - title = models.CharField("链接文字", max_length=50) - url = models.URLField("链接地址") +# --- 修改:友情链接模型 --- +class FriendLink(BaseEntity): + # 1. 关联到新的分类模型(一对多关系:一个分类下有多个链接) + category = models.ForeignKey( + LinkCategory, + on_delete=models.CASCADE, # 如果分类被删,关联的链接也删除 + verbose_name="所属分类", + related_name='links' # 反向查询名称,方便后续调用 + ) + + name = models.CharField("网站名称", max_length=100) + url = models.URLField("链接地址", max_length=200) + + # 2. 排序和状态 order = models.IntegerField("排序", default=0) + is_active = models.BooleanField("是否启用", default=True) class Meta: - verbose_name = "页脚链接" + verbose_name = "友情链接" + verbose_name_plural = "友链管理" + ordering = ['order', '-created_at'] + + def __str__(self): + return f"[{self.category.name}] {self.name}" + + +class MainMenu(BaseEntity): + # --- 基础信息 --- + title = models.CharField("标题", max_length=100) + code = models.SlugField("唯一标识", max_length=50, unique=True, help_text="用于URL或模板识别,建议英文") + + # --- 树形结构 --- + parent = models.ForeignKey( + 'self', + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='children', + verbose_name="父级节点" + ) + + # --- 菜单特有属性 --- + # 只有顶级节点(主菜单)才需要填写这个,子节点作为板块时通常不需要跳转链接 + url = models.CharField("链接地址", max_length=200, blank=True, help_text="如 /menu/news.html 或 外链") + is_external = models.BooleanField("是否外链", default=False) + is_carousel = models.BooleanField("轮播图", default=False) + + # --- 通用属性 --- + order = models.PositiveSmallIntegerField("排序权重", default=0) + visible = models.BooleanField("显示状态", default=True) + + class Meta: + verbose_name = "站点导航" + verbose_name_plural = "站点导航" + ordering = ('order', 'title') + + def __str__(self): + # 显示层级路径,例如:新闻中心 → 国内新闻 + if self.parent: + return f"{self.parent.title} → {self.title}" + return self.title + + def clean(self): + super().clean() + # 限制层级:最多 2 级 + if self.parent and self.parent.parent: + raise ValidationError('层级过深:子节点不能再设置子级,最多只允许两级结构。') + + # 防止自引用 + if self.pk and self.parent and self.parent.pk == self.pk: + raise ValidationError('节点不能以自身为父级。') + + # --- 核心逻辑属性 --- + @property + def is_sub(self): + """是否为子板块(即是否有父级)""" + return self.parent is not None + + @property + def is_main(self): + """是否为主菜单(即是否无父级)""" + return not self.parent + + @property + def active_children(self): + """ + 获取当前节点下可见的子节点(用于前端导航渲染) + """ + return self.children.filter(visible=True) + + +class FooterLink(BaseEntity): + title = models.CharField("页脚信息", max_length=50) + address = models.CharField("单位地址", max_length=200) + postal = models.CharField("邮编", default="") + telephone = models.CharField("服务热线", default="") + fax = models.CharField("传真", default="") + unit = models.CharField("版权单位", default="") + filing = models.CharField("备案号", default="") + official_account = models.ImageField("公众号二维码", default="", upload_to='carousel/') + weibo = models.ImageField("微博二维码", default="", upload_to='carousel/') + is_active = models.BooleanField("生效", default=False) + + class Meta: + verbose_name = "页脚信息" verbose_name_plural = "页脚管理" + + def __str__(self): + return f"{self.title} - 状态:{'使用中' if self.is_active else '已停用'}" + + def save(self, *args, **kwargs): + # 1. 如果当前这条数据被标记为“生效” (is_active=True) + if self.is_active: + # 2. 将数据库中【除了当前这条】以外的所有 FooterLink 都更新为“不生效” + # self.pk 是当前对象的唯一标识 + FooterLink.objects.exclude(pk=self.pk).update(is_active=False) + + # 3. 执行正常的保存操作 + super().save(*args, **kwargs) diff --git a/base/seeder.py b/base/seeder.py new file mode 100644 index 0000000..ce0595f --- /dev/null +++ b/base/seeder.py @@ -0,0 +1,107 @@ +import os +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__))) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") # 修改为你的 settings 路径 +django.setup() + +from faker import Faker +from base.models import MainMenu + +# 初始化 Faker,设置中文(如果需要中文名) +# 初始化 Faker (zh_CN 生成中文数据) +fake = Faker('zh_CN') + +# --- 核心数据结构:定义你的菜单层级 --- +MENU_STRUCTURE = { + "信息公开": [ + "通知通告", "法律法规", "政策文件", "政策解读" + ], + "党建引领": [ + "党建工作", "党建轮播", "党风廉政", "先进典型" + ], + "互动交流": [ + "专家访谈", "调查征集", "在线咨询" + ], + "消防科普": [ + "数字消防", "火灾预防", "自救逃生" + ], + "办事服务": [ + "办事指南", "政务服务", "资料下载", "办事统计" + ] +} + + +def get_slug(text): + """ + 将中文转换为拼音 Slug + 例如: "信息公开" -> "xin-xi-gong-kai" + """ + # lazy_pinyin 返回拼音列表,join 将其用 '-' 连接 + return "-".join(lazy_pinyin(text, style=Style.NORMAL)) + + +def seed_main_menu(): + logger.info("🚀 开始生成站点导航数据...") + + # 1. 清空旧数据 (可选,如果需要重置数据请取消注释) + MainMenu.objects.all().delete() + logger.info("🧹 已清空旧数据") + + created_count = 0 + + # 2. 循环创建主菜单和子板块 + for main_title, sub_titles in MENU_STRUCTURE.items(): + # --- 创建主菜单 --- + # 生成唯一的 code,例如 "xin-xi-gong-kai" + main_code = get_slug(text=main_title) + logger.info(f"主菜单: [{main_title}] Code: [{main_code}]") + main_menu, created = MainMenu.objects.get_or_create( + title=main_title, + defaults={ + 'code': main_code, + 'url': main_code, # 模拟 URL + 'is_external': False, + 'order': created_count, + 'visible': True + } + ) + + if created: + logger.info(f"✅ 创建主菜单: [{main_title}]") + else: + logger.info(f"⏭️ 主菜单已存在: [{main_title}]") + + # --- 创建子板块 --- + for idx, sub_title in enumerate(sub_titles): + sub_code = get_slug(text=sub_title) + logger.info(f"子菜单: [{sub_title}] Code: [{sub_code}]") + sub_menu, sub_created = MainMenu.objects.get_or_create( + title=sub_title, + parent=main_menu, # 关键:关联父级 + defaults={ + 'code': sub_code, + 'url': sub_code, + 'is_external': False, + 'order': idx, # 子菜单排序 + 'visible': True + } + ) + + if sub_created: + logger.info(f" └─ ✅ 创建子板块: [{sub_title}]") + else: + logger.info(f" └─ ⏭️ 子板块已存在: [{sub_title}]") + + created_count += 1 + + logger.info(f"\n🎉 导航数据生成完毕!共处理 {len(MENU_STRUCTURE)} 个主菜单。") + + +if __name__ == "__main__": + seed_main_menu() diff --git a/base/templatetags/__init__.py b/base/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/base/templatetags/footer_tags.py b/base/templatetags/footer_tags.py new file mode 100644 index 0000000..35de31c --- /dev/null +++ b/base/templatetags/footer_tags.py @@ -0,0 +1,36 @@ +from django import template +from loguru import logger + +from ..models import FooterLink, LinkCategory, MainMenu # 假设你的模型在当前目录的上一级 models.py 中 + +register = template.Library() + + +# inclusion_tag 装饰器会自动帮你渲染指定的模板 +@register.inclusion_tag('components/footer.html') +def show_footer(): + # 1. 获取当前生效的那一条页脚数据 + # 根据你之前的模型,is_active=True 的只有一条 + footer = FooterLink.objects.filter(is_active=True).first() + link_categories = LinkCategory.objects.filter(is_active=True) + logger.info(footer) + logger.info(link_categories) + + # 3. 将数据传递给模板 + return { + 'footer': footer, + 'categories': link_categories + } + + +@register.inclusion_tag('components/main_menu.html') +def show_main_menu(): + # 1. 获取当前生效的那一条页脚数据 + # 根据你之前的模型,is_active=True 的只有一条 + menus = MainMenu.objects.filter(visible=True, parent__isnull=True).order_by('order') + logger.info(menus) + + # 3. 将数据传递给模板 + return { + 'menus': menus, + } diff --git a/base/views.py b/base/views.py index 286b1b6..aff25ac 100644 --- a/base/views.py +++ b/base/views.py @@ -1,5 +1,45 @@ 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 os +import uuid +from datetime import datetime # Create your views here. -router = Router(tags=['content']) \ No newline at end of file +router = Router(tags=['content']) + + +@csrf_exempt +@require_http_methods(["POST"]) +def custom_upload_file(request): + """ + 自定义 CKEditor 5 上传视图 + 支持:图片 + PDF / Word / Excel / ZIP 等所有文件 + """ + file = request.FILES.get('upload') + if not file: + return JsonResponse({"error": "请选择文件"}, status=400) + + # 安全处理文件名(避免中文/特殊字符报错) + ext = file.name.split('.')[-1].lower() + filename = f"{uuid.uuid4().hex}_{datetime.now().strftime('%Y%m%d%H%M%S')}.{ext}" + + # 保存路径 + upload_dir = os.path.join(settings.MEDIA_ROOT, 'uploads') + os.makedirs(upload_dir, exist_ok=True) + file_path = os.path.join(upload_dir, filename) + + # 保存文件 + with open(file_path, 'wb+') as f: + for chunk in file.chunks(): + f.write(chunk) + + # 返回文件 URL(CKEditor 规范格式) + file_url = f"{settings.MEDIA_URL}uploads/{filename}" + return JsonResponse({ + "url": file_url, + "default": file_url + }) diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/base.py b/common/base.py new file mode 100644 index 0000000..c9c4b48 --- /dev/null +++ b/common/base.py @@ -0,0 +1,17 @@ +# import django +from django.db import models + + +class BaseEntity(models.Model): + created_at = models.DateTimeField(verbose_name='创建时间', auto_now_add=True) + updated_at = models.DateTimeField(verbose_name='更新时间', auto_now=True) + + class Meta: + abstract = True + + +class DownloaderCategory(models.TextChoices): + # 下载器名称 + # Deluge = 'De', 'Deluge' + Transmission = 'Tr', 'Transmission' + qBittorrent = 'Qb', 'qBittorrent' diff --git a/utils/common_response.py b/common/common_response.py similarity index 100% rename from utils/common_response.py rename to common/common_response.py diff --git a/db.sqlite3 b/db.sqlite3 deleted file mode 100644 index 829d1a2..0000000 Binary files a/db.sqlite3 and /dev/null differ diff --git a/front/urls.py b/front/urls.py index 5b1b3a2..59e4ea9 100644 --- a/front/urls.py +++ b/front/urls.py @@ -6,7 +6,9 @@ app_name = 'front' # 可选,用于命名空间(如 {% url 'main:index' %} urlpatterns = [ path('', views.index, name='index'), - path('h/', views.index, name='h'), + path('{p}
" for p in paragraphs]) + + # 创建新闻对象 + news = News.objects.create( + title=fake.sentence(nb_words=random.randint(4, 8))[:-1], # 生成标题并去掉末尾句号 + section=section, + author=author, + content=content_html, + is_published=True, # 默认已发布 + # 封面图留空,因为自动生成图片文件比较复杂,建议手动上传测试图 + ) + + created_count += 1 + print(f" - [{section.name}] {news.title}") + + print(f"✅ 成功生成 {created_count} 条新闻数据!") + + +if __name__ == "__main__": + seed_news(30) diff --git a/news/tests.py b/news/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/news/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/news/views.py b/news/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/news/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/poetry.lock b/poetry.lock index 25009c6..f3f289c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -46,7 +46,7 @@ version = "3.11.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"}, {file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"}, @@ -98,13 +98,33 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "diff-match-patch" +version = "20241021" +description = "Repackaging of Google's Diff Match and Patch libraries." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "diff_match_patch-20241021-py3-none-any.whl", hash = "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782"}, + {file = "diff_match_patch-20241021.tar.gz", hash = "sha256:beae57a99fa48084532935ee2968b8661db861862ec82c6f21f4acdd6d835073"}, +] + +[package.extras] +dev = ["attribution (==1.8.0)", "black (==24.8.0)", "build (>=1)", "flit (==3.9.0)", "mypy (==1.12.1)", "ufmt (==2.7.3)", "usort (==1.0.8.post1)"] + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [[package]] name = "django" version = "6.0.3" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.12" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3"}, {file = "django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1"}, @@ -201,6 +221,39 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "django-import-export" +version = "4.4.0" +description = "Django application and library for importing and exporting data with included admin integration." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "django_import_export-4.4.0-py3-none-any.whl", hash = "sha256:2d9b234c0f024d3377167f4d9c5a506e095c5bad98e06d30700e1d0752829e3d"}, + {file = "django_import_export-4.4.0.tar.gz", hash = "sha256:9900e99c89027594941074fb4cd63a5f2964975e239021765c0f066003fcd412"}, +] + +[package.dependencies] +diff-match-patch = "20241021" +Django = ">=4.2" +tablib = ">=3.7.0" + +[package.extras] +all = ["tablib[all]"] +cli = ["tablib[cli]"] +docs = ["openpyxl (==3.1.5)", "psycopg[binary] (>=3.2.9)", "sphinx (==8.1.3)", "sphinx-rtd-theme (==3.0.1)"] +ods = ["tablib[ods]"] +pandas = ["tablib[pandas]"] +tests = ["chardet (==5.2.0)", "coverage (==7.6.4)", "django-extensions (==3.2.3)", "memory-profiler (==0.61.0)", "mysqlclient (==2.2.5)", "psycopg[binary] (>=3.2.9)", "pytz (==2024.2)", "setuptools-scm (==8.1.0)", "tablib[all] (>=3.7.0)"] +xls = ["tablib[xls]"] +xlsx = ["tablib[xlsx]"] +yaml = ["tablib[yaml]"] + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [[package]] name = "django-ninja" version = "1.6.2" @@ -251,6 +304,27 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "django-seed" +version = "0.3.1" +description = "Seed your Django project with fake data" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "django-seed-0.3.1.tar.gz", hash = "sha256:93e65d2c10449c464b83e9031be02763ec10e786aa064d649c4dd5ad06fa0eea"}, +] + +[package.dependencies] +django = ">=1.11" +Faker = ">=0.7.7" +toposort = ">=1.5" + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [[package]] name = "django-simpleui" version = "2026.1.13" @@ -270,6 +344,29 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "faker" +version = "40.11.1" +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"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +tzdata = ["tzdata"] + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [[package]] name = "greenlet" version = "3.3.2" @@ -438,6 +535,30 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5,<4.0" +groups = ["main"] +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [[package]] name = "markdown" version = "3.10.2" @@ -844,6 +965,23 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "pypinyin" +version = "0.55.0" +description = "汉字拼音转换模块/工具." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" +groups = ["dev"] +files = [ + {file = "pypinyin-0.55.0-py2.py3-none-any.whl", hash = "sha256:d53b1e8ad2cdb815fb2cb604ed3123372f5a28c6f447571244aca36fc62a286f"}, + {file = "pypinyin-0.55.0.tar.gz", hash = "sha256:b5711b3a0c6f76e67408ec6b2e3c4987a3a806b7c528076e7c7b86fcf0eaa66b"}, +] + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1144,7 +1282,7 @@ version = "0.5.5" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"}, {file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"}, @@ -1159,6 +1297,32 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "tablib" +version = "3.9.0" +description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a"}, + {file = "tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2"}, +] + +[package.extras] +all = ["odfpy", "openpyxl (>=2.6.0)", "pandas", "pyyaml", "tabulate", "xlrd", "xlwt"] +cli = ["tabulate"] +ods = ["odfpy"] +pandas = ["pandas"] +xls = ["xlrd", "xlwt"] +xlsx = ["openpyxl (>=2.6.0)"] +yaml = ["pyyaml"] + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [[package]] name = "toml" version = "0.10.2" @@ -1176,6 +1340,23 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "toposort" +version = "1.10" +description = "Implements a topological sort algorithm." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87"}, + {file = "toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd"}, +] + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1219,12 +1400,12 @@ version = "2025.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] -markers = "sys_platform == \"win32\"" +groups = ["main", "dev"] files = [ {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, ] +markers = {main = "sys_platform == \"win32\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""} [package.source] type = "legacy" @@ -1535,7 +1716,28 @@ type = "legacy" url = "http://192.168.123.5:3141/root/pypi/+simple" reference = "mirrors" +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] + +[package.source] +type = "legacy" +url = "http://192.168.123.5:3141/root/pypi/+simple" +reference = "mirrors" + [metadata] lock-version = "2.1" python-versions = ">=3.13,<3.14" -content-hash = "486f6320097292ce4db7f06f502b35949aaca16bc30356379e2be8e5c17bf774" +content-hash = "3e611ebdd242413bf20b6f4f63f2db3238f46614acb7d4e54d315075c7ba0d7a" diff --git a/pyproject.toml b/pyproject.toml index a5452e4..6c4d96c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,9 +24,15 @@ django-environ = "^0.13.0" django-db-connection-pool = "^1.2.6" pillow = "^12.1.1" django-ckeditor-5 = "^0.2.20" +loguru = "^0.7.3" +django-import-export = "^4.4.0" +[tool.poetry.group.dev.dependencies] +pypinyin = "^0.55.0" +django-seed = "^0.3.1" + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/server/settings.py b/server/settings.py index 8cb421b..42ab2cf 100644 --- a/server/settings.py +++ b/server/settings.py @@ -11,10 +11,34 @@ https://docs.djangoproject.com/en/6.0/ref/settings/ """ import os from pathlib import Path +from loguru import logger +import environ + +env = environ.Env() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +# 获取 env文件路径 +env_file = BASE_DIR / "db/.env" + +environ.Env.read_env(env_file) +logger.info(".ENV变量输出", env.ENVIRON) +# 日志文件存储配置 +LOGURU_FILE = { + "is_show": "off", + "level": "ERROR", + 'path': os.path.join(Path(__file__).parent, 'db/logs', 'test{time:YYYY-MM-DD}.logu'), + "rotation": "10MB", + 'retention': "7days", +} + +# 日志控制台配置 +LOGURU_CONSOLE = { + "is_show": "on", # 配置是否开启控制台打印日志 + "level": "INFO", # 最低打印级别是info级别 +} + # 日志文件夹不存在时创建 logs_path = os.path.join(BASE_DIR, 'db/logs') if not os.path.exists(logs_path): @@ -29,7 +53,16 @@ SECRET_KEY = 'django-insecure-gr&k$v)_t*ax)yr1v9iz3x%$-j812ajpxs+fe2dn*dgy=a!730 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["*"] +ALLOWED_HOSTS = ['*', "127.0.0.1", "localhost"] + +# 2. 【必须添加】这个配置专门用于解决 Origin checking failed 报错 +# 注意:必须包含协议头 (http://) 和端口号 +CSRF_TRUSTED_ORIGINS = [ + 'http://127.0.0.1:11111', # 对应你报错的端口 + 'http://127.0.0.1', + 'http://localhost:11111', + 'http://localhost', +] AUTH_USER_MODEL = 'users.User' @@ -43,18 +76,21 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'import_export', 'django_ckeditor_5', 'base.apps.BaseConfig', 'article.apps.ArticleConfig', 'users.apps.UsersConfig', - 'front.apps.ContentConfig' + 'front.apps.ContentConfig', + 'news.apps.NewsConfig', + 'django_seed', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', @@ -84,12 +120,40 @@ ASGI_APPLICATION = 'server.asgi.application' # Database # https://docs.djangoproject.com/en/6.0/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', +if env('DB_REMOTE', default=False): + # mysql_connect = dj_database_url.parse(os.getenv('MYSQL_CONNECTION'), conn_max_age=0) + # print("数据库连接信息", mysql_connect) + DATABASES = { + 'default': { + 'ENGINE': env('DB_ENGINE', default='dj_db_conn_pool.backends.postgresql'), + 'NAME': env('DB_NAME', default='harvest'), + 'USER': env('DB_USER', default='harvest'), + 'PASSWORD': env('DB_PASSWORD', default='harvest'), + 'HOST': env('DB_HOST', default='harvest_postgres'), + 'PORT': env('DB_PORT', default=5432), + 'CONN_MAX_AGE': env('DB_CONN_MAX_AGE', default=0), # 必须设为 0,让池管理连接生命周期 + 'OPTIONS': { + # 'MAX_CONNS': env('DB_MAX_CONNS', default=30), # 池中最大物理连接数 (根据 PG 总限制调整) + # 'REUSE_CONNS': env('DB_REUSE_CONNS', default=10), # 最小保留连接数 + }, + 'POOL_OPTIONS': { + 'POOL_SIZE': env('DB_MAX_CONNS', default=100), # 增加连接池大小 + 'MAX_OVERFLOW': 20, # 增加最大溢出连接数 + 'TIMEOUT': 30, # 连接超时设置 + } + }, + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db/data.sqlite3', + 'OPTIONS': { + 'timeout': 120, + 'check_same_thread': False + } + }, } -} # Password validation # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators @@ -240,15 +304,25 @@ customColorPalette = [ }, ] -CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # optional -CKEDITOR_5_FILE_STORAGE = "path_to_storage.CustomStorage" # optional +# CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # optional +CKEDITOR_5_ALLOW_ALL_FILE_TYPES = True +CKEDITOR_5_UPLOAD_FILE_TYPES = [ + 'jpeg', 'png', 'jpg', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', +] +# 允许上传非图片文件(PDF、Word、Excel、ZIP等) +CKEDITOR_ALLOW_NONIMAGE_FILES = True # 必须设为 True +CKEDITOR_5_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' # optional CKEDITOR_5_CONFIGS = { 'default': { 'toolbar': { 'items': ['heading', '|', 'bold', 'italic', 'link', - 'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', ], + 'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', 'uploadFile', ], + }, + 'language': 'zh-cn', + # 2. 允许上传的文件类型 (根据需要扩展) + 'fileUploader': { + 'supportedFileTypes': ['pdf', 'doc', 'docx', 'zip', 'txt', 'jpg', 'png'], } - }, 'extends': { 'blockToolbar': [ @@ -261,12 +335,16 @@ CKEDITOR_5_CONFIGS = { 'toolbar': { 'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough', 'code', 'subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage', - 'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|', + 'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', 'uploadFile', '|', 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat', 'insertTable', ], 'shouldNotGroupWhenFull': 'true' }, + # --- 在 extends 配置中也必须加上这个 --- + 'fileUploader': { + 'supportedFileTypes': ['pdf', 'doc', 'docx', 'zip', 'txt'] + }, 'image': { 'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft', 'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'], @@ -313,4 +391,3 @@ CKEDITOR_5_CONFIGS = { CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any" # CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME = "custom_upload_file" - diff --git a/server/urls.py b/server/urls.py index e784c5b..0862e00 100644 --- a/server/urls.py +++ b/server/urls.py @@ -17,7 +17,7 @@ Including another URLconf 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 +from django.urls import path, include, re_path import logging import os @@ -33,8 +33,9 @@ from ninja import NinjaAPI from ninja.errors import ValidationError from ninja.security import HttpBearer +from base.views import custom_upload_file +from common.common_response import CommonResponse from .settings import SECRET_KEY -from utils.common_response import CommonResponse logger = logging.getLogger('ptools') @@ -68,6 +69,7 @@ class GlobalAuth(HttpBearer): # 2. 获取当前文件所在的目录路径 (假设此文件与各个 app 文件夹同级,或者在特定配置目录下) # 如果你的此文件在项目根目录,而 app 文件夹也在根目录: from django.conf import settings + project_root = settings.BASE_DIR @@ -123,6 +125,7 @@ def auto_load_routers(base_path, package_name_prefix=None): except Exception as e: print(f"❌ 注册失败 {modname}: {e}") + api_v1 = NinjaAPI(version='1.0.0', auth=GlobalAuth()) auto_load_routers(project_root) @@ -152,7 +155,7 @@ urlpatterns = [ path('admin/', admin.site.urls), path("api/", api_v1.urls), path("ckeditor5/", include('django_ckeditor_5.urls')), - # path("upload/", custom_upload_function, name="custom_upload_file"), + path("upload/", custom_upload_file, name="custom_upload_file"), ] urlpatterns += staticfiles_urlpatterns() diff --git a/templates/base.html b/templates/base.html index 327a42f..cd033a9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,4 +1,6 @@ {% load static %} +{% load footer_tags %} + @@ -39,32 +41,33 @@