Profileapp Implementation
31강. Profileapp 구현 시작
학습 목표 : Profileapp의 CreateView 부분을 집중적으로 구현하고, 이미지 파일을 다루는 과정에서 어떤 문제가 있는지 확인 후, 문제를 디버깅한다.
step 1. 앞서 만든 모델을 DB에 적용하기 위해, 터미널 창에 입력한다.
$ python manage.py makemigrations
$ python manage.py migrate
step 2.
1) pragmatic/profileApp/views.py
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'
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
복사
2) pragmatic/profileApp/templates/profileApp/create.html (templates, 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" 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
복사
3) pragmatic/accountApp/templates/accountApp/detail.html
{% extends 'base.html' %}
{% block content %}
<div>
<div style="text-align: center; max-width: 500px; margin: 4rem auto;">
<p>
{{ target_user.date_joined }}
</p>
<!-- 유저의 프로필이 있는 경우를 추가한다. -->
{% if target_user.profile %}
<h2 style="font-family: 'NanumPen'">
{{ target_user.profile.nickname }}
</h2>
<!-- 없을 경우, 프로필 생성으로 넘어가도록 한다. -->
{% else %}
<a href="{% url 'profileApp:create' %}">
<h2 style="font-family: 'NanumPen'">
Create Profile
</h2>
</a>
{% endif %}
{% if target_user == user %}
{% endif %}
</div>
</div>
{% endblock %}
HTML
복사
32강. Profileapp 마무리
학습 목표 : ProfileApp의 UpdateView를 만들면서 ProfileApp 구축 마무리를 진행한다.
1) pragmatic/profileApp/views.py
from django.urls import reverse_lazy
from profileApp.forms import ProfileCreationForm
from profileApp.models import Profile
from django.utils.decorators import method_decorator
from django.views.generic import CreateView, UpdateView
from pragmatic.decorators import profile_ownership_required
class ProfileCreateView(CreateView):
# ProfileUpdateView를 추가한다. (Decorator 사용)
@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
success_url = reverse_lazy('accountApp:hello_world')
template_name = 'profileApp/update.html'
Python
복사
2) pragmatic/profileApp/urls.py
from django.urls import path
from profileApp.views import ProfileCreateView, ProfileUpdateView
app_name = 'profileApp'
urlpatterns = [
path('create/', ProfileCreateView.as_view(), name='create'),
# update 경로를 추가한다. 이때 유저의 pk를 토큰으로 받는다.
path('update/<int:pk>', ProfileUpdateView.as_view(), name='update'),
]
Python
복사
2) pragmatic/profileApp/urls.py
from django.urls import path
from profileApp.views import ProfileCreateView, ProfileUpdateView
app_name = 'profileApp'
urlpatterns = [
path('create/', ProfileCreateView.as_view(), name='create'),
# update 경로를 추가한다. 이때 유저의 pk를 토큰으로 받는다.
path('update/<int:pk>', ProfileUpdateView.as_view(), name='update'),
]
Python
복사
3) pragmatic/profileApp/templates/profileApp/update.html (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
복사
4) pragmatic/accountApp/templates/accountApp/detail.html
{% extends 'base.html' %}
{% block content %}
<div>
<div style="text-align: center; max-width: 500px; margin: 4rem auto;">
<p>
{{ target_user.date_joined }}
</p>
<!-- 이미지를 넣기 위해 img 태그를 추가한다. -->
<img src="{{ target_user.profile.image.url }}" alt=""
style="height: 12rem; width: 12rem; border-radius: 20rem; margin-bottom: 2rem;">
{% if target_user.profile %}
{% else %}
{% endif %}
{% if target_user == user %}
{% endif %}
</div>
</div>
{% endblock %}
HTML
복사
5) pragmatic/pragmatic/decorator.py (decorator.py 생성)
from django.contrib.auth.models import User
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
복사
33강. get_success_url 함수 그리고 리팩토링
학습 목표 : 지금까지 짠 코드들의 문제를 조금 해결하고, get_success_url을 통해 동적인 redirect url을 반환해 주도록 변경해 본다.
1) pragmatic/profileApp/views.py
from django.urls import reverse_lazy, reverse
from django.utils.decorators import method_decorator
from django.views.generic import CreateView, UpdateView
from profileApp.forms import ProfileCreationForm
from profileApp.models import Profile
from profileApp.decorators import profile_ownership_required
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.requpytest.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
복사
2) pragmatic/accountApp/templates/accountApp/detail.html
{% 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: 'NanumPen'">
{{ 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 %}
{% if target_user.profile.message %}
<a href="{% url 'profileApp:create' %}">
<h2 style="font-family: 'NanumPen'">
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 %}
HTML
복사
3) pragmatic/accountApp/urls.py
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path
from accountApp.views import hello_world, AccountCreateView, AccountDetailView, AccountUpdateView
from accountApp.views import AccountDeleteView
app_name = "accountApp"
urlpatterns = [
path('hello_world/', hello_world, name='hello_world'),
path('login/', LoginView.as_view(template_name='accountApp/login.html'), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path('create/', AccountCreateView.as_view(), name='create'),
path('detail/<int:pk>', AccountDetailView.as_view(), name='detail'),
path('update/<int:pk>', AccountUpdateView.as_view(), name='update'),
path('delete/<int:pk>', AccountDeleteView.as_view(), name='delete'),
]
Python
복사
Articleapp Implementation
34강. MagicGrid 소개 및 Articleapp 시작
학습 목표 : 핀터레스트의 카드형 레이아웃을 구현하기 위한 라이브럴, MagicGrid를 소개하고, 본인 사이트에 ArticleApp 내부에 간단히 구현해 본다.
※ 참고 사이트
step1. articleApp을 생성한다.
$ python manage.py startapp articleApp
step2. 생성된 articleApp 내부에 여러 기능을 구현한다.
1) settings.py의 INSTALLED_APPS 리스트에 ‘articleApp’, 추가
2) urls.py의 urlpatterns 리스트에 path(’articles/’, include(’articleApp.urls’)), 추가
3) pragmatic/articleApp/urls.py
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
복사
4) pragmatic/articleApp/templates/list.html (templates 및 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 class="item1">
<img src="https://picsum.photos/200/410" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/500" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/100" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/300" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/300" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/320" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/298" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/100" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/425" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/344" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/122" alt="">
</div>
<div class="item1">
<img src="https://picsum.photos/200/398" alt="">
</div>
</div>
<!-- 추가 변경함 -->
<script src="{% static 'js/magicgrid.js' %}"> </script>
{% endblock %}
HTML
복사
5) pragmatic/static/js/magicgrid.js (js 및 magicgrid.js 파일 생성)
MagicGrid github > 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(); }
};
let magicGrid = new MagicGrid({
container: '.container',
animate: true,
gutter: 30,
static: true,
useMin: true
});
magicGrid.listen();
// java 추가
var masonrys = document.getElementByTagName("img");
for (let i = 0; i < masonrys.length; i++){
masonrys[i].addEventListener('load', function (){
magicGrid.positionItems();
}, false);
}
JavaScript
복사
35강. Articleapp 구현
학습 목표 : 34강에서 만들기 시작한 Articleapp의 CRUD 기능을 마무리한다.
※ GitHub 참조 (ArticleApp)
36강. ListView, Pagination 소개 및 적용
학습 목표 : magicgrid 내부에 들어갈 실제 게시물들을 리스트화 해 줄 View, ListView에 대해서 간단히 알아보고, 실제 코드에 ListView와 Pagination을 적용해 본다. 그리고 CommentApp을 만들기 전에 Articleapp Detail 페이지의 세부 디자인을 약간 수정한다.
※ GitHub 참조
1) articleApp/views.py 하단에 ArticleListView를 추가한다.
class ArticleListView(ListView):
model = Article
context_object_name = 'article_list'
template_name = 'articleApp/list.html'
paginate_by = 3
Python
복사
2) articleApp/urls.py의 urlpatterns에서 list 경로를 수정한다.
urlpatterns = [
path('list/', ArticleListView.as_view(), name='list'),
...
]
Python
복사
3) articleApp/templates/articleApp/list.html 하단을 다음과 같이 수정한다.
</style>
<!-- #3.-->
{% if article_list %}
<div class="container">
{% for article in article_list %}
<a href="{% url 'articleApp:detail' 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">
Create Article
</a>
</div>
{% endblock %}
HTML
복사
4) templates/ 하부에 snippets 디렉토리 및 card.html을 생성한다.
<div>
<img src="{{ article.image.url }}" alt="">
</div>
HTML
복사
5) templates/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
복사
6) 추가로 detail.html을 수정한다.
Commentapp Implementation
37강. Mixin 소개 및 Commentapp 구현
학습 목표 : django에서 제공하는 Mixin을 다중 상속 받아 DetailView 안에 form을 포함, 댓글 시스템을 구현해 본다.
Mixin
: Form과 Object 둘 다 사용할 수 있도록 해주는 django 제공 기능 (다중 상속 기능과 흡사)
1.
Create / Delete View
2.
Success_url to related article
3.
Model (article / writer / content / created_at)
※ GitHub 참조 (CommentApp)
▼ 이하 GitHub 참조
※ GitHub 참조
38강. Commentapp 마무리
학습 목표 : View 파트를 수정하지 않고 template 내에서 for문을 통해 댓글을 시각화 하는 작업 및 CommentApp 나머지 부분을 마무리 한다.
Mobile Responsive Layout
39강. 모바일 디버깅, 반응형 레이아웃
학습 목표 : 모바일 기기로 직접 우리가 만든 사이트를 접속하여 모바일에서 실제로 보여지는 화면이 어떤지 확인해보고, 반응형 레이아웃을 위한 설정을 다룬다.
Projectapp Implementation
40강. ProjectApp 구현
학습 목표 : 게시판에 해당하는 projectapp을 구현해 본다.
41강. MultipleObjectMixin을 통한 ProjectApp 마무리
학습 목표 : Project와 Article을 연결하는 작업, 그리고 MultiObjectMixin을 이용해서 ProjectApp의 디테일 페이지를 마무리 하고 같은 방식으로 AccountApp의 디테일 페이지도 수정한다.
Subscribeapp Implementation
42강. RedirectView을 통한 SubscribeApp 시작
학습 목표 : RedirectView 기반의 구독 시스템, 즉 SubscribeApp을 만들어 본다.
43강. Field Lookup을 사용한 구독 페이지 구현
학습 목표 : 장고에서 제공하는 DB Query를 위한 기능, Field Lookup을 사용하여 구독한 게시판의 게시글만 볼 수 있는 구독 페이지를 만들어 본다.