Contents:

Django图片系统


1465 浏览 5 years, 3 months

1 图片上传

版权声明: 转载请注明出处 http://www.codingsoho.com/

本文以头像为例,来讲解如何在django中添加图片

模型定义

首先定义一个简单的model,只包含图片信息,图片字段用到了ImageField, 具体定义可参考 https://docs.djangoproject.com/en/1.8/ref/forms/fields/#django.forms.ImageField

模型定义如下

class Avatar(models.Model):
    image = models.ImageField(_('Image'), blank=False, null=False, upload_to=upload_avatar)

    class Meta:
        verbose_name = _("avatar")
        verbose_name_plural = _("avatar")

其中,upload_to指定图片上传后存放的位置,参数为instancefilename,你可以根据需要上传到指定位置,这儿为了简化用了一个固定路径。

def upload_avatar(instance, filename):
    return 'upload/avatar/' + filename

后台上传

我们先完成一个简单的操作,直接在后台admin进行图片上传

图片的存储涉及到两个存储地址和访问地址,我们在setting里面指定了两个变量MEDIA_URLMEDIA_ROOT,分别是访问相对根路径和存储相对根路径

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "static_in_env", "media_root")

我们上传一个图像,名为Tom-Hanks.jpg, 从admin后台可以看到,当前对象的上传路径为upload/avatar/Tom-Hanks.jpg

其访问链接 http://127.0.0.1:8000/media/upload/avatar/Tom-Hanks.jpg

本地存放路径为 Project Folder\static_in_env\media_root\upload\avatarTom-Hanks.jpg

有两个地方要注意

1.调试模式下,要在网址内访问静态文件,需要在urls.py添加下面两句,否则会提示url找不到

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

2.如果同一路径保存相同文件名时,不会覆盖,会自动生成一个新的文件名(在文件名后面添加一串随机数)

以上面图片为例,如果同名文件再次保存,那么会产生新的文件 upload/avatar/Tom-Hanks_Jzb8Phm.jpg, 后面这一串码是随机的

前台上传

本章内容可参考 https://docs.djangoproject.com/en/1.8/topics/http/file-uploads/#handling-uploaded-files-with-a-model

Handling uploaded files with a model¶
If you’re saving a file on a Model with a FileField, using a ModelForm makes this process much easier. The file object will be saved to the location specified by the upload_to argument of the corresponding FileField when calling form.save():

我们需要在前端定义一个表单,然后通过这个表单完成上传,示例代码如下

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import ModelFormWithFileField

def upload_file(request):
    if request.method == 'POST':
        form = ModelFormWithFileField(request.POST, request.FILES)
        if form.is_valid():
            # file is saved
            form.save()
            return HttpResponseRedirect('/success/url/')
    else:
        form = ModelFormWithFileField()
    return render(request, 'upload.html', {'form': form})

接下来,我们分下面几步来完成头像图片的前台上传。

1.定义表单 ModelForm

这个表单专门针对avatar这个model,元类里的class即为Avatar

class AvatarForm(forms.ModelForm):
    excludes = []

    class Meta:
        model = Avatar
2.添加模板

模板文件avatar_upload.html

在表单中加入enctype="multipart/form-data;如果不加,得不到图片信息,只有路径。
FILES是个字典,它包含每个FileField的键 (或者ImageFieldFileField的子类)。这样的话就可以用request.FILES['File']来存放表单中的这些数据了。

注意: request.FILES只有在请求方法为POST,并且发送请求的<form>拥有enctype="multipart/form-data属性时,才会包含数据。否则request.FILES为空

{% block content %}
<form method='POST' enctype="multipart/form-data" action=''>{% csrf_token %}
 {{ form|crispy }}
 <input class = 'btn btn-primary' type='submit' value='Submit' />
 </form>
 {% endblock %}
3.渲染和处理表单

参考前面的代码,我们使用下面的function base view来处理

def avatar_upload(request):
    if request.method == 'POST':
        form = AvatarForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect('/')
    else:
        form = AvatarForm()
    return render(request, 'avatar_upload.html', {'form': form})

或者我们也可以用django的generic view库来处理

CreateView
class AvatarCreateView(CreateView):
    template_name = 'avatar_upload.html'
    form_class = AvatarForm
    success_url = "/"

以上代码就足以完成图片上传了。

图片的上传处理在CreateView里,我们也可以自己处理图片上传功能,重写函数来实现自己定制化的功能。

class AvatarCreateView(CreateView):
    template_name = 'avatar_upload.html'
    form_class = AvatarForm
    success_url = "/"
    def get_context_data(self, *args, **kwargs):
        context = super(AvatarCreateView, self).get_context_data(*args, **kwargs)
        context["image_form"] = AvatarForm()
        return context
    def post(self, request, *args, **kwargs):        
        imageForm = self.form_class(request.POST, request.FILES)
        if imageForm.is_valid():
            image = imageForm.save(commit=False)
            # add your code here
            image.save()
            return HttpResponseRedirect('/')
        else:
            print imageForm.errors
        return super(AvatarCreateView, self).post(request, *args, **kwargs)

甚至,我们可以换个图片处理的方式,不调用django自带的图片存储接口

这个在需要调用第三方存储容器时会用到,比如SAE,这儿不做详细展开

def get_file_path(filename):
    if filename:        
        photoname = os.path.join("upload", "avatar", filename)
        photodir = os.path.join(settings.MEDIA_ROOT, "upload", "avatar")
        if not os.path.exists(photodir):
            os.makedirs(photodir)
        photopath = os.path.join(settings.MEDIA_ROOT, photoname)
        return photoname, photopath
    return None, None

