Stark 组件:快速开发神器 —— 锦上添花

Stark 组件:快速开发神器 —— 锦上添花

经过前面几个篇章,我们的 Stark 组件已经能够批量生成 URL,快速实现增删改查了,接下来这篇文章,就要对已经完成的 Stark 组件增加更多更有趣的功能。

一、分页

Django 其实有自带的分页器功能,但是呢,我们是想开发一套可以随处使用的组件,因此可以仿照 Django 的分页器自己写一个。

# Stark/utils/pagingDevice.py
class PagingDevice:
	def __init__(self, currentPage, allCount, baseUrl, queryParams, perPage=20, pageCount=11):
		"""
		初始化分页器
		:param currentPage:当前页码
		:param allCount: 数据库中总条数
		:param baseUrl: 基础 URL
		:param queryParams: 包含所有当前 URL 的 QueryDict 对象
		:param perPage: 每页显示数据量
		:param pageCount: 页面最多显示页码数
		"""
		try:
			self.currentPage = int(currentPage)
			if self.currentPage <= 0:
				raise Exception()
		except Exception:
			self.currentPage = 1
		self.allCount, self.baseUrl, self.queryParams, self.perPage, self.pageCount = \
			allCount, baseUrl, queryParams, perPage, pageCount
		# 计算总页码,如果有余要+1
		pagerCount, remainder = divmod(allCount, perPage)
		if remainder:
			pagerCount += 1
		self.pagerCount = pagerCount
		self.halfPagerPageCount = int(pagerCount / 2)

	@property
	def start(self):
		"""数据起始索引"""
		return (self.currentPage - 1) * self.perPage

	@property
	def end(self):
		"""数据结束索引"""
		return self.currentPage * self.perPage

	def PageHtml(self):
		"""生成分页器 HTML 代码"""
		if self.pagerCount < self.pageCount:
			# 如果数据总页码 < 页面上最多显示的页码数
			pagerStart, pagerEnd = 1, self.pagerCount
		else:
			# 数据总页码数 > 页面上最多显示的页码数
			if self.currentPage <= self.halfPagerPageCount:
				# 如果当前页码 < 页面上最大显示的页码半数
				pagerStart, pagerEnd = 1, self.pagerCount
			else:
				if (self.currentPage + self.halfPagerPageCount) > self.pagerCount:
					# 如果当前页 + 页面上最大显示的页码半数 > 数据总页码
					pagerStart, pagerEnd = self.pagerCount - self.pageCount + 1, self.pageCount
				else:
					pagerStart, pagerEnd = self.currentPage - self.halfPagerPageCount, self.currentPage + self.halfPagerPageCount

		pageList = []
		if self.currentPage <= 1:
			previousPage = '<li><a href="#">上一页</a></li>'
		else:
			self.queryParams["page"] = self.currentPage - 1
			previousPage = '<li><a href="%s?%s">上一页</a></li>' % (self.baseUrl, self.queryParams.urlencode())
		pageList.append(previousPage)
		for index in range(pagerStart, pagerEnd - 1):
			self.queryParams["page"] = index
			if self.currentPage == index:
				item = '<li class="active"><a href="%s?%s">%s</a></li>' % (self.baseUrl, self.queryParams.urlencode(), index)
			else:
				item = '<li><a href="%s?%s">%s</a></li>' % (self.baseUrl, self.queryParams.urlencode(), index)
			pageList.append(item)

		if self.currentPage >= self.pagerCount:
			nextPage = '<li><a href="#">下一页</a></li>'
		else:
			self.queryParams["page"] = self.currentPage + 1
			nextPage = '<li><a href="%s?%s">下一页</a></li>' % (self.baseUrl, self.queryParams.urlencode())
		pageList.append(nextPage)
		pageStr = "".join(pageList)
		return pageStr

