Django 웹사이트 예제 - django websaiteu yeje

장고(Django)는 웹 사이트를 쉽게 개발할 수 있도록 파이썬으로 만든 풀 스택(Full Stack) 웹 어플리케이션 프레임워크(Web Application Framework)이다. Django는 모델-뷰-컨트롤러(MVC) 패턴과 유사한 MVT(모델-뷰-템플릿) 패턴에 따라 처리한다.

파이썬 장고 웹 프레임워크로 웹사이트 만들기 예제 (프로젝트 생성 후 배포까지 해보기)

Django 웹사이트 예제 - django websaiteu yeje

장고 프로젝트 만드는 순서(파이참 기준)

  1. 파이참 프로젝트 만들기
  2. 장고 설치
  3. 장고 프로젝트 만들기
  4. 설정하기(데이터베이스, S3)
  5. 데이터베이스 초기화
  6. 관리자 계정 만들기
  7. 앱(apps) 만들기
  8. 모델(models) 설계(데이터베이스)
  9. 뷰(views) 만들기(기능, 계산 처리)
  10. 템플릿(templates) 만들기(화면에 표시될 내용, 양식 - forms)
  11. URL(urls) 만들기
       -대표적인 기능(화면, CRUD)

■ 장고 웹 프로젝트(Bookmark) 생성, 앱(App) 생성 및 등록

o 파이참(PyCharm)에서 아래 위치에  'NewProject' 생성

- Location : C:\Users\haeu\OneDrive\myDev\PyCharm\ogLap1_bookmark

o Terminal 에서 장고(Django) 설치 및 프로젝트 생성하기

(venv) C:\Users\haeu\OneDrive\myDev\PyCharm\ogLap1_bookmark>pip install django

(venv) C:\Users\haeu\OneDrive\myDev\PyCharm\ogLap1_bookmark>django-admin startproject config .

# 생성된 config 폴더 내 파일

__init__.py
asgi.py
settings.py
urls.py
wsgi.py

※ asgi.py (Asynchronous Server Gateway Interface) : 
- 웹 서버, 프레임워크, 어플리케이션을 연결해주는 표준 인터페이스
- 즉, 다양한 서버 및 어플리케이션 프레임워크의 비동기 및 동기 기능의 표준 제공

o 장고 프로젝트 생성 후 서버가 정상적으로 구동하는지 확인 : runserver 명령 (서버 중지 ctrl + c)

-파이참 콘솔도 가능하나 Windows 명령창(cmd)에서 runserver 구동해놓고 사용함
C:\Users\haeu\ogLap1_bookmark>python manage.py runserver

o 기본 DB 생성(sqlite3 db 생성(migrate)) 및 관리자 계정 생성하기

(sqlite3가 아닌 다른 RDB를 사용할 경우, db셋팅을 먼저 한 후 mirgate 및 createsuperuser 함)

(venv) C:\Users\haeu\OneDrive\myDev\PyCharm\ogLap1_bookmark>python manage.py migrate

(venv) C:\Users\haeu\OneDrive\myDev\PyCharm\ogLap1_bookmark>python manage.py createsuperuser

Username (leave blank to use 'haeu'): admin
Email address:
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
Bypass password validation and create user anyway? [y/N]: n
Password: amdin12345
Password (again):
Superuser created successfully.

o bookmark 앱 만들기

(venv) C:\Users\haeu\OneDrive\myDev\PyCharm\ogLap1_bookmark>python manage.py startapp bookmark

# bookmark 폴더 내 폴더&파일

migrations
__init__.py
admin.py
apps.py
models.py
tests.py
views.py

o 장고 앱을 만들고 먼저 해줘야 할 일은 만든 앱(bookmark) srttings.py에 사용 등록 하는 것임

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'bookmark',  # bookmark.apps.BookmarkConfig
]

※ bookmark.apps.BookmarkConfig (apps.py 파일에서 BookmarkConfig 참조)

from django.apps import AppConfig
class BookmarkConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'bookmark'

django.contrib.admin (관리용 사이트)
-모델에 대한 관리용 인터페이스 자동 생성
-사이트 관리용 통합 인터페이스 생성

django.contrib.auth (인증 시스템)
-장고 사용자 인증 시스템 지원(사용자 계정, 그룹, 권한, 쿠키 기반의 사용자 세션)
-인증과 권한 부여 모두 처리

django.contrib.contenttypes (컨텐츠 타입을 위한 프레임워크)
-모델 간의 관계 및 모델 ContentType 중 인스턴스 간의 관계 활성화에 사용
-테이블 간에 조인을 사용할 때 사용하는 프레임워크

