前言

传统上我们都知道在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。这么说有些空洞,我们用一个例子来说明:

场景是用户注册:

  1. 信息填写规范且用户不存在则注册成功并发送账户激活邮件

  2. 如果用户已存在则程序引发错误,然后传递到上层并进行告知用户名已被占用

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工程的分层结构的更多相关文章

  1. Django工程目录结构优化

    1.我看到这篇文章,写的不错,在此复制了一份,防止以后找不到! 感谢作者的翻译--->原文的链接:http://www.loonapp.com/blog/11/ 如果原文存在,请打开原文件阅读 ...

  2. Django工程的建立以及小网站的编写

    这篇博文会详细的介绍如何创建django工程,介绍我如何做了第一个网站.本文基于windows7安装了python2.7.12,Django1.8.18(LTS)版.采用的IDE为pycharm.建议 ...

  3. Django系列:(1)PyCharm下创建并运行我们的第一个Django工程

    准备工作: 假设读者已经安装好python 2x或3x,以及安装好Django,以及Pycharm. 我的配置: – Python 2.7.11 – Pycharm Professional 5.0. ...

  4. Eclipse中一个Maven工程的目录结构

    在之前的javaSE开发中,没有很关注Eclipse工程目录下的环境,总是看见一个src就点进去新建一个包再写一个class.以后的日子中也没有机会注意到一个工程到底是怎么组织的这种问题,跟不要说自己 ...

  5. Design3:使用HierarchyID构建数据的分层结构

    1,传统的分层结构是父子结构,表结构中有一个ParentID字段自引用表的主键,表示“归属”关系,例如 create table dbo.emph ( ID int not null primary ...

  6. CEF3开发者系列之工程和代码结构

    CEF支持一系列的编程语言和操作系统,并且能很容易地整合到新的或已有的工程中去.它的设计思想就是易用且兼顾性能. CEF3支持一系列的编程语言和操作系统,并且能很容易地整合到新的或已有的工程中去.它的 ...

  7. H264的句法和语法总结(一)分层结构

    在H.264 中,句法元素共被组织成  序列.图像.片.宏块.子宏块五个层次.在这样的结构中,每一层的头部和它的数据部分形成管理与被管理的强依赖关系,头部的句法元素是该层数据的核心,而一旦头部丢失,数 ...

  8. 软件包 java.util 的分层结构

    概述  软件包  类  使用   树  已过时  索引  帮助  JavaTM Platform Standard Ed. 6  上一个   下一个 框架    无框架    所有类         ...

  9. Django工程读取mongodb并使用分页器

    pycharm开发django工程(二) 项目需求: 1. 从mongodb中读取数据,并显示到网页中 2. 在网页显示的每一页加入分页符 首先使用pycharm的企业版新建一个django的虚拟工程 ...

随机推荐

  1. BGP的一网双平面规划

    网络拓扑: XRV1 ===================================================================== # sysname XRV1# boa ...

  2. WinForm 清空界面控件值的小技巧

    原文:WinForm 清空界面控件值的小技巧 在WinForm里面有时候需要清空自己输入内容或是选择的选项,以便重新操作流程,那么一般你是怎么清空界面各个控件值的呢?如果窗体里面控件,尤其是TextB ...

  3. C# Lambda表达式Contains方法 like

    原文:Lambda表达式Contains方法 like 1.使用Contains方法的必备条件: Contains等价于SQL中的like语句.不过Contains只针对于字符串(string)类型的 ...

  4. 微信小程序把玩(二十四)toast组件

    原文:微信小程序把玩(二十四)toast组件 toast消息提示框,可用在提示一些信息,比如清楚缓存给用户一个友好的提示!或操作一些请求不想让用户有什么操作,toast也可以做到因为toast显示时其 ...

  5. 基于VUE实现的新闻后台管理系统-二

    基础环境及最后的开发效果已完成说明,接下来就开始配置. ¶npm初始化 新建项目文件夹VueDemo,在其内执行如下脚本 npm init -y 安装vue-cli构建包 yarn add vue-c ...

  6. [转]深入Android内存泄露

    深入内存泄露 Android应用的内存泄露,其实就是java虚拟机的堆内存泄漏. 当然,当应用有ndk,jni时,没有及时free,本地堆也会出现内存泄漏. 本文只是针对JVM内存泄漏应用,进行阐述分 ...

  7. NFS服务设置

    1.安装NFS服务sudo apt-get install nfs-common nfs-kernel-server 2.配置NFS服务首先需要手动编辑/etc/exports配置文件 权限参数说明如 ...

  8. Ring3下无驱动移除winlogon.exe进程ctrl+alt+del,win+u,win+l三个系统热键,非屏蔽热键(子类化SAS 窗口)

    随手而作,纯粹技术研究,没什么实际意义. 打开xuetr,正常情况下.winlogon.exe注册了三个热键.ctrl+alt+del,win+u,win+l三个. 这三个键用SetWindowsHo ...

  9. 长江存储32层3D NAND今年底准备好,预计2020年赶上世界前沿(有些ppt很精彩)

    集微网消息(文/刘洋)2017年1月14日,首届IC咖啡国际智慧科技产业峰会暨ICTech Summit 2017在上海隆重举行.本次峰会以“匠心独运 卓越创‘芯’”为主题,集结了ICT产业领袖与行业 ...

  10. QtStaticBuildScript(会有这么容易)

    https://github.com/dankrusi/QtStaticBuildScript