[django] 03-16 기능 추가 관련 질문

안녕하세요 선생님, 책 구매해서 열심히 쭉쭉 따라가고 있습니다. 어렵긴 하지만 재미있네요 ㅎㅎ..
3장 끝까지 다 나가고 기능을 조금 추가하고 싶어서 수정 중인데 몇 가지 궁금한 것이 있어 질문 드립니다.

(1) 회원가입 기능 추가
현재 회원가입은 id, pw, email인데 추가적으로 사용자에게 몇 가지 input을 더 받고 싶습니다.
실명을 받기 위해서는 아래와 같이 수정하면 될까요? 혹은 별도의 수정이 필요한가요?

# common/forms.py
class UserForm(UserCreationForm):
    ...
    realname = forms.Field(label="실명")

    class Meta:
        model = User
        fields = ("username", "email", "realname")

(2) 추가한 input 항목을 선택항목으로 수정 가능한지?
input 항목을 추가하되 필수항목이 아니도록 하고 싶은데 회원가입 같은 경우는 별도의 model.py를 사용하지 않아서 어떻게 해결해야하는지 궁금합니다.
정 안되면 사용자에게 특정 문자열을 받아서 그걸로 사용 여부를 판단하는 방법도 있을 것 같긴한데, 그냥 공란으로 비울 수 있는 방법이 있는지 궁금합니다.

(3) 페이징 기법 에러 (해결)
메인 화면을 추가하고 싶어 메인화면을 만들고config/urls.py에서 ' '을 새로 만든 메인화면으로 링크를 걸었더니
questionlist.html에서 페이징 기법처리하여 만들어진 버튼들이 새로 만든 메인화면으로 넘어갑니다.
혹시 어느 부분을 건드려야하는지 알려주시면 감사하겠습니다.

(현재 수정한 부분)*

# pybo/urls.py
...
urlpatterns = [
    # base_views
    path('', base_views.main, name='main'),
    path('all/', base_views.index, name='index'), # 여기 수정
    path('<int:question_id>/', base_views.detail, name='detail'),
        ...
]
# base_views.py
...
def main(request): # main.html로 이동하기 위해 추가
    return render(request, 'pybo/main.html')
...

(4) 카테고리 기능
게시글 생성 시 카테고리를 입력받게 만들고 분류하는 것 까지는 다 만들었습니다.
각 카테고리별 html파일을 따로 만들어서 기존 question_list.html의 코드를 가져와 kw를 활용하여 별도의 게시판을 만들면 될까요?

shin 148

M 2021년 7월 22일 6:06 오후

목록으로
6개의 답변이 있습니다. 1 / 1 Page

[자문자답]
3번 해결했습니다.
계속 pybo/url.py만 확인했는데, config/url.py에서 index 설정을 제대로 건들지 못했네요.

shin

M 2021년 7월 22일 6:02 오후

안녕하세요.

1)번은 realname 속성을 User 모델에도 추가해 주셔야 합니다.
2)번은 realname = forms.Field(label="실명", blank=True) 처럼 blank 옵션을 사용하시면 됩니다.
4)번은 카테고리 때문에 기존 작성한 파일들을 모두 중복해서 만들 필요가 없습니다. 카테고리에 대한 글들은 이 곳 게시판에도 있으니 참고하시면 좋을것 같습니다.

박응용

2021년 7월 22일 7:26 오후

1. 앞에서 말씀하신 User 모델이 정확히 어떤 부분을 말씀하신건지 알려주실 수 있나요? -> 혹시 sqlite를 통해서 추가를 해주면 되는건가요? - shin님, M 2021년 7월 22일 10:20 오후 추천 , 대댓글
@shin님 다음 URL 참고해 보세요. https://pybo.kr/pybo/question/detail/491/ - 박응용님, 2021년 7월 22일 10:37 오후 추천 , 대댓글

