美文网首页
Django 开发 MxOnline 项目笔记 -- 第7章 课

Django 开发 MxOnline 项目笔记 -- 第7章 课

作者: 江湖十年 | 来源:发表于2018-03-05 17:45 被阅读284次

一、课程机构列表页

(一)、利用 django templates 的继承机制配置课程机构列表页 org-list.html
  1. 观察页面,提取公共部分,公共 header、footer
  • 分析,可以用 django 的 {% include "test.html" %} 语法来完成,但是有问题,include 进来的页面是全部写死的,比如“登录”按钮,如果用户登录成功是要换成用户名的,但 include 过来的页面是没法自动完成替换的,所以,include 不适合导入动态变化的页面,只适用于静态页面部分。
  • 为了解决上面的这个问题,就要用到 django 提供的另一中模板机制--继承,{% extends "base.html" %}。类似于 python 中的“类”,可以先定义一个整体的框架,然后把页面中动态的部分定义为一个一个的 {% block %},子类继承 "base.html" 的时候只需要重写 {% block %} 部分。
001.png
  • 页面中的公共部分


    001(1).png
  • step1:在 templates/ 目录下 新建 base.html 文件,将 org-list.html 页面页放到此目录下。


    002.png
  • step2:复制 org-list.html 所有内容到 base.html,替换静态文件,定义 {% block %},在 org-list.html 继承 base.html

# templates/base.html

# base.html 中需要定义的 block
1.每个页面的 title 是动态切换的
<title>{% block title %}慕学在线网{% endblock title %}</title>
2.静态文件,静态文件是有共用部分的,很多 css、js 等静态文件都是共用的,有些页面是有自己的新加的 静态文件的
<head>
    {% block custom_css %}{% endblock custom_css %}
    {% block custom_js %}{% endblock custom_js %}
</head>
3.面包屑导航部分
{% block custom_bread %}{% endblock custom_bread %}
4.内容区部分
{% block content %}{% endblock content %}

# templates/org-list.html

{% extends "base.html" %}
{% block title %}课程机构列表 - 慕学在线网{% endblock %}

下图为 {% block custom_bread %} 定义的面包屑导航部分


003.png
  • step3:定义 课程机构列表页类视图 OrgView,配置 url
# apps/organization/views.py

from django.shortcuts import render
from django.views.generic import View


class OrgView(View):
    """
    课程机构列表页类视图
    """
    def get(self, request):
        return render(request, "org-list.html", context={})

# mxonline/urls.py

from organization.views import OrgView


urlpatterns = [
    ...

    # 课程机构首页
    path("org_list/", OrgView.as_view(), name="org_list"),
]

004.png
# templates/org-list.html

{% extends "base.html" %}
{% load staticfiles %}

{% block title %}课程机构列表 - 慕学在线网{% endblock %}

{% block custom_bread %}
    <section>
        <div class="wp">
            <ul  class="crumbs">
                <li><a href="index.html">首页</a>></li>
                <li>课程机构</li>
            </ul>
        </div>
    </section>
{% endblock custom_bread %}

{% block content %}
<section>
    ...
</section>
{% endblock content %}

(二)、编写课程机构列表页后台逻辑
  1. 分析页面的数据,不会变动的数据可以直接写成静态数据,经常要变动的数据要从后台获取,写成动态数据
005.png
  1. 启动项目,进入 admin 后台管理系统
  • step1:随意添加几个城市
006(1).png 006.png 007.png
  • step2:增加课程机构,因为课程机构有上传封面图的按钮,所以需要先在 steeings.py 中配置上传文件路径
008.png
  • 回顾 CourseOrg 课程机构的 model,里面有一个 image 字段是用来上传封面图的字段,其中 upload_to 指明图片上传路径,这个路径是一个相对路径,是相对于项目上传文件的根目录,而这个根目录是需要我们自己手动配置的
class CourseOrg(models.Model):
    """
    课程机构表
    """
    ...
    image = models.ImageField(max_length=100, upload_to="organization/%Y/%m", verbose_name="封面图")

  • 在项目根目录下创建 media/ 目录,作为用户上传文件的根目录,存放上传文件
009.png
  • 需要在 settings.py 中配置文件上传的根目录
# mxonline/settings.py

# 指明上传文件的路径
# media 只能配置一个, 不能像 static 那样配置多个
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

  • 现在可以上传课程机构的封面图了,图片将存放于 /medai/organization/%Y/%m/ 目录下
010.png 011.png 012.png
  • 为了便于测试筛选功能,和稍后需要做的分页功能,需要多添加几条数据
  • 因为机构列表页有一个按照“机构类别”来筛选课程机构的功能,而数据库之前没有相应字段,所以为 CourseOrg model 类添加 category 字段
# apps/organization/models.py