django.contrib.sessions (세션 프레임워크)
-장고의 익명 세션 지원, 사이트 방문자별 임의 데이터 저장 검색 가능
-db 세션 기본 저장하고, 캐시, 파일, 쿠키 등 기반에서 사용

django.contrib.messages (메시징 프레임워크)
-장고는 쿠키 및 세션 기반 메시징 지원
-하나의 요청에 메시지 임시 저장하고 후속 요청에 표시 위해 검색 가능

django.contrib.staticfiles (정적 파일을 관리하는 프레임워크)
-각 애플리케이션의 정적 파일 수집(쉽게 제공 가능한 단일 위치 수집)

# 언어 & 시간 셋팅 (settings.py에서 아래처럼 수정)

LANGUAGE_CODE = 'ko-kr'  # 'en-us'
TIME_ZONE = 'Asia/Seoul'  # 'UTC'

Django 웹사이트 예제 - django websaiteu yeje

■ 모델(models) 다루기

모델은 데이터베이스를 객체화하고 SQL이 아닌 ORM으로 다루기 위한 것임

  • 모델 = 테이블
  • 모델의 필드 =테이블의 컬럼
  • 인스턴스 = 테이블의 레코드
  • 필드의 값(인스턴스의 필드값) = 레코드의 컬럼 데이터값

o 모델 작성

from django.db import models

class Bookmark(models.Model):
    site_name = models.CharField(max_length=200)
    url = models.URLField(verbose_name='Site URL')

- 필드의 종류가 결정하는 것

  • 데이터베이스의 컬험 종류
  • 제약 사항(몇글자)
  • Form의 종류
  • Form에서 제약 사항

- 모델을 생성하기 위해서는 i)models의 변경사항을 추적해서 기록해놓는 makemigrations 명령과 ii)데이터베이스에 적용(반영)하는 단계를 거쳐야 테이블이 생성된다.
- 즉, makemigrations 명령 후 migrate 명령을 통해 테이블이 생성되며, models를 수정한 후에도 반드시 두 단계를 실행해야됨

(venv) C:\Users\haeu\OneDrive\myDev\PyCharm\ogLap1_bookmark>python manage.py makemigrations bookmark

※ migrations 폴더 --> 0001_initial.py

from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True
    dependencies = [
    ]
    operations = [
        migrations.CreateModel(
            name='Bookmark',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('site_name', models.CharField(max_length=200)),
                ('url', models.URLField(verbose_name='Site URL')),
            ],
        ),
    ]

- site_name, url 이외에 'id'를 django에서 자동으로 생성하고 별칭(verbose_name)으로 표시됨

o 모델(models)이 생성되었는지 확인하기 위해 admin.py에서 확인하기 (생성한 모델을 관리자 페이지에서 관리할 수 있도록 admin.py에 등록함)

from django.contrib import admin
from .models import Bookmark

admin.site.register(Bookmark)

o 모델의 클래스 인스턴스 생성시 __str__로 출력되도록 해보기

class Bookmark(models.Model):
    site_name = models.CharField(max_length=200)
    url = models.URLField(verbose_name='Site URL')

    def __str__(self):
        return '이름 : ' + self.site_name + ', 주소(URL) : ' + self.url

■ 뷰(views) 작성

- 사용자가 웹 페이지에 접속(URL) 또는 이벤트가 발생할 때 웹 서버는 사용자 또는 이벤트(CRUD)에 반응(응답)하기 위한 페이지를 뷰에서 찾아서 동작시킨다.(즉, 응답을 위한 절차 실행)

* CRUD(Create, Read, Update, Delete)

- 뷰(view)에는 클래스형 뷰(제네릭 뷰), 함수형 뷰가 있는데, 이번 프로젝트에서는 클래스형 뷰(제네릭 뷰)를 이용함

- views.py

from django.views.generic.list import ListView
from .models import Bookmark

class BookmarkListView(ListView):
    # 어떤 모델에 대한 제네릭 뷰인지 지정
    model = Bookmark  # db에 등록된 Bookmark 데이터 전체 반환

o 사용자가 특정 URL로 접속했을 때 어떤 뷰를 동작할 것인지 지정해주기(URL Config)

사용자 접속 시 프로젝트 내의 config에 있는 urls.py에서 해당 URL을 우선 해석 후 특정 앱의 URL인 경우, 해당 앱의  urls.py로 다시 전달함

- config>urls.py

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('bookmark/', include('bookmark.urls')),  # http://127.0.0.1/bookmark/
]

- bookmark>urls.py

from django.urls import path
from .views import BookmarkListView

