Django - Dstagram part1-Photo app
Web/Django

Django - Dstagram part1-Photo app

뉴비뉴 2019. 7. 18.

기본 세팅

1. Django 설치

$ pip install django

 

2. 프로젝트 생성

$ django-admin startproject config .

 

3. 데이터베이스 초기화

$ python manage.py migrate

 

4. 관리자 계정 생성

$ python manage.py createsuperuser

 

앱 만들기

1. 앱 생성

$ python manage.py startapp photo

.config/settings.py 안에 INSTALLED_APPS 에다가 'photo' 추가

 

2. 모델 생성

from django.db import models
from django.urls import reverse
from django.contrib.auth.models import User # 가져온 User를 class Photo 에서 사용

class Photo(models.Model):
    author = models.ForeignKey(User,on_delete=models.CASCADE,related_name='user_photos')
    # ForeignKey를 사용하여 User 테이블과 관계를 만듭니다. 여기서 User 모델은 장고에서 기본적으로 사용하는
    # 사용자 모델입니다. on_delete 인수는 연결된 모델이 삭제될 경우 현재 모델의 값은 어떻게 할 것이냐 이다.
    '''
    CASCADE : 연결된 객체가 지워지면 해당 하위 객체도 같이 삭제
    PROTECT : 하위 객체가 남아 있다면 연결된 객체가 지워지지 않음
    SET_NULL : 연결된 객체만 삭제하고 필드 값을 NULL로 설정
    SET_DEFAULT : 연결된 객체만 삭제하고 필드 값을 설정된 기본 값으로 변경
    SET() : 연결된 객체만 삭제하고 지정한 값으로 변경
    DO_NOTHING : 아무 일도 하지 않음
    '''
    # 세 번째 인수인 related_name 은 연결된 객체에서 하위 객체의 목록을 부를 때 사용할 이름이다.
    # Photo 모델을 예로 들면 어떤 유저가 작성한 글을 불러 올 때는 유저 객체에 user_photos 속성을 참조하면 된다.
    photo = models.ImageField(upload_to='photos/%Y/%m/%d', default = 'photos/no_image.png')
    # upload_to 는 사진이 업로드 될 경로를 설정한다. 만약 업로드가 되지 않을 경우 default 값으로 대체.
    text = models.TextField()
    # 텍스트 필드, 문자열 길이 제한 X
    created = models.DateTimeField(auto_now_add=True)
    # 글 작성 일을 저장하기 위한 날짜시간 필드
    # auto_now_add 옵션을 설정하면 객체가 추가될 때 자동으로 값을 설정한다.
    # 한마디로 게시글이 작성될 때의 시간을 갖고 upload_to 의 경로에 맞게끔 값을 넣어준다.
    updated = models.DateTimeField(auto_now=True)
    # 글 수정 일을 저장하기 위한 날짜시간 필드이다. auto_now 옵션을 설정하면 객체가 수정 될 때 마다 자동으로 값을 설정한다.

    class Meta: # 옵션 클래스
        ordering = ['-updated']
    # ordering 클래스 변수는 해당 모델의 객체들을 어떤 기준으로 정렬할 것인지 설정하는 옵션
    # -updated 로 설정했으니 글 수정 시간의 내림차순으로 정렬할것이다.
    def __str__(self):
        return self.author.username + " " + self.created.strftime("%Y-%m-%d %H:%M:%S")
    # 작성자의 이름과(author.username) 글 작성일을 합친 문자열을 반환한다.

    def get_absolute_url(self):
        return reverse('photo:photo_detail', args=[str(self.id)]) # photo/id
    # 객체의 상세 페이지의 주소를 반환하는 메서드입니다.
    # 객체를 수정했을 때 이동할 주소를 위해 호출되기도 하고 템플릿에서 상세 화면으로 이동하는
    # 링크를 만들 때 호출하기도 합니다. 이런 주소를 만들기 위해서는 reverse 메서드를 사용하는데
    # reverse 메서드는 URL 패턴 ㅣㅇ름을 가지고 해당 패턴을 찾아 주소를 만들어주는 함수이다.
    

photo = models.ImageField(upload_to='photos/%Y/%m/%d', default = 'photos/no_image.png')

upload_to=' ' 경로에 맞게 폴더가 생성된 걸 확인할 수 있다. 만약 이미지를 추가안한다면?