然后在 checkView 视图函数中添加上我们的分页器:

	def checkView(self, request, *args, **kwargs):
		"""查看功能视图函数"""
		# --------------------- 1.显示表格 ---------------------
		displayList = self.getDisplayList(request, *args, **kwargs)
		# 1.1、处理表格表头
		headerList = []
		if displayList:
			for item in displayList:
				verboseName = item(self, obj=None, isHeader=True) if isinstance(item, FunctionType) \
					else self.model._meta.get_field(item).verbose_name
				headerList.append(verboseName)
		else:
			headerList.append(self.model._meta.model_name)
		# 1.2、处理表格内容
		bodyList = []
		dataQuerySet = self.model.objects.all()
		for row in dataQuerySet:
			rowList = []
			if displayList:
				for item in displayList:
					rowList.append(
						item(self, obj=row, isHeader=False, *args, **kwargs) if isinstance(item, FunctionType)
						else getattr(row, item))
			else:
				rowList.append(row)
			bodyList.append(rowList)

		# --------------------- 2.添加按钮 ---------------------
		addButton = self.getAddButton(request, *args, **kwargs)

		# --------------------- 3.分页器 ---------------------
		allCount = dataQuerySet.count()
		queryParams = request.GET.copy()
		queryParams._mutable = True
		pagingDevice = PagingDevice(
			currentPage=request.GET.get("page"),
			allCount=allCount,
			baseUrl=request.path_info,
			queryParams=queryParams,
			perPage=self.perPageCount
		)
		bodyList = bodyList[pagingDevice.start:pagingDevice.end]

		return render(request, "stark/checkView.html", {
			"headerList": headerList,
			"bodyList": bodyList,
			"dataQuerySet": dataQuerySet,
			"addButton": addButton,
			"pager": pagingDevice,
		})

然后在 checkView.html 中加上分页器:

        <nav class="center">
            <ul class="pagination">
                {{ pager.pageHtml|safe }}
            </ul>
        </nav>

这样分页器就做好了:
在这里插入图片描述

二、排序

我们现在是按照默认的 ID 排序,但有的时候可能我们不想按照它排序,因此可以定制一个排序功能。

	def getOrderList(self):
		"""获取页面排序的表格,预留自定义扩展定制排序选项"""
		return self.orderList or ["-id"]

因为排序要在获取数据库数据的时候进行,因此应该将排序放在显示表格之前:

	def checkView(self, request, *args, **kwargs):
		"""查看功能视图函数"""
		dataQuerySet = self.model.objects.all()
		# --------------------- 4.排序 ---------------------
		orderList = self.getOrderList()
		dataQuerySet = dataQuerySet.order_by(*orderList)

		# --------------------- 1.显示表格 ---------------------
		displayList = self.getDisplayList(request, *args, **kwargs)
		# 1.1、处理表格表头
		headerList = []
		if displayList:
			for item in displayList:
				verboseName = item(self, obj=None, isHeader=True) if isinstance(item, FunctionType) \
					else self.model._meta.get_field(item).verbose_name
				headerList.append(verboseName)
		else:
			headerList.append(self.model._meta.model_name)
		# 1.2、处理表格内容
		bodyList = []
		for row in dataQuerySet:
			rowList = []
			if displayList:
				for item in displayList:
					rowList.append(
						item(self, obj=row, isHeader=False, *args, **kwargs) if isinstance(item, FunctionType)
						else getattr(row, item))
			else:
				rowList.append(row)
			bodyList.append(rowList)

		# --------------------- 2.添加按钮 ---------------------
		addButton = self.getAddButton(request, *args, **kwargs)

		# --------------------- 3.分页器 ---------------------
		allCount = dataQuerySet.count()
		queryParams = request.GET.copy()
		queryParams._mutable = True
		pagingDevice = PagingDevice(
			currentPage=request.GET.get("page"),
			allCount=allCount,
			baseUrl=request.path_info,
			queryParams=queryParams,
			perPage=self.perPageCount
		)
		bodyList = bodyList[pagingDevice.start:pagingDevice.end]

		return render(request, "stark/checkView.html", {
			"headerList": headerList,
			"bodyList": bodyList,
			"dataQuerySet": dataQuerySet,
			"addButton": addButton,
			"pager": pagingDevice,
		})

这样就能实现对表格按照 ID 逆序排列:
在这里插入图片描述
如果想要按照其它方式排列,在子类中重写 orderList 即可。

三、搜索

1.关键字搜索