urlpatterns = [
    # as_view() : 클래스형 뷰를 내부적으로 함수형 뷰로 처리
    # name : 결과 페이지로 보여 줄 템플릿 파일에서 해당 URL을 호출할 때 쓰는 별칭
    path('', BookmarkListView.as_view(), name='list'),  # http://127.0.0.1/bookmark/
]

http://localhost:8000/bookmark/ 로 접속
# TemplateDoesNotExist at /bookmark/ 에러 뜨면 정삭적으로 작동한 것임
urls에서 views의 BookmarkListView()의 함수형 뷰 호출 시 화면에 보여 줄 파일(Template html 파일) )이 없기 때문에 오류 발생

o Template 파일 만들기

각 앱에 templates 폴더 만들고 하위에 다시 앱 이름과 같은 폴더를 만든다. (템플릿 이름이 중복될 수 있기 때문임)

- bookmark>templates>bookmark>

- bookmark>templates>bookmark>bookmark_list.html

<div class="btn-group">
    <a href="#" class="btn btn-info">Add Bookmark</a>
</div>
<p></p>
<table class="table">
    <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Site</th>
            <th scope="col">URL</th>
            <th scope="col">Modify</th>
            <th scope="col">Delete</th>
        </tr>
    </thead>
    <tbody>
        {% for bookmark in object_list %}
        <!-- 규칙 : queryset일 경우 object_list, 1개인 경우 object -->
        <tr>
            <td>{{ forloop.counter }}</td> <!-- number -->
            <td><a href="#">{{ bookmark.site_name }}</a></td> <!-- bookmark 모델 인스턴스.필드(컬럼)명-->
            <td><a href="{{ bookmark.url }}" target="_blank">{{ bookmark.url }}</a></td>
            <td><a href="#" class="bnt btn-success btn-sm">Modify</a></td>
            <td><a href="#" class="bnt btn-danger btn-sm">Delete</a></td>
        </tr>
    </tbody>
</table>

o bookmark에 데이터 추가(add) 로직 작성 : views.py에 클래스형 뷰 CreateView 추가

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView
from .models import Bookmark

class BookmarkListView(ListView):
    # 어떤 모델에 대한 제네릭 뷰인지 지정
    model = Bookmark  # db에 등록된 Bookmark 데이터 전체 반환

class BookmarkCreateView(CreateView):
    model = Bookmark

- bookmark/urls.py 수정

from django.urls import path
from .views import BookmarkListView, BookmarkCreateView

urlpatterns = [
    # as_view() : 클래스형 뷰를 내부적으로 함수형 뷰로 처리
    # name : 결과 페이지로 보여 줄 템플릿 파일에서 해당 URL을 호출할 때 쓰는 별칭
    path('', BookmarkListView.as_view(), name='list'),  # http://127.0.0.1:8000/bookmark/
    path('add/', BookmarkCreateView.as_view(), name='add'),  # http://127.0.0.1:8000/bookmark/add/
]

#ImproperlyConfigured at /bookmark/add/
#Using ModelFormMixin (base class of BookmarkCreateView) without the 'fields' attribute is prohibited.

- 데이터를 입력(추가)하거나 수정 뷰인 경우, 대상 필드를 지정해야 한다는 의미 (filed=[ ] 코드 추가)

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView
from .models import Bookmark

class BookmarkListView(ListView):
    # 어떤 모델에 대한 제네릭 뷰인지 지정
    model = Bookmark  # db에 등록된 Bookmark 데이터 전체 반환

class BookmarkCreateView(CreateView):
    model = Bookmark
   fields = ['site_name', 'url']

#TemplateDoesNotExist at /bookmark/add/
#bookmark/bookmark_form.html

- form 관련 내용이 없다는 의미 --> django에서는 create or update의 경우 _form이 붙음
template_name_suffix = '_create' 추가  # create와 update는 모델명_form.html을 불러오기 때문에 suffix 변경하여 사용

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView
from .models import Bookmark

class BookmarkListView(ListView):
    # 어떤 모델에 대한 제네릭 뷰인지 지정
    model = Bookmark  # db에 등록된 Bookmark 데이터 전체 반환

class BookmarkCreateView(CreateView):
    model = Bookmark
    fields = ['site_name', 'url']
    template_name_suffix = '_create' 추가

#TemplateDoesNotExist at /bookmark/add/
#bookmark/bookmark_create.html

- 테플릿 bookmark/bookmark_create.html 작성 

<!-- django에서는 action 속성을 비워두는 경우가 많음. --> 
<!-- 입력을 받거나 입력을 처리하는 것을 같은 뷰에서 처리하며, 비워 둔 경우 자기 자신을 의미함-->