카테고리 사용하시는 것을 찾아 보고 왔는데 제가 확인 전에 구현한 방법과 방식이 달라서 혹시 그대로 써도 될지, 혹은 변경을 해야될지 여쭤보고 싶어서 남깁니다.

# https://pybo.kr/pybo/question/detail/616/
@login_required(login_url='common:login')
def question_create(request, category_name):
    """
    pybo 질문등록
    """
    category = get_object_or_404(Category, name=category_name)
    if request.method == "POST":
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.author = request.user  # 추가한 속성 author 적용
            question.create_date = timezone.now()
            question.category = category
            question.save()
            return redirect('pybo:question_list', category_name)
    else:
        form = QuestionForm()
    context = {'form': form, 'category': category}
    return render(request, 'pybo/question_form.html', context)

아래는 제가 구현한 방식입니다.

# question_form.html
            <div class="form-group">
                    <label for="subject">제목</label>
                    <input name="subject" id="subject" class="form-control" type="text" value="{{ form.subject.value|default_if_none:'' }}" >
            </div>

            <div class="form-group">
                    <label for="category">카테고리</label>

                    <select class="form-control" name="category" id="category">
                        <option value="free">자유게시판</option>
                        <option value="practice">연습문제</option>
                        <option value="portfolio">포트폴리오</option>
                        <option value="qna">Q & A</option>
                        <option value="notice">공지사항</option>
                    </select>
            </div>

            <div class="form-group">
                    <label for="content">내용</label>
                    <textarea name="content" id="content" rows="10" class="form-control">{{ form.content.value|default_if_none:'' }}</textarea>
            </div>
            <button type="submit" class="btn btn-primary">저장히기</button>
# models.py
class Question(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author_question')
    subject = models.CharField(max_length=200)
    category = models.TextField()
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(User, related_name='voter_question')

shin

M 2021년 7월 22일 9:52 오후

사용하는데 불편하지 않으면 그대로 하셔도 됩니다. 다만, 파이보는 category를 텍스트필드 대신 모델로 만들고 연결시켜 주었습니다. 파이보의 모델 전체를 답변에 달아 놓겠으니 참고해 보세요. - 박응용님, 2021년 7월 22일 10:43 오후 추천 , 대댓글

pybo.kr의 모델 입니다.

from django.db import connection
from django.db import models
from django.urls import reverse

from common.models import CustomUser


class Category(models.Model):
    name = models.CharField(max_length=20, unique=True)
    description = models.CharField(max_length=200, null=True, blank=True)
    has_answer = models.BooleanField(default=True)  # 답변가능 여부

    def __str__(self):
        return self.name


class Question(models.Model):
    class Meta:
        ordering = ['id']

    author = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='author_question')
    subject = models.CharField(max_length=200)
    content = models.TextField()
    create_date = models.DateTimeField(auto_now_add=True, blank=True)
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(CustomUser, related_name='voter_question', blank=True)
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='category_question')
    view_count = models.IntegerField(null=True, blank=True, default=0)
    notice = models.BooleanField(default=False)  # 공지사항 여부

    def __str__(self):
        return self.subject

    def get_absolute_url(self):
        return reverse('pybo:question_detail', args=[self.id])

    def get_recent_comments(self):
        return self.comment_set.all().order_by('-create_date')[:5]


class Answer(models.Model):
    author = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='author_answer')
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(CustomUser, related_name='voter_answer', blank=True)

    def get_absolute_url(self):
        # 현재 답변이 작성된 페이지 정보가 필요
        answer_index = self.get_index()
        page, r = divmod(answer_index, 10)
        if r != 0:
            page += 1
        return reverse('pybo:question_detail', args=[self.question.id]) + "?page=%s#answer_%s" % (page, self.id)

    def get_index(self):
        queryset = self.question.answer_set.order_by('create_date')
        numbered_qs = queryset.extra(select={
            'queryset_row_number': 'ROW_NUMBER() OVER (ORDER BY "id")'
        })
        with connection.cursor() as cursor:
            cursor.execute(
                "WITH OrderedQueryset AS (" + str(numbered_qs.query) + ") "
                "SELECT queryset_row_number FROM OrderedQueryset WHERE id = %s",
                [self.id]
            )
            index = cursor.fetchall()[0][0]

        # index = 0
        # for _answer in self.question.answer_set.all().order_by('create_date'):
        #     index += 1
        #     if self.id == _answer.id:
        #         break

        return index

    def get_recent_comments(self):
        return self.comment_set.all().order_by('-create_date')[:5]