搜索功能可以帮助我们快速筛选目标数据,因此,我们的 Stark 组件当然不能少了关键字搜索了。

	def getSearchList(self):
		"""获取搜索列表"""
		return self.searchList
	def changeView(self, request, pk, *args, **kwargs):
		"""修改功能视图函数"""
		form = None
		currentChangeObject = self.model.objects.filter(pk=pk).first()
		if not currentChangeObject:
			return render(request, "404.html")
		modelForm = self.getModelForm(isAdd=False, request=request, pk=pk, *args, **kwargs)
		if request.method == "GET":
			form = modelForm(instance=currentChangeObject)
		elif request.method == "POST":
			form = modelForm(data=request.POST, instance=currentChangeObject)
			if form.is_valid():
				return HttpResponse(self.save(request=request, form=form, isUpdate=True, *args, **kwargs)) \
				       or redirect(self.reverseListUrl(*args, **kwargs))
		return render(request, self.changeTemplate or "stark/addOrChange.html", {"form": form})

	def checkView(self, request, *args, **kwargs):
		"""查看功能视图函数"""
		dataQuerySet = self.model.objects.all()
		# --------------------- 5.搜索 ---------------------
		searchList = self.getSearchList()
		if searchList:
			searchValue = request.GET.get("keyword", None)
			connect = Q()
			connect.connector = "OR"
			if searchValue:
				for item in searchList:
					connect.children.append((item, searchValue))
			dataQuerySet = dataQuerySet.filter(connect)

		# --------------------- 4.排序 ---------------------
		orderList = self.getOrderList()
		if orderList:
			dataQuerySet = dataQuerySet.order_by(*orderList)

		# --------------------- 1.显示表格 ---------------------
		displayList = self.getDisplayList(request, *args, **kwargs)
		# 1.1、处理表格表头
		headerList = []
		if displayList:
			for item in displayList:
				verboseName = item(self, obj=None, isHeader=True) if isinstance(item, FunctionType) \
					else self.model._meta.get_field(item).verbose_name
				headerList.append(verboseName)
		else:
			headerList.append(self.model._meta.model_name)
		# 1.2、处理表格内容
		bodyList = []
		for row in dataQuerySet:
			rowList = []
			if displayList:
				for item in displayList:
					rowList.append(
						item(self, obj=row, isHeader=False, *args, **kwargs) if isinstance(item, FunctionType)
						else getattr(row, item))
			else:
				rowList.append(row)
			bodyList.append(rowList)

		# --------------------- 2.添加按钮 ---------------------
		addButton = self.getAddButton(request, *args, **kwargs)

		# --------------------- 3.分页器 ---------------------
		allCount = dataQuerySet.count()
		queryParams = request.GET.copy()
		queryParams._mutable = True
		pagingDevice = PagingDevice(
			currentPage=request.GET.get("page"),
			allCount=allCount,
			baseUrl=request.path_info,
			queryParams=queryParams,
			perPage=self.perPageCount
		)
		bodyList = bodyList[pagingDevice.start:pagingDevice.end]

		return render(request, "stark/checkView.html", {
			"addButton": addButton,
			"searchList": searchList,
			"headerList": headerList,
			"bodyList": bodyList,
			"dataQuerySet": dataQuerySet,
			"pager": pagingDevice,
		})

然后在 RbacUserHandler 中定义可搜索的字段:

from Stark.main import StarkHandler, getM2M, getDatetime, getChoice


class RbacUserHandler(StarkHandler):
	def __init__(self, site, modelClass, prefix):
		super().__init__(site, modelClass, prefix)
		self.displayList = ["username", "email", "score", getChoice("等级", "grade"), getM2M("职务", "roles"), "team",
		                    "department", getDatetime("加入时间", "dateJoined")]
		self.searchList = ["username"]

最后在 checkView 页面也要加上搜索框:

{% if searchList %}
      <div style="float: right;margin: 5px 0;">
          <form method="GET" class="form-inline">
              <div class="form-group">
                  <label>
                      <input class="form-control" type="text" name="keyword" value="{{ search_value }}"
                             placeholder="关键字搜索">
                  </label>
                  <button class="btn btn-primary" type="submit">
                      <i class="fa fa-search" aria-hidden="true"></i>
                  </button>
              </div>
          </form>
      </div>
  {% endif %}

在这里插入图片描述

2.组合搜索

通过关键字搜索只能实现单一的搜索功能,我们有时想要的可以能是多个条件筛选的组合搜索结果。

对于组合搜索来说情况比较多,比如好几个字段都支持组合搜搜,还有某一个字段支持多选,类似这些功能比较复杂,因此我们可以将组合搜索的功能抽象成一个类。

可以先写 getSearchGroup 方法,通过这个方法获取组合搜索的条件:

	def getSearchGroup(self, request):
		"""获取组合搜索条件"""
		condition = {}
		for option in self.searchGroup:
			if option.isMulti:
				searchGroupValuesList = request.GET.getlist(option.field)
				if searchGroupValuesList:
					condition["%s__in" % option.field] = searchGroupValuesList
			else:
				searchGroupValue = request.GET.get(option.field)
				if searchGroupValue:
					condition[option.field] = searchGroupValue
		return condition