<form action="" method="post">
  {% csrf_token %}
  {{ form.as_p }}  <!-- 입력, 수정 뷰의 경우 장고에서 자동으로 form을 만들어 출력해줌-->
  <input type="submit" value="Add" class="btn btn-info btn-sm">
</form>

{{ form.as_p }}은 p태그 자동 삽입

- 접속 시 

#ImproperlyConfigured at /bookmark/add/
#No URL to redirect to.  Either provide a url or define a get_absolute_url method on the Model.

데이터 입력 완료 후 redirect할 url이 없다는 의미
success_url = reverse_lazy('list') 추가

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .models import Bookmark

class BookmarkListView(ListView):
    # 어떤 모델에 대한 제네릭 뷰인지 지정
    model = Bookmark  # db에 등록된 Bookmark 데이터 전체 반환

class BookmarkCreateView(CreateView):
    model = Bookmark
    fields = ['site_name', 'url']
    success_url = reverse_lazy('list')  # 성공 후 redirect url 지정
    template_name_suffix = '_create'

- bookmark_list.html 내 Add Bookmark 링크 작동되게 처리

<div class="btn-group">
    <!-- urls.py의 name 속성을 템플릿에서 사용 : url 템플릿 태그 "/bookmark/add/"-->
    <a href="{% url 'add' %}" class="btn btn-info">Add Bookmark</a>
</div>

o deailview 만들기

- bookmark>urls.py

from django.urls import path
from .views import BookmarkListView, BookmarkCreateView, BookmarkDetailView

urlpatterns = [
    path('', BookmarkListView.as_view(), name='list'),  # http://127.0.0.1:8000/bookmark/
    path('add/', BookmarkCreateView.as_view(), name='add'),  # http://127.0.0.1:8000/bookmark/add/
    path('detail/<int:pk>/', BookmarkDetailView.as_view(), name='detail'),  # http://127.0.0.1:8000/bookmark/detail/1
]

- views.py

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView
from django.views.generic.detail import DetailView
from django.urls import reverse_lazy
from .models import Bookmark

class BookmarkListView(ListView):
    # 어떤 모델에 대한 제네릭 뷰인지 지정
    model = Bookmark  # db에 등록된 Bookmark 데이터 전체 반환

class BookmarkCreateView(CreateView):
    model = Bookmark
    fields = ['site_name', 'url']
    success_url = reverse_lazy('list')  # 성공 후 redirect url 지정
    template_name_suffix = '_create'

class BookmarkDetailView(DetailView):  # 한 개의 object를 반환
    model = Bookmark

'detail/<int:pk>'  경로 매칭을 위해 사용하는 형식으로, url에서 'bookmark/detail/<int:pk>'로 접속할 경우, models의 Bookmark 컬럼 중 pk 속성인 id 속성과 일치하는 int 형식의 pk 값을 의미하며, <pk>인 경우는 문자열을 의미함

http://localhost:8000/bookmark/detail/1 접속 시 에러

#TemplateDoesNotExist at /bookmark/detail/1
#bookmark/bookmark_detail.html

- bookmark/bookmark_detail.html

<body>
{{ object.site_name}}<br>  # 한 개의 object인 경우
{{ object.url }}
</body>

- bookmark_list.html 의 site_name 항목에 a href link 연결

<div class="btn-group">
    <!-- urls.py의 name 속성을 템플릿에서 사용 : url 템플릿 태그 "/bookmark/add/"-->
    <a href="{% url 'add' %}" class="btn btn-info">Add Bookmark</a>
</div>
<p></p>
<table class="table">
    <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Site</th>
            <th scope="col">URL</th>
            <th scope="col">Modify</th>
            <th scope="col">Delete</th>
        </tr>
    </thead>
    <tbody>
        {% for bookmark in object_list %}  <!-- 규칙 : queryset일 경우 object_list, 1개인 경우 object -->
            <tr>
                <td>{{ forloop.counter }}</td>  <!-- number -->
                <td><a href="{% url 'detail' pk=bookmark.id %}">{{ bookmark.site_name }}</a></td> <!-- bookmark 모델 인스턴스.필드(컬럼)명-->
                <td><a href="{{ bookmark.url }}" target="_blank">{{ bookmark.url }}</a></td>
                <td><a href="#" class="bnt btn-success btn-sm">Modify</a></td>
                <td><a href="#" class="bnt btn-danger btn-sm">Delete</a></td>
            </tr>
        {% endfor %}

    </tbody>
</table>

o updateview 만들기

- views.py

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.detail import DetailView
from django.urls import reverse_lazy
from .models import Bookmark

