31강. Profileapp 구현 시작
Profileapp의 CreateView 부분을 집중적으로 구현하고, 이미지 파일을 다루는 과정에서 어떤 문제가 있는지 확인 후, 문제를 디버깅합니다.
manage.py makemigrations
view.py 수정.
from django.shortcuts import render
# Create your views here.
from django.urls import reverse_lazy
from django.views.generic import CreateView
from profileapp.forms import ProfileCreationForm
from profileapp.models import Profile
class ProfileCreateView(CreateView):
model = Profile
context_object_name = 'target_profile'
form_class = ProfileCreationForm
success_url = reverse_lazy('accountapp:hello_world')
template_name = 'profileapp/create.html'
Python
복사
profileapp/templates/profileapp 까지 경로 디렉토리를 만들어준다.
작은 profileapp 디렉토리에 create.html 파일 만들기.
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div style="text-align:center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>Profile Create</h4>
</div>
<form action="{% url 'profileapp:create' %}" method="post">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
HTML
복사
urls.py 수정.
from django.urls import path
from profileapp.views import ProfileCreateView
app_name = 'profileapp'
urlpatterns = [
path('create/', ProfileCreateView.as_view(), name='create'),
]
Python
복사
detail.html 상단부분 수정.
{% if target_user.profile %}
<h2 style="font-family: 'NanumSquareB'">
{{ target_user.profile.nickname }}
</h2>
{% else %}
<a href="{% url 'profileapp:create' %}">
<h2 style="font-family: 'NanumSquareB'">
Create Profile
</h2>
</a>
{% endif %}
HTML
복사
profileapp/create.html 폼 태그 수정
<form action="{% url 'profileapp:create' %}" method="post" enctype="multipart/form-data">
HTML
복사
def form_valid(self, form):
temp_profile = form.save(commit=False)
temp_profile.user = self.request.user
temp_profile.save()
return super().form_valid(form)
Python
복사
git add .
git commit -m “django course 31 commit”
32강. Profileapp 마무리
Profileapp의 UpdateView 를 만들면서 Profileapp 구축 마무리를 진행합니다.
views.py에 추가
class ProfileUpdateView(UpdateView):
model = Profile
context_object_name = 'target_profile'
form_class = ProfileCreationForm
success_url = reverse_lazy('accountapp:hello_world')
template_name = 'profileapp/update.html'
Python
복사
urls.py 도 추가
path('update/<int:pk>', ProfileUpdateView.as_view(), name='update'),
Python
복사
update.html 만들기
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div style="text-align:center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>Update Profile</h4>
</div>
<form action="{% url 'profileapp:update' pk=target_profile.pk %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
HTML
복사
detail.html에 추가
<img src="{{target_user.profile.image.url }}" alt=""
style="height: 12rem; width 12rem; border-radius: 20rem; margin-bottom: 2rem;">
....
{{ target_user.profile.nickname }}
<a href="{% url 'profileapp:update' pk=target_user.profile.pk %}">
edit
</a>
....
<h5 style="margin-bottom: 3rem;">
{{ target_user.profile.message }}
</h5>
HTML
복사
pragmatic/urls.py 수정
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accountapp.urls')),
path('profiles/', include('profileapp.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Python
복사
profileapp/decorators.py 생성.
from django.http import HttpResponseForbidden
from profileapp.models import Profile
def profile_ownership_required(func):
def decorated(request, *args, **kwargs):
profile = Profile.objects.get(pk=kwargs['pk'])
if not profile.user == request.user:
return HttpResponseForbidden()
return func(request, *args, **kwargs)
return decorated
Python
복사
profileapp/view.py에 class ProfileUpdateView(UpdateView): 위에 추가.
@method_decorator(profile_ownership_required, 'get')
@method_decorator(profile_ownership_required, 'post')
Python
복사
git add .
git commit -m “django course 32 commit”
33강. get_success_url 함수 그리고 리팩토링
지금까지 짠 코드들의 문제를 조금 해결하고, get_success_url을 통해 동적인 redirect url을 반환해 주도록 변경해봅니다.
profileapp/views.py 수정
class ProfileCreateView(CreateView):
model = Profile
context_object_name = 'target_profile'
form_class = ProfileCreationForm
template_name = 'profileapp/create.html'
def form_valid(self, form):
temp_profile = form.save(commit=False)
temp_profile.user = self.request.user
temp_profile.save()
return super().form_valid(form)
def get_success_url(self):
return reverse('accountapp:detail', kwargs={'pk':self.object.user.pk})
@method_decorator(profile_ownership_required, 'get')
@method_decorator(profile_ownership_required, 'post')
class ProfileUpdateView(UpdateView):
model = Profile
context_object_name = 'target_profile'
form_class = ProfileCreationForm
template_name = 'profileapp/update.html'
def get_success_url(self):
return reverse('accountapp:detail', kwargs={'pk':self.object.user.pk})
Python
복사
detail 수정
{% extends 'base.html' %}
{% block content %}
<div>
<div style="text-align: center; max-width: 500px; margin: 4rem auto;">
{% if target_user.profile %}
<img src="{{target_user.profile.image.url }}" alt=""
style="height: 12rem; width 12rem; border-radius: 20rem; margin-bottom: 2rem;">
<h2 style="font-family: 'NanumSquareB'">
{{ target_user.profile.nickname }}
{% if target_user == user %}
<a href="{% url 'profileapp:update' pk=target_user.profile.pk %}">
edit
</a>
{% endif %}
</h2>
<h5 style="margin-bottom: 3rem;">
{{ target_user.profile.message }}
</h5>
{% else %}
<a href="{% url 'profileapp:create' %}">
<h2 style="font-family: 'NanumSquareB'">
Create Profile
</h2>
</a>
{% else %}
<h2>
닉네임 미설정
</h2>
{% endif %}
{% endif %}
{% if target_user == user %}
<a href="{% url 'accountapp:update' pk=user.pk %}">
<p>
Change Info
</p>
</a>
<a href="{% url 'accountapp:delete' pk=user.pk %}">
<p>
Quit
</p>
</a>
{% endif %}
</div>
</div>
{% endblock %}
Python
복사
git add .
git commit -m “django course 33 commit”
Articleapp Implementation
34강. MagicGrid 소개 및 Articleapp 시작
핀터레스트의 카드형 레이아웃을 구현하기 위한 라이브러리, MagicGrid를 소개하고, 저희 사이트에 ArticleApp 내부에 간단히 구현해봅니다.
MagicGrid github : https://github.com/e-oj/Magic-Grid
Lorem Picsum : https://picsum.photos/
settings.py에 INSTALLED_APPS에 articleapp 추가
articleapp에 urls 만들고 작성.
from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
path('list/', TemplateView.as_view(template_name='articleapp/list.html'), name='list'),
]
Python
복사
articleapp/templates/list.html 만들기.
밑에 내려서 on JSFIDDLE 링크에 html과 css 부분을 가져온다.
{% extends 'base.html' %}
{% load static %}
{% block content %}
<style>
.container div {
width: 280px;
height: 500px;
background-color: antiquewhite;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
}
.container .item1 { height: 200px; }
.container .item4 { height: 800px; }
.container .item6 { height: 600px; }
.container .item11 { height: 400px; }
</style>
<div class="container">
<div class="item1">1</div>
<div class="item2">2</div>
<div class="item3">3</div>
<div class="item4">4</div>
<div class="item5">5</div>
<div class="item6">6</div>
<div class="item7">7</div>
<div class="item8">8</div>
<div class="item9">9</div>
<div class="item10">10</div>
<div class="item11">11</div>
<div class="item12">12</div>
<div class="item13">13</div>
</div>
<script src="{% static 'js/magicgrid.js' %}"></script>
{% endblock %}
HTML
복사
static/js/magicgrid.js 만들기.
코드는 매직그리드 깃헙에서 dist에 magic-grid.cjs.js 파일 코드를 끌어온다.
'use strict';
/**
* @author emmanuelolaojo
* @since 11/11/18
*/
/**
* Validates the configuration object.
*
* @param config - configuration object
*/
var checkParams = function (config) {
var DEFAULT_GUTTER = 25;
var booleanProps = ["useTransform", "center"];
if (!config) {
throw new Error("No config object has been provided.");
}
for(var prop of booleanProps){
if(typeof config[prop] !== "boolean"){
config[prop] = true;
}
}
if(typeof config.gutter !== "number"){
config.gutter = DEFAULT_GUTTER;
}
if (!config.container) { error("container"); }
if (!config.items && !config.static) { error("items or static"); }
};
/**
* Handles invalid configuration object
* errors.
*
* @param prop - a property with a missing value
*/
var error = function (prop) {
throw new Error(("Missing property '" + prop + "' in MagicGrid config"));
};
/**
* Finds the shortest column in
* a column list.
*
* @param cols - list of columns
*
* @return shortest column
*/
var getMin = function (cols) {
var min = cols[0];
for (var col of cols) {
if (col.height < min.height) { min = col; }
}
return min;
};
/**
* @author emmanuelolaojo
* @since 11/10/18
*
* The MagicGrid class is an
* implementation of a flexible
* grid layout.
*/
var MagicGrid = function MagicGrid (config) {
checkParams(config);
if (config.container instanceof HTMLElement) {
this.container = config.container;
this.containerClass = config.container.className;
}
else {
this.containerClass = config.container;
this.container = document.querySelector(config.container);
}
this.static = config.static || false;
this.size = config.items;
this.gutter = config.gutter;
this.maxColumns = config.maxColumns || false;
this.useMin = config.useMin || false;
this.useTransform = config.useTransform;
this.animate = config.animate || false;
this.center = config.center;
this.styledItems = new Set();
};
/**
* Initializes styles
*
* @private
*/
MagicGrid.prototype.initStyles = function initStyles () {
if (!this.ready()) { return; }
this.container.style.position = "relative";
var items = this.items();
for (var i = 0; i < items.length; i++) {
if (this.styledItems.has(items[i])) { continue; }
var style = items[i].style;
style.position = "absolute";
if (this.animate) {
style.transition = (this.useTransform ? "transform" : "top, left") + " 0.2s ease";
}
this.styledItems.add(items[i]);
}
};
/**
* Gets a collection of all items in a grid.
*
* @return {HTMLCollection}
* @private
*/
MagicGrid.prototype.items = function items () {
return this.container.children;
};
/**
* Calculates the width of a column.
*
* @return width of a column in the grid
* @private
*/
MagicGrid.prototype.colWidth = function colWidth () {
return this.items()[0].getBoundingClientRect().width + this.gutter;
};
/**
* Initializes an array of empty columns
* and calculates the leftover whitespace.
*
* @return {{cols: Array, wSpace: number}}
* @private
*/
MagicGrid.prototype.setup = function setup () {
var width = this.container.getBoundingClientRect().width;
var colWidth = this.colWidth();
var numCols = Math.floor(width/colWidth) || 1;
var cols = [];
if (this.maxColumns && numCols > this.maxColumns) {
numCols = this.maxColumns;
}
for (var i = 0; i < numCols; i++) {
cols[i] = {height: 0, index: i};
}
var wSpace = width - numCols * colWidth + this.gutter;
return {cols: cols, wSpace: wSpace};
};
/**
* Gets the next available column.
*
* @param cols list of columns
* @param i index of dom element
*
* @return {*} next available column
* @private
*/
MagicGrid.prototype.nextCol = function nextCol (cols, i) {
if (this.useMin) {
return getMin(cols);
}
return cols[i % cols.length];
};
/**
* Positions each item in the grid, based
* on their corresponding column's height
* and index then stretches the container to
* the height of the grid.
*/
MagicGrid.prototype.positionItems = function positionItems () {
var ref = this.setup();
var cols = ref.cols;
var wSpace = ref.wSpace;
var maxHeight = 0;
var colWidth = this.colWidth();
var items = this.items();
wSpace = this.center ? Math.floor(wSpace / 2) : 0;
this.initStyles();
for (var i = 0; i < items.length; i++) {
var col = this.nextCol(cols, i);
var item = items[i];
var topGutter = col.height ? this.gutter : 0;
var left = col.index * colWidth + wSpace + "px";
var top = col.height + topGutter + "px";
if(this.useTransform){
item.style.transform = "translate(" + left + ", " + top + ")";
}
else{
item.style.top = top;
item.style.left = left;
}
col.height += item.getBoundingClientRect().height + topGutter;
if(col.height > maxHeight){
maxHeight = col.height;
}
}
this.container.style.height = maxHeight + this.gutter + "px";
};
/**
* Checks if every item has been loaded
* in the dom.
*
* @return {Boolean} true if every item is present
*/
MagicGrid.prototype.ready = function ready () {
if (this.static) { return true; }
return this.items().length >= this.size;
};
/**
* Periodically checks that all items
* have been loaded in the dom. Calls
* this.listen() once all the items are
* present.
*
* @private
*/
MagicGrid.prototype.getReady = function getReady () {
var this$1 = this;
var interval = setInterval(function () {
this$1.container = document.querySelector(this$1.containerClass);
if (this$1.ready()) {
clearInterval(interval);
this$1.listen();
}
}, 100);
};
/**
* Positions all the items and
* repositions them whenever the
* window size changes.
*/
MagicGrid.prototype.listen = function listen () {
var this$1 = this;
if (this.ready()) {
var timeout;
window.addEventListener("resize", function () {
if (!timeout){
timeout = setTimeout(function () {
this$1.positionItems();
timeout = null;
}, 200);
}
});
this.positionItems();
}
else { this.getReady(); }
};
// 이 부분은 밑에 내려서 on JSFIDDLE 링크에 자바스크립트 부분을 가져온다.
let magicGrid = new MagicGrid({
container: '.container',
animate: true,
gutter: 30,
static: true,
useMin: true
});
// 직접입력
var masonrys = document.getElementsByTagName("img");
for (let i = 0; i < masonrys.length; i++) {
masonrys[i].addEventListener('load', function () {
magicGrid.positionItems();
}, false);
}
magicGrid.listen();
JavaScript
복사
list.html 이미지 추가해주기
{% extends 'base.html' %}
{% load static %}
{% block content %}
<style>
.container div {
width: 250px;
background-color: antiquewhite;
display: flex;
justify-content: center;
align-items: center;
border-radius: 1rem;
}
.container img {
width: 100%;
border-radius: 1rem;
}
</style>
<div class="container">
<div class="item1">
<img src="https://picsum.photos/200/300" alt="">
</div>
<div>
<img src="https://picsum.photos/200/340" alt="">
</div>
<div>
<img src="https://picsum.photos/200/260" alt="">
</div>
<div>
<img src="https://picsum.photos/200/400" alt="">
</div>
<div>
<img src="https://picsum.photos/200/510" alt="">
</div>
<div>
<img src="https://picsum.photos/200/300" alt="">
</div>
<div>
<img src="https://picsum.photos/200/258" alt="">
</div>
<div>
<img src="https://picsum.photos/200/288" alt="">
</div>
<div>
<img src="https://picsum.photos/200/448" alt="">
</div>
<div>
<img src="https://picsum.photos/200/320" alt="">
</div>
<div>
<img src="https://picsum.photos/200/298" alt="">
</div>
<div>
<img src="https://picsum.photos/200/298" alt="">
</div>
<div>
<img src="https://picsum.photos/200/428" alt="">
</div>
</div>
<script src="{% static 'js/magicgrid.js' %}"></script>
{% endblock %}
HTML
복사
git add .
git commit -m “django course 34 commit”
35강. Article 모델 생성 오류 수정
created_at = models.DateField(auto_now_add=True, null=True)
35강. Articleapp 구현
models.py 작성
from django.contrib.auth.models import User
from django.db import models
# Create your models here.
class Article(models.Model):
writer = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='article', null=True)
title = models.CharField(max_length=200, null=True)
image = models.ImageField(upload_to='article/', null=False)
content = models.TextField(null=True)
created_at = models.DateField(auto_now_add=True, null=True)
Python
복사
forms.py를 만들어준다.
from django.forms import ModelForm
from articleapp.models import Article
class ArticleCreationForm(ModelForm):
class Meta:
model = Article
fields = ['title', 'image', 'content']
Python
복사
views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
# Create your views here.
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.generic import CreateView, DetailView
from articleapp.forms import ArticleCreationForm
from articleapp.models import Article
@method_decorator(login_required, 'get')
@method_decorator(login_required, 'post')
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleCreationForm
template_name = 'articleapp/create.html'
def form_valid(self, form):
temp_article = form.save(commit=False)
temp_article.writer = self.request.user
temp_article.save()
return super().form_valid(form)
def get_success_url(self):
return reverse('articleapp:detail', kwargs={'pk': self.object.pk})
class ArticleDetailView(DetailView):
model = Article
context_object_name = 'target_article'
template_name = 'articleapp/detail.html'
Python
복사
create.html 만들기
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div style="text-align:center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>Article Create</h4>
</div>
<form action="{% url 'articleapp:create' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
HTML
복사
detail.html 만들기
{% extends 'base.html' %}
{% block content %}
<div>
<div style="text-align: center; max-width: 500px; margin: 4rem auto;">
<h1>
{{ target_article.title }}
</h1>
<img src="{{ target_article.image.url }}" alt="">
<p>
{{ target_article.content }}
</p>
</div>
</div>
{% endblock %}
HTML
복사
header.html nav1수정
<a href="{% url 'articleapp:list' %}">
<span>Articles</span>
</a>
HTML
복사
list.html 아래쪽에 추가
....
<div>
<a href="{% url 'articleapp:create' %}" class="btn btn-dark rounded-pill col-3 mt-3 mb-3">
Create Article
</a>
</div>
....
HTML
복사
urls.py 수정
from django.urls import path
from django.views.generic import TemplateView
from articleapp.views import ArticleCreateView, ArticleDetailView
app_name = 'articleapp'
urlpatterns = [
path('list/', TemplateView.as_view(template_name='articleapp/list.html'), name='list'),
path('create/', ArticleCreateView.as_view(), name='create'),
path('detail/<int:pk>', ArticleDetailView.as_view(), name='detail'),
]
Python
복사
views.py ArticleUpdateView 만들기
@method_decorator(article_ownership_required, 'get')
@method_decorator(article_ownership_required, 'post')
class ArticleUpdateView(UpdateView):
model = Article
context_object_name = 'target_article'
form_class = ArticleCreationForm
template_name = 'articleapp/update.html'
def get_success_url(self):
return reverse('articleapp:detail', kwargs={'pk': self.object.pk})
Python
복사
articleapp폴더에 decorators 복붙.
from django.http import HttpResponseForbidden
from articleapp.models import Article
def article_ownership_required(func):
def decorated(request, *args, **kwargs):
article = Article.objects.get(pk=kwargs['pk'])
if not article.writer == request.user:
return HttpResponseForbidden()
return func(request, *args, **kwargs)
return decorated
Python
복사
detail.html 코드 추가
....
<a href="{% url 'articleapp:update' pk=target_article.pk %}">
<p>Update Article</p>
</a>
....
HTML
복사
urls에 추가
path('update/<int:pk>', ArticleUpdateView.as_view(), name='detail'),
update.html 생성.
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div style="text-align:center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>Article Update</h4>
</div>
<form action="{% url 'article:update' pk=target_article.pk %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
HTML
복사
@method_decorator(article_ownership_required, 'get')
@method_decorator(article_ownership_required, 'post')
class ArticleDeleteView(DeleteView):
model = Article
context_object_name = 'target_article'
success_url = reverse_lazy('articleapp:list')
template_name = 'articleapp/delete.html'
Python
복사
delete.html
{% extends 'base.html' %}
{% block content %}
<div style="text-align:center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>Delete Article : {{ target_article.title }}</h4>
</div>
<form action="{% url 'accountapp:delete' pk=target_article.pk %}" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-danger rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
HTML
복사
detail.html에 코드 추가
<a href="{% url 'articleapp:delete' pk=target_article.pk %}">
<p>Delete Article</p>
</a>
HTML
복사
urls에 추가
path('delete/<int:pk>', ArticleDeleteView.as_view(), name='delete'),
git add .
git commit -m “django course 35 commit”
36강. ListView, Pagination 소개 및 적용
magicgrid 내부에 들어갈 실제 게시물들을 리스트화해줄 View, ListView에 대해서 간단히 알아보고, 실제 코드에 ListView와 Pagination을 적용해봅니다. 그리고 CommentApp을 만들기 전에 Articleapp Detail 페이지의 세부 디자인을 약간 수정합니다.
views.py 에 추가
....
class ArticleListView(ListView):
model = Article
context_object_name = 'article_list'
template_name = 'articleapp/list.html'
paginate_by = 5
Python
복사
urls.py list 부분 수정
path('list/', ArticleListView.as_view(), name='list'),
templates/articleapp/snippets/card.html 만들기
<div>
<img src="{{ article.image.url }}" alt="">
</div>
HTML
복사
스타일 아래부분 list.html 수정.
{% if article_list %}
<div class="container">
{% for article in article_list %}
<a href="{% url 'articleapp:dtail' pk=article.pk %}">
{% include 'snippets/card.html' with article=article %}
</a>
{% endfor %}
</div>
<script src="{% static 'js/magicgrid.js' %}"></script>
{% else %}
<div class="text-center">
<h1>No Articles YET!</h1>
</div>
{% endif %}
{% include 'snippets/pagination.html' with page_obj=page_obj %}
<div style="text-align: center">
<a href="{% url 'articleapp:create' %}" class="btn btn-dark rounded-pill col-3 mt-3 mb-3">
Create Article
</a>
</div>
HTML
복사
snippets 안에 pagination.html 만들기
<div style="text-align: center; margin: 1rem 0;">
{% if page_obj.has_previous %}
<a href="{% url 'articleapp:list' %}?page={{ page_obj.previous_page_number }}"
class="btn btn-secondary rounded-pill">
{{ page_obj.previous_page_number }}
</a>
{% endif %}
<a href="{% url 'articleapp:list' %}?page={{ page_obj.number }}"
class="btn btn-secondary rounded-pill active">
{{ page_obj.number }}
</a>
{% if page_obj.has_next %}
<a href="{% url 'articleapp:list' %}?page={{ page_obj.next_page_number }}"
class="btn btn-secondary rounded-pill">
{{ page_obj.next_page_number }}
</a>
{% endif %}
</div>
HTML
복사
detail.html 수정
<div style="text-align: center; max-width: 700px; margin: 4rem auto">
<h1>
{{ target_article.title }}
</h1>
<h5>
{{ target_article.writer.profile.nickname }}
</h5>
<hr>
<img style="width: 100%; border-radius: 1rem; margin: 2rem 0;"
src="{{ target_article.image.url }}" alt="">
<p>
{{ target_article.content }}
</p>
{% if target_article.writer == user %}
<a href="{% url 'articleapp:update' pk=target_article.pk %}"
class="btn btn-primary rounded-pill col-3">
Update
</a>
<a href="{% url 'articleapp:delete' pk=target_article.pk %}"
class="btn btn-danger rounded-pill col-3">
Delete
</a>
{% endif %}
<hr>
</div>
HTML
복사
git add .
git commit -m “django course 36 commit”
Commentapp Implementation
37강. Mixin 소개 및 Commentapp 구현
django에서 제공하는 Mixin을 다중 상속 받아 DetailView 안에 form을 포함, 댓글시스템을 구현해봅니다.
pragmatic/settings.py INSTALLED_APPS 에 'commentapp', 추가
pragmatic/urls.py urlpatterns 에 path('comments/', include('commentapp.urls')), 추가
commentapp/urls.py 생성.
from django.urls import path
app_name = 'commentapp'
urlpatterns = [
]
Python
복사
views.py 작성.
class CommentCreateView(CreateView):
model = Comment
form_class = CommentCreationForm
template_name = 'commentapp/create.html'
def form_valid(self, form):
temp_comment = form.save(commit=False)
temp_comment.article = Article.objects.get(pk=self.request.POST['article_pk'])
temp_comment.writer = self.requset.user
temp_comment.save()
return super().form_valid(form)
def get_success_url(self):
return reverse('articleapp:detail', kwargs={'pk': self.object.article.pk})
Python
복사
models.py 작성.
class Comment(models.Model):
article = models.ForeignKey(Article, on_delete=models.SET_NULL, null=True, related_name='comment')
writer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='comment')
content = models.TextField(null=False)
created_at = models.DateTimeField(auto_now=True)
Python
복사
commentapp/forms.py 생성.
from django.forms import ModelForm
from commentapp.models import Comment
class CommentCreationForm(ModelForm):
class Meta:
model = Comment
fields = ['content']
Python
복사
commentapp/urls.py path('create/', CommentCreateView.as_view(), name='create'), 추가
commentapp/templates/commentapp/create.html 생성
{% load bootstrap4 %}
{% block content %}
<div style="text-align: center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>Comment Create</h4>
</div>
<form action="{% url 'commentapp:create' %}" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% if user.is_authenticated %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
{% else %}
<a href="{% url 'accountapp:login' %}?next={{ request.path }}"
class="btn btn-dark rounded-pill col-6 mt-3">
Login
</a>
{% endif %}
<input type="hidden" name="article_pk" value="{{ article.pk }}">
</form>
</div>
{% endblock %}
HTML
복사
articleapp/detail.html <hr> 밑에 추가
{% include 'commentapp/create.html' with article=target_article %}
articleapp/views.py ArticleDetailView 수정
class ArticleDetailView(DetailView, FormMixin):
model = Article
form_class = CommentCreationForm
context_object_name = 'target_article'
template_name = 'articleapp/detail.html'
Python
복사
git add .
git commit -m “django course 37 commit”
38강. Commentapp 마무리
View 파트를 수정하지 않고 template 내에서 for 문을 통해 댓글을 시각화 하는 작업 및 CommentApp 나머지 부분을 마무리합니다.
articleapp/detail.html <hr>과 ‘댓글 작성칸’ 사이 코드 추가
{% for comment in target_article.comment.all %}
{% include 'commentapp/detail.html' with comment=comment %}
{% endfor %}
HTML
복사
commentapp/detail.html 생성
<div style="border: 1px solid; text-align: left; padding: 4%; margin: 1rem 0;
border-radius: 1rem; border-color: #bbb;">
<div>
<strong>
{{ comment.writer.profile.nickname }}
</strong>
   