class Comment(models.Model):
    author = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    question = models.ForeignKey(Question, null=True, blank=True, on_delete=models.CASCADE)
    answer = models.ForeignKey(Answer, null=True, blank=True, on_delete=models.CASCADE)
    voter = models.ManyToManyField(CustomUser, related_name='voter_comment')

    class Meta:
        ordering = ['create_date']

    def get_question(self):
        if self.question:
            return self.question
        elif self.answer:
            return self.answer.question
        else:
            return None

    def get_absolute_url(self):
        if self.question:
            _anchor = "#anchor_question_all_comments"
            return reverse('pybo:question_detail', args=[self.question.id]) + _anchor
        elif self.answer:
            # 현재 답변이 작성된 페이지 정보가 필요
            answer_index = self.answer.get_index()
            page, r = divmod(answer_index, 10)
            if r != 0:
                page += 1
            _anchor = "?page=%s#anchor_answer_all_comments_%s" % (page, self.answer.id)
            return reverse('pybo:question_detail', args=[self.answer.question.id]) + _anchor
        else:
            return None


class QuestionCount(models.Model):
    ip = models.CharField(max_length=30)
    question = models.ForeignKey(Question, on_delete=models.CASCADE)

    def __unicode__(self):
        return self.ip

박응용

2021년 7월 22일 10:43 오후

감사합니다! :) - shin님, M 2021년 7월 23일 12:19 오전 추천 , 대댓글

에고고.. 자꾸 여쭤봐서 죄송합니다..
AbstractUser를 이용해서 수정을 했는데 자꾸 문제가 나타나네요..
인터넷을 뒤져봐도 다람쥐 챗바퀴 구르듯이 다시 같은 문제로 돌아옵니다.
수정 후 'python manage.py makemigrations'를 하려고 하니 에러가 나타납니다.
혹시 해결 방법이 있을까요...

