Django基础(五)

停更一周,主要学习了用户认证django-allauth库,主要写一下博客网站views.py中的编写,使用一些通用视图类,通过修改其中的方法,或者加一些修饰器来实现目标视图。还有利用bootstrap做一点修改美化。还没有完全完善,仅仅记录一下过去一周的学习进展,理解还不够深刻。

Posted by K4ys0n on December 30, 2019

前言

前面说到建了一个base.html页面的模板,接下来会先根据那个基础模板,创建一系列页面。当然,页面内部暂时先不做具体设计。

然后我们接着models.py编写之后,继续来完成一些View的编写。

完成了Model的设计,我们在views.py中做一些视图函数,去调用这些模型提取数据,处理好然后嵌入到模板Template中, 之后我们再分配路由url给到这些视图函数。

步骤

1. 创建一系列待用页面html

我们将会用到至少以下这些模板:

  • 首页:index.html
  • 我的主页:myblog.html
  • 博客列表页:post_list.html
  • 博客详情页:post_detail.html
  • 草稿箱列表页:post_draft_list.html
  • 已发表博客列表页:post_published_list.html
  • 添加博客页:post_create_form.html
  • 更新博客页:post_update_form.html
  • 类别列表页:category_list.html
  • 类别详情页:category_detail.html
  • 标签列表页:tag_list.html
  • 标签详情页:tag_detail.html
  • 搜索页:post_search.html

在templates文件夹下创建blog文件夹,把base.html放在blog文件夹下,并在blog中创建上述文件,在每个文件中都写入以下代码:

{% extends "blog/base.html" %}
{% block title %}博客{% endblock %}
{% block body %}<p>test</p>{% endblock %}

这样一来,我们就可以在views视图中暂时调用这些html文件了,后续再逐步更新优化相应的视图、路由和模板,这里先用简单的文件代替。

但是,由于我们在使用博客系统的时候,有些页面只能有用户权限才能访问,也就是我们还要在原 有基础上增加用户登录等操作,于是这里插入一点用户认证的知识,用到的库是django-allauth。

在此之前我们先建立好用户模型,我们将借助Django自带的User类来建立用户模型。

2. 建立用户Model

在models.py中添加以下代码:

from django.db import models
from django.utils import timezone
from django.template.defaultfilters import slugify
from django.urls import reverse
from unidecode import unidecode
from django.contrib.auth.models import User
from allauth.account.models import EmailAddress

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    org = models.CharField('组织', max_length=128, blank=True)
    telephone = models.CharField("电话", max_length=50, blank=True)
    last_mod_time = models.DateTimeField('最近更新时间', auto_now=True)

    def __str__(self):
        return "%s的个人信息" % self.user.__str__()

    def account_verified(self):
        if self.user.is_authenticated:
            result = EmailAddress.objects.filter(email=self.user.email)
            if len(result):
                return result[0].verified
        return False

    class Meta:
        verbose_name = '用户个人信息'
        verbose_name_plural = verbose_name

这个类会一对一绑定User类,account_verified()函数用于判断用户邮箱是否已验证。由于User类 只有5个字段如first_name、last_name、email…我们实际上还会有手机啊、公司啊之类的信息, 因此需要对原有User类进行拓展。

同时还要重写用户登录表单。因为django-allauth在用户注册只会创建User对象,不会创建与之关 联的UserProfile对象,而我们需要用户在注册时两个对象必须同时都创建,并存储到数据库中。

3. 添加表单forms.py

我们在blog文件夹下创建一个forms.py文件,用来存放所需的各种表单。 以下是forms.py中的内容:

from django import forms

class ProfileForm(forms.Form):
    first_name = forms.CharField(label='姓氏', max_length=50, required=False)
    last_name = forms.CharField(label='名字', max_length=50, required=False)
    org = forms.CharField(label='组织', max_length=50, required=False)
    telephone = forms.CharField(label='电话', max_length=50, required=False)

class SignupForm(forms.Form):
    def signup(self, request, user):
        user_profile = UserProfile()
        user_profile.user = user
        user.save()
        user_profile.save()

第一个表单是提供用户提交修改信息的表单,第二个表单是新用户注册时,除了Django自动创建User对象外, 我们还要同时创建一个与之相关联的UserProfile对象。

SignupForm表单类将在后面django-allauth库安装完成的时候添加使用, 届时需要在settings.py中增加一个参数来调用这个表单类

完了之后光创建了用户模型类和表单类是没用的,还得有人来帮我们处理用户注册登录和验证的流程,于是 我们用django-admin来做这件事。

4. 添加django-allauth库