class BookmarkListView(ListView):
    # 어떤 모델에 대한 제네릭 뷰인지 지정
    model = Bookmark  # db에 등록된 Bookmark 데이터 전체 반환

class BookmarkCreateView(CreateView):
    model = Bookmark
    fields = ['site_name', 'url']
    success_url = reverse_lazy('list')  # 성공 후 redirect url 지정
    template_name_suffix = '_create'  # create와 update는 모델명_form.html을 불러오기 때문에 suffix 변경하여 사용

class BookmarkDetailView(DetailView):
    model = Bookmark

class BookmarkUpdateView(UpdateView):
    model = Bookmark
    fields = ['site_name', 'url']
    template_name_suffix = '_update'

- urls.py

from django.urls import path
from .views import BookmarkListView, BookmarkCreateView, BookmarkDetailView, BookmarkUpdateView

urlpatterns = [
    path('', BookmarkListView.as_view(), name='list'),  # http://127.0.0.1:8000/bookmark/
    path('add/', BookmarkCreateView.as_view(), name='add'),  # http://127.0.0.1:8000/bookmark/add/
    path('detail/<int:pk>/', BookmarkDetailView.as_view(), name='detail'),  # http://127.0.0.1:8000/bookmark/detail/1
    path('update/<int:pk>/', BookmarkUpdateView.as_view(), name='update'),
]

http://localhost:8000/bookmark/update/1/ 로 접속
#TemplateDoesNotExist at /bookmark/update/1/
#bookmark/bookmark_update.html

- bookmark/bookmark_update.html

<body>
  <form action="" method="post">
    {% csrf_token %}
    {{ form.as_p}}
    <input type="submit" value="Update" class="btn btn-info btn-sm">
  </form>
</body>

- bookmark_list.html 의 Modify 부분 수정  a href link

<div class="btn-group">
    <!-- urls.py의 name 속성을 템플릿에서 사용 : url 템플릿 태그 "/bookmark/add/"-->
    <a href="{% url 'add' %}" class="btn btn-info">Add Bookmark</a>
</div>
<p></p>
<table class="table">
    <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Site</th>
            <th scope="col">URL</th>
            <th scope="col">Modify</th>
            <th scope="col">Delete</th>
        </tr>
    </thead>
    <tbody>
        {% for bookmark in object_list %}  <!-- 규칙 : queryset일 경우 object_list, 1개인 경우 object -->
            <tr>
                <td>{{ forloop.counter }}</td>  <!-- number -->
                <td><a href="{% url 'detail' pk=bookmark.id %}">{{ bookmark.site_name }}</a></td> <!-- bookmark 모델 인스턴스.필드(컬럼)명-->
                <td><a href="{{ bookmark.url }}" target="_blank">{{ bookmark.url }}</a></td>
                <td><a href="{% url 'update' pk=bookmark.id %}" class="bnt btn-success btn-sm">Modify</a></td>
                <td><a href="#" class="bnt btn-danger btn-sm">Delete</a></td>
            </tr>
        {% endfor %}
    </tbody>
</table>

http://localhost:8000/bookmark/update/1/ 에서 '네이트' 수정 후 에러 발생

#ImproperlyConfigured at /bookmark/update/1/
#No URL to redirect to.  Either provide a url or define a get_absolute_url method on the Model.

o get_absolute_url() 만들어서 해결하기
get_absolute_url()는 reverse 함수를 이용하여 모델의 개별 데이터 url을 문자열로 반환. 즉, Bookmark 모델의 인스턴스의 상세페이지가 뭔지를 지정하는 방법
- models.py

from django.db import models
from django.urls import reverse  # models 에서는 reverse, class 안에서는 reverse_lazy 사용

class Bookmark(models.Model):
    site_name = models.CharField(max_length=200)
    url = models.URLField(verbose_name='Site URL')

    def __str__(self):
        return '이름 : ' + self.site_name + ', 주소(URL) : ' + self.url

    def get_absolute_url(self):
        return reverse('detail', kwargs={'pk':self.id})  # args=[str(self.id)]) or kwargs={'pk':self.id}

o deleteview 만들기

- views.py

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic.detail import DetailView
from django.urls import reverse_lazy
from .models import Bookmark

class BookmarkListView(ListView):
    # 어떤 모델에 대한 제네릭 뷰인지 지정
    model = Bookmark  # db에 등록된 Bookmark 데이터 전체 반환

class BookmarkCreateView(CreateView):
    model = Bookmark
    fields = ['site_name', 'url']
    success_url = reverse_lazy('list')  # 성공 후 redirect url 지정
    template_name_suffix = '_create'  # create와 update는 모델명_form.html을 불러오기 때문에 suffix 변경하여 사용

