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 %}