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

文章搜索功能

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容

  • 文/泡泡圈漫评团 一梦心汐 一天,马俊熙悄悄躲在角落里捂着嘴阴笑:“嘿嘿嘿,明天就是女神的生日了,我老马一定要好好...
    泡泡国漫漫研社阅读 347评论 0 1
  • 如果不知道我们这里要实现什么要的效果,可以先参考前面的博客,这篇主要记录我们实现这样封装的思路和记录下一些有用的点...
    小人物灌篮阅读 880评论 1 1
  • 不需要去担心,尽管好好地复习,然后好好地考试。 好好地去完成这一个梦想。 最后的时间,珍惜。 不知道为什么,体测完...
    减肥的女孩阅读 309评论 0 1
  • 2018年1月23日 周二 阴天 文/佼佼者脾气很差 长沙篇―五一广场―橘子洲头―火宫殿―坡子街―太平街―...
    佼佼者脾气很差阅读 897评论 59 17