📢 공지사항
home
4️⃣

4주차 (31-43)

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 내부에 간단히 구현해 본다.
※ 참고 사이트
MagicGrid github (JS 라이브러리) : https://github.com/e-oj/Magic-Grid
Lorem Picsum (랜덤 이미지) : https://picsum.photos/
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 파일 생성)
MagicGrid github > https://jsfiddle.net/eolaojo/4pov0rdf/ 에서 HTML / CSS / JS 코드를 가져온다.
{% 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을 사용하여 구독한 게시판의 게시글만 볼 수 있는 구독 페이지를 만들어 본다.