Django工程的分层结构
前言
传统上我们都知道在Django中的MTV模式,具体内容含义我们再来回顾一下:
M:是Model的简称,它的目标就是通过定义模型来处理和数据库进行交互,有了这一层或者这种类型的对象,我们就可以通过对象来操作数据。
V:是View的简称,它的工作很少,就是接受用户请求换句话说就是通过HTTP请求接受用户的输入;另外把输入信息发送给处理程并获取结果;最后把结果发送给用户,当然最后这一步还可以使用模板来修饰数据。
T:是Template的简称,这里主要是通过标记语言来定义页面,另外还可以嵌入模板语言让引擎来渲染动态数据。
这时候我们看到网上大多数的列子包括有些视频课程里面只讲MVT以及语法和其他功能实现等,但大家有没有想过一个问题,你的业务逻辑放在哪里?课程中的逻辑通常放在了View里面,就像下面:
# urls.py
path('hello/', Hello),
path('helloworld/', HelloWorld.as_view())
# View
from django.views import View
# FVB
def Hello(request):
if request.method == "GET":
return HttpResponse("Hello world")
# CVB
class HelloWorld(View):
def get(self, request):
pass
def post(self, request):
pass
无论是FBV还是CBV,当用户请求进来并通过URL路由找到对应的方法或者类,然后对请求进行处理,比如可以直接返回模型数据、验证用户输入或者校验用户名和密码等。在学习阶段或者功能非常简单的时候使用这种写法没问题,但是对于相对大一点的项目来说你很多具体的处理流程开始出现,而这些东西都写到View里显然你自己都看不下去。
FBV全名Function-based views,基于函数的视图;CBV全名Class-based views,基于类的视图
所以View,它就是一个控制器,它不应该包含业务逻辑,事实上它应该是一个很薄的层。
业务逻辑到底放哪里
网上也有很多文章回答了这个问题,提到了Form层,这个其实是用于验证用户输入数据的格式,比如邮件地址是否正确、是否填写了用户名和密码,至于这个用户名或者邮箱到底在数据库中是否真实存在则不是它应该关心的,它只是一个数据格式验证器。所以业务逻辑到底放哪里呢?显然要引入另外一层。
关于这一层的名称有些人叫做UseCase,也有些人叫做Service,至于什么名字无所谓只要是大家一看就明白的名称就好。如果我们使用UseCase这个名字,那么我们的Djaong工程架构就变成了MUVT,如果是Service那么就MSVT。
这一层的目标是什么呢?它专注于具体业务逻辑,也就是不同用例的具体操作,比如用户注册、登陆和注销都一个用例。所有模型都只是工作流程的一部分并且这一层也知道模型有哪些API。这么说有些空洞,我们用一个例子来说明:
场景是用户注册:
信息填写规范且用户不存在则注册成功并发送账户激活邮件
如果用户已存在则程序引发错误,然后传递到上层并进行告知用户名已被占用
Django 2.2.1、Python 3.7
下图是整个工程的结构
Models层
models.py
from django.db import models
from django.utils.translation import gettext as _
# Create your models here.
from django.contrib.auth.models import AbstractUser, UserManager, User
class UserAccountManager(UserManager):
# 管理器
def find_by_username(self, username):
queryset = self.get_queryset()
return queryset.filter(username=username)
class UserAccount(AbstractUser):
# 扩展一个字段,家庭住址
home_address = models.CharField(_('home address'), max_length=150, blank=True)
# 账户是否被激活,与users表里默认的is_active不是一回事
is_activated = models.BooleanField(_('activatition'), default=False, help_text=_('新账户注册后是否通过邮件验证激活。'),)
# 指定该模型的manager类
objects = UserAccountManager()
我们知道Django会为我们自动建立一个叫做auth_user的表,也就是它自己的认证内容,这个user表本身就是一个模型,它就是继承了AbstractUser类,而这个类有继承了AbstractBaseUser,而这个类继承了models.Model,所以我们这里就是一个模型。再说回AbstractUser类,这个类里面定义了一些username、first_name、email、is_active等用户属性相关的字段,如果你觉得不够用还可以自己扩展。
为了让Django使用我们扩展的用户模型,所以需要在settings.py中添加如下内容:
AUTH_USER_MODEL = "users.UserAccount"
工具类
这个文件主要是放一些通用工具,比如发送邮件这种公共会调用的功能,utils.py内容如下:
from django.core.mail import send_mail
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils import six
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from mysite import settings
class TokenGenerator(PasswordResetTokenGenerator):
def __init__(self):
super(TokenGenerator, self).__init__()
# def _make_hash_value(self, user, timestamp):
# return (
# six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active)
# )
class WelcomeEmail:
subject = 'Activate Your Account'
@classmethod
def send_to(cls, request, user_account):
try:
current_site = get_current_site(request)
account_activation_token = TokenGenerator()
message = render_to_string('activate_account.html', {
'username': user_account.username,
'domain': current_site.domain,
'uid': urlsafe_base64_encode(force_bytes(user_account.id)),
'token': account_activation_token.make_token(user_account),
})
send_mail(
subject=cls.subject,
message=message,
from_email=settings.EMAIL_HOST_USER,
recipient_list=[user_account.email]
)
except Exception as err:
print(err)
TokenGenerator这个东西使用还是它父类本身的功能,之所以这样做是为了在必要的时候可以重写一些功能。父类PasswordResetTokenGenerator的功能主要是根据用户主键来生成token,之后还会根据传递的token和用户主键去检查传递的token是否一致。
针对邮件发送我这里使用Django提供的封装,你需要在settings.py中添加如下内容:
# 邮件设置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = True
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = '' # 发件人邮箱地址
EMAIL_HOST_PASSWORD = '' # 发件人邮箱密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
Services层
这层主要是根据用例来实现业务逻辑,比如注册用户账号和激活用户账号。
"""
Service层,针对不同用例实现的业务逻辑代码
"""
from django.utils.translation import gettext as _
from django.shortcuts import render
from .utils import (
WelcomeEmail,
TokenGenerator,
)
from users.models import (
UserAccount
)
class UsernameAlreadyExistError(Exception):
pass
class UserIdIsNotExistError(Exception):
"""
用户ID,主键不存在
"""
pass
class ActivatitionTokenError(Exception):
pass
class RegisterUserAccount:
def __init__(self, request, username, password, confirm_password, email):
self._username = username
self._password = password
self._email = email
self._request = request
def valid_data(self):
"""
检查用户名是否已经被注册
:return:
"""
user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
if user_query_set:
error_msg = ('用户名 {} 已被注册,请更换。'.format(self._username))
raise UsernameAlreadyExistError(_(error_msg))
return True
def _send_welcome_email_to(self, user_account):
"""
注册成功后发送电子邮件
:param user_account:
:return:
"""
WelcomeEmail.send_to(self._request, user_account)
def execute(self):
self.valid_data()
user_account = self._factory_user_account()
self._send_welcome_email_to(user_account)
return user_account
def _factory_user_account(self):
"""
这里是创建用户
:return:
"""
# 这样创建需要调用save()
# ua = UserAccount(username=self._username, password=self._password, email=self._email)
# ua.save()
# return ua
# 直接通过create_user则不需要调用save()
return UserAccount.objects.create_user(
self._username,
self._email,
self._password,
)
class ActivateUserAccount:
def __init__(self, uid, token):
self._uid = uid
self._token = token
def _account_valid(self):
"""
验证用户是否存在
:return: 模型对象或者None
"""
return UserAccount.objects.all().get(id=self._uid)
def execute(self):
# 查询是否有用户
user_account = self._account_valid()
account_activation_token = TokenGenerator()
if user_account is None:
error_msg = ('激活用户失败,提供的用户标识 {} 不正确,无此用户。'.format(self._uid))
raise UserIdIsNotExistError(_(error_msg))
if not account_activation_token.check_token(user_account, self._token):
error_msg = ('激活用户失败,提供的Token {} 不正确。'.format(self._token))
raise ActivatitionTokenError(_(error_msg))
user_account.is_activated = True
user_account.save()
return True
这里定义的异常类比如UsernameAlreadyExistError等里面的内容就是空的,目的是raise异常到自定义的异常中,这样调用方通过try就可以捕获,有些时候代码执行的结果影响调用方后续的处理,通常大家可能认为需要通过返回值来判断,比如True或者False,但通常这不是一个好办法或者说在有些时候不是,因为那样会造成代码冗长,比如下面的代码:
这是上面代码中的一部分,
def valid_data(self):
"""
检查用户名是否已经被注册
:return:
"""
user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
if user_query_set:
error_msg = ('用户名 {} 已被注册,请更换。'.format(self._username))
raise UsernameAlreadyExistError(_(error_msg))
return True
def execute(self):
self.valid_data()
user_account = self._factory_user_account()
self._send_welcome_email_to(user_account)
return user_account
execute函数会执行valid_data()函数,如果执行成功我才会向下执行,可是你看我在execute函数中并没有这样的语句,比如:
def execute(self):
if self.valid_data():
user_account = self._factory_user_account()
self._send_welcome_email_to(user_account)
return user_account
else:
pass
换句话说你的每个函数都可能有返回值,如果每一个你都这样写代码就太啰嗦了。其实你可以看到在valid_data函数中我的确返回了True,但是我希望你也应该注意,如果用户存在的话我并没有返回False,而是raise一个异常,这样这个异常就会被调用方获取而且还能获取错误信息,这种方式将是一个很好的处理方式,具体你可以通过views.py中看到。
Forms表单验证
这里是对于用户输入做检查
"""
表单验证功能
"""
from django import forms
from django.utils.translation import gettext as _
class RegisterAccountForm(forms.Form):
username = forms.CharField(max_length=50, required=True, error_messages={
'max_length': '用户名不能超过50个字符',
'required': '用户名不能为空',
})
email = forms.EmailField(required=True)
password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())
confirm_password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())
def clean_confirm_password(self) -> str: # -> str 表示的含义是函数返回值类型是str,在打印函数annotation的时候回显示。
"""
clean_XXXX XXXX是字段名
比如这个方法是判断两次密码是否一致,密码框输入的密码就算符合规则但是也不代表两个密码一致,所以需要自己来进行检测
:return:
"""
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if confirm_password != password:
raise forms.ValidationError(message='Password and confirmation do not match each other')
return confirm_password
前端可以实现输入验证,但是也很容易被跳过,所以后端肯定也需要进行操作,当然我这里并没有做预防XSS攻击的措施,因为这个不是我们今天要讨论的主要内容。
Views
from django.shortcuts import render, HttpResponse, HttpResponseRedirect
from rest_framework.views import APIView
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from .forms import (
RegisterAccountForm,
)
from .services import (
RegisterUserAccount,
UsernameAlreadyExistError,
ActivateUserAccount,
ActivatitionTokenError,
UserIdIsNotExistError,
)
# Create your views here.
class Register(APIView):
def get(self, request):
return render(request, 'register.html')
def post(self, request):
# print("request.data 的内容: ", request.data)
# print("request.POST 的内容: ", request.POST)
# 针对数据输入做检查,是否符合规则
ra_form = RegisterAccountForm(request.POST)
if ra_form.is_valid():
# print("验证过的数据:", ra_form.cleaned_data)
rua = RegisterUserAccount(request=request, **ra_form.cleaned_data)
try:
rua.execute()
except UsernameAlreadyExistError as err:
# 这里就是捕获自定义异常,并给form对象添加一个错误信息,并通过模板渲染然后返回前端页面
ra_form.add_error('username', str(err))
return render(request, 'register.html', {'info': ra_form.errors})
return HttpResponse('We have sent you an email, please confirm your email address to complete registration')
# return HttpResponseRedirect("/account/login/")
else:
return render(request, 'register.html', {'info': ra_form.errors})
class Login(APIView):
def get(self, request):
return render(request, 'login.html')
def post(self, request):
print("request.data 的内容: ", request.data)
print("request.POST 的内容: ", request.POST)
pass
class ActivateAccount(APIView):
# 用户激活账户
def get(self, request, uidb64, token):
try:
# 获取URL中的用户ID
uid = force_bytes(urlsafe_base64_decode(uidb64))
# 激活用户
aua = ActivateUserAccount(uid, token)
aua.execute()
return render(request, 'login.html')
except(ActivatitionTokenError, UserIdIsNotExistError) as err:
return HttpResponse('Activation is failed.')
这里就是视图层不同URL由不同的类来处理,这里只做基本的接收输入和返回输出功能,至于接收到的输入该如何处理则有其他组件来完成,针对输入格式规范则由forms中的类来处理,针对数据验证过后的具体业务逻辑则由services中的类来处理。
Urls
from django.urls import path, re_path, include
from .views import (
Register,
Login,
ActivateAccount,
)
app_name = 'users'
urlpatterns = [
re_path(r'^register/$', Register.as_view(), name='register'),
re_path(r'^login/$', Login.as_view(), name='login'),
re_path(r'^activate/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
ActivateAccount.as_view(), name='activate'),
]
Templates
是我用到的html模板,我就不放在这里了,大家可以去这里https://files.cnblogs.com/files/rexcheny/mysite.zip
下载全部的代码
页面效果
激活邮件内容
点击后就会跳转到登陆页。下面我们从Django admin中查看,2个用户是激活状态的。
Django工程的分层结构的更多相关文章
- Django工程目录结构优化
1.我看到这篇文章,写的不错,在此复制了一份,防止以后找不到! 感谢作者的翻译--->原文的链接:http://www.loonapp.com/blog/11/ 如果原文存在,请打开原文件阅读 ...
- Django工程的建立以及小网站的编写
这篇博文会详细的介绍如何创建django工程,介绍我如何做了第一个网站.本文基于windows7安装了python2.7.12,Django1.8.18(LTS)版.采用的IDE为pycharm.建议 ...
- Django系列:(1)PyCharm下创建并运行我们的第一个Django工程
准备工作: 假设读者已经安装好python 2x或3x,以及安装好Django,以及Pycharm. 我的配置: – Python 2.7.11 – Pycharm Professional 5.0. ...
- Eclipse中一个Maven工程的目录结构
在之前的javaSE开发中,没有很关注Eclipse工程目录下的环境,总是看见一个src就点进去新建一个包再写一个class.以后的日子中也没有机会注意到一个工程到底是怎么组织的这种问题,跟不要说自己 ...
- Design3:使用HierarchyID构建数据的分层结构
1,传统的分层结构是父子结构,表结构中有一个ParentID字段自引用表的主键,表示“归属”关系,例如 create table dbo.emph ( ID int not null primary ...
- CEF3开发者系列之工程和代码结构
CEF支持一系列的编程语言和操作系统,并且能很容易地整合到新的或已有的工程中去.它的设计思想就是易用且兼顾性能. CEF3支持一系列的编程语言和操作系统,并且能很容易地整合到新的或已有的工程中去.它的 ...
- H264的句法和语法总结(一)分层结构
在H.264 中,句法元素共被组织成 序列.图像.片.宏块.子宏块五个层次.在这样的结构中,每一层的头部和它的数据部分形成管理与被管理的强依赖关系,头部的句法元素是该层数据的核心,而一旦头部丢失,数 ...
- 软件包 java.util 的分层结构
概述 软件包 类 使用 树 已过时 索引 帮助 JavaTM Platform Standard Ed. 6 上一个 下一个 框架 无框架 所有类 ...
- Django工程读取mongodb并使用分页器
pycharm开发django工程(二) 项目需求: 1. 从mongodb中读取数据,并显示到网页中 2. 在网页显示的每一页加入分页符 首先使用pycharm的企业版新建一个django的虚拟工程 ...
随机推荐
- SQL Server 数据库所有表增加同一列
SET @COLUMN_NAME = 'ColumnNameYouWantToAdd'; SET @COLUMN_DATATYPE = 'DataTypeOfColumn'; ------------ ...
- Android View 滚动边界的测量
最近一直在用Android TV的RecyclerView,实现视频搜索列表卡片的滚动显示,由于采用了双排滚动,打破了系统默认的单排滚动,且每一屏幕显示10个完整卡片5个半漏边卡片,每个完整卡片的左下 ...
- 从wireshark中学网络分析(一)
http://blog.csdn.net/nk_test/article/details/56509688 http://blog.csdn.net/nk_test/article/details/5 ...
- 认识Docker
以下是个人学习过程中所记,仅作为学习经历和备忘,有问题不负责,但可以交流和探讨. 1 什么是Docker? 在Docker的官网,Docker的设计师们对Docker的定义是: Docke ...
- Qt常见皮肤qss代码(有Metro的风格)
##QTabWidget 淡蓝色效果TabWidget(属性值lightblue) QTabWidget[lightblue = "true"] QTabBar::tab{ bor ...
- 使用BCP批量导入数据
本文原创,转载请标明出处 BCP 工具的使用 The bulk copy program utility (bcp) bulk copies data between an instance of M ...
- 为什么有如此多的C++测试框架 - from Google Testing Blog
Why Are There So Many C++ Testing Frameworks? by Zhanyong Wan (Software Engineer) 最近貌似有很多人正在开发他们自己的C ...
- java.lang.ClassNotFoundException: org.jaxen.JaxenException 解决方法
当遇到如下exception时, May 11, 2017 4:23:17 PM org.apache.catalina.core.StandardWrapperValve invoke SEVERE ...
- ES 20 - 查询Elasticsearch中的数据 (基于DSL查询, 包括查询校验match + bool + term)
目录 1 什么是DSL 2 DSL校验 - 定位不合法的查询语句 3 match query的使用 3.1 简单功能示例 3.1.1 查询所有文档 3.1.2 查询满足一定条件的文档 3.1.3 分页 ...
- 浅谈Java中的命名规范
现代软件架构的复杂性需要协同开发完成,如何高效地协同呢? 答案是:制定一整套统一的规范. 无规矩不成方圆,无规范难以协同,比如,制订交通法规表面上是要限制行车权,实际上是保障公众的人身安全,试想如果没 ...