diff --git a/auth/auth_systems/__init__.py b/auth/auth_systems/__init__.py index 39f5ef14314d57671f71fec507dd80f6508f7d42..202fe95c079bc270639b57941e33f72f53078990 100644 --- a/auth/auth_systems/__init__.py +++ b/auth/auth_systems/__init__.py @@ -13,3 +13,9 @@ AUTH_SYSTEMS['yahoo'] = yahoo # not ready #import live #AUTH_SYSTEMS['live'] = live + +def can_check_constraint(auth_system): + return hasattr(AUTH_SYSTEMS[auth_system], 'check_constraint') + +def can_list_categories(auth_system): + return hasattr(AUTH_SYSTEMS[auth_system], 'list_categories') diff --git a/auth/auth_systems/cas.py b/auth/auth_systems/cas.py index d5b824e86acce5bd10436c71ba8d7b32033598f5..407cccac471c8548f4af42638efe28b166538ed7 100644 --- a/auth/auth_systems/cas.py +++ b/auth/auth_systems/cas.py @@ -216,7 +216,7 @@ def send_message(user_id, name, user_info, subject, body): send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (name, email)], fail_silently=False) -def check_constraint(constraint, user_info): - if not user_info.has_key('category'): +def check_constraint(constraint, user): + if not user.user_info.has_key('category'): return False - return constraint['year'] == user_info['category'] + return constraint['year'] == user.user_info['category'] diff --git a/auth/auth_systems/facebook.py b/auth/auth_systems/facebook.py index 4dc1e7406331c224f6b422bd06bb32b9325961c0..7ace5cb0900150a47a40f99362ec0a6784483932 100644 --- a/auth/auth_systems/facebook.py +++ b/auth/auth_systems/facebook.py @@ -39,7 +39,7 @@ def get_auth_url(request, redirect_url): return facebook_url('/oauth/authorize', { 'client_id': APP_ID, 'redirect_uri': redirect_url, - 'scope': 'publish_stream,email'}) + 'scope': 'publish_stream,email,user_groups'}) def get_user_info_after_auth(request): args = facebook_get('/oauth/access_token', { @@ -67,3 +67,45 @@ def update_status(user_id, user_info, token, message): def send_message(user_id, user_name, user_info, subject, body): if user_info.has_key('email'): send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (user_name, user_info['email'])], fail_silently=False) + + +## +## eligibility checking +## + +# a constraint looks like +# {'group' : {'id': 123, 'name': 'asdfsdf'}} +# +# only the ID matters for checking, the name of the group is cached +# here for ease of display so it doesn't have to be re-queried. + +def get_user_groups(user): + groups_raw = utils.from_json(facebook_get('/me/groups', {'access_token':user.token['access_token']})) + return groups_raw['data'] + +def check_constraint(constraint, user): + # get the groups for the user + groups = [group['id'] for group in get_user_groups(user.token)] + + # check if one of them is the group in the constraint + try: + return constraint['group']['id'] in groups + except: + # FIXME: be more specific about exception catching + return False + +def generate_constraint(category_id, user): + """ + generate the proper basic data structure to express a constraint + based on the category string + """ + groups = get_user_groups(user) + the_group = [g for g in groups if g['id'] == category_id][0] + + return {'group': the_group} + +def list_categories(user): + return get_user_groups(user) + +def pretty_eligibility(constraint): + return "Facebook users who are members of the \"%s\" group" % constraint['group']['name'] diff --git a/auth/models.py b/auth/models.py index b09e873d8a166d047f73fad6884fa0b52207f32e..f511425da6498ff0e5b112069c569d4276279c88 100644 --- a/auth/models.py +++ b/auth/models.py @@ -12,7 +12,7 @@ from jsonfield import JSONField import datetime, logging -from auth_systems import AUTH_SYSTEMS +from auth_systems import AUTH_SYSTEMS, can_check_constraint, can_list_categories class User(models.Model): @@ -111,7 +111,7 @@ class User(models.Model): for constraint in eligibility_case['constraint']: # do we match on this constraint? - if auth_system.check_constraint(constraint=constraint, user_info = self.info): + if auth_system.check_constraint(constraint=constraint, user = self): return True # no luck diff --git a/auth/urls.py b/auth/urls.py index cedf65e3d0d47548c898aae5ebc5cbcb23db83d5..57e0f0b87ca3bf322398f30fcaddb2c41d5eadf8 100644 --- a/auth/urls.py +++ b/auth/urls.py @@ -15,7 +15,8 @@ urlpatterns = patterns('', (r'^$', index), (r'^logout$', logout), (r'^start/(?P<system_name>.*)$', start), - (r'^after$', after), + # weird facebook constraint for trailing slash + (r'^after/$', after), (r'^after_intervention$', after_intervention), ## should make the following modular diff --git a/helios/election_urls.py b/helios/election_urls.py index 1b9b6aae872ff15effe6a9151c47db669c441850..31760a5944145aacc6197a05cf44e18d7229315d 100644 --- a/helios/election_urls.py +++ b/helios/election_urls.py @@ -71,6 +71,7 @@ urlpatterns = patterns('', (r'^/voters/upload$', voters_upload), (r'^/voters/upload-cancel$', voters_upload_cancel), (r'^/voters/list$', voters_list_pretty), + (r'^/voters/eligibility$', voters_eligibility), (r'^/voters/email$', voters_email), (r'^/voters/(?P<voter_uuid>[^/]+)$', one_voter), (r'^/voters/(?P<voter_uuid>[^/]+)/delete$', voter_delete), diff --git a/helios/models.py b/helios/models.py index 6e3f7d78ed527f0b971b37e933f42193167961cc..f0ca2468eebae6411b159cc0b929a0a8f9e43313 100644 --- a/helios/models.py +++ b/helios/models.py @@ -243,6 +243,27 @@ class Election(HeliosModel): return True return False + + def eligibility_constraint_for(self, user_type): + if not self.eligibility: + return [] + + return [constraint['constraint'] for constraint in self.eligibility if constraint['auth_system'] == user_type][0] + + @property + def pretty_eligibility(self): + if not self.eligibility: + return "Everyone can vote." + else: + return_val = "Only the following users can vote:<ul>" + + for constraint in self.eligibility: + for one_constraint in constraint['constraint']: + return_val += "<li>%s</li>" % AUTH_SYSTEMS[constraint['auth_system']].pretty_eligibility(one_constraint) + + return_val += "</ul>" + + return return_val def voting_has_started(self): """ diff --git a/helios/templates/voters_eligibility.html b/helios/templates/voters_eligibility.html new file mode 100644 index 0000000000000000000000000000000000000000..eb40311348cbec764bd2bd66b0595cd0629c109a --- /dev/null +++ b/helios/templates/voters_eligibility.html @@ -0,0 +1,26 @@ +{% extends TEMPLATE_BASE %} + +{% block title %}Voter Eligibility for {{election.name}}{% endblock %} +{% block content %} + <h2 class="title">{{election.name}} — Voter Eligibility <span style="font-size:0.7em;">[<a href="{% url helios.views.voters_list_pretty election.uuid %}">back to voters</a>]</span></h2> + +<p> +<em>{{election.pretty_eligibility|safe}}</em> +</p> + +<p> +You may limit eligibility of voters to one of these categories, as defined by {{user.user_type}}: +</p> + +<form method="post" action=""> +<input type="hidden" name="csrf_token" value="{{csrf_token}}" /> +<select name="category_id"> +<option value="" SELECTED>(no constraint)</option> +{% for category in categories %} +<option value="{{category.id}}"> {{category.name}}</option> +{% endfor %} +</select> +<input type="submit" value="set eligibility" /> +</form> +</ul> +{% endblock %} diff --git a/helios/templates/voters_list.html b/helios/templates/voters_list.html index 588fb046688ac28d8066135edb8860a58df2d55d..6bdd14fa7f259a6033d260a46edc49e172c68e3c 100644 --- a/helios/templates/voters_list.html +++ b/helios/templates/voters_list.html @@ -9,6 +9,16 @@ {% if admin_p and not election.frozen_at %} {% if election.openreg %} [<a href="{% url helios.views.one_election_set_reg election.uuid %}?open_p=0">switch to closed</a>] + +{% if can_set_eligibility %} +<p> +<em>{{election.pretty_eligibility|safe}}</em> +<br /> +You can <a href="{% url helios.views.voters_eligibility election.uuid %}">limit the eligibility</a> of voters based on their {{user.user_type}} credentials. +</p> +{% endif %} + + {% else %} {% if election.private_p %} <br /> diff --git a/helios/views.py b/helios/views.py index 33efb07fbc7a3abf25d3a95b7e4f0536cf817357..2a82eae8de22d6dcd982774d8d2280b6134a7bd6 100644 --- a/helios/views.py +++ b/helios/views.py @@ -20,7 +20,10 @@ from crypto import utils as cryptoutils from workflows import homomorphic from helios import utils as helios_utils from view_utils import * + from auth.security import * +from auth.auth_systems import AUTH_SYSTEMS, can_list_categories + from helios import security from auth import views as auth_views @@ -1110,6 +1113,11 @@ def voters_list_pretty(request, election): user = get_user(request) admin_p = security.user_can_admin_election(user, election) + + if admin_p and election.openreg: + can_set_eligibility = can_list_categories(user.user_type) + else: + can_set_eligibility = False # files being processed voter_files = election.voterfile_set.all() @@ -1129,13 +1137,42 @@ def voters_list_pretty(request, election): total_voters = voter_paginator.count - return render_template(request, 'voters_list', {'election': election, 'voters_page': voters_page, - 'voters': voters_page.object_list, 'admin_p': admin_p, - 'email_voters': helios.VOTERS_EMAIL, - 'limit': limit, 'total_voters': total_voters, - 'upload_p': helios.VOTERS_UPLOAD, 'q' : q, - 'voter_files': voter_files}) + return render_template(request, 'voters_list', + {'election': election, 'voters_page': voters_page, + 'voters': voters_page.object_list, 'admin_p': admin_p, + 'email_voters': helios.VOTERS_EMAIL, + 'limit': limit, 'total_voters': total_voters, + 'upload_p': helios.VOTERS_UPLOAD, 'q' : q, + 'voter_files': voter_files, + 'can_set_eligibility': can_set_eligibility}) + +@election_admin() +def voters_eligibility(request, election): + user = get_user(request) + + if not can_list_categories(user.user_type): + return HttpResponseRedirect(reverse(voters_list_pretty, args=[election.uuid])) + if request.method == "GET": + categories = AUTH_SYSTEMS[user.user_type].list_categories(user) + + return render_template(request, 'voters_eligibility', + {'categories' : categories, 'election': election}) + + # now process the constraint + category_id = request.POST['category_id'] + if category_id == "": + category_id = None + + if category_id: + constraint = AUTH_SYSTEMS[user.user_type].generate_constraint(category_id, user) + election.eligibility = [{'auth_system': user.user_type, 'constraint': [constraint]}] + else: + election.eligibility = None + + election.save() + return HttpResponseRedirect(reverse(voters_eligibility, args=[election.uuid])) + @election_admin() def voters_upload(request, election): """