创建视图处理如下:

class AvatarCreateView(CreateView):
    template_name = 'avatar_upload.html'
    form_class = AvatarForm
    success_url = "/"
    def get_context_data(self, *args, **kwargs):
        context = super(AvatarCreateView, self).get_context_data(*args, **kwargs)
        context["image_form"] = AvatarForm()
        return context
    def post(self, request, *args, **kwargs):        
        filename=request.FILES['image']
        print (filename, 0)
        photoname, photopath = get_file_path(filename.name)
        if photopath:
            img=Image.open(filename)
            img.save(photopath)
            Avatar.objects.create(image = photoname)
            return HttpResponseRedirect('/')
        return super(AvatarCreateView, self).post(request, *args, **kwargs)   
UpdateView

上面主要讲的是创建一个新的图片,接下来讲讲怎么去更新一个图片,这儿我们会使用UpdateView

跟创建视图相比,因为对象已经存在,需要在Form初始化时传递相应的参数data

本例中,data = object.image

class AvatarUpdateView(UpdateView):
    template_name = 'avatar_upload.html'
    form_class = AvatarForm
    # success_url = "/"
    model = Avatar
    def get_context_data(self, *args, **kwargs):
        context = super(AvatarUpdateView, self).get_context_data(*args, **kwargs)
        context["image_form"] = AvatarForm(
            instance=self.get_object(), 
            initial={"image":self.get_object().image},
            data={"image":self.get_object().image}
        )
        return context
    def get_success_url(self, *args, **kwargs):
        return self.object.get_absolute_url()

编辑界面如下

Demo项目操作路径:http://demo.codingsoho.com/image/

4. 优化widget

就基本功能而已,这个模板已经够了,但是不够user friendly,我们希望直观的看到图片的显示,而不是只是一个url。 要实现这个目标有很多方法,可以修改template内容或者修改widget的render。

这儿介绍的是后者,这个方法更具备灵活性和扩展性。在模板里,<img class= 'img-responsive' src="{{object.get_image_url}}"/>会把图片显示出来,我们会把它写进render函数。

默认的文件上传组件是ClearableFileInput,我们继承这个类,并添加自己的实现。

:django 1.8 和 django 1.11在ClearableFileInput的实现上有很大的差别,整体上django 1.11的实现更先进一点,所以本文只介绍基于后者的实现。
它主要是通过内部模板来实现组件的渲染。所以我们只需要修改这个模板就行了。

class ImageFileInput(ClearableFileInput):
    if 1.11 > float(django.get_version()): # version to be confirmed
        template_with_initial = ('<p class="file-upload">%s</p>'
                                % ClearableFileInput.template_with_initial)
        template_with_clear = ('<span class="clearable-file-input">%s</span>'
                               % ClearableFileInput.template_with_clear)  
    else:
        template_name = 'widgets/clearable_file_input.html'
    # 
    # [https://stackoverflow.com/questions/27071648/django-imagefield-widget-to-add-thumbnail-with-clearable-checkbox](https://stackoverflow.com/questions/27071648/django-imagefield-widget-to-add-thumbnail-with-clearable-checkbox)
    def render(self, name, value, attrs=None):
        if 1.11 > float(django.get_version()):
            substitutions = {
                'initial_text': self.initial_text,
                'input_text': self.input_text,
                'clear_template': '',
                'clear_checkbox_label': self.clear_checkbox_label,
            }
            template = '%(input)s'
            substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs)
            if self.is_initial(value):
                template = self.template_with_initial
                substitutions.update(self.get_template_substitution_values(value))
                ### 
                substitutions['initial'] = (
                    '<img src="%s" alt="%s" style="max-width: 200px; max-height: 200px; border-radius: 5px;" /><br/>' % (
                        escape(value.url), escape(force_unicode(value))
                    )
                )
                ###
                if not self.is_required:
                    checkbox_name = self.clear_checkbox_name(name)
                    checkbox_id = self.clear_checkbox_id(checkbox_name)
                    substitutions['clear_checkbox_name'] = conditional_escape(checkbox_name)
                    substitutions['clear_checkbox_id'] = conditional_escape(checkbox_id)
                    substitutions['clear'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id})
                    substitutions['clear_template'] = self.template_with_clear % substitutions
            return mark_safe(template % substitutions)
        else:
            return super(ImageFileInput, self).render(name, value, attrs)

增加新的模板widgets/clearable_file_input.html,实现如下.

修改比较简单,也就是在a里面用图片替换了文字。

{% if is_initial %}
    {{ initial_text }}: <a href="{{ widget.value.url }}"><img src="{{widget.value.url}}" alt="{{widget.value}}" style="max-width: 200px; max-height: 200px; border-radius: 5px;" /></a><br/>
    {% if not widget.required %}
        <input type="checkbox" name="{{ checkbox_name }}" id="{{ checkbox_id }}" />
        <label for="{{ checkbox_id }}">{{ clear_checkbox_label }}</label>
    {% endif %}<br />
    {{ input_text }}:
{% endif %}

<input type="{{ widget.type }}" name="{{ widget.name }}"
{% include "django/forms/widgets/attrs.html" %} />

最后,在form里替换这个widget为ImageFileInput,这样新的模板会用这个widget来渲染

class AvatarForm(forms.ModelForm):  
    class Meta:
        model = Avatar
        exclude = []

        widgets = {
            'image': ImageFileInput(),
        }

效果如下:

参考文档