Django实战教程: 开发企业级应用智能文档管理系统smartdoc(1)
学好Django不仅可以开发中大型网站, 还可以开发专业的企业级 网络应用(Web App)。今天小编我就带你用Django开发一个企业级应用,一个名叫smartdoc的网络智能文档管理系统。在阅读本文前,请一定先阅读Django核心基础(3): View视图详解。一旦你使用通用视图,你就会爱上她。这是因为我们视图部分会直接使用Django的通用视图。如果你对代码有疑问,欢迎评论区留言。
项目背景
一个公司有很多产品,每个产品有各种各样的文档(图片,测试报告,证书,宣传单页)。为了便于全国的销售和客户通过网络及时获取产品的最新文件,公司需要开发一个智能文档管理系统方便全国销售人员和客户按关键词搜索各个文档并提供下载功能。只有公司指定用户有编辑产品和修改上传文档的权限,而外地销售人员和客户只有查询,查看和下载的权限。
一般用户最后看到的界面如下所示。
总体思路
我们要开发一个叫smartdoc的app,具体包括以下几个功能性页面。
-
产品列表,产品详情,产品创建和产品修改 - 4个页面
-
类别列表,类别详情,类别创建和类别修改 - 4个页面
-
文档列表,文档详情,文档创建和文件修改 - 4个页面
-
文档一般搜索和Ajax搜索 - 1个页面
其中对象创建和修改权限仅限指定用户。本教程一共分三部分,第1部分搭基础框架(标黄部分),第2部分讲解用户权限控制,第3部分建立文档搜索和Ajax搜索功能。本文是第一部分。
项目开发环境
Django 2.1 + Python 3.5 + SQLite。因为用户上传的文件还包括图片,所以请确保你已通过pip安装python的pillow图片库。
项目配置settings.py
我们通过python manage.py startapp smartdoc创建一个叫smartdoc的APP,把它加到settings.py里INSATLLED_APP里去,如下所示。我们用户注册登录功能交给了django-allauth, 所以把allauth也进去了。如果你不了解django-allauth,请阅读django-allauth教程(1): 安装,用户注册,登录,邮箱验证和密码重置(更新)。
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', 'allauth', 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.baidu', 'smartdoc', ]
因为我们要用到静态文件如css和图片,我们需要在settings.py里设置STATIC_URL和MEDIA。用户上传的图片会放在/media/文件夹里。
STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, "static"), ] # specify media root for user uploaded files, MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/'
整个项目的urls.py如下所示。我们把smartdoc的urls.py也加进去了。别忘了在结尾部分加static配置。
from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('accounts/', include('allauth.urls')), path('smartdoc/', include('smartdoc.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
模型models.py
在本例中我们只需要创建Product, Category和Document 3个模型(如下所示)。因为我们在视图中会应用Django的通用视图,所以我们在每个模型里需要定义get_abosolute_url。Django的CreateView和UpdateView在完成对象的创建或编辑后会自动跳转到这个绝对url。如果对get_absolute_url方法里的reverse方法不理解,请阅读Django基础(10): URL重定向的HttpResponseDirect, redirect和reverse的用法详解
在Product和Category中我们还定义了document_count这个方法,并用@property把它伪装成属性,便于我们统计属于同一产品或同一类别的文档数量。在Document模型里, 我们定义get_format方法用来快速获取文件格式。
from django.db import models from django.contrib.auth.models import User from django.urls import reverse import os import uuid def user_directory_path(instance, filename): ext = filename.split('.')[-1] filename = '{}.{}'.format(uuid.uuid4().hex[:10], ext) sub_folder = 'file' if ext.lower() in ["jpg", "png", "gif"]: sub_folder = "picture" if ext.lower() in ["pdf", "docx", "txt"]: sub_folder = "document" return os.path.join(str(instance.author.id), sub_folder, filename) class AbstractModel(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE) create_date = models.DateField(auto_now_add=True) mod_date = models.DateField(auto_now=True) class Meta: abstract = True class Product(AbstractModel): """产品""" name = models.TextField(max_length=30, verbose_name='Product Name',) code = models.TextField( max_length=30, blank=True, default='', verbose_name='Product Code',) def __str__(self): return self.name def get_absolute_url(self): return reverse('smartdoc:product_detail', args=[str(self.id)]) @property def document_count(self): return Document.objects.filter(product_id=self.id).count() class Meta: ordering = ['-mod_date'] verbose_name = "产品" class Category(AbstractModel): """文档类型""" name = models.CharField(max_length=30, unique=True) def __str__(self): return self.name def get_absolute_url(self): return reverse('smartdoc:category_detail', args=[self.id]) @property def document_count(self): return Document.objects.filter(category_id=self.id).count() class Meta: ordering = ['-mod_date'] verbose_name = "文档分类" class Document(AbstractModel): """文件""" title = models.TextField(max_length=30, verbose_name='Title',) version_no = models.IntegerField(blank=True, default=1, verbose_name='Version No.',) doc_file = models.FileField(upload_to=user_directory_path, blank=True, null=True) product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='documents',) category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='documents', ) def __str__(self): return self.title def get_format(self): return self.doc_file.url.split('.')[-1].upper() def get_absolute_url(self): return reverse('smartdoc:document_detail', args=[str(self.product.id), str(self.id)]) class Meta: ordering = ['-mod_date'] verbose_name = "文档"
为了防止后来上传的文件与已上传的文件重名而发生覆盖,我们动态地定义了文件上传路径user_directory_path,并对上传的文件以uuid形式重命名。更多内容见Django自定义图片和文件上传路径(upload_to)的2种方式。
在模板中,我们可以通过如下方式获取上传文件的格式,大小和链接。
<td> {{ document.get_format }} </td> <td> {{ document.doc_file.size | filesizeformat }} </td>
<td> {{ document.doc_file.url }} </td>
URLConf配置urls.py
每个path都对应一个视图,一个命名的url和我们本文刚开始介绍的一个功能性页面(一共13个urls, 13个页面)。对于每个模型(Product, Category, Document), 我们都要创建列表,详情,创建和修改4个页面。所以对于视图部分,你只需要了解其中一个模型的列表,详情,创建和修改的通用视图是如何工作的,其余的都是重复工作了。Django核心基础(3): View视图详解。一旦你使用通用视图,你就会爱上她。
from django.urls import path, re_path from . import views # namespace app_name = 'smartdoc' urlpatterns = [ # 展示产品列表 path('product/', views.ProductList.as_view(), name='product_list'), # 展示产品详情 re_path(r'^product/(?P<pk>\d+)/$', views.ProductDetail.as_view(), name='product_detail'), # 创建产品 re_path(r'^product/create/$', views.ProductCreate.as_view(), name='product_create'), # 修改产品 re_path(r'^product/(?P<pk>\d+)/update/$', views.ProductUpdate.as_view(), name='product_update'), # 展示类别列表 path('category/', views.CategoryList.as_view(), name='category_list'), # 展示类别详情 re_path(r'^category/(?P<pk>\d+)/$', views.CategoryDetail.as_view(), name='category_detail'), # 创建类别 re_path(r'^category/create/$', views.CategoryCreate.as_view(), name='category_create'), # 修改类别 re_path(r'^category/(?P<pk>\d+)/update/$', views.CategoryUpdate.as_view(), name='category_update'), # 展示文档列表 path('document/', views.DocumentList.as_view(), name='document_list'), # 展示文档详情 re_path(r'^product/(?P<pkr>\d+)/document/(?P<pk>\d+)/$', views.DocumentDetail.as_view(), name='document_detail'), # 创建文档 re_path(r'^product/(?P<pk>\d+)/document/create/$', views.DocumentCreate.as_view(), name='document_create'), # 修改文档 re_path(r'^product/(?P<pkr>\d+)/document/(?P<pk>\d+)/update/$', views.DocumentUpdate.as_view(), name='document_update'), # 文档搜索 path('document/search/', views.document_search, name='document_search'), ]
视图views.py
为了简化开发,我们使用了Django自带的通用视图。我们使用ListView来显示对象列表,使用DetailView来显示对象详情,使用CreateView来创建对象,使用UpdateView来编辑对象。关于文档搜索对应的视图document_search,我们会在下篇教程重点讲解。
# Create your views here. from django.views.generic import DetailView, ListView, UpdateView from django.views.generic.edit import CreateView from .models import Product, Category, Document from .forms import ProductForm, CategoryForm, DocumentForm class ProductList(ListView): model = Product class ProductDetail(DetailView): model = Product class ProductCreate(CreateView): model = Product template_name = 'smartdoc/form.html' form_class = ProductForm # Associate form.instance.user with self.request.user def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) class ProductUpdate(UpdateView): model = Product template_name = 'smartdoc/form.html' form_class = ProductForm class CategoryList(ListView): model = Category class CategoryDetail(DetailView): model = Category class CategoryCreate(CreateView): model = Category template_name = 'smartdoc/form.html' form_class = CategoryForm # Associate form.instance.user with self.request.user def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) class CategoryUpdate(UpdateView): model = Product template_name = 'smartdoc/form.html' form_class = CategoryForm class DocumentList(ListView): model = Document class DocumentDetail(DetailView): model = Document class DocumentCreate(CreateView): model = Document template_name = 'smartdoc/form.html' form_class =DocumentForm # Associate form.instance.user with self.request.user def form_valid(self, form): form.instance.author = self.request.user form.instance.product = Product.objects.get(id=self.kwargs['pk']) return super().form_valid(form) class DocumentUpdate(UpdateView): model = Document template_name = 'smartdoc/form.html' form_class = DocumentForm def document_search(request): pass
表单forms.py
创建和编辑对象时需要用到表单forms.py,我们在表单里定义了需要显示的字段。我们在前端表单里移除了user,所以在视图里采用了form_valid方法添加。我们添加了widget和labels。添加widget的目的时为了定制用户输入控件,并给其添加css样式(因为boostrap表单需要form-control这个样式)。
from django.forms import ModelForm, TextInput, FileInput, Select from .models import Product, Category, Document class ProductForm(ModelForm): class Meta: model = Product exclude = ('author', 'create_date', 'mod_date') widgets = { 'name': TextInput(attrs={'class': 'form-control'}), 'code': TextInput(attrs={'class': 'form-control'}), } labels = { 'name': '产品名称', 'code': '产品代码', } class CategoryForm(ModelForm): class Meta: model = Category exclude = ('author', 'create_date', 'mod_date') widgets = { 'name': TextInput(attrs={'class': 'form-control'}), } labels = { 'name': '类别', } class DocumentForm(ModelForm): class Meta: model = Document exclude = ('author', 'create_date', 'mod_date', 'product') widgets = { 'title': TextInput(attrs={'class': 'form-control'}), 'version_no': TextInput(attrs={'class': 'form-control'}), 'category': Select(attrs={'class': 'form-control'}), 'doc_file': FileInput(attrs={'class': 'form-control'}), } labels = { 'title': '文档标题', 'version_no': '版本号', 'category': '文档类别', 'doc_file': '上传文件', }
模板文件
我们在目录中创建templates/smartdoc/目录,添加如下html模板。
# product_list.html 产品列表
{% extends "smartdoc/base.html" %} {% block content %} <h3> {{ product.name }} - {{ product.code }} - 所有文档 </h3> {% if product.documents.all %} <table class="table table-striped"> <thead> <tr> <th>标题</th> <th>类别</th> <th>格式</th> <th>大小</th> <th>版本</th> <th>修改日期</th> <th>详情</th> <th>下载</th> </tr> </thead> <tbody> {% for document in product.documents.all %} <tr> <td> {{ document.title }} </td> <td> {{ document.category.name }} </td> <td> {{ document.get_format }} </td> <td> {{ document.doc_file.size | filesizeformat }} </td> <td> {{ document.version_no }} </td> <td> {{ document.mod_date | date:"Y-m-d" }} </td> <td> <a href="{% url 'smartdoc:document_detail' document.product.id document.id %}"><span class="glyphicon glyphicon-eye-open"></span></a> </td> <td> <a href="{{ document.doc_file.url }}"><span class="glyphicon glyphicon-download"></span></a> </td> {% endfor %} </tr> </tbody> </table> {% else %} <p>对不起,没有文档。</p> {% endif %} {% if request.user.is_authenticated %} <p> <span class="glyphicon glyphicon-plus"></span> <a href="{% url 'smartdoc:document_create' product.id %}">上传文档</a> | <span class="glyphicon glyphicon-plus"></span> <a href="{% url 'smartdoc:product_create' %}">添加产品</a> {% if request.user == product.author %} | <span class="glyphicon glyphicon-wrench"></span> <a href="{% url 'smartdoc:product_update' product.id %}">编辑产品</a> {% endif %} </p> {% else %} <p>请先<a href="{% url 'account_login' %}?next={% url 'smartdoc:product_detail' product.id %}">登录</a>添加产品,编辑产品或上传文档。</p> {% endif %} <p><a href="{% url 'smartdoc:product_list' %}">查看全部产品</a> | <a href="{% url 'smartdoc:category_list' %}">查看全部类别</a> | <a href="{% url 'smartdoc:document_list' %}">查看全部文档</a> </p> {% endblock %}
对应效果如下所示。
模板里我们通过request.user.is_authenticated来判断用户是否已登录,如果用户已登录,下面会出现添加产品的链接。(这是我们目前仅有的权限控制,显然是不够的:))。
# product_detail.html 产品详情
{% extends "smartdoc/base.html" %} {% block content %} <h3> {{ document.title }} </h3> <ul> <li>产品名称: {{ document.product.name }}</li> <li>产品代码: {{ document.product.code }}</li> <li>文档类别: {{ document.category.name }}</li> <li> 格式大小: {{ document.get_format }} | {{ document.doc_file.size | filesizeformat }} </li> <li> 版本号: {{ document.version_no }}</li> <li> 上传日期: {{ document.create_date | date:"Y-m-d" }}</li> <li> 修改日期: {{ document.mod_date | date:"Y-m-d" }}</li> <li>作者: {{ document.author.username }}</li> </ul> <p><span class="glyphicon glyphicon-download"></span> <a href="{{ document.doc_file.url }}">下载文档</a> {% if request.user.is_authenticated %} {% if request.user == document.author %} | <span class="glyphicon glyphicon-wrench"></span> <a href="{% url 'smartdoc:document_update' document.product.id document.id %}">编辑文档</a> {% endif %} {% else %} <p>请先<a href="{% url 'account_login' %}?next={% url 'smartdoc:document_detail' document.product.id document.id %}">登录</a>再上传或编辑文档。</p> {% endif %} <p><a href="{% url 'smartdoc:product_list' %}">查看全部产品</a> | <a href="{% url 'smartdoc:category_list' %}">查看全部类别</a> | <a href="{% url 'smartdoc:document_list' %}">查看全部文档</a> </p> {% endblock %}
# form.html 产品创建和修改
{% extends "smartdoc/base.html" %} {% block content %} <form action="" method="post" enctype="multipart/form-data" > {% csrf_token %} {% for hidden_field in form.hidden_fields %} {{ hidden_field }} {% endfor %} {% if form.non_field_errors %} <div class="alert alert-danger" role="alert"> {% for error in form.non_field_errors %} {{ error }} {% endfor %} </div> {% endif %} {% for field in form.visible_fields %} <div class="form-group"> {{ field.label_tag }} {{ field }} {% if field.help_text %} <small class="form-text text-muted">{{ field.help_text }}</small> {% endif %} </div> {% endfor %} <button class="btn btn-info form-control " type="submit" value="submit">提交</button> </form> <br /> <p><span class="glyphicon glyphicon-plus"></span> <a href="{% url 'smartdoc:category_create' %}">添加类别</a></p> {% endblock %}
产品创建和资料修改页面,如下所示。
对应Category和Document的模板是非常类似的,在这里就不重复了。等第3部分教程结束时,我会把整个项目代码分享到github,到时大家可以自己去下载。
小结
本文利用Django建立一个基本的企业级应用智能文档管理系统,然而这个应用还有很多问题,比如用户权限的控制太儿戏,仅靠request.user.is_authenticated来进行。另外智能文档搜索功能还没添加,我会在第2和第3部分教程里重点讲解权限控制和实现一般文档搜索和高级Ajax搜索,欢迎关注本公众号。
大江狗
2018.8.18