[toc]
- 사진 목록 보여주기 기능
- 각 사진별로 작성자, 텍스트 설명, 댓글달기 버튼이 같이 출력된다.
- 사진 생성, 사진 업데이트 기능
- 사진 상세정보 확인 기능
- 내부에서 댓글 기능
- 사진 삭제 기능
- 로그인 로그아웃 기능
- 회원가입 기능
pip install django
django-admin startproject config .
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
이제 웹 서버를 구동할 준비가 완료되었다.
-
.gitignore
*.pyc *~ /venv __pycache__ db.sqlite3 .DS_Store
-
python manage.py startapp photo
-
config/settings.py
INSTALLED_APPS 변수에 추가INSTALLED_APPS = [ 'photo', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
photo/models/py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
# Create your models here.
class Photo(models.Model):
author = models.ForeignKey(User,on_delete=models.CASCADE, related_name='user_photos')
photo = models.ImageField(upload_to='photos/%Y/%m/%d',default='photos/no_image.png')
text = models.TextField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-updated']
def __str__(self):
return self.author.username + " " + self.created.strftime("%Y-%m-%d %H:%M:%S")
def get_absolute_url(self):
return reverse('photo:photo_detail',args=[str(self.id)])
-
ImageField 를 사용하기 위해 Pillow 모듈 설치하기
pip install pillow
-
makemigrations
명령어를 사용해 모델의 변경 사항을 기록한다.python manage.py makemigrations photo
-
기록한 변경사항을 DB에 적용하기 위해
python manage.py migrate photo 0001
-
관리자 사이트에 모델 등록 시 모델 관리 뷰를 만들기 전에 모델을 테스트 가능하다.
-
photo/admin.py
from django.contrib import admin from .models import Photo admin.site.register(Photo)
- 미디어 파일 추가 : 보안
config/settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
photo/admin.py
from django.contrib import admin
from .models import Photo
class PhotoAdmin(admin.ModelAdmin):
list_display = ['id','author','created','updated']
raw_id_fields = ['author']
list_filter = ['created','updated','author']
search_fields = ['text','created']
ordering = ['-updated','-created']
admin.site.register(Photo,PhotoAdmin)
from django.shortcuts import render
from .models import Photo
from django.views.generic.edit import CreateView,DeleteView,UpdateView
from django.shortcuts import redirect
# Create your views here.
def photo_list(request):
photos = Photo.objects.all()
return render(request,'photo/list.html',{'photos':photos})
class PhotoUploadView(CreateView):
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():
form.instance.save()
return redirect('/')
else:
return self.render_to_response({'form':form})
class PhotoDeleteView(DeleteView):
model = Photo
success_url = '/'
template_name = 'photo/delete.html'
fields = '__all__'
class PhotoUpdateView(UpdateView):
model = Photo
success_url = '/'
template_name = 'photo/update.html'
fields = '__all__'
- 뷰 동작을 위한 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'
urlpatterns=[
path('',photo_list,name="photo_list"),
path('detail/<int:pk>/',DetailView.as_view(model=Photo,template_name='photo/detail.html'),name='photo_detail'),
path('upload/',PhotoUpdateView.as_view(),app_name='photo_upload'),
path('delete/<int:pk>/',PhotoDeleteView.as_view(),app_name='photo_delete'),
path('update/<int:pk>/', PhotoUpdateView.as_view(), app_name='photo_update'),
]
config/urls.py
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('',include('photo.urls')),
path('admin/', admin.site.urls),
]
from django.conf.urls.static import static
from django.conf import settings
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
-
base template 만들기
templates/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>© 2018 Baepeu. Powered By Django 2</p>
</footer>
</div>
</body>
</html>
config/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'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 %}
photo/templates/photo/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.as_p}}
{% csrf_token %}
<input type="submit" class="btn btn-primary" value="Upload">
</form>
</div>
<div class="col-md-2"></div>
</div>
{% endblock %}
photo/templates/photo/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}}</button>
<p>{{object.text|linebreaksbr}}</p>
<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-success btn-sm float-right">
Update</a>
</div>
<div class="col-md-2"></div>
</div>
{% endblock %}
photo/templates/photo/update.html
{% extends 'base.html' %}
{% block title %}
- Upate
{% 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 %}
photo/templates/photo/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}}?
</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 %}
python manage.py startapp accounts
config/settings.py
에 'accounts' 추가
from django.urls import path
from django.contrib.auth import views as auth_view
app_name = 'photo'
urlpatterns = [
path('login/', auth_view.LoginView.as_view(),name='login'),
path('logout/', auth_view.LogoutView.as_view(template_name='registration/logout.html'),name='logout'),
]
config/urls.py
path('accounts/',include('accounts.urls')),
accounts/templates/registration/login.html
{% extends 'base.html' %}
{% block title %}- Login{% 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">Please enter your login informations.</div>
<form action="" method="post">
{{form.as_p}}
{% csrf_token %}
<input class="btn btn-primary" type="submit" value="Login">
</form>
</div>
<div class="col-md-2"></div>
</div>
{% endblock %}
accounts/templates/registration/logout.html
{% extends 'base.html' %}
{% block title %}- Logout{% 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">You have been successfully logged out.</div>
<a class="btn btn-primary" href="{% url 'accounts:login' %}">Click to Login</a>
</div>
<div class="col-md-2"></div>
</div>
{% endblock %}
<li class="nav-item"><a href="{% url 'accounts:logout' %}" class="nav-link">Logout</a></li>
{% else %}
<li class="nav-item"><a href="{% url 'accounts:login' %}" class="nav-link">Login</a></li>
accounts/forms.py
from django.contrib.auth.models import User
from django import forms
class RegisterForm(forms.ModelForm):
password = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Repeat Password', widget=forms.PasswordInput)
class Meta:
model = User
fields = ['username','first_name','last_name','email']
def clean_password2(self):
cd = self.cleaned_data
if cd['password'] != cd['password2']:
raise forms.ValidationError('Passwords not matched!')
return cd['password2']
accounts/views.py
from django.shortcuts import render
from .forms import RegisterForm
# Create your views here.
def register(request):
if request.method == 'POST':
user_form = RegisterForm(request.POST)
if user_form.is_valid():
new_user = user_form.save(commit=False)
new_user.set_password(user_form.cleaned_data['password'])
new_user.save()
return render(request,'registration/register_done.html',{'new_user':new_user})
else:
user_form = RegisterForm()
return render(request,'registration/register.html',{'form':user_form})
from .views import register
path('register/',register,name='register'),
- register, register_done 파일 작성
pip install django-disqus
settings.py 에 'disqus','django.contrib.sites' 등록
python manage.py migrate
-
Disqus 사용을 위해 변수 추가
DISQUS_WEBSITE_SHORTNAME = 'dstagram-django' # <-- 설치앱이름으로다가 SITE_ID = 1
-
댓글 시스템 추가
<div class="row"> <div class="col-md-2"></div> <div class="col-md-8 panel panel-default"> {% load disqus_tags %} {% disqus_show_comments %} </div> <div class="col-md-2"></div> </div>
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
- 데코레이터는 함수형 뷰에서 사용, 믹스인은 클래스형 뷰에서 사용
@login_required
def photo_list(request):
...
class PhotoUploadView(LoginRequiredMixin, CreateView):
...
- AmazonS3FullAccess 설정하기
pip install boto3
pip install django-storage
config/settings.py
INSTALLED_APPS=[
'storages',
]
AWS_ACCESS_KEY_ID = 'Your S3 Key'
AWS_SECRET_ACCESS_KEY = 'Your S3 Secret'
AWS_REGION = 'ap-northeast-2'
AWS_STORAGE_BUCKET_NAME = 'Your Bucket Name'
AWS_S3_CUSTOM_DOMAIN = '%s.s3.%s.amazonaws.com' % (AWS_STORAGE_BUCKET_NAME,AWS_REGION)
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
DEFAULT_FILE_STORAGE = 'config.asset_storage.MediaStorage'
이렇게 된다면 비밀키를 노출할 수 있다.
이를 해결하기 위해 따로 설정하는 방법이 여기에 있다.
- config/asset_storage.py`
from storages.backends.s3boto3 import S3Boto3Storage
class MediaStorage(S3Boto3Storage):
location = 'media'
file_overwrite = False
config/urls.py
# 아마존 s3 서비스 이용으로 인한 제거
# from django.conf.urls.static import static
# from django.conf import settings
#
# urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
- 해당 교과서에서는 heroku 배포하기 위해서는 AWS_ACCESS_CODE 를 git에 올려야 하기 때문에 여기까지 진행