Currently: 부분에 defalut로 지정한 경로와 파일명이 들어가 있는 걸 확인할 수 있다.

데이터베이스 적용

$ python manage.py makemigrations photo

[!] Error 발생, ImageField 를 사용하려면 Pillow 모듈이 필요

$ pip install Pillow

$ python manage.py makemigrations photo

$ python manage.py migrate photo

관리자 사이트에 모델 등록

# admin.py
from django.contrib import admin

from .models import Photo

admin.site.register(Photo)

하지만 업로드 하는 앱이 하나가 아니라 여러개라고 생각해보자, 여러 파일 및 사진이 업로드 될텐데 업로드 될 때 마다 많은 폴더가 생기게 된다. 이를 해결하기 위해서 파일들이 모이는 폴더를 따로 하나 만들어주자.

업로드 폴더 관리

각 앱에서 업로드 하는 파일들을 한 폴더를 중심으로 모으려면 settings.py에 MEDIA_ROOT라는 옵션을 설정해야 한다.

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

어떤 앱에서 업로드를 하더라도 media 폴더 밑에 각 앱 별로 폴더를 만들고 파일을 업로드 하게 된다.

MEDIA_URL은 STATIC_URL처럼 파일을 브라우저로 서빙할 때 보여줄 가상의 URL 이다.

가상 URL은 여러 가지 편의도 있지만 보안을 위해 필요한 기능입니다. 이 가상 URL 을 사용하면 서버 내부의 폴더

구조를 숨길 수 있기 때문에 해커에게 정보를 적게 노출할 수 있습니다.

modia 라는 폴더 안에 upload 에서 설정한 경로처럼 폴더가 생성되고 파일이 추가되는 걸 확인할 수 있다.

기존에 만들었던 Photos 폴더는 지워주도록 하자.

관리자 페이지 커스터마이징

 

from django.contrib import admin

from .models import Photo

class PhotoAdmin(admin.ModelAdmin):
    # PhotoAdmin 클래스에는 관리자 사이트에서 보이는 목록 화면을 커스터마이징 할 수 있는 옵션을 설정
    list_display = ['id','author','created','updated']
    # 목록에 보일 필드를 설정한다. 모델의 필드를 선택하거나 별도 함수를 만들어 필드처럼 등록할 수 있다.
    raw_id_fields = ['author']
    # ForeignKey 필드의 경우 연결된 모델의 객체 목록을 출력하고 선택해야 하는 목록이 너무 길 경우 불편해진다.
    # 이런 경우 raw_id_fields로 설정하면 값을 써 넣는 형태로 바뀌고 검색 기능을 사용해 선택할 수 있게 된다.
    list_filter = ['created','updated','author']
    # 필터 기능을 사용할 필드를 선택합니다. 장고가 적절하게 필터 범위를 출력해준다.
    search_fields = ['text','created']
    # 검색 기능을 통해 검색할 필드를 선택한다. ForeignKey 필드는 설정할 수 없다. ex) author
    ordering = ['-updated','-created']
    # 모델의 기본 정렬값이 아닌 관리자 사이트에서 기본으로 사용할 정렬값을 설정한다.
    # -updated, -created = 내림차순

admin.site.register(Photo, PhotoAdmin)

뷰 만들기(사용자에게 보여질 화면 만들기)

사진 목록, 업로드, 확인, 수정, 삭제 기능을 위한 뷰를 만들어 보겠습니다.

 

첫 번째 목록이다. 함수형 뷰로 만들기 위해 photo_list 라는 함수를 만든다.

from django.shortcuts import render

from .models import Photo

def photo_list(request):
    photos = Photo.objects.all() # 데이터베이스에 저장 된 모든 사진을 불러온다.
    return render(request,'photo/list.html', {'photos':photos})
# 템플릿과 뷰를 연동하기 위해서 render 함수를 사용한다.
# render 함수는 첫 번째 인자로 request
# 두 번째 인자는 랜더링 할 템플릿
# 세 번째 인자는 템플릿에 보내줄 객체나 값

두 번째는 업로드 뷰를 만들겠습니다. 제네릭 뷰를 사용할텐데 다른 뷰에 사용할 제네릭 뷰들을 미리 임포트하겠습니다.

