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을 사용하여 구독한 게시판의 게시글만 볼 수 있는 구독 페이지를 만들어 본다.