class CourseOrg(models.Model):
    """
    课程机构表
    """

    pxjg = "pxjg"
    gr = "gr"
    gx = "gx"
    category_choices = (
        (pxjg, "培训机构"),
        (gr, "个人"),
        (gx, "高校"),
    )

    name = models.CharField(max_length=50, verbose_name="机构名称")
    describe = models.TextField(verbose_name="机构描述")
    category = models.CharField(max_length=20, choices=category_choices, default=pxjg, verbose_name="所属机构")
    favorite_nums = models.IntegerField(default=0, verbose_name="收藏数")
    click_nums = models.IntegerField(default=0, verbose_name="点击数")
    image = models.ImageField(max_length=100, upload_to="organization/%Y/%m", verbose_name="封面图")
    address = models.CharField(max_length=100, verbose_name="机构地址")
    city = models.ForeignKey(CityDict, on_delete=models.SET_NULL, null=True, verbose_name="所在城市")
    add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

    class Meta:
        verbose_name = "课程机构"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name

  • 运行 makemigrations organization,migrate organization,重启项目
  1. 完善 OrgView 类视图
# apps/organization/views.py
from django.shortcuts import render
from django.views.generic import View

from .models import CourseOrg, CityDict


class OrgView(View):
    """
    课程机构列表页类视图
    """
    def get(self, request):
        # 课程机构
        all_orgs = CourseOrg.objects.all()
        # 课程机构所在城市
        all_citys = CityDict.objects.all()
        # 课程机构总数量
        org_nums = CourseOrg.objects.count()
        context = {
            "all_orgs": all_orgs,
            "all_citys": all_citys,
            "org_nums": org_nums,
        }
        return render(request, "org-list.html", context)

  • 将 课程机构 和 课程机构所在城市 传递到前端页面,完成动态获取数据
  • 先替换 org-list.html 中 课程机构所在城市 部分代码的 <a> 标签
# templates/org-list.html

<h2>所在地区</h2>
<div class="more">更多</div>
<div class="cont">
    <a href="?ct="><span class="active2">全部</span></a>

            <a href="?city=1&ct="><span class="">北京市</span></a>

            <a href="?city=2&ct="><span class="">上海市</span></a>

            <a href="?city=3&ct="><span class="">广州市</span></a>

            <a href="?city=4&ct="><span class="">深圳市</span></a>

            <a href="?city=5&ct="><span class="">天津市</span></a>

</div>

  • 替换结果如下
# templates/org-list.html

<h2>所在地区</h2>
<div class="more">更多</div>
<div class="cont">
    <a href="?ct="><span class="active2">全部</span></a>
        {% for city in all_citys %}
            <a href="?city=1&ct="><span class="">{{ city }}</span></a>
        {% endfor %}
</div>

  • 刷新课程机构列表页,所在地区已经由静态变成动态获取数据库中的数据了
013.png
  • 观察 课程机构 部分代码都是由 <dl> 标签构成的
# templates/org-list.html

<dl class="des difdes">
    <dt>
        <a href="org-detail-homepage.html">
            <img width="200" height="120" class="scrollLoading" data-url="{% static "media/org/2016/11/imooc.png" %}"/>
        </a>
    </dt>
    <dd>
        <div class="clearfix">
             <a href="org-detail-homepage.html">
                 <h1>慕课网</h1>
                 <div class="pic fl">
                     <img src="{% static "images/authentication.png" %}"/>
                     <img src="{% static "images/gold.png" %}"/>
                 </div>
             </a>
        </div>
        <ul class="cont">
            <li class="first"><p class="pic9">课程数:<span>1</span></p><p class="c7">学习人数:<span>1000</span></p></li>
            <li class="c8" style="padding-left:18px;">北京市海淀区中关村北大街</li>
            <li class="pic10" style="padding-left:18px;">经典课程:
                <a href="/diary/19/">c语言基础入门</a>
                <a href="/diary/16/">数据库基础</a>
            </li>
        </ul>
    </dd>
    <div class="buy start_groupbuy jsShowPerfect2" data-id="22"><br/>联系<br/>服务</div>
</dl>
...

  • 替换结果如下
# templates/org-list.html

{% for course_org in all_orgs %}
    <dl class="des difdes">
        <dt>
            <a href="org-detail-homepage.html">
                <img width="200" height="120" class="scrollLoading" data-url="{{ MEDIA_URL }}{{ course_org.image }}"/>
            </a>
        </dt>
        <dd>
            <div class="clearfix">
                 <a href="org-detail-homepage.html">
                     <h1>{{ course_org.name }}</h1>
                     <div class="pic fl">

                             <img src="{% static "images/authentication.png" %}"/>

                             <img src="{% static "images/gold.png" %}"/>

                     </div>
                 </a>
            </div>
            <ul class="cont">
                <li class="first"><p class="pic9">课程数:<span>1</span></p><p class="c7">学习人数:<span>1000</span></p></li>
                <li class="c8" style="padding-left:18px;">北京市海淀区中关村北大街</li>
                <li class="pic10" style="padding-left:18px;">经典课程:

                        <a href="/diary/19/">c语言基础入门</a>

                        <a href="/diary/16/">数据库基础</a>

                </li>
            </ul>
        </dd>
        <div class="buy start_groupbuy jsShowPerfect2" data-id="22"><br/>联系<br/>服务</div>
    </dl>
{% endfor %}

  • 其中 课程机构的 logo(封面图) 是 通过 <img> 标签的 data-url 属性获得的