在命令行输入以下命令:

pip install django-allauth

安装完成后,找到安装路径(我的是安装在虚拟环境venv中的这里 E:\workspace\python_workspace\venv\Lib\site-packages\allauth\templates\account), 把整个account文件夹复制到myblog/templates/文件夹下。

5. 在settings.py中配置allauth

接着修改settings.py,注册allauth相关app,同时还要设置SITE_ID=1:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',

    # allauth相关app的注册
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.github',   # 用于GitHub第三方平台登录接口
    'allauth.socialaccount.providers.baidu',    # 用于百度第三方平台登录接口
]
SITE_ID = 1

注:我们要注意在之前我们就在settings.py中修改了TEMPLATES的DIRS参数,也就 是templates文件夹的路径。如果没有设置,那么默认allauth会找到安装路径下的account文 件夹中的模板,但是现在我们设置了这个参数,所以前面才会要把account整个文件夹复制过来。

接下来settings.py还有一些邮箱认证的设置:

# allauth设定
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
ACCOUNT_EMAIL_REQUIRED = True
LOGIN_REDIRECT_URL = '/profile/'    # '/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/accounts/login/'

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
)

# 设置邮箱
EMAIL_HOST = 'smtp.qq.com'
EMAIL_PORT = 25   # 三个中的一个:25,465,587
EMAIL_HOST_USER = 'xxxx@xxx.com'   # 你的QQ邮箱
EMAIL_HOST_PASSWORD = 'xxxxxxxxxxx'     # 授权码,在邮箱中可以获得(自行百度)
EMAIL_USE_TLS = True    # 这里必须是 True,否则发送不成功
EMAIL_FROM = 'xxxx@xxx.com'    # 你的QQ邮箱
DEFAULT_FROM_EMAIL = 'xxxx@xxx.com'     # 邮件中注明出处用的,随便显示就行

# 设置allauth使用自定义的注册表单
ACCOUNT_SIGNUP_FORM_CLASS = 'blog.forms.SignupForm'

这里的ACCOUNT_SIGNUP_FORM_CLASS参数就是前面第3点我们说到的:SignupForm表单类添加使用 需要在settings.py中增加一个参数来调用这个表单类。

另外,allauth可配置项如下表:

参数 默认值或可选值 说明
ACCOUNT_AUTHENTICATION_METHOD “username” or “email” or “username_email” 指定要使用的登录方法(用户名、电子邮件地址或两者之一)
ACCOUNT_EMAIL_REQUIRED True 为True时要求登录时一定要输入邮箱
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS 3 邮件确认邮件的截止日期(天数)
ACCOUNT_EMAIL_VERIFICATION “optional” 注册中邮件验证方法:“强制(mandatory)”,“可选(optional)”或“否(none)”之一
ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN 180 邮件发送后的冷却时间(以秒为单位)
ACCOUNT_LOGIN_ATTEMPTS_LIMIT 5 登录尝试失败的次数
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT 300 从上次失败的登录尝试,用户被禁止尝试登录的持续时间
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION False 更改为True,用户一旦确认他们的电子邮件地址,就会自动登录
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE False 更改或设置密码后是否自动退出
ACCOUNT_LOGIN_ON_PASSWORD_RESET False 更改为True,用户将在重置密码后自动登录
ACCOUNT_SESSION_REMEMBER None 控制会话的生命周期,可选项还有:False,True
ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE False 用户注册时是否需要输入邮箱两遍
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE True 用户注册时是否需要用户输入两遍密码
ACCOUNT_USERNAME_BLACKLIST [] 用户不能使用的用户名列表
ACCOUNT_UNIQUE_EMAIL True 加强电子邮件地址的唯一性
ACCOUNT_USERNAME_MIN_LENGTH 1 用户名允许的最小长度的整数
SOCIALACCOUNT_AUTO_SIGNUP True 使用从社会帐户提供者检索的字段(如用户名、邮件)来绕过注册表单
LOGIN_REDIRECT_URL ”/” 设置登录后跳转链接
ACCOUNT_LOGOUT_REDIRECT_URL ”/” 设置退出登录后跳转链接

6. 配置路由urls.py

配置完了之后,网站发生了什么呢?我们可以设置一点视图,来显示我们安装配置allauth之后发生的变化。 首先打开myblog/urls.py设置一下路由:

from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('allauth.urls')),
    path('', include('blog.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

把allauth.urls添加进来,顺便把MEDIA_URL也设置一下,可能以后上传图片什么的会用到。

接着是blog/urls.py,修改如下:

from django.urls import path, re_path
from . import views
app_name = 'blog'
urlpatterns = [
    re_path(r'^profile/$', views.profile, name='profile'),
    re_path(r'^profile/update/$', views.profile_update, name='profile_update'),
]

7. 配置视图views.py

然后打开views.py修改如下:

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
from .models import UserProfile
from .forms import ProfileForm
from django.contrib.auth.decorators import login_required   # 登录装饰器

# 登录后主页
@login_required
def profile(request):
    user = request.user
    return render(request, 'account/profile.html', {'user': user})

# 登录后更新个人信息页
@login_required
def profile_update(request):
    user = request.user
    user_profile = get_object_or_404(UserProfile, user=user)

    if request.method == 'POST':
        form = ProfileForm(request.POST)
        if form.is_valid():
            user.first_name = form.cleaned_data['first_name']
            user.last_name = form.cleaned_data['last_name']
            user.save()

            user_profile.org = form.cleaned_data['org']
            user_profile.telephone = form.cleaned_data['telephone']
            user_profile.save()

            return HttpResponseRedirect(reverse('blog:profile'))
    else:
        default_data = {'first_name': user.first_name, 'last_name': user.last_name,
                        'org': user_profile.org, 'telephone': user_profile.telephone}
        form = ProfileForm(default_data)

    return render(request, 'account/profile_update.html', {'form': form, 'user': user})

login_required是装饰器,经过装饰之后的视图函数都会去验证是否为用户登录状态,不是的话会跳转到登录页。

8. 修改模板(相关html文件)

8.1 修改account/base.html

接着我们来修改一点html模板内容。首先打开templates/account/base.html

<!DOCTYPE html>
<html lang="zh-CN">
{% load staticfiles %}
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block head_title %}{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}">
</head>
<body>
<main>
    <div class="container">
        {% block content %}
        {% endblock %}
    </div>
</main>
<script src="{% static 'bootstrap/js/jquery.min.js' %}"></script>
<script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>"

</body>
</html>
8.2 导入美化bootstrap的库——django-widget-tweaks

这里我们为了稍微美化一下,我们还可以引用django-widget-tweaks库,输入以下命令安装:

pip install django-widget-tweaks

安装后在settings.py中设置:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',

    # allauth相关app的注册
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.github',
    'allauth.socialaccount.providers.baidu',

    # bootstrap的css设置相关app的注册
    'widget_tweaks',            # 多了这一行
]
8.3 新建通用bootstrap模板——bs4_forms.html

然后在模板中可以用{% load widget_tweaks %}导入即可使用,我们主要用到render_field标签, 我们在templates/account/snippets/目录下新建一个通用bootstrap显示表单的html文件,命名bs4_forms.html:

{% load widget_tweaks %}

{% 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 }}

        {% if form.is_bound %}
            {% if field.errors %}
                {% render_field field class="form-control is-invalid" %}
                {% for error in field.errors %}
                    <div class="invalid-feedback">
                        {{ error }}
                    </div>
                {% endfor %}
            {% else %}
                {% render_field field class="form-control is-valid" %}
            {% endif %}
        {% else %}
            {% render_field field class="form-control" %}
        {% endif %}

        {% if field.help_text %}
            <small class="form-text text-muted">{{ field.help_text}}</small>
        {% endif %}
    </div>
{% endfor %}
8.4 修改signup.html

然后我们修改原来的templates/account/signup.html成如下:

{% extends "account/base.html" %}

{% load i18n %}
{% load widget_tweaks %}

{% block head_title %}{% trans "Signup" %}{% endblock %}

{% block content %}
<h1>{% trans "Sign Up" %}</h1>

<p>{% blocktrans %}Already have an account? Then please <a href="{{ login_url }}">sign in</a>.{% endblocktrans %}</p>

<form class="signup" id="signup_form" method="post" action="{% url 'account_signup' %}">
  {% csrf_token %}
  {% include 'account/snippets/bs4_form.html' with form=form %}
  {% if redirect_field_value %}
  <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
  {% endif %}
  <div class="form-group">
    <button type="submit">{% trans "Sign Up" %} &raquo;</button>
  </div>
</form>
{% endblock %}

其中csrf_token是django为用户实现防止跨站请求伪造的功能,在中间件django.middleware.csrf.CsrfViewMiddleware中完成。

登录界面如下图: 登录界面

8.5 新建用户主页和用户信息更新页

我们再在templates/account/目录下新增两个html文件:profile.html和profile_update.html,用来显示登录后界 面和自定义个人信息更新,辅助我们理解allauth用户认证之间的跳转逻辑:

