From e2e859e0c6ac427445a53b77f0f084ec19798c80 Mon Sep 17 00:00:00 2001 From: vanitha Date: Tue, 23 Aug 2016 23:50:09 +0530 Subject: [PATCH] Voting for Nominations #95 Feature to send email to selected set of board members for voting. Voting expires in 10 days from the time of request and user can vote only once. Required validations are added. Added 2 new models to store url hash and capture votes submitted. --- apps/common/emailer.py | 20 ++ .../migrations/0004_auto_20160729_0933.py | 19 ++ .../migrations/0005_auto_20160823_2212.py | 53 +++++ apps/nominations/models.py | 26 +++ apps/nominations/views.py | 189 +++++++++++++++++- pssi/settings.py | 2 + pssi/urls.py | 28 ++- templates/nominations/list.html | 4 + templates/nominations/nomination_list.html | 102 ++++++++++ templates/nominations/nomination_vote.html | 110 ++++++++++ templates/nominations/voting_summary.html | 79 ++++++++ 11 files changed, 625 insertions(+), 7 deletions(-) create mode 100644 apps/nominations/migrations/0004_auto_20160729_0933.py create mode 100644 apps/nominations/migrations/0005_auto_20160823_2212.py create mode 100644 templates/nominations/nomination_list.html create mode 100644 templates/nominations/nomination_vote.html create mode 100644 templates/nominations/voting_summary.html diff --git a/apps/common/emailer.py b/apps/common/emailer.py index a1623f8..42d6d3d 100644 --- a/apps/common/emailer.py +++ b/apps/common/emailer.py @@ -114,6 +114,26 @@ def send_payment_confirmation_email(user, instance): _send_payment_confirmation_email_to_staff(user, instance) +def send_voting_email(user, nomination_type, slug, vote_url): + subject = "PSSI Voting Request for {nomination_type} - {slug}".format(nomination_type=nomination_type, + slug=slug) + message = """ + Hi %s, + + As you are aware that we have closed the Nomination for %s - %s. + + We are in the process of voting. Being a PSSI member, your vote makes a lot of difference. + Below is the link for voting. + Vote + Request you to vote for a nominee and help us select one. + + Cheers! + PSSI Board. + """ % (user.first_name, nomination_type, slug, vote_url) + + return _send_mail(subject, message, recipient_list=[user.email]) + + # Private functions def _send_mail(subject, message, recipient_list): """All the email originating from system should go via this interface. diff --git a/apps/nominations/migrations/0004_auto_20160729_0933.py b/apps/nominations/migrations/0004_auto_20160729_0933.py new file mode 100644 index 0000000..1fe8c87 --- /dev/null +++ b/apps/nominations/migrations/0004_auto_20160729_0933.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('nominations', '0003_nominationtype_description'), + ] + + operations = [ + migrations.AlterField( + model_name='nominationtype', + name='description', + field=models.TextField(verbose_name='Description about nomination', default=''), + ), + ] diff --git a/apps/nominations/migrations/0005_auto_20160823_2212.py b/apps/nominations/migrations/0005_auto_20160823_2212.py new file mode 100644 index 0000000..9df2db5 --- /dev/null +++ b/apps/nominations/migrations/0005_auto_20160823_2212.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('nominations', '0004_auto_20160729_0933'), + ] + + operations = [ + migrations.CreateModel( + name='UserVoting', + fields=[ + ('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('comments', models.TextField()), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('vote', models.ForeignKey(to='nominations.Nomination')), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='VotingURL', + fields=[ + ('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('url_hash', models.CharField(max_length=32, verbose_name='hash', unique=True)), + ('expiry', models.DateTimeField(verbose_name='Expiry')), + ('ntype', models.ForeignKey(to='nominations.NominationType')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='uservoting', + name='voting_url', + field=models.ForeignKey(to='nominations.VotingURL'), + preserve_default=True, + ), + ] diff --git a/apps/nominations/models.py b/apps/nominations/models.py index c3e9518..f3092dc 100644 --- a/apps/nominations/models.py +++ b/apps/nominations/models.py @@ -53,3 +53,29 @@ def __str__(self): ntype=self.ntype.name, fullname=self.fullname ) + + +class VotingURL(BaseModel): + user = models.ForeignKey(settings.AUTH_USER_MODEL) + url_hash = models.CharField("hash", blank=False, max_length=32, unique=True) + expiry = models.DateTimeField("Expiry") + ntype = models.ForeignKey('NominationType') + + def __str__(self): + return "{user}: {hash} - {expiry}". format( + user=self.user.username, + hash=self.url_hash, + expiry=self.expiry) + + +class UserVoting(BaseModel): + user = models.ForeignKey(settings.AUTH_USER_MODEL) + vote = models.ForeignKey('Nomination') + voting_url = models.ForeignKey('VotingURL') + comments = models.TextField() + + def __str__(self): + return "{user}: {vote} - {comments}". format( + user=self.user.username, + vote=self.vote, + comments=self.comments,) diff --git a/apps/nominations/views.py b/apps/nominations/views.py index 45a2c75..7c9d6c1 100644 --- a/apps/nominations/views.py +++ b/apps/nominations/views.py @@ -1,17 +1,27 @@ # -*- coding: utf-8 -*- -from datetime import datetime +import json +import hashlib, pickle + +from datetime import datetime, timedelta + +from django.conf import settings +from django.utils import timezone from django.views.generic.edit import CreateView from django.views.generic import ListView -from django.shortcuts import get_object_or_404 -from django.core.urlresolvers import reverse_lazy +from django.shortcuts import get_object_or_404, render +from django.core.urlresolvers import reverse_lazy, reverse from django.utils.decorators import method_decorator from django.contrib.auth.decorators import login_required -from django.http import HttpResponseForbidden +from django.http import HttpResponse, HttpResponseForbidden +from django.contrib.auth.models import User +from django.contrib.sites.shortcuts import get_current_site -from .models import Nomination, NominationType +from .models import Nomination, NominationType, VotingURL, UserVoting from .forms import NominationForm from common import emailer from board.models import BoardMember +from apps.common.emailer import send_voting_email +# from django.contrib.sites.models import Sites class LoginRequiredMixin(object): @@ -95,3 +105,172 @@ def form_valid(self, form): instance=form.instance ) return super(NominationCreateView, self).form_valid(form) + + +class ViewNominationListView(ListView, LoginRequiredMixin): + model = NominationType + template_name = 'nominations/nomination_list.html' + context_object_name = 'nomination_types' + + def dispatch(self, request, *args, **kwargs): + if not is_board_member(self.request.user): + return HttpResponseForbidden("Sorry! You do not have permission to view the Nominations!") + return super(ViewNominationListView, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, *args, **kwargs): + context = super( + ViewNominationListView, self).get_context_data(*args, **kwargs) + + nominations = NominationType.objects.values_list('id', 'name', 'slug').distinct() + nomination_dict = {} + for each in nominations: + nomination_dict[each[0]] = '%s - %s' % (each[1], each[2]) + + context['nomination_types'] = nomination_dict + + board_members_list = BoardMember.objects.all() #.filter(end_date__gte=datetime.now()) + board_members = {} + for member in board_members_list: + board_members[member.user.id] = member.user.get_full_name() + + context['board_members'] = board_members + + return context + + +class CreateVoteUrlView(ListView, LoginRequiredMixin): + model = NominationType + template_name = 'nominations/nomination_list.html' + context_object_name = 'nomination_type_list' + + def dispatch(self, request, *args, **kwargs): + if not is_board_member(self.request.user): + return HttpResponseForbidden("Sorry! You do not have permission to view the Nominations!") + return super(CreateVoteUrlView, self).dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + nom_id = request.POST.get('nomination_id') + nomination = request.POST.get('nomination') + nom_type, slug = nomination.split(' - ') + board_members = request.POST.getlist('board_members[]') + ntype = get_object_or_404(NominationType, id=nom_id) + + for member in board_members: + user_obj = get_object_or_404(User, id=int(member)) + + # unique hash per user + data = [user_obj.get_full_name(), user_obj.email, slug, nom_type, datetime.now()] + + hash_ = hashlib.md5(pickle.dumps(data, 0)).hexdigest() + expiry_date = datetime.now() + timedelta(days=10) + + host = '{}://{}'.format(settings.SITE_PROTOCOL, + request.META['HTTP_HOST']) + url = '%s%s' % (host, reverse('vote_nominee', kwargs={'nomination': nom_id, 'hash': hash_})) + + # store the hash and expiry in DB. Expiry is set to 10 days from date of creation + voting_url = VotingURL(user=user_obj, url_hash=hash_, expiry=expiry_date, ntype=ntype) + voting_url.save() + + send_voting_email(user_obj, nom_type, slug, url) + + return HttpResponse('Success') + + +class ViewNominations(ListView, LoginRequiredMixin): + model = Nomination + template_name = 'nominations/nomination_vote.html' + context_object_name = 'nominees' + + def dispatch(self, request, *args, **kwargs): + if not is_board_member(self.request.user): + return HttpResponseForbidden("Sorry! You do not have permission to view the Nominations!") + return super(ViewNominations, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, *args, **kwargs): + context = super( + ViewNominations, self).get_context_data(*args, **kwargs) + + nomination_id = self.kwargs.get('nomination') + hash_ = self.kwargs.get('hash') + + ntype = get_object_or_404(NominationType, id=nomination_id) + + # logged in user verification + voting_url_obj = VotingURL.objects.filter(user=self.request.user, url_hash=hash_, ntype=ntype) + if voting_url_obj: + voting_url_obj = voting_url_obj[0] + if voting_url_obj.expiry <= timezone.now(): + context['message'] = 'Sorry! Voting is closed.' + return context + # check if the user has already voted + if UserVoting.objects.filter(voting_url=voting_url_obj).exists(): + context['message'] = 'Sorry! You have already Voted.' + return context + + nominees = Nomination.objects.filter(ntype=nomination_id) + context['nominees'] = nominees + context['nomination'] = '%s - %s' % (ntype.name, ntype.slug) + context['expiry'] = voting_url_obj.expiry + context['hash'] = hash_ + context['nomination_id'] = nomination_id + return context + + else: + context['message'] = 'Sorry! You are not the intended recipient.' + return context + + def post(self, request, *args, **kwargs): + nomination_id = self.kwargs.get('nomination') + hash_ = self.kwargs.get('hash') + + nominee = request.POST.get('nominee') + comments = request.POST.get('comments') + + ntype = get_object_or_404(NominationType, id=nomination_id) + voting_url_obj = get_object_or_404(VotingURL, user=self.request.user, url_hash=hash_, ntype=ntype) + nomination_obj = get_object_or_404(Nomination, id=nominee) + + voting = UserVoting(user=request.user, vote=nomination_obj, voting_url=voting_url_obj, comments=comments) + voting.save() + + return HttpResponse('Success') + + +class VotingSummaryList(ListView, LoginRequiredMixin): + model = UserVoting + template_name = 'nominations/voting_summary.html' + context_object_name = 'summary' + + def dispatch(self, request, *args, **kwargs): + if not is_board_member(self.request.user): + return HttpResponseForbidden("Sorry! You do not have permission to view the Nominations!") + return super(VotingSummaryList, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, *args, **kwargs): + context = super( + VotingSummaryList, self).get_context_data(*args, **kwargs) + + nominations = NominationType.objects.values_list('id', 'name', 'slug').distinct() + nomination_dict = {} + for each in nominations: + nomination_dict[each[0]] = '%s - %s' % (each[1], each[2]) + + context['nomination_types'] = nomination_dict + + return context + + def post(self, request, *args, **kwargs): + + nomination_id = request.POST.get('nomination_id') + ntype = get_object_or_404(NominationType, id=nomination_id) + + # get all the nominations for ntype + nominations = Nomination.objects.filter(ntype=ntype) + vote_summary = [] + + for each_nom in nominations: + vote_count = UserVoting.objects.filter(vote=each_nom).count() + vote_summary.append({'name': each_nom.fullname, 'vote_count': vote_count, 'profession': each_nom.profession, + 'contribution_info': each_nom.contribution_info}) + return HttpResponse(json.dumps(vote_summary)) diff --git a/pssi/settings.py b/pssi/settings.py index 4f996c9..e92f85c 100644 --- a/pssi/settings.py +++ b/pssi/settings.py @@ -185,6 +185,8 @@ SECRET_KEY = '^@p!fj5df100)%gd7g&$c^7znjs0(uJY6qt/<19M-Zkbymc$|C' +SITE_PROTOCOL = 'http' + # Instamojo (payemnt) link # Add this in local settings diff --git a/pssi/urls.py b/pssi/urls.py index 5c923f4..cc6394f 100644 --- a/pssi/urls.py +++ b/pssi/urls.py @@ -4,8 +4,14 @@ from django.contrib.auth.decorators import login_required from grants.views import GrantRequestCreateView, GrantTypeListView -from nominations.views import NominationCreateView, NominationTypeListView -from nominations.views import NomineeListView +from nominations.views import ( + NominationCreateView, + NominationTypeListView, + NomineeListView, + ViewNominationListView, + CreateVoteUrlView, + ViewNominations, + VotingSummaryList) from board.views import BoardListView urlpatterns = patterns( @@ -15,6 +21,7 @@ template_name='index.html', ), name='home'), url(r'^about/$', BoardListView.as_view(), name='about-static'), + url(r'^membership/$', TemplateView.as_view( template_name='membership.html', ), name='membership-static'), @@ -27,6 +34,7 @@ url(r'^awards/$', TemplateView.as_view( template_name='awards.html', ), name='awards-static'), + url(r'^by-laws/$', TemplateView.as_view( template_name='by_laws.html', ), name='by-laws'), @@ -57,6 +65,22 @@ template_name='nominations/nomination_success.html')), name='nominee_req_success'), + url(r'^nomination/view/$', + login_required(ViewNominationListView.as_view()), + name='view_all_nominations'), + + url(r'^nomination/request_vote/$', + login_required(CreateVoteUrlView.as_view()), + name='request_for_vote'), + + url(r'^nomination/vote/summary/$', + login_required(VotingSummaryList.as_view()), + name='vote_summary'), + + url(r'^nomination/vote/(?P.*)/(?P.*)/$', + login_required(ViewNominations.as_view()), + name='vote_nominee'), + url(r'^nomination/(?P[\w+]+)/$', login_required(NominationCreateView.as_view()), name='create_nominee'), diff --git a/templates/nominations/list.html b/templates/nominations/list.html index 7162b2c..dcecc07 100644 --- a/templates/nominations/list.html +++ b/templates/nominations/list.html @@ -23,6 +23,10 @@

