9. articleapp만들기
계정관련된 앱을 모두 완료했고 이제 게시물을 만든다.
먼저 만들 앱은 articleapp이다. articleapp을 만들기 전에 pinterest사이트를 보면 다음과같이 되어있다.
카드들이 사진 크기에 맞춰 사이즈가 다른채로 엇갈려서 배치되어있고 창 사이즈가 줄어들면 그에따라 구조가 변경된다.
이를 위해 javascript magicgrid를 사용한다.
사용할 magicgrid는 아래 url에서 확인할 수 있다. 위의 url는 magicgrid github 아래 url은 readme에 있는 jsfiddle이다.
magicgrid코드는 github/dist/magic-grid.cjs.js파일을 사용한다.
GitHub - e-oj/Magic-Grid: A simple, lightweight Javascript library for dynamic grid layouts.
A simple, lightweight Javascript library for dynamic grid layouts. - GitHub - e-oj/Magic-Grid: A simple, lightweight Javascript library for dynamic grid layouts.
github.com
Magic Grid - JSFiddle - Code Playground
jsfiddle.net
먼저 시작에 앞서 python manage.py startapp articleapp을 실시한다. 이후 settings, pinterest_clone/settings.py, pinterest_clone/urls.py, articleapp/urls.py, aticleapp/models.py, migration을 세팅한다.
<articleapp/models.py>
Article의 경우 작성자, 게시글 제목, 이미지, 내용, 작성날짜를 필드로 갖는다.
작성자는 회원인 사람만 작성할 수 있으므로 User의 Foreignkey가 된다.
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, null=True, related_name='article')
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)
<CreateView>
본격적으로 CRUD를 실시하기 위해 CreateView를 작성한다.
<views.py>
from django.shortcuts import render
from django.views.generic import CreateView
from articleapp.forms import ArticleCreationForm
from articleapp.models import Article
# Create your views here.
class ArticleCreateView(CreateView):
model = Article # 사용할 모델
form_class = ArticleCreationForm #사용할 폼 --> 새로 생성
template_name = 'articleapp/create.html' #사용할 html
# success_url =
def form_valid(self, form): #폼 에러나지 않게 writer 서버로부터 받기
temp_article = form.save(commit=False)
temp_article.writer = self.request.user
temp_article.save()
return super().form_valid(form)
<forms.py 생성>
from django.forms import ModelForm
from articleapp.models import Article
class ArticleCreationForm(ModelForm):
class Meta:
model = Article
fields = ['image', 'title', 'content']
<templates/articleapp/create.html 생성>
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div class="account_create">
<div>
<h4>Create Article</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 %}
<결과화면>
화면이 잘 나온다. 위의 views.py에서 success_url은 주석처리 되어있다. 왜냐하면 이 화면에서 제출을 눌렀을때 어디로 향하게 할지 정하지 않았기 때문이다. 생각해보면 제출을 눌렀을때 article들이 모여있는 페이지로 가면 된다. 이를위해 ArticleListView를 만든다. CreateView의 success_url은 ArticleListView를 만든 후 커스터마이징을 통해 def get_success_url을 추가할 것이다.
2. ListView
<urls.py>
from django.urls import path
from articleapp.views import ArticleCreateView, ArticleListView
app_name = "articleapp"
urlpatterns = [
path('create/', ArticleCreateView.as_view(), name='create'),
path('list/', ArticleListView.as_view(), name='list'),
]
<views.py>
ArticleListview를 작성해 준다.
class ArticleListView(ListView):
model = Article
template_name = 'articleapp/list.html'
<list.html>
여기에서 제일 위에 걸어둔 링크가 생긴다. jsfiddle에 보면 html, css, js세 분류로 나뉘어있다. 여기서 css는 style태그로 감싸 안에 넣어준다. html부분은 그대로 복사해서 밑에 넣어준다. 그리고 static/js/magicgrid.js 파일을 경로에 맞게 만들어준다. 그 안에 위의 github링크에서 dist/magic-grid.cjs.js안에 있는 내용을 그대로 붙여넣기 해주고 jsfiddle의 js영역을 복사하여 가장 하단에 붙여넣기 한다. 이후 static폴더 안의 내용을 사용하는 것이므로 {% load static %}을 해주고 하단에 script태그를 사용하여 사용할 static 경로를 작성해 준다.
{% extends 'base.html' %}
{% load static %} # 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>
<!DOCTYPE html>
<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>
<script src="{% static 'js/magicgrid.js' %}"></script> # static 사용할 경로
</div>
{% endblock %}
<static/js/magicgrid.js>
'use strict';
/**
* @author emmanuelolaojo
* @since 11/11/18
*/
###### 중략 ######
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(); }
};
########### jsfiddle 부분
let magicGrid = new MagicGrid({
container: '.container',
animate: true,
gutter: 30,
static: true,
useMin: true
});
magicGrid.listen();
###############################
//module.exports = MagicGrid; # 이건 없애준다
<화면 결과>
여기까지 진행하고 라우팅 경로로 들어가보면 아래와같이 나타나게 된다. 밑작업이 끝났다.
이제 이미지를 넣어본다. 이미지는 아래사이트에서 바로 가져올 수 있다.
Lorem Picsum
Lorem Ipsum... but for photos
picsum.photos
html상 img태그에서 경로에 아래 주소를 넣어주면 알아서 그림을 가져온다. 이렇게 작성해준다.
<!DOCTYPE html>
<div class="container">
<div class="item1">
<img src="https://picsum.photos/200/300" alt=""> # 이렇게 작성하고 화면 본다
</div>
<div class="item2">2</div>
<div class="item3">3</div>
<div class="item4">4</div>
그러면 이렇게 나타나는것을 확인할 수 있다. 이유는 화면이 로드되는 속도와 이미지가 가져와지는 속도가 차이가 있기 때문이다. 이를 방지하기 위해 magicgrid.js파일에 내용을 추가한다.
<magicgrid.js 내용 추가>
var masonrys = document.getElementsByTagName("img");
# i가 증가함에 따라
for (let i=0; i < masonrys.length; i++){
# 'load' 라는 event추가
masonrys[i].addEventListener('load', function() {
magicGrid.positionItems();
}, false);
}
이후 list.html도 아래와 같이 수정해준다.
<style>
.container div {
width: 280px; # height 삭제
background-color: antiquewhite;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
}
# 이부분 추가: container클래스 안에 img태그의 스타일
.container img {
width: 100%;
}
# 하단에 있던 부분들 삭제
</style>
<결과화면>
이제 로드된 후 잘 들어가는 것을 확인할 수 있다.
그럼 이제 내가 게시물을 올리면 저자리에 들어가게 하면 된다. 이를위해 ArticleListView를 다음과 같이 수정한다. context_object_name = 'article_list'를 추가해주고 list.html에는 for문을 사용한다. CreateView에도 success_url을 설정한다. 또 로그인 한사람만 게시글을 쓸 수 있으므로 method_decorator를 통해 login_required를 적용한다.
<views.py>
@method_decorator(login_required, 'get')
@method_decorator(login_required, 'post')
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleCreationForm
template_name = 'articleapp/create.html'
success_url = reverse_lazy('articleapp:list') #추가
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)
class ArticleListView(ListView):
model = Article
context_object_name = 'article_list' # 추가
template_name = 'articleapp/list.html'
<list.html 수정>
<!DOCTYPE html>
{% if article_list %} # 게시글이 있으면
<div class="container">
{% for article in article_list %} # for문으로 하나씩 빼주기
<div>
<img src="{{ article.image.url }}" alt=""> #article의 url
</div>
{% endfor %}
<script src="{% static 'js/magicgrid.js' %}"></script>
</div>
{% else %}
<h3>Article Not Yet</h3>
{% endif %}
<게시글 등록 전 화면>
<게시글 등록 후 화면>
게시글이 잘 등록되는것을 확인할 수 있다. 이제 게시글을 작성할 수 있는 버튼을 만들어 줘야 한다. 그리고 저 게시글을 클릭하면 저 게시글의 내용을 확인할 수 있게 한다. 게시글 작성 버튼은 로그인 되어 있을때만 활성화되게 할 것이고, 게시글 내용 확인은 ArticleDetailView를 만들어 이동한다. 또한, ArticleDetailView가 만들어지면 ArticleCreatView의 success_url도 변경해 준다. 이를 위해 다음과 같이 작업을 진행한다.
<list.html 추가>
{% if user.is_authenticated %}
<div style="text-align:center;">
<a href="{% url 'articleapp:create' %}" class="btn btn-dark rounded-pill mt-3 mb-3 px-3">
Create Article
</a>
</div>
{% endif %}
<로그인 했을때>
<head.html변경>
<span> # Articles로 향하도록 변경
<a href="{% url 'articleapp:list' %}">Articles</a> |
</span>
<span>nav2</span> |
<span>nav3</span> |
<span>nav4</span> |
{% if user.is_authenticated %}
<a href="{% url 'accountapp:detail' pk=user.pk%}">
<span>MyPage</span> |
</a>
<a href="{% url 'accountapp:logout' %}">
<span>Logout</span>
</a>
<로그인 안하고 바로 사이트 들어갔을때>
<article 추가>
<ListView에 pagenation추가>
화면에 몇개만 보이게 만들것인지 정한다. 그리고 페이지가 생성되게 만들어 준다. 이를위해 templates/snippets/pagination.html파일을 만들어주고 내용을 입력한 뒤 list.html에는 {% include %}를 활용해 넣어준다.
class ArticleListView(ListView):
model = Article
context_object_name = 'article_list'
template_name = 'articleapp/list.html'
paginate_by = 6 # 페이지당 6개 보여주길 원함
<list.html에 include추가>
<!-- pagination -->
{% include 'snippets/pagination.html' %} # include구문 추가
{% if user.is_authenticated %}
<div style="text-align:center;">
<a href="{% url 'articleapp:create' %}" class="btn btn-dark rounded-pill mt-3 mb-3 px-3">
Create Article
</a>
</div>
{% endif %}
<snippets/pagination.html>
# 전페이지가 있을때
<div class="text-center mt-3 mb-3">
{% 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-dark rounded-pill">
{{ 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>
<결과>
<Updateview>
게시글 수정 페이지를 만든다. CreateView와 동일하다.
<views.py>
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})
<update.html>
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div class="account_create">
<div>
<h4>Update Article</h4>
</div>
<form action="{% url 'articleapp: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 %}
update는 article/detail.html에서 진행하므로 update할 수 있는 버튼을 만들어 준다.
<detail.html 수정>
<div>
<p>{{ target_article.title }}</p>
<p>{{ target_article.content }}</p>
</div>
# 해당내용 추가(update로 향하는 버튼)
<div>
<a href="{% url 'articleapp:update' pk=target_article.pk %}" class="btn btn-dark rounded-pill col-6 mt-3">Update</a>
</div>
</div>
<DeleteView>
게시글삭제 페이지를 만든다.
<views.py>
class ArticleDeleteView(DeleteView):
model = Article
context_object_name = 'target_article'
template_name = 'articleapp/delete.html'
success_url = reverse_lazy('articleapp:list')
<urls.py>
from django.urls import path
from articleapp.views import ArticleCreateView, ArticleListView, ArticleDetailView, ArticleUpdateView, ArticleDeleteView
app_name = "articleapp"
urlpatterns = [
path('create/', ArticleCreateView.as_view(), name='create'),
path('list/', ArticleListView.as_view(), name='list'),
path('detail/<int:pk>', ArticleDetailView.as_view(), name="detail"),
path('update/<int:pk>', ArticleUpdateView.as_view(), name="update"),
path('delete/<int:pk>', ArticleDeleteView.as_view(), name='delete'),
]
<delete.html>
{% extends 'base.html' %}
{% block content %}
<div class="account_create">
<div>
<h4>Quit: {{ target_article.title }}</h4>
</div>
<form action="{% url 'articleapp:delete' pk=target_article.pk %}" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
articleapp/detail.html에 delete 버튼을 만들어 준다.
<div>
<a href="{% url 'articleapp:update' pk=target_article.pk %}" class="btn btn-dark rounded-pill col-3 mt-3">Update</a>
# delete버튼 만들기
<a href="{% url 'articleapp:delete' pk=target_article.pk %}" class="btn btn-danger rounded-pill col-3 mt-3">Delete</a>
</div>
이렇게 articleapp은 완료가 되었다. 한가지 더 할것이 있다. pinterest에는 마이페이지에 들어가면 내가 작성한 게시글들이 마이페이지 안에 보이게 만들어져 있다. 이를위해 accountapp의 detail페이지에 article이 보여지도록 한다. 그러면 현재 User모델을 사용하고 있는 accountapp에서 Article모델도 사용해야 한다. 이렇게 한 앱에서 두가지 모델을 사용하기 위해 Mixin이라는 개념을 활용한다.
<accountapp/views.AccountDetailView수정>
#MultipleObjectMixin: 한개의 앱에서 여러개 모델을 사용할때 사용
class AccountDetailView(DetailView, MultipleObjectMixin):
model = User
context_object_name = 'target_user'
template_name = 'accountapp/detail.html'
pagenate_by = 5 # MultipleObjectMixin을 사용하게 되면 pagenate_by를 사용할 수 있다.
#Article모델에서writer가 user인 article만 가져온다.
def get_context_data(self, **kwargs):
object_list = Article.objects.filter(writer=self.get_object())
return super().get_context_data(object_list=object_list, **kwargs)
<templates/accountapp/detail.html 수정>
</div>
# 이부분을 가장 하단에 추가해준다.
<div>
{% include 'snippets/list_fragment.html' with article_list=object_list %}
</div>
{% endblock %}
# snippets/list_fragment.html은 articleapp의 list.html을 그대로 복사해 snippets폴더에 list_fragment라는 이름으로 만든것이다.
# 한번이상 사용되고 사용되는 형식도 동일하기 때문에 따로 파일을 만들어서 불러와 사용하였다.
# list_fragment파일은 만들때, header와 footers는 필요 없기 때문에 지워준다.
<snippets/pagination.html수정>
페이지의url을 그대로 받아올 수 있도록 앞에 href속성의 {% url %} 부분을 삭제한다.
# 그 페이지의 url을 받아올 수 있도록 변경하기
<div class="text-center mt-3 mb-3">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}" class="btn btn-secondary rounded-pill">
{{ page_obj.previous_page_number }}
</a>
{% endif %}
<a href="?page={{ page_obj.number }}" class="btn btn-dark rounded-pill">
{{ page_obj.number }}
</a>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="btn btn-secondary rounded-pill">
{{ page_obj.next_page_number }}
</a>
{% endif %}
</div>
<결과화면>
프로필과 내가 올린 게시글이 잘 나타나는것을 볼 수 있다.