{% extends "account/base.html" %}
{% block content %}
{% if user.is_authenticated %}
<a href="{% url 'blog:profile_update' %}">Update Profile</a> | <a href="{% url 'account_email' %}">Manage Email</a>  | <a href="{% url 'account_change_password' %}">Change Password</a> |
<a href="{% url 'account_logout' %}">Logout</a>
{% endif %}
<p>Welcome, {{ user.username }}.
    {% if not user.profile.account_verified %}
    (Unverified email.)
    {% endif %}
</p>

<h2>My Profile</h2>

<ul>
    <li>First Name: {{ user.first_name }} </li>
    <li>Last Name: {{ user.last_name }} </li>
    <li>Organization: {{ user.profile.org }} </li>
    <li>Telephone: {{ user.profile.telephone }} </li>
</ul>
{% endblock %}

user.is_authenticated用于判断用户是否登录,account_email、account_change_password 和account_logout是allauth自带的路由分发关键词,与urls.py中path()函数中的name参数一致。

if not user.profile.account_verified判断邮箱是否已经验证。

profile_update.html文件如下:

{% extends "account/base.html" %}
{% block content %}
{% if user.is_authenticated %}
<a href="{% url 'blog:profile_update' %}">Update Profile</a> | <a href="{% url 'account_email' %}">Manage Email</a>  | <a href="{% url 'account_change_password' %}">Change Password</a> |
<a href="{% url 'account_logout' %}">Logout</a>
{% endif %}
<h2>Update My Profile</h2>

<div class="form-wrapper">
   <form method="post" action="" enctype="multipart/form-data">
      {% csrf_token %}
      {% for field in form %}
           <div class="fieldWrapper">
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
        {% if field.help_text %}
             <p class="help">{{ field.help_text|safe }}</p>
        {% endif %}
           </div>
        {% endfor %}
      <div class="button-wrapper submit">
         <input type="submit" value="Update" />
      </div>
   </form>
</div>
{% endblock %}
8.6 可尝试访问测试一下

这么一来会发生什么呢?现在我们运行网站的话,可以访问

  • 127.0.0.1:8000/accounts/signup 注册页
  • 127.0.0.1:8000/accounts/login 登录页
  • 127.0.0.1:8000/accounts/logout 退出登录页
  • 127.0.0.1:8000/accounts/password/reset 修改密码页
  • … 我们修改一些路由和跳转逻辑,然后修改一定的模板,就可以实现一个简单的用户注册登录和验证的功能了。

比方说在主页放一个注册按钮,点击之后跳转到accounts/signup链接,注册的时候就会用我们上面设置的邮箱, 去发送邮箱验证,当然没有验证成功也没关系,还是可以登录,登录后跳转到/profile/路由,在profile.html中 去验证,或者更新信息、修改邮箱、修改密码等操作,退出登录后回到登录页面等。

django-allauth已经提供了一些基本页面的模板,如登录页、注册页,但是没有提供登录后的显示页, 因此我们在调用接口时要自己写一个html文件,如本篇博客中的profile.html文件,同时 在settings.py中设置好LOGIN_REDIRECT_URL参数,即配置登录重定向。

到这里为止,一般来说allauth是没什么问题的,主要就是添加一些跳转逻辑,优化网页就可以了。 于是profile和profile_update这两个视图后面可能会删掉不用, 同时从现在开始,登录后重定向到首页,也就是修改settings.py中这一行:

LOGIN_REDIRECT_URL = '/'    # 把原来的'/profile/'换成'/'

9. 再次修改路由blog/urls.py

在blog/urls.py文件中添加可能用到的url分配给对应的视图:

from django.urls import path, re_path
from . import views

app_name = 'blog'

