Skip to content

Instantly share code, notes, and snippets.

@snsn
Last active August 29, 2015 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save snsn/9bdc0d1e4bd71ed60473 to your computer and use it in GitHub Desktop.
Save snsn/9bdc0d1e4bd71ed60473 to your computer and use it in GitHub Desktop.

파일 업로드 과정

  1. 사용자가 choose file 버튼을 클릭
  2. 원하는 파일을 선택
  3. upload 버튼을 클릭해 선택한 파일을 전송

( 3.의 과정에서 multipart/form-data을 통해 전송합니다. )

Django model

첫번쨰 방법으로, Django에서 기본적으로 제공하는 Image field를 이용할 수 있습니다. Image fieldFile field와 거의 동일하지만 이미지의 가로, 세로 길이를 정해줄 수 있다는 점이 다릅니다. ( 가로, 세로 길이를 생각하지 않는다고 해도 이미지를 다루는 필드라면 Image field로 명시해주는게 협업에 있어서 좋을 것입니다. )

하지만 이것만으로는 커스텀한 이미지를 만들 수 없어서 여기서는 django-imagekit을 이용하기로 합니다. django-imagekit은 이미지 처리 라이브러리인 Pillow를(Imaging Library) 이용해서 이미지에 변화를 줄 수 있게하는 패키지입니다.

먼저 모델을 살펴볼까요? Model

[app]/models/image.py

from django.db import models

# Django 3rd Party Modules
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill


class ImageExample(models.Model):
    image = ProcessedImageField(
        upload_to=_generate_upload_path,
        processors=[ResizeToFill(100, 50)],
        format='JPEG',
        options={'quality': 60}
    )

코드 해설

  1. ImageField대신에 ProcessedImageField를 이용하였습니다. 이를 통해 얻은 이점은 파일 사이즈 수정, 이미지 포맷 설정, Opacity를 조정할 수 있게 된 점입니다.
  2. upload_to_generate_upload_path라는 이름의 함수를 넣어주었습니다. 이를 통해 이미지 업로드 경로를 동적으로 정해주도록 하였습니다.

어떻게 동적으로 업로드 경로를 정해줄 수 있다는 걸까요? 이 부분은 뷰 코드와 함께 살펴보겠습니다. 이제 폼 필드를 보시겠습니다. Form

[app]/forms/image_example_form.py

from django.forms import ModelForm


class ImageExampleForm(ModelForm):
    class Meta:
        model = ImageExample
        fields = ['image']

코드 해설

  1. 모델로부터 폼을 만들기로 했습니다. 모델폼을 사용한 이유는 프로그램의 무게를 최대한 모델에 주고 나머지는 가볍게 해야한다는 생각 때문입니다. 게다가 모델의 속성을 그대로 가져올 수 있어서 코드를 작성하기 간편해지는 효과 역시 있다고 생각을 합니다.
  2. fields = ['image']라고 적힌 이 부분은 모델이 가지고 있는 'image'라는 이름의 필드를 폼(위젯)으로 만들겠다는 설정입니다.

모델과 폼을 짰으니 업로드 페이지를 위한 템플릿을 만들어 보겠습니다. Template

[app]/templates/[app]/upload.html

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

  {% block content %}
    <form action="" method="post" enctype="multipart/form-data">
      {% csrf_token %}

      {{ form }}

      <input type="submit" value="Upload" />
  </form>
{% endblock %}

코드 해설

  1. {% load imagekit %}을 이용해 imagekit에 필요한 템플릿 요소를 현재 템플릿에 가져옵니다.
  2. multipart/form-data를 이용해 파일을 전송하는 폼을 작성했습니다.
  3. {{ form }}뷰 코드에서 날려준 인자입니다. 이것으로 현재 설정한 폼(위젯)을 적용하는 UI를 생성해냅니다. 물론 기능까지 합니다.

뷰로 넘어가겟습니다. View

[app]/views/upload_image_example.py

from django.shortcuts import render
from [app].forms.image_example_form import ImageExampleForm
from [app].utils.handle_upload_file import handle_uploaded_file


def upload(request):
    form = ImageExampleForm(request.POST, request.FILES)

    if request.method == 'POST':
        if form.is_valid():
            handle_uploaded_file(request.FILES['image'])
            return render(request, "somewhere.html")

    return render(request, "[app]/upload.html", {'form': form})