{{ comment.created_at }}
</div>
<div style="margin: 1rem 0;">
{{ comment.content }}
</div>
{% if comment.writer == user %}
<div style="text-align: right">
<a href="{% url 'commentapp:delete' pk=comment.pk%}"
class="btn btn-danger rounded-pill">
Delete
</a>
</div>
{% endif %}
</div>
HTML
복사
commentapp/views.py 마지막에 추가
class CommentDeleteView(DeleteView):
model = Comment
context_object_name = 'target_comment'
template_name = 'commentapp/delete.html'
def get_success_url(self):
return reverse('articleapp:detail', kwargs={'pk': self.object.article.pk})
Python
복사
commentapp/urls.py path('delete/<int:pk>', CommentDeleteView.as_view(), name='delete'), 추가.
commentapp/delete.html 생성.
{% extends 'base.html' %}
{% block content %}
<div style="text-align:center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>Delete Comment : {{ target_comment.content }}</h4>
</div>
<form action="{% url 'commentapp:delete' pk=target_comment.pk %}" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-danger rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
HTML
복사
from django.http import HttpResponseForbidden
from commentapp.models import Comment
def comment_ownership_required(func):
def decorated(request, *args, **kwargs):
comment = Comment.objects.get(pk=kwargs['pk'])
if not comment.writer == request.user:
return HttpResponseForbidden()
return func(request, *args, **kwargs)
return decorated
Python
복사
commentapp/views.py class CommentDeleteView(DeleteView): 윗 부분에 작성.
@method_decorator(comment_ownership_required, 'get')
@method_decorator(comment_ownership_required, 'post')
Python
복사
git add .
git commit -m “django course 38 commit”
Mobile Responsive Layout
39강. 모바일 디버깅, 반응형 레이아웃
모바일 기기로 직접 우리가 만든 사이트를 접속하여 모바일에서 실제로 보여지는 화면이 어떤지 확인해보고, 반응형 레이아웃을 위한 설정을 다룹니다.
pragmatic/settings.py ALLOWED_HOSTS = [] -> ALLOWED_HOSTS = ['*'] 수정
templates/head.html <meta charset="UTF-8"> 밑에
<meta name="viewpoint" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 추가
articleapp/list.html 스타일 태그 변경.
.container {
padding: 0;
margin: 0, auto;
}
.container a {
width: 45%;
max-width: 250px;
}
.container div {
display: flex;
justify-content: center;
align-items: center;
border-radius: 1rem;
}
.container img {
width: 100%;
border-radius: 1rem;
}
HTML
복사
static/base.css 밑에 추가
@mdeia screen and (max-width: 500px) {
html {
font-size: 13px;
}
}
CSS
복사
static/js/magicgrid.js 수정
let magicGrid = new MagicGrid({
container: '.container',
animate: true,
gutter: 12,
static: true,
useMin: true
});
JavaScript
복사
git add .
git commit -m “django course 39 commit”
Projectapp Implementation
40강. ProjectApp 구현
게시판에 해당하는 projectapp 을 구현해봅니다.
1.
Create, Detail, List View
2.
Success_url to realated Project
3.
Login_required to CreateView
4.
Model (: title / description / image / created_at)
settings INSTALLED_APP에 추가
'projectapp',
pragmatic/urls.py 에 추가
path('projects/', include('projectapp.urls')),
projectapp/urls.py 만들기
from django.urls import path
from projectapp.views import ProjectListView, ProjectCreateView, ProjectDetailView
app_name = 'projectapp'
urlpatterns = [
path('list/', ProjectListView.as_view(), name='list'),
path('create/', ProjectCreateView.as_view(), name='create'),
path('detail/<int:pk>', ProjectDetailView.as_view(), name='detail'),
]
Python
복사
views.py 작성
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
# Create your views here.
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.generic import CreateView, DetailView, ListView
from projectapp.forms import ProjectCreationForm
from projectapp.models import Project
@method_decorator(login_required, 'get')
@method_decorator(login_required, 'post')
class ProjectCreateView(CreateView):
model = Project
form_class = ProjectCreationForm
template_name = 'projectapp/create.html'
def get_success_url(self):
return reverse('projectapp:detail', kwargs={'pk': self.object.pk})
class ProjectDetailView(DetailView):
model = Project
context_object_name = 'target_project'
template_name = 'projectapp/detail.html'
class ProjectListView(ListView):
model = Project
context_object_name = 'project_list'
template_name = 'projectapp/list.html'
paginate_by = 25
Python
복사
models.py 만들기
from django.db import models
# Create your models here.
class Project(models.Model):
image = models.ImageField(upload_to='project/', null=False)
title = models.CharField(max_length=20, null=False)
description = models.CharField(max_length=200, null=True)
created_at = models.DateTimeField(auto_now=True)
Python
복사
forms.py 만들기
from django.forms import ModelForm
from projectapp.models import Project
class ProjectCreationForm(ModelForm):
class Meta:
model = Project
fields = ['image', 'title', 'description']
Python
복사
create.html
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div style="text-align:center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>Create Project</h4>
</div>
<form action="{% url 'projectapp:create' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
HTML
복사
detail.html
{% extends 'base.html' %}
{% block content %}
<div>
<div style="text-align: center; max-width: 500px; margin: 4rem auto;">
<img src="{{target_user.profile.image.url }}" alt=""
style="height: 12rem; width: 12rem; border-radius: 20rem; margin-bottom: 2rem; object-fit: cover;">
<h2 style="font-family: 'NanumSquareB'">
{{ target_project.title }}
</h2>
<h5 style="margin-bottom: 3rem;">
{{ target_project.description }}
</h5>
</div>
</div>
{% endblock %}
HTML
복사
list.html
{% extends 'base.html' %}
{% load static %}
{% block content %}
<style>
.container {
padding: 0;
margin: 0, auto;
}
.container a {
width: 45%;
max-width: 250px;
}
.container div {
display: flex;
justify-content: center;
align-items: center;
border-radius: 1rem;
}
.container img {
width: 100%;
border-radius: 1rem;
}
</style>
{% if project_list %}
<div class="container">
{% for project in project_list %}
<a href="{% url 'projectapp:dtail' pk=project.pk %}">
{% include 'snippets/card_project.html' with project=project %}
</a>
{% endfor %}
</div>
<script src="{% static 'js/magicgrid.js' %}"></script>
{% else %}
<div class="text-center">
<h1>No Projects YET!</h1>
</div>
{% endif %}
{% include 'snippets/pagination.html' with page_obj=page_obj %}
<div style="text-align: center">
<a href="{% url 'projectapp:create' %}" class="btn btn-dark rounded-pill mt-3 mb-3 px-3">
Create Project
</a>
</div>
{% endblock %}
HTML
복사
templates/snippets/card_project.html
<div style="display: block; text-align: center">
<img src="{{ project.image.url }}" alt="">
<h5 class="mt-2" style="font-family: 'NanumSquareB';">
{{ project.title | truncatechars:8 }}
</h5>
</div>
HTML
복사
base.css 에 코드 추가
a {
color: black;
}
a:hover{
color: black;
text-decoration: none;
}
.pragmatic_header_nav {
margin: 0 0.5rem;
}
.pragmatic_header_navbar {
margin: 1rem 0;
}
CSS
복사
header.html 수정
<div class="pragmatic_header">
<div>
<h1 class="pragmatic_logo">Pragmatic</h1>
</div>
<div class="pragmatic_header_navbar">
<a href="{% url 'articleapp:list' %}" class="pragmatic_header_nav">
<span>Articles</span>
</a>
<a href="{% url 'projectapp:list' %}" class="pragmatic_header_nav">
<span>Projects</span>
</a>
{% if not user.is_authenticated %}
<a href="{% url 'accountapp:login' %}?next={{ request.path }}" class="pragmatic_header_nav">
<span>login</span>
</a>
<a href="{% url 'accountapp:create' %}" class="pragmatic_header_nav">
<span>SignUp</span>
</a>
{% else %}
<a href="{% url 'accountapp:detail' pk=user.pk %}" class="pragmatic_header_nav">
<span>MyPage</span>
</a>
<a href="{% url 'accountapp:logout' %}?next={{ request.path }}" class="pragmatic_header_nav">
<span>logout</span>
</a>
{% endif %}
</div>
</div>
HTML
복사
git add .
git commit -m “django course 40 commit”