urlpatterns = [
    re_path(r'^profile/$', views.profile, name='profile'),
    re_path(r'^profile/update/$', views.profile_update, name='profile_update'),

    # - 首页(不需要登录(登录后也是这个页面)):index.html
    path('', views.index, name='index'),
    # - 博客列表页:post_list.html
    path('post/list/', views.PostListView.as_view(), name='post_list'),
    # - 博客详情页(不需要登录(登录后也是这个页面)):post_detail.html
    re_path(r'^post/(?P<pk>\d+)/(?P<slug1>[-\w]+)/$', views.PostDetailView.as_view(), name='post_detail'),
    # 我的主页(需要登录):myblog.html
    path('user/', views.myblogView, name='myblog'),
    # - 草稿箱列表页(需要登录):post_draft_list.html
    path('draft/', views.PostDraftListView.as_view(), name='post_draft_list'),
    # - 已发表博客列表页(需要登录):post_published_list.html
    path('admin/', views.PostPublishedListView.as_view(), name='published_post_list'),
    # - 添加博客页(需要登录):post_create_form.html
    re_path(r'^post/create/$', views.PostCreateView.as_view(), name='post_create'),
    # - 更新博客页(需要登录):post_update_form.html
    re_path(r'^post/(?P<pk>\d+)/(?P<slug>[-\w]+)/update/$', views.post_publish, name='post_publish'),
    # - 类别列表页(需要登录):category_list.html
    re_path(r'^category/$', views.CategoryListView.as_view(), name='category_list'),
    # - 类别详情页(需要登录):category_detail.html
    re_path(r'^category/(?P<slug>[-\w]+)/$', views.CategoryDetailView.as_view(), name='category_detail'),
    # - 标签列表页(需要登录):tag_list.html
    re_path(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
    # - 标签详情页(需要登录):tag_detail.html
    re_path(r'^tags/(?P<slug>[-\w]+)/$', views.TagDetailView.as_view(), name='tag_detail'),
    # - 搜索页(不需要登录):post_search.html
    re_path(r'^search/$', views.post_search, name='post_search'),
]

可以看到,一般我们使用path函数即可,re_path函数则可以使用正则规则匹配传入进来的url链接中的参数,用“(?P<参数名>正则规则)”来匹配,

path或者re_path传入的第二个参数是视图函数,如果是通用视图(如DetailView等),则需要调用as_view()方法,否则给函数名就行。

10. 再次修改views.py

前面我们说到了django-allauth,其实就是提供了用户登录注册和验证的操作。现在我们完成了用户注册登录验证这些 之后,可以在views.py中创建一些视图函数来处理博客系统的页面需求了。还是先写点简单就好,后面再更新优化。

发现没有,我们的思路基本就是:模型,路由,视图,表单,模板,模型,路由。。。一直循环。

ok,那么views.py改成啥样呢:

from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponseRedirect, Http404
from django.urls import reverse, reverse_lazy

from .models import UserProfile, Post, Tag, Category
from .forms import ProfileForm, PostForm

from django.contrib.auth.decorators import login_required   # 登录装饰器
from django.utils.decorators import method_decorator    # 函数装饰器转方法装饰器

from django.views.generic import DetailView, ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger    # 分页


# 登录后主页
@login_required
def profile(request):
    user = request.user
    return render(request, 'account/profile.html', {'user': user})


# 登录后更新个人信息页
@login_required
def profile_update(request):
    user = request.user
    user_profile = get_object_or_404(UserProfile, user=user)

    if request.method == 'POST':
        form = ProfileForm(request.POST)
        if form.is_valid():
            user.first_name = form.cleaned_data['first_name']
            user.last_name = form.cleaned_data['last_name']
            user.save()

            user_profile.org = form.cleaned_data['org']
            user_profile.telephone = form.cleaned_data['telephone']
            user_profile.save()

            return HttpResponseRedirect(reverse('blog:profile'))
    else:
        default_data = {'first_name': user.first_name, 'last_name': user.last_name,
                        'org': user_profile.org, 'telephone': user_profile.telephone}
        form = ProfileForm(default_data)

    return render(request, 'account/profile_update.html', {'form': form, 'user': user})


# 首页(不需要登录(登录后也是这个页面)):index.html
def index(request):
    user = request.user

    posts = Post.objects.all()
    paginator = Paginator(posts, 3, 2)
    page = request.GET.get('page')
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        posts = paginator.page(1)
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)
    return render(request, "blog/index.html", {'user': user, 'posts': posts})


# 博客列表页:post_list.html
class PostListView(ListView):
    paginate_by = 10  # 每多少条博客分一页
    template_name = 'blog/post_list.html'

    def get_queryset(self):
        return Post.objects.all().order_by('-published_time')


# 博客详情页(不需要登录(登录后也是这个页面)):post_detail.html
class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'

    def get_object(self, queryset=None):
        obj = super().get_object(queryset=queryset)
        obj.viewed()
        return obj

# 我的主页(需要登录):myblog.html
@login_required()
def myblogView(request):
    post = Post.objects.filter(author=request.user)
    return render(request, 'blog/myblog.html', {'post': post})

# 草稿箱列表页(需要登录):post_draft_list.html
@method_decorator(login_required, name='dispatch')
class PostDraftListView(ListView):
    template_name = 'blog/post_draft_list.html'
    paginate_by = 10

    def get_queryset(self):
        return Post.objects.filter(author=self.request.user).filter(status='draft').order_by('-published_time')


# 已发表博客列表页(需要登录):post_published_list.html
@method_decorator(login_required, name='dispatch')
class PostPublishedListView(ListView):
    template_name = 'blog/post_published_list.html'
    paginate_by = 10

    def get_queryset(self):
        return Post.objects.filter(status='published').order_by('-published_time')

# 添加博客页(需要登录):post_create_form.html
@method_decorator(login_required, name='dispatch')
class PostCreateView(CreateView):
    template_name = 'blog/post_create_form.html'
    model = Post
    form_class = PostForm

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

# 更新博客页(需要登录):post_update_form.html
@method_decorator(login_required, name='dispatch')
class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_update_form.html'

    def get_object(self, queryset=None):
        obj = super().get_object(queryset=queryset)
        if obj.author != self.request.user:
            raise Http404()

# 类别列表页(需要登录):category_list.html
@method_decorator(login_required, name='dispatch')
class CategoryListView(ListView):
    template_name = 'blog/category_list.html'
    model = Category

# 类别详情页(需要登录):category_detail.html
@method_decorator(login_required, name='dispatch')
class CategoryDetailView(DetailView):
    template_name = 'blog/category_detail.html'
    model = Category

# 标签列表页(需要登录):tag_list.html
@method_decorator(login_required, name='dispatch')
class TagListView(ListView):
    template_name = 'blog/tag_list.html'
    model = Tag

# 标签详情页(需要登录):tag_detail.html
@method_decorator(login_required, name='dispatch')
class TagDetailView(DetailView):
    template_name = 'blog/tag_detail.html'
    model = Tag

# 搜索页(不需要登录):post_search.html
@login_required()
def post_search(request):
    return render(request, 'blog/post_search.html')

# 保存的草稿发布出去:
@login_required()
def post_publish(request, pk, slug):
    post = get_object_or_404(Post, pk=pk, author=request.user)
    post.published()
    return redirect(reverse("blog:post_detail", args=[str(pk), slug]))

在原来的基础上增加了上面这些函数和视图类,其中视图类是继承自一些通用视图类,有DetailView, ListView, CreateView, UpdateView, DeleteView。

前面我们说到了如果是函数,如def profile(request),那么只需要在函数定义前 加@login_required()装饰器,但如果是类,我们则需要用@method_decorator(login_required, name=’dispatch’) 来将login_required函数装饰器转化为方法装饰器。

reverse()中第一个参数为软编码路由,args为参数,这个函数用于将软编码路由转化成url链接, 然后丢给相应的视图函数处理。这里实现的功能就是在当前视图函数跳转到另一个视图函数,可以理 解为跳转到目标网页。

这其中还要注意通用视图类的参数,如template_name、model、form_class、paginate_by, 还有一些内置方法,如get_object(self, queryset=None)、get_queryset(self)、form_valid(self, form)等。 这些后面详细修改页面内容的时候再来说。

11. 再次修改forms.py

上面views.py中提到了一个PostForm是新建博客用的表单: 在forms.py中新增表单类:

from django import forms
from .models import UserProfile, Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        exclude = ['author', 'views', 'slug', 'published_time']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'body': forms.Textarea(attrs={'class': 'form-control'}),
            'status': forms.Select(attrs={'class': 'form-control'}),
            'category': forms.Select(attrs={'class': 'form-control'}),
            'tags': forms.CheckboxSelectMultiple(attrs={'class': 'multi-checkbox'}),
        }
  • TextInput是单行输入
  • Textarea是输入区
  • Select是下拉选项,所以需要提前在后台或数据库中添加category可选项
  • CheckboxSelectMultiple是多个勾选项,也需要提前预设(有点蛋疼,后面得想办法优化)

到目前为止,只能在数据库或后台手动添加,后台手动添加的话得先在blog/admin.py中注册:

from django.contrib import admin
from .models import Post, Category, Tag

# Register your models here.
admin.site.register(Post)
admin.site.register(Category)
admin.site.register(Tag)

访问后台127.0.0.1:8000/admin就可以直接增删查改了,不赘述。

12. 修改templates/blog/base.html

配置完成我们可以借鉴bootstrap提供的blog.html和blog.css博客模板来搭建我们的首页,当然 我还没有改好,仅做了一点调整,模板上的很多链接啊按钮啊都没用上。

把 bootstrap-3.3.7/docs/examples/blog/blog.css 这个文件复制到static/css/目录下。

接着我们先修改templates/blog/base.html如下:

