Django工程的分层结构详解

  • 时间:
  • 浏览:36

媒介

传统上我们皆晓得正在Django中的MTV形式,详细内容寄义我们再去回忆1下:

M:是Model的简称,它的目的便是经由过程界说模子去处置战数据库停止交互,有了那1层或这类范例的工具,我们就能够经由过程工具去操纵数据。

V:是View的简称,它的事情很少,便是承受用户要求换句话道便是经由过程HTTP要求承受用户的输出;别的把输出疑息收收给处置程并获得成果;最初把成果收收给用户,固然最初那1步借可使用模板去润色数据。

T:是Template的简称,那里次要是经由过程标识表记标帜言语去界说页里,别的借能够嵌进模板言语让引擎去衬着静态数据。

这时候候我们看到网上年夜大都的列子包罗有些视频课程内里只讲MVT和语法战其他功用真现等,但各人有无念过1个成绩,您的营业逻辑放正在那里?课程中的逻辑凡是放正在了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路由找到对应的办法或类,然后对要求停止处置,好比能够间接前往模子数据、考证用户输出或校验用户名战稀码等。正在进修阶段或功用十分简朴的时分利用这类写法出成绩,可是对绝对年夜1面的项目来讲您良多详细的处置流程起头呈现,而那些工具皆写到View里明显您本身皆看没有下来。

FBV齐名Function-based views,基于函数的视图;CBV齐名Class-based views,基于类的视图

以是View,它便是1个掌握器,它不该该包括营业逻辑,究竟上它应当是1个很薄的层。

营业逻辑究竟放那里

网上也有良多文章答复了那个成绩,提到了Form层,那个实际上是用于考证用户输出数据的格局,好比邮件地点是不是准确、是不是挖写了用户名战稀码,至于那个用户名或邮箱究竟正在数据库中是不是实在存正在则没有是它应当体贴的,它只是1个数据格局考证器。以是营业逻辑究竟放那里呢?明显要引进别的1层。

闭于那1层的称号有些人叫做UseCase,也有些人叫做Service,至于甚么名字无所谓只需是各人1看便大白的称号便好。若是我们利用UseCase那个名字,那末我们的Djaong工程架构便酿成了MUVT,若是是Service那末便MSVT。

那1层的目的是甚么呢?它专注于详细营业逻辑,也便是差别用例的详细操纵,好比用户注册、登岸战登记皆1个用例。一切模子皆只是事情流程的1部份而且那1层也晓得模子有哪些API。那么道有些浮泛,我们用1个例子来讲明:

场景是用户注册:

  • 疑息挖写标准且用户没有存正在则注册胜利并收收账户激活邮件
  • 若是用户已存正在则法式激发毛病,然后通报到下层并停止见告用户名已被占用

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):
 # 扩大1个字段,家庭住址
 home_address = models.CharField(_('home address'), max_length=150, blank=True)
 # 账户是不是被激活,取users内外默许的is_active没有是1回事
 is_activated = models.BooleanField(_('activatition'), default=False, help_text=_('新账户注册后是不是经由过程邮件考证激活。'),)

 # 指定该模子的manager类
 objects = UserAccountManager()

我们晓得Django会为我们主动成立1个叫做auth_user的表,也便是它本身的认证内容,那个user表自己便是1个模子,它便是担当了AbstractUser类,而那个类有担当了AbstractBaseUser,而那个类担当了models.Model,以是我们那里便是1个模子。再道回AbstractUser类,那个类内里界说了1些username、first_name、email、is_active等用户属性相干的字段,若是您以为不敷用借能够本身扩大。

为了让Django利用我们扩大的用户模子,以是需求正在settings.py中增加以下内容:

AUTH_USER_MODEL = "users.UserAccount"

东西类

那个文件次要是放1些通用东西,好比收收邮件这类大众会挪用的功用,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那个工具利用仍是它女类自己的功用,之以是如许做是为了正在需要的时分能够重写1些功用。女类PasswordResetTokenGenerator的功用次要是按照用户主键去死成token,以后借会按照通报的token战用户主键来查抄通报的token是不是1致。

针对邮件收收我那里利用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,但凡是那没有是1个好法子或道正在有些时分没有是,由于那样会形成代码冗杂,好比上面的代码:

那是下面代码中的1部份,

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,而是raise1个非常,如许那个非常便会被挪用圆获得并且借能获得毛病疑息,这类体例将是1个很好的处置体例,详细您能够经由过程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是字段名
 好比那个办法是判定两次稀码是不是1致,稀码框输出的稀码便算契合划定规矩可是也没有代表两个稀码1致,以是需求本身去停止检测
 :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工具增加1个毛病疑息,并经由过程模板衬着然后前往前端页里
  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⑼A-Za-z_\-]+)/(?P[0⑼A-Za-z]{1,13}-[0⑼A-Za-z]{1,20})/$',
  ActivateAccount.as_view(), name='activate'),
]

Templates

是我用到的html模板,我便没有放正在那里了

下载全数的代码

页里结果

激活邮件内容

面击后便会跳转到登岸页。上面我们从Django admin中检察,2个用户是激活形态的。