CreateView, DeleteView, UpdateView를 임포트하고 CreateView를 상속받는 Photo Upload View를 만든다.

from django.shortcuts import render
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.shortcuts import redirect

from .models import Photo

class PhotoUploadView(CreateView): # CreateView 를 PhotoUploadView가 상속받는다.
    model = Photo
    fields = ['photo','text']
    template_name = 'photo/upload.html' # 클래스 변수 생성, 이 변수는 실제 사용할 템플릿을 설정한다.

    def form_valid(self, form): # 업로드를 끝내고 이동할 페이지를 호출하기 위해 사용하는 메서드
        # 이 메서드를 오버라이드해서 작성자를 설정하는 기능을 추가했다.
        form.instance.author_id = self.request.user.id # 작성자는 현재 로그인한 사용자로 설정한다.
        if form.is_vaild(): # is_vaild() 입력 된 값들을 검증한다.
            form.instance.save() # 이상이 없다면 데이터베이스에 저장하고
            return redirect('/') # redirect 메서드를 이용해 메인 페이지로 이동한다.
        else:
            return self.render_to_response({'form':form}) # 문제가 있다면 내용을 그대로 작성 페이지에 표시한다.

나머지 뷰들도 제네릭 뷰를 사용해서 적절히 만들어 준다.

제네릭 뷰 시스템

아래와 같이 최소한 그 뷰가 어떤 모델을 사용할 것인지만 지정해주면 나머진 모두 상위 클래스(DeleteView, UpdateView')가 모든 걸 알아서 해준다. 필요에 따라 각 뷰마다 갈라지는 값을 넣어주기만 하면 된다. 이게 상속을

사용할 수 있는 클래스 뷰를 사용하는 힘이자 이유이다. 코드를 쓰는 것 자체가 목적이 아닌 도구임을 생각해보면

Django 를 왜 쓰는가에 대해 고민했던 사람이라면 제네릭뷰를 활용한 구현은 두 손 들어 활영할만한 훌륭한 방법이다.

class PhotoDeleteView(DeleteView): # 제네릭 뷰 DeleteView를 사용하기 위해 상속 받는다.
    model = Photo
    success_url = '/' # 성공 시 사이트 메인으로 이동한다.
    template_name = 'photo/delete.html'

class PhotoUpdateView(UpdateView): # 제네릭 뷰 UpdateView를 사용하기 위해 상속 받는다.
    model = Photo
    fields = ['photo','text']
    template_name = 'photo/update.html'
from django.shortcuts import render
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.shortcuts import redirect

from .models import Photo

class PhotoUploadView(CreateView): # CreateView 를 PhotoUploadView가 상속받는다.
    model = Photo
    fields = ['photo','text']
    template_name = 'photo/upload.html' # 클래스 변수 생성, 이 변수는 실제 사용할 템플릿을 설정한다.

    def form_valid(self, form): # 업로드를 끝내고 이동할 페이지를 호출하기 위해 사용하는 메서드
        # 이 메서드를 오버라이드해서 작성자를 설정하는 기능을 추가했다.
        form.instance.author_id = self.request.user.id # 작성자는 현재 로그인한 사용자로 설정한다.
        if form.is_valid(): # is_vaild() 입력 된 값들을 검증한다.
            form.instance.save() # 이상이 없다면 데이터베이스에 저장하고
            return redirect('/') # redirect 메서드를 이용해 메인 페이지로 이동한다.
        else:
            return self.render_to_response({'form':form}) # 문제가 있다면 내용을 그대로 작성 페이지에 표시한다.

def photo_list(request):
    photos = Photo.objects.all() # 데이터베이스에 저장 된 모든 사진을 불러온다.
    return render(request,'photo/list.html', {'photos':photos})
# 템플릿과 뷰를 연동하기 위해서 render 함수를 사용한다.
# render 함수는 첫 번째 인자로 request
# 두 번째 인자는 랜더링 할 템플릿
# 세 번째 인자는 템플릿에 보내줄 객체나 값

class PhotoDeleteView(DeleteView): # 제네릭 뷰 DeleteView를 사용하기 위해 상속 받는다.
    model = Photo
    success_url = '/' # 성공 시 사이트 메인으로 이동한다.
    template_name = 'photo/delete.html'

class PhotoUpdateView(UpdateView): # 제네릭 뷰 UpdateView를 사용하기 위해 상속 받는다.
    model = Photo
    fields = ['photo','text']
    template_name = 'photo/update.html'

URL 연결

뷰를 동작시키기 위해서는 URL을 연결해야 하낟. photo 앱 폴더에 urls.py 를 만들고 연결해보자.

from django.urls import path
from django.views.generic.detail import DetailView

from .views import *
from .models import Photo

app_name = 'photo' # 네임스페이스로 사용되는 값이다.
# 템플릿에서 url 템플릿 태그를 사용할 때 app_name 값이 설정되어 있다면
# [app_name:URL패턴이름] 형태로 사용한다.
urlpatterns = [
    path('', photo_list, name='photo_list'), # 함수형(def) 뷰
    path('detail/<int:pk>/',DetailView.as_view(model=Photo,template_name='photo/detail.html'), name='photo_detail'),
    # 제네릭 뷰를 그대로 사용하는 인라인 뷰
    # urls.py 에서 인라인 코드로 작성할 수 있습니다. path 함수에 인수로 전달할 때는
    # as_view안에 클래스 변수들을 설정해 사용합니다.
    path('upload/', PhotoUploadView.as_view(), name='photo_upload'),
    path('delete/<int:pk>/', PhotoDeleteView.as_view(), name='photo_delete'),
    path('update/<int:pk>/', PhotoUpdateView.as_view(), name='photo_update'),
    # 함수 형 뷰는 뷰 이름만 써주고 클래스(class) 형 뷰는 뒤에 .as_view()를 붙인다.
]

뷰의 url들은 다 설정 했으니 이제 루트 urls.py 에 앱의 urls.py를 연결해주겠습니다.

#config/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('photo.urls')), # 이 때 URL 패턴을 '' 로 설정하면 photo 앱이 메인 페이지로 동작
]

