美文网首页
后台管理站点 -- 3.文章管理

后台管理站点 -- 3.文章管理

作者: 爱修仙的道友 | 来源:发表于2019-03-12 23:58 被阅读0次

    文章搜索功能

    1.分析

    请求方法GET
    url定义/admin/news/
    请求参数:字符串参数传参

    参数 类型 前端是否必须传 描述
    start_time 字符串 开始时间
    end_time 字符串 结束时间
    title 字符串 文章标题
    author_name 字符串 作者姓名
    tag_id 字符串 标签id
    page 字符串 页数
    2.后端视图
    • views.py
    import json
    import logging
    from datetime import datetime
    from urllib.parse import urlencode
    
    from django.http import Http404, JsonResponse
    from django.views import View
    from django.db.models import Count
    from django.shortcuts import render
    from django.core.paginator import Paginator, EmptyPage
    from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
    
    from Dreamblog import settings
    from admin import forms
    from news import models
    from . import constants
    from .scripts import paginator_script
    from utils.json_translate.json_fun import to_json_data
    from utils.res_code.rescode import Code, error_map
    
    # 后台文章搜索
    class NewsManageView(PermissionRequiredMixin, View):
        """
        1.创建类视图
        2.从前端获取参数
            -- 传参方式:查询字符串方式 ?author=author&
        3.查询数据
        4.分页操作
        5.模板渲染
        """
        permission_required = ('news.add_news','news.view_news')
        raise_exception = True
    
        def get(self, request):
            """
            获取文章列表信息
            :param request:
            :return:
            """
            # 数据库提供数据
            tags = models.Tag.objects.only('id', 'name').filter(is_delete=False)
            newses = models.News.objects.select_related('author','tag').\
                only('id', 'title', 'author__username', 'tag__name', 'update_time').\
                filter(is_delete=False)
    
            # 通过时间进行过滤
            # 将时间字符串转换与数据库相同的时间参数(datat格式)
            # datetime.strftime() f->from time->str
            # datetime.strptime() p->pass str->time
            # format: 'yyyy/mm/dd', '%Y/%m/%d' 前后文呼应
            try:
                start_time = request.GET.get('start_time','')
                start_time = datetime.strptime(start_time,'%Y/%m/%d') if start_time else ''
            except Exception as e:
                logger.info("用户输入的时间有误:\n{}".format(e))
                start_time = ''
    
            try:
                end_time = request.GET.get('end_time','')
                end_time = datetime.strptime(end_time,'%Y/%m/%d') if end_time else ''
            except Exception as e:
                logger.info("用户输入的时间有误:\n{}".format(e))
                end_time = ''
    
            # lte <=     gte >=
            if start_time and not end_time:
                newses = newses.filter(update_time__lte=start_time)
            if end_time and not start_time:
                newses = newses.filter(update_time__gte=end_time)
    
            # 判断时间前后是否颠倒  __range  取范围,若开始时间小于结束时间为空
            if start_time and end_time:
                newses = newses.filter(update_time__range=(start_time,end_time))
    
                #优化:若newses查询集为空,就没必要执行下面代码
    
            # 通过title进行过滤
            title = request.GET.get('title','')
            if title:
                newses:newses.filter(title__icontains=title)
    
            # 通过作者名进行过滤
            author_name = request.GET.get('author_name','')
            if author_name:
                newses = newses.filter(author__username__icontains=author_name)
    
            # 通过标签id进行过滤
            try:
                tag_id = int(request.GET.get('tag_id',0))
            except Exception as e:
                logger.info("标签错误:\n{}".format(e))
                tag_id = 0
    
            newses = newses.filter(is_delete=False, tag_id=tag_id) or \
                        newses.filter(is_delete=False)
    
            # 分页处理
            try:
                page = int(request.GET.get('page', 1))
                if page == 0:
                    page = 1
            except Exception as e:
                logger.info("当前页数错误:\n{}".format(e))
                page = 1
    
            # (可迭代对象,每页显示数目)
            paginator = Paginator(newses, constants.PER_PAGE_NEWS_COUNT)
    
            try:
                news_info = paginator.page(page)
            except EmptyPage:
                # 若用户访问的页数大于实际页数,则返回最后一页数据
                news_info = paginator.page(paginator.num_pages)
    
            # 分页算法
            paginator_data = paginator_script.get_paginator_data(paginator,news_info)
    
            start_time = start_time.strftime('%Y/%m/%d') if start_time else ''
            end_time = end_time.strftime('%Y/%m/%d') if end_time else ''
    
            context = {
                'news_info':news_info,
                'tags':tags,
                'paginator':paginator,
                'start_time':start_time,
                'end_time':end_time,
                'author_name':author_name,
                'tag_id':tag_id,
                "other_param":urlencode({
                    'start_time':start_time,
                    'end_time':end_time,
                    'author_name':author_name,
                    'tag_id':tag_id,
                })
            }
            context.update(paginator_data)
    
            return render(request, 'admin/news/news_manage.html',context=context)
    
    # 创建apps/admin/constants.py文件
    # 每页新闻数
    PER_PAGE_NEWS_COUNT = 8
    
    • 路由 urls.py
    from django.urls import path
    from . import views
    
    app_name='admin'
    
    urlpatterns = [
        path('index/', views.AdminIndexView.as_view(), name='admin_index'),
        path('tags/', views.TagsManageView.as_view(), name='admin_tags'),
        path('tags/<int:tag_id>/', views.TagEditView.as_view(), name='tag_edit'),
        path('news/', views.NewsManageView.as_view(), name='news_manage'),
    ]
    
    • 分页算法
    # 创建scripts/paginator_script.py文件,定义如下方法:
    def get_paginator_data(paginator, current_page, around_count=3):
        """
        :param paginator: 分页对象
        :param current_page: 当前页数据
        :param around_count: 显示的页码数
        :return: 当前页码、总页数、左边是否有更多页标记、右边是否有更多标记
        左边页码范围、右边页码范围
        """
        current_page_num = current_page.number  # 获取当前页面所在的页码
        total_page_num = paginator.num_pages  # 获取总页数
    
        left_has_more_page = False  # 默认左边没有更多页
        right_has_more_page = False  # 默认右边没有更多页
    
        # 算出当前页面左边的页码
        left_start_index = current_page_num - around_count
        left_end_index = current_page_num
        if current_page_num <= around_count * 2 + 1:
            left_page_range = range(1, left_end_index)
        else:
            left_has_more_page = True
            left_page_range = range(left_start_index, left_end_index)
    
        right_start_index = current_page_num + 1
        right_end_index = current_page_num + around_count + 1
        if current_page_num >= total_page_num - around_count * 2:
            right_page_range = range(right_start_index, total_page_num + 1)
        else:
            right_has_more_page = True
            right_page_range = range(right_start_index, right_end_index)
    
        return {
            "current_page_num": current_page_num,
            "total_page_num": total_page_num,
            "left_has_more_page": left_has_more_page,
            "right_has_more_page": right_has_more_page,
            "left_pages": left_page_range,
            "right_pages": right_page_range,
        }
    
    • 前端代码
    <!-- 创建templates/admin/news/news_manage.html文件 -->
    
    {% extends 'admin/base/base.html' %}
    
    
    {% block title %}
     文章管理页
    {% endblock %}
    
    {% block content_header %}
      文章管理
    {% endblock %}
    
    {% block header_option_desc %}
      正确的决策来自众人的智慧
    {% endblock %}
    
    
    {% block content %}
      <link rel="stylesheet" href="{% static 'css/admin/base/bootstrap-datepicker.min.css' %}">
     <style>
       .ml20 {
         margin-left: 20px;
       }
    
       .mt20 {
         margin-top: 20px;
       }
     </style>
     <div class="box">
       <div class="box header" style="margin: 0;">
         <form action="" class="form-inline" method="get">
           <div class="form-group ml20 mt20">
             <label for="select-time">时间:</label>
             {% if start_time %}
             <input type="text" class="form-control" placeholder="请选择起始时间" readonly
                    id="select-time" name="start_time" value="{{ start_time }}">
               {% else %}
               <input type="text" class="form-control" placeholder="请选择起始时间" readonly
                      id="select-time" name="start_time">
             {% endif %}
             -
              {% if end_time %}
            <input type="text" class="form-control" placeholder="请选择结束时间" readonly
                   name="end_time" value="{{ end_time }}">
              {% else %}
                <input type="text" class="form-control" placeholder="请选择结束时间" readonly name="end_time">
              {% endif %}
           </div>
           <div class="form-group ml20 mt20">
             <label for="title">标题:</label>
             {% if title %}
               <input type="text" class="form-control" placeholder="请输入标题" id="title" name="title" value="{{ title }}">
               {% else %}
              <input type="text" class="form-control" placeholder="请输入标题" id="title" name="title">
             {% endif %}
    
           </div>
           <div class="form-group ml20 mt20">
             <label for="author">作者:</label>
             {% if author_name %}
               <input type="text" class="form-control" placeholder="请输入作者" id="author" name="author_name"
                      value="{{ author_name }}">
             {% else %}
               <input type="text" class="form-control" placeholder="请输入作者" id="author" name="author_name">
             {% endif %}
           </div>
           <div class="form-group ml20 mt20">
             <label for="tag">标签:</label>
             <select class="form-control" id="tag" name="tag_id">
               <option value="0">--请选择标签--</option>
               {% for one_tag in tags %}
    
                 {% if tag_id and one_tag.id == tag_id %}
                   <option value="{{ one_tag.id }}" selected>{{ one_tag.name }}</option>
                 {% else %}
                   <option value="{{ one_tag.id }}">{{ one_tag.name }}</option>
                 {% endif %}
    
               {% endfor %}
             </select>
           </div>
           <div class="form-group ml20 mt20">
             <button class="btn btn-primary">查询</button>
             <a href="#" class="btn btn-info ml20">清除查询</a>
           </div>
         </form>
       </div>
       <div class="box-body">
         <table class="table table-bordered table-hover">
           <thead>
           <tr>
             <th>标题</th>
             <th>作者</th>
             <th>标签</th>
             <th>发布时间</th>
             <th>操作</th>
           </tr>
           </thead>
           <tbody>
            {% for one_news in news_info %}
              <tr>
               <td><a href="{% url 'news:news_detail' one_news.id%}" target="_blank">{{ one_news.title }}</a></td>
               <td>{{ one_news.author.username }}</td>
               <td>{{ one_news.tag.name }}</td>
               <td>{{ one_news.update_time }}</td>
               <td>
                 <a href="{% url 'admin:news_edit' one_news.id %}" class="btn btn-xs btn-warning">编辑</a>
                 <a href="javascript:void (0);" class="btn btn-xs btn-danger btn-del" data-news-id="{{ one_news.id }}">删除</a>
               </td>
             </tr>
            {% endfor %}
    
    
           </tbody>
         </table>
       </div>
       <div class="box-footer">
         <span class="pull-left">第{{ current_page_num }}页/总共{{ total_page_num }}页</span>
         <nav class="pull-right">
           <!-- 分页 -->
           <ul class="pagination">
             <!-- 上一页 -->
             {% if news_info.has_previous %}
                <li><a href="?page={{ news_info.previous_page_number }}&{{ other_param }}">上一页</a></li>
               {% else %}
               <li class="disabled"><a href="javascript:void(0);">上一页</a></li>
             {% endif %}
           
              {% if left_has_more_page %}
                <li><a href="?page=1&{{ other_param }}">1</a></li>
                <li><a href="javascript:void(0);">...</a></li>
              {% endif %}
              <!-- 左边的页码 -->
              {% for left_page in left_pages %}
                <li><a href="?page={{ left_page }}&{{ other_param }}">{{ left_page }}</a></li>
              {% endfor %}
    
              <!-- 当前页面 -->
              {% if current_page_num %}
                <li class="active"><a href="?page={{ current_page_num }}&{{ other_param }}">{{ current_page_num }}</a></li>
              {% endif %}
              <!-- 右边的页面 -->
              {% for right_page in right_pages %}
                  <li><a href="?page={{ right_page }}&{{ other_param }}">{{ right_page }}</a></li>
              {% endfor %}
    
             {% if right_has_more_page %}
              <li><a href="javascript:void(0);">...</a></li>
                <li><a href="?page={{ total_page_num }}&{{ other_param }}">{{ total_page_num }}</a></li>
            {% endif %}
    
             <!-- 下一页 -->
              {% if news_info.has_next %}
                <li><a href="?page={{ news_info.next_page_number }}&{{ other_param }}">下一页</a></li>
                {% else %}
                <li class="disabled"><a href="javascript:void(0);">下一页</a></li>
              {% endif %}
    
           </ul>
         </nav>
       </div>
     </div>
    {% endblock %}
    
    {% block script %}
     <script src="{% static 'js/admin/news/bootstrap-datepicker.min.js' %}"></script>
     <script src="{% static 'js/admin/news/bootstrap-datepicker.zh-CN.min.js' %}"></script>
     <script src="{% static 'js/admin/news/news_manage.js' %}"></script>
    {% endblock %}
    
    // 创建static/js/admin/news/news_manage.js文件
    
    $(function () {
      let $startTime = $("input[name=start_time]");
      let $endTime = $("input[name=end_time]");
      const config = {
        // 自动关闭
        autoclose: true,
        // 日期格式
        format: 'yyyy/mm/dd',
        // 选择语言为中文
        language: 'zh-CN',
        // 优化样式
        showButtonPanel: true,
        // 高亮今天
        todayHighlight: true,
        // 是否在周行的左侧显示周数
        calendarWeeks: true,
        // 清除
        clearBtn: true,
        // 0 ~11  网站上线的时候
        startDate: new Date(2018, 10, 1),
        // 今天
        endDate: new Date(),
      };
      $startTime.datepicker(config);
      $endTime.datepicker(config);
    
      // 删除标签
      let $newsDel = $(".btn-del");  // 1. 获取删除按钮
      $newsDel.click(function () {   // 2. 点击触发事件
        let _this = this;
        let sNewsId = $(this).data('news-id');
        swal({
          title: "确定删除这篇文章吗?",
          text: "删除之后,将无法恢复!",
          type: "warning",
          showCancelButton: true,
          confirmButtonColor: "#DD6B55",
          confirmButtonText: "确定删除",
          cancelButtonText: "取消",
          closeOnConfirm: true,
          animation: 'slide-from-top',
        }, function () {
    
          $.ajax({
            // 请求地址
            url: "/admin/news/" + sNewsId + "/",  // url尾部需要添加/
            // 请求方式
            type: "DELETE",
            dataType: "json",
          })
            .done(function (res) {
              if (res.errno === "200") {
                // 更新标签成功
                message.showSuccess("标签删除成功");
                $(_this).parents('tr').remove();
              } else {
                swal({
                  title: res.errmsg,
                  type: "error",
                  timer: 1000,
                  showCancelButton: false,
                  showConfirmButton: false,
                })
              }
            })
            .fail(function () {
              message.showError('服务器超时,请重试!');
            });
        });
    
      });
    
    
      // get cookie using jQuery
      function getCookie(name) {
        let cookieValue = null;
        if (document.cookie && document.cookie !== '') {
          let cookies = document.cookie.split(';');
          for (let i = 0; i < cookies.length; i++) {
            let cookie = jQuery.trim(cookies[i]);
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
              cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
              break;
            }
          }
        }
        return cookieValue;
      }
    
      function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
      }
    
      // Setting the token on the AJAX request
      $.ajaxSetup({
        beforeSend: function (xhr, settings) {
          if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
          }
        }
      });
    
    });
    

    特定文章删除,更新,查看功能

    1.分析

    请求方法GET、DELETE、PUT
    url定义: 'news/<int:news_id>/'
    请求参数:url路径传参

    参数 类型 前端是否必须传 描述
    news_id 字符串 文章id
    # 后台文章管理
    class NewsEditView(PermissionRequiredMixin,View):
        """
        1.权限校验
        2.get -- 渲染需更新文章界面
        3.put -- 更新文章
        4.delete -- 删除文章
        """
        permission_required = ('news.change_news','news.delete_news', 'news.view_news')
        raise_exception = True
    
        def handle_no_permission(self):
            if self.request.method.lower() != 'get':
                return to_json_data(errno=Code.ROLEERR, errmsg='没有操作权限')
            else:
                return super(NewsEditView, self).handle_no_permission()
    
        def get(self, request, news_id):
            """
            1.校验文章是否存在
            2.获取数据
            3.渲染前端界面
            :param request:
            :param news_id:
            :return:
            """
            news = models.News.objects.filter(is_delete=False,id=news_id).first()
            if news:
                tags = models.Tag.objects.only('id','name').filter(is_delete=False)
                context = {
                    'tags':tags,
                    'news':news,
                }
                return render(request, 'admin/news/news_pub.html',context=context)
    
            else:
                raise Http404('需要更新的文章不存在')
    
        def delete(self,request, news_id):
            """
            1.校验文章是否存在
            2.删除数据
            3.返回前端 True/False
            :param request:
            :param news_id:
            :return:
            """
            news = models.News.objects.only('id').filter(id=news_id).first()
            if news:
                news.is_delete = True
                news.save(update_fields = ['is_delete'])
                return to_json_data(errmsg="文章删除成功")
            else:
                return to_json_data(errno=Code.PARAMERR, errmsg="需要删除的文章不存在")
    
        def put(self, request, news_id):
            """
            更新文章
            :param request:
            :param news_id:
            :return:
            """
            news = models.News.objects.filter(is_delete=False, id=news_id).first()
            if not news:
                return to_json_data(errno=Code.NODATA, errmsg='需要更新的文章不存在')
    
            json_data = request.body
            if not json_data:
                return to_json_data(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR])
            # 将json转化为dict
            dict_data = json.loads(json_data.decode('utf8'))
    
            form = forms.NewsPubForm(data=dict_data)
            if form.is_valid():
                news.title = form.cleaned_data.get('title')
                news.digest = form.cleaned_data.get('digest')
                news.content = form.cleaned_data.get('content')
                news.image_url = form.cleaned_data.get('image_url')
                news.tag = form.cleaned_data.get('tag')
                news.save()
                return to_json_data(errmsg='文章更新成功')
            else:
                # 定义一个错误信息列表
                err_msg_list = []
                for item in form.errors.get_json_data().values():
                    err_msg_list.append(item[0].get('message'))
                err_msg_str = '/'.join(err_msg_list)  # 拼接错误信息为一个字符串
    
                return to_json_data(errno=Code.PARAMERR, errmsg=err_msg_str)
    

    文章发布功能

    1.分析

    请求方法GET、POST
    url定义: 'news/pub/'
    请求参数:body

    参数 类型 前端是否必须传 描述
    news_id 字符串 文章id
    # 后台文章发布
    class NewsPubView(PermissionRequiredMixin, View):
        """
    
        """
        permission_required = ('news.add_news','news.view_news')
        raise_exception = True
    
        def handle_no_permission(self):
            if self.request.method.lower() != 'get':
                return to_json_data(errno=Code.ROLEERR, errmsg='没有操作权限')
            else:
                return super(NewsPubView, self).handle_no_permission()
    
        def get(self,request):
            """
            1.获取文章标签
            2.渲染文章发布页
            :param request:
            :return:
            """
            tags = models.Tag.objects.only('id','name').filter(is_delete=False)
    
            return render(request, 'admin/news/news_pub.html', locals())
        def post(self,request):
            """
            需要将文章保存到数据库
            新增文章
            1.从前端获取参数
            2.校验参数
            3.把数据保存到数据库
            4.返回给前端执行结果 -- ok/false
            :param request:
            :return:
            """
            json_data = request.body
            if not json_data:
                return to_json_data(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR])
            # 将json转化为dict
            dict_data = json.loads(json_data.decode('utf8'))
    
            form = forms.NewsPubForm(data=dict_data)
            if form.is_valid():
                # 只有继承model.Form,会提供一个save方法,利用表单对象.save直接保存,并写入数据库
                # commit=true (默认)直接保存并写入数据库 news_instance = form.save(commit=False),先不写入数据库,在添加其他数据信息后在调用news_instance.save(),保存并写入数据库
                news_instance = form.save(commit=False)
                # 如果没有设置作者信息不会保存,因为外键允许为空,但是创建没错,打开其他界面会出错,
                news_instance.author_id = request.user.id
                # news_instance.author= request.user 传入实例对象也可以
                news_instance.save()
    
                # 若不用上述方法
                # n = model.News(**form.cleaned_data)
                # n.title = form.cleaned_data('title')
                # n.save()
                return to_json_data(errmsg='文章创建成功')
            else:
                # 定义一个错误信息列表
                err_msg_list = []
                for item in form.errors.get_json_data().values():
                    err_msg_list.append(item[0].get('message'))
                err_msg_str = '/'.join(err_msg_list)  # 拼接错误信息为一个字符串
    
                return to_json_data(errno=Code.PARAMERR, errmsg=err_msg_str)
    
    # 创建apps/admin/forms.py文件:
    
    from django import forms
    from news.models import News, Tag
    
    
    class NewsPubForm(forms.ModelForm):
        """
        """
       # 创建模型时,允许为空,所以需要重写,定义不能为空
        image_url = forms.URLField(label='文章图片url',
                                   error_messages={"required": "文章图片url不能为空"})
        # 限制tag_id范围,tag是外键指定的多个值的多选框,所以定义的字段类型为ModelChoiceField
        tag = forms.ModelChoiceField(queryset=Tag.objects.only('id').filter(is_delete=False),
                                     error_messages={"required": "文章标签id不能为空", "invalid_choice": "文章标签id不存在", }
                                     )
    
        class Meta: # 元数据信息
            # 指定那个数据库模型来创建表单
            model = News  # 与数据库模型关联
            # 需要关联的字段
            # exclude 排除
            # 此处tag 指的是文章分类的实例对象,并不是tag_id,会出问题
            fields = ['title', 'digest', 'content', 'image_url', 'tag']
            # 自定义报错信息(由于定义模型没有写,所以在此处写)
            error_messages = {
                'title': {
                    'max_length': "文章标题长度不能超过150",
                    'min_length': "文章标题长度大于1",
                    # 传入字符串为空和,传入为空格是有区别的
                    'required': '文章标题不能为空',
                },
                'digest': {
                    'max_length': "文章摘要长度不能超过200",
                    'min_length': "文章标题长度大于1",
                    'required': '文章摘要不能为空',
                },
                'content': {
                    'required': '文章内容不能为空',
                },
            }
       
    
    from django.db import models
    from utils._Models._models import BaseModel
    # 增加最小长度校验器
    from django.core.validators import MinLengthValidator
    
    class News(BaseModel):
        """
        super: create_time update_time is_delete
        built-in: title digest content clicks image_url
        ForeignKey: tag  author
        """
        # MinLengthValidator(1) 长度不小于1
        title = models.CharField(max_length=150, validators=[MinLengthValidator(1),], verbose_name="标题", help_text="标题")
        digest = models.CharField(max_length=200, validators=[MinLengthValidator(1),], verbose_name="摘要", help_text="摘要")
        content = models.TextField(verbose_name="新闻内容", help_text="新闻内容")
        clicks = models.IntegerField(default=0, verbose_name="点击量", help_text="点击量")
        image_url = models.URLField(default="", verbose_name="图片url", help_text="图片url", )
    
        tag = models.ForeignKey('Tag',on_delete=models.SET_NULL, null=True)
        author = models.ForeignKey('users.Users', on_delete=models.SET_NULL, null=True)
    
        class Meta:
            ordering = ['-update_time', '-id']
            db_table = 'tb_news'
            verbose_name = '新闻'
            verbose_name_plural = verbose_name
    
        def __str__(self):
            return self.title
    
    • fastfds 功能实现
    # 图片上传至FastDFS服务器功能实现
    
    class NewsUploadImage(PermissionRequiredMixin, View):
        """
        """
        permission_required = ('news.add_news',)
    
        def handle_no_permission(self):
            return to_json_data(errno=Code.ROLEERR, errmsg='没有上传图片的权限')
    
        def post(self, request):
            # request.FILES.get('image_file') 获取图片对象
            image_file = request.FILES.get('image_file')
            if not image_file:
                logger.info('从前端获取图片失败')
                return to_json_data(errno=Code.NODATA, errmsg='从前端获取图片失败')
            # 文件类型有content_type这个属性
            if image_file.content_type not in ('image/jpeg', 'image/png', 'image/gif'):
                return to_json_data(errno=Code.DATAERR, errmsg='不能上传非图片文件')
    
            # image_file.name 文件名
            try:
                image_ext_name = image_file.name.split('.')[-1]
            except Exception as e:
                logger.info('图片拓展名异常:{}'.format(e))
                image_ext_name = 'jpg'
    
            try:
                # 前端传的是文件 需要通过upload_by_buffer() 方法,
                FDFS_Client = Fdfs_client('utils/fastdfs/client.conf')
                upload_res = FDFS_Client.upload_by_buffer(image_file.read(), file_ext_name=image_ext_name)
            except Exception as e:
                logger.error('图片上传出现异常:{}'.format(e))
                return to_json_data(errno=Code.UNKOWNERR, errmsg='图片上传异常')
            else:
                # 此处有个点Upload successed.
                if upload_res.get('Status') != 'Upload successed.':
                    logger.info('图片上传到FastDFS服务器失败')
                    return to_json_data(Code.UNKOWNERR, errmsg='图片上传到服务器失败')
                else:
                    image_name = upload_res.get('Remote file_id')
                    # from django.conf import settings
                    # 定义域名 FASTDFS_SERVER_DOMAIN = "http://127.0.0.1:8888/",配置文件里的
                    image_url = settings.FASTDFS_SERVER_DOMAIN + image_name
                    return to_json_data(data={'image_url': image_url}, errmsg='图片上传成功')
    
    # utils。fastdfs.fdfs.py
    from fdfs_client.client import Fdfs_client
    
    # 指定fdfs客户端配置文件所在路径
    FDFS_Client = Fdfs_client('utils/fastdfs/client.conf')
    
    if __name__ == '__main__':
        try:
            # 此处指定图片路径上传的,知道文件后缀 upload_by_filename()
            ret = FDFS_Client.upload_by_filename('media/captcha.png')
        except Exception as e:
            print("fdfs测试异常:{}".format(e))
        else:
            print(ret)
    
    • 七牛云 功能实现
    # 在虚拟机中安装七牛云所需模块
    pip install qiniu
    
    # 创建utils/secrets/qiniu_secret_info.py文件
    
    # 从七牛云"个人中心>密钥管理"中获取自己的 Access Key 和 Secret Key
    
    QI_NIU_ACCESS_KEY = '你自己七牛云上的AK'
    QI_NIU_SECRET_KEY = '你自己七牛云上的SK'
    QI_NIU_BUCKET_NAME = '你自己在七牛云上创建的存储空间名'
    
    # 并将qiniu_secret_info.py添加到.gitignore中,让该文件不上传
    qiniu_secret_info.py
    
    import qiniu
    
    from utils.SECRET import qiniu_secret_info
    
    
    class UploadToken(PermissionRequiredMixin, View):
        """
        """
        permission_required = ('news.add_news', 'news.view_news')
    
        def handle_no_permission(self):
            return to_json_data(errno=Code.ROLEERR, errmsg='没有相关权限')
    
        def get(self, request):
            access_key = qiniu_secret_info.QI_NIU_ACCESS_KEY
            secret_key = qiniu_secret_info.QI_NIU_SECRET_KEY
            bucket_name = qiniu_secret_info.QI_NIU_BUCKET_NAME
            # 构建鉴权对象
            q = qiniu.Auth(access_key, secret_key)
            token = q.upload_token(bucket_name)
            # 最好直接返回原生js
            return JsonResponse({"uptoken": token})
    
    # 在apps/admin/urls.py中添加如下路由:
    
    urlpatterns = [
        path('news/<int:news_id>/', views.NewsEditView.as_view(), name='news_edit'),
        path('news/pub/', views.NewsPubView.as_view(), name='news_pub'),
        path('news/images/', views.NewsUploadImage.as_view(), name='upload_image'),
        path('token/', views.UploadToken.as_view(), name='upload_token'),  # 七牛云上传图片需要调用token
        
    
    • 前端代码
    <!-- 创建templates/admin/news/news_pub.html文件 -->
    
    
    {% extends 'admin/base/base.html' %}
    
    
    {% block title %}
      文章发布页
    {% endblock %}
    
    {% block content_header %}
      文章发布
    {% endblock %}
    
    {% block header_option_desc %}
      书是人类进步的阶梯
    {% endblock %}
    
    {% block content %}
    <div class="row">
      <div class="col-md-12 col-xs-12 col-sm-12">
        <div class="box box-primary">
          <div class="box-body">
            <div class="form-group">
              <label for="news-title">文章标题</label>
              {% if news %}
                <input type="text" class="form-control" id="news-title" name="news-title" placeholder="请输入文章标题"
                       value="{{ news.title }}">
              {% else %}
                <input type="text" class="form-control" id="news-title" name="news-title" placeholder="请输入文章标题" autofocus>
              {% endif %}
            </div>
            <div class="form-group">
              <label for="news-desc">文章摘要</label>
              {% if news %}
                <textarea name="news-desc" id="news-desc" placeholder="请输入新闻描述" class="form-control"
                          style="height: 8rem; resize: none;">{{ news.digest }}</textarea>
              {% else %}
                <textarea name="news-desc" id="news-desc" placeholder="请输入新闻描述" class="form-control"
                          style="height: 8rem; resize: none;"></textarea>
              {% endif %}
            </div>
            <div class="form-group">
              <label for="news-category">文章分类</label>
              <select name="news-category" id="news-category" class="form-control">
                <option value="0">-- 请选择文章分类 --</option>
                {% for one_tag in tags %}
                  <!-- 传tag_id到后台 -->
                  {% if news and one_tag == news.tag %}
                    <option value="{{ one_tag.id }}" selected>{{ one_tag.name }}</option>
                  {% else %}
                    <option value="{{ one_tag.id }}">{{ one_tag.name }}</option>
                  {% endif %}
                {% endfor %}
              </select>
            </div>
            <div class="form-group" id="container">
              <label for="news-thumbnail-url">文章缩略图</label>
              <div class="input-group">
                {% if news %}
                <input type="text" class="form-control" id="news-thumbnail-url" name="news-thumbnail-url"
                       placeholder="请上传图片或输入文章缩略图地址" value="{{ news.image_url }}">
                  {% else %}
                  <input type="text" class="form-control" id="news-thumbnail-url" name="news-thumbnail-url"
                       placeholder="请上传图片或输入文章缩略图地址">
                {% endif %}
    
                <div class="input-group-btn">
                  <label class="btn btn-default btn-file">
                    上传至服务器 <input type="file" id="upload-news-thumbnail">
                  </label>
                  <button class="btn btn-info" id="upload-btn">上传至七牛云</button>
                </div>
              </div>
            </div>
            <div class="form-group">
              <div class="progress" style="display: none">
                <div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0;">0%</div>
              </div>
            </div>
            <div class="form-group">
              <label for="news-content">文章内容</label>
              {% if news %}
                <div id="news-content"></div>
                <script>
                  window.onload = function () {
                    window.editor.txt.html('{{ news.content|safe }}')
                  }
                </script>
              {% else %}
                <div id="news-content"></div>
              {% endif %}
            </div>
          </div>
          <div class="box-footer">
              {% if news %}
                <a href="javascript:void (0);" class="btn btn-primary pull-right" id="btn-pub-news" data-news-id="{{ news.id }}">更新文章 </a>
              {% else %}
               <a href="javascript:void (0);" class="btn btn-primary pull-right" id="btn-pub-news">发布文章 </a>
              {% endif %}
          </div>
        </div>
      </div>
    </div>
    {% endblock %}
    
    {% block script %}
      <script src="{% static 'js/admin/news/wangEditor.min.js' %}"></script>
      {# 导入七牛云需要的3个js文件 #}
      <script src="https://cdn.bootcss.com/plupload/2.1.9/moxie.min.js"></script>
      <script src="https://cdn.bootcss.com/plupload/2.1.9/plupload.dev.js"></script>
      {# 这3个js文件有依赖关系,qiniu.min.js需要放在后面 #}
      <script src="https://cdn.bootcss.com/qiniu-js/1.0.17.1/qiniu.min.js"></script>
      <script src="{% static 'js/admin/base/fqiniu.js' %}"></script>
      <script src="{% static 'js/admin/news/news_pub.js' %}"></script>
    {% endblock %}
    
    
    // 创建static/js/admin/news/news_pub.js文件
    
    $(function () {
      let $e = window.wangEditor;
      window.editor = new $e('#news-content');
      window.editor.create();
    
      // 获取缩略图输入框元素
      let $thumbnailUrl = $("#news-thumbnail-url");
    
      // ================== 上传图片文件至服务器 ================
      let $upload_to_server = $("#upload-news-thumbnail");
      $upload_to_server.change(function () {
        let file = this.files[0];   // 获取文件
        let oFormData = new FormData();  // 创建一个 FormData
        oFormData.append("image_file", file); // 把文件添加进去
        // 发送请求
        $.ajax({
          url: "/admin/news/images/",
          method: "POST",
          data: oFormData,
          processData: false,   // 定义文件的传输
          contentType: false,
        })
          .done(function (res) {
            if (res.errno === "0") {
              // 更新标签成功
              message.showSuccess("图片上传成功");
              let sImageUrl = res["data"]["image_url"];
              // console.log(thumbnailUrl);
              $thumbnailUrl.val('');
              $thumbnailUrl.val(sImageUrl);
            } else {
              message.showError(res.errmsg)
            }
          })
          .fail(function () {
            message.showError('服务器超时,请重试!');
          });
    
      });
    
    
      // ================== 上传至七牛(云存储平台) ================
      let $progressBar = $(".progress-bar");
      QINIU.upload({
        "domain": "http://pl3yncr1e.bkt.clouddn.com/",  // 七牛空间域名
        "uptoken_url": "/admin/token/",  // 后台返回 token的地址
        "browse_btn": "upload-btn",     // 按钮
        "success": function (up, file, info) {   // 成功
          let domain = up.getOption('domain');
          let res = JSON.parse(info);
          let filePath = domain + res.key;
          console.log(filePath);
          $thumbnailUrl.val('');
          $thumbnailUrl.val(filePath);
        },
        "error": function (up, err, errTip) {
          // console.log('error');
          console.log(up);
          console.log(err);
          console.log(errTip);
          // console.log('error');
          message.showError(errTip);
        },
        "progress": function (up, file) {
          let percent = file.percent;
          $progressBar.parent().css("display", 'block');
          $progressBar.css("width", percent + '%');
          $progressBar.text(parseInt(percent) + '%');
        },
        "complete": function () {
          $progressBar.parent().css("display", 'none');
          $progressBar.css("width", '0%');
          $progressBar.text('0%');
        }
      });
    
    
      // ================== 发布文章 ================
      let $newsBtn = $("#btn-pub-news");
      $newsBtn.click(function () {
        // 判断文章标题是否为空
        let sTitle = $("#news-title").val();  // 获取文章标题
        if (!sTitle) {
            message.showError('请填写文章标题!');
            return
        }
        // 判断文章摘要是否为空
        let sDesc = $("#news-desc").val();  // 获取文章摘要
        if (!sDesc) {
            message.showError('请填写文章摘要!');
            return
        }
    
        let sTagId = $("#news-category").val();
        if (!sTagId || sTagId === '0') {
          message.showError('请选择文章标签');
          return
        }
    
        let sThumbnailUrl = $thumbnailUrl.val();
        if (!sThumbnailUrl) {
          message.showError('请上传文章缩略图');
          return
        }
    
        let sContentHtml = window.editor.txt.html();
        if (!sContentHtml || sContentHtml === '<p><br></p>') {
            message.showError('请填写文章内容!');
            return
        }
    
        // 获取news_id 存在表示更新 不存在表示发表
        let newsId = $(this).data("news-id");
        let url = newsId ? '/admin/news/' + newsId + '/' : '/admin/news/pub/';
        let data = {
          "title": sTitle,
          "digest": sDesc,
          "tag": sTagId,
          "image_url": sThumbnailUrl,
          "content": sContentHtml,
        };
    
        $.ajax({
          // 请求地址
          url: url,
          // 请求方式
          type: newsId ? 'PUT' : 'POST',
          data: JSON.stringify(data),
          // 请求内容的数据类型(前端发给后端的格式)
          contentType: "application/json; charset=utf-8",
          // 响应数据的格式(后端返回给前端的格式)
          dataType: "json",
        })
          .done(function (res) {
            if (res.errno === "0") {
              if (newsId) {
                fAlert.alertNewsSuccessCallback("文章更新成功", '跳到后台首页', function () {
                  window.location.href = '/admin/'
                });
    
              } else {
                fAlert.alertNewsSuccessCallback("文章发表成功", '跳到后台首页', function () {
                  window.location.href = '/admin/'
                });
              }
            } else {
              fAlert.alertErrorToast(res.errmsg);
            }
          })
          .fail(function () {
            message.showError('服务器超时,请重试!');
          });
      });
    
    
      // get cookie using jQuery
      function getCookie(name) {
        let cookieValue = null;
        if (document.cookie && document.cookie !== '') {
          let cookies = document.cookie.split(';');
          for (let i = 0; i < cookies.length; i++) {
            let cookie = jQuery.trim(cookies[i]);
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
              cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
              break;
            }
          }
        }
        return cookieValue;
      }
    
      function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
      }
    
      // Setting the token on the AJAX request
      $.ajaxSetup({
        beforeSend: function (xhr, settings) {
          if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
          }
        }
      });
    
    });
    
    

    最终展示

    • 查询文章


      image.png
      image.png
    • 发布文章


      image.png
      image.png
    • 更新文章


      image.png
    • 删除文章


      image.png
      image.png

    相关文章

      网友评论

          本文标题:后台管理站点 -- 3.文章管理

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