通过图片网址保存图片(增删+缩略图)

功能需求说明:用户提交图片网址,然后由程序将该图片下载并保存到指定的位置。

注:提前安装图片支持模块Pillow(pip install Pillow)

建立数据模型,models.py:
from django.db import models
from django.contrib.auth.models import User
from slugify import slugify

class Image(models.Model):
    user = models.ForeignKey(User,on_delete=models.CASCADE,related_name='images')
    title = models.CharField(max_length=300)
    url = models.URLField()       # 用来保存网络图片的URL,URLField继承了CharField,默认最大长度是200
    slug = models.SlugField(max_length=500,blank=True)          # SlugField(),如果没有指定 max_length ,Django 会默认字段长度为50。
    description = models.TextField(blank=True)
    created = models.DateField(auto_now_add=True,db_index=True)        # db_index=True 用数据库的此字段作为索引
    image = models.ImageField(upload_to='images/%Y/%m/%D')

    # ImageField,Django 会自动检测所提交的是否是图片,不需要用户检验方法

    def __str__(self):
        return self.title

    def save(self,*args,**kwargs):          # 重写父类save()方法,slug通常被用作网址的一部分
        self.slug = slugify(self.title)
        super(Image,self).save(*args,**kwargs)
编写表单类,forms.py:
from django import forms
from django.core.files.base import ContentFile
from slugify import slugify
from urllib import request
from .models import Image

class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ['title','url','description']

    def clean_url(self):   
        url = self.cleaned_data['url']
        valid_extensions = ['jpg','jpeg','png','webp']    # 规定图片的拓展名
        extension = url.rsplit('.',1)[1].lower()    # 从得到的图片网址中分解出其拓展名
        if extension not in valid_extensions:
            raise forms.ValidationError('The given Url does not match valid image extension.')
        return url      # 别忘了return!!!

    def save(self,force_insert=False,force_update=False,commit=True):    # 重写save()方法
        image = super(ImageForm,self).save(commit=False)  # commit=False 表示实例虽然被建立,但并没有保存数据
        image_url = self.cleaned_data['url']
        image_name = '{0}.{1}'.format(slugify(image.title),image_url.rsplit('.',1)[1].lower())  # 给图片文件命名
        response = request.urlopen(image_url)    # request是urllib的一部分,request.urlopen(image_url),以GET方式访问该图片地址
        image.image.save(image_name,ContentFile(response.read()),save=False)  #  将上面的语句返回的结果保存到本地,并按照约定的名称给图片文件命名
        if commit:
            image.save()
        return image   # 别忘了return!!!
   

以上代码解析:
clean_<fieldname> 处理某个字段值。
clean_url(),规定图片拓展名范围,如果不在此范围内,就报错。

request.urlopen(image_url):
这里的request 不是视图函数中的参数request,而是Python标准库Urllib中的一部分。request.urlopen(image_url)的作用是以GET方式访问该图片地址,模拟用户把网址输入到浏览器的地址栏,之后在浏览器中返回相应的内容,这里同样返回一个Request对象。通过该对象得到所访问URL的数据(图片的ASCII字符串),通过response.read()可以得到此数据。

ContentFile:
ContentFile是File类的子类,接收字符串为参数。

image.image.save():
第一个image是实例对象,第二个image是属性/字段

官方样式说明:
FieldFile.save(name, content, save=True)
Note that the content argument should be an instance of django.core.files.File, not Python’s built-in file object. You can construct a File from an existing Python file object like this:

from django.core.files import File
f = open('/path/to/hello.world')
myfile = File(f)
或者
from django.core.files.base import ContentFile
myfile = ContentFile("hello world")

编写视图函数,views.py:

两个函数,一个是接收并处理前端提交的数据,一个用来展示图片

from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse

from .models import Image
from .forms import ImageForm

# 处理前端表单提交的图片URL及相关信息
@login_required(login_url='/account/login/')
@csrf_exempt
@require_POST
def upload_image(request):
    form = ImageForm(data=request.POST)
    if form.is_valid():
        try:
            new_item = form.save(commit=False)
            new_item.user = request.user
            new_item.save()
            return JsonResponse({'status':'1'})   # 返回JSON数据。这里可以改用字符串【return HttpResponse('1')】
        except:
            return JsonResponse({'status':'0'})

# 展示图片
@login_required(login_url='/account/login/')
def list_image(request):
    images = Image.objects.filter(user=request.user)
    return render(request,'image/list_images.html',locals())
创建模板文件,list_images.html:
{% extends "article/base.html" %}
{% load static %}

{% block title %}images{% endblock %}

{% block content %}
<div>
    <button type="button" class="btn btn-primary btn-lg btn-block" onclick="addImage()">添加图片</button>
    <div style="margin-top:10px;">
    <table class="table table-hover">
        <tr>
            <td>序号</td>
            <td>标题</td>
            <td>图片</td>
            <td>操作</td>
        </tr>
        {% for image in images %}
        <tr>
            <td>{{ forloop.counter }}</td>
            <td>{{ image.title }}</td>
            <td>{{image.image}}</td>

            <td><a name="delete" href="javascript:" onclick="del_image(this, {{ image.id }})"><span class="glyphicon glyphicon-trash" style="margin-left:20px;"></span></a></td>
        </tr>
        {% empty %}
        <p>还没有图片,请点击上面的按钮添加图片</p>
        {% endfor %}
    </table>
    </div>