템플릿 분리와 확장

뷰를 만들었으니 뷰에 사용할 템플릿을 만들겠습니다. 각 템플릿을 만들기에 앞서 템플릿 확장을 사용하기 위해 기본이 되는 base.html 템플릿부터 만들겠습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    <title>Dstagram {% block title %}{% endblock %}</title>
</head>
<body>

<div class="container">
    <header class="header clearfix">
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <a class="navbar-brand" href="/">Dstagram</a>
            <ul class="nav">
                <li class="nav-item"><a href="/" class="active nav-link ">Home</a></li>
                {% if user.is_authenticated %} <!-- 로그인 여부를 판단할 수 있다.-->
                <li class="nav-item"><a href="#" class="nav-link">Welcome, {{user.get_username}}</a></li>
                <li class="nav-item"><a href="{% url 'photo:photo_upload' %}" class="nav-link">Upload</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Logout</a></li>
                {% else %}
                <li class="nav-item"><a href="#" class="nav-link">Login</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Signup</a></li>
                {% endif %}
            </ul>
        </nav>
    </header>
    {% block content %}
    {% endblock %}

    <footer class="footer">
        <p>&copy; 2018 Baepeu. Powered By Django 2</p>
    </footer>
</div>


</body>
</html>

템플릿이 검색되도록 settings.py 에 경로를 추가하겠습니다.

TEMPLATES 변수에 있는 DIRS 키의 값에 추가합니다.

'DIRS': [os.path.join(BASE_DIR, "templates")],
<!--photo/templates/photo/list.html-->
{% extends 'base.html' %}

{% block title %}- List{% endblock %}

{% block content %}
    {% for post in photos %}
        <div class="row">
            <div class="col-md-2"></div>
            <div class="col-md-8 panel panel-default" >
                <p><img src="{{post.photo.url}}" style="width:100%;"></p>
                <button type="button" class="btn btn-xs btn-info">
                    {{post.author.username}}
                </button>
                <p>{{post.text|linebreaksbr}}</p>
                <p class="text-right">
                    <a href="{% url 'photo:photo_detail' pk=post.id %}"
                    class="btn btn-xs btn-success">댓글달기</a>
                </p>
            </div>
            <div class="col-md-2"></div>
        </div>
    {% endfor %}
{% endblock %}

upload.html

{% extends 'base.html' %}
{% block title %}- Upload{% endblock %}