对于可以多选的字段,通过condition["%s__in" % option.field] = searchGroupValuesList的方式比较,而对于单选的字段,可以通过condition[option.field] = searchGroupValue的方式比较,前边是筛选语句,后边是筛选值。

例如对于 RBACUserInfo 来说,我们可以定义几个组合搜索的字段:

		self.searchGroup = [
			SearchGroupOption(field="team"),
			SearchGroupOption(field="department"),
			SearchGroupOption(field="grade"),
		]

team 和 department 都是单选,而 grade 可以多选。

这时候我们可以编写 SearchGroupOption 类了,首先确定这个类需要什么参数:

  1. 支持组合搜索的字段;
  2. 组合搜索的按钮显示问呢;
  3. 组合搜索的按钮值
  4. 该字段是否支持多选
class SearchGroupOption:
    def __init__(self, field, text=None, value=None, isMulti=False, condition=None):
        """
        :param field:组合搜索关联的字段
        :param text: 显示组合搜索的按钮文本
        :param value: 显示组合搜索按钮值
        :param isMulti: 是否支持多选
        :param condition: 数据库查询条件
        """
        self.field, self.text, self.value, self.isMulti, self.condition, self.isChoice = \
            field, text, value, isMulti, condition if condition else {}, False

    def getCondition(self, request, *args, **kwargs):
        return self.condition

    def getFieldData(self, modelClass, request, *args, **kwargs):
        """根据字段去数据库获取关联的数据"""
        fieldObject = modelClass._meta.get_field(self.field)
        verboseName = fieldObject.verbose_name
        if isinstance(fieldObject, ForeignKey) or isinstance(fieldObject, ManyToManyField):
            condition = self.getCondition(request=request, *args, **kwargs)
            return SearchGroupRow(header=verboseName,
                                  fieldData=fieldObject.remote_field.model.objects.filter(**condition),
                                  option=self, queryDict=request.GET)
        else:
            self.isChoice = True
            return SearchGroupRow(header=verboseName, fieldData=fieldObject.choices, option=self, queryDict=request.GET)

    def getText(self, fieldObject):
        """获取选项文本"""
        if self.text:
            return self.text(fieldObject)
        if self.isChoice:
            return fieldObject[1]
        return str(fieldObject)

    def getValue(self, fieldObject):
        """获取选项值"""
        if self.value:
            return self.value(fieldObject)
        if self.isChoice:
            return fieldObject[0]
        return fieldObject.pk

SearchGroupRow 其实就是每一个组合搜索行,只不过我们也给封装成一个类:

class SearchGroupRow:
    def __init__(self, header, fieldData, option, queryDict):
        """
        :param header:组合搜索列名
        :param fieldData: 关联数据
        :param option: 配置
        :param queryDict: request.GET
        """
        self.title, self.fieldData, self.option, self.queryDict = header, fieldData, option, queryDict

    def __iter__(self):
        yield '<span class="col-md-1" style="font-size: 17px;">' + self.title + '</span>'
        yield '<div class="col-md-11 choice" style="display: inline-block; margin-bottom: 20px;">'
        # 1.request.GET 拷贝一份并将其设置为可修改类型
        totalQueryDict = self.queryDict.copy()
        totalQueryDict._mutable = True
        # 2.获取当前的总搜索条件
        originValueList = self.queryDict.getlist(self.option.field)
        if not originValueList:
            yield "<a class='active btn btn-info' href='?%s'>全部</a>" % totalQueryDict.urlencode()
        else:
            totalQueryDict.pop(self.option.field)
            yield '<a class="btn btn-default" href="?%s">全部</a>' % totalQueryDict.urlencode()
        # 3.遍历所有支持组合搜索的字段
        for item in self.fieldData:
            # 3.1、获取按钮显示文本
            text = self.option.getText(item)
            # 3.2、获取按钮值
            value = str(self.option.getValue(item))
            queryDict = self.queryDict.copy()
            queryDict._mutable = True
            if self.option.isMulti:
                # 3.3、如果当前选项支持多选
                multiValueList = queryDict.getlist(self.option.field)
                if value in multiValueList:
                    multiValueList.remove(value)
                    queryDict.setlist(self.option.field, multiValueList)
                    yield "<a class='active btn btn-info' href='?%s'>%s</a>" % (queryDict.urlencode(), text)
                else:
                    multiValueList.append(value)
                    queryDict.setlist(self.option.field, multiValueList)
                    yield "<a class='btn btn-default' href='?%s'>%s</a>" % (queryDict.urlencode(), text)
            else:
                # 3.4、如果当前选项不支持多选
                queryDict[self.option.field] = value
                if value in originValueList:
                    queryDict.pop(self.option.field)
                    yield '<a class="active btn btn-info"  href="?%s">%s</a>' % (queryDict.urlencode(), text)
                else:
                    yield '<a class="btn btn-default" href="?%s">%s</a>' % (queryDict.urlencode(), text)
        yield '</div>'