class BookmarkDetailView(DetailView):
    model = Bookmark

class BookmarkUpdateView(UpdateView):
    model = Bookmark
    fields = ['site_name', 'url']
    template_name_suffix = '_update'

class BookmarkDeleteView(DetailView):
    model = Bookmark
    success_url = reverse_lazy('list')

- urls.py

from django.urls import path
# from .views import BookmarkListView, BookmarkCreateView, BookmarkDetailView, BookmarkUpdateView, BookmarkDeleteView
from .views import *

urlpatterns = [
    path('', BookmarkListView.as_view(), name='list'),  # http://127.0.0.1:8000/bookmark/
    path('add/', BookmarkCreateView.as_view(), name='add'),  # http://127.0.0.1:8000/bookmark/add/
    path('detail/<int:pk>/', BookmarkDetailView.as_view(), name='detail'),  # http://127.0.0.1:8000/bookmark/detail/1
    path('update/<int:pk>/', BookmarkUpdateView.as_view(), name='update'),
    path('delete/<int:pk>/', BookmarkDeleteView.as_view(), name='delete'),
]

http://localhost:8000/bookmark/delete/1/ 접속 시 에러
#TemplateDoesNotExist at /bookmark/delete/1/

bookmark/bookmark_confirm_delete.html

<body>
  <form action="" method="post">
    {% csrf_token %}
    <div class="alert alert-danger">Do you want to delete Bookmark "{{ object }}" ?</div>
    <input type="submit" value="Delete" class="btn btn-danger">
  </form>
</body>

bookmark_list.html 의 Delete 부분 수정  a href link

<div class="btn-group">
    <!-- urls.py의 name 속성을 템플릿에서 사용 : url 템플릿 태그 "/bookmark/add/"-->
    <a href="{% url 'add' %}" class="btn btn-info">Add Bookmark</a>
</div>
<p></p>
<table class="table">
    <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Site</th>
            <th scope="col">URL</th>
            <th scope="col">Modify</th>
            <th scope="col">Delete</th>
        </tr>
    </thead>
    <tbody>
        {% for bookmark in object_list %}  <!-- 규칙 : queryset일 경우 object_list, 1개인 경우 object -->
            <tr>
                <td>{{ forloop.counter }}</td>  <!-- number -->
                <td><a href="{% url 'detail' pk=bookmark.id %}">{{ bookmark.site_name }}</a></td> <!-- bookmark 모델 인스턴스.필드(컬럼)명-->
                <td><a href="{{ bookmark.url }}" target="_blank">{{ bookmark.url }}</a></td>
                <td><a href="{% url 'update' pk=bookmark.id %}" class="bnt btn-success btn-sm">Modify</a></td>
                <td><a href="{% url 'delete' pk=bookmark.id %}" class="bnt btn-danger btn-sm">Delete</a></td>
            </tr>
        {% endfor %}
    </tbody>
</table>

■ 템플릿 분리와 확장하기

중복되는 html 부분을 하나의 파일(공통 부분을 레이아웃 파일로 만듦)로 합치고, 하위 템플릿은 미리 지정해놓은 템플릿을 확장해서 사용해본다. 

※ bookmark_list.html, bookmark_create.html, bookmark_detail.html, bookmark_update.html, bookmark_confirm_delete.html 파일에 대해 공통된 부분을 하나의 템플릿(base.html)으로 통합한 후 개별로 템플릿 태그를 삽입하여 재작성한다. 

1. project root에 templates 폴더 추가 후 base.html 파일 만든다.


2. settings.py에서 TEMPLATES = [ ] 부분의 'DIRS' 수정

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,  # 이 부분이 있어서 앱 하위에 있는 templates 폴더를 자동으로 찾게 됨
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

3. 템플릿 적용하기

- base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %} {% endblock %}</title>
</head>
<body>
    <h2>즐겨찾기</h2>
    {% block content %}
    {% endblock %}
</body>
</html>

- bookmark_list.html

{% extends 'base.html' %}
{% block title %} Bookmark List {% endblock %}
{% block content %}

<div class="btn-group">
    <!-- urls.py의 name 속성을 템플릿에서 사용 : url 템플릿 태그 "/bookmark/add/"-->
    <a href="{% url 'add' %}" class="btn btn-info">Add Bookmark</a>
</div>
<p></p>
<table class="table">
    <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Site</th>
            <th scope="col">URL</th>
            <th scope="col">Modify</th>
            <th scope="col">Delete</th>
        </tr>
    </thead>
    <tbody>
        {% for bookmark in object_list %}  <!-- 규칙 : queryset일 경우 object_list, 1개인 경우 object -->
            <tr>
                <td>{{ forloop.counter }}</td>  <!-- number -->
                <td><a href="{% url 'detail' pk=bookmark.id %}">{{ bookmark.site_name }}</a></td> <!-- bookmark 모델 인스턴스.필드(컬럼)명-->
                <td><a href="{{ bookmark.url }}" target="_blank">{{ bookmark.url }}</a></td>
                <td><a href="{% url 'update' pk=bookmark.id %}" class="bnt btn-success btn-sm">Modify</a></td>
                <td><a href="{% url 'delete' pk=bookmark.id %}" class="bnt btn-danger btn-sm">Delete</a></td>
            </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

- bookmark_create.html

<!-- django에서는 action= 속성을 비워두는 경우가 많음 입력받거나 입력을 처리하는 것을 같은 뷰에서 처리가 많고,
비워 둔 경우 자기 자신을 의미함-->
{% extends 'base.html' %}
{% block title %} Bookmark ADD {% endblock %}
{% block content %}
<h3>사이트 추가하기</h3>
<form action="" method="post">
  {% csrf_token %}
  {{ form.as_p }}  <!-- 입력, 수정 뷰의 경우 장고에서 자동으로 form을 만들어 출력해줌-->
  <input type="submit" value="Add" class="btn btn-info btn-sm">
</form>
{% endblock %}

- bookmark_detail.html

{% extends 'base.html' %}
{% block title %} Bookmark Detail {% endblock %}
{% block content %}

    {{ object.site_name}}<br>
    {{ object.url }}

{% endblock %}

- bookmark_update.html

{% extends 'base.html' %}
{% block title %} Bookmark Update {% endblock %}
{% block content %}
<form action="" method="post">
  {% csrf_token %}
  {{ form.as_p}}
  <input type="submit" value="Update" class="btn btn-info btn-sm">
</form>
{% endblock %}

- bookmark_confirm_delete.html

{% extends 'base.html' %}
{% block title %} Confirm Delete {% endblock %}
{% block content %}
<form action="" method="post">
  {% csrf_token %}
  <div class="alert alert-danger">Do you want to delete Bookmark "{{ object }}" ?</div>
  <input type="submit" value="Delete" class="btn btn-danger">
</form>
{% endblock %}

4. 부트스트랩(Bootstrap) 적용하여 화면 표시

- base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %} {% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm//dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
    <div class="container">
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <div class="container-fluid">
                <a class="navbar-brand" href="#">Django Bookmark</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
                    data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
                    aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                        <li class="nav-item">
                            <a class="nav-link active" aria-current="page" href="#">Home</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
        <p></p>
        <div class="row">
            <div class="col">
                {% block content %}
                {% endblock %}
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs//dist/umd/popper.min.js"
        integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p"
        crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm//dist/js/bootstrap.min.js"
        integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF"
        crossorigin="anonymous"></script>
</body>
</html>

■ 정적 파일(static file) 적용하기

css, js, image 파일 등을 적용할 경우, 정적파일을 불러올 수 있는 폴더를 명시해야 함

1. settings.py에서 
STATIC_URL = '/static/' 밑에
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]  추가

2. project root 폴더에 'static' 폴더 만들고,

3. 폴더에 style.css파일 생성해본다.