</div>
<script src="{% static 'js/jquery.js' %}"></script>
<script src="{% static 'js/layer.js' %}"></script>
<script type="text/javascript">
    function addImage(){
        var index=layer.open({
          type: 1,
          skin: 'layui-layer-demo',
          closeBtn: 0,
          shift: 2,
          shadeClose: true,
          title: "Add Image",
          area: ['600px', '440px'],
          content: "<div style='padding:20px'><p>请新增扩展名是.jpg或.png的网上照片地址</p><form><div class='form-group'><label for='phototitle' class='col-sm-2 control-label'>标题</label><div class='col-sm-10'><input id='phototitle' type='text' class='form-control' style='margin-bottom:5px'></div></div><div class='form-group'><label for='photourl' class='col-sm-2 control-label'>地址</label><div class='col-sm-10'><input id='photourl' style='margin-bottom:5px' type='text' class='form-control'></div></div><div class='form-group'><label for='description' class='col-sm-2 control-label'>描述</label><div class='col-sm-10'><textarea class='form-control' style='margin-bottom:5px' row='2' id='photodescription'></textarea></div></div><div class='form-group'><div class='col-sm-offset-2 col-sm-10'><input id='newphoto' type='button' class='btn btn-default' value='Add It'></div></div></form></div>",
          success: function(){
            $("#newphoto").on('click', function(){
              var title = $("#phototitle").val();
              var url = $("#photourl").val();
              var description = $("#photodescription").val();
              var photo = {"title":title, "url":url, "description":description};
              $.ajax({
                url: '{% url "image:upload_image" %}',
                type: "POST",
                data: photo,
                success: function(e){
                  var status = e['status']
                  if(status =="1"){
                    layer.close(index);
                    window.location.reload();
                  } else {
                    layer.msg("图片无法获取,请更换图片");
                  }
                },
              });
            });
          },
        });
    }
</script>
{% endblock %}

layer.js是弹出层插件

实现删除功能:

在views.py中添加删除函数

# 删除图片
@login_required(login_url='/account/login/')
@csrf_exempt
@require_POST
def del_image(request):
    image_id = request.POST['image_id']
    try:
        image = Image.objects.get(id=image_id)
        image.delete()
        return JsonResponse({'status':'1'})
    except:
        return JsonResponse({'status':'2'})

在list_images.html中添加JS代码

function del_image(the, image_id){
  var image_title = $(the).parents("tr").children("td").eq(1).text();
  layer.open({
    type: 1,
    skin: "layui-layer-rim",
    area: ["400px", "200px"],
    title: "删除图片",
    content: '<div class="text-center" style="margin-top:20px"><p>是否确定删除《'+image_title+'》</p></div>',
    btn:['确定', '取消'],
    yes: function(){
      $.ajax({
        url: '{% url "image:del_image" %}',
        type:"POST",
        data: {"image_id":image_id},
        success: function(e){
          var status = e['status']
          if(status=="1"){
            parent.location.reload();
            layer.msg("has been deleted.");
          }else{
            layer.msg("删除失败");
          }
          
        },
      })  
    },
  });
}

注:这种删除,只是从本项目的数据库中删除了,并没有从磁盘上删除。
另外,在模板文件list_images.html中,<td>{{image.image}}</td>不显示图片,而是图片路径地址,如:images/2018/10/10/18/18/chai-quan.jpeg
要显示图片的话,要用image.image.url,把上面的代码换成:

<td><img src="{{ image.image.url }}" width="100px" width="100px"></td>

前提是已经配置好了media和url

# settings.py
MEDIA_URL = '/media/'   # 设置URL映射中的路径
MEDIA_ROOT = os.path.join(BASE_DIR,'media/')   # 声明相对项目根目录的文件保存地址
# views.py
from django.contrib import admin
from django.urls import path,include
from django.conf import settings
from django.conf.urls.static import static

#  为每个上传的静态文件配置URL路径
urlpatterns = [
    path('admin/', admin.site.urls),
    path('image/',include('image.urls',namespace='image')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

注:代码更改后,展示的图片虽然已经进行了长宽的设置,但仅仅是在显示的时候对图片尺寸进行了更改,实际上仍要读取原图。

缩略图:

使用应用 sorl-thumbnail
官网文档:https://sorl-thumbnail.readthedocs.io/en/latest/

pip install sorl-thumbnail

在settings.py中注册应用sorl.thumbnail

INSTALLED_APPS = [
      # 其他应用
      'sorl.thumbnail',  # 缩略图
]
# 注册后,别忘了迁移数据!!!

修改模板文件list_images.html

# 在文件第一行导入
{% load thumbnail %}    

# 修改原来用来显示图片的代码
{% thumbnail image.image "100x100" crop="center" as im %}
     <td><img src="{{im.url}}" width="{{ im.width }}" height="{{ im.height }}"></img></td>
{% endthumbnail %}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容