(mysite) D:\Study\django\mysite>python manage.py makemigrations
Traceback (most recent call last):
  File "D:\Study\django\mysite\manage.py", line 22, in <module>
    main()
  File "D:\Study\django\mysite\manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "C:\venvs\mysite\lib\site-packages\django\core\management\__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "C:\venvs\mysite\lib\site-packages\django\core\management\__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "C:\venvs\mysite\lib\site-packages\django\core\management\base.py", line 330, in run_from_argv
    self.execute(*args, **cmd_options)
  File "C:\venvs\mysite\lib\site-packages\django\core\management\base.py", line 371, in execute
    output = self.handle(*args, **options)
  File "C:\venvs\mysite\lib\site-packages\django\core\management\base.py", line 85, in wrapped
    res = handle_func(*args, **kwargs)
  File "C:\venvs\mysite\lib\site-packages\django\core\management\commands\makemigrations.py", line 101, in handle
    loader.check_consistent_history(connection)
  File "C:\venvs\mysite\lib\site-packages\django\db\migrations\loader.py", line 302, in check_consistent_history
    raise InconsistentMigrationHistory(
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency common.0001_initial on database 'default'.

shin

M 2021년 7월 23일 1:15 오전

@박응용님 감사합니다. 로그인도 조언 덕분에 잘 해결하였습니다..! - shin님, 2021년 7월 23일 3:10 오후 추천 , 대댓글

원래 인터넷 찾아보면서 독학을 해야하는데 다 물어보고 해결하는 것 같네요.. 번거롭게 해서 죄송합니다..
위에서 말씀하신대로 카테고리를 pybo 코드를 참고하여 수정하였습니다.

Page not found (404)
Request Method: GET
Request URL:    http://localhost:8000/pybo/list/free
Raised by:  pybo.views.base_views.index
No Category matches the given query.

You're seeing this error because you have DEBUG = True in your Django settings file. Change that to False, and Django will display a standard 404 page.

현재 게시판에 접근을 시도 하면 현재 이런 에러가 브라우저에 나타납니다.
shell을 통해 Category를 먼저 별도로 생성을 해야하는 것인가요?


혹시 코드 에러인가 싶어 관련 코드도 같이 올립니다.

navi.html

<li class="nav-item">
                    <a href="{% url 'pybo:index' 'free' %}" class="nav-link">자유게시판</a>
</li>

url.py

urlpatterns = [
    path('list/<str:category_name>/', base_views.index, name='index'),
]

forms.py

class QuestionForm(forms.ModelForm):
    class Meta:
        model = Question
        fields = ['subject', 'content', 'category']
        labels = {
            'subject': '제목',
            'category': '카테고리',
            'content': '내용',
        }

views.py

def index(request, category_name):
    page = request.GET.get('page', '1')
    kw = request.GET.get('kw', '')
    so = request.GET.get('so', 'recent')

    category = get_object_or_404(Category, name=category_name)
    _question_list = Question.objects.filter(category__name=category.name)

    # 정렬
    if so == 'recommend':
        _question_list = Question.objects.annotate(num_voter=Count('voter')).order_by('-num_voter', '-create_date')
    elif so == 'popular':
        _question_list = Question.objects.annotate(num_answer=Count('answer')).order_by('-num_answer', '-create_date')
    else:
        _question_list = Question.objects.order_by('-create_date')

    # 검색
    if kw:
        _question_list = _question_list.filter(
            Q(subject__icontains=kw) |
            Q(content__icontains=kw) |
            Q(author__username__icontains=kw) |
            Q(answer__author__username__icontains=kw)
        ).distinct()

    # 페이징 처리
    paginator = Paginator(_question_list, 10)
    page_obj = paginator.get_page(page)

    context = {'question_list': page_obj, 'page': page, 'kw': kw, 'category': category}
    return render(request, 'pybo/question_list.html', context)


def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    context = {'question': question, 'category': question.category}
    return render(request, 'pybo/question_detail.html', context)

shell에서 Category결과

<QuerySet [{'id': 1, 'name': 'free', 'description': '1', 'has_answer': True}, {'id': 2, 'name': 'practice', 'description': '2', 'has_answer': True}, {'id': 3, 'name': 'portfolio', 'description': '3', 'has_answer': True}, {'id': 4, 'name': 'qna', 'description': '4', 'has_answer': True}, {'id': 5, 'name': 'notice', 'description': '5', 'has_answer': True}]>

model.py는 pybo와 동일하게 수정했습니다.

발생하는 문제

NoReverseMatch at /pybo/list/free/
Reverse for 'index' with no arguments not found. 1 pattern(s) tried: ['pybo/list/(?P<category_name>[^/]+)/$']

shin

M 2021년 7월 23일 8:20 오후

방법은 여러가지가 있지만, 카테고리는 어드민에 등록하신후 어드민에서 등록하는게 가장 편합니다. - 박응용님, 2021년 7월 23일 4:29 오후 추천 , 대댓글
@박응용님 어드민으로 필드 내용 추가하고 위 코드 실행 시 Reverse for 'index' with no arguments not found. 1 pattern(s) tried: ['pybo/list/(?P<category_name>[^/]+)/$']와 같은 에러가 나타나는데 혹시 어디가 잘못된 것일까요? - shin님, 2021년 7월 23일 8:10 오후 추천 , 대댓글
@shin님 사용중인 템플릿에서 category 가 적용되지 않은 URL이 존재하는것 같은데요? - 박응용님, 2021년 7월 23일 8:46 오후 추천 , 대댓글