4. 템플릿 전체에 적용하기 위해서는 base.html에 다음과 같이 추가한다.
- 페이지 맨 위에 {% load static %} 추가
- <head>태그 내에
- <link rel="stylesheet" href="{% static 'style.css' %}"> 추가

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %} {% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm//dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link rel="stylesheet" href="{% static 'style.css' %}">

# static 파일이 웹 페이지에 적용되었는지 확인하는 방법은 크롬 개발자도구 sources tap에서 static 이 보이는지 확인

■ 소스를 pythonanywhere.com에 zip 파일 압축으로 배포해보기 (구글링 검색 자료 기준)

1. 소스 파일을 특정 폴더(예로, 바탕화면)에 복사

2. settings.py에서
DEBUG = False  # True

# DEBUG = True 셋팅을 False로 바꿔준다.

ALLOWED_HOSTS = ['haeyou.pythonanywhere.com'] # []

#  pythonanywhere.com에 배포할 것이기 때문에, 자신의 ID.pythonanywhere.com 으로 변경한다.

3. STATIC 파일

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')

# 'STATICFILES_DIRS'로 static 파일이 여러 곳에 있는 경우, 배포에서는 static 파일을 한 폴더에 모아서 처리한다. static 파일을 한 곳으로 모아주는 명령어 python manage.py collectstatic 실행할 수도 있다. (static 파일이 한 곳에 있는 경우, 위와 같이 수정함)

4. 소스 파일(폴더 포함)을 압축한다.

# bookmark, config, static, templates, manage.py

5. pythonanywhere.com 접속 및 로그인

- Bash console에서 작업

12:41 ~ $ pwd
/home/haeyou
12:41 ~ $ python3 --version
Python 3.8.0
12:41 ~ $ mkdir bookmark
12:42 ~ $ ls
README.txt  bookmark
12:42 ~ $

- 압축 파일 업로드 : File 탭 클릭한 후 bookmark 폴더로 클릭, Upload a file 버튼 클릭하여 압축 파일 업로드

- 콘솔 다시 진입 후,

압축을 풀고, unzip bookmark.zip

가상환경 만들고, virtualenv --python=python3.8 venv

장고(django) 설치 후, pip install django

db migrate, python manage.py migrate

관리자 계정 생성, python manage.py createsuperuser

12:42 ~ $ ls
README.txt  bookmark
12:43 ~ $ cd bookmark/
12:43 ~/bookmark $ ls
bookmark.zip
12:43 ~/bookmark $ 
12:43 ~/bookmark $ unzip bookmark.zip

# 압축이 모두 풀린 후

12:44 ~/bookmark $ ls
bookmark  bookmark.zip  config  manage.py  static  templates
12:44 ~/bookmark $ virtualenv --python=python3.8 venv
12:47 ~/bookmark $ source venv/bin/activate
(venv) 12:47 ~/bookmark $
(venv) 12:47 ~/bookmark $ pip install django
....
Successfully installed asgiref-3.4.1 django-3.2.5 pytz-2021.1 sqlparse-0.4.1

(venv) 13:02 ~/bookmark $ ls
bookmark  bookmark.zip  config  manage.py  static  templates  venv

(venv) 13:03 ~/bookmark $ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, bookmark, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying bookmark.0001_initial... OK
  Applying sessions.0001_initial... OK
(venv) 13:03 ~/bookmark $ ls
bookmark  bookmark.zip  config  db.sqlite3  manage.py  static  templates  venv
(venv) 13:03 ~/bookmark $ 
(venv) 13:03 ~/bookmark $ python manage.py createsuperuser
사용자 이름 (leave blank to use 'haeyou'): admin
이메일 주소: 
Password: 
Password (again): 
Superuser created successfully.

- Web 탭으로 이동

(1) pythonanywhere.com 상단의 Web 선택
(2) Add a new web app 선택
(3) Next > Manual Configuration > Python 3.x > Next 를 차례대로 선택

Code:
What your site is running.

Source code: /home/haeyou/bookmark
Working directory: /home/haeyou/
WSGI configuration file: /var/www/haeyou_pythonanywhere_com_wsgi.py

Source code: 부분을 아래와 같이 수정함 
/home/haeyou/bookmark

/var/www/haeyou_pythonanywhere_com_wsgi.py  클릭한 후
프로젝트의 wsgi.py의 내용을 copy&paste 후 아래와 같이 수정하고 저장한 수 뒤로가기

※ /var/www/haeyou_pythonanywhere_com_wsgi.py

import os
import sys

path = '/home/haeyou/bookmark'
if path not in sys.path:
    sys.path.append(path)

from django.contrib.staticfiles.handlers import StaticFilesHandler
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = StaticFilesHandler(get_wsgi_application())

※ WSGI(Web Server Gateway Interface)란?

Web Server가 받은 호출을 Python 어플리케이션에게 전달하고 응답받기 위한 미들웨어(Middleware)로
WAS(Web Application Server)를 구성하는 요소임

WSGI 미들웨어는 WSGI module(Web Server)과 WSGI Process(Gunicorn)로 구성되며, 
Web Server와 Application을 연결시켜준다.

WSGI module과 WSGI Process는 WSGI 전용 프로토콜로 정보를 주고 받는다.

WSGI 미들웨어로는 Bjoern, uWSGI, mod_wsgi, Gunicorn 등이 있다. 

Web Application Server 구성
Gunicorn(WSGI Process) > Django(Framework) > Python module

Virtualenv 부분 수정

/home/haeyou/bookmark/venv/
에러 나지 않았으면 정상적으로 수정된 것임

상단 Reload haeyou.pythonanywhere.com 클릭

6. haeyou.pythonanywhere.com 에 접속하여 테스트를 해본다.

[출처 및 참고 사이트] 오지랖 파이썬 웹 프로그래밍