# 练手案例集合 **Repository Path**: pengjinOnline/cms ## Basic Information - **Project Name**: 练手案例集合 - **Description**: 练习Form,ModelForm,上传,KindEditor富文本编辑器 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-01-02 - **Last Updated**: 2026-02-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 练手笔记 该项目属于我个人的项目组件,主要手段包括如下: * 后端采用的是Django3.2.24 + MySql5.7 * 前端为Bootstrap3 + Jquery + echarts * 内容涵盖:Form,ModelForm,上传,Ajax,统计图,KindEditor富文本编辑器 * 该项目是动静不分离的项目 * 这些组件会持续更新,这里先上列表组件,有兴趣的请关注 ## requirements依赖 ```txt asgiref==3.11.0 Django==3.2.24 PyMySQL==1.0.2 pytz==2025.2 sqlparse==0.5.4 typing-extensions==4.15.0 ``` ## 第1部分:定义列表组件 ### 1.1 定义列表组件的html模板 此组件支持的样式为bootstrap v3,列表组件模板如下所示: `此组件的名称为: bootstrap_list_component.html` 此组件虽然分页类和列表组件类对其进行支持: ```html {% extends "public/bootstrap_base_layout.html" %} {% block content %}
{% for btn in btnGroup %} {{btn.text}} {% endfor %}
{{headingTitle}}
{% for filed_title in list_fields %} {% endfor %} {% if rsTotal > 0 %} {% for item in rs %} {% for key,value in item.items %} {% if forloop.counter == 1 %} {% else %} {% endif %} {% if forloop.counter == btns_col_flag %} {% endif %} {% endfor %} {% endfor %} {% else %} {% endif %}
{{filed_title}}操作
{{value}} {% if key in media_fields %} {% else %} {{value}} {% endif %} {% for btn in rows_btnGroup %} {% if btn.type == "delete" %} {{btn.text}} {% else %} {{btn.text}} {% endif %} {% endfor %}
当前找不到任何可展示的数据!
{% if rsTotal > 0 %}
总记录{{pager.rsTotal}}条,当前{{pager.current}}/{{pager.pageTotal}}页
{% endif %} {% endblock %} {% block js %} {% endblock %} ``` ### 1.2 分页类(pagenation.py) ```python from math import ceil class Pageniation(object): def __init__(self, request, rsTotal, parameter="page", pageSize=10): # 总记录数 self.rsTotal = rsTotal # 每页多少条记录 self.pageSize = pageSize # 总页数 self.pageTotal = ceil(self.rsTotal / self.pageSize) # 页码变量 self.parameter = parameter # 当前页 self.current = int(request.GET.get(parameter, 1)) if self.current > self.pageTotal: self.current = self.pageTotal elif self.current < 1: self.current = 1 # 分页截取记录的开始 self.start = (self.current - 1) * self.pageSize # 分页截取记录的结束 self.end = self.current * self.pageSize # plus为前后5页 self.plus = 5 def getPageNumbers(self): # 总页数不足<=11页时,也就是应该有多少页就是多少页 if self.pageTotal <= 2 * self.plus + 1: start_page = 1 end_page = self.pageTotal else: # 当前页如果小于等于<5,也是用户的当前页在1-5页范围时,显示在节目上的页码数为1-10 if self.current <= self.plus: start_page = 1 end_page = 2 * self.plus else: # 当前页如果大于5(很简单,你就按当前在第6页来看),应该显示为1 - 11 # 但这样就会出现一个问题,start解决了,可是end_page会超过总页码。 # 所以这里还需要解决end_page超过总页码的问题 if self.current + self.plus > self.pageTotal: # 你想象总页数是第20页且当前页也是第20页,显示10-20 start_page = self.pageTotal - 2 * self.plus end_page = self.pageTotal else: start_page = self.current - self.plus end_page = self.current + self.plus return list(range(start_page, end_page + 1)) ``` ### 1.3 列表视图类(listView.py) ```python from .pagenation import Pageniation class BootstrapListView(object): # 构造函数 def __init__(self, request, dbTable, headingTitle, list_fields, btnGroup, search={"field": "name", "keyword": "", "placeholder": "请输入搜索关键字"} ): # 列表的表格标题 self.headingTitle = headingTitle self.btnGroup = btnGroup # 表格的列名称 self.list_fields = list_fields # 操作栏的按钮组flag self.btns_col_flag = len(list_fields) # orm的model self.dbTable = dbTable # self.media_fields默认为空列表,必须通过set_mediafields设置 self.media_fields = [] # 搜索组件 self.search = search if self.search["keyword"]: self.rsTotal = self.dbTable.objects.filter( **{self.search["field"] + "__contains": self.search["keyword"]}).count() else: self.rsTotal = self.dbTable.objects.count() # 获取数据库总记录数 # 分页组件 if self.rsTotal > 0: self.pager = Pageniation(request=request, rsTotal=self.rsTotal) else: self.pager = None # 操作栏按钮组 self.rows_btnGroup = [ {"type": "edit", "class": "btn btn-sm btn-info", "text": "rrr", "icon": "glyphicon glyphicon-pencil", "url": "", "id": ""}, {"type": "delete", "class": "btn btn-sm btn-danger", "text": "", "icon": "glyphicon glyphicon-trash", "url": "", "id": ""}, ] # 默认按照order by id desc self.order_by = "-id" # 设置操作栏默认按钮组属性 def setListBtns(self, edit, delete): # 设置编辑按钮id,text,url self.rows_btnGroup[0]["id"] = edit["id"] self.rows_btnGroup[0]["text"] = edit.get("text","编 辑") self.rows_btnGroup[0]["url"] = edit["url"] # 设置编辑删除按钮id,text,url self.rows_btnGroup[1]["id"] = delete["id"] self.rows_btnGroup[1]["text"] = delete.get("text","删 除") self.rows_btnGroup[1]["url"] = delete["url"] # 在操作栏按钮组中追加按钮 def appendListButton(self, button): self.rows_btnGroup.append(button) # 设置媒体字段列表 def setMediaFields(self, media_fields): self.media_fields = media_fields # 设置获取数据列表方式为normal还是 def setListMode(self, normal=True, fields=tuple()): if normal: self.recordset = self.getNormallist(*fields) else: self.recordset = self.getRefChoiceList(fields) # 设置order by def setOrderBy(self,order_by): self.order_by = order_by # 获取没有关联和choice的QuerySet def getNormallist(self, *args): if self.search["keyword"] and self.rsTotal > 0: return self.dbTable.objects.filter(**{self.search["field"] + "__contains": self.search["keyword"]}).order_by(self.order_by).values( *args)[self.pager.start:self.pager.end] elif self.search["keyword"] == "" and self.rsTotal > 0: return self.dbTable.objects.order_by(self.order_by).values(*args)[self.pager.start:self.pager.end] else: return [] # 获取有关联和choice的QuerySet def getRefChoiceList(self, fields): modelObj = [] if self.search["keyword"] and self.rsTotal > 0: modelObj = self.dbTable.objects.filter( **{self.search["field"] + "__contains": self.search["keyword"]}).order_by(self.order_by).all()[self.pager.start:self.pager.end] elif self.search["keyword"] == "" and self.rsTotal > 0: modelObj = self.dbTable.objects.order_by(self.order_by).all()[self.pager.start:self.pager.end] processed_rs = [] for rs in modelObj: item_dict = {} # 处理每个字段 for field in fields: print(field) # 特别处理以 'get_' 开头和 '_display' 结尾的字段 if field.startswith('get_') and field.endswith('_display'): # 使用 getattr 动态获取方法并调用 display_value = getattr(rs, field)() item_dict[field] = display_value else: if "." in field: attrs = field.split(".") current_object = rs for attr in attrs: current_object = getattr(current_object, attr) item_dict[field] = current_object else: # 普通字段,直接获取值 item_dict[field] = getattr(rs, field) processed_rs.append(item_dict) return processed_rs def dict(self): return { "search": self.search, "media_fields": self.media_fields, "btnGroup": self.btnGroup, "rows_btnGroup": self.rows_btnGroup, "headingTitle": self.headingTitle, "list_fields": self.list_fields, "rs": self.recordset, "btns_col_flag": self.btns_col_flag, "rsTotal":self.rsTotal, "pager": self.pager, } ``` ### 1.4 组件使用示例 没有choice和关联的示例: ```python from django.shortcuts import render, HttpResponse from apps.models import Category from apps.utils.listView import BootstrapListView # 类别列表 def index(request): bootstrapList = BootstrapListView(request=request, dbTable=Category, headingTitle="用户分类列表", list_fields=["# ID", "分类名称"], btnGroup=[ { "class": "btn btn-primary", "text": "新增用户类别", "icon": "glyphicon glyphicon-plus", "url": "category/insert/", "id": "addCategoryBtn" }, ], search={"field": "title", "keyword": request.GET.get("keyword", ""), "placeholder": "请输入类别名称关键字"} ) # 设置当前列表数据方式,只查找id和title两个字段 bootstrapList.setListMode(normal=True, fields=("id", "title")) # 设置列表中的默认按钮组属性 bootstrapList.setListBtns(edit={"id": "cateEditBtn", "url": "/category/edit/", "text": "编辑类别"}, delete={"id": "cateRemoveBtn", "url": "/category/delete/", "text": "删除类别"} ) return render(request, "components/bootstrap_list_component.html", bootstrapList.dict()) ``` 有关联和choices的示例 ```python from django.shortcuts import render, HttpResponse from apps.models import User from apps.utils.listView import BootstrapListView # 用户列表 def index(request): bootstrapList = BootstrapListView(request=request, dbTable=User, headingTitle="用户列表", list_fields=["# ID", "用户名", "用户类别", "性别"], btnGroup=[ { "class": "btn btn-primary", "text": "新增用户", "icon": "glyphicon glyphicon-plus", "url": "/users/insert/", "id": "addUserBtn" }, ], search={"field": "username", "keyword": request.GET.get("keyword", ""), "placeholder": "请输入用户名关键字"} ) # 设置当前列表数据方式 bootstrapList.setListMode(normal=False, fields=("id", "username", "cate.title", "get_gender_display")) # 设置列表中的默认按钮组属性 bootstrapList.setListBtns(edit={"id": "userEditBtn", "url": "/users/edit/", "text": "编辑用户"}, delete={"id": "userRemoveBtn", "url": "/users/delete/", "text": "删除用户"} ) return render(request, "components/bootstrap_list_component.html", bootstrapList.dict()) ``` ## 第2部分:目录结构和路由设置 ```python cms/ apps/ users/ # 用户管理 __init__.py views.py # 视图文件 urls.py # 所有用户相关路由 category/ # 分类管理 __init__.py views.py # 视图文件 urls.py # 所有用户相关路由 login/ # 登录 __init__.py views.py # 视图文件 urls.py # 所有登录相关路由 ``` 在apps中包含的包`users,category,login`是子应用的功能模块,设置为包. 在该目录的`urls.py`中包含类似如下的路由设置: ```python from django.urls import path from . import views urlpatterns = [ path('index/', views.index ), #....有更多的路由 ] ``` 最后在项目的`urls.py`中设置如下路由: ```python from django.urls import path,include urlpatterns = [ # 用户管理 path('users/', include("apps.users.urls") ), # 类别管理 path('category/', include("apps.category.urls") ), # 登录入口,是我们最先看到的入口,所以设置为/ path('/',include("apps.login.urls")), ] ``` ## 第3部:构建kindEditor组件 ### 3.1 解决X-Frame-Options错误 在kindEditor上传中会产生X-Frame-Options错误,我们需要在setting.py中设置如下内容: ```python # 允许同源框架嵌入 X_FRAME_OPTIONS = 'SAMEORIGIN' # 或者如果需要允许特定域名嵌入 #X_FRAME_OPTIONS = 'ALLOW-FROM http://127.0.0.1:8000/' # 完全禁用(仅开发环境) # X_FRAME_OPTIONS = 'DENY' # 改为SAMEORIGIN或注释掉 ``` ### 3.2 编写KindEditor上传图片的脚本 ```python import os from django.http import JsonResponse from django.conf import settings from apps.utils.ymdhis import generate_single_timestamp def kindeditor(request): uploader = request.FILES.get("kindImg") if not uploader: dic = {'error': 1,'url': '',"message":"上传错误"} else: file_name = "{}{}".format(generate_single_timestamp(),os.path.splitext(uploader.name)[1]) file_path = os.path.join(settings.MEDIA_ROOT,file_name) with open(file_path,mode="wb") as f: for chunk in uploader.chunks(): f.write(chunk) dic = {'error': 0, 'url': f"/media/{file_name}", "message": file_name} print(dic) return JsonResponse(dic) ``` generate_single_timestamp 函数用来为图片命名: ```python from datetime import datetime def generate_single_timestamp(): """ 生成一个用于文件命名的单一时点时间戳。 格式为:年月日_时分秒,例如 '20250112160205' """ # 获取当前时间 now = datetime.now() # 格式化为字符串:年-月-日-时-分-秒 timestamp_str = now.strftime("%Y%m%d%H%M%S") return timestamp_str ``` ### 3.3 安装KindEditor ```python {% extends "./bootstrap_form_component.html" %} {% load static %} {% block js %} {% endblock %} ``` ### 3.4 测试KindEditor的使用 ```python # 用户详情 def details(request,id): #查询当前需要编辑的记录,如果不存在则调回首页 action = f"/users/details/{id}/" btnText = "编辑用户详情" detail = Details.objects.filter(user_id__exact=id).first() if not detail: return redirect("/users/index/") if request.method == "GET": forms = UserDetailsModelForm(instance=detail) return render(request, "components/bootstrap_kindEditor_component.html", { "forms": forms, "action": action, "btnText": btnText, "panelTitle": " 编辑用户详情" }) else: # print(request.POST) forms = UserDetailsModelForm(data=request.POST,instance=detail) if forms.is_valid(): forms.save() return redirect("/users/index/") else: return render(request, "components/bootstrap_kindEditor_component.html", { "forms": forms, "action": action, "btnText": btnText, "panelTitle": " 编辑用户详情" }) ```