<!DOCTYPE html>
<html lang="zh-CN">
{% load staticfiles %}
{% load widget_tweaks %}
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="{% static 'img/favicon.ico' %}">

    <title>{% block title %}{% endblock %}</title>

    <!-- Bootstrap core CSS -->
    <link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">

    <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
    <link href="{% static 'bootstrap/css/ie10-viewport-bug-workaround.css' %}" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="{% static 'css/blog.css' %}" rel="stylesheet">

    <!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
    <!--[if lt IE 9]><script src="{% static 'bootstrap/js/ie8-responsive-file-warning.js' %}"></script><![endif]-->   <!--这两行修改跟上面一样修改一下路径-->
    <script src="{% static 'bootstrap/js/ie-emulation-modes-warning.js' %}"></script>

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>

  <body>
    <div class="blog-masthead">
      <div class="container">
        <nav class="blog-nav">
          {% block head %}{% endblock %}
        </nav>
      </div>
    </div>

    {% block body%}{% endblock %}

    <footer class="blog-footer">
      {% block footer %}{% endblock %}
    </footer>

    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="{% static 'bootstrap/js/jquery.min.js' %}"></script>
    <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>

    <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
    <script src="{% static 'bootstrap/js/ie10-viewport-bug-workaround.js' %}"></script>
  </body>
</html>

其中有一些注释是IE浏览器兼容的,其他都是Django模板风格的代码。

13. 首页index.html

首页没有把blog.html的内容全保留,删了绝大部分后如下:

{% extends "blog/base.html" %}
{% block title %}博客{% endblock %}
{% block body %}
    <div class="blog-masthead">
      <div class="container">
        <nav class="blog-nav">
          <a class="blog-nav-item active" href="{% url 'blog:index' %}">首页</a>
          {% if user.is_authenticated %}
          <a class="blog-nav-item" href="{% url 'blog:myblog' %}">我的主页</a>
          <a class="blog-nav-item" href="/accounts/logout/">退出</a>
          {% else %}
          <a class="blog-nav-item" href="/accounts/signup/">注册</a>
          <a class="blog-nav-item" href="/accounts/login/">登录</a>
          {% endif %}
          <form class="navbar-search pull-right" action="{% url 'blog:post_search'%}">
            <input type="text" class="search-query" placeholder="search">
            <button type="submit" class="btn btn-default">搜索</button>
          </form>
        </nav>
      </div>
    </div>

    <div class="container">
        <ul>
          {% for post in posts %}
          <li>{{ post }}</li>
          {% endfor %}
        </ul>
    </div>
{% endblock %}

{% block footer %}
{% endblock %}

这段代码主要只是实现了当用户没有登录时会有登录和注册按键,当用户登录后,只有退出和我的主页按键。 当然首页按键是一直有的。还提供了的博客文章的显示,但是还很简陋。

14. 我的主页myblog.html

直接贴代码了,不过还没改好,只实现了一点跳转的逻辑,也没有美化。

{% extends "blog/base.html" %}
{% block title %}博客{% endblock %}
{% block body %}
    <div class="blog-masthead">
      <div class="container">
        <nav class="blog-nav">
          <a class="blog-nav-item" href="{% url 'blog:index' %}">首页</a>
          {% if user.is_authenticated %}
          <a class="blog-nav-item active" href="{% url 'blog:myblog' %}">我的主页</a>
          <a class="blog-nav-item" href="/accounts/logout/">退出</a>
          {% else %}
          <a class="blog-nav-item" href="/accounts/signup/">注册</a>
          <a class="blog-nav-item" href="/accounts/login/">登录</a>
          {% endif %}
          <form class="navbar-search pull-right" action="{% url 'blog:post_search'%}">
            <input type="text" class="search-query" placeholder="search">
            <button type="submit" class="btn btn-default">搜索</button>
          </form>
        </nav>
      </div>
    </div>

    <div class="container">
      <a class="button" href="{% url 'blog:post_create' %}">新增博客</a>
      <a class="button" href="{% url 'blog:profile_update' %}">修改个人信息</a>

      {% if page_obj %}
      <table class="table table-striped">
          <thead>
              <tr>
                  <th>标题</th>
                  <th>类别</th>
                  <th>发布日期</th>
                  <th>查看</th>
                  <th>修改</th>
                  <th>删除</th>
              </tr>
          </thead>
          <tbody>
           {% for post in page_obj %}
              <tr>
                  <td>
                  {{ post.title }}
                  </td>
                  <td>
                  {{ post.category.name }}
                  </td>
                  <td>
                  {{ post.published_time | date:"Y-m-d" }}
                  </td>
                   <td>
                       <a href="{% url 'blog:post_detail' post.id post.slug %}"><span class="glyphicon glyphicon-eye-open"></span></a>
                  </td>

                   <td>
                      <a href="{% url 'blog:post_update' post.id post.slug %}"><span class="glyphicon glyphicon-wrench"></span></a>
                  </td>

                   <td>
                      <a href="{% url 'blog:post_delete' post.id post.slug %}"><span class="glyphicon glyphicon-trash"></span></a>
                  </td>
           {% endfor %}
              </tr>
          </tbody>
      </table>

      {% else %}
      {# 注释: 这里可以换成自己的对象 #}
          <p>没有文章。</p>
      {% endif %}

<!--      分页-->
          <nav>
            <ul class="pager">
              <li><a href="#">Previous</a></li>
              <li><a href="#">Next</a></li>
            </ul>
          </nav>
    </div><!-- /.container -->
{% endblock %}

{% block footer %}

{% endblock %}

其实跟首页内容类似,不过多的这个分页功能还没有测试,首页后面也要添加这个分页功能。

15. 新建博客页post_create_form.html

用bootstrap简单美化的新建博客页提交表单,代码如下:

{% extends "blog/base.html" %}
{% block title %}博客{% endblock %}
{% block body %}

<h3>添加新文章</h3>

<form method="POST" class="form-horizontal" role="form" action="" >
  {% csrf_token %}
  {% for hidden_field in form.hidden_fields %}
    {{ hidden_field }}
  {% endfor %}
  {% if form.non_field_errors %}
    <div class="alert alert-danger col-md-12" role="alert">
      {% for error in form.non_field_errors %}
        {{ error }}
      {% endfor %}
    </div>
  {% endif %}

  {% for field in form.visible_fields %}
  <div class="form-group col-md-12">
        {{ field.label_tag }}
        {{ field }}
        {% if field.errors %}
          {% for error in field.errors %}
            <div class="invalid-feedback">
              {{ error }}
            </div>
          {% endfor %}
        {% endif %}
      {% if field.help_text %}
        <small class="form-text text-muted">{{ field.help_text }}</small>
      {% endif %}
    </div>
  {% endfor %}
  <div class="form-group">
     <div class="col-md-12">
    <input type="submit" class="btn btn-primary form-control" value="提交">
     </div>
  </div>
</form>
{% endblock %}

其中涉及到一点表单类的东西,比如form.visible_fields,field.label_tag等,后面再进一步研究。 这里先直接使用。

到目前为止,稍微动过刀的就有base.html、index.html、myblog.html、post_create_form.html。 没有动过刀的还有很多,包括templates/account/文件夹下的模板,还有一些static/下的css、js文件。

补充:Django模板风格

  • 静态文件链接:{% static ‘链接’ %}
  • 软编码路由:{% url ‘链接’ %},这里的’链接’由’app_name:url_name’组成如:’blog:index’
  • 变量:{{ 变量|选择器 }}
  • 注释:{# 注释内容 #}
  • for循环:{% for a in a_list %}html代码{% endfor %}
  • if条件:{% if a.value==’a’ %}html代码{% elseif a.value==’b’ %}html代码{% else %}html代码{% endif %}
  • 块:{% block block_name %}html代码{% endblock %}
  • 加载库或文件夹:{% load widget_tweaks %},{% load staticfiles%}…
  • 继承模板:{% extends “blog/base.html” %},…

后记

这可能是我写的最长的一篇博客了,其实内容很乱,主要是记录过去一周我对博客系统这个项目 的进度,对学习内容的一点思考以及一些学习记录。

到目前为止,稍微动过刀的就有base.html、index.html、myblog.html、post_create_form.html。 没有动过刀的还有很多,包括templates/account/文件夹下的模板,还有一些static/下的css、js文件。

整体实现了用户注册登录验证,简单的views.py视图逻辑,首页主页登录页注册页的一些跳转, 仅提供了按键。很多东西还没有显示出来,还有很多没有考虑到正在构思中的东西,比如点赞、 评论功能、搜索功能、时间分类、推荐算法等。url跳转也比较混乱,没有规划好,在过去一周 中重新构思了很多,原本只是想做一个简单的个人博客,现在引入了用户注册之后想上升为博客 系统或者博客平台。

另外,目前bootstrap不熟,Django很多类的内置方法没有系统地整理出来,我打算接下来把模 型、视图、模板的一些类方法整理一下。

还有慢慢接触一点设计模式,数据库,缓存,消息队列,服务器,序列化,前后端解耦的后 端(Django-Rest-Framework框架)。

参考:

Python Web与Django开发必读

刘江的Django教程