__iter__ 方法可以通过 yield 将属性转换为可遍历的类型。

这样在 checkView.html 中就可以遍历这个类,其实就是一个 HTML 代码:

{% if searchGroupRowList %}
            <div class="panel panel-default">
                <div class="panel-heading">
                    <i class="fa fa-filter" aria-hidden="true"></i>
                    <span>快速筛选</span>
                </div>
                <div class="panel-body">
                    <div class="search-group">
                        {% for row in searchGroupRowList %}
                            <div class="row">
                                {% for obj in row %}
                                    {{ obj|safe }}
                                {% endfor %}
                            </div>
                        {% endfor %}
                    </div>
                </div>
            </div>
        {% endif %}

这样就可以实现组合搜索的功能了:
在这里插入图片描述

四、批量操作

假如出现了一批不和我心意的人,数量巨大,这时候我们要是一个一个的删除费时费力,因此,我们要为 Stark 组件开发一个批量操作的功能。

对于批量操作,哦们首先要有一个多选框:

    def getCheckbox(self, obj=None, isHeader=None, *args, **kwargs):
        """多选框"""
        return "选择" if isHeader else mark_safe('<input type="checkbox" name="pk" value="%s" />' % obj.pk)

把它加到 displayList 中就可以在页面中显示一个选择框。

我们先写一个简单的批量删除的功能:

    def getActionList(self):
        """获取批量操作列表"""
        return self.actionList

    def actionMultiDelete(self, request, *args, **kwargs):
        """批量删除"""
        pkList = request.POST.getlist("pk")
        self.model.objects.filter(id__in=pkList).delete()

如果想添加其它的批量操作功能可以在子类中编写相应的方法去处理:

from Stark.main import StarkHandler, getM2M, getDatetime, getChoice, SearchGroupOption


class RbacUserHandler(StarkHandler):
	def __init__(self, site, modelClass, prefix):
		super().__init__(site, modelClass, prefix)
		self.displayList = [StarkHandler.getCheckbox, "username", "email", "score", getChoice("等级", "grade"), getM2M("职务", "roles"), "team",
		                    "department", getDatetime("加入时间", "dateJoined")]
		self.searchList = ["username"]
		StarkHandler.actionMultiDelete.text = "批量删除成员"
		self.actionList = [StarkHandler.actionMultiDelete]
		self.searchGroup = [
			SearchGroupOption(field="team"),
			SearchGroupOption(field="department"),
			SearchGroupOption(field="grade", isMulti=True),
		]

这里我们就不搞了,写一个通用的就 OK。

在我们返回页面的时候,如果先获取数据后执行批量操作的话,页面显示的数据还是原来的,因此我们需要把批量操作放在获取数据库数据之前:

    def checkView(self, request, *args, **kwargs):
        """查看功能视图函数"""
        # --------------------- 6.批量操作 ---------------------
        actionList = self.getActionList()
        actionDict = {func.__name__: func.text for  func in actionList}
        if request.method == "POST":
            actionFuncName = request.POST.get("action")
            if actionFuncName and actionFuncName in actionDict:
                actionResponse = getattr(self, actionFuncName)(request, *args, **kwargs)
                if actionResponse:
                    return HttpResponse(actionResponse)

        dataQuerySet = self.model.objects.all()

这样,一个批量删除的功能就完成了:
在这里插入图片描述

添加了分页、排序、关键字搜索、组合搜索和批量操作之后的 Stark 组件已经基本完善了,接下里就是针对每一个数据表做个性化处理。

发布了726 篇原创文章 · 获赞 402 · 访问量 31万+

猜你喜欢

转载自blog.csdn.net/weixin_43336281/article/details/105310464