Nomination

Nominate for {{ nomination.name }} {% if board_member %} View list + + Generate Voting Email + + View Voting Summary {% endif %} {% endfor %} diff --git a/templates/nominations/nomination_list.html b/templates/nominations/nomination_list.html new file mode 100644 index 0000000..0f1269d --- /dev/null +++ b/templates/nominations/nomination_list.html @@ -0,0 +1,102 @@ +{% extends 'base.html' %} +{% load markdown_tags %} + +{% block head_title %}Nominations - {{ block.super }}{% endblock %} + +{% block header %} +
+
+
+

Nomination

+ + +
+
+
+ +{% endblock %} + +{% block content %} +
+
+ + +

+
+
+    +

+
+ {% for mem_id, member in board_members.items %} +    {{ member }}
+ {% endfor %} +
+


+ + Send Email +
+
+{% endblock %} diff --git a/templates/nominations/nomination_vote.html b/templates/nominations/nomination_vote.html new file mode 100644 index 0000000..fadd63e --- /dev/null +++ b/templates/nominations/nomination_vote.html @@ -0,0 +1,110 @@ +{% extends 'base.html' %} +{% load markdown_tags %} + +{% block head_title %}Nominations - {{ block.super }}{% endblock %} + +{% block header %} +
+
+
+

Nomination

+ + +
+
+
+ +{% endblock %} + +{% block content %} +
+
+
+
+ We have received below nominations for {{ nomination }}.
+ Voting is open till {{ expiry }}.
Please Vote a nominee of your choice and also let us know the reason for your selection. +


+
+ + + + + + + + {% for nominee in nominees %} + + + + + + {% endfor %} + +
NomineeProfessionContribution Details
{{ nominee.fullname }}{{ nominee.profession }}{{ nominee.contribution_info }}
+

+ + + +

+
+ Submit +
+
+
+{% endblock %} diff --git a/templates/nominations/voting_summary.html b/templates/nominations/voting_summary.html new file mode 100644 index 0000000..9df7a23 --- /dev/null +++ b/templates/nominations/voting_summary.html @@ -0,0 +1,79 @@ +{% extends 'base.html' %} +{% load markdown_tags %} + +{% block head_title %}Nominations - {{ block.super }}{% endblock %} + +{% block header %} +
+
+
+

Nomination

+ + +
+
+
+ +{% endblock %} + +{% block content %} +
+
+ + + + + View Summary +

+ +
+ +
+
+
+
+{% endblock %}