코드 해설

  1. FBV로 만든 뷰 코드입니다.
  2. form = ImageExampleForm(request.POST, request.FILES)을 통해 ImageExampleForm을 요청받을 때 사용할 폼으로 설정합니다.
  3. POST로 날아온 요청에 대하여 코드를 수행합니다. 사용자가 upload 버튼을 눌렀을 때 동작하는 부분입니다.
  4. form.is_valid()를 통해 생각보다 많은 부분을 해결할 수가 있습니다. 맞지 않는 형식의 파일들 역시 걸러지게 됩니다.
  5. 기본적으로 리턴 값에 {'form': form}을 템플릿에 추가적으로 넘겨주어야 템플릿에서 {{ form }}형태로 사용이 가능하는걸 명심해야 합니다.
  6. 모델에서 살펴보았던 동적 결로로 지정해주는 함수인 _generate_upload_path을 호출합니다.

_generate_upload_path는 다양하게 작성할 수 있지만 여기서는 파일 객체를 통해 업로드 경로를 지정해주는 방법을 사용하였습니다.

def _generate_upload_path(self, f):
    """
    customizing file name code here
    """
    return '%s/%s' % ("first_path", "second_path")

마지막으로 이것에 대한 테스트 코드를 작성해보겠습니다. Test

[app]/tests/views/test_upload_image_example.py

from django.test import TestCase, Client
from django.test.utils import override_settings

# Settings
from [project].settings.partials import media

# Built-in module
import os
from shutil import rmtree
import urllib2


class ImageExampleViewTest(TestCase):
    """
    For saftry purpose, media.TEST_MEDIA_ROOT is used to upload test.
    (after set the media.MEDIA_ROOT value by media.TEST_MEDIA_ROOT)
    """

    def setUp(self):
        # test upload directory
        if not os.path.isdir(media.TEST_MEDIA_ROOT):
            os.mkdir(media.TEST_MEDIA_ROOT)

        # sample file set
        TEST_IMAGE_URL = os.environ['TEST_IMAGE_URL']
        self.file_path = \
            "%s/%s.%s" % (media.TEST_MEDIA_ROOT, 'temp_name', 'png')
        self.f = open(self.file_path, 'w+')
        self.f.write(urllib2.urlopen(TEST_IMAGE_URL).read())
        self.f.seek(0)

        # client
        self.client = Client()
        self.upload_url = '/upload/'

    def tearDown(self):
        if os.path.isdir(media.TEST_MEDIA_ROOT):
            rmtree(media.TEST_MEDIA_ROOT)

    def test_media_directory_should_not_exist_after_rmtree(self):
        rmtree(media.TEST_MEDIA_ROOT)
        self.assertFalse(
            os.path.isfile(media.TEST_MEDIA_ROOT)
        )

    def test_setup_should_create_media_directory(self):
        self.assertTrue(
            os.path.isdir(media.TEST_MEDIA_ROOT)
        )

    def test_sub_directory_of_media_path_should_be_created_after_upload(self):
        self.client.post(self.upload_url, {'image': self.f})
        self.assertTrue(os.path.isdir(media.TEST_MEDIA_ROOT + '/test/'))


# Settings for Test
ImageExampleViewTest = override_settings(
    MEDIA_ROOT=media.TEST_MEDIA_ROOT,
    MEDIA_URL=media.MEDIA_URL
)(ImageExampleViewTest)

코드 해설

  1. 테스트에만 적용할 MEDIA PATH 설정을 위해 settings를 override합니다. (# Settings for Test 아래 부분)
  2. 안전성을 위해 MEDIA_ROOTTEST_MEDIA_ROOT로 오버라이드한 이후에도 TEST_MEDIA_ROOT를 이용합니다.
  3. 매 테스트 시마다 미디어 디렉토리를 생성하고 삭제하는 과정을 반복합니다. (setUp()에서 만들고 tearDown에서 지우고)
  4. 올바른 이미지 파일이 아니면 업로드가 되지 않습니다. 그렇다고 테스트를 위해 레포에 이미지 파일을 저장하는 것도 마음에 들지 않습니다. 따라서 온라인으로부터 이미지 코드를 복사하여 테스트를 진행하였습니다. (self.f.write(urllib2.urlopen(TEST_IMAGE_URL).read()))
  5. 파일 업로드 시에 파일을 검사하는 객체인 FileObject는 파일 포인터를 처음으로 되돌려서 작업을 하지 않습니다. 따라서 직접 처음으로 되돌려주는 작업이 필요합니다. (self.f.seek(0))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment