수학과의 좌충우돌 프로그래밍

[Django]투표 기능 구현하기 본문

웹프로그래밍/Django

[Django]투표 기능 구현하기

ssung.k 2019. 4. 6. 02:12

투표 기능 구현하기

학교내 고양이 관리하는 사이트 UOSCAT 의 기능 중 일부이다. 길고양이 이니 만큼 이름을 투표를 통해 지어주었고 그 투표구현하는 방법을 알아보자.

 

models.py

먼저 model 을 추가해줘야한다. 현재 있는 모델은 Cat 으로서 일반적인 Post 글에 해당한다. 각 Cat 마다 하나의 투표가 필요하므로 그 이름을 Vote 라고 한다. 그리고 Cat 과 Vote는 1대1 대응이다. 그리고 각 Vote마다 여러 개의 표가 존재하고 그 이름을 Choice 라고 하자. Vote와 Choice는 일대다 대응이다.

class Vote(models.Model):
    created = models.DateTimeField(default=django.utils.timezone.now)
    # 투표는 시간이 지나면 종료되야하므로 생성날짜를 현재로 지정
    cat = models.OneToOneField(Cat,on_delete=models.CASCADE)
    # Cat 과 one to one
    
    def __str__(self):
        return self.cat.name+"의 투표"

class Choice(models.Model):
    vote = models.ForeignKey(Vote,on_delete=models.CASCADE)
    # Vote 와 one to many
    name = models.CharField(max_length=20)
    # 투표 후보인 새로운 고양이 이름
    count = models.IntegerField(default=0)
    # 투표 수

    def __str__(self):
        return self.name

 

detail.html

투표에 대한 html 부분 이다. 투표 중이 아닐 때와 투표 중일 때 크게 두 부분으로 나뉜다.

  • 투표 중이 아닐 때

    • {% if not cat.voting %} 에 해당 되는 부분
    • 투표를 시작하는 버튼이 존재하고 버튼을 누르면 vote_condition url로 이동한다. 이 부분에 대해서는 뒤에서 설명하겠다.
  • 투표 중일 때

    • 먼저 각 투표 현황은 table, 표로 나타냈다.
    • view에서 choices를 받아오는데 해당 cat 에 해당되는 모든 choice들이다. 즉 모든 투표 후보들이 오고 이를 순회하는 반복문으로서 표를 완성한다.
    • 이름과 투표수, 선택 버튼이 순서대로 위치한다.
    • 선택을 누를 경우, choice 해당 투표 후보의 id를 value로 넘기며 vote url 로 이동한다.
    • 투표 테이블 아래에는 새 이름을 추가할 수 있는 input 과 버튼이 존재하고 add_name url 로 이동한다.
    • 투표를 종료하는 버튼이 존재하고 버튼을 누르면 onclick="count_overlap() 이 발생하는데 js로 예외처리해주기 위해 존재하고 결국에는 vote_condition url로 이동한다.

    {% if not cat.voting %}
    <div class="row">
      	<p>새 이름을 지어줄까요?</p>
		<a href="{% url 'vote_condition' cat.id %}" class="btn btn-success">네</a>
    </div>

    {% else %}
    <p>투표 진행중</p>
    <!-- 이름 투표 테이블 -->
    <table class="table table-striped">
      <thead>
        <tr>
          <td><B>이름</B></td>
          <td><B>투표수</B></td>
          <td><B>지지하기</B></td>
        </tr>
      </thead>

      {% for choice in choices  %}
      <tbody>
        <tr>
          <td id="cat_name">{{choice.name}}</td>
          <td>{{choice.count}}</td>
          <td>
            <form action="{%url 'vote' vote.id%}" method="POST">
              {% csrf_token %}
              <button name="choice" value="{{choice.id}}">선택</button>
            </form>
          </td>
        </tr>
      </tbody>
      {% endfor %}
    </table>
	<!------------------->

    <form method="POST" id="newname_form" action="{% url 'add_name' cat.id%}">
      <input id="newname_input" type="text" name="add_name">
      <input onclick="check_error()" class="btn btn-secondary" value="새 이름 추가하기">
    </form>

    <a href="#" class="btn btn-success" onclick="count_overlap()">투표 종료</a>
    {% endif %}
  </div>

 

urls.py

전체 url은 너무 많아서 해당 부분의 url만 가져왔다. 위의 detail.html과 연관지어서 생각하면 각각 어느 함수를 실행할지 알 수 있다. 바로 views.py 를 알아보자.

urlpatterns = [
	path('votecondition/<int:cat_id>',postapp.views.vote_condition, name='vote_condition'),
	path('vote/<int:vote_id>', postapp.views.vote, name='vote'),    					path('addname/<int:cat_id>', postapp.views.add_name, name='add_name'),  
]

 

views.py

세 개의 함수가 있고 각각에 대해서 알아보자.

 

  1. vote_condition
  • 투표 상태를 바꿔주는 함수이다.

  • 투표 중일 경우

    • if cat.voting 에 해당한다.

    • 투표를 끝내기 위해 cat.voting = False 로 바꿔준다.

    • 가장 많은 득표 수를 구해서 해당 투표에서, 가장 많은 득표수를 가진 Choice 들을 골라낸다. 이 때 get 이 아닌 filter 를 사용한 이유는 득표수가 동률일 경우에는 예외 처리를 해줘야하기 때문이다.

      • 최대 득표수의 이름의 개수가 1개 초과일 경우

        • 저장하지 않고 현재페이지로 다시 redirect 한다.
      • 최대 득표수가 1개일 경우

        • 첫번째 이름의 str 형으로 가져오고 투표는 끝내야 하므로 vote 를 삭제해준다.
  • 투표 중이지 않을 경우

    • else 에 해당한다.
    • 투표를 시작하기 위해 cat.voting = True 로 바꿔준다.
    • 원래 이름에 해당되는 이름으로 ,count=0 으로 새로운 투표 후보를 생성하고 이를 저장한다.
  • 최종적으로 cat의 정보를 모두 저장하고 현재 페이지로 redirect 한다.

def vote_condition(request,cat_id):
    cat = Cat.objects.get(id=cat_id)
	
    # 투표 종료
    if cat.voting:
        cat.voting = False
        choices = Choice.objects.filter(vote_id=cat.vote.id)
        max_count = 0
        for choice in choices:
            if max_count < choice.count:
                max_count = choice.count
        new_name = Choice.objects.filter(vote_id=cat.vote.id, count = max_count)
        if (len(new_name) > 1):
            return redirect('/detail/'+str(cat.pk))
        cat.name = new_name[0].as_str()
        vote = Vote.objects.get(cat_id=cat.id).delete()

    # 투표 시작
    else:
        cat.voting = True
        origin_name = Choice(vote_id=cat.vote.id, name=cat.name, count = 0)
        origin_name.save()
        
    cat.save()
    return redirect('/detail/'+str(cat.pk))

 

  1. vote
  • 현재 투표를 가져오고 입력한 값을 저장한다.
  • 투표 시 받았던 choice.id 로 원하는 값을 찾고 1 증가 시켜서 저장한다.
  • 헌재 페이지로 redirect 한다.

def vote(request, vote_id):
    vote = Vote.objects.get(pk=vote_id)
    selection = request.POST['choice']

    choice = Choice.objects.get(vote_id=vote_id, id=selection)
    choice.count += 1
    choice.save()

    return redirect('/detail/'+str(vote.cat.id))
  1. add_name
  • 이름 후보 choice 를 하나만들어준다.
  • filter를 통해 현재 투표의 이름 후보들을 모두 가져와서 choice_all 에 저장한다.
  • 입력한 이름을 공백없이 가져와서 이름 후보의 이름으로 저장한다.
  • 예외처리를 해줘야한다. 기존의 이름들을 순회하며 해당이름이 존재하거나 입력을 공백만 했다면 별도의 저장없이 현재 페이지로 redirect 한다.
  • 그렇지 않으면 저장하고 redirect 한다.
def add_name(request, cat_id):
    cat = Cat.objects.get(id = cat_id)
    choice = Choice(vote_id = cat.vote.id, count=0)
    choice_all = Choice.objects.filter(vote_id=cat.vote.id)

    choice.name = request.POST['add_name'].strip()
    for choice_one in choice_all:
        if str(choice_one) == choice.name or len(choice.name) == 0:
            return redirect('/detail/'+str(cat.id))

    choice.save()
    return redirect('/detail/'+str(cat.id))

 

Comments