{% block content%}
<div class="row">
    <div class="col-md-2"></div>
    <div class="col-md-8 panel panel-default">
        <form action="" method="post" enctype="multipart/form-data"> <!-- 업로드 뷰는 form 태그로 구성 -->
            <!-- enctype은 form 태그로 작성한 정보를 어떤 형태로 인코딩해서 서버로 전달할 것인지 결정하는 옵션입니다.
            method 가 "post" 일 때만 사용할 수 있습니다.

            1. application/x-www-form-urlencoded : 기본 옵션입니다. 모든 문자열을 인코딩해 전달하며
            특수 문자는 ASCII HEX 값으로 변환하고 띄어쓰기는 +로 변환하여 전달합니다.
            2. multipart/form-data : 파일 업로드 때 사용하는 옵션이며 데이터를 문자열로 인코딩 하지 않고 전달
            3. text/plain : 띄어쓰기만 +로 변환하고 특별한 인코딩 없이 전달한다.-->
            {{form.as_p}} <!-- 입력form HTML 생성 -->
            {% csrf_token %}
            <input type="submit" class="btn btn-primary" value="Updata">
        </form>
    </div>
    <div class="col-md-2"></div>
</div>
{% endblock %}

detail.html

{% extends 'base.html' %}
{% block title %}
    {{object.text|truncatechars:10}} <!-- 문자열을 지정 글자갯수까지 줄이며, 줄여질 경우 끝 에 "..."를 추가 -->
{% endblock %}

{% block content %}
    <div class="row">
        <div class="col-md-2"></div>
        <div class="col-md-8 panel panel-default">
            <p><img src="{{object.photo.url}}" style="width:100%;"></p>
            <button type="button" class="btn btn-outline-primary btn-sm">
                {{object.author.username}} <!-- 2044smile -->
            </button>
            <p>{{object.text|linebreaksbr}}</p>
            <!-- HTML은 기본적으로 공백문자를 인식하지 못하는데 linebreaksbr 필터를 사용하면 줄바꿈이 일어나는 곳에 <br> 태그를 추가! -->
            <a href="{% url 'photo:photo_delete' pk=object.id %}" class="btn btn-outline-danger btn-sm float-right">
                Delete
            </a>
            <a href="{% url 'photo:photo_update' pk=object.id %}" class="btn btn-outline-danger btn-sm float-right">
                Update
            </a>
        </div>
        <div class="col-md-2"></div>
    </div>
{% endblock%}

update.html

{% extends 'base.html' %}
{% block title %}
- Update
{% endblock%}

{% block content %}
<div class="row">
    <div class="col-md-2"></div>
    <div class="col-md-8 panel panel-default">
        <form action="" method="post" enctype="multipart/form-data">
            {{form.as_p}}
            {% csrf_token %}
            <input type="submit" class="btn btn-primary" value="Update">
        </form>
    </div>
    <div class="col-md-2"></div>
</div>
{% endblock %}

delete.html

{% extends 'base.html' %}
{% block title %}- Delete{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-2"></div>
    <div class="col-md-8 panel panel-default">
        <div class="alert alert-info">
            Do you want to delete {{object}}? <!-- 2044smile 2019-07-18 04:40:03 -->
        </div>
        <form action="" method="post">
            {{form.as_p}}
            {% csrf_token %}
            <input type="submit" class="btn btn-danger" value="Confirm">
        </form>
    </div>
    <div class="col-md-2"></div>
</div>
{% endblock %}

detail.html 에서 Delete를 눌렀을 때 작동원리

사진 표시하기

config/urls.py

 

 

 

 

 

 

 

참조

django url reverse

https://wayhome25.github.io/django/2017/05/05/django-url-reverse/

 

render 함수

https://new93helloworld.tistory.com/286

 

View 함수 HttpResponse, render, Generic View

https://devlog.jwgo.kr/2016/11/08/python-jango-why-generic-views/

 

django form

https://wayhome25.github.io/django/2017/05/06/django-form/

 

 

 

'Web > Django' 카테고리의 다른 글

Django - Dstagram part3-댓글 기능 구현하기  (0) 2019.07.23
Django - Dstagram part2-Account app  (0) 2019.07.18
Django / 복습(book)  (0) 2019.07.05
Django / 복습(AWS)  (0) 2019.07.05
Django / 복습(실습)  (0) 2019.07.03

댓글

💲 추천 글