<img width="200" height="120" class="scrollLoading" data-url="{% static "media/org/2016/11/imooc.png" %}"/>
  • 因为 data-url 需要的是一个 url 地址,所以需要把 image (model 中字段类型为 ImageField)字段转换成一个 url 地址,先看一下数据库是如何保存的
014.png
  • 实际上 image 字段在数据库中存储的就是一个字符串,这个字符串是一个相对路径地址
# 如果直接这样写,实际上是取得相对路径地址,不能完成图片存取
<img data-url="{{ course_org.image }}"/>
# 可以这样写成完整路径地址,这样是可以完成图片存取的
<img data-url="/media/{{ course_org.image }}"/>
# 可以使用可灵活配置的更优雅的方式
<img data-url="{{ MEDIA_URL }}{{ course_org.image }}"/>
  • 完成 <img data-url="{{ MEDIA_URL }}{{ course_org.image }}"/> 配置实际上前端页面还是获取不到图片的

  • 在 settings.py 中的 TEMPLATES找到 context_processors 变量,并在内部添加 media 文件的配置 "django.template.context_processors.media", 只有配置了这个类,django 才会自动将 {{ MEDIA_URL }} 注册到 html 代码中,不然是不会显示在 html 代码中的


TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')]
        ,
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                ...
                "django.template.context_processors.media",
            ],
        },
    },
]

  • 虽然 代码中是下面这样写的
<img width="200" height="120" class="scrollLoading" data-url="{{ MEDIA_URL }}{{ course_org.image }}"/>
  • 如果不在 settings.py 中配置 "django.template.context_processors.media", 前端显示结果
015.png
  • 配置后 前端显示结果
016.png
  • 现在 <img> 标签的 url 是对了,但图片还是没有显示出来,还需要在 urls.py 中配置对应的 url,用来告诉 django 如果 url 路径是在 media/ 目录之下应该去哪里取相应文件
  • 所以要配置一个 ulr 专门来处理 media 的信息
# mxonline/urls.py

from django.views.static import serve
from mxonline.settings import MEDIA_ROOT


urlpatterns = [
    ...

    # 配置上传文件的访问处理函数
    # 这里的配置一定要这样写, 先取出 media/ 之下的路径, 放到 path 变量下,
    # media/ 之下的文件是静态文件, 为了处理这些静态文件,
    # 需要用到 django 用来处理静态文件的内置函数 static 的 serve方法,
    # serve 方法需要传递一个参数, 参数的名称叫 document_root,
    # 意思是处理 path 路径之下的访问, 需要去 MEDIA_ROOT 路径之下去寻找
    re_path(r"^media/(?P<path>.*)$", serve, {"document_root": MEDIA_ROOT}),
]

  • 刷新页面,图片已经传递到前端了
017.png
  1. 完成分页功能
  • step1:分页功能可以用第三方开源库 django-pure-pagination 来完成,可以在 github 上面搜索到(github 地址:https://github.com/jamespacileo/django-pure-pagination

  • 1.进入项目虚拟环境,pip install django-pure-pagination 进行安装

(mxonline) pythonic@pythonic-machine:~$ pip install django-pure-pagination
Collecting django-pure-pagination
  Downloading django-pure-pagination-0.3.0.tar.gz
Building wheels for collected packages: django-pure-pagination
  Running setup.py bdist_wheel for django-pure-pagination ... done
  Stored in directory: /home/pythonic/.cache/pip/wheels/6a/ba/38/21d6880456355583455f47c35e0c14659c66bdcfb8214aad1a
Successfully built django-pure-pagination
Installing collected packages: django-pure-pagination
Successfully installed django-pure-pagination-0.3.0

  • 2.将 pure_pagination 添加到 settings.py 中的 INSTALLED_APPS 当中
# mxonline/settings.py

INSTALLED_APPS = [
    ...
    "pure_pagination",
]
  • 3.将分页逻辑添加到 OrgView 中
# apps/organization/views.py

from pure_pagination import Paginator, EmptyPage, PageNotAnInteger


class OrgView(View):
    """
    课程机构列表页类视图
    """
    def get(self, request):
        # 课程机构
        all_orgs = CourseOrg.objects.all()
        # 课程机构总数量
        org_nums = CourseOrg.objects.count()
        # 课程机构所在城市
        all_citys = CityDict.objects.all()

        # 对课程机构进行分页
        try:
            page = request.GET.get('page', 1)
        except PageNotAnInteger:
            page = 1
        # Paginator 接收 3 个参数, 需要分页的对象列表, 每页显示的对象数量, 以及 request        
        p = Paginator(all_orgs, 1, request=request)
        orgs = p.page(page)

        context = {
            "all_orgs": orgs,
            "org_nums": org_nums,
            "all_citys": all_citys,
        }
        return render(request, "org-list.html", context)

  • 4.模板中处理分页需要显示的逻辑
# templates/org-list.html

显示课程机构列表部分,只需要修改 for 循环遍历的 all_orgs 对象,其他部分完全不用修改
{% for course_org in all_orgs %}
    ...
{% endfor %}
将上面代码改成如下代码,现在后端传递过来的 all_orgs 已经不是所有课程机构列表了,而是课程机构列表的一个 page 对象,这个对象的 object_list 方法即可取出当前页面全部的课程机构
{% for course_org in all_orgs.object_list %}
    ...
{% endfor %}

# templates/org-list.html

# 页码部分逻辑
<div class="pageturn pagination">
     <ul>
         {% if all_orgs.has_previous %}
             <li class="long"><a href="?{{ all_orgs.previous_page_number.querystring }}">上一页</a></li>
         {% endif %}
         {% for page in all_orgs.pages %}
             {% if page %}
                 {% ifequal page all_orgs.number %}
                     <li class="page active"><a href="?{{ page.querystring }}">{{ page }}</a></li>
                 {% else %}
                     <li><a href="?{{ page.querystring }}" class="page">{{ page }}</a></li>
                 {% endifequal %}
             {% else %}
                 <li class="none"><a href="">...</a></li>
             {% endif %}
         {% endfor %}
         {% if all_orgs.has_next %}
             <li class="long"><a href="?{{ all_orgs.next_page_number.querystring }}">下一页</a></li>
         {% endif %}
     </ul>
</div>

  • 5.访问页面查看结果
018.png
  • 6.实现按照机构类别和所在地区筛选课程机构
# apps/organization/views.py

class OrgView(View):
    """
    课程机构列表页类视图
    """
    def get(self, request):
        # 课程机构
        all_orgs = CourseOrg.objects.all()
        # 课程机构所在城市
        all_citys = CityDict.objects.all()

        # 取出筛选城市
        city_id = request.GET.get("city", "")
        # 如果有 city_id, 对 all_orgs 结果集进行进一步筛选
        if city_id:
            all_orgs = all_orgs.filter(city_id=city_id)

        # 取出筛选机构类别
        category = request.GET.get("category", "")
        # 如果有 category, 对 all_orgs 结果集进行进一步筛选
        if category:
            all_orgs = all_orgs.filter(category=category)

        # 课程机构总数量
        org_nums = all_orgs.count()

        # 对课程机构进行分页
        try:
            page = request.GET.get('page', 1)
        except PageNotAnInteger:
            page = 1
        # Paginator 接收 3 个参数, 需要分页的对象列表, 每页显示的对象数量, 以及 request
        p = Paginator(all_orgs, 1, request=request)
        orgs = p.page(page)

        context = {
            "all_orgs": orgs,
            "org_nums": org_nums,
            "all_citys": all_citys,
            "city_id": city_id,
            "category": category,
        }
        return render(request, "org-list.html", context)

# templates/org-list.html

<div class="listoptions">
    <ul>
        <li>
            <h2>机构类别</h2>
            <div class="cont">
                <a href="?city={{ city_id }}"><span {% ifequal category "" %}class="active2"{% endifequal %}>全部</span></a>

                <a href="?category=pxjg&city={{ city_id }}"><span {% ifequal category "pxjg" %}class="active2"{% endifequal %}>培训机构</span></a>

                <a href="?category=gx&city={{ city_id }}"><span {% ifequal category "gx" %}class="active2"{% endifequal %}>高校</span></a>

                <a href="?category=gr&city={{ city_id }}"><span {% ifequal category "gr" %}class="active2"{% endifequal %}>个人</span></a>

            </div>
        </li>
        <li>
            <h2>所在地区</h2>
            <div class="more">更多</div>
            <div class="cont">
                <a href="?category={{ category }}"><span {% ifequal city_id "" %}class="active2"{% endifequal %}>全部</span></a>
                {% for city in all_citys %}
                    <a href="?city={{ city.id }}&category={{ category }}"><span {% ifequal city_id city.id|stringformat:"i" %}class="active2"{% endifequal %}>{{ city }}</span></a>
                {% endfor %}
            </div>
        </li>
    </ul>
</div>

  • 对以上模板中部分代码做讲解:
{% ifequal city_id city.id|stringformat:"i" %}class="active2"{% endifequal %}
其中:
1. ifequal 是 django 在模板中判定两个变量是否相等的语法,
ifequal var1 var2 ==等价于==> if var1 == var2;
2. city.id|stringformat:"i" 是通过过滤器将 int 类型的 city.id 转换成 str 类型,因为 city_id 是字符串类型,想比较两者是否相等就要转换成相同的类型比较
  • 7.实现效果
019.png
  • 通过观察 org-list.html 中的代码和 url 地址栏,可以发现 django-pure-pagination 强大的地方就在于,其实在写分页部分逻辑的时候,<a> 标签 href 属性我们没有自己写,而是直接采用了这个库提供的 href="?{{ page.querystring }}" ,它能够自动将 URL 地址中的 city=1&category=pxjg 这两个参数记住,这样就能够实现 既可以通过机构类别和所在地区筛选课程机构,同时分页功能还可以适用筛选结果页
  1. 完成页面右侧“授课机构排名”,根据点击量进行排序
020.png
# apps/organization/views.py

class OrgView(View):
    """
    课程机构列表页类视图
    """
    def get(self, request):
        # 课程机构
        all_orgs = CourseOrg.objects.all()

        # 根据点击量对课程机构进行排序
        hot_orgs = all_orgs.order_by("-click_nums")[:3]
        ...

        context = {
            ...
            "hot_orgs": hot_orgs,
        }
        return render(request, "org-list.html", context)

# templates/org-list.html

<div class="head">授课机构排名</div>
    {% for hot_org in hot_orgs %}
        <dl class="des">
            <dt class="num fl">{{ forloop.counter }}</dt>
            <dd>
                <a href="/company/2/"><h1>{{ hot_org.name }}</h1></a>
                <p>{{ hot_org.address }}</p>
            </dd>
        </dl>
    {% endfor %}
</div>

021.png
  1. 实现点击页面“学习人数”和“课程数”能够使课程机构排序
022.png
  • 因为课程机构模型类 CourseOrg 中没有这两个字段,所以先加上这两个字段
# apps/organization/models.py

class CourseOrg(models.Model):
    """
    课程机构表
    """

    ...
    students = models.IntegerField(default=0, verbose_name="学习人数")
    course_nums = models.IntegerField(default=0, verbose_name="课程数")

  • 依次运行 makemigrations organization,migrate organization 完成数据库迁移
# apps/organization/views.py

class OrgView(View):
    """
    课程机构列表页类视图
    """
    def get(self, request):
        # 课程机构
        all_orgs = CourseOrg.objects.all()
        ...
        # 根据学习人数或课程数对 all_orgs 进行排序
        sort = request.GET.get("sort", "")
        if sort:
            if sort == "students":
                all_orgs = all_orgs.order_by("-students")
            elif sort == "courses":
                all_orgs = all_orgs.order_by("-course_nums")

        ...

        context = {
            ...
            # 这里要把 sort 传递到前端模板中, 目的是为了在前端模板中
            # 可以通过 sort 来判断当前排序规则的文字应该处于激活状态
            "sort": sort,
        }
        return render(request, "org-list.html", context)

# templates/org-list.html

<div class="head">
    <ul class="tab_header">
        <li class="{% if sort == "" %}active{% endif %}"><a href="?category={{ category }}&city={{ city_id }}">全部</a> </li>
        <li class="{% if sort == "students" %}active{% endif %}"><a href="?sort=students&category={{ category }}&city={{ city_id }}">学习人数 &#8595;</a></li>
        <li class="{% if sort == "courses" %}active{% endif %}"><a href="?sort=courses&category={{ category }}&city={{ city_id }}">课程数 &#8595;</a></li>
    </ul>
</div>

023.png
  1. 整理下 url,在 organization/ 目录下新建 urls.py 文件
024.png
# mxonline/urls.py

from django.urls import path, re_path, include


urlpatterns = [
    ...

    # 课程机构首页
    path("org/", include("organization.urls")),
]

# apps/organization/urls.py

from django.urls import path, re_path

from .views import OrgView


app_name = "org"
urlpatterns = [
    path("", OrgView.as_view(), name="org_list"),
]

  1. 通过 ModelForm 完成 我要学习功能
025.png
# apps/operation/models.py

class UserAsk(models.Model):
    """
    用户咨询表
    """
    name = models.CharField(max_length=20, verbose_name="用户名")
    mobile = models.CharField(max_length=11, verbose_name="手机")
    course_name = models.CharField(max_length=50, verbose_name="课程名称")
    add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

  • 分析发现,前端页面用户所要提交的我要学习表单和后台定义的 UserAsk 模型类是相对应的,所以可以采用 django 的 ModelForm 来处理表单的提交验证
026.png
# apps/organization/forms.py

from django import forms
from operation.models import UserAsk


# class UserAskForm(forms):
#     name = forms.CharField(required=True, min_length=2, max_length=20)
#     mobile = forms.CharField(required=True, min_length=11, max_length=11)
#     course_name = forms.CharField(required=True, min_length=5, max_length=50)

# 上面是通常需要定义的验证表单的类, 但是这个 UserAskForm
# 有一个与之对应的 model, 所以完全可以用下面的写法来替代上面的写法


# 因为前端用户咨询所要提交的表单刚好有一个模型类与之对应,
# 所以这里可以直接定义一个 UserAskForm 表单类, 继承自
# forms.ModelForm, 这样就不需要自己再定义需要验证的字段,
# 只需要在 Meta 中指定 表单所对应的数据库模型类, 和所需要
# 验证的字段就可以了
class UserAskForm(forms.ModelForm):
    """
    用户咨询表单
    """
    class Meta:
        model = UserAsk
        fields = ["name", "mobile", "course_name"]

# apps/organization/views.py

class UserAskView(View):
    def post(self, request):
        userask_form = UserAskForm(request.POST)
        userask_form.save(commit=True)

  • 观察分析我要学习功能,有两个特点:1.这个功能是页面的一个小的独立区域,适合用 ajax 异步,而不是点击点击立即咨询按钮就刷新整个页面;2.如果提交的表单信息有错误,前端页面是要返回错误信息的

  • 定义 AddUserAskView 用来处理用户点击"我要咨询"按钮,提交表单信息

# apps/organization/views.py

from django.http import JsonResponse


class AddUserAskView(View):
    """
    用户添加咨询类视图
    """
    def post(self, request):
        userask_form = UserAskForm(request.POST)

        if userask_form.is_valid():
            # userask_form 的 save 方法有个参数 commit, 默认就是为 True,
            # 是直接将数据保存到数据库中, 如果 commit 为 False, 则这里只是
            # 将结果提交到数据库, 但并没有真正保存到数据库
            user_ask = userask_form.save(commit=True)

            # 这里是进行 ajax 操作, 所以不能用 render 来返回整个页面,
            # 用 render 返回页面是要刷新整个页面的, 而进行 ajax 异步操作,
            # 这里应该返回 json, 通过 JsonResponse 返回 json 字符串,
            # JsonResponse 是 HttpResponse 的子类 
            return JsonResponse(data='{"status": "success"}', safe=False)
        else:
            # 注意: 这里有个坑, data 是要返回到前端的 json, 标准 json 是用的
            # 双引号, 所以这里一定要在 json 中用 双引号, 花括号外面用单引号
            return JsonResponse(data='{"status": "fail", "error_msg": "填写错误!"}', safe=False)

  • 定义相应的 url
# apps/organization/urls.py

from django.urls import path, re_path

from .views import OrgView, AddUserAskView


app_name = "org"
urlpatterns = [
    ...
    path("add_ask/", AddUserAskView.as_view(), name="add_ask"),
]

  • 利用 jQuery 编写课程机构列表页 我要学习表单的 ajax 部分代码
# templates/org-list.html

{% block custom_js %}
<script>
    $(function(){
        $('#jsStayBtn').on('click', function(){
            $.ajax({
                cache: false,
                type: "POST",
                url: "{% url "org:add_ask" %}",
                data: $('#jsStayForm').serialize(),
                dataType: "json",
                async: true,
                success: function(data) {
                    console.log(typeof(data));
                    console.log(data);
                    var data = JSON.parse(data);
                    console.log(typeof(data));
                    console.log(data.status);
                    if(data.status == 'success'){
                        $('#jsStayForm')[0].reset();
                        alert("提交成功")
                    }else if(data.status == 'fail'){
                        $('#jsCompanyTips').html(data.error_msg)
                        alert("失败");
                    }else {
                        alert("见鬼了");
                    }
                },
            });
        });
    })

</script>
{% endblock custom_js %}

027.png 028.png 029.png
  • 修改用户咨询表单验证视图 UserAskForm, 利用正则来自定义手机号的验证
# apps/organization/forms.py

import re

from django import forms
from operation.models import UserAsk

class UserAskForm(forms.ModelForm):
    """
    用户咨询表单
    """
    class Meta:
        model = UserAsk
        fields = ["name", "mobile", "course_name"]

    # 因为 model 中只是对 "mobile" 字段进行了长度验证,
    # 这里需要对手机号进行更加严格的匹配, 采用正则来验证手机号是否合法,
    # django 的 ModelForm 为我们提供了一个方便自定义字段验证规则的方法,
    # 使用这个方法的要求是必须以 clean_ 开头, 下划线后面可以根据需求自定义,
    # 这样, django 在这个 UserAskForm 表单类被实例化的时候, 会自动在初始化
    # 过程中调用下面所定义的 clean_mobile 方法, 来完成 "mobile" 的验证
    def clean_mobile(self):
        """
        验证手机号码是否合法
        """
        # 先获取要自定义验证规则的字段, 字段会以字典的形式存储在 cleaned_data 中,
        # 可以通过 cleaned_data["字段名"] 来得到前端提交的相应数据
        mobile = self.cleaned_data["mobile"]

        re_mobile = r"^1[34578]\d{9}$"
        p = re.compile(re_mobile)
        if p.match(mobile):
            # 验证成功, 返回 mobile
            return mobile
        else:
            # 验证失败, 抛出自定义错误信息
            # 这里用 raise 来抛出异常
            raise forms.ValidationError("手机号码不合法", code="mobile_invalid")

030.png 031.png 032.png

二、课程机构详情页

  • 课程机构详情页主要包括 机构课程、机构介绍、机构讲师
033.png
  • 可以通过 admin 后台增加几条数据
  • 增加数据过程发现 "机构课程" 与 "机构" 之间是 多 对 1 的关系,所以要在 课程 的 model 中新增一个 机构 的 外键
# apps/courses/models.py

from organization.models import CourseOrg


class Course(models.Model):
    """
    课程表
    """

    cj = "cj"
    zj = "zj"
    gj = "gj"
    degree_choices = (
        (cj, "初级"),
        (zj, "中级"),
        (gj, "高级"),
    )

    course_org = models.ForeignKey(CourseOrg, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="课程机构")
    name = models.CharField(max_length=50, verbose_name="课程名称")
    describe = models.CharField(max_length=200, verbose_name="课程描述")
    detail = models.TextField(verbose_name="课程详情")
    degree = models.CharField(max_length=2, choices=degree_choices, verbose_name="课程难度")
    learn_times = models.IntegerField(default=0, verbose_name="学习时长(分钟数)")
    students = models.IntegerField(default=0, verbose_name="学习人数")
    favorite_nums = models.IntegerField(default=0, verbose_name="收藏人数")
    image = models.ImageField(max_length=100, upload_to="courses/%Y/%m", verbose_name="封面图")

    click_nums = models.IntegerField(default=0, verbose_name="点击数")
    add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

    class Meta:
        verbose_name = "课程"
        verbose_name_plural = verbose_name

  • 运行 makemigrations courses,migrate courses,重启项目
  • 再次进入后台添加课程的时候,就会多出来一个 "课程机构" 的下拉框,可以选择课程所属的 课程机构
034.png
  • 将页面复制到 templates/ 目录下
035.png
  • 定义 OrgHomeView 类视图
# apps/organization/views.py

class OrgHomeView(View):
    """
    机构首页类视图
    """
    def get(self, request, org_id):
        # 通过 url 地址中传递进来的 org_id, 取出对应的 课程机构
        course_org = CourseOrg.objects.get(id=org_id)
        # 知道了课程机构 course_org, 就可以通过这个 课程机构
        # 反向取出所有该课程机构下面的 课程 courses,
        # 在 django 的 ORM 中 可以直接通过 course_set 来得到
        courses = course_org.course_set.all()[:3]
        teachers = course_org.teacher_set.all()[:1]

        context = {
            "courses": courses,
            "teachers": teachers,
        }
        return render(request, "org-detail-homepage.html", context)

  • 配置 url
# apps/organization/urls.py

from django.urls import path, re_path

from .views import OrgHomeView


app_name = "org"
urlpatterns = [
    ...
    re_path(r"^home/(?P<org_id>\d+)/$", OrgHomeView.as_view(), name="org_home"),
]

  • 定义 "org_base.html" 页面, 用做 机构详情页的基本页面, 其他 4 个页面可以继承此页面
  • 先来完善 "org-detail-homepage.html" 页面, 替换模板中的静态文件, 将后台传递过来的数据显示出来
  • 访问 http://127.0.0.1:8000/org/home/2/ 查看效果
036.png
  • 因为 4 个页面左侧导航菜单部分都是共用的, 所以要处理下这 4 个导航菜单, 当前页面处于哪个页面, 就让对应的导航菜单处于激活状态
037.png
  • 处理办法:
  • (1)先在 类视图中 定义一个 current_name 变量传递到前端页面
# apps/organization/views.py

class OrgHomeView(View):
    """
    机构首页类视图
    """
    def get(self, request, org_id):
        # 因为前端页面中机构相关的四个页面是共用的, 所以这里定义
        # 一个 current_name 变量传递到前端页面, 用来判断当前
        # 导航菜单是否为激活状态
        current_name = "home"

        # 通过 url 地址中传递进来的 org_id, 取出对应的 课程机构
        course_org = CourseOrg.objects.get(id=org_id)
        # 知道了课程机构 course_org, 就可以通过这个 课程机构
        # 反向取出所有该课程机构下面的 课程 courses,
        # 在 django 的 ORM 中 可以直接通过 course_set 来得到
        courses = course_org.course_set.all()[:3]
        teachers = course_org.teacher_set.all()[:1]

        context = {
            "course_org": course_org,
            "courses": courses,
            "teachers": teachers,
            "current_name": current_name,
        }
        return render(request, "org-detail-homepage.html", context)

  • (2)在前端页面判断后台传递过来的变量 current_name 是否等于当前页面, 如果等于当前页面, 添加 active2
# templates/org_base.html

<div class="left">
    <ul>
        <li class="{% ifequal current_name "home" %}active2{% endifequal %}"><a href="{% url "org:org_home" course_org.id %}">机构首页</a></li>
        <li class="{% ifequal current_name "course" %}active2{% endifequal %}"><a href="{% url "org:org_course" course_org.id %}">机构课程</a></li>
        <li class="{% ifequal current_name "description" %}active2{% endifequal %}"><a href="org-detail-desc.html">机构介绍</a></li>
        <li class="{% ifequal current_name "teacher" %}active2{% endifequal %}"><a href="org-detail-teachers.html">机构讲师</a></li>
    </ul>
</div>

  • 其他 3 个页面同机构首页类似
  • 利用 Ajax 完成课程机构的收藏功能
038.png
  • (1) 定义 AddFavoriteView 类视图,用来处理用户收藏
# apps/organization/views.py

class AddFavoriteView(View):
    """
    用户收藏、取消收藏类视图
    """
    def post(self, request):
        # 这里 favorite_id 和 favorite_type 默认值都设定为 "0",
        # 而不是 "", 是因为下面查询 exist_records 的时候需要将这两个
        # 变量转换成 int 类型, 如果默认值设定为 "", 转换 int 是会报错的
        favorite_id = request.POST.get("favorite_id", "0")
        favorite_type = request.POST.get("favorite_type", "0")

        # 既然是用户收藏, 这里首先要判断用户是否登录
        # 即使用戶未登录状态, request 中也是有 user 的, 只需要调用
        # user 的 is_authenticated 方法, 就可以验证用户是否登录
        # 如果用户未登录, 执行以下逻辑
        if not request.user.is_authenticated:
            # 如果用户没有登录, 返回错误信息, 告诉 Ajax 用户未登录,
            # 不能进行收藏, 通过传递到 ajax 的数据, ajax 判断如果
            # 用户在点击收藏按钮时, 处于未登录状态, 则跳转到登录页面
            return JsonResponse(data='{"status": "fail", "msg": "用户未登录"}', safe=False)

        # 如果用户已经登录, 执行以下逻辑
        exist_records = UserFavorite.objects.filter(user=request.user, favorite_id=int(favorite_id), favorite_type=int(favorite_type))
        if exist_records:
            # 如果记录已经存在, 则表示用户要进行取消收藏的操作
            # 调用 delete 方法删除记录
            exist_records.delete()
            return JsonResponse(data='{"status": "success", "msg": "收藏"}', safe=False)
        else:
            # 如果记录不存在, 则表示用户要进行添加收藏的操作
            user_favorite = UserFavorite()
            if int(favorite_id) > 0 and int(favorite_type) > 0:
                user_favorite.user = request.user
                user_favorite.favorite_id = int(favorite_id)
                user_favorite.favorite_type = int(favorite_type)
                user_favorite.save()
                return JsonResponse(data='{"status": "success", "msg": "已收藏"}', safe=False)
            else:
                return JsonResponse(data='{"status": "fail", "msg": "收藏出错"}', safe=False)

  • (2) 配置 url
# apps/organization/urls.py

from django.urls import path, re_path

from .views import AddFavoriteView


app_name = "org"
urlpatterns = [
    ...
    # 添加收藏
    re_path(r"^add_favorite/$", AddFavoriteView.as_view(), name="add_favorite"),
]

  • (3) 前端 ajax 部分代码

<script type="text/javascript">
//收藏分享
function add_favorite(current_elem, favorite_id, favorite_type){
    $.ajax({
        cache: false,
        type: "POST",
        url:"{% url "org:add_favorite" %}",
        data:{'favorite_id':favorite_id, 'favorite_type':favorite_type},
        async: true,
        // 因为用户点击"收藏"按钮后,向后台提交 POST 请求时,没有通过 form,
        // 不能在 form 中定义 {% csrf_token %}, 所以只能在这里把 {{ csrf_token }} 传递进来
        beforeSend:function(xhr, settings){
            xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");
        },
        success: function(data) {
            var data = JSON.parse(data);
            if(data.status == "fail"){
                if(data.msg == "用户未登录"){
                    window.location.href="{% url "login" %}";
                }else{
                    alert(data.msg);
                }

            }else if(data.status == 'success'){
                current_elem.text(data.msg);
            }
        },
    });
}

// 监听事件, 用来监听收藏按钮的点击操作
$('.collectionbtn').on('click', function(){
    add_favorite($(this), {{ course_org.id }}, 2);
});
</script>

  • (4) 另外, 需要在页面加载的时候判断,用户是否已经收藏了该课程机构,所以要在 OrgHomeView、OrgCourseView、OrgDescriptionView、OrgTeacherView 类视图中都同时加上判断
# apps/organization/views.py

class OrgHomeView(View):
    """
    机构首页类视图
    """
    def get(self, request, org_id):
        ...
        # 判断用户是否收藏该机构, 定义 has_favorite 变量, 默认为 False
        has_favorite = False
        # 首先要判断用户是否登录, 用户为登录状态才能判断用户是否收藏该机构
        if request.user.is_authenticated:
            if UserFavorite.objects.filter(user=request.user, favorite_id=course_org.id, favorite_type=2):
                has_favorite = True
        ...
        context = {
            ...
            "has_favorite": has_favorite,
        }
        return render(request, "org-detail-homepage.html", context)

前端模板中判断应该显示"收藏", 还是"已收藏"

# templates/org_base.html

<div class="btn fr collectionbtn notlogin">{% if has_favorite %}已收藏{% else %}收藏{% endif %}</div>

相关文章

网友评论

      本文标题:Django 开发 MxOnline 项目笔记 -- 第7章 课

      本文链接:https://www.haomeiwen.com/subject/zkuanxtx.html