From f5b8551257cfe10799d060ed493d609f61ef8a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B3mas=20=C3=81rni=20J=C3=B3nasson?= Date: Sat, 21 May 2022 10:07:28 -0700 Subject: [PATCH] Running the black code formatter on the codebase --- Makefile | 5 + core/admin.py | 3 +- core/ajax/utils.py | 1 - core/apps.py | 1 + core/authentication.py | 2 + core/contextprocessors.py | 6 +- core/dataviews.py | 141 +++---- core/django_mdmail.py | 22 +- core/forms.py | 40 +- core/management/commands/closeissues.py | 7 +- core/management/commands/export_db.py | 74 +++- core/management/commands/heartbeat.py | 1 + core/management/commands/load_fake_data.py | 161 +++++--- core/management/commands/lookup_usernames.py | 3 +- core/management/commands/processissues.py | 5 +- core/middleware.py | 66 ++-- core/migrations/0001_initial.py | 107 +++++- core/migrations/0002_reset_migrations.py | 21 +- core/migrations/0003_event.py | 20 +- ...04_userprofile_declaration_of_interests.py | 4 +- core/migrations/0005_auto_20190822_2006.py | 21 +- core/migrations/0006_userprofile_phone.py | 8 +- core/migrations/0007_merge_20191019_1939.py | 3 +- core/migrations/0008_auto_20210116_2004.py | 7 +- core/migrations/0009_auto_20210827_1638.py | 10 +- core/models.py | 134 +++++-- core/saml.py | 47 ++- core/templatetags/wasa2il.py | 33 +- core/urls.py | 25 +- core/utils.py | 80 +++- core/views.py | 348 +++++++++++------- datetimewidget/widgets.py | 228 +++++++----- election/__init__.py | 2 - election/dataviews.py | 110 ++++-- .../management/commands/processelections.py | 25 +- election/migrations/0001_initial.py | 261 +++++++++++-- .../migrations/0002_auto_20190822_1451.py | 6 +- .../migrations/0003_auto_20190822_2006.py | 21 +- .../migrations/0004_auto_20200121_1340.py | 16 +- .../migrations/0005_auto_20200909_1553.py | 17 +- .../migrations/0006_auto_20220102_1823.py | 14 +- election/models.py | 203 +++++++--- election/schulze.py | 39 +- election/templatetags/elections.py | 2 +- election/test/test_schulze.py | 33 +- election/urls.py | 36 +- election/utils.py | 333 +++++++++++------ election/views.py | 43 ++- emailconfirmation/migrations/0001_initial.py | 27 +- emailconfirmation/models.py | 20 +- emailconfirmation/urls.py | 6 +- emailconfirmation/views.py | 6 +- gateway/apps.py | 2 +- gateway/register.py | 13 +- gateway/signals.py | 14 +- gateway/utils.py | 59 ++- initial_setup.py | 130 +++++-- issue/__init__.py | 3 +- issue/admin.py | 7 +- issue/dataviews.py | 39 +- issue/forms.py | 12 +- issue/migrations/0001_initial.py | 296 +++++++++++++-- issue/migrations/0003_auto_20190822_2006.py | 100 ++++- issue/models.py | 150 +++++--- issue/urls.py | 57 ++- issue/views.py | 90 +++-- languagecontrol/middleware.py | 1 + languagecontrol/signals.py | 1 + languagecontrol/utils.py | 1 + polity/contextprocessors.py | 9 +- polity/forms.py | 1 + polity/migrations/0001_initial.py | 132 ++++++- polity/migrations/0002_auto_20181207_0944.py | 35 +- polity/migrations/0003_auto_20190822_2006.py | 26 +- ...3_polity_require_phone_for_volunteering.py | 6 +- polity/migrations/0005_merge_20191019_1939.py | 3 +- polity/migrations/0006_auto_20200909_2020.py | 6 +- polity/migrations/0007_polity_name_short.py | 7 +- polity/migrations/0008_polity_eligibles.py | 4 +- polity/migrations/0008_polity_polity_type.py | 11 +- polity/migrations/0009_merge_20210118_2102.py | 3 +- polity/migrations/0010_auto_20210118_2102.py | 26 +- polity/migrations/0011_auto_20210118_2303.py | 11 +- polity/migrations/0012_auto_20210118_2342.py | 9 +- polity/models.py | 154 +++++--- polity/urls.py | 18 +- polity/views.py | 24 +- py3votecore/abstract_classes.py | 47 ++- py3votecore/common_functions.py | 6 +- py3votecore/condorcet.py | 43 ++- py3votecore/irv.py | 1 - py3votecore/plurality.py | 5 +- py3votecore/plurality_at_large.py | 19 +- py3votecore/ranked_pairs.py | 9 +- py3votecore/schulze_by_graph.py | 31 +- py3votecore/schulze_helper.py | 97 ++++- py3votecore/schulze_method.py | 1 - py3votecore/schulze_npr.py | 9 +- py3votecore/schulze_pr.py | 29 +- py3votecore/schulze_stv.py | 43 ++- py3votecore/stv.py | 70 +++- py3votecore/tie_breaker.py | 13 +- pyproject.toml | 5 + requirements.txt | 3 + requirements.txt.freeze | 7 + tasks/forms.py | 7 +- tasks/migrations/0001_initial.py | 88 ++++- tasks/migrations/0002_auto_20190730_2053.py | 16 +- .../0003_taskrequest_available_time.py | 4 +- tasks/migrations/0004_auto_20190822_2006.py | 18 +- tasks/migrations/0006_auto_20190831_1804.py | 30 +- tasks/migrations/0007_task_require_phone.py | 6 +- tasks/migrations/0008_merge_20191019_1939.py | 3 +- tasks/models.py | 59 ++- tasks/urls.py | 37 +- tasks/views.py | 31 +- tests/test_views.py | 7 +- topic/admin.py | 1 + topic/dataviews.py | 10 +- topic/migrations/0001_initial.py | 80 +++- topic/migrations/0002_auto_20190822_2006.py | 18 +- topic/models.py | 40 +- topic/urls.py | 20 +- topic/views.py | 1 + urls.py | 78 ++-- wasa2il/forms.py | 25 +- wasa2il/settings.py | 166 +++++---- wasa2il/utils.py | 4 +- wasa2il/wsgi.py | 4 +- 129 files changed, 4065 insertions(+), 1440 deletions(-) create mode 100644 pyproject.toml diff --git a/Makefile b/Makefile index df71b871..5572b80c 100644 --- a/Makefile +++ b/Makefile @@ -82,6 +82,11 @@ update-dependencies: @make freeze-dependencies +.PHONY: format +format: setup + @. .venv/bin/activate && black . + + .PHONY: test test: setup ## Runs the unit tests @. .venv/bin/activate && ./manage.py test diff --git a/core/admin.py b/core/admin.py index f9aa616f..ad276c4e 100644 --- a/core/admin.py +++ b/core/admin.py @@ -7,6 +7,7 @@ def getDerivedAdmin(base_admin, **kwargs): class DerivedAdmin(base_admin): pass + derived = DerivedAdmin for k, v in kwargs.iteritems(): setattr(derived, k, getattr(base_admin, k, []) + v) @@ -26,7 +27,7 @@ class UserProfileInline(admin.StackedInline): class UserAdmin(auth.admin.UserAdmin): - inlines = (UserProfileInline, ) + inlines = (UserProfileInline,) # Register the admins diff --git a/core/ajax/utils.py b/core/ajax/utils.py index 1abff853..7fc89d46 100644 --- a/core/ajax/utils.py +++ b/core/ajax/utils.py @@ -1,4 +1,3 @@ - from django.http import HttpResponse import json diff --git a/core/apps.py b/core/apps.py index a4a1af30..c54ad0f8 100644 --- a/core/apps.py +++ b/core/apps.py @@ -2,6 +2,7 @@ from core.django_mdmail import convert_md_templates + class CoreConfig(AppConfig): name = 'core' verbose_name = 'Wasa2il Core' diff --git a/core/authentication.py b/core/authentication.py index d6278b3d..e0c2059b 100644 --- a/core/authentication.py +++ b/core/authentication.py @@ -21,6 +21,7 @@ def authenticate(self, request, username=None, password=None): class EmailAuthenticationBackend(CustomAuthenticationBackend): """Allow users to log in using their e-mail address""" + def custom_get_user(self, email): try: return User.objects.get(email=email) @@ -30,6 +31,7 @@ def custom_get_user(self, email): class SSNAuthenticationBackend(CustomAuthenticationBackend): """Allow users to log in using their SSN""" + def custom_get_user(self, ssn): # FIXME: This may be Iceland specific; we ignore dashes. ssn = ssn.replace('-', '').strip() diff --git a/core/contextprocessors.py b/core/contextprocessors.py index 3f2d025a..3f6c5034 100644 --- a/core/contextprocessors.py +++ b/core/contextprocessors.py @@ -13,7 +13,7 @@ def globals(request): 'INSTANCE_VERSION': settings.WASA2IL_VERSION, 'FEATURES': settings.FEATURES, 'GCM_APP_ID': settings.GCM_APP_ID, - 'settings': settings + 'settings': settings, } # Get global variables from GlobalsMiddleWare. @@ -25,7 +25,9 @@ def globals(request): def auto_logged_out(request): if hasattr(request, 'auto_logged_out') and request.auto_logged_out: return { - 'splash_message': _('For security reasons, you have been automatically logged out due to inactivity.') + 'splash_message': _( + 'For security reasons, you have been automatically logged out due to inactivity.' + ) } return {} diff --git a/core/dataviews.py b/core/dataviews.py index 55d4edc5..aee91435 100644 --- a/core/dataviews.py +++ b/core/dataviews.py @@ -3,90 +3,91 @@ from election.models import Election from issue.models import Issue + @jsonize def recent_activity(request): # Names prefixed with "q_" to distinguish them as queries. We will be # returning the same data in JSON format, which we will call "issues" and # "elections". - q_issues = Issue.objects.select_related('polity').recent().order_by('polity__id', '-deadline_votes') - q_elections = Election.objects.prefetch_related( - 'electionvote_set', - 'candidate_set' - ).select_related( - 'result' - ).recent() + q_issues = ( + Issue.objects.select_related('polity') + .recent() + .order_by('polity__id', '-deadline_votes') + ) + q_elections = ( + Election.objects.prefetch_related('electionvote_set', 'candidate_set') + .select_related('result') + .recent() + ) issues = [] for q_issue in q_issues: - issues.append({ - # Web location for further info on the issue. - 'url': request.build_absolute_uri( - reverse('issue', args=(q_issue.polity_id, q_issue.id)) - ), - - # The polity to which this issue belongs. - 'polity': q_issue.polity.name, - - # The polity's short name, if available. - 'polity_shortname': q_issue.polity.name_short, - - # A unique identifier for formal reference. Example: 6/2019 - 'log_number': '%d/%d' % (q_issue.issue_num, q_issue.issue_year), - - # The issue's name or title. - 'name': q_issue.name, - - # Options are: concluded/voting/accepting_proposals/discussion - # Note that the state does not give the *result*, i.e. whether the - # proposal was accepted or rejected, but rather where the issue is - # currently in the decision-making process. Therefore "concluded" - # only means that the issue has concluded, but does not tell us - # *how* it concluded. - 'state': q_issue.issue_state(), - - # Translated, human-readable version of the issue state. - 'state_human_readable': q_issue.get_issue_state_display(), - - # A boolean indicating whether the issue has been approved or not. - 'majority_reached': q_issue.majority_reached(), - - # Translated, human-readable version of the result. - 'majority_reached_human_readable': q_issue.get_majority_reached_display(), - - # When the issue's fate is not determined by vote from within - # Wasa2il, for example when a vote is made outside of Wasa2il but - # still placed here for reference or historical reasons, or when - # an issue is retracted without ever coming to a vote. - # - # Consider displaying only this value if it is non-null, and the - # `majority_reached` value only if this is null. - 'special_process': q_issue.special_process, - - # Translated, human-readable version of the result. - 'special_process_human_readable': q_issue.get_special_process_display(), - - # Comment count. - 'comment_count': q_issue.comment_count, - - # Vote count. - 'vote_count': q_issue.votecount, - }) + issues.append( + { + # Web location for further info on the issue. + 'url': request.build_absolute_uri( + reverse('issue', args=(q_issue.polity_id, q_issue.id)) + ), + # The polity to which this issue belongs. + 'polity': q_issue.polity.name, + # The polity's short name, if available. + 'polity_shortname': q_issue.polity.name_short, + # A unique identifier for formal reference. Example: 6/2019 + 'log_number': '%d/%d' + % (q_issue.issue_num, q_issue.issue_year), + # The issue's name or title. + 'name': q_issue.name, + # Options are: concluded/voting/accepting_proposals/discussion + # Note that the state does not give the *result*, i.e. whether the + # proposal was accepted or rejected, but rather where the issue is + # currently in the decision-making process. Therefore "concluded" + # only means that the issue has concluded, but does not tell us + # *how* it concluded. + 'state': q_issue.issue_state(), + # Translated, human-readable version of the issue state. + 'state_human_readable': q_issue.get_issue_state_display(), + # A boolean indicating whether the issue has been approved or not. + 'majority_reached': q_issue.majority_reached(), + # Translated, human-readable version of the result. + 'majority_reached_human_readable': q_issue.get_majority_reached_display(), + # When the issue's fate is not determined by vote from within + # Wasa2il, for example when a vote is made outside of Wasa2il but + # still placed here for reference or historical reasons, or when + # an issue is retracted without ever coming to a vote. + # + # Consider displaying only this value if it is non-null, and the + # `majority_reached` value only if this is null. + 'special_process': q_issue.special_process, + # Translated, human-readable version of the result. + 'special_process_human_readable': q_issue.get_special_process_display(), + # Comment count. + 'comment_count': q_issue.comment_count, + # Vote count. + 'vote_count': q_issue.votecount, + } + ) elections = [] for q_election in q_elections: # See comments for issue above, which are more detailed but are mostly # applicable to this section as well. - elections.append({ - 'url': request.build_absolute_uri(reverse('election', args=(q_election.polity_id, q_election.id))), - 'polity': q_election.polity.name, - 'polity_shortname': q_election.polity.name_short, - 'name': q_election.name, - 'state': q_election.election_state(), - 'state_human_readable': q_election.get_election_state_display(), - 'candidate_count': q_election.candidate_set.count(), - 'vote_count': q_election.get_vote_count(), - }) + elections.append( + { + 'url': request.build_absolute_uri( + reverse( + 'election', args=(q_election.polity_id, q_election.id) + ) + ), + 'polity': q_election.polity.name, + 'polity_shortname': q_election.polity.name_short, + 'name': q_election.name, + 'state': q_election.election_state(), + 'state_human_readable': q_election.get_election_state_display(), + 'candidate_count': q_election.candidate_set.count(), + 'vote_count': q_election.get_vote_count(), + } + ) return { 'elections': elections, diff --git a/core/django_mdmail.py b/core/django_mdmail.py index ea09a300..0910ca89 100644 --- a/core/django_mdmail.py +++ b/core/django_mdmail.py @@ -46,6 +46,7 @@ from django.conf import settings from django.core.mail import EmailMultiAlternatives + if sys.version_info[0] == 3: from email.mime.image import MIMEImage else: @@ -57,17 +58,24 @@ OVERRIDE_WARNING = 'WARNING! THIS FILE IS AUTO-GENERATED by django_mdmail upon Django startup. Changes to this file WILL be overwritten. In the same directory, there should be a file with the same name, except an ".md" ending (for Markdown). Edit that instead and restart Django.' -def send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None): +def send_mail( + subject, + message, + from_email, + recipient_list, + fail_silently=False, + auth_user=None, + auth_password=None, + connection=None, + html_message=None, +): # Have `mdmail` do its Markdown magic. content = EmailContent(message) # Create the email message and fill it with the relevant data. email = EmailMultiAlternatives( - subject, - content.text, - from_email, - recipient_list + subject, content.text, from_email, recipient_list ) email.attach_alternative(html_message or content.html, 'text/html') email.mixed_subtype = 'related' @@ -115,7 +123,9 @@ def ready(self): convert_md_templates() ''' - override_comment = '{%% comment %%}%s{%% endcomment %%}\n' % OVERRIDE_WARNING + override_comment = ( + '{%% comment %%}%s{%% endcomment %%}\n' % OVERRIDE_WARNING + ) # Find all the template directories we'll need to process and put them # in a flat list. diff --git a/core/forms.py b/core/forms.py index 2795bb88..a11640d3 100644 --- a/core/forms.py +++ b/core/forms.py @@ -21,11 +21,25 @@ class EmailWantedField(ChoiceWidget): class UserProfileForm(Wasa2ilForm): - email = EmailField(label=_("E-mail"), help_text=_("You can change your email address, but will then need to verify it.")) + email = EmailField( + label=_("E-mail"), + help_text=_( + "You can change your email address, but will then need to verify it." + ), + ) class Meta: model = UserProfile - fields = ('displayname', 'email', 'phone', 'picture', 'bio', 'declaration_of_interests', 'language', 'email_wanted') + fields = ( + 'displayname', + 'email', + 'phone', + 'picture', + 'bio', + 'declaration_of_interests', + 'language', + 'email_wanted', + ) # We need to keep the 'request' object for certain kinds of validation ('picture' in this case) def __init__(self, *args, **kwargs): @@ -38,15 +52,20 @@ def clean_picture(self): picture = self.request.FILES.get('picture') if picture: if picture.name.find('.') == -1: - raise ValidationError(_('Filename must contain file extension')) + raise ValidationError( + _('Filename must contain file extension') + ) else: ext = picture.name.split('.')[-1].lower() allowed_exts = ['jpg', 'jpeg', 'png', 'gif'] if ext not in allowed_exts: - raise ValidationError(u'%s: %s' % ( - _('Only the following file endings are allowed'), - ', '.join(allowed_exts) - )) + raise ValidationError( + u'%s: %s' + % ( + _('Only the following file endings are allowed'), + ', '.join(allowed_exts), + ) + ) return data @@ -55,13 +74,16 @@ class Wasa2ilRegistrationForm(RegistrationForm): username = UsernameField( widget=TextInput(attrs={'autofocus': True}), label=_('Username'), - help_text=_('Only letters, numbers and the symbols @/./+/-/_ are allowed.') #_('Aðeins er leyfilegt að nota bókstafi, tölustafi og táknin @/./+/-/_') + help_text=_( + 'Only letters, numbers and the symbols @/./+/-/_ are allowed.' + ), # _('Aðeins er leyfilegt að nota bókstafi, tölustafi og táknin @/./+/-/_') ) email_wanted = TypedChoiceField( choices=((True, _('Yes')), (False, _('No'))), widget=EmailWantedField, - label=_('Consent for sending email') + label=_('Consent for sending email'), ) + class PushNotificationForm(Form): text = CharField(label=_('Message')) diff --git a/core/management/commands/closeissues.py b/core/management/commands/closeissues.py index 204ebeb0..cb629be0 100644 --- a/core/management/commands/closeissues.py +++ b/core/management/commands/closeissues.py @@ -5,8 +5,8 @@ from core.models import * -class Command(BaseCommand): +class Command(BaseCommand): def handle(self, *args, **options): now = datetime.now() @@ -15,7 +15,9 @@ def handle(self, *args, **options): for i in issues: if i.deadline_votes > now: - stdout.write("Setting closing time of issue '%s' to now..." % i.name) + stdout.write( + "Setting closing time of issue '%s' to now..." % i.name + ) stdout.flush() i.deadline_votes = now @@ -27,4 +29,3 @@ def handle(self, *args, **options): i.save() stdout.write(" done\n") - diff --git a/core/management/commands/export_db.py b/core/management/commands/export_db.py index 00383208..02a3b56e 100644 --- a/core/management/commands/export_db.py +++ b/core/management/commands/export_db.py @@ -17,8 +17,8 @@ SUPPORTED_ENGINES = ['django.db.backends.mysql'] -class Command(BaseCommand): +class Command(BaseCommand): def handle(self, *args, **options): # Creates a random datetime from the beginning of 2010 to now. @@ -32,7 +32,9 @@ def random_time(): # Creates a random string according to specifications. def ran(length_min, length_max=0, lc=False, uc=False, digits=False): if lc == uc == digits == False: - raise Exception('ran function: At last one of parameters "lc", "uc" or "digits" must be True.') + raise Exception( + 'ran function: At last one of parameters "lc", "uc" or "digits" must be True.' + ) chars = '' if lc: @@ -53,8 +55,8 @@ def ran(length_min, length_max=0, lc=False, uc=False, digits=False): # the impression that they might be names in an exotic language. def random_name(): chars = { - 'v': 'eyuioa', # Vowels - 'c': 'qwrtpsdfghjklzxcvbnm' # Consonants, + 'v': 'eyuioa', # Vowels + 'c': 'qwrtpsdfghjklzxcvbnm', # Consonants, } length = random.randint(3, 10) @@ -82,17 +84,30 @@ def random_name(): # Replace personal data with random garbage. for user in User.objects.using('export').select_related('userprofile'): user.username = ran(6, 12, lc=True) - user.email = '%s@%s.%s' % (ran(4, 10, lc=True), ran(4, 10, lc=True), ran(2, lc=True)) + user.email = '%s@%s.%s' % ( + ran(4, 10, lc=True), + ran(4, 10, lc=True), + ran(2, lc=True), + ) user.date_joined = random_time() if hasattr(user, 'userprofile'): user.userprofile.verified_ssn = ran(10, digits=True) - user.userprofile.verified_name = '%s %s' % (random_name(), random_name()) - user.userprofile.verified_token = ran(30, lc=True, uc=True, digits=True) - user.userprofile.verified_assertion_id = ran(30, lc=True, uc=True, digits=True) + user.userprofile.verified_name = '%s %s' % ( + random_name(), + random_name(), + ) + user.userprofile.verified_token = ran( + 30, lc=True, uc=True, digits=True + ) + user.userprofile.verified_assertion_id = ran( + 30, lc=True, uc=True, digits=True + ) user.userprofile.verified_timing = random_time() user.userprofile.bio = 'The entire bio has been replaced with this mysterious text.' - user.userprofile.declaration_of_interests = 'The interest rate is currently around 470%.' + user.userprofile.declaration_of_interests = ( + 'The interest rate is currently around 470%.' + ) user.userprofile.picture = None user.userprofile.joined_org = user.date_joined @@ -102,15 +117,21 @@ def random_name(): user.save() - # Create an export database, an exact replica of the default database. def mirror_databases(self): if not 'export' in settings.DATABASES: - raise Exception('This function only works if an export database is defined in settings.') + raise Exception( + 'This function only works if an export database is defined in settings.' + ) - if settings.DATABASES['default']['ENGINE'] != settings.DATABASES['export']['ENGINE']: - raise Exception('Database engine of default and export databases must be the same.') + if ( + settings.DATABASES['default']['ENGINE'] + != settings.DATABASES['export']['ENGINE'] + ): + raise Exception( + 'Database engine of default and export databases must be the same.' + ) engine = settings.DATABASES['default']['ENGINE'] username = settings.DATABASES['default']['USER'] @@ -119,26 +140,43 @@ def mirror_databases(self): db_export = settings.DATABASES['export']['NAME'] if engine not in SUPPORTED_ENGINES: - raise Exception('Database engine %s not (yet) supported for exporting.' % engine) + raise Exception( + 'Database engine %s not (yet) supported for exporting.' + % engine + ) if engine == 'django.db.backends.mysql': # Make sure that database is empty and that it exists. subprocess.check_output( - ['mysql', '-u', username, '-p%s' % password, '-e', 'DROP DATABASE IF EXISTS `%s`;' % db_export] + [ + 'mysql', + '-u', + username, + '-p%s' % password, + '-e', + 'DROP DATABASE IF EXISTS `%s`;' % db_export, + ] ) subprocess.check_output( - ['mysql', '-u', username, '-p%s' % password, '-e', 'CREATE DATABASE `%s`;' % db_export] + [ + 'mysql', + '-u', + username, + '-p%s' % password, + '-e', + 'CREATE DATABASE `%s`;' % db_export, + ] ) # Transfer schema and data from default database to export # database. ps = subprocess.Popen( ['mysqldump', db_default, '-u', username, '-p%s' % password], - stdout=subprocess.PIPE + stdout=subprocess.PIPE, ) output = subprocess.check_output( ['mysql', db_export, '-u', username, '-p%s' % password], - stdin=ps.stdout + stdin=ps.stdout, ) ps.wait() diff --git a/core/management/commands/heartbeat.py b/core/management/commands/heartbeat.py index e41bf3bb..ef303cd1 100644 --- a/core/management/commands/heartbeat.py +++ b/core/management/commands/heartbeat.py @@ -4,6 +4,7 @@ from core.utils import heartbeat + class Command(BaseCommand): def handle(self, *args, **options): """ diff --git a/core/management/commands/load_fake_data.py b/core/management/commands/load_fake_data.py index 617cdef4..157b3611 100644 --- a/core/management/commands/load_fake_data.py +++ b/core/management/commands/load_fake_data.py @@ -22,11 +22,30 @@ ADJECTIVES = ('Sad', 'Happy', 'Good', 'Evil', 'Liberal', 'Dadaist', 'Hungry') -THINGS = ('Chicken', 'Human', 'Automobile', 'Fish', 'Puppy', 'Kitten', - 'Infant', 'Lobster', 'Nature') +THINGS = ( + 'Chicken', + 'Human', + 'Automobile', + 'Fish', + 'Puppy', + 'Kitten', + 'Infant', + 'Lobster', + 'Nature', +) ACTIONS = ('Farming', 'Justice', 'Education', 'Welfare', 'Entertainment') -ACTACTS = ('Stop', 'Improve', 'Defuzz', 'Budget', 'Plan', 'Restrict', - 'Disarm', 'Avoid', 'Protest', 'Support') +ACTACTS = ( + 'Stop', + 'Improve', + 'Defuzz', + 'Budget', + 'Plan', + 'Restrict', + 'Disarm', + 'Avoid', + 'Protest', + 'Support', +) def req(method, user, data, **kwargs): @@ -38,10 +57,15 @@ def req(method, user, data, **kwargs): class Command(BaseCommand): - def add_arguments(self, parser): - for flag in ('users', 'topics', 'documents', 'elections', - 'reset', 'full'): + for flag in ( + 'users', + 'topics', + 'documents', + 'elections', + 'reset', + 'full', + ): parser.add_argument('--%s' % flag, action='store_true', dest=flag) @transaction.atomic @@ -50,30 +74,38 @@ def handle(self, *args, **options): if not options.get('full'): print() - print('NOTE: Creating small test data set, use --full for MOAR DATA.') + print( + 'NOTE: Creating small test data set, use --full for MOAR DATA.' + ) print() reset = False if options.get('reset'): - yn = raw_input('Are you sure you want to delete precious data? [y/N] ') + yn = raw_input( + 'Are you sure you want to delete precious data? [y/N] ' + ) if yn.strip().lower() == 'y': reset = True else: return - create_all = not (options.get('users', False) or - options.get('topics', False) or - options.get('elections', False) or - options.get('documents', False)) + create_all = not ( + options.get('users', False) + or options.get('topics', False) + or options.get('elections', False) + or options.get('documents', False) + ) userlist = [ ('a', 'a@example.com', 'Alpha'), ('b', 'b@example.com', 'Beta'), ('c', 'c@example.com', 'Foo'), - ('d', 'd@example.com', 'Baz')] + ('d', 'd@example.com', 'Baz'), + ] userlist += [ ('user%s' % i, 'user%s@example.com' % i, 'User %s' % i) - for i in range(0, 1110)] + for i in range(0, 1110) + ] serial_ssn = 0 if options.get('users') or create_all: if not options.get('full'): @@ -88,7 +120,10 @@ def handle(self, *args, **options): users[u] = User.objects.create_user(u, password=u) users[u].is_staff = True users[u].is_superuser = True - print(' * Creating user "%s" with password "%s"' % (u, u)) + print( + ' * Creating user "%s" with password "%s"' + % (u, u) + ) else: users[u] = User.objects.create_user(u) users[u].email = email @@ -98,8 +133,10 @@ def handle(self, *args, **options): # (UserProfile is automatically created when User is # saved, via signal. up = users[u].userprofile - up.verified_ssn = '%10.10d' % serial_ssn, - up.joined_org = now - timedelta(hours=random.randint(0, 24 * 5)) + up.verified_ssn = ('%10.10d' % serial_ssn,) + up.joined_org = now - timedelta( + hours=random.randint(0, 24 * 5) + ) up.save() serial_ssn += 1 @@ -109,10 +146,11 @@ def handle(self, *args, **options): print('Generating/updating 4 polities of varying sizes ...') pollist = [ - ('d', 'The Big Polity', 'abc', 1000), - ('c', 'The Medium Polity', 'abc', 100), - ('b', 'The Small Polity', 'ab', 10), - ('a', 'The Dinky Polity', 'a', 1)] + ('d', 'The Big Polity', 'abc', 1000), + ('c', 'The Medium Polity', 'abc', 100), + ('b', 'The Small Polity', 'ab', 10), + ('a', 'The Dinky Polity', 'a', 1), + ] if not options.get('full'): pollist = pollist[2:] topiclist = [] @@ -129,9 +167,11 @@ def handle(self, *args, **options): p = Polity.objects.get(name=name) new = False except: - p = Polity(name=name, - slug=name.lower().replace(' ', '-'), - description='A polity with about %d things' % size) + p = Polity( + name=name, + slug=name.lower().replace(' ', '-'), + description='A polity with about %d things' % size, + ) p.created = now - timedelta(hours=random.randint(0, 24 * 5)) p.created_by = usr p.modified_by = usr @@ -142,25 +182,24 @@ def handle(self, *args, **options): issue_majority=50, issue_discussion_time=timedelta(hours=24), issue_proposal_time=timedelta(hours=24), - issue_vote_time=timedelta(hours=24) + issue_vote_time=timedelta(hours=24), ).save() new = True polities[u] = (p, size) if new or options.get('topics') or create_all: - n = 1 + min(size//5, len(topiclist)) + n = 1 + min(size // 5, len(topiclist)) print(' - Creating %d topics' % n) if reset: Topic.objects.filter(polity=p).delete() for topic in random.sample(topiclist, n): - Topic(name=topic, - polity=p, - created_by=usr).save() + Topic(name=topic, polity=p, created_by=usr).save() if new or options.get('users') or create_all: print(' - Adding ~%d users' % size) - for m in set([m for m in members] + - random.sample(users.keys(), size)): + for m in set( + [m for m in members] + random.sample(users.keys(), size) + ): try: # User d is a member of no polities if m != 'd': @@ -181,7 +220,8 @@ def handle(self, *args, **options): voting_system='schulze', deadline_candidacy=now + timedelta(days=dc), deadline_votes=now + timedelta(days=dv), - deadline_joined_org=now + timedelta(days=dv)) + deadline_joined_org=now + timedelta(days=dv), + ) e.save() if (dc < 0) or (dv < 0): @@ -192,33 +232,46 @@ def handle(self, *args, **options): voterc = min(p.members.count(), 5) candidates = [] - for cand in random.sample(list(p.members.all()), candidatec): + for cand in random.sample( + list(p.members.all()), candidatec + ): c = Candidate(election=e, user=cand) c.save() candidates.append(c) for voter in random.sample(list(p.members.all()), voterc): random.shuffle(candidates) for rank, cand in enumerate(candidates): - ElectionVote( - election=e, - user=voter, - candidate=cand, - value=rank).save() + ElectionVote( + election=e, + user=voter, + candidate=cand, + value=rank, + ).save() if (dv < 0) and voterc and candidatec: try: e.process() except: traceback.print_exc() - print('Votes cast on %s: %s' % (e, ElectionVote.objects.filter(election=e).count())) + print( + 'Votes cast on %s: %s' + % ( + e, + ElectionVote.objects.filter( + election=e + ).count(), + ) + ) if new or options.get('documents') or create_all: # We create a list of authors biased towards the first # users created, so some users will have lots of documents # and others will have less. ul = [username for username, e, n in userlist] - aw = [(m.username, max(20 - ul.index(m.username), 1)) - for m in p.members.all()] + aw = [ + (m.username, max(20 - ul.index(m.username), 1)) + for m in p.members.all() + ] authors = [a for a, w in aw for i in range(0, w)] # Get a list of topics... @@ -229,23 +282,26 @@ def handle(self, *args, **options): Document.objects.filter(polity=p).delete() for docn in range(0, size): topic = random.choice(topics) - subject = '%s %s with %s' % (random.choice(ACTACTS), - topic.name, - random.choice(THINGS) + 's') + subject = '%s %s with %s' % ( + random.choice(ACTACTS), + topic.name, + random.choice(THINGS) + 's', + ) author = User.objects.get(username=random.choice(authors)) - doc = Document( - name=subject, - user=author, - polity=p) + doc = Document(name=subject, user=author, polity=p) doc.save() - doc.created = now - timedelta(hours=random.randint(0, 24 * 3)) + doc.created = now - timedelta( + hours=random.randint(0, 24 * 3) + ) doc.save() documents[doc.id] = (topic, doc) text = subject for version in range(0, random.randint(1, 3)): text = '%s\n%s' % (text, text) - docc = DocumentContent(document=doc, user=author, text=text) + docc = DocumentContent( + document=doc, user=author, text=text + ) docc.status = 'proposed' docc.order = version docc.save() @@ -265,7 +321,8 @@ def handle(self, *args, **options): issue_year=2018, ruleset=PolityRuleset.objects.filter(polity=doc.polity)[0], majority_percentage=50, - documentcontent=doc.preferred_version()) + documentcontent=doc.preferred_version(), + ) i.save() i.created = doc.created i.apply_ruleset(now=doc.created) diff --git a/core/management/commands/lookup_usernames.py b/core/management/commands/lookup_usernames.py index 3a54bd17..55852088 100644 --- a/core/management/commands/lookup_usernames.py +++ b/core/management/commands/lookup_usernames.py @@ -9,7 +9,6 @@ class Command(BaseCommand): - def add_arguments(self, parser): parser.add_argument('username', nargs='+', action='append') @@ -22,5 +21,5 @@ def handle(self, *args, **options): name = User.objects.get(username=u).get_name() except: name = '[no such user]' - print ('%d. %s (%s)' % (count, name, u)).encode('utf-8') + print('%d. %s (%s)' % (count, name, u)).encode('utf-8') count += 1 diff --git a/core/management/commands/processissues.py b/core/management/commands/processissues.py index 82b4b798..7990f9f5 100644 --- a/core/management/commands/processissues.py +++ b/core/management/commands/processissues.py @@ -6,15 +6,14 @@ from core.models import * -class Command(BaseCommand): +class Command(BaseCommand): def handle(self, *args, **options): now = datetime.now() unprocessed_issues = Issue.objects.filter( - deadline_votes__lte=now, - is_processed=False + deadline_votes__lte=now, is_processed=False ).order_by('deadline_votes', 'id') for issue in unprocessed_issues: diff --git a/core/middleware.py b/core/middleware.py index dcd46d68..be15c539 100644 --- a/core/middleware.py +++ b/core/middleware.py @@ -1,4 +1,3 @@ - from django.conf import settings from django.shortcuts import redirect, render from django.urls import resolve @@ -33,20 +32,28 @@ def process_request(self, request): if 'polity_id' in match.kwargs: polity_id = int(match.kwargs['polity_id']) - global_vars['polity'] = polity = Polity.objects.prefetch_related( - 'members', - 'officers', - 'wranglers' - ).get(id=polity_id) + global_vars[ + 'polity' + ] = polity = Polity.objects.prefetch_related( + 'members', 'officers', 'wranglers' + ).get( + id=polity_id + ) if not request.user.is_anonymous: - global_vars['user_is_member'] = request.user in polity.members.all() - global_vars['user_is_officer'] = request.user in polity.officers.all() + global_vars['user_is_member'] = ( + request.user in polity.members.all() + ) + global_vars['user_is_officer'] = ( + request.user in polity.officers.all() + ) # Officers are automatically wranglers. if global_vars['user_is_officer']: global_vars['user_is_wrangler'] = True else: - global_vars['user_is_wrangler'] = request.user in polity.wranglers.all() + global_vars['user_is_wrangler'] = ( + request.user in polity.wranglers.all() + ) except: # Basically only 404-errors and such cause errors here. Besides, # we'll want to move on with our lives anyway. @@ -63,20 +70,29 @@ def process_request(self, request): now = datetime.now() - if not request.user.is_authenticated : + if not request.user.is_authenticated: # Set the last visit to now when attempting to log in, so that # auto-logout feature doesn't immediately log the user out # when the user is already logged out but the session is still # active. - if request.path_info == '/accounts/login/' and request.method == 'POST': - request.session['last_visit'] = now.strftime('%Y-%m-%d %H:%M:%S') + if ( + request.path_info == '/accounts/login/' + and request.method == 'POST' + ): + request.session['last_visit'] = now.strftime( + '%Y-%m-%d %H:%M:%S' + ) # Can't log out if not logged in return if 'last_visit' in request.session: - last_visit = datetime.strptime(request.session['last_visit'], '%Y-%m-%d %H:%M:%S') - if now - last_visit > timedelta(0, settings.AUTO_LOGOUT_DELAY * 60, 0): + last_visit = datetime.strptime( + request.session['last_visit'], '%Y-%m-%d %H:%M:%S' + ) + if now - last_visit > timedelta( + 0, settings.AUTO_LOGOUT_DELAY * 60, 0 + ): auth.logout(request) request.auto_logged_out = True @@ -88,10 +104,12 @@ def process_request(self, request): class SamlMiddleware(MiddlewareMixin): def process_request(self, request): - if settings.SAML['URL']: # Is SAML support enabled? + if settings.SAML['URL']: # Is SAML support enabled? if hasattr(settings, 'SAML_VERIFICATION_EXCLUDE_URL_PREFIX_LIST'): - exclude_urls = settings.SAML_VERIFICATION_EXCLUDE_URL_PREFIX_LIST + exclude_urls = ( + settings.SAML_VERIFICATION_EXCLUDE_URL_PREFIX_LIST + ) else: exclude_urls = [] @@ -99,20 +117,26 @@ def process_request(self, request): path_ok = request.path_info in [ '/accounts/verify/', '/accounts/logout/', - '/accounts/login-or-saml-redirect/' + '/accounts/login-or-saml-redirect/', ] or any([request.path_info.find(p) == 0 for p in exclude_urls]) logged_in = request.user.is_authenticated - verified = request.user.userprofile.verified if logged_in else False + verified = ( + request.user.userprofile.verified if logged_in else False + ) if logged_in and not verified and not path_ok: - ctx = { 'auth_url': settings.SAML['URL'] } - return render(request, 'registration/verification_needed.html', ctx) + ctx = {'auth_url': settings.SAML['URL']} + return render( + request, 'registration/verification_needed.html', ctx + ) def process_response(self, request, response): if settings.SAML['URL'] and hasattr(request, 'user'): logged_in = request.user.is_authenticated - verified = request.user.userprofile.verified if logged_in else False + verified = ( + request.user.userprofile.verified if logged_in else False + ) just_logged_in = ( request.path == '/accounts/login/' and response.status_code == 302 diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 43d63551..ea701484 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -19,21 +19,102 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserProfile', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('verified_ssn', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('verified_name', models.CharField(blank=True, max_length=100, null=True)), - ('verified_token', models.CharField(blank=True, max_length=100, null=True)), - ('verified_timing', models.DateTimeField(blank=True, null=True)), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'verified_ssn', + models.CharField( + blank=True, max_length=30, null=True, unique=True + ), + ), + ( + 'verified_name', + models.CharField(blank=True, max_length=100, null=True), + ), + ( + 'verified_token', + models.CharField(blank=True, max_length=100, null=True), + ), + ( + 'verified_timing', + models.DateTimeField(blank=True, null=True), + ), ('verified', models.BooleanField(default=False)), - ('displayname', models.CharField(blank=True, help_text='The name to display on the site.', max_length=255, null=True, verbose_name='Name')), - ('email_visible', models.BooleanField(default=False, help_text='Whether to display your email address on your profile page.', verbose_name='E-mail visible')), - ('bio', models.TextField(blank=True, null=True, verbose_name='Bio')), - ('picture', models.ImageField(blank=True, null=True, upload_to=b'profiles', verbose_name='Picture')), + ( + 'displayname', + models.CharField( + blank=True, + help_text='The name to display on the site.', + max_length=255, + null=True, + verbose_name='Name', + ), + ), + ( + 'email_visible', + models.BooleanField( + default=False, + help_text='Whether to display your email address on your profile page.', + verbose_name='E-mail visible', + ), + ), + ( + 'bio', + models.TextField( + blank=True, null=True, verbose_name='Bio' + ), + ), + ( + 'picture', + models.ImageField( + blank=True, + null=True, + upload_to=b'profiles', + verbose_name='Picture', + ), + ), ('joined_org', models.DateTimeField(blank=True, null=True)), - ('email_wanted', models.NullBooleanField(default=False, help_text='Whether to consent to receiving notifications via email.', verbose_name='Consent for sending email')), - ('language', models.CharField(choices=[(b'is', b'\xc3\x8dslenska'), (b'en', b'English')], default=b'en', max_length=6, verbose_name='Language')), - ('topics_showall', models.BooleanField(default=True, help_text='Whether to show all topics in a polity, or only starred.')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + 'email_wanted', + models.NullBooleanField( + default=False, + help_text='Whether to consent to receiving notifications via email.', + verbose_name='Consent for sending email', + ), + ), + ( + 'language', + models.CharField( + choices=[ + (b'is', b'\xc3\x8dslenska'), + (b'en', b'English'), + ], + default=b'en', + max_length=6, + verbose_name='Language', + ), + ), + ( + 'topics_showall', + models.BooleanField( + default=True, + help_text='Whether to show all topics in a polity, or only starred.', + ), + ), + ( + 'user', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/core/migrations/0002_reset_migrations.py b/core/migrations/0002_reset_migrations.py index 0d18d52f..5a381d0a 100644 --- a/core/migrations/0002_reset_migrations.py +++ b/core/migrations/0002_reset_migrations.py @@ -39,6 +39,7 @@ ''' + class Migration(migrations.Migration): dependencies = [ @@ -46,14 +47,26 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunSQL([ - (""" + migrations.RunSQL( + [ + ( + """ DELETE FROM django_migrations WHERE app = '%s' AND - name != '0001_initial'""" % app - ) for app in ['core', 'polity', 'issue', 'tasks', 'topic', 'election']] + name != '0001_initial'""" + % app + ) + for app in [ + 'core', + 'polity', + 'issue', + 'tasks', + 'topic', + 'election', + ] + ] ) ] diff --git a/core/migrations/0003_event.py b/core/migrations/0003_event.py index a3d554b6..5b68f700 100644 --- a/core/migrations/0003_event.py +++ b/core/migrations/0003_event.py @@ -18,13 +18,29 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Event', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('timestamp', models.DateTimeField(auto_now=True)), ('module', models.CharField(max_length=32)), ('action', models.CharField(max_length=32)), ('category', models.CharField(blank=True, max_length=64)), ('event', models.CharField(blank=True, max_length=1024)), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + 'user', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/core/migrations/0004_userprofile_declaration_of_interests.py b/core/migrations/0004_userprofile_declaration_of_interests.py index 28735d96..e678580f 100644 --- a/core/migrations/0004_userprofile_declaration_of_interests.py +++ b/core/migrations/0004_userprofile_declaration_of_interests.py @@ -15,6 +15,8 @@ class Migration(migrations.Migration): migrations.AddField( model_name='userprofile', name='declaration_of_interests', - field=models.TextField(blank=True, null=True, verbose_name='Declaration of interests'), + field=models.TextField( + blank=True, null=True, verbose_name='Declaration of interests' + ), ), ] diff --git a/core/migrations/0005_auto_20190822_2006.py b/core/migrations/0005_auto_20190822_2006.py index fd7cca2a..121cebba 100644 --- a/core/migrations/0005_auto_20190822_2006.py +++ b/core/migrations/0005_auto_20190822_2006.py @@ -15,16 +15,31 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='event', name='user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( model_name='userprofile', name='language', - field=models.CharField(choices=[('is', 'Íslenska'), ('en', 'English')], default='en', max_length=6, verbose_name='Language'), + field=models.CharField( + choices=[('is', 'Íslenska'), ('en', 'English')], + default='en', + max_length=6, + verbose_name='Language', + ), ), migrations.AlterField( model_name='userprofile', name='picture', - field=models.ImageField(blank=True, null=True, upload_to='profiles', verbose_name='Picture'), + field=models.ImageField( + blank=True, + null=True, + upload_to='profiles', + verbose_name='Picture', + ), ), ] diff --git a/core/migrations/0006_userprofile_phone.py b/core/migrations/0006_userprofile_phone.py index 4f2888d5..d73ccf89 100644 --- a/core/migrations/0006_userprofile_phone.py +++ b/core/migrations/0006_userprofile_phone.py @@ -15,6 +15,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='userprofile', name='phone', - field=models.CharField(blank=True, help_text='Mostly intended for active participants such as volunteers and candidates.', max_length=30, null=True, verbose_name='Phone'), + field=models.CharField( + blank=True, + help_text='Mostly intended for active participants such as volunteers and candidates.', + max_length=30, + null=True, + verbose_name='Phone', + ), ), ] diff --git a/core/migrations/0007_merge_20191019_1939.py b/core/migrations/0007_merge_20191019_1939.py index 6f72e9d5..b9674865 100644 --- a/core/migrations/0007_merge_20191019_1939.py +++ b/core/migrations/0007_merge_20191019_1939.py @@ -10,5 +10,4 @@ class Migration(migrations.Migration): ('core', '0006_userprofile_phone'), ] - operations = [ - ] + operations = [] diff --git a/core/migrations/0008_auto_20210116_2004.py b/core/migrations/0008_auto_20210116_2004.py index 92e06859..0f3f427c 100644 --- a/core/migrations/0008_auto_20210116_2004.py +++ b/core/migrations/0008_auto_20210116_2004.py @@ -13,6 +13,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='userprofile', name='email_wanted', - field=models.BooleanField(default=False, help_text='Whether to consent to receiving notifications via email.', null=True, verbose_name='Consent for sending email'), + field=models.BooleanField( + default=False, + help_text='Whether to consent to receiving notifications via email.', + null=True, + verbose_name='Consent for sending email', + ), ), ] diff --git a/core/migrations/0009_auto_20210827_1638.py b/core/migrations/0009_auto_20210827_1638.py index 136e47f8..a082e88f 100644 --- a/core/migrations/0009_auto_20210827_1638.py +++ b/core/migrations/0009_auto_20210827_1638.py @@ -14,8 +14,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( name='User', - fields=[ - ], + fields=[], options={ 'proxy': True, 'indexes': [], @@ -26,6 +25,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='event', name='user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.User'), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='core.User', + ), ), ] diff --git a/core/models.py b/core/models.py index 75d86cf7..4fd84e31 100644 --- a/core/models.py +++ b/core/models.py @@ -1,4 +1,4 @@ -#coding:utf-8 +# coding:utf-8 import os import re import json @@ -33,8 +33,12 @@ class UserManager(models.Manager): def annotate_task_stats(self): return self.annotate( tasks_applied_count=Count('taskrequest'), - tasks_completed_count=Count('taskrequest', filter=Q(taskrequest__task__is_done=True)), - tasks_accepted_count=Count('taskrequest', filter=Q(taskrequest__is_accepted=True)) + tasks_completed_count=Count( + 'taskrequest', filter=Q(taskrequest__task__is_done=True) + ), + tasks_accepted_count=Count( + 'taskrequest', filter=Q(taskrequest__is_accepted=True) + ), ) @@ -50,18 +54,34 @@ def tasks_percent(self): # Make sure that we instruct the programmer properly if they're using # this function without using the proper annotation function that # produces the required data. - needed_attrs = ['tasks_applied_count', 'tasks_accepted_count', 'tasks_completed_count'] + needed_attrs = [ + 'tasks_applied_count', + 'tasks_accepted_count', + 'tasks_completed_count', + ] if not all(hasattr(self, a) for a in needed_attrs): - raise Exception('User.tasks_percent() function can only be called when User.objects.annotate_task_stats() has been applied') + raise Exception( + 'User.tasks_percent() function can only be called when User.objects.annotate_task_stats() has been applied' + ) # Let's not bother calculating things if everything is zero anyway. if self.tasks_applied_count == 0: return {'applied': 0, 'accepted': 0, 'completed': 100} return { - 'applied': 100*(self.tasks_applied_count - self.tasks_accepted_count - self.tasks_completed_count) / float(self.tasks_applied_count), - 'accepted': 100*(self.tasks_accepted_count - self.tasks_completed_count) / float(self.tasks_applied_count), - 'completed': 100*(self.tasks_completed_count) / float(self.tasks_applied_count) + 'applied': 100 + * ( + self.tasks_applied_count + - self.tasks_accepted_count + - self.tasks_completed_count + ) + / float(self.tasks_applied_count), + 'accepted': 100 + * (self.tasks_accepted_count - self.tasks_completed_count) + / float(self.tasks_applied_count), + 'completed': 100 + * (self.tasks_completed_count) + / float(self.tasks_applied_count), } class Meta: @@ -70,15 +90,20 @@ class Meta: class UserProfile(models.Model): """A user's profile data. Contains various informative areas, plus various settings.""" + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=CASCADE) # Verification # Field `verified_token` was used with SAML 1.2 whereas # `verified_assertion_id` has been used since adopting SAML 2. - verified_ssn = models.CharField(max_length=30, null=True, blank=True, unique=True) + verified_ssn = models.CharField( + max_length=30, null=True, blank=True, unique=True + ) verified_name = models.CharField(max_length=100, null=True, blank=True) verified_token = models.CharField(max_length=100, null=True, blank=True) - verified_assertion_id = models.CharField(max_length=50, null=True, blank=True) + verified_assertion_id = models.CharField( + max_length=50, null=True, blank=True + ) verified_timing = models.DateTimeField(null=True, blank=True) # When using SAML, the 'verified' field is set to true if verified_ssn, # verified_name and verified_timing have all been set with actual content. @@ -86,13 +111,39 @@ class UserProfile(models.Model): verified = models.BooleanField(default=False) # User information - displayname = models.CharField(max_length=255, verbose_name=_("Name"), help_text=_("The name to display on the site."), null=True, blank=True) - phone = models.CharField(max_length=30, verbose_name=_('Phone'), help_text=_('Mostly intended for active participants such as volunteers and candidates.'), null=True, blank=True) - email_visible = models.BooleanField(default=False, verbose_name=_("E-mail visible"), help_text=_("Whether to display your email address on your profile page.")) + displayname = models.CharField( + max_length=255, + verbose_name=_("Name"), + help_text=_("The name to display on the site."), + null=True, + blank=True, + ) + phone = models.CharField( + max_length=30, + verbose_name=_('Phone'), + help_text=_( + 'Mostly intended for active participants such as volunteers and candidates.' + ), + null=True, + blank=True, + ) + email_visible = models.BooleanField( + default=False, + verbose_name=_("E-mail visible"), + help_text=_( + "Whether to display your email address on your profile page." + ), + ) bio = models.TextField(verbose_name=_("Bio"), null=True, blank=True) - declaration_of_interests = models.TextField(verbose_name=_('Declaration of interests'), null=True, blank=True) - picture = models.ImageField(upload_to='profiles', verbose_name=_("Picture"), null=True, blank=True) - joined_org = models.DateTimeField(null=True, blank=True) # Time when user joined organization, as opposed to registered in the system + declaration_of_interests = models.TextField( + verbose_name=_('Declaration of interests'), null=True, blank=True + ) + picture = models.ImageField( + upload_to='profiles', verbose_name=_("Picture"), null=True, blank=True + ) + joined_org = models.DateTimeField( + null=True, blank=True + ) # Time when user joined organization, as opposed to registered in the system # When this is null (None), it means that the user has not consented to, # nor specifically rejected receiving email. This is a left-over state @@ -103,11 +154,23 @@ class UserProfile(models.Model): default=False, null=True, verbose_name=_('Consent for sending email'), - help_text=_('Whether to consent to receiving notifications via email.') + help_text=_( + 'Whether to consent to receiving notifications via email.' + ), ) - language = models.CharField(max_length=6, default='en', choices=settings.LANGUAGES, verbose_name=_("Language")) - topics_showall = models.BooleanField(default=True, help_text=_("Whether to show all topics in a polity, or only starred.")) + language = models.CharField( + max_length=6, + default='en', + choices=settings.LANGUAGES, + verbose_name=_("Language"), + ) + topics_showall = models.BooleanField( + default=True, + help_text=_( + "Whether to show all topics in a polity, or only starred." + ), + ) def save(self, *largs, **kwargs): is_new = self.pk is None @@ -117,15 +180,18 @@ def save(self, *largs, **kwargs): if not self.picture: self.picture.name = os.path.join( - self.picture.field.upload_to, - 'default.jpg' + self.picture.field.upload_to, 'default.jpg' ) if settings.SAML['URL']: - self.verified = all(( - self.verified_ssn is not None and len(self.verified_ssn) > 0, - self.verified_name is not None and len(self.verified_name) > 0 - )) + self.verified = all( + ( + self.verified_ssn is not None + and len(self.verified_ssn) > 0, + self.verified_name is not None + and len(self.verified_name) > 0, + ) + ) else: self.verified = self.user.is_active @@ -137,10 +203,12 @@ def __str__(self): def get_polity_ids(self): return [x.id for x in self.user.polities.all()] + # Make sure registration creates profiles def _create_user_profile(**kwargs): UserProfile.objects.get_or_create(user=kwargs['user']) + user_registered.connect(_create_user_profile) @@ -164,6 +232,7 @@ def get_name(user): return name + # We need to monkey-patch both `BaseUser` and `User` because we've added # `User` as a proxy model. Both monkey-patches should removed when # `get_name` gets refactored out. @@ -180,7 +249,15 @@ class Event(models.Model): event = models.CharField(max_length=1024, blank=True) def __str__(self): - return "[%s][%s.%s/%s@%s] %s" % (self.timestamp, self.module, self.action, self.category, self.user, self.event) + return "[%s][%s.%s/%s@%s] %s" % ( + self.timestamp, + self.module, + self.action, + self.category, + self.user, + self.event, + ) + def event_register(action, category="", event={}, user=None): e = Event() @@ -193,8 +270,11 @@ def event_register(action, category="", event={}, user=None): e.event = json.dumps(event) e.save() + def event_time_since_last(module, action): - e = Event.objects.filter(module=module, action=action).order_by('-timestamp') + e = Event.objects.filter(module=module, action=action).order_by( + '-timestamp' + ) if len(e) == 0: return timedelta(100000000) else: diff --git a/core/saml.py b/core/saml.py index c436ede1..2fb435d9 100644 --- a/core/saml.py +++ b/core/saml.py @@ -15,24 +15,43 @@ class SamlException(Exception): def authenticate(input_xml, ca_pem_file): # @test: Check signature and retrieve the XML that's guaranteed to be signed. - signed_xml = XMLVerifier().verify(input_xml, require_x509=True, ca_pem_file=ca_pem_file).signed_xml + signed_xml = ( + XMLVerifier() + .verify(input_xml, require_x509=True, ca_pem_file=ca_pem_file) + .signed_xml + ) # @test: Verify they're not sending us multiple root level assertions. - if len(signed_xml.findall('./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion')) > 1: + if ( + len( + signed_xml.findall( + './{urn:oasis:names:tc:SAML:2.0:assertion}Assertion' + ) + ) + > 1 + ): raise SamlException('Too many assertion matched') # @process: Obtain the assertion. - assertion = signed_xml.find('./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion') + assertion = signed_xml.find( + './{urn:oasis:names:tc:SAML:2.0:assertion}Assertion' + ) if not assertion: raise SamlException('Could not find valid assertion') # @process: Obtain the conditions. - conds_xml = assertion.find('./{urn:oasis:names:tc:SAML:2.0:assertion}Conditions') + conds_xml = assertion.find( + './{urn:oasis:names:tc:SAML:2.0:assertion}Conditions' + ) if not conds_xml: - raise SamlException('Could not find valid conditions statement. This is required.') + raise SamlException( + 'Could not find valid conditions statement. This is required.' + ) # @test: Verify audience. - audience = conds_xml.find("{urn:oasis:names:tc:SAML:2.0:assertion}AudienceRestriction/{urn:oasis:names:tc:SAML:2.0:assertion}Audience").text + audience = conds_xml.find( + "{urn:oasis:names:tc:SAML:2.0:assertion}AudienceRestriction/{urn:oasis:names:tc:SAML:2.0:assertion}Audience" + ).text if audience not in settings.ALLOWED_HOSTS: raise SamlException('Incorrect audience specified') @@ -40,8 +59,12 @@ def authenticate(input_xml, ca_pem_file): # Apparently the datetimes have 7 digits of microseconds when normally # they should be 6 when parsed. We'll leave them out by only using the # first 19 characters. - time_limit_lower = datetime.strptime(conds_xml.attrib['NotBefore'][:19],'%Y-%m-%dT%H:%M:%S') - time_limit_upper = datetime.strptime(conds_xml.attrib['NotOnOrAfter'][:19],'%Y-%m-%dT%H:%M:%S') + time_limit_lower = datetime.strptime( + conds_xml.attrib['NotBefore'][:19], '%Y-%m-%dT%H:%M:%S' + ) + time_limit_upper = datetime.strptime( + conds_xml.attrib['NotOnOrAfter'][:19], '%Y-%m-%dT%H:%M:%S' + ) now = datetime.now() if time_limit_lower > now or time_limit_upper < now: raise SamlException('Remote authentication expired') @@ -55,7 +78,11 @@ def authenticate(input_xml, ca_pem_file): # @process: Translate SAML attributes into a handy dictionary. attributes = {} - for attribute in assertion.findall('.//{urn:oasis:names:tc:SAML:2.0:assertion}Attribute'): - attributes[attribute.attrib['Name']] = attribute.find('.//{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue').text + for attribute in assertion.findall( + './/{urn:oasis:names:tc:SAML:2.0:assertion}Attribute' + ): + attributes[attribute.attrib['Name']] = attribute.find( + './/{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue' + ).text return assertion_id, attributes diff --git a/core/templatetags/wasa2il.py b/core/templatetags/wasa2il.py index 497521eb..a95b3653 100644 --- a/core/templatetags/wasa2il.py +++ b/core/templatetags/wasa2il.py @@ -26,7 +26,9 @@ def may_expire(dt): css_class = 'expired' if dt < now else 'not-expired' formatted_datetime = dateformat.format(dt, settings.DATETIME_FORMAT) - return mark_safe('%s' % (css_class, formatted_datetime)) + return mark_safe( + '%s' % (css_class, formatted_datetime) + ) @register.filter @@ -71,7 +73,9 @@ def thumbnail(file, size='104x104'): # Check if the image is newer than the requested thumbnail, and if so, # remove the thumbnail so that it will be regenerated. - if os.path.exists(thumb_fullpath) and os.path.getmtime(file.path) > os.path.getmtime(thumb_fullpath): + if os.path.exists(thumb_fullpath) and os.path.getmtime( + file.path + ) > os.path.getmtime(thumb_fullpath): os.unlink(thumb_fullpath) # Create the thumbnail if it does not already exist. @@ -91,7 +95,11 @@ def thumbnail(file, size='104x104'): @register.filter(is_safe=True) @stringfilter def markdown(value): - return mark_safe(markdown2.markdown(value, extras=['break-on-newline']).replace('\\', '')) + return mark_safe( + markdown2.markdown(value, extras=['break-on-newline']).replace( + '\\', '' + ) + ) @register.filter(is_safe=True, needs_autoescape=True) @@ -102,11 +110,8 @@ def urlize(value, trim_url_limit=None, autoescape=None): value, trim_url_limit=trim_url_limit, nofollow=True, - autoescape=autoescape - ).replace( - ' 31 else 'human' + def random_word(length): - return ''.join(random.choice(string.ascii_lowercase) for i in range(length)) + return ''.join( + random.choice(string.ascii_lowercase) for i in range(length) + ) ## Heartbeat command. Intended to be run either as a command ## (see core.management.commands.heartbeat) or as a daemon thread. + def heartbeat(): # Do all sorts of things that # a) are due at this point in time. @@ -74,38 +83,59 @@ def heartbeat(): users = { 'total_count': User.objects.count(), 'verified_count': User.objects.filter(is_active=True).count(), - 'last30_count': User.objects.filter(last_login__gte=datetime.now()-timedelta(days=30)).count(), - 'last365_count': User.objects.filter(last_login__gte=datetime.now()-timedelta(days=365)).count(), + 'last30_count': User.objects.filter( + last_login__gte=datetime.now() - timedelta(days=30) + ).count(), + 'last365_count': User.objects.filter( + last_login__gte=datetime.now() - timedelta(days=365) + ).count(), } event_register('user_review', category='statistics', event=users) ## Push notifications tools + def push_server_post(action, payload): if not settings.FEATURES['push_notifications']: return False - header = {"Content-Type": "application/json; charset=utf-8", - "Authorization": "Basic %s" % settings.GCM_REST_API_KEY } - req = requests.post("https://onesignal.com/api/v1/%s" % action, headers=header, data=json.dumps(payload)) + header = { + "Content-Type": "application/json; charset=utf-8", + "Authorization": "Basic %s" % settings.GCM_REST_API_KEY, + } + req = requests.post( + "https://onesignal.com/api/v1/%s" % action, + headers=header, + data=json.dumps(payload), + ) return req + def push_server_get(action, payload): if not settings.FEATURES['push_notifications']: return False - header = {"Content-Type": "application/json; charset=utf-8", - "Authorization": "Basic %s" % settings.GCM_REST_API_KEY } - req = requests.get("https://onesignal.com/api/v1/%s" % action, headers=header, params=payload) + header = { + "Content-Type": "application/json; charset=utf-8", + "Authorization": "Basic %s" % settings.GCM_REST_API_KEY, + } + req = requests.get( + "https://onesignal.com/api/v1/%s" % action, + headers=header, + params=payload, + ) return req + def push_send_notification(messages, segments, filters=None, buttons=None): - payload = {"app_id": settings.GCM_APP_ID, - "included_segments": segments, - "contents": messages} + payload = { + "app_id": settings.GCM_APP_ID, + "included_segments": segments, + "contents": messages, + } # Example buttons: # @@ -123,14 +153,20 @@ def push_send_notification(messages, segments, filters=None, buttons=None): event_register('push_notification_sent', event=payload) return push_server_post('notifications', payload) + def push_send_notification_to_all_users(message, filters=None, buttons=None): # TODO: This needs to be updated to support i18n the way # push_send_notification_to_polity_users does. # messages = {"en": message, "is": message} - return push_send_notification(messages, ["All"], filters=filters, buttons=buttons) + return push_send_notification( + messages, ["All"], filters=filters, buttons=buttons + ) -def push_send_notification_to_polity_users(polity, message, msgargs=(), buttons=None): + +def push_send_notification_to_polity_users( + polity, message, msgargs=(), buttons=None +): # NOTE: Because it's hard to control user's language code as # the push service understands it, we are instead using # a tag named 'lang' to store the user's language preference. @@ -140,18 +176,26 @@ def push_send_notification_to_polity_users(polity, message, msgargs=(), buttons= # old_lang = translations.get_language() - for lang in ["is", "en"]: # TODO: This should not be hard-coded. + for lang in ["is", "en"]: # TODO: This should not be hard-coded. translation.activate(lang) messages = {"en": _(message) % msgargs} polityfilters = [ {"field": "tag", "key": "lang", "relation": "=", "value": lang}, {"operator": "and"}, - {"field": "tag", "key": "polity%d" % polity, "relation": "=", "value": "true"} + { + "field": "tag", + "key": "polity%d" % polity, + "relation": "=", + "value": "true", + }, ] - return push_send_notification(messages, ["All"], polityfilters, buttons) + return push_send_notification( + messages, ["All"], polityfilters, buttons + ) translation.activate(old_lang) + def push_get_all_users(): res = push_server_get('players', {"app_id": settings.GCM_APP_ID}) if res.status_code == 200: diff --git a/core/views.py b/core/views.py index be25e587..c8e98981 100644 --- a/core/views.py +++ b/core/views.py @@ -14,6 +14,7 @@ import hashlib import urllib from urllib.parse import parse_qs + # SSO done from django.contrib.auth import logout @@ -71,6 +72,7 @@ from registration.backends.default.views import ActivationView from registration.backends.default.views import RegistrationView from registration import signals as registration_signals + # END @@ -96,77 +98,90 @@ def home(request): # polity listing. return HttpResponseRedirect(reverse('polities')) + def manifest(request): manifest = { - "name": "%s" % (settings.INSTANCE_NAME), - "short_name": "%s" % (settings.INSTANCE_NAME), - "icons": [ - { - "src": "/static/img/logo-32.png", - "sizes": "32x32", - "type": "image/png" - }, - { - "src": "/static/img/logo-100.png", - "sizes": "100x100", - "type": "image/png" - }, - { - "src": "/static/img/logo-101.png", - "sizes": "101x101", - "type": "image/png" + "name": "%s" % (settings.INSTANCE_NAME), + "short_name": "%s" % (settings.INSTANCE_NAME), + "icons": [ + { + "src": "/static/img/logo-32.png", + "sizes": "32x32", + "type": "image/png", + }, + { + "src": "/static/img/logo-100.png", + "sizes": "100x100", + "type": "image/png", + }, + { + "src": "/static/img/logo-101.png", + "sizes": "101x101", + "type": "image/png", + }, + { + "src": "/static/img/logo-192.png", + "sizes": "192x192", + "type": "image/png", + }, + { + "src": "/static/img/logo-256.png", + "sizes": "256x256", + "type": "image/png", + }, + ], + "start_url": "/", + "background_color": "#ffffff", + "theme_color": "#e9e9e9", + "display": "standalone", + "serviceworker": { + "src": "/service-worker.js?ts=%s" % (settings.WASA2IL_HASH), + "scope": "/", + "use_cache": False, }, - { - "src": "/static/img/logo-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/static/img/logo-256.png", - "sizes": "256x256", - "type": "image/png" - }, - ], - "start_url": "/", - "background_color": "#ffffff", - "theme_color": "#e9e9e9", - "display": "standalone", - "serviceworker": { - "src": "/service-worker.js?ts=%s" % (settings.WASA2IL_HASH), - "scope": "/", - "use_cache": False - }, - "gcm_sender_id": "%d" % (settings.GCM_SENDER_ID), + "gcm_sender_id": "%d" % (settings.GCM_SENDER_ID), } return JsonResponse(manifest) + def help(request, page): - ctx = { - 'language_code': settings.LANGUAGE_CODE - } - for locale in [settings.LANGUAGE_CODE, "is"]: # Icelandic fallback - filename = "help/%s/%s.html" % (locale, page) - if os.path.isfile(os.path.join(os.path.dirname(__file__), '..', 'wasa2il/templates', filename)): - return render(request, filename, ctx) + ctx = {'language_code': settings.LANGUAGE_CODE} + for locale in [settings.LANGUAGE_CODE, "is"]: # Icelandic fallback + filename = "help/%s/%s.html" % (locale, page) + if os.path.isfile( + os.path.join( + os.path.dirname(__file__), '..', 'wasa2il/templates', filename + ) + ): + return render(request, filename, ctx) raise Http404 + @user_passes_test(lambda u: u.is_superuser) def view_admintools(request): push_form = PushNotificationForm() users = { 'total_count': User.objects.count(), 'verified_count': User.objects.filter(is_active=True).count(), - 'last30_count': User.objects.filter(last_login__gte=datetime.now()-timedelta(days=30)).count(), - 'last365_count': User.objects.filter(last_login__gte=datetime.now()-timedelta(days=365)).count(), + 'last30_count': User.objects.filter( + last_login__gte=datetime.now() - timedelta(days=30) + ).count(), + 'last365_count': User.objects.filter( + last_login__gte=datetime.now() - timedelta(days=365) + ).count(), } - return render(request, 'admintools.html', {'push_form': push_form, 'users': users}) + return render( + request, 'admintools.html', {'push_form': push_form, 'users': users} + ) + @user_passes_test(lambda u: u.is_superuser) def view_admintools_push(request): push_info = push_get_all_users() return render(request, 'admintools_push.html', {'push_users': push_info}) + @never_cache @login_required def profile(request, username=None): @@ -185,17 +200,13 @@ def profile(request, username=None): # Get running elections in which the user is currently a candidate current_elections = Election.objects.filter( - candidate__user=profile_user, - deadline_votes__gte=timezone.now() + candidate__user=profile_user, deadline_votes__gte=timezone.now() ) - candidacies = Candidate.objects.select_related( - 'result_row', - 'election' - ).filter( - user=profile_user - ).order_by( - '-election__deadline_votes' + candidacies = ( + Candidate.objects.select_related('result_row', 'election') + .filter(user=profile_user) + .order_by('-election__deadline_votes') ) ctx = { @@ -227,7 +238,7 @@ def view_settings(request): con = EmailConfirmation( user=request.user, action='email_change', - data=form.cleaned_data['email'] + data=form.cleaned_data['email'], ) con.save() con.send(request) @@ -247,7 +258,7 @@ def new_full_name(extension): # Filename relative to the uploads directory. filename = os.path.join( UserProfile.picture.field.upload_to, - 'userimg_%s.%s' % (random_word(12), extension) + 'userimg_%s.%s' % (random_word(12), extension), ) # Full path of the image on disk. @@ -275,7 +286,12 @@ def new_full_name(extension): # Cleanup time! # First, find picture files used by any profile. - db_pictures = [up['picture'] for up in UserProfile.objects.all().values('picture').distinct()] + db_pictures = [ + up['picture'] + for up in UserProfile.objects.all() + .values('picture') + .distinct() + ] # Paths of profile pictures are denoted relative to the # settings.MEDIA_ROOT directory. The "upload_to" parameter @@ -290,7 +306,9 @@ def new_full_name(extension): # List the files that are actually in the profile picture # directory and delete them if they are no longer in use. - items = os.listdir(os.path.join(settings.MEDIA_ROOT, upload_to)) + items = os.listdir( + os.path.join(settings.MEDIA_ROOT, upload_to) + ) for item in items: # Let's not delete the default image. That would be silly. @@ -298,7 +316,9 @@ def new_full_name(extension): continue # We'll use full disk paths for file operations. - item_fullpath = os.path.join(settings.MEDIA_ROOT, upload_to, item) + item_fullpath = os.path.join( + settings.MEDIA_ROOT, upload_to, item + ) if os.path.isdir(item_fullpath): # If this is a directory, we are slightly more shy of @@ -306,7 +326,11 @@ def new_full_name(extension): # if it's a thumbnail directory (ending with # "-thumbnail"). If it's some random directory of # unknown origin, we'll leave it alone. - if item[-10:] == '-thumbnail' and os.path.join(upload_to, item[:-10]) not in db_pictures: + if ( + item[-10:] == '-thumbnail' + and os.path.join(upload_to, item[:-10]) + not in db_pictures + ): shutil.rmtree(item_fullpath) elif os.path.isfile(item_fullpath): # If this is a file, and it's not being used in a user @@ -314,7 +338,6 @@ def new_full_name(extension): if os.path.join(upload_to, item) not in db_pictures: os.unlink(item_fullpath) - if settings.ICEPIRATE['url']: # The request.user object doesn't yet reflect recently made # changes, so we need to ask the database explicitly. @@ -330,7 +353,9 @@ def new_full_name(extension): return redirect(reverse('profile')) else: - form = UserProfileForm(initial={'email': request.user.email}, instance=profile) + form = UserProfileForm( + initial={'email': request.user.email}, instance=profile + ) ctx = { 'form': form, @@ -341,11 +366,11 @@ def new_full_name(extension): @login_required def personal_data(request): - terms = TermsAndConditions.objects.filter( - userterms__user=request.user - ).order_by( - '-userterms__date_accepted' - ).first() + terms = ( + TermsAndConditions.objects.filter(userterms__user=request.user) + .order_by('-userterms__date_accepted') + .first() + ) ctx = { 'terms': terms, @@ -371,8 +396,10 @@ def cd(newdir, cleanup=lambda: True): @contextlib.contextmanager def tempdir(): dirpath = tempfile.mkdtemp() + def cleanup(): shutil.rmtree(dirpath) + with cd(dirpath, cleanup): yield dirpath @@ -383,7 +410,9 @@ def jsonize_issue(issue): 'name': issue.name, 'polity': issue.polity.slug, 'polity_name': issue.polity.name, - 'issue_num': ('%d/%d' % (issue.issue_num, issue.issue_year)) if issue.issue_num else None, + 'issue_num': ('%d/%d' % (issue.issue_num, issue.issue_year)) + if issue.issue_num + else None, 'state': issue.issue_state(), 'created': issue.created.strftime(dt_format), 'ended': issue.deadline_votes.strftime(dt_format), @@ -403,10 +432,7 @@ def write_json(filename, data): with open(filename, 'w') as f: output = json.dumps( - data, - indent=4, - sort_keys=True, - ensure_ascii=False + data, indent=4, sort_keys=True, ensure_ascii=False ) f.write(output) @@ -417,13 +443,17 @@ def write_json(filename, data): # https://stackoverflow.com/questions/1855095/how-to-create-a-zip-archive-of-a-directory def make_zipfile(output_filename, source_dir): relroot = os.path.abspath(os.path.join(source_dir, os.pardir)) - with zipfile.ZipFile(output_filename, 'w', zipfile.ZIP_DEFLATED) as zip: + with zipfile.ZipFile( + output_filename, 'w', zipfile.ZIP_DEFLATED + ) as zip: for root, dirs, files in os.walk(source_dir): zip.write(root, os.path.relpath(root, relroot)) for file in files: filename = os.path.join(root, file) if os.path.isfile(filename): - arcname = os.path.join(os.path.relpath(root, relroot), file) + arcname = os.path.join( + os.path.relpath(root, relroot), file + ) zip.write(filename, arcname) # Short-hands. @@ -448,7 +478,7 @@ def make_zipfile(output_filename, source_dir): user.username, # Not the same datetime format as used in the files themselves, # because this needs to be filesystem-compatible. - timezone.now().strftime('%Y-%m-%d.%H-%M-%S') + timezone.now().strftime('%Y-%m-%d.%H-%M-%S'), ) os.mkdir(package_name) os.chdir(package_name) @@ -456,35 +486,41 @@ def make_zipfile(output_filename, source_dir): # We are now working within the temporary package directory. # Compile information about user's participation in elections. - elections = Election.objects.select_related( - 'polity' - ).filter( - candidate__user_id=user.id - ).order_by( - 'deadline_candidacy' + elections = ( + Election.objects.select_related('polity') + .filter(candidate__user_id=user.id) + .order_by('deadline_candidacy') ) election_export = [] for election in elections: try: if election.results_are_ordered: - user_place = election.result.rows.get(candidate__user_id=user.id).order + user_place = election.result.rows.get( + candidate__user_id=user.id + ).order else: - if election.result.rows.filter(candidate__user_id=user.id).exists(): + if election.result.rows.filter( + candidate__user_id=user.id + ).exists(): user_place = 'selected' else: user_place = 'not-selected' except ElectionResult.DoesNotExist: user_place = 'not-yet-determined' - election_export.append({ - 'name': election.name, - 'started': election.deadline_candidacy.strftime(dt_format), - 'ended': election.deadline_votes.strftime(dt_format), - 'polity_name': election.polity.name, - 'result_type': 'ordered' if election.results_are_ordered else 'not-ordered', - 'user_place': user_place, - }) + election_export.append( + { + 'name': election.name, + 'started': election.deadline_candidacy.strftime(dt_format), + 'ended': election.deadline_votes.strftime(dt_format), + 'polity_name': election.polity.name, + 'result_type': 'ordered' + if election.results_are_ordered + else 'not-ordered', + 'user_place': user_place, + } + ) write_json('elections.json', election_export) # Compile user information from Wasa2il itself. @@ -497,7 +533,12 @@ def make_zipfile(output_filename, source_dir): 'verified_ssn': profile.verified_ssn, 'verified_name': profile.verified_name, 'verified_timing': profile.verified_timing.strftime(dt_format), - 'polities': dict([(p.slug, p.name) for p in user.polities.exclude(is_front_polity=True)]), + 'polities': dict( + [ + (p.slug, p.name) + for p in user.polities.exclude(is_front_polity=True) + ] + ), 'date_joined': user.date_joined.strftime(dt_format), 'bio': profile.bio, } @@ -508,36 +549,46 @@ def make_zipfile(output_filename, source_dir): # the user, but is the aggregate result of whatever was before, plus # the work of the user. document_content_export = [] - for dc in user.documentcontent_set.select_related('document__polity').order_by('created'): + for dc in user.documentcontent_set.select_related( + 'document__polity' + ).order_by('created'): try: - issue = Issue.objects.select_related('polity').get(documentcontent=dc) + issue = Issue.objects.select_related('polity').get( + documentcontent=dc + ) issue_data = jsonize_issue(issue) except Issue.DoesNotExist: issue_data = None - document_content_export.append({ - 'name': dc.name, - 'order': dc.order, - 'author_comment': dc.comments, - 'status': dc.status, - 'created': dc.created.strftime(dt_format), - 'polity': dc.document.polity.slug, - 'polity_name': dc.document.polity.name, - 'issue': issue_data, - 'text': dc.text, - }) + document_content_export.append( + { + 'name': dc.name, + 'order': dc.order, + 'author_comment': dc.comments, + 'status': dc.status, + 'created': dc.created.strftime(dt_format), + 'polity': dc.document.polity.slug, + 'polity_name': dc.document.polity.name, + 'issue': issue_data, + 'text': dc.text, + } + ) write_json('document_contents.json', document_content_export) # Compile comments made by user. comment_export = [] - for comment in user.comment_created_by.select_related('issue__polity').order_by('created'): - comment_export.append({ - 'issue': jsonize_issue(comment.issue), - 'created': comment.created.strftime(dt_format), - 'text': comment.comment, - }) + for comment in user.comment_created_by.select_related( + 'issue__polity' + ).order_by('created'): + comment_export.append( + { + 'issue': jsonize_issue(comment.issue), + 'created': comment.created.strftime(dt_format), + 'text': comment.comment, + } + ) write_json('comments.json', comment_export) # Include the user's picture, if available. @@ -567,7 +618,9 @@ def make_zipfile(output_filename, source_dir): # Push the content to the user as a zip file for download. response = HttpResponse(package_data, content_type='application/zip') - response['Content-Disposition'] = 'attachment; filename=%s.zip' % package_name + response['Content-Disposition'] = ( + 'attachment; filename=%s.zip' % package_name + ) return response @@ -593,37 +646,36 @@ def register(self, form): ) registration_signals.user_registered.send( - sender=self.__class__, - user=new_user, - request=self.request + sender=self.__class__, user=new_user, request=self.request ) return new_user class Wasa2ilActivationView(ActivationView): - def activate(self, *args, **kwargs): activation_key = kwargs.get('activation_key', '') site = get_current_site(self.request) user, activated = self.registration_profile.objects.activate_user( - activation_key, - site + activation_key, site ) if activated: registration_signals.user_activated.send( - sender=self.__class__, - user=user, - request=self.request + sender=self.__class__, user=user, request=self.request ) - login(self.request, user, 'django.contrib.auth.backends.ModelBackend') + login( + self.request, user, 'django.contrib.auth.backends.ModelBackend' + ) return user def get_success_url(self, user): - return '%s?returnTo=%s' % (reverse('tc_accept_page'), reverse('login_or_saml_redirect')) + return '%s?returnTo=%s' % ( + reverse('tc_accept_page'), + reverse('login_or_saml_redirect'), + ) @csrf_exempt @@ -650,7 +702,11 @@ def verify(request): # this, we will grab the token if the user is not logged in (because # of the SameSite restriction), place it in an HTML form and # automatically submit it to the same page again. - return render(request, 'registration/verification_autopost.html', {'token': token}) + return render( + request, + 'registration/verification_autopost.html', + {'token': token}, + ) # XML is received as a base64-encoded string. input_xml = b64decode(token) @@ -667,7 +723,9 @@ def verify(request): 'ssn': auth['UserSSN'], 'name': auth['Name'].encode('utf8'), } - return render(request, 'registration/verification_invalid_entity.html', ctx) + return render( + request, 'registration/verification_invalid_entity.html', ctx + ) # Make sure that user has reached the minimum required age, if applicable. if hasattr(settings, 'AGE_LIMIT') and settings.AGE_LIMIT > 0: @@ -678,10 +736,16 @@ def verify(request): 'age': age, 'age_limit': settings.AGE_LIMIT, } - return render(request, 'registration/verification_age_limit.html', ctx) + return render( + request, 'registration/verification_age_limit.html', ctx + ) if UserProfile.objects.filter(verified_ssn=auth['UserSSN']).exists(): - taken_user = UserProfile.objects.select_related('user').get(verified_ssn=auth['UserSSN']).user + taken_user = ( + UserProfile.objects.select_related('user') + .get(verified_ssn=auth['UserSSN']) + .user + ) ctx = { 'taken_user': taken_user, } @@ -698,7 +762,9 @@ def verify(request): profile.save() event_register('user_verified', user=request.user) - user_verified.send(sender=request.user.__class__, user=request.user, request=request) + user_verified.send( + sender=request.user.__class__, user=request.user, request=request + ) return HttpResponseRedirect('/') @@ -741,7 +807,9 @@ def sso(request): except: return HttpResponseBadRequest('Malformed payload.') - our_signature = hmac.new(key, payload, digestmod=hashlib.sha256).hexdigest() + our_signature = hmac.new( + key, payload, digestmod=hashlib.sha256 + ).hexdigest() if our_signature != their_signature: return HttpResponseBadRequest('Malformed payload.') @@ -760,11 +828,19 @@ def sso(request): 'name': name, } - out_payload = base64.b64encode(urllib.parse.urlencode(outbound).encode('utf-8')) - out_signature = hmac.new(key, out_payload, digestmod=hashlib.sha256).hexdigest() - out_query = urllib.parse.urlencode({'sso': out_payload, 'sig' : out_signature}) + out_payload = base64.b64encode( + urllib.parse.urlencode(outbound).encode('utf-8') + ) + out_signature = hmac.new( + key, out_payload, digestmod=hashlib.sha256 + ).hexdigest() + out_query = urllib.parse.urlencode( + {'sso': out_payload, 'sig': out_signature} + ) - event_register('sso_signin', event={'client': 'discourse'}, user=request.user) + event_register( + 'sso_signin', event={'client': 'discourse'}, user=request.user + ) return HttpResponseRedirect('%s?%s' % (return_url, out_query)) diff --git a/datetimewidget/widgets.py b/datetimewidget/widgets.py index 58c90b33..07bdbbd5 100644 --- a/datetimewidget/widgets.py +++ b/datetimewidget/widgets.py @@ -1,4 +1,3 @@ - __author__ = 'Alfredo Saglimbeni' from datetime import datetime @@ -6,7 +5,12 @@ import uuid from django.forms import forms, widgets -from django.forms.widgets import MultiWidget, DateTimeInput, DateInput, TimeInput +from django.forms.widgets import ( + MultiWidget, + DateTimeInput, + DateInput, + TimeInput, +) from django.utils.formats import get_format, get_language from django.utils.safestring import mark_safe from django.utils.six import string_types @@ -14,31 +18,58 @@ try: from django.forms.widgets import to_current_timezone except ImportError: - to_current_timezone = lambda obj: obj # passthrough, no tz support + to_current_timezone = lambda obj: obj # passthrough, no tz support # This should be updated as more .po files are added to the datetime picker javascript code -supported_languages = set([ - 'ar', - 'bg', - 'ca', 'cs', - 'da', 'de', - 'ee', 'el', 'es','eu', - 'fi', 'fr', - 'he', 'hr', 'hu', - 'id', 'is', 'it', - 'ja', - 'ko', 'kr', - 'lt', 'lv', - 'ms', - 'nb', 'nl', 'no', - 'pl', 'pt-BR', 'pt', - 'ro', 'rs', 'rs-latin', 'ru', - 'sk', 'sl', 'sv', 'sw', - 'th', 'tr', - 'ua', 'uk', - 'zh-CN', 'zh-TW', - ]) +supported_languages = set( + [ + 'ar', + 'bg', + 'ca', + 'cs', + 'da', + 'de', + 'ee', + 'el', + 'es', + 'eu', + 'fi', + 'fr', + 'he', + 'hr', + 'hu', + 'id', + 'is', + 'it', + 'ja', + 'ko', + 'kr', + 'lt', + 'lv', + 'ms', + 'nb', + 'nl', + 'no', + 'pl', + 'pt-BR', + 'pt', + 'ro', + 'rs', + 'rs-latin', + 'ru', + 'sk', + 'sl', + 'sv', + 'sw', + 'th', + 'tr', + 'ua', + 'uk', + 'zh-CN', + 'zh-TW', + ] +) def get_supported_language(language_country_code): @@ -80,7 +111,9 @@ def get_supported_language(language_country_code): 'yyyy': '%Y', } -toPython_re = re.compile(r'\b(' + '|'.join(dateConversiontoPython.keys()) + r')\b') +toPython_re = re.compile( + r'\b(' + '|'.join(dateConversiontoPython.keys()) + r')\b' +) dateConversiontoJavascript = { @@ -92,10 +125,12 @@ def get_supported_language(language_country_code): '%Y': 'yyyy', '%y': 'yy', '%p': 'P', - '%S': 'ss' + '%S': 'ss', } -toJavascript_re = re.compile(r'(? $("#%(id)s").datetimepicker({%(options)s}).find('input').addClass("form-control"); - """ - } - -CLEAR_BTN_TEMPLATE = {2: """""", - 3: """"""} - - -quoted_options = set([ - 'format', - 'startDate', - 'endDate', - 'startView', - 'minView', - 'maxView', - 'todayBtn', - 'language', - 'pickerPosition', - 'viewSelect', - 'initialDate', - 'weekStart', - 'minuteStep' - 'daysOfWeekDisabled', - ]) + """, +} + +CLEAR_BTN_TEMPLATE = { + 2: """""", + 3: """""", +} + + +quoted_options = set( + [ + 'format', + 'startDate', + 'endDate', + 'startView', + 'minView', + 'maxView', + 'todayBtn', + 'language', + 'pickerPosition', + 'viewSelect', + 'initialDate', + 'weekStart', + 'minuteStep' 'daysOfWeekDisabled', + ] +) # to traslate boolean object to javascript -quoted_bool_options = set([ - 'autoclose', - 'todayHighlight', - 'showMeridian', - 'clearBtn', - ]) +quoted_bool_options = set( + [ + 'autoclose', + 'todayHighlight', + 'showMeridian', + 'clearBtn', + ] +) def quote(key, value): @@ -161,7 +201,7 @@ def quote(key, value): return "'%s'" % value if key in quoted_bool_options and isinstance(value, bool): - return {True:'true',False:'false'}[value] + return {True: 'true', False: 'false'}[value] return value @@ -171,9 +211,11 @@ class PickerWidgetMixin(object): format_name = None glyphicon = None - def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=None): + def __init__( + self, attrs=None, options=None, usel10n=None, bootstrap_version=None + ): - if bootstrap_version in [2,3]: + if bootstrap_version in [2, 3]: self.bootstrap_version = bootstrap_version else: # default 2 to mantain support to old implemetation of django-datetime-widget @@ -200,9 +242,8 @@ def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=Non # Convert Python format specifier to Javascript format specifier self.options['format'] = toJavascript_re.sub( - lambda x: dateConversiontoJavascript[x.group()], - self.format - ) + lambda x: dateConversiontoJavascript[x.group()], self.format + ) # Set the local language self.options['language'] = get_supported_language(get_language()) @@ -213,17 +254,18 @@ def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=Non # with a default, and convert it to a Python data format for later string parsing format = self.options['format'] self.format = toPython_re.sub( - lambda x: dateConversiontoPython[x.group()], - format - ) + lambda x: dateConversiontoPython[x.group()], format + ) super(PickerWidgetMixin, self).__init__(attrs, format=self.format) def render(self, name, value, attrs=None, renderer=None): final_attrs = self.build_attrs(attrs) - rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs) + rendered_widget = super(PickerWidgetMixin, self).render( + name, value, final_attrs + ) - #if not set, autoclose have to be true. + # if not set, autoclose have to be true. self.options.setdefault('autoclose', True) # Build javascript options out of python dictionary @@ -236,17 +278,21 @@ def render(self, name, value, attrs=None, renderer=None): # Use provided id or generate hex to avoid collisions in document id = final_attrs.get('id', uuid.uuid4().hex) - clearBtn = quote('clearBtn', self.options.get('clearBtn', 'true')) == 'true' + clearBtn = ( + quote('clearBtn', self.options.get('clearBtn', 'true')) == 'true' + ) return mark_safe( BOOTSTRAP_INPUT_TEMPLATE[self.bootstrap_version] - % dict( - id=id, - rendered_widget=rendered_widget, - clear_button=CLEAR_BTN_TEMPLATE[self.bootstrap_version] if clearBtn else "", - glyphicon=self.glyphicon, - options=js_options - ) + % dict( + id=id, + rendered_widget=rendered_widget, + clear_button=CLEAR_BTN_TEMPLATE[self.bootstrap_version] + if clearBtn + else "", + glyphicon=self.glyphicon, + options=js_options, + ) ) def _media(self): @@ -257,12 +303,7 @@ def _media(self): if language != 'en': js.append("js/locales/bootstrap-datetimepicker.%s.js" % language) - return widgets.Media( - css={ - 'all': ('css/datetimepicker.css',) - }, - js=js - ) + return widgets.Media(css={'all': ('css/datetimepicker.css',)}, js=js) media = property(_media) @@ -276,7 +317,9 @@ class DateTimeWidget(PickerWidgetMixin, DateTimeInput): format_name = 'DATETIME_INPUT_FORMATS' glyphicon = 'glyphicon-th' - def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=None): + def __init__( + self, attrs=None, options=None, usel10n=None, bootstrap_version=None + ): if options is None: options = {} @@ -284,7 +327,9 @@ def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=Non # Set the default options to show only the datepicker object options['format'] = options.get('format', 'dd/mm/yyyy hh:ii') - super(DateTimeWidget, self).__init__(attrs, options, usel10n, bootstrap_version) + super(DateTimeWidget, self).__init__( + attrs, options, usel10n, bootstrap_version + ) class DateWidget(PickerWidgetMixin, DateInput): @@ -296,7 +341,9 @@ class DateWidget(PickerWidgetMixin, DateInput): format_name = 'DATE_INPUT_FORMATS' glyphicon = 'glyphicon-calendar' - def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=None): + def __init__( + self, attrs=None, options=None, usel10n=None, bootstrap_version=None + ): if options is None: options = {} @@ -306,7 +353,9 @@ def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=Non options['minView'] = options.get('minView', 2) options['format'] = options.get('format', 'dd/mm/yyyy') - super(DateWidget, self).__init__(attrs, options, usel10n, bootstrap_version) + super(DateWidget, self).__init__( + attrs, options, usel10n, bootstrap_version + ) class TimeWidget(PickerWidgetMixin, TimeInput): @@ -318,7 +367,9 @@ class TimeWidget(PickerWidgetMixin, TimeInput): format_name = 'TIME_INPUT_FORMATS' glyphicon = 'glyphicon-time' - def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=None): + def __init__( + self, attrs=None, options=None, usel10n=None, bootstrap_version=None + ): if options is None: options = {} @@ -329,5 +380,6 @@ def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=Non options['maxView'] = options.get('maxView', 1) options['format'] = options.get('format', 'hh:ii') - super(TimeWidget, self).__init__(attrs, options, usel10n, bootstrap_version) - + super(TimeWidget, self).__init__( + attrs, options, usel10n, bootstrap_version + ) diff --git a/election/__init__.py b/election/__init__.py index ff3c16bd..81d28f60 100644 --- a/election/__init__.py +++ b/election/__init__.py @@ -1,4 +1,2 @@ - - def heartbeat(t): pass diff --git a/election/dataviews.py b/election/dataviews.py index 80921673..46b9dc98 100644 --- a/election/dataviews.py +++ b/election/dataviews.py @@ -34,9 +34,12 @@ def _ordered_candidates(user, all_candidates, candidates): return [] if user.is_authenticated: - randish = int(md5((repr(user) + str(user.id)).encode('utf-8')).hexdigest()[:8], 16) + randish = int( + md5((repr(user) + str(user.id)).encode('utf-8')).hexdigest()[:8], + 16, + ) else: - randish = random.randint(0, 0xffffff) + randish = random.randint(0, 0xFFFFFF) def _sname(u): try: @@ -58,8 +61,10 @@ def _sname(u): @jsonize def election_poll(request, **kwargs): - election = get_object_or_404(Election, - id=request.POST.get("election", request.GET.get("election", -1))) + election = get_object_or_404( + Election, + id=request.POST.get("election", request.GET.get("election", -1)), + ) user_can_vote = election.can_vote(request.user) all_candidates = election.get_candidates() @@ -67,31 +72,41 @@ def election_poll(request, **kwargs): ctx = { "logged_out": not request.user.is_authenticated, "election": { - "user_is_candidate": - (request.user in [x.user for x in election.candidate_set.all()]), + "user_is_candidate": ( + request.user in [x.user for x in election.candidate_set.all()] + ), "election_state": election.election_state(), "votes": election.get_vote_count(), "candidates": all_candidates, - "vote": {}}} + "vote": {}, + }, + } ctx["election"]["candidates"]["html"] = render_to_string( - "election/_election_candidate_list.html", { + "election/_election_candidate_list.html", + { "user_can_vote": user_can_vote, "election": election, "candidate_total": len(all_candidates), "candidates": _ordered_candidates( request.user, Candidate.objects.filter(election=election), - election.get_unchosen_candidates(request.user)), - "candidate_selected": False}) + election.get_unchosen_candidates(request.user), + ), + "candidate_selected": False, + }, + ) ctx["election"]["vote"]["html"] = render_to_string( - "election/_election_candidate_list.html", { + "election/_election_candidate_list.html", + { "user_can_vote": user_can_vote, "election": election, "candidate_total": len(all_candidates), "candidates": election.get_vote(request.user), - "candidate_selected": True}) + "candidate_selected": True, + }, + ) for k, v in kwargs.items(): ctx["election"][k] = v @@ -113,7 +128,9 @@ def election_candidacy(request): if val == 0: Candidate.objects.filter(user=request.user, election=election).delete() elif election.can_be_candidate(request.user): - cand, created = Candidate.objects.get_or_create(user=request.user, election=election) + cand, created = Candidate.objects.get_or_create( + user=request.user, election=election + ) return election_poll(request) @@ -126,7 +143,7 @@ def _record_votes(election, user, order): candidate = Candidate.objects.get(id=order[i]) ElectionVote( election=election, user=user, candidate=candidate, value=i - ).save() + ).save() @require_http_methods(["POST"]) @@ -161,13 +178,17 @@ def election_showclosed(request): ctx = {} polity_id = int(request.GET.get('polity_id', 0)) - showclosed = int(request.GET.get('showclosed', 0)) # 0 = False, 1 = True + showclosed = int(request.GET.get('showclosed', 0)) # 0 = False, 1 = True try: if polity_id: - elections = Election.objects.filter(Q(polity_id=polity_id) | Q(polity__parent_id=polity_id)) + elections = Election.objects.filter( + Q(polity_id=polity_id) | Q(polity__parent_id=polity_id) + ) else: - elections = Election.objects.order_by('polity__name', '-deadline_votes') + elections = Election.objects.order_by( + 'polity__name', '-deadline_votes' + ) if not showclosed: elections = elections.recent() @@ -184,25 +205,33 @@ def election_showclosed(request): } ctx['showclosed'] = showclosed - ctx['html'] = render_to_string('election/_elections_recent_table.html', html_ctx) + ctx['html'] = render_to_string( + 'election/_elections_recent_table.html', html_ctx + ) ctx['ok'] = True except Exception as e: - ctx['error'] = e.__str__() if settings.DEBUG else 'Error raised. Turn on DEBUG for details.' + ctx['error'] = ( + e.__str__() + if settings.DEBUG + else 'Error raised. Turn on DEBUG for details.' + ) return ctx -def election_stats_download(request, polity_id=None, election_id=None, filename=None): +def election_stats_download( + request, polity_id=None, election_id=None, filename=None +): election = get_object_or_404( Election, id=election_id, polity_id=polity_id, is_processed=True, - stats_publish_files=True + stats_publish_files=True, ) filetype = filename.split('.')[-1].lower() - assert(filetype in ('json', 'xlsx', 'ods', 'html')) + assert filetype in ('json', 'xlsx', 'ods', 'html') response = HttpResponse( election.get_formatted_stats(filetype, user=request.user), @@ -210,8 +239,9 @@ def election_stats_download(request, polity_id=None, election_id=None, filename= 'json': 'application/json; charset=utf-8', 'ods': 'application/vnd.oasis.opendocument.spreadsheet', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'html': 'text/html; charset=utf-8' - }.get(filetype, 'application/octet-stream')) + 'html': 'text/html; charset=utf-8', + }.get(filetype, 'application/octet-stream'), + ) response['Content-Disposition'] = 'attachment; filename="%s"' % filename return response @@ -220,23 +250,37 @@ def election_stats_download(request, polity_id=None, election_id=None, filename= @login_required def election_candidates_details(request, polity_id, election_id): try: - election = Election.objects.get(id=election_id, polity_id=polity_id, polity__officers=request.user) + election = Election.objects.get( + id=election_id, polity_id=polity_id, polity__officers=request.user + ) except Election.DoesNotExist: raise PermissionDenied() - candidates = election.candidate_set.select_related('user__userprofile').order_by('user__userprofile__verified_name') + candidates = election.candidate_set.select_related( + 'user__userprofile' + ).order_by('user__userprofile__verified_name') candidate_list = ['"SSN","Name from registry","Email address","Username"'] for user in [c.user for c in candidates]: - candidate_list.append(','.join(['"%s"' % item for item in [ - user.userprofile.verified_ssn, - user.userprofile.verified_name, - user.email, - user.username, - ]])) + candidate_list.append( + ','.join( + [ + '"%s"' % item + for item in [ + user.userprofile.verified_ssn, + user.userprofile.verified_name, + user.email, + user.username, + ] + ] + ) + ) filename = u'Candidates - %s.csv' % election.name - response = HttpResponse('\n'.join(candidate_list), content_type='application/csv; charset=utf-8') + response = HttpResponse( + '\n'.join(candidate_list), + content_type='application/csv; charset=utf-8', + ) response['Content-Disposition'] = 'attachment; filename="%s"' % filename return response diff --git a/election/management/commands/processelections.py b/election/management/commands/processelections.py index 207be5e2..f422ac19 100644 --- a/election/management/commands/processelections.py +++ b/election/management/commands/processelections.py @@ -8,7 +8,6 @@ class Command(BaseCommand): - def add_arguments(self, parser): parser.add_argument('election_id', nargs='*', type=int) @@ -17,12 +16,18 @@ def handle(self, *args, **options): try: if not settings.BALLOT_SAVEFILE_FORMAT: print() - print('WARNING! This command will permanently delete EVERY ballot of EVERY election!') - print('Only do this if you know what you\'re doing. You have been warned.') + print( + 'WARNING! This command will permanently delete EVERY ballot of EVERY election!' + ) + print( + 'Only do this if you know what you\'re doing. You have been warned.' + ) print() response = '' while response != 'yes' and response != 'no': - response = raw_input('Are you REALLY certain that you wish to proceed? (yes/no) ').lower() + response = raw_input( + 'Are you REALLY certain that you wish to proceed? (yes/no) ' + ).lower() if response == 'no': print() @@ -33,9 +38,13 @@ def handle(self, *args, **options): elections = Election.objects.filter(is_processed=False) for election in elections: - if (options.get('election_id') and - election.id not in options['election_id']): - stdout.write('Skipping election %s (%s)\n' % (election, election.id)) + if ( + options.get('election_id') + and election.id not in options['election_id'] + ): + stdout.write( + 'Skipping election %s (%s)\n' % (election, election.id) + ) continue stdout.write('Processing election %s...' % election) @@ -49,10 +58,10 @@ def handle(self, *args, **options): stdout.write(' still in progress\n') except: import traceback + stdout.write(' failed for unknown reasons\n') traceback.print_exc() except KeyboardInterrupt: print quit() - diff --git a/election/migrations/0001_initial.py b/election/migrations/0001_initial.py index 75ffa4e4..c1285641 100644 --- a/election/migrations/0001_initial.py +++ b/election/migrations/0001_initial.py @@ -20,31 +20,159 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Candidate', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ], ), migrations.CreateModel( name='Election', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='Name')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'name', + models.CharField(max_length=128, verbose_name='Name'), + ), ('slug', models.SlugField(blank=True, max_length=128)), - ('voting_system', models.CharField(choices=[(b'condorcet', 'Condorcet'), (b'schulze', 'Schulze, ordered list'), (b'schulze_old', 'Schulze, ordered list (old)'), (b'schulze_new', 'Schulze, ordered list (new)'), (b'schulze_both', 'Schulze, ordered list (both)'), (b'stcom', 'Steering Committee Election'), (b'stv1', 'STV, single winner'), (b'stv2', 'STV, two winners'), (b'stv3', 'STV, three winners'), (b'stv4', 'STV, four winners'), (b'stv5', 'STV, five winners'), (b'stv8', 'STV, eight winners'), (b'stv10', 'STV, ten winners'), (b'stonethor', 'STV partition with Schulze ranking')], max_length=30, verbose_name='Voting system')), - ('results_are_ordered', models.BooleanField(default=True, verbose_name='Results are ordered')), - ('results_limit', models.IntegerField(blank=True, null=True, verbose_name='How many candidates will be publicly listed in the results of an election')), - ('deadline_candidacy', models.DateTimeField(verbose_name='Deadline for candidacies')), - ('starttime_votes', models.DateTimeField(blank=True, null=True, verbose_name='Election begins')), - ('deadline_votes', models.DateTimeField(verbose_name='Election ends')), - ('deadline_joined_org', models.DateTimeField(blank=True, null=True, verbose_name='Membership deadline')), + ( + 'voting_system', + models.CharField( + choices=[ + (b'condorcet', 'Condorcet'), + (b'schulze', 'Schulze, ordered list'), + (b'schulze_old', 'Schulze, ordered list (old)'), + (b'schulze_new', 'Schulze, ordered list (new)'), + (b'schulze_both', 'Schulze, ordered list (both)'), + (b'stcom', 'Steering Committee Election'), + (b'stv1', 'STV, single winner'), + (b'stv2', 'STV, two winners'), + (b'stv3', 'STV, three winners'), + (b'stv4', 'STV, four winners'), + (b'stv5', 'STV, five winners'), + (b'stv8', 'STV, eight winners'), + (b'stv10', 'STV, ten winners'), + ( + b'stonethor', + 'STV partition with Schulze ranking', + ), + ], + max_length=30, + verbose_name='Voting system', + ), + ), + ( + 'results_are_ordered', + models.BooleanField( + default=True, verbose_name='Results are ordered' + ), + ), + ( + 'results_limit', + models.IntegerField( + blank=True, + null=True, + verbose_name='How many candidates will be publicly listed in the results of an election', + ), + ), + ( + 'deadline_candidacy', + models.DateTimeField( + verbose_name='Deadline for candidacies' + ), + ), + ( + 'starttime_votes', + models.DateTimeField( + blank=True, null=True, verbose_name='Election begins' + ), + ), + ( + 'deadline_votes', + models.DateTimeField(verbose_name='Election ends'), + ), + ( + 'deadline_joined_org', + models.DateTimeField( + blank=True, + null=True, + verbose_name='Membership deadline', + ), + ), ('is_processed', models.BooleanField(default=False)), - ('instructions', models.TextField(blank=True, null=True, verbose_name='Instructions')), - ('stats', models.TextField(blank=True, null=True, verbose_name='Statistics as JSON')), - ('stats_publish_ballots_basic', models.BooleanField(default=False, verbose_name='Publish basic ballot statistics')), - ('stats_publish_ballots_per_candidate', models.BooleanField(default=False, verbose_name='Publish ballot statistics for each candidate')), - ('stats_publish_files', models.BooleanField(default=False, verbose_name='Publish advanced statistics (downloadable)')), - ('candidate_polities', models.ManyToManyField(blank=True, related_name='remote_election_candidates', to='polity.Polity', verbose_name='Candidate polities')), - ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')), - ('voting_polities', models.ManyToManyField(blank=True, related_name='remote_election_votes', to='polity.Polity', verbose_name='Voting polities')), + ( + 'instructions', + models.TextField( + blank=True, null=True, verbose_name='Instructions' + ), + ), + ( + 'stats', + models.TextField( + blank=True, + null=True, + verbose_name='Statistics as JSON', + ), + ), + ( + 'stats_publish_ballots_basic', + models.BooleanField( + default=False, + verbose_name='Publish basic ballot statistics', + ), + ), + ( + 'stats_publish_ballots_per_candidate', + models.BooleanField( + default=False, + verbose_name='Publish ballot statistics for each candidate', + ), + ), + ( + 'stats_publish_files', + models.BooleanField( + default=False, + verbose_name='Publish advanced statistics (downloadable)', + ), + ), + ( + 'candidate_polities', + models.ManyToManyField( + blank=True, + related_name='remote_election_candidates', + to='polity.Polity', + verbose_name='Candidate polities', + ), + ), + ( + 'polity', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='polity.Polity', + ), + ), + ( + 'voting_polities', + models.ManyToManyField( + blank=True, + related_name='remote_election_votes', + to='polity.Polity', + verbose_name='Voting polities', + ), + ), ], options={ 'ordering': ['-deadline_votes'], @@ -53,18 +181,54 @@ class Migration(migrations.Migration): migrations.CreateModel( name='ElectionResult', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('vote_count', models.IntegerField()), - ('election', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='result', to='election.Election')), + ( + 'election', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='result', + to='election.Election', + ), + ), ], ), migrations.CreateModel( name='ElectionResultRow', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('order', models.IntegerField()), - ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='election.Candidate')), - ('election_result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rows', to='election.ElectionResult')), + ( + 'candidate', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='election.Candidate', + ), + ), + ( + 'election_result', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='rows', + to='election.ElectionResult', + ), + ), ], options={ 'ordering': ['order'], @@ -73,25 +237,62 @@ class Migration(migrations.Migration): migrations.CreateModel( name='ElectionVote', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('value', models.IntegerField()), - ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='election.Candidate')), - ('election', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='election.Election')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + 'candidate', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='election.Candidate', + ), + ), + ( + 'election', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='election.Election', + ), + ), + ( + 'user', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AddField( model_name='candidate', name='election', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='election.Election'), + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='election.Election', + ), ), migrations.AddField( model_name='candidate', name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterUniqueTogether( name='electionvote', - unique_together=set([('election', 'user', 'candidate'), ('election', 'user', 'value')]), + unique_together=set( + [ + ('election', 'user', 'candidate'), + ('election', 'user', 'value'), + ] + ), ), ] diff --git a/election/migrations/0002_auto_20190822_1451.py b/election/migrations/0002_auto_20190822_1451.py index 9cec8746..3560133e 100644 --- a/election/migrations/0002_auto_20190822_1451.py +++ b/election/migrations/0002_auto_20190822_1451.py @@ -16,6 +16,10 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='electionresultrow', name='candidate', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='result_row', to='election.Candidate'), + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='result_row', + to='election.Candidate', + ), ), ] diff --git a/election/migrations/0003_auto_20190822_2006.py b/election/migrations/0003_auto_20190822_2006.py index bc323383..e844cec1 100644 --- a/election/migrations/0003_auto_20190822_2006.py +++ b/election/migrations/0003_auto_20190822_2006.py @@ -13,6 +13,25 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='election', name='voting_system', - field=models.CharField(choices=[('condorcet', 'Condorcet'), ('schulze', 'Schulze, ordered list'), ('schulze_old', 'Schulze, ordered list (old)'), ('schulze_new', 'Schulze, ordered list (new)'), ('schulze_both', 'Schulze, ordered list (both)'), ('stcom', 'Steering Committee Election'), ('stv1', 'STV, single winner'), ('stv2', 'STV, two winners'), ('stv3', 'STV, three winners'), ('stv4', 'STV, four winners'), ('stv5', 'STV, five winners'), ('stv8', 'STV, eight winners'), ('stv10', 'STV, ten winners'), ('stonethor', 'STV partition with Schulze ranking')], max_length=30, verbose_name='Voting system'), + field=models.CharField( + choices=[ + ('condorcet', 'Condorcet'), + ('schulze', 'Schulze, ordered list'), + ('schulze_old', 'Schulze, ordered list (old)'), + ('schulze_new', 'Schulze, ordered list (new)'), + ('schulze_both', 'Schulze, ordered list (both)'), + ('stcom', 'Steering Committee Election'), + ('stv1', 'STV, single winner'), + ('stv2', 'STV, two winners'), + ('stv3', 'STV, three winners'), + ('stv4', 'STV, four winners'), + ('stv5', 'STV, five winners'), + ('stv8', 'STV, eight winners'), + ('stv10', 'STV, ten winners'), + ('stonethor', 'STV partition with Schulze ranking'), + ], + max_length=30, + verbose_name='Voting system', + ), ), ] diff --git a/election/migrations/0004_auto_20200121_1340.py b/election/migrations/0004_auto_20200121_1340.py index a4ea82ac..bc105ffa 100644 --- a/election/migrations/0004_auto_20200121_1340.py +++ b/election/migrations/0004_auto_20200121_1340.py @@ -13,6 +13,20 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='election', name='voting_system', - field=models.CharField(choices=[('condorcet', 'Condorcet'), ('schulze', 'Schulze, ordered list'), ('stv1', 'STV, single winner'), ('stv2', 'STV, two winners'), ('stv3', 'STV, three winners'), ('stv4', 'STV, four winners'), ('stv5', 'STV, five winners'), ('stv8', 'STV, eight winners'), ('stv10', 'STV, ten winners')], max_length=30, verbose_name='Voting system'), + field=models.CharField( + choices=[ + ('condorcet', 'Condorcet'), + ('schulze', 'Schulze, ordered list'), + ('stv1', 'STV, single winner'), + ('stv2', 'STV, two winners'), + ('stv3', 'STV, three winners'), + ('stv4', 'STV, four winners'), + ('stv5', 'STV, five winners'), + ('stv8', 'STV, eight winners'), + ('stv10', 'STV, ten winners'), + ], + max_length=30, + verbose_name='Voting system', + ), ), ] diff --git a/election/migrations/0005_auto_20200909_1553.py b/election/migrations/0005_auto_20200909_1553.py index 06133884..b656aa76 100644 --- a/election/migrations/0005_auto_20200909_1553.py +++ b/election/migrations/0005_auto_20200909_1553.py @@ -13,6 +13,21 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='election', name='voting_system', - field=models.CharField(choices=[('condorcet', 'Condorcet'), ('schulze', 'Schulze, ordered list'), ('stv1', 'STV, single winner'), ('stv2', 'STV, two winners'), ('stv3', 'STV, three winners'), ('stv4', 'STV, four winners'), ('stv5', 'STV, five winners'), ('stv6', 'STV, six winners'), ('stv8', 'STV, eight winners'), ('stv10', 'STV, ten winners')], max_length=30, verbose_name='Voting system'), + field=models.CharField( + choices=[ + ('condorcet', 'Condorcet'), + ('schulze', 'Schulze, ordered list'), + ('stv1', 'STV, single winner'), + ('stv2', 'STV, two winners'), + ('stv3', 'STV, three winners'), + ('stv4', 'STV, four winners'), + ('stv5', 'STV, five winners'), + ('stv6', 'STV, six winners'), + ('stv8', 'STV, eight winners'), + ('stv10', 'STV, ten winners'), + ], + max_length=30, + verbose_name='Voting system', + ), ), ] diff --git a/election/migrations/0006_auto_20220102_1823.py b/election/migrations/0006_auto_20220102_1823.py index f2853c25..1f0ae0c9 100644 --- a/election/migrations/0006_auto_20220102_1823.py +++ b/election/migrations/0006_auto_20220102_1823.py @@ -15,12 +15,22 @@ class Migration(migrations.Migration): migrations.AddField( model_name='election', name='conditions', - field=models.TextField(blank=True, help_text='Candidates must accept these conditions to be allowed to run in the election. Anything binding for the candidates should be placed here, for example if candidates are expected to abide by certain rules, to volunteer their time in a some way or provide particular information.', null=True, verbose_name='Conditions for candidates'), + field=models.TextField( + blank=True, + help_text='Candidates must accept these conditions to be allowed to run in the election. Anything binding for the candidates should be placed here, for example if candidates are expected to abide by certain rules, to volunteer their time in a some way or provide particular information.', + null=True, + verbose_name='Conditions for candidates', + ), ), migrations.AlterField( model_name='election', name='instructions', - field=models.TextField(blank=True, help_text='Instructions or other information that might be of importance to those casting their votes.', null=True, verbose_name='Instructions for voters'), + field=models.TextField( + blank=True, + help_text='Instructions or other information that might be of importance to those casting their votes.', + null=True, + verbose_name='Instructions for voters', + ), ), migrations.AlterUniqueTogether( name='candidate', diff --git a/election/models.py b/election/models.py index 26aa73e9..f7fde073 100644 --- a/election/models.py +++ b/election/models.py @@ -15,13 +15,18 @@ class ElectionQuerySet(models.QuerySet): def recent(self): - return self.filter(deadline_votes__gt=datetime.now() - timedelta(days=settings.RECENT_ELECTION_DAYS)) + return self.filter( + deadline_votes__gt=datetime.now() + - timedelta(days=settings.RECENT_ELECTION_DAYS) + ) + class Election(models.Model): """ An election is different from an issue vote; it's a vote on people. Users, specifically. """ + objects = ElectionQuerySet.as_manager() # Note: Not used for model field options (at least not yet), but rather @@ -39,7 +44,9 @@ class Election(models.Model): slug = models.SlugField(max_length=128, blank=True) polity = models.ForeignKey('polity.Polity', on_delete=CASCADE) - voting_system = models.CharField(max_length=30, verbose_name=_('Voting system'), choices=VOTING_SYSTEMS) + voting_system = models.CharField( + max_length=30, verbose_name=_('Voting system'), choices=VOTING_SYSTEMS + ) # Tells whether the election results page should show the winning # candidates as an ordered list or as a set of winners. Some voting @@ -48,7 +55,9 @@ class Election(models.Model): # elegant to set this in a model describing the voting system in more # detail. To achieve that, the BallotCounter.VOTING_SYSTEMS list above # should to be turned into a proper Django model. - results_are_ordered = models.BooleanField(default=True, verbose_name=_('Results are ordered')) + results_are_ordered = models.BooleanField( + default=True, verbose_name=_('Results are ordered') + ) # How many candidates will be publicly listed in the results of an # election. Officers still see entire list. Individual candidates can also @@ -56,33 +65,75 @@ class Election(models.Model): results_limit = models.IntegerField( null=True, blank=True, - verbose_name=_('How many candidates will be publicly listed in the results of an election') + verbose_name=_( + 'How many candidates will be publicly listed in the results of an election' + ), ) - deadline_candidacy = models.DateTimeField(verbose_name=_('Deadline for candidacies')) - starttime_votes = models.DateTimeField(null=True, blank=True, verbose_name=_('Election begins')) + deadline_candidacy = models.DateTimeField( + verbose_name=_('Deadline for candidacies') + ) + starttime_votes = models.DateTimeField( + null=True, blank=True, verbose_name=_('Election begins') + ) deadline_votes = models.DateTimeField(verbose_name=_('Election ends')) # This allows one polity to host elections for one or more others, in # particular allowing access to elections based on geographical polities # without residency granting access to participate in all other polity # activities. - voting_polities = models.ManyToManyField('polity.Polity', blank=True, related_name='remote_election_votes', verbose_name=_('Voting polities')) - candidate_polities = models.ManyToManyField('polity.Polity', blank=True, related_name='remote_election_candidates', verbose_name=_('Candidate polities')) + voting_polities = models.ManyToManyField( + 'polity.Polity', + blank=True, + related_name='remote_election_votes', + verbose_name=_('Voting polities'), + ) + candidate_polities = models.ManyToManyField( + 'polity.Polity', + blank=True, + related_name='remote_election_candidates', + verbose_name=_('Candidate polities'), + ) # Sometimes elections may depend on a user having been the organization's member for an X amount of time # This optional field lets the vote counter disregard members who are too new. - deadline_joined_org = models.DateTimeField(null=True, blank=True, verbose_name=_('Membership deadline')) + deadline_joined_org = models.DateTimeField( + null=True, blank=True, verbose_name=_('Membership deadline') + ) is_processed = models.BooleanField(default=False) - instructions = models.TextField(null=True, blank=True, verbose_name=_('Instructions for voters'), help_text=_('Instructions or other information that might be of importance to those casting their votes.')) - conditions = models.TextField(null=True, blank=True, verbose_name=_('Conditions for candidates'), help_text=_('Candidates must accept these conditions to be allowed to run in the election. Anything binding for the candidates should be placed here, for example if candidates are expected to abide by certain rules, to volunteer their time in a some way or provide particular information.')) + instructions = models.TextField( + null=True, + blank=True, + verbose_name=_('Instructions for voters'), + help_text=_( + 'Instructions or other information that might be of importance to those casting their votes.' + ), + ) + conditions = models.TextField( + null=True, + blank=True, + verbose_name=_('Conditions for candidates'), + help_text=_( + 'Candidates must accept these conditions to be allowed to run in the election. Anything binding for the candidates should be placed here, for example if candidates are expected to abide by certain rules, to volunteer their time in a some way or provide particular information.' + ), + ) # These are election statistics; - stats = models.TextField(null=True, blank=True, verbose_name=_('Statistics as JSON')) - stats_publish_ballots_basic = models.BooleanField(default=False, verbose_name=_('Publish basic ballot statistics')) - stats_publish_ballots_per_candidate = models.BooleanField(default=False, verbose_name=_('Publish ballot statistics for each candidate')) - stats_publish_files = models.BooleanField(default=False, verbose_name=_('Publish advanced statistics (downloadable)')) + stats = models.TextField( + null=True, blank=True, verbose_name=_('Statistics as JSON') + ) + stats_publish_ballots_basic = models.BooleanField( + default=False, verbose_name=_('Publish basic ballot statistics') + ) + stats_publish_ballots_per_candidate = models.BooleanField( + default=False, + verbose_name=_('Publish ballot statistics for each candidate'), + ) + stats_publish_files = models.BooleanField( + default=False, + verbose_name=_('Publish advanced statistics (downloadable)'), + ) class Meta: ordering = ['-deadline_votes'] @@ -101,13 +152,15 @@ def save_ballots(self, ballot_counter): try: filename = settings.BALLOT_SAVEFILE_FORMAT % { 'election_id': self.id, - 'voting_system': self.voting_system} + 'voting_system': self.voting_system, + } directory = os.path.dirname(filename) if not os.path.exists(directory): os.mkdir(directory) ballot_counter.save_ballots(filename) except: import traceback + traceback.print_exc() return False return True @@ -118,20 +171,26 @@ def load_archived_ballots(self): try: filename = settings.BALLOT_SAVEFILE_FORMAT % { 'election_id': self.id, - 'voting_system': self.voting_system} + 'voting_system': self.voting_system, + } bc.load_ballots(filename) except: import traceback + traceback.print_exc() return bc @transaction.atomic def process(self): if self.election_state() != 'concluded': - raise Election.ElectionInProgressException('Election %s is still in progress!' % self) + raise Election.ElectionInProgressException( + 'Election %s is still in progress!' % self + ) if self.is_processed: - raise Election.AlreadyProcessedException('Election %s has already been processed!' % self) + raise Election.AlreadyProcessedException( + 'Election %s has already been processed!' % self + ) # "Flatten" the values of votes in an election. A candidate may be # removed from an election when voting has already started. When that @@ -163,7 +222,9 @@ def process(self): else: ordered_candidates, ballot_counter = self.process_votes() - vote_count = self.electionvote_set.values('user').distinct().count() + vote_count = ( + self.electionvote_set.values('user').distinct().count() + ) # Save anonymized ballots to a file, so we can recount later save_failed = not self.save_ballots(ballot_counter) @@ -175,7 +236,9 @@ def process(self): try: election_result = ElectionResult.objects.get(election=self) except ElectionResult.DoesNotExist: - election_result = ElectionResult.objects.create(election=self, vote_count=vote_count) + election_result = ElectionResult.objects.create( + election=self, vote_count=vote_count + ) election_result.rows.all().delete() order = 0 @@ -197,7 +260,11 @@ def process(self): if self.polity.push_on_election_end: # Doing this just to force the translation string creation: __ = _("Election results in election '%s' have been calculated.") - push_send_notification_to_polity_users(self.polity.id, "Election results in election '%s' have been calculated.", [self.name]) + push_send_notification_to_polity_users( + self.polity.id, + "Election results in election '%s' have been calculated.", + [self.name], + ) def generate_stats(self): ballot_counter = self.load_archived_ballots() @@ -213,22 +280,31 @@ def generate_stats(self): def get_voters(self): if self.voting_polities.count() > 0: - voters = User.objects.filter(polities__in=self.voting_polities.all()) + voters = User.objects.filter( + polities__in=self.voting_polities.all() + ) else: voters = self.polity.election_voters() if self.deadline_joined_org: - return voters.filter(userprofile__joined_org__lt = self.deadline_joined_org) + return voters.filter( + userprofile__joined_org__lt=self.deadline_joined_org + ) else: return voters def can_vote(self, user=None, user_id=None): - return self.get_voters().filter( - id=(user_id if (user_id is not None) else user.id)).exists() + return ( + self.get_voters() + .filter(id=(user_id if (user_id is not None) else user.id)) + .exists() + ) def get_potential_candidates(self): if self.candidate_polities.count() > 0: - pcands = User.objects.filter(polities__in=self.candidate_polities.all()) + pcands = User.objects.filter( + polities__in=self.candidate_polities.all() + ) else: pcands = self.polity.election_potential_candidates() @@ -239,14 +315,24 @@ def get_potential_candidates(self): return pcands def can_be_candidate(self, user=None, user_id=None): - return self.get_potential_candidates().filter( - id=(user_id if (user_id is not None) else user.id)).exists() + return ( + self.get_potential_candidates() + .filter(id=(user_id if (user_id is not None) else user.id)) + .exists() + ) def process_votes(self): if self.deadline_joined_org: - votes = ElectionVote.objects.select_related('candidate__user').filter(election=self, user__userprofile__joined_org__lt = self.deadline_joined_org) + votes = ElectionVote.objects.select_related( + 'candidate__user' + ).filter( + election=self, + user__userprofile__joined_org__lt=self.deadline_joined_org, + ) else: - votes = ElectionVote.objects.select_related('candidate__user').filter(election=self) + votes = ElectionVote.objects.select_related( + 'candidate__user' + ).filter(election=self) votemap = {} for vote in votes: @@ -311,7 +397,8 @@ def get_stats(self, user=None, load_users=True, rename_users=False): 'ballot_lengths': {}, 'ballots': 0, 'ballot_length_average': 0, - 'ballot_length_most_common': 0} + 'ballot_length_most_common': 0, + } # Parse the stats JSON, if it exists. try: @@ -331,8 +418,10 @@ def get_stats(self, user=None, load_users=True, rename_users=False): if self.results_limit: excluded = set([]) if not user or not user.is_staff: - excluded |= set(cand.user.username for cand in - self.get_winners()[self.results_limit:]) + excluded |= set( + cand.user.username + for cand in self.get_winners()[self.results_limit :] + ) if user and user.username in excluded: excluded.remove(user.username) stats = BallotCounter.exclude_candidate_stats(stats, excluded) @@ -373,12 +462,19 @@ def get_formatted_stats(self, fmt, user=None): return None def get_winners(self): - return [r.candidate for r in self.result.rows.select_related('candidate__user__userprofile').order_by('order')] + return [ + r.candidate + for r in self.result.rows.select_related( + 'candidate__user__userprofile' + ).order_by('order') + ] def get_candidates(self): ctx = {} ctx["count"] = self.candidate_set.count() - ctx["users"] = [{"username": x.user.username} for x in self.candidate_set.all()] + ctx["users"] = [ + {"username": x.user.username} for x in self.candidate_set.all() + ] return ctx def get_unchosen_candidates(self, user): @@ -388,7 +484,11 @@ def get_unchosen_candidates(self, user): votes = ElectionVote.objects.filter(election=self, user=user) votedcands = [x.candidate.id for x in votes] if len(votedcands) != 0: - candidates = Candidate.objects.filter(election=self).exclude(id__in=votedcands).order_by('?') + candidates = ( + Candidate.objects.filter(election=self) + .exclude(id__in=votedcands) + .order_by('?') + ) else: candidates = Candidate.objects.filter(election=self).order_by('?') @@ -404,12 +504,15 @@ def has_voted(self, user, **constraints): if user.is_anonymous: return False return ElectionVote.objects.filter( - election=self, user=user, **constraints).exists() + election=self, user=user, **constraints + ).exists() def get_vote(self, user): votes = [] if not user.is_anonymous: - votes = ElectionVote.objects.filter(election=self, user=user).order_by("value") + votes = ElectionVote.objects.filter( + election=self, user=user + ).order_by("value") return [x.candidate for x in votes] def get_ballots(self): @@ -417,7 +520,9 @@ def get_ballots(self): for voter in self.electionvote_set.values("user").distinct(): user = User.objects.get(pk=voter["user"]) ballot = [] - for vote in user.electionvote_set.filter(election=self).order_by('value'): + for vote in user.electionvote_set.filter(election=self).order_by( + 'value' + ): ballot.append(vote.candidate.user.username) ballot_box.append(ballot) random.shuffle(ballot_box) @@ -446,21 +551,29 @@ class ElectionVote(models.Model): value = models.IntegerField() class Meta: - unique_together = (('election', 'user', 'candidate'), - ('election', 'user', 'value')) + unique_together = ( + ('election', 'user', 'candidate'), + ('election', 'user', 'value'), + ) def __str__(self): return u'User %s has voted in election %s' % (self.user, self.election) class ElectionResult(models.Model): - election = models.OneToOneField('Election', related_name='result', on_delete=CASCADE) + election = models.OneToOneField( + 'Election', related_name='result', on_delete=CASCADE + ) vote_count = models.IntegerField() class ElectionResultRow(models.Model): - election_result = models.ForeignKey('ElectionResult', related_name='rows', on_delete=CASCADE) - candidate = models.OneToOneField('Candidate', related_name='result_row', on_delete=CASCADE) + election_result = models.ForeignKey( + 'ElectionResult', related_name='rows', on_delete=CASCADE + ) + candidate = models.OneToOneField( + 'Candidate', related_name='result_row', on_delete=CASCADE + ) order = models.IntegerField() class Meta: diff --git a/election/schulze.py b/election/schulze.py index 89898f55..ab0000fe 100644 --- a/election/schulze.py +++ b/election/schulze.py @@ -17,7 +17,7 @@ def compute_strongest_paths(preference, candidates): strongest_paths[i,j] = bottleneck number in the strongest path between i and j """ strongest_paths = defaultdict(lambda: defaultdict(int)) - + # Calculate the strongest paths between candidates for i in candidates: for j in candidates: @@ -32,19 +32,22 @@ def compute_strongest_paths(preference, candidates): if i != j: for k in candidates: if i != k and j != k: - #p[j,k] := max ( p[j,k], min ( p[j,i], p[i,k] ) ) - strongest_paths[j][k] = max(strongest_paths[j][k], min(strongest_paths[j][i], strongest_paths[i][k])) + # p[j,k] := max ( p[j,k], min ( p[j,i], p[i,k] ) ) + strongest_paths[j][k] = max( + strongest_paths[j][k], + min(strongest_paths[j][i], strongest_paths[i][k]), + ) return strongest_paths def get_ordered_voting_results(strongest_paths): """ - strongest_paths: the strongest paths of each candidate. - returns: - ordered dictionary, ordered by how many wins a candidate had against other candidates - key is candidate, value is list of candidates defeated by that candidate. - """ + strongest_paths: the strongest paths of each candidate. + returns: + ordered dictionary, ordered by how many wins a candidate had against other candidates + key is candidate, value is list of candidates defeated by that candidate. + """ # We need to determine the ordering among the candidates by comparing their respective path strengths. # For all candidates, compare their path strengths in both directions, the candidate that has stronger path @@ -60,7 +63,9 @@ def get_ordered_voting_results(strongest_paths): wins[ci].append(cj) # Create ordered results of candidates that actually won other candidates - ordered_results = sorted(wins.items(), key=lambda x: len(x[1]), reverse=True) + ordered_results = sorted( + wins.items(), key=lambda x: len(x[1]), reverse=True + ) # Add any candidates that did not win anything in a random order stragglers = [c for c in strongest_paths.keys() if c not in wins] @@ -92,7 +97,7 @@ def rank_votes(votes, candidates): } """ invalid_votes = list() - #prepare the output - 0 set all candidates + # prepare the output - 0 set all candidates preference = defaultdict(lambda: defaultdict(int)) for vote in votes: @@ -109,14 +114,21 @@ def rank_votes(votes, candidates): for i, choice in enumerate(vote): # resolve ties: [(1, 'a'), (2, 'c'), (2, 'e'), (3, 'b'), (5, 'd')] 'e' also gets a 'c' increment tied_candidates = [x[1] for x in vote if choice[0] == x[0]] - not_voted_candidates = set(candidates)-voted_candidates + not_voted_candidates = set(candidates) - voted_candidates # increment against all other candidates candidate = vote[i][1] opponents_to_increment = list( - set([x[1] for x in vote[i+1:]] + list(not_voted_candidates) + tied_candidates)) + set( + [x[1] for x in vote[i + 1 :]] + + list(not_voted_candidates) + + tied_candidates + ) + ) - increment_candidate(candidate, opponents_to_increment, preference) + increment_candidate( + candidate, opponents_to_increment, preference + ) return preference @@ -127,4 +139,3 @@ def increment_candidate(candidate, opponents, preference_dict): preference_dict[candidate][opponent] += 1 else: preference_dict[candidate][opponent] = 1 - diff --git a/election/templatetags/elections.py b/election/templatetags/elections.py index a4982296..f68efa14 100644 --- a/election/templatetags/elections.py +++ b/election/templatetags/elections.py @@ -13,7 +13,7 @@ def electionvoted(election, user): except TypeError: pass - return (ut > 0) + return ut > 0 @register.filter diff --git a/election/test/test_schulze.py b/election/test/test_schulze.py index 3a6b532e..6b96c7cf 100644 --- a/election/test/test_schulze.py +++ b/election/test/test_schulze.py @@ -54,7 +54,7 @@ def test_6_candidates_45_votes(self): [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], - [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')] # 8 + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], # 8 ] # Randomize the votes @@ -64,7 +64,9 @@ def test_6_candidates_45_votes(self): preference = schulze.rank_votes(votes, candidates) # Get the strongest paths of each candidate - strongest_paths = schulze.compute_strongest_paths(preference, candidates) + strongest_paths = schulze.compute_strongest_paths( + preference, candidates + ) # Get final, ordered, results results = schulze.get_ordered_voting_results(strongest_paths) @@ -77,7 +79,7 @@ def test_3_candidates_3_votes_cyclical(self): votes = [ [(1, 'x'), (2, 'y'), (3, 'z')], [(1, 'y'), (2, 'z'), (3, 'x')], - [(1, 'z'), (2, 'x'), (3, 'y')] + [(1, 'z'), (2, 'x'), (3, 'y')], ] # Randomize the votes @@ -87,7 +89,9 @@ def test_3_candidates_3_votes_cyclical(self): preference = schulze.rank_votes(votes, candidates) # Get the strongest paths of each candidate - strongest_paths = schulze.compute_strongest_paths(preference, candidates) + strongest_paths = schulze.compute_strongest_paths( + preference, candidates + ) # Get final, ordered, results results = schulze.get_ordered_voting_results(strongest_paths) @@ -155,7 +159,9 @@ def test_statistical(self, nr_candidates=10, nr_voters=1000): preference = schulze.rank_votes(votes, candidates) # Get the strongest paths of each candidate - strongest_paths = schulze.compute_strongest_paths(preference, candidates) + strongest_paths = schulze.compute_strongest_paths( + preference, candidates + ) # Get final, ordered, results results = schulze.get_ordered_voting_results(strongest_paths) @@ -164,16 +170,21 @@ def test_statistical(self, nr_candidates=10, nr_voters=1000): for place, candidate in enumerate(candidate_chances): expected_place = candidate_chances.index(candidate) self.assertIn( - place, - (expected_place, expected_place + 1, expected_place - 1) + place, (expected_place, expected_place + 1, expected_place - 1) ) - print("candidate %s had %s%% chance of winning" % ( - candidate[1], candidate[0])) - others = (0 if results[candidate[1]] is None - else len(results[candidate[1]])) + print( + "candidate %s had %s%% chance of winning" + % (candidate[1], candidate[0]) + ) + others = ( + 0 + if results[candidate[1]] is None + else len(results[candidate[1]]) + ) print("candidate was ranked above %s other candidates" % (others)) + """ Example results from 50 candidates and 300,000 voters: statistical_test(50, 300000) diff --git a/election/urls.py b/election/urls.py index c4ad6be8..d9f386c0 100644 --- a/election/urls.py +++ b/election/urls.py @@ -15,13 +15,35 @@ urlpatterns = [ - url(r'^polity/(?P\d+)/elections/$', never_cache(election_list), name='elections'), - url(r'^polity/(?P\d+)/election/new/$', election_add_edit, name='election_add_edit'), - url(r'^polity/(?P\d+)/election/(?P\d+)/edit/$', election_add_edit, name='election_add_edit'), - url(r'^polity/(?P\d+)/election/(?P\d+)/candidates-details/$', never_cache(election_candidates_details), name='election_candidates_details'), - url(r'^polity/(?P\d+)/election/(?P\d+)/$', never_cache(election_view), name='election'), - url(r'^polity/(?P\d+)/election/(?P\d+)/stats-dl/(?P.+)$', election_stats_download), - + url( + r'^polity/(?P\d+)/elections/$', + never_cache(election_list), + name='elections', + ), + url( + r'^polity/(?P\d+)/election/new/$', + election_add_edit, + name='election_add_edit', + ), + url( + r'^polity/(?P\d+)/election/(?P\d+)/edit/$', + election_add_edit, + name='election_add_edit', + ), + url( + r'^polity/(?P\d+)/election/(?P\d+)/candidates-details/$', + never_cache(election_candidates_details), + name='election_candidates_details', + ), + url( + r'^polity/(?P\d+)/election/(?P\d+)/$', + never_cache(election_view), + name='election', + ), + url( + r'^polity/(?P\d+)/election/(?P\d+)/stats-dl/(?P.+)$', + election_stats_download, + ), url(r'^api/election/poll/$', never_cache(election_poll)), url(r'^api/election/vote/$', never_cache(election_vote)), url(r'^api/election/candidacy/$', never_cache(election_candidacy)), diff --git a/election/utils.py b/election/utils.py index 7d6da95e..7e22426d 100644 --- a/election/utils.py +++ b/election/utils.py @@ -31,6 +31,7 @@ class BallotContainer(object): or restoring internal state during analysis, and returning the list of ballots in a few different formats. """ + def __init__(self, ballots=None): """ Ballots should be a list of lists of (rank, candidate) tuples. @@ -80,7 +81,8 @@ def save_ballots(self, filename): unicode_ballots = [] for ballot in self.ballots: unicode_ballots.append( - [(rank, str(cand)) for rank, cand in ballot]) + [(rank, str(cand)) for rank, cand in ballot] + ) random.shuffle(unicode_ballots) if filename == '-': json.dump(unicode_ballots, sys.stdout, indent=1) @@ -103,10 +105,13 @@ def exclude_candidates(self, excluded): def ballots_as_lists(self): for ballot in self.ballots: - as_list = [candidate for rank, candidate in sorted(ballot) - if candidate not in self.excluded] + as_list = [ + candidate + for rank, candidate in sorted(ballot) + if candidate not in self.excluded + ] if as_list: - yield(as_list) + yield (as_list) def ballots_as_rankings(self): b = self.ballots_as_lists() if (self.collapse_gaps) else self.ballots @@ -116,7 +121,7 @@ def ballots_as_rankings(self): for rank, candidate in ranked: if candidate not in self.excluded: rankings[candidate] = rank - yield(rankings) + yield (rankings) def hashes_with_counts(self, ballots): hashes = {} @@ -133,12 +138,15 @@ class BallotAnalyzer(BallotContainer): """ This class will analyze and return statistics about a set of ballots. """ + def _cands_and_stats(self): cands = sorted(self.get_candidates()) - return (cands, OrderedDict([ - ('ballots', len(self.ballots)), - ('candidates', cands) - ])) + return ( + cands, + OrderedDict( + [('ballots', len(self.ballots)), ('candidates', cands)] + ), + ) def get_ballot_stats(self): cands, stats = self._cands_and_stats() @@ -147,16 +155,20 @@ def get_ballot_stats(self): l = len(ballot) lengths[l] = lengths.get(l, 0) + 1 stats['ballot_lengths'] = lengths - stats['ballot_length_average'] = float(sum( - (k * v) for k, v in iter(lengths.items()))) / len(self.ballots) + stats['ballot_length_average'] = float( + sum((k * v) for k, v in iter(lengths.items())) + ) / len(self.ballots) def ls(l): return { "length": l, "count": lengths[l], - "pct": float(100 * lengths[l]) / len(self.ballots)} - stats['ballot_length_most_common'] = ls(max( - (v, k) for k, v in iter(lengths.items()))[1]) + "pct": float(100 * lengths[l]) / len(self.ballots), + } + + stats['ballot_length_most_common'] = ls( + max((v, k) for k, v in iter(lengths.items()))[1] + ) stats['ballot_length_longest'] = ls(max(lengths.keys())) stats['ballot_length_shortest'] = ls(min(lengths.keys())) @@ -170,7 +182,7 @@ def reranked_ballot(self, ballot): fixed.append((rank, candidate)) rank += 1 return fixed - + def get_candidate_rank_stats(self): cands, stats = self._cands_and_stats() ranks = [[0 for r in cands] for c in cands] @@ -233,61 +245,104 @@ def exclude_candidate_stats(self, stats, excluded): @classmethod def stats_as_text(self, stats): lines = [ - '
' % (
-                datetime.datetime.now()),
+            '
'
+            % (datetime.datetime.now()),
             '',
-            'Analyzed %d ballots with %d candidates.' % (
-                stats['ballots'], len(stats['candidates']))]
+            'Analyzed %d ballots with %d candidates.'
+            % (stats['ballots'], len(stats['candidates'])),
+        ]
 
         if 'ballot_lengths' in stats:
-            lines += ['', 'Ballots:',
-                ('   - Average ballot length: %.2f'
-                     ) % stats['ballot_length_average'],
-                ('   - Shortest ballot length: %(length)d (%(count)d '
-                        'ballots=%(pct)d%%)') % stats['ballot_length_shortest'],
-                ('   - Most common ballot length: %(length)d (%(count)d '
-                        'ballots=%(pct)d%%)') % stats['ballot_length_most_common'],
-                ('   - Longest ballot length: %(length)d (%(count)d '
-                        'ballots=%(pct)d%%)') % stats['ballot_length_longest'],
-                '   - L/B: [%s]' % (' '.join(
-                    '%d/%d' % (k, stats['ballot_lengths'][k])
-                        for k in sorted(stats['ballot_lengths'].keys())))]
+            lines += [
+                '',
+                'Ballots:',
+                ('   - Average ballot length: %.2f')
+                % stats['ballot_length_average'],
+                (
+                    '   - Shortest ballot length: %(length)d (%(count)d '
+                    'ballots=%(pct)d%%)'
+                )
+                % stats['ballot_length_shortest'],
+                (
+                    '   - Most common ballot length: %(length)d (%(count)d '
+                    'ballots=%(pct)d%%)'
+                )
+                % stats['ballot_length_most_common'],
+                (
+                    '   - Longest ballot length: %(length)d (%(count)d '
+                    'ballots=%(pct)d%%)'
+                )
+                % stats['ballot_length_longest'],
+                '   - L/B: [%s]'
+                % (
+                    ' '.join(
+                        '%d/%d' % (k, stats['ballot_lengths'][k])
+                        for k in sorted(stats['ballot_lengths'].keys())
+                    )
+                ),
+            ]
 
         if stats.get('duplicates'):
-            lines += ['',
-                'Frequent ballots: (>= %d occurrances, %d%%)' % (
+            lines += [
+                '',
+                'Frequent ballots: (>= %d occurrances, %d%%)'
+                % (
                     stats['duplicate_threshold'],
-                    (100 * stats['duplicate_threshold']) / stats['ballots'])]
+                    (100 * stats['duplicate_threshold']) / stats['ballots'],
+                ),
+            ]
             for dup in stats['duplicates']:
                 lines += ['   - %(count)d times: %(ballot)s' % dup]
 
         if stats.get('ranking_matrix'):
             rm = stats['ranking_matrix']
-            lines += ['',
+            lines += [
+                '',
                 'Rankings:',
-                ' %16.16s  %s ANY' % ('CANDIDATE', ' '.join(
-                    '%3.3s' % (i+1) for i in range(0, len(rm[0])-1)))]
+                ' %16.16s  %s ANY'
+                % (
+                    'CANDIDATE',
+                    ' '.join(
+                        '%3.3s' % (i + 1) for i in range(0, len(rm[0]) - 1)
+                    ),
+                ),
+            ]
             rls = []
             for i, candidate in enumerate(stats['candidates']):
-                rls += [' %16.16s  %s' % (candidate, ' '.join(
-                    '%3.3s' % v for v in rm[i]))]
+                rls += [
+                    ' %16.16s  %s'
+                    % (candidate, ' '.join('%3.3s' % v for v in rm[i]))
+                ]
 
             def safe_int(i):
                 try:
                     return int(i)
                 except ValueError:
                     return 0
+
             rls.sort(key=lambda l: -safe_int(l.strip().split()[-1]))
             lines.extend(rls)
 
         if stats.get('pairwise_matrix'):
-            lines += ['',
+            lines += [
+                '',
                 'Pairwise victories:',
-                ' %16.16s  %s' % ('WINNER', ' '.join(
-                    '%3.3s' % c for c in stats['candidates']))]
+                ' %16.16s  %s'
+                % (
+                    'WINNER',
+                    ' '.join('%3.3s' % c for c in stats['candidates']),
+                ),
+            ]
             for i, candidate in enumerate(stats['candidates']):
-                lines += [' %16.16s  %s' % (candidate, ' '.join(
-                    '%3.3s' % v for v in stats['pairwise_matrix'][i]))]
+                lines += [
+                    ' %16.16s  %s'
+                    % (
+                        candidate,
+                        ' '.join(
+                            '%3.3s' % v for v in stats['pairwise_matrix'][i]
+                        ),
+                    )
+                ]
 
         lines += ['', '%s
' % (' ' * 60,)] return '\n'.join(lines) @@ -303,26 +358,35 @@ def stats_as_spreadsheet(self, fmt, stats): bll = stats['ballot_length_longest'] blmc = stats['ballot_length_most_common'] pages['Ballots'] = [ - ['Ballots', count], - [''], - ['', 'Length', 'Ballots', '%'], - ['Shortest', bls['length'], bls['count'], bls['pct']], - ['Longest', bll['length'], bll['count'], bll['pct']], - ['Average', stats['ballot_length_average'], '', ''], - ['Most common', blmc['length'], blmc['count'], blmc['pct']], - [''], - ['Ballot length', 'Ballots'] - ] + sorted([ + ['Ballots', count], + [''], + ['', 'Length', 'Ballots', '%'], + ['Shortest', bls['length'], bls['count'], bls['pct']], + ['Longest', bll['length'], bll['count'], bll['pct']], + ['Average', stats['ballot_length_average'], '', ''], + ['Most common', blmc['length'], blmc['count'], blmc['pct']], + [''], + ['Ballot length', 'Ballots'], + ] + sorted( + [ [l, stats['ballot_lengths'][l]] - for l in stats['ballot_lengths']]) + for l in stats['ballot_lengths'] + ] + ) if stats.get('duplicates'): pages['Duplicates'] = page = [ - ['Frequent ballots: (>= %d occurrances, %d%%)' % ( - stats['duplicate_threshold'], - (100 * stats['duplicate_threshold']) / stats['ballots'])], + [ + 'Frequent ballots: (>= %d occurrances, %d%%)' + % ( + stats['duplicate_threshold'], + (100 * stats['duplicate_threshold']) + / stats['ballots'], + ) + ], [''], - ['Count', 'Ballot ...']] + ['Count', 'Ballot ...'], + ] for dup in stats['duplicates']: page += [[dup["count"]] + dup['ballot']] @@ -330,8 +394,9 @@ def stats_as_spreadsheet(self, fmt, stats): rm = stats['ranking_matrix'] pages['Rankings'] = page = [ ['CANDIDATE'] - + [(i+1) for i in range(0, len(rm[0])-1)] - + ['ANY']] + + [(i + 1) for i in range(0, len(rm[0]) - 1)] + + ['ANY'] + ] rls = [] for i, candidate in enumerate(stats['candidates']): rls.append([candidate] + rm[i]) @@ -340,12 +405,14 @@ def stats_as_spreadsheet(self, fmt, stats): if stats.get('pairwise_matrix'): pages['Pairwise Victories'] = page = [ - ['WINNER'] + stats['candidates']] + ['WINNER'] + stats['candidates'] + ] for i, candidate in enumerate(stats['candidates']): page.append([candidate] + stats['pairwise_matrix'][i]) import pyexcel import StringIO + buf = StringIO.StringIO() pyexcel.Book(sheets=pages).save_to_memory(fmt, stream=buf) return buf.getvalue() @@ -362,10 +429,10 @@ class BallotCounter(BallotAnalyzer): VOTING_SYSTEMS = ( ('condorcet', _('Condorcet')), ('schulze', _('Schulze, ordered list')), - #('schulze_old', _('Schulze, ordered list (old)')), - #('schulze_new', _('Schulze, ordered list (new)')), - #('schulze_both', _('Schulze, ordered list (both)')), - #('stcom', _('Steering Committee Election')), + # ('schulze_old', _('Schulze, ordered list (old)')), + # ('schulze_new', _('Schulze, ordered list (new)')), + # ('schulze_both', _('Schulze, ordered list (both)')), + # ('stcom', _('Steering Committee Election')), ('stv1', _('STV, single winner')), ('stv2', _('STV, two winners')), ('stv3', _('STV, three winners')), @@ -374,7 +441,7 @@ class BallotCounter(BallotAnalyzer): ('stv6', _('STV, six winners')), ('stv8', _('STV, eight winners')), ('stv10', _('STV, ten winners')), - #('stonethor', _('STV partition with Schulze ranking')) + # ('stonethor', _('STV partition with Schulze ranking')) ) def system_name(self, system): @@ -383,8 +450,12 @@ def system_name(self, system): def schulze_results_old(self): candidates = self.candidates preference = schulze.rank_votes(self.ballots, candidates) - strongest_paths = schulze.compute_strongest_paths(preference, candidates) - ordered_candidates = schulze.get_ordered_voting_results(strongest_paths) + strongest_paths = schulze.compute_strongest_paths( + preference, candidates + ) + ordered_candidates = schulze.get_ordered_voting_results( + strongest_paths + ) return [cand for cand in ordered_candidates] def schulze_results_new(self, winners=None): @@ -394,10 +465,10 @@ def schulze_results_new(self, winners=None): else: winners = 1 return Schulze( - list(self.hashes_with_counts(self.ballots_as_rankings())), - winner_threshold=min(winners, len(self.candidates)), - ballot_notation=Schulze.BALLOT_NOTATION_RANKING, - ).as_dict()['order'] + list(self.hashes_with_counts(self.ballots_as_rankings())), + winner_threshold=min(winners, len(self.candidates)), + ballot_notation=Schulze.BALLOT_NOTATION_RANKING, + ).as_dict()['order'] def schulze_results_both(self, winners=None): """Wrapper to canary new schulze code, comparing with old""" @@ -416,11 +487,15 @@ def schulze_results_both(self, winners=None): def schulze_stv_results(self, winners=None): if winners is None: winners = 1 - return sorted(list(SchulzeSTV( - list(self.hashes_with_counts(self.ballots_as_rankings())), - required_winners=min(winners, len(self.candidates)), - ballot_notation=Schulze.BALLOT_NOTATION_RANKING, - ).as_dict()['winners'])) + return sorted( + list( + SchulzeSTV( + list(self.hashes_with_counts(self.ballots_as_rankings())), + required_winners=min(winners, len(self.candidates)), + ballot_notation=Schulze.BALLOT_NOTATION_RANKING, + ).as_dict()['winners'] + ) + ) def stv_results(self, winners=None): # FIXME: Variable names here may be somewhat confusing. @@ -431,7 +506,10 @@ def stv_results(self, winners=None): winnerset = [] - mid_result = STV(list(self.hashes_with_counts(self.ballots_as_lists())), required_winners=winners).as_dict() + mid_result = STV( + list(self.hashes_with_counts(self.ballots_as_lists())), + required_winners=winners, + ).as_dict() winners_found = 0 for mid_round in mid_result['rounds']: if not 'winners' in mid_round: @@ -439,7 +517,11 @@ def stv_results(self, winners=None): found_this_time = len(mid_round['winners']) if winners_found + found_this_time > winners: - winnerset.extend(random.sample(mid_round['winners'], winners - winners_found)) + winnerset.extend( + random.sample( + mid_round['winners'], winners - winners_found + ) + ) else: winnerset.extend(mid_round['winners']) @@ -452,9 +534,9 @@ def stv_results(self, winners=None): def condorcet_results(self): result = Condorcet( - list(self.hashes_with_counts(self.ballots_as_rankings())), - ballot_notation=Schulze.BALLOT_NOTATION_RANKING, - ).as_dict() + list(self.hashes_with_counts(self.ballots_as_rankings())), + ballot_notation=Schulze.BALLOT_NOTATION_RANKING, + ).as_dict() if not result.get('tied_winners'): return [result['winner']] else: @@ -481,9 +563,11 @@ def stonethor_results(self, partition=None, winners=None): partition separately using Schulze. The default partition is one quarter of the candidate count. """ - top = self.stv_results(winners=min( - partition or (len(self.candidates) / 4), - len(self.candidates))) + top = self.stv_results( + winners=min( + partition or (len(self.candidates) / 4), len(self.candidates) + ) + ) bottom = list(set(self.get_candidates()) - set(top)) if top: with self: @@ -494,7 +578,7 @@ def stonethor_results(self, partition=None, winners=None): return (top + bottom)[:winners] def results(self, method, winners=None, sysarg=None): - assert(method in [system for system, name in self.VOTING_SYSTEMS]) + assert method in [system for system, name in self.VOTING_SYSTEMS] if method == 'schulze': return self.schulze_results_new(winners=(winners or sysarg)) @@ -530,8 +614,8 @@ def constrained_results(self, method, winners=None, below=None): position = max(0, below.get(candidate, 1) - 1) for p in range(position, len(constrained)): if not constrained[p]: - constrained[p] = candidate - break + constrained[p] = candidate + break return [c for c in constrained if c] @@ -545,18 +629,28 @@ def constrained_results(self, method, winners=None, below=None): logger.setLevel(logging.DEBUG) ap = argparse.ArgumentParser() - ap.add_argument('-e', '--exclude', action='append', - help="Candidate(s) to exclude when counting") - ap.add_argument('-b', '--below', action='append', - help="seat,candidate pairs, to constrain final ordering") - ap.add_argument('--keep-gaps', action='store_true', - help="Preserve gaps if ballots are not sequential") - ap.add_argument('operation', - help="Operation to perform (count)") - ap.add_argument('system', - help="Counting system to use (schulze, stv5, ...)") - ap.add_argument('filenames', nargs='+', - help="Ballot files to read") + ap.add_argument( + '-e', + '--exclude', + action='append', + help="Candidate(s) to exclude when counting", + ) + ap.add_argument( + '-b', + '--below', + action='append', + help="seat,candidate pairs, to constrain final ordering", + ) + ap.add_argument( + '--keep-gaps', + action='store_true', + help="Preserve gaps if ballots are not sequential", + ) + ap.add_argument('operation', help="Operation to perform (count)") + ap.add_argument( + 'system', help="Counting system to use (schulze, stv5, ...)" + ) + ap.add_argument('filenames', nargs='+', help="Ballot files to read") args = ap.parse_args() system = args.system @@ -584,19 +678,37 @@ def constrained_results(self, method, winners=None, below=None): print('Voting system:\n\t%s (%s)' % (bc.system_name(system), system)) print('') - print('Loaded %d ballots from:\n\t%s' % ( - len(bc.ballots), '\n\t'.join(args.filenames))) + print( + 'Loaded %d ballots from:\n\t%s' + % (len(bc.ballots), '\n\t'.join(args.filenames)) + ) print('') if below: - print(('Results(C):\n\t%s' % ', '.join(bc.constrained_results( - system, sysarg=sysarg, below=below))).encode('utf-8')) + print( + ( + 'Results(C):\n\t%s' + % ', '.join( + bc.constrained_results( + system, sysarg=sysarg, below=below + ) + ) + ).encode('utf-8') + ) else: - print(('Results:\n\t%s' % ', '.join(bc.results( - system, sysarg=sysarg))).encode('utf-8')) + print( + ( + 'Results:\n\t%s' + % ', '.join(bc.results(system, sysarg=sysarg)) + ).encode('utf-8') + ) print('') elif args.operation in ( - 'analyze', 'analyze:json', 'analyze:ods', 'analyze:xlsx'): + 'analyze', + 'analyze:json', + 'analyze:ods', + 'analyze:xlsx', + ): stats = OrderedDict() if system == 'all': @@ -627,8 +739,9 @@ def constrained_results(self, method, winners=None, below=None): json.dump(stats, sys.stdout, indent=1) elif args.operation in ('analyze:ods', 'analyze:xlsx'): - sys.stdout.write(bc.stats_as_spreadsheet( - args.operation.split(':')[1], stats)) + sys.stdout.write( + bc.stats_as_spreadsheet(args.operation.split(':')[1], stats) + ) else: raise ValueError('Unknown operation: %s' % args.operation) diff --git a/election/views.py b/election/views.py index ef7ec13b..2796f70a 100644 --- a/election/views.py +++ b/election/views.py @@ -58,11 +58,19 @@ def election_view(request, polity_id, election_id): # about in the future. Still, we'd like to retain some history of their # candidacy. To try and attain both goals, we require a login for older # elections. - election_protection_timing = datetime.now() - timedelta(days=settings.RECENT_ISSUE_DAYS) - if not request.user.is_authenticated and election.deadline_votes < election_protection_timing: + election_protection_timing = datetime.now() - timedelta( + days=settings.RECENT_ISSUE_DAYS + ) + if ( + not request.user.is_authenticated + and election.deadline_votes < election_protection_timing + ): return redirect_to_login(request.path) - voting_interface_enabled = election.election_state() == 'voting' and election.can_vote(request.user) + voting_interface_enabled = ( + election.election_state() == 'voting' + and election.can_vote(request.user) + ) if election.is_processed: ordered_candidates = election.get_winners() @@ -91,26 +99,31 @@ def election_view(request, polity_id, election_id): 'vote_count': vote_count, 'voting_interface_enabled': voting_interface_enabled, 'user_result': user_result, - 'can_vote': (request.user is not None and election.can_vote(request.user)), - 'can_run': (request.user is not None and election.can_be_candidate(request.user)) + 'can_vote': ( + request.user is not None and election.can_vote(request.user) + ), + 'can_run': ( + request.user is not None + and election.can_be_candidate(request.user) + ), } if voting_interface_enabled: - ctx.update({ - 'started_voting': election.has_voted(request.user), - 'finished_voting': False - }) + ctx.update( + { + 'started_voting': election.has_voted(request.user), + 'finished_voting': False, + } + ) return render(request, 'election/election_view.html', ctx) def election_list(request, polity_id): polity = get_object_or_404(Polity, id=polity_id) - elections = Election.objects.filter( - polity=polity - ).annotate( - candidate_count=Count('candidate') - ).order_by( - '-deadline_votes' + elections = ( + Election.objects.filter(polity=polity) + .annotate(candidate_count=Count('candidate')) + .order_by('-deadline_votes') ) ctx = { diff --git a/emailconfirmation/migrations/0001_initial.py b/emailconfirmation/migrations/0001_initial.py index 87f3e636..f8011e2c 100644 --- a/emailconfirmation/migrations/0001_initial.py +++ b/emailconfirmation/migrations/0001_initial.py @@ -19,12 +19,33 @@ class Migration(migrations.Migration): migrations.CreateModel( name='EmailConfirmation', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('key', models.CharField(max_length=40)), ('timing_created', models.DateTimeField(auto_now=True)), - ('action', models.CharField(choices=[('email_change', 'Email change')], max_length=30)), + ( + 'action', + models.CharField( + choices=[('email_change', 'Email change')], + max_length=30, + ), + ), ('data', models.CharField(max_length=100, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_confirmations', to=settings.AUTH_USER_MODEL)), + ( + 'user', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='email_confirmations', + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/emailconfirmation/models.py b/emailconfirmation/models.py index 1c599a34..b025277d 100644 --- a/emailconfirmation/models.py +++ b/emailconfirmation/models.py @@ -17,11 +17,13 @@ class EmailConfirmation(models.Model): - ACTIONS = ( - ('email_change', _('Email change')), - ) + ACTIONS = (('email_change', _('Email change')),) - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='email_confirmations', on_delete=CASCADE) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + related_name='email_confirmations', + on_delete=CASCADE, + ) key = models.CharField(max_length=40) timing_created = models.DateTimeField(auto_now=True) @@ -34,7 +36,7 @@ def save(self, send=False, *args, **kwargs): if self.key == '': # Automatically generate a unique key, but make sure that it isn't # already in use. - key = self.generate_key(); + key = self.generate_key() while EmailConfirmation.objects.filter(key=key).count() > 0: key = self.generate_key() self.key = key @@ -42,14 +44,18 @@ def save(self, send=False, *args, **kwargs): super(EmailConfirmation, self).save(*args, **kwargs) def generate_key(self): - random_string = get_random_string(length=32, allowed_chars=string.printable) + random_string = get_random_string( + length=32, allowed_chars=string.printable + ) return hashlib.sha1(random_string.encode('utf-8')).hexdigest() def send(self, request): action_msg = dict(self.ACTIONS)[self.action] subject = '%s%s' % (settings.EMAIL_SUBJECT_PREFIX, action_msg) - confirmation_url = request.build_absolute_uri(reverse('email_confirmation', args=(self.key,))) + confirmation_url = request.build_absolute_uri( + reverse('email_confirmation', args=(self.key,)) + ) email = self.data if self.action == 'email_change' else self.user.email ctx = { diff --git a/emailconfirmation/urls.py b/emailconfirmation/urls.py index 5f222b47..1cd272e7 100644 --- a/emailconfirmation/urls.py +++ b/emailconfirmation/urls.py @@ -3,5 +3,9 @@ from emailconfirmation.views import email_confirmation urlpatterns = [ - url(r'^email-confirmation/(?P[a-zA-Z0-9]{40})/$', email_confirmation, name='email_confirmation'), + url( + r'^email-confirmation/(?P[a-zA-Z0-9]{40})/$', + email_confirmation, + name='email_confirmation', + ), ] diff --git a/emailconfirmation/views.py b/emailconfirmation/views.py index f3ee0918..091dcf99 100644 --- a/emailconfirmation/views.py +++ b/emailconfirmation/views.py @@ -13,6 +13,7 @@ from gateway.utils import update_member + def email_confirmation(request, key): try: con = EmailConfirmation.objects.get(key=key) @@ -42,7 +43,9 @@ def email_confirmation(request, key): EmailConfirmation.objects.filter(user=con.user, action=con.action).delete() # Clean up expired confirmation requests, since we're here. - EmailConfirmation.objects.filter(timing_created__lt=timezone.now() - timedelta(days=1)).delete() + EmailConfirmation.objects.filter( + timing_created__lt=timezone.now() - timedelta(days=1) + ).delete() ctx = { 'action_msg': action_msg, @@ -51,4 +54,3 @@ def email_confirmation(request, key): 'return_name': return_name, } return render(request, 'emailconfirmation/confirmed.html', ctx) - diff --git a/gateway/apps.py b/gateway/apps.py index fbe175aa..9330abb7 100644 --- a/gateway/apps.py +++ b/gateway/apps.py @@ -1,9 +1,9 @@ from django.apps import AppConfig + class IcePirateGatewayConfig(AppConfig): name = 'gateway' verbose_name = 'IcePirate Gateway' def ready(self): import gateway.signals - diff --git a/gateway/register.py b/gateway/register.py index 44943fe9..b87a0fd8 100644 --- a/gateway/register.py +++ b/gateway/register.py @@ -11,13 +11,15 @@ class PreverifiedRegistrationView(RegistrationView): A registration backend which accepts e-mail addresses which have been validated by icepirate and immediately creates a user. """ + SIG_VALIDITY = 31 * 24 * 3600 def _make_email_sig(self, email, when=None): ts = '%x/' % (when or time.time()) key = settings.ICEPIRATE['key'] - return ts + hashlib.sha1( - '%s:%s%s:%s' % (key, ts, email, key)).hexdigest() + return ( + ts + hashlib.sha1('%s:%s%s:%s' % (key, ts, email, key)).hexdigest() + ) def _email_sig_is_ok(self, email, email_sig): try: @@ -31,7 +33,7 @@ def _email_sig_is_ok(self, email, email_sig): def registration_allowed(self): # Check if there is an email_sig variable that correctly signs - # the provided e-mail address. + # the provided e-mail address. email = self.request.GET.get('email') email2 = self.request.POST.get('email') @@ -39,8 +41,9 @@ def registration_allowed(self): if email2 is None and email_sig is None: return True - elif (email2 in (None, email) - and self._email_sig_is_ok(email, email_sig)): + elif email2 in (None, email) and self._email_sig_is_ok( + email, email_sig + ): return RegistrationView.registration_allowed(self) else: return False diff --git a/gateway/signals.py b/gateway/signals.py index c9b5b305..b3b070ea 100644 --- a/gateway/signals.py +++ b/gateway/signals.py @@ -59,18 +59,20 @@ def verified_sync(sender, user, request, **kwargs): success, member, error = get_member(user.userprofile.verified_ssn) # Have any of these values changed? - changed = any([ - member['email'] != user.email, - member['email_wanted'] != user.userprofile.email_wanted, - member['username'] != user.username - ]) + changed = any( + [ + member['email'] != user.email, + member['email_wanted'] != user.userprofile.email_wanted, + member['username'] != user.username, + ] + ) if changed: # If so, we'll update the member registry, because we've just # verified our account here and we'll know this information better # than the registry, if they differ. success, member, error = update_member(user) - if success: # Success may have changed since last time we asked. + if success: # Success may have changed since last time we asked. apply_member_locally(member, user) except IcePirateException as e: diff --git a/gateway/utils.py b/gateway/utils.py index 6c4fc7cd..4fb39a2f 100644 --- a/gateway/utils.py +++ b/gateway/utils.py @@ -14,7 +14,6 @@ def user_to_member_args(user): info = { 'json_api_key': settings.ICEPIRATE['key'], - 'ssn': user.userprofile.verified_ssn, 'name': user.userprofile.verified_name, 'email': user.email, @@ -27,7 +26,13 @@ def user_to_member_args(user): # If email_wanted is None, then we still don't know the user's preference, # so we'll say nothing about it. if user.userprofile.email_wanted is not None: - info.update({'email_wanted': 'true' if user.userprofile.email_wanted else 'false'}) + info.update( + { + 'email_wanted': 'true' + if user.userprofile.email_wanted + else 'false' + } + ) return info @@ -45,7 +50,9 @@ def response_to_results(response): error = remote_data['error'] if 'error' in remote_data else None if error is not None: - raise IcePirateException('Error in communication with member database: %s' % error, error) + raise IcePirateException( + 'Error in communication with member database: %s' % error, error + ) # TODO: The success-indicator and error are redundant because we are now # throwing an exception when something goes wrong. @@ -57,9 +64,13 @@ def add_member(user): data = user_to_member_args(user) try: - response = requests.post('%s/member/api/add/' % settings.ICEPIRATE['url'], data=data) + response = requests.post( + '%s/member/api/add/' % settings.ICEPIRATE['url'], data=data + ) except: - raise IcePirateException('Failed adding member to remote member registry') + raise IcePirateException( + 'Failed adding member to remote member registry' + ) return response_to_results(response) @@ -70,11 +81,14 @@ def update_member(user): try: response = requests.post( - '%s/member/api/update/ssn/%s/' % (settings.ICEPIRATE['url'], user.userprofile.verified_ssn), - data=data + '%s/member/api/update/ssn/%s/' + % (settings.ICEPIRATE['url'], user.userprofile.verified_ssn), + data=data, ) except: - raise IcePirateException('Failed updating member in remote member registry') + raise IcePirateException( + 'Failed updating member in remote member registry' + ) return response_to_results(response) @@ -84,10 +98,12 @@ def get_member(ssn): try: response = requests.post( '%s/member/api/get/ssn/%s/' % (settings.ICEPIRATE['url'], ssn), - data={'json_api_key': settings.ICEPIRATE['key']} + data={'json_api_key': settings.ICEPIRATE['key']}, ) except: - raise IcePirateException('Failed getting member from remote member registry') + raise IcePirateException( + 'Failed getting member from remote member registry' + ) return response_to_results(response) @@ -96,17 +112,17 @@ def add_member_to_membergroup(user, polity): try: response = requests.post( - '%s/member/api/add-to-membergroup/%s/' % ( - settings.ICEPIRATE['url'], - user.userprofile.verified_ssn - ), + '%s/member/api/add-to-membergroup/%s/' + % (settings.ICEPIRATE['url'], user.userprofile.verified_ssn), data={ 'json_api_key': settings.ICEPIRATE['key'], 'membergroup_techname': polity.slug, - } + }, ) except: - raise IcePirateException('Failed getting member from remote member registry') + raise IcePirateException( + 'Failed getting member from remote member registry' + ) return response_to_results(response) @@ -122,8 +138,7 @@ def apply_member_locally(member, user): # Add user to polities according to remote user's groups, as well as # front polity, if one is designated. membership_polities = Polity.objects.filter( - Q(slug__in=member['groups'].keys()) - | Q(is_front_polity=True) + Q(slug__in=member['groups'].keys()) | Q(is_front_polity=True) ) for polity in membership_polities: polity.members.add(user) @@ -140,10 +155,14 @@ def apply_member_locally(member, user): polity.officers.remove(user) # Sync info on polity eligibility of user. - polities_eligible = Polity.objects.filter(slug__in=member['eligible_groups']) + polities_eligible = Polity.objects.filter( + slug__in=member['eligible_groups'] + ) for polity in polities_eligible: polity.eligibles.add(user) - for polity in user.polities_eligible.exclude(id__in=[p.id for p in polities_eligible]): + for polity in user.polities_eligible.exclude( + id__in=[p.id for p in polities_eligible] + ): polity.eligibles.remove(user) # Keep track of whether we need to save the profile. diff --git a/initial_setup.py b/initial_setup.py index 73e688eb..4679b6c9 100755 --- a/initial_setup.py +++ b/initial_setup.py @@ -13,20 +13,24 @@ from sys import argv import random + random = random.SystemRandom() TERMINAL_WIDTH = 80 venv_path = os.path.relpath(os.path.dirname(sys.executable), sys.path[0]) + def get_executable_path(executable): if len(argv) > 1 and argv[1] == '--venv': return os.path.join(venv_path, executable) return executable -def get_random_string(length=12, - allowed_chars='abcdefghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): +def get_random_string( + length=12, + allowed_chars='abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', +): """ Returns a securely generated random string. @@ -48,7 +52,7 @@ def get_secret_key(): return get_random_string(50, chars) -def get_answer(question, proper_answers=('yes','no')): +def get_answer(question, proper_answers=('yes', 'no')): """ Ask a question and keep asking until a proper answer is received. @@ -62,19 +66,34 @@ def get_answer(question, proper_answers=('yes','no')): return answer -print "*" * TERMINAL_WIDTH -print "Setting up Wasa2il from scratch with an SQLite database called 'wasa2il.sqlite'." -print "This script assumes that both Python and Pip are installed." -print "*" * TERMINAL_WIDTH +print("*" * TERMINAL_WIDTH) +print( + "Setting up Wasa2il from scratch with an SQLite database called 'wasa2il.sqlite'." +) +print("This script assumes that both Python and Pip are installed.") +print("*" * TERMINAL_WIDTH) # Install (or upgrade) Python package dependencies stdout.write('Installing dependencies:\n') -result = subprocess.call([get_executable_path("pip"), "install", "--upgrade", "-r", "requirements.txt"]) +result = subprocess.call( + [ + get_executable_path("pip"), + "install", + "--upgrade", + "-r", + "requirements.txt", + ] +) if result == 0: stdout.write('Dependency installation complete.\n') else: - if get_answer('Dependency installation seems to have failed. Continue anyway? (yes/no): ') != 'yes': + if ( + get_answer( + 'Dependency installation seems to have failed. Continue anyway? (yes/no): ' + ) + != 'yes' + ): stdout.write('Okay, quitting.\n') quit(1) @@ -84,10 +103,16 @@ def get_answer(question, proper_answers=('yes','no')): stdout.write('Checking if Django is installed...') stdout.flush() import django - stdout.write(' yes (%d.%d.%d)\n' % (django.VERSION[0], django.VERSION[1], django.VERSION[2])) + + stdout.write( + ' yes (%d.%d.%d)\n' + % (django.VERSION[0], django.VERSION[1], django.VERSION[2]) + ) except ImportError: stdout.write(' no\n') - stderr.write('Error: Cannot continue without Django which should be installed but is not. Quitting.\n') + stderr.write( + 'Error: Cannot continue without Django which should be installed but is not. Quitting.\n' + ) quit(2) @@ -98,7 +123,9 @@ def get_answer(question, proper_answers=('yes','no')): stdout.write('Creating local settings file (local_settings.py)...') stdout.flush() try: - shutil.copy('wasa2il/local_settings.py-example', 'wasa2il/local_settings.py') + shutil.copy( + 'wasa2il/local_settings.py-example', 'wasa2il/local_settings.py' + ) stdout.write(' done\n') except IOError as e: stdout.write(' failed\n') @@ -113,34 +140,45 @@ def get_answer(question, proper_answers=('yes','no')): if line.startswith("SECRET_KEY = ''"): stdout.write('- Setting secret key to random string...') stdout.flush() - print "SECRET_KEY = '%s'" % get_secret_key() + print("SECRET_KEY = '%s'" % get_secret_key()) stdout.write(' done\n') local_settings_changed = True elif line.startswith("DATABASE_ENGINE = 'django.db.backends.'"): stdout.write('- Configuring database engine (SQLite)...') stdout.flush() - print "DATABASE_ENGINE = 'django.db.backends.sqlite3'" + print("DATABASE_ENGINE = 'django.db.backends.sqlite3'") stdout.write(' done\n') local_settings_changed = True elif line.startswith("DATABASE_NAME = ''"): stdout.write('- Setting database name (wasa2il.sqlite)...') stdout.flush() - print "DATABASE_NAME = 'wasa2il.sqlite'" + print("DATABASE_NAME = 'wasa2il.sqlite'") stdout.write(' done\n') local_settings_changed = True else: - print line.strip() + print(line.strip()) if not local_settings_changed: stdout.write('- No changes needed.\n') # Compile the translation files -subprocess.call([get_executable_path('python'), os.path.join(os.getcwd(), 'manage.py'), 'compilemessages']) +subprocess.call( + [ + get_executable_path('python'), + os.path.join(os.getcwd(), 'manage.py'), + 'compilemessages', + ] +) # Setup database if needed create_database = False if os.path.exists('wasa2il.sqlite'): - if get_answer('SQLite database already exists. Kill it and start over? (yes/no): ') == 'yes': + if ( + get_answer( + 'SQLite database already exists. Kill it and start over? (yes/no): ' + ) + == 'yes' + ): stdout.write('Deleting wasa2il.sqlite...') stdout.flush() os.remove('wasa2il.sqlite') @@ -151,28 +189,46 @@ def get_answer(question, proper_answers=('yes','no')): if create_database: stdout.write('Setting up database (via "migrate"):\n') - migrate_result = subprocess.call([get_executable_path('python'), os.path.join(os.getcwd(), 'manage.py'), 'migrate']) + migrate_result = subprocess.call( + [ + get_executable_path('python'), + os.path.join(os.getcwd(), 'manage.py'), + 'migrate', + ] + ) if migrate_result != 0: stderr.write('Error: Django migration gave errors. Quitting.\n') quit(1) - stdout.write('We will now create a superuser to configure polities within Wasa2il once it has been set up.\n') - subprocess.call([get_executable_path('python'), os.path.join(os.getcwd(), 'manage.py'), 'createsuperuser']) + stdout.write( + 'We will now create a superuser to configure polities within Wasa2il once it has been set up.\n' + ) + subprocess.call( + [ + get_executable_path('python'), + os.path.join(os.getcwd(), 'manage.py'), + 'createsuperuser', + ] + ) stdout.write('Populate wasa2il with some fake data...\n') - subprocess.call([get_executable_path('python'), os.path.join(os.getcwd(), 'manage.py'), 'load_fake_data']) - - -print "*" * TERMINAL_WIDTH -print "All done!" -print "To run Wasa2il and start configuring polities, follow these steps:" -print "- Run '"+get_executable_path('python')+" manage.py runserver'" -print "- Open your favorite browser and type in: http://localhost:8000" -print "- Log in with the superuser account created previously" -print -print "Additional superuser accounts can be created with" -print "- "+get_executable_path('python')+" manage.py createsuperuser" -print "*" * TERMINAL_WIDTH - - + subprocess.call( + [ + get_executable_path('python'), + os.path.join(os.getcwd(), 'manage.py'), + 'load_fake_data', + ] + ) + + +print("*" * TERMINAL_WIDTH) +print("All done!") +print("To run Wasa2il and start configuring polities, follow these steps:") +print("- Run '" + get_executable_path('python') + " manage.py runserver'") +print("- Open your favorite browser and type in: http://localhost:8000") +print("- Log in with the superuser account created previously") +print() +print("Additional superuser accounts can be created with") +print("- " + get_executable_path('python') + " manage.py createsuperuser") +print("*" * TERMINAL_WIDTH) diff --git a/issue/__init__.py b/issue/__init__.py index 52586a4f..fd3c7035 100644 --- a/issue/__init__.py +++ b/issue/__init__.py @@ -1,6 +1,5 @@ - - def heartbeat(t): from issue.models import Issue + # m = Issue.objects.filter(issue_state='') pass diff --git a/issue/admin.py b/issue/admin.py index 6492fa26..57600cf2 100644 --- a/issue/admin.py +++ b/issue/admin.py @@ -9,7 +9,12 @@ class IssueAdmin(admin.ModelAdmin): fieldsets = None list_display = ['name', 'slug', 'description'] - exclude = ['votecount', 'votecount_yes', 'votecount_abstain', 'votecount_no'] + exclude = [ + 'votecount', + 'votecount_yes', + 'votecount_abstain', + 'votecount_no', + ] class DocumentAdmin(admin.ModelAdmin): diff --git a/issue/dataviews.py b/issue/dataviews.py index c1521286..bbd1340d 100644 --- a/issue/dataviews.py +++ b/issue/dataviews.py @@ -23,6 +23,7 @@ from polity.models import Polity + @login_required @jsonize def issue_vote(request): @@ -45,7 +46,9 @@ def issue_vote(request): vote.save() # Update vote counts - issue.votecount = issue.votecount_yes = issue.votecount_abstain = issue.votecount_no = 0 + issue.votecount = ( + issue.votecount_yes + ) = issue.votecount_abstain = issue.votecount_no = 0 votes = issue.vote_set.all() for vote in votes: if vote.value == 1: @@ -85,13 +88,15 @@ def issue_poll(request): "id": comment.id, "created_by": comment.created_by.username, "created_by_thumb": thumbnail( - comment.created_by.userprofile.picture, '40x40'), + comment.created_by.userprofile.picture, '40x40' + ), "created": str(comment.created), "created_since": timesince(comment.created), - "comment": comment.comment - } for comment in issue.comment_set.all().order_by("created") + "comment": comment.comment, + } + for comment in issue.comment_set.all().order_by("created") ] - ctx["issue"] = {"comments": comments, "votecount": issue.votecount } + ctx["issue"] = {"comments": comments, "votecount": issue.votecount} if issue.issue_state() == 'concluded': ctx["issue"]["votecount_abstain"] = issue.votecount_abstain ctx["ok"] = True @@ -109,7 +114,7 @@ def issue_showclosed(request): ctx = {} polity_id = int(request.GET.get('polity_id', 0)) - showclosed = int(request.GET.get('showclosed', 0)) # 0 = False, 1 = True + showclosed = int(request.GET.get('showclosed', 0)) # 0 = False, 1 = True try: issues = Issue.objects.select_related('polity') @@ -134,10 +139,16 @@ def issue_showclosed(request): } ctx['showclosed'] = showclosed - ctx['html'] = render_to_string('issue/_issues_recent_table.html', html_ctx) + ctx['html'] = render_to_string( + 'issue/_issues_recent_table.html', html_ctx + ) ctx['ok'] = True except Exception as e: - ctx['error'] = e.__str__() if settings.DEBUG else 'Error raised. Turn on DEBUG for details.' + ctx['error'] = ( + e.__str__() + if settings.DEBUG + else 'Error raised. Turn on DEBUG for details.' + ) return ctx @@ -163,9 +174,15 @@ def documentcontent_render_diff(request): def documentcontent_retract(request, documentcontent_id): # Only polity officers and the documentcontent's author are allowed to do this. try: - documentcontent = DocumentContent.objects.select_related('issue').distinct().exclude(issue=None).get( - Q(user_id=request.user.id) | Q(document__polity__officers__id=request.user.id), - id=documentcontent_id + documentcontent = ( + DocumentContent.objects.select_related('issue') + .distinct() + .exclude(issue=None) + .get( + Q(user_id=request.user.id) + | Q(document__polity__officers__id=request.user.id), + id=documentcontent_id, + ) ) except DocumentContent.DoesNotExist: return error('Access denied') diff --git a/issue/forms.py b/issue/forms.py index b09f54f4..68a51309 100644 --- a/issue/forms.py +++ b/issue/forms.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _ + class IssueForm(Wasa2ilForm): class Meta: model = Issue @@ -49,11 +50,18 @@ def clean_text(self): # here, so that the comparison is on an equal footing. text = self.cleaned_data['text'].replace('\r\n', '\n') pred = self.instance.document.preferred_version() - if pred is not None and pred.id != self.instance.id and pred.text.strip() == text.strip(): - raise ValidationError(_('Content must differ from previous version')) + if ( + pred is not None + and pred.id != self.instance.id + and pred.text.strip() == text.strip() + ): + raise ValidationError( + _('Content must differ from previous version') + ) return text + class CommentForm(forms.ModelForm): class Meta: model = Comment diff --git a/issue/migrations/0001_initial.py b/issue/migrations/0001_initial.py index fe9d7e3c..668220e7 100644 --- a/issue/migrations/0001_initial.py +++ b/issue/migrations/0001_initial.py @@ -21,22 +21,74 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Comment', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('created', models.DateTimeField(auto_now_add=True)), ('modified', models.DateTimeField(auto_now=True)), ('comment', models.TextField()), - ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comment_created_by', to=settings.AUTH_USER_MODEL)), + ( + 'created_by', + models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='comment_created_by', + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( name='Document', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='Name')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'name', + models.CharField(max_length=128, verbose_name='Name'), + ), ('slug', models.SlugField(blank=True, max_length=128)), - ('document_type', models.IntegerField(choices=[(1, 'Policy'), (2, 'Bylaw'), (3, 'Motion'), (999, 'Other')], default=1)), - ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + 'document_type', + models.IntegerField( + choices=[ + (1, 'Policy'), + (2, 'Bylaw'), + (3, 'Motion'), + (999, 'Other'), + ], + default=1, + ), + ), + ( + 'polity', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='polity.Polity', + ), + ), + ( + 'user', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ 'ordering': ['-id'], @@ -45,48 +97,197 @@ class Migration(migrations.Migration): migrations.CreateModel( name='DocumentContent', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='Name')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'name', + models.CharField(max_length=128, verbose_name='Name'), + ), ('created', models.DateTimeField(auto_now_add=True)), ('text', models.TextField()), ('order', models.IntegerField(default=1)), ('comments', models.TextField(blank=True)), - ('status', models.CharField(choices=[(b'proposed', 'Proposed'), (b'accepted', 'Accepted'), (b'rejected', 'Rejected'), (b'deprecated', 'Deprecated'), (b'retracted', 'Retracted')], default=b'proposed', max_length=32)), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue.Document')), - ('predecessor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='issue.DocumentContent')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + 'status', + models.CharField( + choices=[ + (b'proposed', 'Proposed'), + (b'accepted', 'Accepted'), + (b'rejected', 'Rejected'), + (b'deprecated', 'Deprecated'), + (b'retracted', 'Retracted'), + ], + default=b'proposed', + max_length=32, + ), + ), + ( + 'document', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='issue.Document', + ), + ), + ( + 'predecessor', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='issue.DocumentContent', + ), + ), + ( + 'user', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( name='Issue', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='A great issue name expresses the essence of a proposal as briefly as possible.', max_length=128, verbose_name='Name')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'name', + models.CharField( + help_text='A great issue name expresses the essence of a proposal as briefly as possible.', + max_length=128, + verbose_name='Name', + ), + ), ('slug', models.SlugField(blank=True, max_length=128)), ('issue_num', models.IntegerField(null=True)), ('issue_year', models.IntegerField(null=True)), - ('description', models.TextField(blank=True, help_text="An issue description is usually just a copy of the proposal's description, but you can customize it here if you so wish.", null=True, verbose_name='Description')), + ( + 'description', + models.TextField( + blank=True, + help_text="An issue description is usually just a copy of the proposal's description, but you can customize it here if you so wish.", + null=True, + verbose_name='Description', + ), + ), ('created', models.DateTimeField(auto_now_add=True)), ('modified', models.DateTimeField(auto_now=True)), - ('deadline_discussions', models.DateTimeField(blank=True, null=True)), - ('deadline_proposals', models.DateTimeField(blank=True, null=True)), - ('deadline_votes', models.DateTimeField(blank=True, null=True)), - ('majority_percentage', models.DecimalField(decimal_places=2, max_digits=5)), + ( + 'deadline_discussions', + models.DateTimeField(blank=True, null=True), + ), + ( + 'deadline_proposals', + models.DateTimeField(blank=True, null=True), + ), + ( + 'deadline_votes', + models.DateTimeField(blank=True, null=True), + ), + ( + 'majority_percentage', + models.DecimalField(decimal_places=2, max_digits=5), + ), ('is_processed', models.BooleanField(default=False)), ('votecount', models.IntegerField(default=0)), ('votecount_yes', models.IntegerField(default=0)), ('votecount_abstain', models.IntegerField(default=0)), ('votecount_no', models.IntegerField(default=0)), - ('special_process', models.CharField(blank=True, choices=[(b'accepted_at_assembly', 'Accepted at assembly'), (b'rejected_at_assembly', 'Rejected at assembly'), (b'retracted', 'Retracted')], default=b'', max_length=32, null=True, verbose_name='Special process')), + ( + 'special_process', + models.CharField( + blank=True, + choices=[ + (b'accepted_at_assembly', 'Accepted at assembly'), + (b'rejected_at_assembly', 'Rejected at assembly'), + (b'retracted', 'Retracted'), + ], + default=b'', + max_length=32, + null=True, + verbose_name='Special process', + ), + ), ('comment_count', models.IntegerField(default=0)), ('archived', models.BooleanField(default=False)), - ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issue_created_by', to=settings.AUTH_USER_MODEL)), - ('documentcontent', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issue', to='issue.DocumentContent')), - ('modified_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issue_modified_by', to=settings.AUTH_USER_MODEL)), - ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')), - ('ruleset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.PolityRuleset', verbose_name='Ruleset')), - ('special_process_set_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='special_process_issues', to=settings.AUTH_USER_MODEL)), - ('topics', models.ManyToManyField(to='topic.Topic', verbose_name='Topics')), + ( + 'created_by', + models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='issue_created_by', + to=settings.AUTH_USER_MODEL, + ), + ), + ( + 'documentcontent', + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='issue', + to='issue.DocumentContent', + ), + ), + ( + 'modified_by', + models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='issue_modified_by', + to=settings.AUTH_USER_MODEL, + ), + ), + ( + 'polity', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='polity.Polity', + ), + ), + ( + 'ruleset', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='polity.PolityRuleset', + verbose_name='Ruleset', + ), + ), + ( + 'special_process_set_by', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='special_process_issues', + to=settings.AUTH_USER_MODEL, + ), + ), + ( + 'topics', + models.ManyToManyField( + to='topic.Topic', verbose_name='Topics' + ), + ), ], options={ 'ordering': ['-deadline_votes'], @@ -95,22 +296,51 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Vote', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('value', models.IntegerField()), ('cast', models.DateTimeField(auto_now_add=True)), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue.Issue')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + 'issue', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='issue.Issue', + ), + ), + ( + 'user', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AddField( model_name='comment', name='issue', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue.Issue'), + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to='issue.Issue' + ), ), migrations.AddField( model_name='comment', name='modified_by', - field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comment_modified_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='comment_modified_by', + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterUniqueTogether( name='vote', diff --git a/issue/migrations/0003_auto_20190822_2006.py b/issue/migrations/0003_auto_20190822_2006.py index 7607a1d5..84ef9c23 100644 --- a/issue/migrations/0003_auto_20190822_2006.py +++ b/issue/migrations/0003_auto_20190822_2006.py @@ -15,61 +15,137 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='comment', name='created_by', - field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comment_created_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='comment_created_by', + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( model_name='comment', name='modified_by', - field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comment_modified_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='comment_modified_by', + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( model_name='document', name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( model_name='documentcontent', name='predecessor', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='issue.DocumentContent'), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='issue.DocumentContent', + ), ), migrations.AlterField( model_name='documentcontent', name='status', - field=models.CharField(choices=[('proposed', 'Proposed'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('deprecated', 'Deprecated'), ('retracted', 'Retracted')], default='proposed', max_length=32), + field=models.CharField( + choices=[ + ('proposed', 'Proposed'), + ('accepted', 'Accepted'), + ('rejected', 'Rejected'), + ('deprecated', 'Deprecated'), + ('retracted', 'Retracted'), + ], + default='proposed', + max_length=32, + ), ), migrations.AlterField( model_name='documentcontent', name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( model_name='issue', name='created_by', - field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='issue_created_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='issue_created_by', + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( model_name='issue', name='documentcontent', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='issue', to='issue.DocumentContent'), + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='issue', + to='issue.DocumentContent', + ), ), migrations.AlterField( model_name='issue', name='modified_by', - field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='issue_modified_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='issue_modified_by', + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( model_name='issue', name='ruleset', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='polity.PolityRuleset', verbose_name='Ruleset'), + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to='polity.PolityRuleset', + verbose_name='Ruleset', + ), ), migrations.AlterField( model_name='issue', name='special_process', - field=models.CharField(blank=True, choices=[('accepted_at_assembly', 'Accepted at assembly'), ('rejected_at_assembly', 'Rejected at assembly'), ('retracted', 'Retracted')], default='', max_length=32, null=True, verbose_name='Special process'), + field=models.CharField( + blank=True, + choices=[ + ('accepted_at_assembly', 'Accepted at assembly'), + ('rejected_at_assembly', 'Rejected at assembly'), + ('retracted', 'Retracted'), + ], + default='', + max_length=32, + null=True, + verbose_name='Special process', + ), ), migrations.AlterField( model_name='issue', name='special_process_set_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='special_process_issues', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='special_process_issues', + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/issue/models.py b/issue/models.py index 35a5e226..44f9fe25 100644 --- a/issue/models.py +++ b/issue/models.py @@ -18,14 +18,19 @@ # seem to exist in QuerySet, we cannot simply use IssueQuerySet.as_manager() # as is commonly recommended. Instead, this class is used as an extra # constructor for both IssueManager and IssueQuerySet. -class IssueMixin(): +class IssueMixin: def recent(self): - return self.filter(deadline_votes__gt=timezone.now() - timezone.timedelta(days=settings.RECENT_ISSUE_DAYS)) + return self.filter( + deadline_votes__gt=timezone.now() + - timezone.timedelta(days=settings.RECENT_ISSUE_DAYS) + ) + class IssueManager(models.Manager, IssueMixin): def get_queryset(self): return IssueQuerySet(self.model, using=self._db).filter(archived=False) + # Inherits from IssueMixin. class IssueQuerySet(models.QuerySet, IssueMixin): pass @@ -49,17 +54,26 @@ class Issue(models.Model): ('discussion', _('In discussion')), ) - name = models.CharField(max_length=128, verbose_name=_('Name'), help_text=_( - 'A great issue name expresses the essence of a proposal as briefly as possible.' - )) + name = models.CharField( + max_length=128, + verbose_name=_('Name'), + help_text=_( + 'A great issue name expresses the essence of a proposal as briefly as possible.' + ), + ) slug = models.SlugField(max_length=128, blank=True) issue_num = models.IntegerField(null=True) issue_year = models.IntegerField(null=True) - description = models.TextField(verbose_name=_("Description"), null=True, blank=True, help_text=_( - 'An issue description is usually just a copy of the proposal\'s description, but you can customize it here if you so wish.' - )) + description = models.TextField( + verbose_name=_("Description"), + null=True, + blank=True, + help_text=_( + 'An issue description is usually just a copy of the proposal\'s description, but you can customize it here if you so wish.' + ), + ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -67,7 +81,7 @@ class Issue(models.Model): null=True, blank=True, related_name='issue_created_by', - on_delete=PROTECT + on_delete=PROTECT, ) modified_by = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -75,7 +89,7 @@ class Issue(models.Model): null=True, blank=True, related_name='issue_modified_by', - on_delete=PROTECT + on_delete=PROTECT, ) created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) @@ -88,13 +102,18 @@ class Issue(models.Model): related_name='issue', null=True, blank=True, - on_delete=PROTECT + on_delete=PROTECT, ) deadline_discussions = models.DateTimeField(null=True, blank=True) deadline_proposals = models.DateTimeField(null=True, blank=True) deadline_votes = models.DateTimeField(null=True, blank=True) majority_percentage = models.DecimalField(max_digits=5, decimal_places=2) - ruleset = models.ForeignKey('polity.PolityRuleset', verbose_name=_("Ruleset"), editable=True, on_delete=PROTECT) + ruleset = models.ForeignKey( + 'polity.PolityRuleset', + verbose_name=_("Ruleset"), + editable=True, + on_delete=PROTECT, + ) is_processed = models.BooleanField(default=False) votecount = models.IntegerField(default=0) @@ -116,14 +135,14 @@ class Issue(models.Model): choices=SPECIAL_PROCESS_CHOICES, default='', null=True, - blank=True + blank=True, ) special_process_set_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, related_name='special_process_issues', - on_delete=PROTECT + on_delete=PROTECT, ) comment_count = models.IntegerField(default=0) @@ -145,10 +164,15 @@ def save(self, *args, **kwargs): with transaction.atomic(): try: - self.issue_num = Issue.objects.filter( - polity_id=self.polity_id, - issue_year=self.issue_year - ).order_by('-issue_num')[0].issue_num + 1 + self.issue_num = ( + Issue.objects.filter( + polity_id=self.polity_id, + issue_year=self.issue_year, + ) + .order_by('-issue_num')[0] + .issue_num + + 1 + ) except IndexError: self.issue_num = 1 @@ -174,7 +198,9 @@ def apply_ruleset(self, now=None): self.deadline_proposals = now self.deadline_votes = now else: - self.deadline_discussions = now + self.ruleset.issue_discussion_time + self.deadline_discussions = ( + now + self.ruleset.issue_discussion_time + ) self.deadline_proposals = now + self.ruleset.issue_proposal_time self.deadline_votes = now + self.ruleset.issue_vote_time @@ -264,19 +290,25 @@ def process(self): if self.polity.push_on_vote_end: # Forcing translation string creation. __ = _("Voting closed on issue '%s'.") - push_send_notification_to_polity_users(issue.polity.id, "Voting closed on issue '%s'.", [issue.name]) + push_send_notification_to_polity_users( + issue.polity.id, + "Voting closed on issue '%s'.", + [issue.name], + ) return True - def get_voters(self): # FIXME: This is one place to check if we've invited other groups to # participate in an election, if we implement that feature... return self.polity.issue_voters() def can_vote(self, user=None, user_id=None): - return self.get_voters().filter( - id=(user_id if (user_id is not None) else user.id)).exists() + return ( + self.get_voters() + .filter(id=(user_id if (user_id is not None) else user.id)) + .exists() + ) def user_documents(self, user): try: @@ -294,12 +326,19 @@ def majority_reached(self): result = True else: if self.votecount > 0: - result = float(self.votecount_yes) / self.votecount > float(self.majority_percentage) / 100 + result = ( + float(self.votecount_yes) / self.votecount + > float(self.majority_percentage) / 100 + ) return result def get_majority_reached_display(self): - return _('Accepted').__str__() if self.majority_reached() else _('Rejected').__str__() + return ( + _('Accepted').__str__() + if self.majority_reached() + else _('Rejected').__str__() + ) def update_comment_count(self): self.comment_count = self.comment_set.count() @@ -316,7 +355,7 @@ class Vote(models.Model): cast = models.DateTimeField(auto_now_add=True) class Meta: - unique_together = (('user', 'issue')) + unique_together = ('user', 'issue') class Comment(models.Model): @@ -326,7 +365,7 @@ class Comment(models.Model): null=True, blank=True, related_name='comment_created_by', - on_delete=SET_NULL + on_delete=SET_NULL, ) modified_by = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -334,7 +373,7 @@ class Comment(models.Model): null=True, blank=True, related_name='comment_modified_by', - on_delete=SET_NULL + on_delete=SET_NULL, ) created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) @@ -368,7 +407,9 @@ class Document(models.Model): name = models.CharField(max_length=128, verbose_name=_('Name')) slug = models.SlugField(max_length=128, blank=True) - document_type = models.IntegerField(choices=DOCUMENT_TYPE_CHOICES, default=1) + document_type = models.IntegerField( + choices=DOCUMENT_TYPE_CHOICES, default=1 + ) polity = models.ForeignKey('polity.Polity', on_delete=CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=PROTECT) @@ -386,17 +427,23 @@ def get_versions(self): # TODO: Make this faster and cached per request. Preferably still Pythonic. -helgi@binary.is, 2014-07-02 def preferred_version(self): # Latest accepted version... - accepted_versions = self.documentcontent_set.filter(status='accepted').order_by('-order') + accepted_versions = self.documentcontent_set.filter( + status='accepted' + ).order_by('-order') if accepted_versions.count() > 0: return accepted_versions[0] # ...and if none are found, find the earliest proposed one... - proposed_versions = self.documentcontent_set.filter(status='proposed').order_by('order') + proposed_versions = self.documentcontent_set.filter( + status='proposed' + ).order_by('order') if proposed_versions.count() > 0: return proposed_versions[0] # ...boo, go for the first rejected one? - rejected_versions = self.documentcontent_set.filter(status='rejected').order_by('order') + rejected_versions = self.documentcontent_set.filter( + status='rejected' + ).order_by('order') if rejected_versions.count() > 0: return rejected_versions[0] @@ -410,12 +457,13 @@ def preferred_version(self): # Returns true if a documentcontent in this document already has an issue in progress. def has_open_issue(self): documentcontent_ids = [dc.id for dc in self.documentcontent_set.all()] - count = Issue.objects.filter( - is_processed=False, - documentcontent_id__in=documentcontent_ids - ).exclude( - special_process='retracted' - ).count() + count = ( + Issue.objects.filter( + is_processed=False, documentcontent_id__in=documentcontent_ids + ) + .exclude(special_process='retracted') + .count() + ) return count > 0 def __str__(self): @@ -437,8 +485,12 @@ class DocumentContent(models.Model): ('deprecated', _('Deprecated')), ('retracted', _('Retracted')), ) - status = models.CharField(max_length=32, choices=STATUS_CHOICES, default='proposed') - predecessor = models.ForeignKey('issue.DocumentContent', null=True, blank=True, on_delete=SET_NULL) + status = models.CharField( + max_length=32, choices=STATUS_CHOICES, default='proposed' + ) + predecessor = models.ForeignKey( + 'issue.DocumentContent', null=True, blank=True, on_delete=SET_NULL + ) class Meta: unique_together = ['document', 'order'] @@ -451,9 +503,13 @@ def previous_topics(self): # This function actually regards Issues, not DocumentContents, but is determined by DocumentContent as input. # Find the last accepted documentcontent - prev_contents = self.document.documentcontent_set.exclude(id=self.id).order_by('-order') + prev_contents = self.document.documentcontent_set.exclude( + id=self.id + ).order_by('-order') selected_topics = [] - for c in prev_contents: # NOTE: We're iterating from newest to oldest. + for ( + c + ) in prev_contents: # NOTE: We're iterating from newest to oldest. if c.status == 'accepted': # A previously accepted DocumentContent MUST correspond to an issue so we brutally assume so. selected_topics = [t.id for t in c.issue.topics.all()] @@ -465,7 +521,7 @@ def previous_topics(self): try: c_issue = c.issue.get() selected_topics = [t.id for t in c_issue.topics.all()] - break; + break except: pass @@ -473,7 +529,9 @@ def previous_topics(self): # Gets all DocumentContents which belong to the Document to which this DocumentContent belongs to. def siblings(self): - siblings = DocumentContent.objects.filter(document_id=self.document_id).order_by('order') + siblings = DocumentContent.objects.filter( + document_id=self.document_id + ).order_by('order') return siblings # Generates a diff between this DocumentContent and the one provided to the function. @@ -492,7 +550,9 @@ def diff(self, documentcontent_id): dmp.diff_cleanupSemantic(d) result = dmp.diff_prettyHtml(d).replace('¶', '') - result = re.sub(r'\r
', r'
', result) # Because we're using
 in the template, so the HTML creates two newlines.
+        result = re.sub(
+            r'\r
', r'
', result + ) # Because we're using
 in the template, so the HTML creates two newlines.
 
         return result
 
diff --git a/issue/urls.py b/issue/urls.py
index d1b84a1a..f5e28898 100644
--- a/issue/urls.py
+++ b/issue/urls.py
@@ -20,24 +20,55 @@
 from issue.views import issues
 
 urlpatterns = [
-
     url(r'^polity/(?P\d+)/issues/$', issues, name='issues'),
-    url(r'^polity/(?P\d+)/issue/(?P\d+)/edit/$', issue_add_edit, name='issue_edit'),
-    url(r'^polity/(?P\d+)/issue/new/(documentcontent/(?P\d+)/)?$', issue_add_edit, name='issue_add'),
-    url(r'^polity/(?P\d+)/issue/(?P\d+)/$', never_cache(issue_view), name='issue'),
-
-    url(r'^polity/(?P\d+)/agreements/$', document_agreements, name="agreements"),
+    url(
+        r'^polity/(?P\d+)/issue/(?P\d+)/edit/$',
+        issue_add_edit,
+        name='issue_edit',
+    ),
+    url(
+        r'^polity/(?P\d+)/issue/new/(documentcontent/(?P\d+)/)?$',
+        issue_add_edit,
+        name='issue_add',
+    ),
+    url(
+        r'^polity/(?P\d+)/issue/(?P\d+)/$',
+        never_cache(issue_view),
+        name='issue',
+    ),
+    url(
+        r'^polity/(?P\d+)/agreements/$',
+        document_agreements,
+        name="agreements",
+    ),
     url(r'^polity/(?P\d+)/document/new/$', document_add),
-    url(r'^polity/(?P\d+)/document/(?P\d+)/v(?P\d+)/$', document_view, name='document_view'),
-    url(r'^polity/(?P\d+)/document/(?P\d+)/v(?P\d+)/edit/$', documentcontent_edit, name='documentcontent_edit'),
-    url(r'^polity/(?P\d+)/document/(?P\d+)/new/$', documentcontent_add, name='documentcontent_add'),
-    url(r'^polity/(?P\d+)/document/(?P\d+)/$', document_view, name='document'),
-
+    url(
+        r'^polity/(?P\d+)/document/(?P\d+)/v(?P\d+)/$',
+        document_view,
+        name='document_view',
+    ),
+    url(
+        r'^polity/(?P\d+)/document/(?P\d+)/v(?P\d+)/edit/$',
+        documentcontent_edit,
+        name='documentcontent_edit',
+    ),
+    url(
+        r'^polity/(?P\d+)/document/(?P\d+)/new/$',
+        documentcontent_add,
+        name='documentcontent_add',
+    ),
+    url(
+        r'^polity/(?P\d+)/document/(?P\d+)/$',
+        document_view,
+        name='document',
+    ),
     url(r'^api/issue/comment/send/$', never_cache(issue_comment_send)),
     url(r'^api/issue/poll/$', never_cache(issue_poll)),
     url(r'^api/issue/vote/$', never_cache(issue_vote)),
     url(r'^api/issue/showclosed/$', issue_showclosed),
-
     url(r'^api/documentcontent/render-diff/$', documentcontent_render_diff),
-    url(r'^api/documentcontent/(?P\d+)/retract/$', documentcontent_retract),
+    url(
+        r'^api/documentcontent/(?P\d+)/retract/$',
+        documentcontent_retract,
+    ),
 ]
diff --git a/issue/views.py b/issue/views.py
index 9dd65874..60663d49 100644
--- a/issue/views.py
+++ b/issue/views.py
@@ -29,7 +29,10 @@ def issue_add_edit(request, polity_id, issue_id=None, documentcontent_id=None):
     polity = get_object_or_404(Polity, id=polity_id)
 
     # Make sure that user is allowed to do this.
-    if polity.is_newissue_only_officers and request.user not in polity.officers.all():
+    if (
+        polity.is_newissue_only_officers
+        and request.user not in polity.officers.all()
+    ):
         raise PermissionDenied()
 
     if issue_id:
@@ -43,7 +46,9 @@ def issue_add_edit(request, polity_id, issue_id=None, documentcontent_id=None):
         issue = Issue(polity=polity)
         if documentcontent_id:
             try:
-                current_content = DocumentContent.objects.select_related('document').get(id=documentcontent_id)
+                current_content = DocumentContent.objects.select_related(
+                    'document'
+                ).get(id=documentcontent_id)
             except DocumentContent.DoesNotExist:
                 raise Http404
         else:
@@ -55,7 +60,9 @@ def issue_add_edit(request, polity_id, issue_id=None, documentcontent_id=None):
             issue = form.save(commit=False)
             issue.apply_ruleset()
             issue.documentcontent = current_content
-            issue.special_process_set_by = request.user if issue.special_process else None
+            issue.special_process_set_by = (
+                request.user if issue.special_process else None
+            )
             issue.save()
 
             issue.topics.clear()
@@ -76,11 +83,16 @@ def issue_add_edit(request, polity_id, issue_id=None, documentcontent_id=None):
                 name += u', %s %d' % (_(u'version'), current_content.order)
                 selected_topics = current_content.previous_topics()
 
-            form = IssueForm(instance=issue, initial={
-                'name': name,
-                'description': current_content.comments.replace("\n", "\\n"),
-                'topics': selected_topics,
-            })
+            form = IssueForm(
+                instance=issue,
+                initial={
+                    'name': name,
+                    'description': current_content.comments.replace(
+                        "\n", "\\n"
+                    ),
+                    'topics': selected_topics,
+                },
+            )
         else:
             form = IssueForm(instance=issue)
 
@@ -115,20 +127,29 @@ def issue_view(request, polity_id, issue_id):
         if issue.is_processed:
             ctx['selected_diff_documentcontent'] = documentcontent.predecessor
         else:
-            ctx['selected_diff_documentcontent'] = documentcontent.document.preferred_version()
+            ctx[
+                'selected_diff_documentcontent'
+            ] = documentcontent.document.preferred_version()
 
     ctx['polity'] = polity
     ctx['issue'] = issue
-    ctx['can_vote'] = (request.user is not None and issue.can_vote(request.user))
-    ctx['comments_closed'] = not request.user.is_authenticated or issue.discussions_closed()
+    ctx['can_vote'] = request.user is not None and issue.can_vote(request.user)
+    ctx['comments_closed'] = (
+        not request.user.is_authenticated or issue.discussions_closed()
+    )
 
     # People say crazy things on the internet. We'd like to keep the record of
     # conversations about issues well into the future but still we'd like to
     # protect users from having to answer for something they said a long time
     # ago. To try and achieve both goals, we require a logged in user to see
     # comments to older issues.
-    comment_protection_timing = datetime.now() - timedelta(days=settings.RECENT_ISSUE_DAYS)
-    if not request.user.is_authenticated and issue.deadline_votes < comment_protection_timing:
+    comment_protection_timing = datetime.now() - timedelta(
+        days=settings.RECENT_ISSUE_DAYS
+    )
+    if (
+        not request.user.is_authenticated
+        and issue.deadline_votes < comment_protection_timing
+    ):
         ctx['comments_hidden'] = True
 
     return render(request, 'issue/issue_detail.html', ctx)
@@ -162,7 +183,9 @@ def document_add(request, polity_id):
             document.polity = polity
             document.user = request.user
             document.save()
-            return redirect(reverse('documentcontent_add', args=(polity_id, document.id)))
+            return redirect(
+                reverse('documentcontent_add', args=(polity_id, document.id))
+            )
     else:
         form = DocumentForm()
 
@@ -175,11 +198,15 @@ def document_add(request, polity_id):
 
 def document_view(request, polity_id, document_id, version=None):
     polity = get_object_or_404(Polity, id=polity_id)
-    document = get_object_or_404(Document, id=document_id, polity__id=polity_id)
+    document = get_object_or_404(
+        Document, id=document_id, polity__id=polity_id
+    )
 
     # If version is not specified, we want the "preferred" version
     if version is not None:
-        current_content = get_object_or_404(DocumentContent, document=document, order=version)
+        current_content = get_object_or_404(
+            DocumentContent, document=document, order=version
+        )
     else:
         current_content = document.preferred_version()
 
@@ -193,8 +220,9 @@ def document_view(request, polity_id, document_id, version=None):
         'edit_proposal': False,
         'retract_proposal': False,
     }
-    if ((not issue or issue.issue_state() != 'voting')
-            and current_content is not None):
+    if (
+        not issue or issue.issue_state() != 'voting'
+    ) and current_content is not None:
 
         # Check if the user should be allowed to retract the issue, which is
         # at any point in which an issue has been founded but not concluded.
@@ -207,9 +235,13 @@ def document_view(request, polity_id, document_id, version=None):
                 buttons['propose_change'] = 'enabled'
         elif current_content.status == 'proposed':
             if request.globals['user_is_officer'] and not issue:
-                buttons['put_to_vote'] = 'disabled' if document.has_open_issue() else 'enabled'
+                buttons['put_to_vote'] = (
+                    'disabled' if document.has_open_issue() else 'enabled'
+                )
             if current_content.user_id == request.user.id:
-                buttons['edit_proposal'] = 'disabled' if issue is not None else 'enabled'
+                buttons['edit_proposal'] = (
+                    'disabled' if issue is not None else 'enabled'
+                )
 
     ctx = {
         'polity': polity,
@@ -231,7 +263,7 @@ def documentcontent_edit(request, polity_id, document_id, version):
         DocumentContent,
         document_id=document_id,
         document__polity_id=polity_id,
-        order=version
+        order=version,
     )
 
     # Editing of documentcontents should only be allowed by its author or the
@@ -252,7 +284,11 @@ def documentcontent_edit(request, polity_id, document_id, version):
         form = DocumentContentForm(request.POST, instance=dc)
         if form.is_valid():
             form.save()
-            return redirect(reverse('document_view', args=(polity_id, document_id, version)))
+            return redirect(
+                reverse(
+                    'document_view', args=(polity_id, document_id, version)
+                )
+            )
     else:
         form = DocumentContentForm(instance=dc)
 
@@ -286,10 +322,12 @@ def documentcontent_add(request, polity_id, document_id):
             form.instance.order = doc.documentcontent_set.count() + 1
 
             form.save()
-            return redirect(reverse(
-                'document_view',
-                args=(polity_id, document_id, form.instance.order)
-            ))
+            return redirect(
+                reverse(
+                    'document_view',
+                    args=(polity_id, document_id, form.instance.order),
+                )
+            )
     else:
         form = DocumentContentForm()
 
diff --git a/languagecontrol/middleware.py b/languagecontrol/middleware.py
index d04de26e..fc6896fd 100644
--- a/languagecontrol/middleware.py
+++ b/languagecontrol/middleware.py
@@ -1,5 +1,6 @@
 from django.utils.deprecation import MiddlewareMixin
 
+
 class LanguageControlMiddleware(MiddlewareMixin):
     def process_request(self, request):
         if 'HTTP_ACCEPT_LANGUAGE' in request.META:
diff --git a/languagecontrol/signals.py b/languagecontrol/signals.py
index 05e498f7..7363e04c 100644
--- a/languagecontrol/signals.py
+++ b/languagecontrol/signals.py
@@ -3,6 +3,7 @@
 from django.dispatch import receiver
 from django.utils import translation
 
+
 @receiver(user_logged_out)
 def switch_to_default_language_on_logout(sender, user, request, **kwargs):
     # When logged out, we want to set the default language.
diff --git a/languagecontrol/utils.py b/languagecontrol/utils.py
index 79f31761..b09d28b9 100644
--- a/languagecontrol/utils.py
+++ b/languagecontrol/utils.py
@@ -1,5 +1,6 @@
 from django.utils import translation
 
+
 def set_language(request, language):
     translation.activate(language)
     request.session[translation.LANGUAGE_SESSION_KEY] = language
diff --git a/polity/contextprocessors.py b/polity/contextprocessors.py
index 211d89e5..86f382af 100644
--- a/polity/contextprocessors.py
+++ b/polity/contextprocessors.py
@@ -1,6 +1,7 @@
 from collections import OrderedDict
 from polity.models import Polity
 
+
 def navigation(request):
 
     # For now, we only support one level of sub-polities in the navigation.
@@ -22,7 +23,11 @@ def navigation(request):
     type_order = [t[0] for t in Polity.POLITY_TYPES]
 
     # Get visible sub-polities of uppermost polity.
-    polities = Polity.objects.visible().exclude(parent=None).filter(parent__parent=None)
+    polities = (
+        Polity.objects.visible()
+        .exclude(parent=None)
+        .filter(parent__parent=None)
+    )
 
     # Sort polities by type, in the order defined in Polity.POLITY_TYPES.
     polities = sorted(polities, key=lambda p: type_order.index(p.polity_type))
@@ -38,4 +43,4 @@ def navigation(request):
 
         polity_nav[polity.polity_type]['polities'].append(polity)
 
-    return { 'polity_nav': polity_nav }
+    return {'polity_nav': polity_nav}
diff --git a/polity/forms.py b/polity/forms.py
index 6e266fa5..eb682755 100644
--- a/polity/forms.py
+++ b/polity/forms.py
@@ -2,6 +2,7 @@
 
 from polity.models import Polity
 
+
 class PolityForm(Wasa2ilForm):
     class Meta:
         model = Polity
diff --git a/polity/migrations/0001_initial.py b/polity/migrations/0001_initial.py
index 870cfc60..1f86123c 100644
--- a/polity/migrations/0001_initial.py
+++ b/polity/migrations/0001_initial.py
@@ -19,33 +19,135 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='Polity',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'name',
+                    models.CharField(max_length=128, verbose_name='Name'),
+                ),
                 ('slug', models.SlugField(blank=True, max_length=128)),
-                ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
-                ('is_listed', models.BooleanField(default=True, help_text='Whether the polity is publicly listed or not.', verbose_name='Publicly listed?')),
-                ('is_newissue_only_officers', models.BooleanField(default=False, help_text="If this is checked, only officers can create new issues. If it's unchecked, any member can start a new issue.", verbose_name='Can only officers make new issues?')),
-                ('is_front_polity', models.BooleanField(default=False, help_text='If checked, this polity will be displayed on the front page. The first created polity automatically becomes the front polity.', verbose_name='Front polity?')),
+                (
+                    'description',
+                    models.TextField(
+                        blank=True, null=True, verbose_name='Description'
+                    ),
+                ),
+                (
+                    'is_listed',
+                    models.BooleanField(
+                        default=True,
+                        help_text='Whether the polity is publicly listed or not.',
+                        verbose_name='Publicly listed?',
+                    ),
+                ),
+                (
+                    'is_newissue_only_officers',
+                    models.BooleanField(
+                        default=False,
+                        help_text="If this is checked, only officers can create new issues. If it's unchecked, any member can start a new issue.",
+                        verbose_name='Can only officers make new issues?',
+                    ),
+                ),
+                (
+                    'is_front_polity',
+                    models.BooleanField(
+                        default=False,
+                        help_text='If checked, this polity will be displayed on the front page. The first created polity automatically becomes the front polity.',
+                        verbose_name='Front polity?',
+                    ),
+                ),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('modified', models.DateTimeField(auto_now=True)),
-                ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polity_created_by', to=settings.AUTH_USER_MODEL)),
-                ('members', models.ManyToManyField(related_name='polities', to=settings.AUTH_USER_MODEL)),
-                ('modified_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polity_modified_by', to=settings.AUTH_USER_MODEL)),
-                ('officers', models.ManyToManyField(related_name='officers', to=settings.AUTH_USER_MODEL, verbose_name='Officers')),
-                ('parent', models.ForeignKey(blank=True, help_text=b'Parent polity', null=True, on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')),
-                ('wranglers', models.ManyToManyField(related_name='wranglers', to=settings.AUTH_USER_MODEL, verbose_name='Volunteer wranglers')),
+                (
+                    'created_by',
+                    models.ForeignKey(
+                        blank=True,
+                        editable=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='polity_created_by',
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                (
+                    'members',
+                    models.ManyToManyField(
+                        related_name='polities', to=settings.AUTH_USER_MODEL
+                    ),
+                ),
+                (
+                    'modified_by',
+                    models.ForeignKey(
+                        blank=True,
+                        editable=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='polity_modified_by',
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                (
+                    'officers',
+                    models.ManyToManyField(
+                        related_name='officers',
+                        to=settings.AUTH_USER_MODEL,
+                        verbose_name='Officers',
+                    ),
+                ),
+                (
+                    'parent',
+                    models.ForeignKey(
+                        blank=True,
+                        help_text=b'Parent polity',
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='polity.Polity',
+                    ),
+                ),
+                (
+                    'wranglers',
+                    models.ManyToManyField(
+                        related_name='wranglers',
+                        to=settings.AUTH_USER_MODEL,
+                        verbose_name='Volunteer wranglers',
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
             name='PolityRuleset',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
                 ('name', models.CharField(max_length=255)),
-                ('issue_majority', models.DecimalField(decimal_places=2, max_digits=5)),
+                (
+                    'issue_majority',
+                    models.DecimalField(decimal_places=2, max_digits=5),
+                ),
                 ('issue_discussion_time', models.DurationField()),
                 ('issue_proposal_time', models.DurationField()),
                 ('issue_vote_time', models.DurationField()),
-                ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')),
+                (
+                    'polity',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='polity.Polity',
+                    ),
+                ),
             ],
         ),
     ]
diff --git a/polity/migrations/0002_auto_20181207_0944.py b/polity/migrations/0002_auto_20181207_0944.py
index 9fa0c68f..af661935 100644
--- a/polity/migrations/0002_auto_20181207_0944.py
+++ b/polity/migrations/0002_auto_20181207_0944.py
@@ -15,36 +15,57 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='polity',
             name='push_before_election_end',
-            field=models.BooleanField(default=False, verbose_name='Send notification an hour before election ends?'),
+            field=models.BooleanField(
+                default=False,
+                verbose_name='Send notification an hour before election ends?',
+            ),
         ),
         migrations.AddField(
             model_name='polity',
             name='push_before_vote_end',
-            field=models.BooleanField(default=False, verbose_name='Send notification an hour before voting ends?'),
+            field=models.BooleanField(
+                default=False,
+                verbose_name='Send notification an hour before voting ends?',
+            ),
         ),
         migrations.AddField(
             model_name='polity',
             name='push_on_debate_start',
-            field=models.BooleanField(default=False, verbose_name='Send notification when debate starts?'),
+            field=models.BooleanField(
+                default=False,
+                verbose_name='Send notification when debate starts?',
+            ),
         ),
         migrations.AddField(
             model_name='polity',
             name='push_on_election_end',
-            field=models.BooleanField(default=False, verbose_name='Send notification when an election ends?'),
+            field=models.BooleanField(
+                default=False,
+                verbose_name='Send notification when an election ends?',
+            ),
         ),
         migrations.AddField(
             model_name='polity',
             name='push_on_election_start',
-            field=models.BooleanField(default=False, verbose_name='Send notification when an election starts?'),
+            field=models.BooleanField(
+                default=False,
+                verbose_name='Send notification when an election starts?',
+            ),
         ),
         migrations.AddField(
             model_name='polity',
             name='push_on_vote_end',
-            field=models.BooleanField(default=False, verbose_name='Send notification when voting ends?'),
+            field=models.BooleanField(
+                default=False,
+                verbose_name='Send notification when voting ends?',
+            ),
         ),
         migrations.AddField(
             model_name='polity',
             name='push_on_vote_start',
-            field=models.BooleanField(default=False, verbose_name='Send notification when issue goes to vote?'),
+            field=models.BooleanField(
+                default=False,
+                verbose_name='Send notification when issue goes to vote?',
+            ),
         ),
     ]
diff --git a/polity/migrations/0003_auto_20190822_2006.py b/polity/migrations/0003_auto_20190822_2006.py
index cfbe1d8e..20d3c463 100644
--- a/polity/migrations/0003_auto_20190822_2006.py
+++ b/polity/migrations/0003_auto_20190822_2006.py
@@ -15,16 +15,36 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='polity',
             name='created_by',
-            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='polity_created_by', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='polity_created_by',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
         migrations.AlterField(
             model_name='polity',
             name='modified_by',
-            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='polity_modified_by', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='polity_modified_by',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
         migrations.AlterField(
             model_name='polity',
             name='parent',
-            field=models.ForeignKey(blank=True, help_text='Parent polity', null=True, on_delete=django.db.models.deletion.SET_NULL, to='polity.Polity'),
+            field=models.ForeignKey(
+                blank=True,
+                help_text='Parent polity',
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to='polity.Polity',
+            ),
         ),
     ]
diff --git a/polity/migrations/0003_polity_require_phone_for_volunteering.py b/polity/migrations/0003_polity_require_phone_for_volunteering.py
index 37c97e3f..4dbac98d 100644
--- a/polity/migrations/0003_polity_require_phone_for_volunteering.py
+++ b/polity/migrations/0003_polity_require_phone_for_volunteering.py
@@ -15,6 +15,10 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='polity',
             name='require_phone_for_volunteering',
-            field=models.BooleanField(default=True, help_text='Make users provide their phone numbers in the profiles to partake in tasks that need volunteers.', verbose_name='Require phone for volunteering'),
+            field=models.BooleanField(
+                default=True,
+                help_text='Make users provide their phone numbers in the profiles to partake in tasks that need volunteers.',
+                verbose_name='Require phone for volunteering',
+            ),
         ),
     ]
diff --git a/polity/migrations/0005_merge_20191019_1939.py b/polity/migrations/0005_merge_20191019_1939.py
index 35ff9c11..7eec2a10 100644
--- a/polity/migrations/0005_merge_20191019_1939.py
+++ b/polity/migrations/0005_merge_20191019_1939.py
@@ -10,5 +10,4 @@ class Migration(migrations.Migration):
         ('polity', '0003_auto_20190822_2006'),
     ]
 
-    operations = [
-    ]
+    operations = []
diff --git a/polity/migrations/0006_auto_20200909_2020.py b/polity/migrations/0006_auto_20200909_2020.py
index 81f89302..16d245ec 100644
--- a/polity/migrations/0006_auto_20200909_2020.py
+++ b/polity/migrations/0006_auto_20200909_2020.py
@@ -17,6 +17,10 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='polity',
             name='order',
-            field=models.IntegerField(default=1, help_text='Optional, custom sort order. Polities with the same order are ordered by name.', verbose_name='Order'),
+            field=models.IntegerField(
+                default=1,
+                help_text='Optional, custom sort order. Polities with the same order are ordered by name.',
+                verbose_name='Order',
+            ),
         ),
     ]
diff --git a/polity/migrations/0007_polity_name_short.py b/polity/migrations/0007_polity_name_short.py
index 5fd109ed..ced1a2ab 100644
--- a/polity/migrations/0007_polity_name_short.py
+++ b/polity/migrations/0007_polity_name_short.py
@@ -13,6 +13,11 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='polity',
             name='name_short',
-            field=models.CharField(default='', help_text='Optional. Could be an abbreviation or acronym, for example.', max_length=30, verbose_name='Short name'),
+            field=models.CharField(
+                default='',
+                help_text='Optional. Could be an abbreviation or acronym, for example.',
+                max_length=30,
+                verbose_name='Short name',
+            ),
         ),
     ]
diff --git a/polity/migrations/0008_polity_eligibles.py b/polity/migrations/0008_polity_eligibles.py
index 0ba5be21..e9ea5750 100644
--- a/polity/migrations/0008_polity_eligibles.py
+++ b/polity/migrations/0008_polity_eligibles.py
@@ -15,6 +15,8 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='polity',
             name='eligibles',
-            field=models.ManyToManyField(related_name='polities_eligible', to=settings.AUTH_USER_MODEL),
+            field=models.ManyToManyField(
+                related_name='polities_eligible', to=settings.AUTH_USER_MODEL
+            ),
         ),
     ]
diff --git a/polity/migrations/0008_polity_polity_type.py b/polity/migrations/0008_polity_polity_type.py
index 9e7a1489..6ec1d99c 100644
--- a/polity/migrations/0008_polity_polity_type.py
+++ b/polity/migrations/0008_polity_polity_type.py
@@ -13,6 +13,15 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='polity',
             name='polity_type',
-            field=models.CharField(choices=[('U', 'Unspecified'), ('S', 'Regional group'), ('C', 'Constituency group'), ('I', 'Special Interest Group')], default='U', max_length=1),
+            field=models.CharField(
+                choices=[
+                    ('U', 'Unspecified'),
+                    ('S', 'Regional group'),
+                    ('C', 'Constituency group'),
+                    ('I', 'Special Interest Group'),
+                ],
+                default='U',
+                max_length=1,
+            ),
         ),
     ]
diff --git a/polity/migrations/0009_merge_20210118_2102.py b/polity/migrations/0009_merge_20210118_2102.py
index 4ed8dd1a..7eb21bf4 100644
--- a/polity/migrations/0009_merge_20210118_2102.py
+++ b/polity/migrations/0009_merge_20210118_2102.py
@@ -10,5 +10,4 @@ class Migration(migrations.Migration):
         ('polity', '0008_polity_eligibles'),
     ]
 
-    operations = [
-    ]
+    operations = []
diff --git a/polity/migrations/0010_auto_20210118_2102.py b/polity/migrations/0010_auto_20210118_2102.py
index d96fabf8..96e31bff 100644
--- a/polity/migrations/0010_auto_20210118_2102.py
+++ b/polity/migrations/0010_auto_20210118_2102.py
@@ -15,16 +15,36 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='polity',
             name='eligibles',
-            field=models.ManyToManyField(blank=True, related_name='polities_eligible', to=settings.AUTH_USER_MODEL),
+            field=models.ManyToManyField(
+                blank=True,
+                related_name='polities_eligible',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
         migrations.AlterField(
             model_name='polity',
             name='name_short',
-            field=models.CharField(blank=True, default='', help_text='Optional. Could be an abbreviation or acronym, for example.', max_length=30, null=True, verbose_name='Short name'),
+            field=models.CharField(
+                blank=True,
+                default='',
+                help_text='Optional. Could be an abbreviation or acronym, for example.',
+                max_length=30,
+                null=True,
+                verbose_name='Short name',
+            ),
         ),
         migrations.AlterField(
             model_name='polity',
             name='polity_type',
-            field=models.CharField(choices=[('U', 'Unspecified'), ('R', 'Regional group'), ('C', 'Constituency group'), ('I', 'Special Interest Group')], default='U', max_length=1),
+            field=models.CharField(
+                choices=[
+                    ('U', 'Unspecified'),
+                    ('R', 'Regional group'),
+                    ('C', 'Constituency group'),
+                    ('I', 'Special Interest Group'),
+                ],
+                default='U',
+                max_length=1,
+            ),
         ),
     ]
diff --git a/polity/migrations/0011_auto_20210118_2303.py b/polity/migrations/0011_auto_20210118_2303.py
index 39c08e1e..69f11fb6 100644
--- a/polity/migrations/0011_auto_20210118_2303.py
+++ b/polity/migrations/0011_auto_20210118_2303.py
@@ -13,6 +13,15 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='polity',
             name='polity_type',
-            field=models.CharField(choices=[('unspecified', 'Unspecified'), ('regional', 'Regional Group'), ('constituency', 'Constituency Group'), ('special_interest', 'Special Interest Group')], default='unspecified', max_length=20),
+            field=models.CharField(
+                choices=[
+                    ('unspecified', 'Unspecified'),
+                    ('regional', 'Regional Group'),
+                    ('constituency', 'Constituency Group'),
+                    ('special_interest', 'Special Interest Group'),
+                ],
+                default='unspecified',
+                max_length=20,
+            ),
         ),
     ]
diff --git a/polity/migrations/0012_auto_20210118_2342.py b/polity/migrations/0012_auto_20210118_2342.py
index b294b184..fdb598ab 100644
--- a/polity/migrations/0012_auto_20210118_2342.py
+++ b/polity/migrations/0012_auto_20210118_2342.py
@@ -11,7 +11,10 @@ def update_polity_types(apps, schema_editor):
     Polity.objects.filter(polity_type='U').update(polity_type='unspecified')
     Polity.objects.filter(polity_type='R').update(polity_type='regional')
     Polity.objects.filter(polity_type='C').update(polity_type='constituency')
-    Polity.objects.filter(polity_type='I').update(polity_type='special_interest')
+    Polity.objects.filter(polity_type='I').update(
+        polity_type='special_interest'
+    )
+
 
 class Migration(migrations.Migration):
 
@@ -19,6 +22,4 @@ class Migration(migrations.Migration):
         ('polity', '0011_auto_20210118_2303'),
     ]
 
-    operations = [
-        migrations.RunPython(update_polity_types)
-    ]
+    operations = [migrations.RunPython(update_polity_types)]
diff --git a/polity/models.py b/polity/models.py
index 6d8815a6..6f273a44 100644
--- a/polity/models.py
+++ b/polity/models.py
@@ -12,6 +12,7 @@ class PolityQuerySet(models.QuerySet):
     def visible(self):
         return self.filter(is_listed=True)
 
+
 class Polity(models.Model):
     objects = PolityQuerySet.as_manager()
 
@@ -27,18 +28,30 @@ class Polity(models.Model):
     name_short = models.CharField(
         max_length=30,
         verbose_name=_('Short name'),
-        help_text=_('Optional. Could be an abbreviation or acronym, for example.'),
+        help_text=_(
+            'Optional. Could be an abbreviation or acronym, for example.'
+        ),
         default='',
         null=True,
-        blank=True
+        blank=True,
     )
     slug = models.SlugField(max_length=128, blank=True)
 
-    description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
+    description = models.TextField(
+        verbose_name=_("Description"), null=True, blank=True
+    )
 
-    order = models.IntegerField(default=1, verbose_name=_('Order'), help_text=_('Optional, custom sort order. Polities with the same order are ordered by name.'))
+    order = models.IntegerField(
+        default=1,
+        verbose_name=_('Order'),
+        help_text=_(
+            'Optional, custom sort order. Polities with the same order are ordered by name.'
+        ),
+    )
 
-    polity_type = models.CharField(max_length=20, choices=POLITY_TYPES, default='unspecified')
+    polity_type = models.CharField(
+        max_length=20, choices=POLITY_TYPES, default='unspecified'
+    )
 
     created_by = models.ForeignKey(
         settings.AUTH_USER_MODEL,
@@ -46,7 +59,7 @@ class Polity(models.Model):
         null=True,
         blank=True,
         related_name='polity_created_by',
-        on_delete=SET_NULL
+        on_delete=SET_NULL,
     )
     modified_by = models.ForeignKey(
         settings.AUTH_USER_MODEL,
@@ -54,35 +67,81 @@ class Polity(models.Model):
         null=True,
         blank=True,
         related_name='polity_modified_by',
-        on_delete=SET_NULL
+        on_delete=SET_NULL,
     )
     created = models.DateTimeField(auto_now_add=True)
     modified = models.DateTimeField(auto_now=True)
 
-    parent = models.ForeignKey('Polity', help_text="Parent polity", null=True, blank=True, on_delete=SET_NULL)
-    members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='polities')
-    eligibles = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='polities_eligible', blank=True)
-    officers = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_("Officers"), related_name="officers")
-    wranglers = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_("Volunteer wranglers"), related_name="wranglers")
-
-    is_listed = models.BooleanField(verbose_name=_("Publicly listed?"), default=True, help_text=_("Whether the polity is publicly listed or not."))
-    is_newissue_only_officers = models.BooleanField(verbose_name=_("Can only officers make new issues?"), default=False, help_text=_("If this is checked, only officers can create new issues. If it's unchecked, any member can start a new issue."))
-    is_front_polity = models.BooleanField(verbose_name=_("Front polity?"), default=False, help_text=_("If checked, this polity will be displayed on the front page. The first created polity automatically becomes the front polity."))
-
-    push_on_debate_start = models.BooleanField(default=False,
-        verbose_name=_("Send notification when debate starts?"))
-    push_on_vote_start = models.BooleanField(default=False,
-        verbose_name=_("Send notification when issue goes to vote?"))
-    push_before_vote_end = models.BooleanField(default=False,
-        verbose_name=_("Send notification an hour before voting ends?"))
-    push_on_vote_end = models.BooleanField(default=False,
-        verbose_name=_("Send notification when voting ends?"))
-    push_on_election_start = models.BooleanField(default=False,
-        verbose_name=_("Send notification when an election starts?"))
-    push_before_election_end = models.BooleanField(default=False,
-        verbose_name=_("Send notification an hour before election ends?"))
-    push_on_election_end = models.BooleanField(default=False,
-        verbose_name=_("Send notification when an election ends?"))
+    parent = models.ForeignKey(
+        'Polity',
+        help_text="Parent polity",
+        null=True,
+        blank=True,
+        on_delete=SET_NULL,
+    )
+    members = models.ManyToManyField(
+        settings.AUTH_USER_MODEL, related_name='polities'
+    )
+    eligibles = models.ManyToManyField(
+        settings.AUTH_USER_MODEL, related_name='polities_eligible', blank=True
+    )
+    officers = models.ManyToManyField(
+        settings.AUTH_USER_MODEL,
+        verbose_name=_("Officers"),
+        related_name="officers",
+    )
+    wranglers = models.ManyToManyField(
+        settings.AUTH_USER_MODEL,
+        verbose_name=_("Volunteer wranglers"),
+        related_name="wranglers",
+    )
+
+    is_listed = models.BooleanField(
+        verbose_name=_("Publicly listed?"),
+        default=True,
+        help_text=_("Whether the polity is publicly listed or not."),
+    )
+    is_newissue_only_officers = models.BooleanField(
+        verbose_name=_("Can only officers make new issues?"),
+        default=False,
+        help_text=_(
+            "If this is checked, only officers can create new issues. If it's unchecked, any member can start a new issue."
+        ),
+    )
+    is_front_polity = models.BooleanField(
+        verbose_name=_("Front polity?"),
+        default=False,
+        help_text=_(
+            "If checked, this polity will be displayed on the front page. The first created polity automatically becomes the front polity."
+        ),
+    )
+
+    push_on_debate_start = models.BooleanField(
+        default=False, verbose_name=_("Send notification when debate starts?")
+    )
+    push_on_vote_start = models.BooleanField(
+        default=False,
+        verbose_name=_("Send notification when issue goes to vote?"),
+    )
+    push_before_vote_end = models.BooleanField(
+        default=False,
+        verbose_name=_("Send notification an hour before voting ends?"),
+    )
+    push_on_vote_end = models.BooleanField(
+        default=False, verbose_name=_("Send notification when voting ends?")
+    )
+    push_on_election_start = models.BooleanField(
+        default=False,
+        verbose_name=_("Send notification when an election starts?"),
+    )
+    push_before_election_end = models.BooleanField(
+        default=False,
+        verbose_name=_("Send notification an hour before election ends?"),
+    )
+    push_on_election_end = models.BooleanField(
+        default=False,
+        verbose_name=_("Send notification when an election ends?"),
+    )
 
     def is_member(self, user):
         return self.members.filter(id=user.id).exists()
@@ -106,23 +165,25 @@ def election_potential_candidates(self):
 
     def agreements(self, query=None):
         DocumentContent = apps.get_model('issue', 'DocumentContent')
-        res = DocumentContent.objects.select_related(
-            'document',
-            'issue'
-        ).filter(
-            status='accepted',
-            document__polity_id=self.id
-        ).order_by('-issue__deadline_votes')
+        res = (
+            DocumentContent.objects.select_related('document', 'issue')
+            .filter(status='accepted', document__polity_id=self.id)
+            .order_by('-issue__deadline_votes')
+        )
         if query:
-            res = res.filter(Q(issue__name__icontains=query)
-                           | Q(issue__description__icontains=query)
-                           | Q(text__icontains=query))
+            res = res.filter(
+                Q(issue__name__icontains=query)
+                | Q(issue__description__icontains=query)
+                | Q(text__icontains=query)
+            )
 
         return res
 
     def update_agreements(self):
         Issue = apps.get_model('issue', 'Issue')
-        issues_to_process = Issue.objects.filter(is_processed=False).filter(deadline_votes__lt=timezone.now())
+        issues_to_process = Issue.objects.filter(is_processed=False).filter(
+            deadline_votes__lt=timezone.now()
+        )
         for issue in issues_to_process:
             issue.process()
         return None
@@ -133,7 +194,9 @@ def save(self, *args, **kwargs):
         if polities.count() == 0:
             self.is_front_polity = True
         elif self.is_front_polity:
-            for frontpolity in polities.filter(is_front_polity=True).exclude(id=self.id): # Should never return more than 1
+            for frontpolity in polities.filter(is_front_polity=True).exclude(
+                id=self.id
+            ):  # Should never return more than 1
                 frontpolity.is_front_polity = False
                 frontpolity.save()
 
@@ -148,6 +211,7 @@ class Meta:
 
 class PolityRuleset(models.Model):
     """A polity's ruleset."""
+
     polity = models.ForeignKey('polity.Polity', on_delete=CASCADE)
     name = models.CharField(max_length=255)
 
@@ -160,8 +224,8 @@ class PolityRuleset(models.Model):
     issue_proposal_time = models.DurationField()
     issue_vote_time = models.DurationField()
 
-    #issue_proponents_required = models.IntegerField(help_text='The minimum number of people who must explicitly state support before the issue progresses. If zero, no automatic progression will occur.')
-    #issue_voter_quorum = models.IntegerField()
+    # issue_proponents_required = models.IntegerField(help_text='The minimum number of people who must explicitly state support before the issue progresses. If zero, no automatic progression will occur.')
+    # issue_voter_quorum = models.IntegerField()
 
     def __str__(self):
         return u'%s' % self.name
diff --git a/polity/urls.py b/polity/urls.py
index eb299c88..70ac41d2 100644
--- a/polity/urls.py
+++ b/polity/urls.py
@@ -14,8 +14,20 @@
 urlpatterns = [
     url(r'^polities/$', polity_list, name='polities'),
     url(r'^polity/new/$', polity_add_edit, name='polity_add'),
-    url(r'^polity/(?P\d+)/edit/$', polity_add_edit, name='polity_edit'),
-    url(r'^polity/(?P\d+)/officers/$', polity_officers, name='polity_officers'),
-    url(r'^polity/(?P\d+)/$', never_cache(polity_view), name='polity'),
+    url(
+        r'^polity/(?P\d+)/edit/$',
+        polity_add_edit,
+        name='polity_edit',
+    ),
+    url(
+        r'^polity/(?P\d+)/officers/$',
+        polity_officers,
+        name='polity_officers',
+    ),
+    url(
+        r'^polity/(?P\d+)/$',
+        never_cache(polity_view),
+        name='polity',
+    ),
     path('polity//apply/', polity_apply, name='polity_apply'),
 ]
diff --git a/polity/views.py b/polity/views.py
index 4be05cad..cd08d508 100644
--- a/polity/views.py
+++ b/polity/views.py
@@ -27,8 +27,16 @@
 def polity_list(request):
     polities = Polity.objects.all()
 
-    issues_recent = Issue.objects.recent().filter(polity__in=polities).order_by('polity__name')
-    elections_recent = Election.objects.recent().filter(polity__in=polities).order_by('polity__name')
+    issues_recent = (
+        Issue.objects.recent()
+        .filter(polity__in=polities)
+        .order_by('polity__name')
+    )
+    elections_recent = (
+        Election.objects.recent()
+        .filter(polity__in=polities)
+        .order_by('polity__name')
+    )
 
     ctx = {
         'polities': polities,
@@ -47,7 +55,9 @@ def polity_view(request, polity_id):
 
     sub_polities = polity.polity_set.all()
 
-    election_set = Election.objects.recent().filter(Q(polity=polity) | Q(polity__parent=polity))
+    election_set = Election.objects.recent().filter(
+        Q(polity=polity) | Q(polity__parent=polity)
+    )
 
     ctx = {
         'sub_polities': sub_polities,
@@ -57,7 +67,9 @@ def polity_view(request, polity_id):
         'elections_recent': election_set,
         'RECENT_ISSUE_DAYS': settings.RECENT_ISSUE_DAYS,
         'RECENT_ELECTION_DAYS': settings.RECENT_ELECTION_DAYS,
-        'verified_user_count': polity.members.filter(userprofile__verified=True).count(),
+        'verified_user_count': polity.members.filter(
+            userprofile__verified=True
+        ).count(),
     }
 
     return render(request, 'polity/polity_detail.html', ctx)
@@ -134,7 +146,9 @@ def polity_apply(request, polity_id):
     # from IcePirate again and apply the new version locally. This will place
     # the user in the local polity.
     try:
-        success, member, error = add_member_to_membergroup(request.user, polity)
+        success, member, error = add_member_to_membergroup(
+            request.user, polity
+        )
         if success:
             apply_member_locally(member, request.user)
     except:
diff --git a/py3votecore/abstract_classes.py b/py3votecore/abstract_classes.py
index c40e11c7..250747be 100644
--- a/py3votecore/abstract_classes.py
+++ b/py3votecore/abstract_classes.py
@@ -89,14 +89,20 @@ def as_dict(self):
 
 
 # Given a set of candidates, return a fixed number of winners
-class AbstractSingleWinnerVotingSystem(SingleWinnerVotingSystem, metaclass=ABCMeta):
+class AbstractSingleWinnerVotingSystem(
+    SingleWinnerVotingSystem, metaclass=ABCMeta
+):
     @abstractmethod
     def __init__(self, ballots, multiple_winner_class, tie_breaker=None):
         self.multiple_winner_class = multiple_winner_class
-        super(AbstractSingleWinnerVotingSystem, self).__init__(ballots, tie_breaker=tie_breaker)
+        super(AbstractSingleWinnerVotingSystem, self).__init__(
+            ballots, tie_breaker=tie_breaker
+        )
 
     def calculate_results(self):
-        self.multiple_winner_instance = self.multiple_winner_class(self.ballots, tie_breaker=self.tie_breaker, required_winners=1)
+        self.multiple_winner_instance = self.multiple_winner_class(
+            self.ballots, tie_breaker=self.tie_breaker, required_winners=1
+        )
         self.__dict__.update(self.multiple_winner_instance.__dict__)
         self.winner = list(self.winners)[0]
         del self.winners
@@ -113,7 +119,9 @@ class OrderingVotingSystem(VotingSystem, metaclass=ABCMeta):
     @abstractmethod
     def __init__(self, ballots, tie_breaker=None, winner_threshold=None):
         self.winner_threshold = winner_threshold
-        super(OrderingVotingSystem, self).__init__(ballots, tie_breaker=tie_breaker)
+        super(OrderingVotingSystem, self).__init__(
+            ballots, tie_breaker=tie_breaker
+        )
 
     def as_dict(self):
         data = super(OrderingVotingSystem, self).as_dict()
@@ -126,9 +134,17 @@ def as_dict(self):
 # smaller subset of candidates until all candidates are consumed.
 class AbstractOrderingVotingSystem(OrderingVotingSystem, metaclass=ABCMeta):
     @abstractmethod
-    def __init__(self, ballots, single_winner_class, winner_threshold=None, tie_breaker=None):
+    def __init__(
+        self,
+        ballots,
+        single_winner_class,
+        winner_threshold=None,
+        tie_breaker=None,
+    ):
         self.single_winner_class = single_winner_class
-        super(AbstractOrderingVotingSystem, self).__init__(ballots, winner_threshold=winner_threshold, tie_breaker=tie_breaker)
+        super(AbstractOrderingVotingSystem, self).__init__(
+            ballots, winner_threshold=winner_threshold, tie_breaker=tie_breaker
+        )
 
     def calculate_results(self):
         self.order = []
@@ -136,12 +152,16 @@ def calculate_results(self):
         remaining_ballots = deepcopy(self.ballots)
         remaining_candidates = True
         while (
-            (remaining_candidates is True or len(remaining_candidates) > 1)
-            and (self.winner_threshold is None or len(self.order) < self.winner_threshold)
+            remaining_candidates is True or len(remaining_candidates) > 1
+        ) and (
+            self.winner_threshold is None
+            or len(self.order) < self.winner_threshold
         ):
 
             # Given the remaining ballots, who should win?
-            result = self.single_winner_class(deepcopy(remaining_ballots), tie_breaker=self.tie_breaker)
+            result = self.single_winner_class(
+                deepcopy(remaining_ballots), tie_breaker=self.tie_breaker
+            )
 
             # Mark the candidate that won
             r = {'winner': result.winner}
@@ -159,10 +179,15 @@ def calculate_results(self):
                 self.candidates = result.candidates
                 remaining_candidates = copy(self.candidates)
             remaining_candidates.remove(result.winner)
-            remaining_ballots = self.ballots_without_candidate(result.ballots, result.winner)
+            remaining_ballots = self.ballots_without_candidate(
+                result.ballots, result.winner
+            )
 
         # Note the last remaining candidate
-        if (self.winner_threshold is None or len(self.order) < self.winner_threshold):
+        if (
+            self.winner_threshold is None
+            or len(self.order) < self.winner_threshold
+        ):
             r = {'winner': list(remaining_candidates)[0]}
             self.order.append(r['winner'])
             self.rounds.append(r)
diff --git a/py3votecore/common_functions.py b/py3votecore/common_functions.py
index afe1ce12..5f87f00e 100644
--- a/py3votecore/common_functions.py
+++ b/py3votecore/common_functions.py
@@ -1,9 +1,5 @@
 def matching_keys(dict, target_value):
-    return set([
-        key
-        for key, value in dict.items()
-        if value == target_value
-    ])
+    return set([key for key, value in dict.items() if value == target_value])
 
 
 def unique_permutations(xs):
diff --git a/py3votecore/condorcet.py b/py3votecore/condorcet.py
index ef65e6d1..411af8e9 100644
--- a/py3votecore/condorcet.py
+++ b/py3votecore/condorcet.py
@@ -42,7 +42,10 @@ def standardize_ballots(self, ballots, ballot_notation):
             for ballot in self.ballots:
                 for candidate, rating in ballot["ballot"].items():
                     ballot["ballot"][candidate] = -float(rating)
-        elif ballot_notation == CondorcetHelper.BALLOT_NOTATION_RATING or ballot_notation is None:
+        elif (
+            ballot_notation == CondorcetHelper.BALLOT_NOTATION_RATING
+            or ballot_notation is None
+        ):
             for ballot in self.ballots:
                 for candidate, rating in ballot["ballot"].items():
                     ballot["ballot"][candidate] = float(rating)
@@ -74,39 +77,51 @@ def ballots_into_graph(candidates, ballots):
         graph = digraph()
         graph.add_nodes(candidates)
         for pair in itertools.permutations(candidates, 2):
-            graph.add_edge(pair, sum([
-                ballot["count"]
-                for ballot in ballots
-                if ballot["ballot"][pair[0]] > ballot["ballot"][pair[1]]
-            ]))
+            graph.add_edge(
+                pair,
+                sum(
+                    [
+                        ballot["count"]
+                        for ballot in ballots
+                        if ballot["ballot"][pair[0]]
+                        > ballot["ballot"][pair[1]]
+                    ]
+                ),
+            )
         return graph
 
     @staticmethod
     def edge_weights(graph):
-        return dict([
-            (edge, graph.edge_weight(edge))
-            for edge in graph.edges()
-        ])
+        return dict(
+            [(edge, graph.edge_weight(edge)) for edge in graph.edges()]
+        )
 
     @staticmethod
     def remove_weak_edges(graph):
         for pair in itertools.combinations(graph.nodes(), 2):
             pairs = (pair, (pair[1], pair[0]))
-            weights = (graph.edge_weight(pairs[0]), graph.edge_weight(pairs[1]))
+            weights = (
+                graph.edge_weight(pairs[0]),
+                graph.edge_weight(pairs[1]),
+            )
             if weights[0] >= weights[1]:
                 graph.del_edge(pairs[1])
             if weights[1] >= weights[0]:
                 graph.del_edge(pairs[0])
 
-# This class determines the Condorcet winner if one exists.
 
+# This class determines the Condorcet winner if one exists.
 
-class CondorcetSystem(SingleWinnerVotingSystem, CondorcetHelper, metaclass=ABCMeta):
 
+class CondorcetSystem(
+    SingleWinnerVotingSystem, CondorcetHelper, metaclass=ABCMeta
+):
     @abstractmethod
     def __init__(self, ballots, tie_breaker=None, ballot_notation=None):
         self.standardize_ballots(ballots, ballot_notation)
-        super(CondorcetSystem, self).__init__(self.ballots, tie_breaker=tie_breaker)
+        super(CondorcetSystem, self).__init__(
+            self.ballots, tie_breaker=tie_breaker
+        )
 
     def calculate_results(self):
         self.graph = self.ballots_into_graph(self.candidates, self.ballots)
diff --git a/py3votecore/irv.py b/py3votecore/irv.py
index d9c7b6a5..f1d26f4e 100644
--- a/py3votecore/irv.py
+++ b/py3votecore/irv.py
@@ -3,7 +3,6 @@
 
 
 class IRV(AbstractSingleWinnerVotingSystem):
-
     def __init__(self, ballots, tie_breaker=None):
         super(IRV, self).__init__(ballots, STV, tie_breaker=tie_breaker)
 
diff --git a/py3votecore/plurality.py b/py3votecore/plurality.py
index 4a8c2757..88c4d963 100644
--- a/py3votecore/plurality.py
+++ b/py3votecore/plurality.py
@@ -3,6 +3,7 @@
 
 
 class Plurality(AbstractSingleWinnerVotingSystem):
-
     def __init__(self, ballots, tie_breaker=None):
-        super(Plurality, self).__init__(ballots, PluralityAtLarge, tie_breaker=tie_breaker)
+        super(Plurality, self).__init__(
+            ballots, PluralityAtLarge, tie_breaker=tie_breaker
+        )
diff --git a/py3votecore/plurality_at_large.py b/py3votecore/plurality_at_large.py
index f185a44b..783f3909 100644
--- a/py3votecore/plurality_at_large.py
+++ b/py3votecore/plurality_at_large.py
@@ -20,9 +20,10 @@
 
 
 class PluralityAtLarge(MultipleWinnerVotingSystem):
-
     def __init__(self, ballots, tie_breaker=None, required_winners=1):
-        super(PluralityAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners)
+        super(PluralityAtLarge, self).__init__(
+            ballots, tie_breaker=tie_breaker, required_winners=required_winners
+        )
 
     def calculate_results(self):
 
@@ -57,10 +58,18 @@ def calculate_results(self):
             top_candidates = matching_keys(tallies, largest_tally)
 
             # Reduce the found candidates if there are too many
-            if len(top_candidates | winning_candidates) > self.required_winners:
+            if (
+                len(top_candidates | winning_candidates)
+                > self.required_winners
+            ):
                 self.tied_winners = top_candidates.copy()
-                while len(top_candidates | winning_candidates) > self.required_winners:
-                    top_candidates.remove(self.break_ties(top_candidates, True))
+                while (
+                    len(top_candidates | winning_candidates)
+                    > self.required_winners
+                ):
+                    top_candidates.remove(
+                        self.break_ties(top_candidates, True)
+                    )
 
             # Move the top candidates into the winning pile
             winning_candidates |= top_candidates
diff --git a/py3votecore/ranked_pairs.py b/py3votecore/ranked_pairs.py
index 4e326521..09d001ff 100644
--- a/py3votecore/ranked_pairs.py
+++ b/py3votecore/ranked_pairs.py
@@ -22,9 +22,10 @@
 
 # This class implements the Schulze Method (aka the beatpath method)
 class RankedPairs(CondorcetSystem, CondorcetHelper):
-
     def __init__(self, ballots, tie_breaker=None, ballot_notation=None):
-        super(RankedPairs, self).__init__(ballots, tie_breaker=tie_breaker, ballot_notation=ballot_notation)
+        super(RankedPairs, self).__init__(
+            ballots, tie_breaker=tie_breaker, ballot_notation=ballot_notation
+        )
 
     def condorcet_completion_method(self):
 
@@ -40,7 +41,9 @@ def condorcet_completion_method(self):
 
             # Find the strongest pair
             largest_strength = max(remaining_strong_pairs.values())
-            strongest_pairs = matching_keys(remaining_strong_pairs, largest_strength)
+            strongest_pairs = matching_keys(
+                remaining_strong_pairs, largest_strength
+            )
             if len(strongest_pairs) > 1:
                 r["tied_pairs"] = strongest_pairs
                 strongest_pair = self.break_ties(strongest_pairs)
diff --git a/py3votecore/schulze_by_graph.py b/py3votecore/schulze_by_graph.py
index 43fa056a..defb2615 100644
--- a/py3votecore/schulze_by_graph.py
+++ b/py3votecore/schulze_by_graph.py
@@ -21,14 +21,17 @@
 
 # This class provides Schulze Method results, but bypasses ballots and uses preference tallies instead.
 class SchulzeMethodByGraph(SchulzeMethod):
-
     def __init__(self, edges, tie_breaker=None, ballot_notation=None):
         self.edges = edges
-        super(SchulzeMethodByGraph, self).__init__([], tie_breaker=tie_breaker, ballot_notation=ballot_notation)
+        super(SchulzeMethodByGraph, self).__init__(
+            [], tie_breaker=tie_breaker, ballot_notation=ballot_notation
+        )
 
     def standardize_ballots(self, ballots, ballot_notation):
         self.ballots = []
-        self.candidates = set([edge[0] for edge, weight in self.edges.items()]) | set([edge[1] for edge, weight in self.edges.items()])
+        self.candidates = set(
+            [edge[0] for edge, weight in self.edges.items()]
+        ) | set([edge[1] for edge, weight in self.edges.items()])
 
     def ballots_into_graph(self, candidates, ballots):
         graph = digraph()
@@ -37,14 +40,22 @@ def ballots_into_graph(self, candidates, ballots):
             graph.add_edge(edge[0], edge[1])
         return graph
 
+
 # This class provides Schulze NPR results, but bypasses ballots and uses preference tallies instead.
 
 
 class SchulzeNPRByGraph(AbstractOrderingVotingSystem, SchulzeHelper):
-
-    def __init__(self, edges, winner_threshold=None, tie_breaker=None, ballot_notation=None):
+    def __init__(
+        self,
+        edges,
+        winner_threshold=None,
+        tie_breaker=None,
+        ballot_notation=None,
+    ):
         self.edges = edges
-        self.candidates = set([edge[0] for edge, weight in edges.items()]) | set([edge[1] for edge, weight in edges.items()])
+        self.candidates = set(
+            [edge[0] for edge, weight in edges.items()]
+        ) | set([edge[1] for edge, weight in edges.items()])
         super(SchulzeNPRByGraph, self).__init__(
             [],
             single_winner_class=SchulzeMethodByGraph,
@@ -53,7 +64,13 @@ def __init__(self, edges, winner_threshold=None, tie_breaker=None, ballot_notati
         )
 
     def ballots_without_candidate(self, ballots, candidate):
-        self.edges = dict([(edge, weight) for edge, weight in self.edges.items() if edge[0] != candidate and edge[1] != candidate])
+        self.edges = dict(
+            [
+                (edge, weight)
+                for edge, weight in self.edges.items()
+                if edge[0] != candidate and edge[1] != candidate
+            ]
+        )
         return self.edges
 
     def calculate_results(self):
diff --git a/py3votecore/schulze_helper.py b/py3votecore/schulze_helper.py
index 5f5fbe12..d6cbfe5a 100644
--- a/py3votecore/schulze_helper.py
+++ b/py3votecore/schulze_helper.py
@@ -13,7 +13,10 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see .
 
-from pygraph.algorithms.accessibility import accessibility, mutual_accessibility
+from pygraph.algorithms.accessibility import (
+    accessibility,
+    mutual_accessibility,
+)
 from pygraph.classes.digraph import digraph
 from pygraph.algorithms.minmax import maximum_flow
 from .condorcet import CondorcetHelper
@@ -31,7 +34,6 @@
 
 
 class SchulzeHelper(CondorcetHelper):
-
     def condorcet_completion_method(self):
         self.schwartz_set_heuristic()
 
@@ -44,7 +46,9 @@ def schwartz_set_heuristic(self):
             mutual_access = mutual_accessibility(self.graph)
             candidates_to_remove = set()
             for candidate in self.graph.nodes():
-                candidates_to_remove |= (set(access[candidate]) - set(mutual_access[candidate]))
+                candidates_to_remove |= set(access[candidate]) - set(
+                    mutual_access[candidate]
+                )
 
             # Remove nodes at the end of non-cycle paths
             if len(candidates_to_remove) > 0:
@@ -55,7 +59,13 @@ def schwartz_set_heuristic(self):
             # If none exist, remove the weakest edges
             else:
                 edge_weights = self.edge_weights(self.graph)
-                self.actions.append({'edges': matching_keys(edge_weights, min(edge_weights.values()))})
+                self.actions.append(
+                    {
+                        'edges': matching_keys(
+                            edge_weights, min(edge_weights.values())
+                        )
+                    }
+                )
                 for edge in self.actions[-1]["edges"]:
                     self.graph.del_edge(edge)
 
@@ -64,7 +74,9 @@ def schwartz_set_heuristic(self):
     def generate_vote_management_graph(self):
         self.vote_management_graph = digraph()
         self.vote_management_graph.add_nodes(self.completed_patterns)
-        self.vote_management_graph.del_node(tuple([PREFERRED_MORE] * self.required_winners))
+        self.vote_management_graph.del_node(
+            tuple([PREFERRED_MORE] * self.required_winners)
+        )
         self.pattern_nodes = self.vote_management_graph.nodes()
         self.vote_management_graph.add_nodes([NODE_SOURCE, NODE_SINK])
         for pattern_node in self.pattern_nodes:
@@ -83,21 +95,33 @@ def generate_completed_patterns(self):
         self.completed_patterns = []
         for i in range(0, self.required_winners + 1):
             for pattern in unique_permutations(
-                    [PREFERRED_LESS] * (self.required_winners - i)
-                    + [PREFERRED_MORE] * (i)
+                [PREFERRED_LESS] * (self.required_winners - i)
+                + [PREFERRED_MORE] * (i)
             ):
                 self.completed_patterns.append(tuple(pattern))
 
     def proportional_completion(self, candidate, other_candidates):
-        profile = dict(list(zip(self.completed_patterns, [0] * len(self.completed_patterns))))
+        profile = dict(
+            list(
+                zip(
+                    self.completed_patterns, [0] * len(self.completed_patterns)
+                )
+            )
+        )
 
         # Obtain an initial tally from the ballots
         for ballot in self.ballots:
             pattern = []
             for other_candidate in other_candidates:
-                if ballot["ballot"][candidate] < ballot["ballot"][other_candidate]:
+                if (
+                    ballot["ballot"][candidate]
+                    < ballot["ballot"][other_candidate]
+                ):
                     pattern.append(PREFERRED_LESS)
-                elif ballot["ballot"][candidate] == ballot["ballot"][other_candidate]:
+                elif (
+                    ballot["ballot"][candidate]
+                    == ballot["ballot"][other_candidate]
+                ):
                     pattern.append(PREFERRED_SAME)
                 else:
                     pattern.append(PREFERRED_MORE)
@@ -119,7 +143,10 @@ def proportional_completion(self, candidate, other_candidates):
         try:
             assert round(weight_sum, 5) == round(sum(profile.values()), 5)
         except:
-            print("Proportional completion broke (went from %s to %s)" % (weight_sum, sum(profile.values())))
+            print(
+                "Proportional completion broke (went from %s to %s)"
+                % (weight_sum, sum(profile.values()))
+            )
 
         return profile
 
@@ -156,16 +183,28 @@ def proportional_completion_round(self, completion_pattern, profile):
         # Reweight the remaining items
         for pattern in list(patterns_to_consider.keys()):
             if denominator == 0:
-                profile[pattern] += completion_pattern_weight / len(patterns_to_consider)
+                profile[pattern] += completion_pattern_weight / len(
+                    patterns_to_consider
+                )
             else:
                 if pattern not in profile:
                     profile[pattern] = 0
-                profile[pattern] += sum(profile[considered_pattern] for considered_pattern in patterns_to_consider[pattern]) * completion_pattern_weight / denominator
+                profile[pattern] += (
+                    sum(
+                        profile[considered_pattern]
+                        for considered_pattern in patterns_to_consider[pattern]
+                    )
+                    * completion_pattern_weight
+                    / denominator
+                )
 
         try:
             assert round(weight_sum, 5) == round(sum(profile.values()), 5)
         except:
-            print("Proportional completion round broke (went from %s to %s)" % (weight_sum, sum(profile.values())))
+            print(
+                "Proportional completion round broke (went from %s to %s)"
+                % (weight_sum, sum(profile.values()))
+            )
 
         return profile
 
@@ -178,18 +217,36 @@ def strength_of_vote_management(self, voter_profile):
 
         # Initialize the graph weights
         for pattern in self.pattern_nodes:
-            self.vote_management_graph.set_edge_weight((NODE_SOURCE, pattern), voter_profile[pattern])
+            self.vote_management_graph.set_edge_weight(
+                (NODE_SOURCE, pattern), voter_profile[pattern]
+            )
             for i in range(self.required_winners):
                 if pattern[i] == 1:
-                    self.vote_management_graph.set_edge_weight((pattern, i), voter_profile[pattern])
+                    self.vote_management_graph.set_edge_weight(
+                        (pattern, i), voter_profile[pattern]
+                    )
 
         # Iterate towards the limit
-        r = [(float(sum(voter_profile.values())) - voter_profile[tuple([PREFERRED_MORE] * self.required_winners)]) / self.required_winners]
+        r = [
+            (
+                float(sum(voter_profile.values()))
+                - voter_profile[
+                    tuple([PREFERRED_MORE] * self.required_winners)
+                ]
+            )
+            / self.required_winners
+        ]
         while len(r) < 2 or r[-2] - r[-1] > STRENGTH_TOLERANCE:
             for i in range(self.required_winners):
-                self.vote_management_graph.set_edge_weight((i, NODE_SINK), r[-1])
-            max_flow = maximum_flow(self.vote_management_graph, NODE_SOURCE, NODE_SINK)
-            sink_sum = sum(v for k, v in max_flow[0].items() if k[1] == NODE_SINK)
+                self.vote_management_graph.set_edge_weight(
+                    (i, NODE_SINK), r[-1]
+                )
+            max_flow = maximum_flow(
+                self.vote_management_graph, NODE_SOURCE, NODE_SINK
+            )
+            sink_sum = sum(
+                v for k, v in max_flow[0].items() if k[1] == NODE_SINK
+            )
             r.append(sink_sum / self.required_winners)
 
             # We expect strengths to be above a specified threshold
diff --git a/py3votecore/schulze_method.py b/py3votecore/schulze_method.py
index c8d89c34..0211269c 100644
--- a/py3votecore/schulze_method.py
+++ b/py3votecore/schulze_method.py
@@ -19,7 +19,6 @@
 
 # This class implements the Schulze Method (aka the beatpath method)
 class SchulzeMethod(CondorcetSystem, SchulzeHelper):
-
     def __init__(self, ballots, tie_breaker=None, ballot_notation=None):
         super(SchulzeMethod, self).__init__(
             ballots,
diff --git a/py3votecore/schulze_npr.py b/py3votecore/schulze_npr.py
index fb22f70c..396158f9 100644
--- a/py3votecore/schulze_npr.py
+++ b/py3votecore/schulze_npr.py
@@ -20,8 +20,13 @@
 
 #
 class SchulzeNPR(AbstractOrderingVotingSystem, SchulzeHelper):
-
-    def __init__(self, ballots, winner_threshold=None, tie_breaker=None, ballot_notation=None):
+    def __init__(
+        self,
+        ballots,
+        winner_threshold=None,
+        tie_breaker=None,
+        ballot_notation=None,
+    ):
         self.standardize_ballots(ballots, ballot_notation)
         super(SchulzeNPR, self).__init__(
             self.ballots,
diff --git a/py3votecore/schulze_pr.py b/py3votecore/schulze_pr.py
index f91e3af8..170b5892 100644
--- a/py3votecore/schulze_pr.py
+++ b/py3votecore/schulze_pr.py
@@ -21,8 +21,13 @@
 
 
 class SchulzePR(OrderingVotingSystem, SchulzeHelper):
-
-    def __init__(self, ballots, tie_breaker=None, winner_threshold=None, ballot_notation=None):
+    def __init__(
+        self,
+        ballots,
+        tie_breaker=None,
+        winner_threshold=None,
+        ballot_notation=None,
+    ):
         self.standardize_ballots(ballots, ballot_notation)
         super(SchulzePR, self).__init__(
             self.ballots,
@@ -39,7 +44,9 @@ def calculate_results(self):
         if self.winner_threshold is None:
             winner_threshold = len(self.candidates)
         else:
-            winner_threshold = min(len(self.candidates), self.winner_threshold + 1)
+            winner_threshold = min(
+                len(self.candidates), self.winner_threshold + 1
+            )
 
         for self.required_winners in range(1, winner_threshold):
 
@@ -55,12 +62,18 @@ def calculate_results(self):
 
             # Generate the edges between nodes
             for candidate_from in remaining_candidates:
-                other_candidates = sorted(list(remaining_candidates - set([candidate_from])))
+                other_candidates = sorted(
+                    list(remaining_candidates - set([candidate_from]))
+                )
                 for candidate_to in other_candidates:
-                    completed = self.proportional_completion(candidate_from, set([candidate_to]) | set(self.order))
+                    completed = self.proportional_completion(
+                        candidate_from, set([candidate_to]) | set(self.order)
+                    )
                     weight = self.strength_of_vote_management(completed)
                     if weight > 0:
-                        self.graph.add_edge((candidate_to, candidate_from), weight)
+                        self.graph.add_edge(
+                            (candidate_to, candidate_from), weight
+                        )
 
             # Determine the round winner through the Schwartz set heuristic
             self.schwartz_set_heuristic()
@@ -78,7 +91,9 @@ def calculate_results(self):
                 del self.tied_winners
 
         # Attach the last candidate as the sole winner if necessary
-        if self.winner_threshold is None or self.winner_threshold == len(self.candidates):
+        if self.winner_threshold is None or self.winner_threshold == len(
+            self.candidates
+        ):
             self.rounds.append({"winner": list(remaining_candidates)[0]})
             self.order.append(list(remaining_candidates)[0])
 
diff --git a/py3votecore/schulze_stv.py b/py3votecore/schulze_stv.py
index 4ae6ae99..84e90e41 100644
--- a/py3votecore/schulze_stv.py
+++ b/py3votecore/schulze_stv.py
@@ -21,10 +21,19 @@
 
 
 class SchulzeSTV(MultipleWinnerVotingSystem, SchulzeHelper):
-
-    def __init__(self, ballots, tie_breaker=None, required_winners=1, ballot_notation=None):
+    def __init__(
+        self,
+        ballots,
+        tie_breaker=None,
+        required_winners=1,
+        ballot_notation=None,
+    ):
         self.standardize_ballots(ballots, ballot_notation)
-        super(SchulzeSTV, self).__init__(self.ballots, tie_breaker=tie_breaker, required_winners=required_winners)
+        super(SchulzeSTV, self).__init__(
+            self.ballots,
+            tie_breaker=tie_breaker,
+            required_winners=required_winners,
+        )
 
     def calculate_results(self):
 
@@ -39,18 +48,34 @@ def calculate_results(self):
 
         # Build the graph of possible winners
         self.graph = digraph()
-        for candidate_set in itertools.combinations(self.candidates, self.required_winners):
+        for candidate_set in itertools.combinations(
+            self.candidates, self.required_winners
+        ):
             self.graph.add_nodes([tuple(sorted(list(candidate_set)))])
 
         # Generate the edges between nodes
-        for candidate_set in itertools.combinations(self.candidates, self.required_winners + 1):
+        for candidate_set in itertools.combinations(
+            self.candidates, self.required_winners + 1
+        ):
             for candidate in candidate_set:
-                other_candidates = sorted(set(candidate_set) - set([candidate]))
-                completed = self.proportional_completion(candidate, other_candidates)
+                other_candidates = sorted(
+                    set(candidate_set) - set([candidate])
+                )
+                completed = self.proportional_completion(
+                    candidate, other_candidates
+                )
                 weight = self.strength_of_vote_management(completed)
                 if weight > 0:
-                    for subset in itertools.combinations(other_candidates, len(other_candidates) - 1):
-                        self.graph.add_edge((tuple(other_candidates), tuple(sorted(list(subset) + [candidate]))), weight)
+                    for subset in itertools.combinations(
+                        other_candidates, len(other_candidates) - 1
+                    ):
+                        self.graph.add_edge(
+                            (
+                                tuple(other_candidates),
+                                tuple(sorted(list(subset) + [candidate])),
+                            ),
+                            weight,
+                        )
 
         # Determine the winner through the Schwartz set heuristic
         self.graph_winner()
diff --git a/py3votecore/stv.py b/py3votecore/stv.py
index 6f3266bf..84aa6f96 100644
--- a/py3votecore/stv.py
+++ b/py3votecore/stv.py
@@ -25,9 +25,10 @@
 # Alternate counting methods such as Meek's and Warren's would be nice, but
 # would need to be covered in a separate class.
 class STV(MultipleWinnerVotingSystem):
-
     def __init__(self, ballots, tie_breaker=None, required_winners=1):
-        super(STV, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners)
+        super(STV, self).__init__(
+            ballots, tie_breaker=tie_breaker, required_winners=required_winners
+        )
 
     def calculate_results(self):
 
@@ -46,7 +47,11 @@ def calculate_results(self):
         remaining_candidates = self.candidates - self.winners
 
         # Loop until we have enough candidates
-        while len(self.winners) < self.required_winners and len(remaining_candidates) + len(self.winners) != self.required_winners:
+        while (
+            len(self.winners) < self.required_winners
+            and len(remaining_candidates) + len(self.winners)
+            != self.required_winners
+        ):
 
             # Repopulate the remaining candidates if necessary
             if not remaining_candidates:
@@ -54,13 +59,28 @@ def calculate_results(self):
 
             # If all the votes have been used up, start from scratch for the remaining candidates
             round = {}
-            if len([ballot for ballot in ballots if ballot["count"] > 0 and ballot["ballot"]]) == 0:
+            if (
+                len(
+                    [
+                        ballot
+                        for ballot in ballots
+                        if ballot["count"] > 0 and ballot["ballot"]
+                    ]
+                )
+                == 0
+            ):
                 remaining_candidates = self.candidates - self.winners
                 round["note"] = "reset"
                 ballots = copy.deepcopy(self.ballots)
                 for ballot in ballots:
-                    ballot["ballot"] = [x for x in ballot["ballot"] if x in remaining_candidates]
-                quota = STV.droop_quota(ballots, self.required_winners - len(self.winners))
+                    ballot["ballot"] = [
+                        x
+                        for x in ballot["ballot"]
+                        if x in remaining_candidates
+                    ]
+                quota = STV.droop_quota(
+                    ballots, self.required_winners - len(self.winners)
+                )
 
             round["tallies"] = self.tallies(ballots)
             if round["tallies"]:
@@ -69,27 +89,41 @@ def calculate_results(self):
                 if max(round["tallies"].values()) >= quota:
 
                     # Collect candidates as winners
-                    round["winners"] = set([
-                        candidate
-                        for candidate, tally in list(round["tallies"].items())
-                        if tally >= self.quota
-                    ])
+                    round["winners"] = set(
+                        [
+                            candidate
+                            for candidate, tally in list(
+                                round["tallies"].items()
+                            )
+                            if tally >= self.quota
+                        ]
+                    )
                     self.winners |= round["winners"]
                     remaining_candidates -= round["winners"]
 
                     # Redistribute excess votes
                     for ballot in ballots:
-                        if ballot["ballot"] and ballot["ballot"][0] in round["winners"]:
-                            ballot["count"] *= (round["tallies"][ballot["ballot"][0]] - self.quota) / round["tallies"][ballot["ballot"][0]]
+                        if (
+                            ballot["ballot"]
+                            and ballot["ballot"][0] in round["winners"]
+                        ):
+                            ballot["count"] *= (
+                                round["tallies"][ballot["ballot"][0]]
+                                - self.quota
+                            ) / round["tallies"][ballot["ballot"][0]]
 
                     # Remove candidates from remaining ballots
-                    ballots = self.remove_candidates_from_ballots(round["winners"], ballots)
+                    ballots = self.remove_candidates_from_ballots(
+                        round["winners"], ballots
+                    )
 
                 # If no candidate exceeds the quota, elimiate the least preferred
                 else:
                     round.update(self.loser(round["tallies"]))
                     remaining_candidates.remove(round["loser"])
-                    ballots = self.remove_candidates_from_ballots([round["loser"]], ballots)
+                    ballots = self.remove_candidates_from_ballots(
+                        [round["loser"]], ballots
+                    )
 
             # Record this round's actions
             self.rounds.append(round)
@@ -114,7 +148,7 @@ def loser(self, tallies):
         else:
             return {
                 "tied_losers": losers,
-                "loser": self.break_ties(losers, True)
+                "loser": self.break_ties(losers, True),
             }
 
     @staticmethod
@@ -133,7 +167,9 @@ def tallies(self, ballots):
         for ballot in ballots:
             if ballot["ballot"]:
                 tallies[ballot["ballot"][0]] += ballot["count"]
-        return dict((candidate, votes) for (candidate, votes) in tallies.items())
+        return dict(
+            (candidate, votes) for (candidate, votes) in tallies.items()
+        )
 
     @staticmethod
     def droop_quota(ballots, seats=1):
diff --git a/py3votecore/tie_breaker.py b/py3votecore/tie_breaker.py
index e85174bd..d9e9b3bc 100644
--- a/py3votecore/tie_breaker.py
+++ b/py3votecore/tie_breaker.py
@@ -54,8 +54,17 @@ def break_complex_ties(tied_candidates, random_ordering):
         max_columns = len(list(tied_candidates)[0])
         column = 0
         while len(tied_candidates) > 1 and column < max_columns:
-            min_index = min(random_ordering.index(list(candidate)[column]) for candidate in tied_candidates)
-            tied_candidates = set([candidate for candidate in tied_candidates if candidate[column] == random_ordering[min_index]])
+            min_index = min(
+                random_ordering.index(list(candidate)[column])
+                for candidate in tied_candidates
+            )
+            tied_candidates = set(
+                [
+                    candidate
+                    for candidate in tied_candidates
+                    if candidate[column] == random_ordering[min_index]
+                ]
+            )
             column += 1
         return list(tied_candidates)[0]
 
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..5be18d73
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,5 @@
+[tool.black]
+line-length = 79
+# NOTE: PEP8 does recommend double-quotes, but to start with we'll support
+# single quotes as to minimize the patch/noise produced by introducing black
+skip-string-normalization = true
diff --git a/requirements.txt b/requirements.txt
index 2f54e92b..74eaf00f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -28,6 +28,9 @@ requests==2.22.0
 signxml==2.7.2
 suds-jurko==0.6
 
+# Development
+black==22.3.0
+
 # Unpinned (not found in production!)
 dj_database_url
 gunicorn
diff --git a/requirements.txt.freeze b/requirements.txt.freeze
index ad73691c..678e332f 100644
--- a/requirements.txt.freeze
+++ b/requirements.txt.freeze
@@ -1,10 +1,12 @@
 asn1crypto==1.3.0
 backcall==0.1.0
 beautifulsoup4==4.8.2
+black==22.3.0
 cachetools==4.0.0
 certifi==2019.11.28
 cffi==1.13.2
 chardet==3.0.4
+click==8.1.3
 commonmark==0.8.0
 cryptography==2.8
 cssselect==1.1.0
@@ -29,11 +31,14 @@ lxml==4.4.2
 Markdown==3.1.1
 markdown2==2.3.8
 mdmail==0.1.3
+mypy-extensions==0.4.3
 parso==0.6.1
+pathspec==0.9.0
 pexpect==4.8.0
 pickleshare==0.7.5
 pilkit==2.0
 Pillow==7.0.0
+platformdirs==2.5.2
 premailer==3.6.1
 prompt-toolkit==2.0.10
 ptyprocess==0.6.0
@@ -49,6 +54,8 @@ six==1.14.0
 soupsieve==1.9.5
 sqlparse==0.3.0
 suds-jurko==0.6
+tomli==2.0.1
 traitlets==4.3.3
+typing-extensions==4.2.0
 urllib3==1.25.7
 wcwidth==0.1.8
diff --git a/tasks/forms.py b/tasks/forms.py
index 1a07be29..dbba08c0 100644
--- a/tasks/forms.py
+++ b/tasks/forms.py
@@ -5,12 +5,15 @@
 
 from tasks.models import Task
 
+
 class TaskForm(Wasa2ilForm):
     short_description = CharField(
         widget=Textarea(attrs={'rows': 2}),
         label=_('Short description'),
         max_length=200,
-        help_text=_('Clearly state the objective of the task. Maximum 200 letters.')
+        help_text=_(
+            'Clearly state the objective of the task. Maximum 200 letters.'
+        ),
     )
 
     class Meta:
@@ -26,5 +29,3 @@ class Meta:
             'categories',
             'skills',
         )
-
-
diff --git a/tasks/migrations/0001_initial.py b/tasks/migrations/0001_initial.py
index eb1613d7..3fe08da6 100644
--- a/tasks/migrations/0001_initial.py
+++ b/tasks/migrations/0001_initial.py
@@ -20,12 +20,26 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='Task',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'name',
+                    models.CharField(max_length=128, verbose_name='Name'),
+                ),
                 ('slug', models.SlugField(blank=True, max_length=128)),
                 ('description', models.TextField(verbose_name='Description')),
                 ('objectives', models.TextField(verbose_name='Objectives')),
-                ('requirements', models.TextField(verbose_name='Requirements')),
+                (
+                    'requirements',
+                    models.TextField(verbose_name='Requirements'),
+                ),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('modified', models.DateTimeField(auto_now=True)),
                 ('volunteers_needed', models.IntegerField(default=1)),
@@ -38,25 +52,61 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='TaskCategory',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
                 ('name', models.CharField(max_length=128)),
             ],
         ),
         migrations.CreateModel(
             name='TaskRequest',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
                 ('date_offered', models.DateTimeField(auto_now_add=True)),
                 ('is_accepted', models.BooleanField(default=False)),
                 ('whyme', models.TextField(verbose_name='Why me?')),
-                ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.Task')),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+                (
+                    'task',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='tasks.Task',
+                    ),
+                ),
+                (
+                    'user',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
             name='TaskSkill',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
                 ('name', models.CharField(max_length=128)),
             ],
         ),
@@ -68,17 +118,33 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='task',
             name='created_by',
-            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='task_created_by', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='task_created_by',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
         migrations.AddField(
             model_name='task',
             name='modified_by',
-            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='task_modified_by', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='task_modified_by',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
         migrations.AddField(
             model_name='task',
             name='polity',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity'),
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE, to='polity.Polity'
+            ),
         ),
         migrations.AddField(
             model_name='task',
diff --git a/tasks/migrations/0002_auto_20190730_2053.py b/tasks/migrations/0002_auto_20190730_2053.py
index b66be329..1504a9f3 100644
--- a/tasks/migrations/0002_auto_20190730_2053.py
+++ b/tasks/migrations/0002_auto_20190730_2053.py
@@ -15,21 +15,29 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='task',
             name='estimated_duration_weeks',
-            field=models.IntegerField(default=1, verbose_name='Estimated number of weeks'),
+            field=models.IntegerField(
+                default=1, verbose_name='Estimated number of weeks'
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='estimated_hours_per_week',
-            field=models.IntegerField(default=1, verbose_name='Estimated hours per week'),
+            field=models.IntegerField(
+                default=1, verbose_name='Estimated hours per week'
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='is_recruiting',
-            field=models.BooleanField(default=True, verbose_name='Is recruiting'),
+            field=models.BooleanField(
+                default=True, verbose_name='Is recruiting'
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='volunteers_needed',
-            field=models.IntegerField(default=1, verbose_name='Number of volunteers needed'),
+            field=models.IntegerField(
+                default=1, verbose_name='Number of volunteers needed'
+            ),
         ),
     ]
diff --git a/tasks/migrations/0003_taskrequest_available_time.py b/tasks/migrations/0003_taskrequest_available_time.py
index 53a4c1f7..79a347d8 100644
--- a/tasks/migrations/0003_taskrequest_available_time.py
+++ b/tasks/migrations/0003_taskrequest_available_time.py
@@ -15,7 +15,9 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='taskrequest',
             name='available_time',
-            field=models.TextField(default='', verbose_name='What available time do I have?'),
+            field=models.TextField(
+                default='', verbose_name='What available time do I have?'
+            ),
             preserve_default=False,
         ),
     ]
diff --git a/tasks/migrations/0004_auto_20190822_2006.py b/tasks/migrations/0004_auto_20190822_2006.py
index 65175149..c7ff92e9 100644
--- a/tasks/migrations/0004_auto_20190822_2006.py
+++ b/tasks/migrations/0004_auto_20190822_2006.py
@@ -15,11 +15,25 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='task',
             name='created_by',
-            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_created_by', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='task_created_by',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='modified_by',
-            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_modified_by', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='task_modified_by',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
     ]
diff --git a/tasks/migrations/0006_auto_20190831_1804.py b/tasks/migrations/0006_auto_20190831_1804.py
index 0d600e4f..4c574741 100644
--- a/tasks/migrations/0006_auto_20190831_1804.py
+++ b/tasks/migrations/0006_auto_20190831_1804.py
@@ -15,31 +15,49 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='task',
             name='detailed_description',
-            field=models.TextField(blank=True, null=True, verbose_name='Detailed description'),
+            field=models.TextField(
+                blank=True, null=True, verbose_name='Detailed description'
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='estimated_duration_weeks',
-            field=models.IntegerField(default=1, help_text='Select 0 if not applicable.', verbose_name='Estimated number of weeks'),
+            field=models.IntegerField(
+                default=1,
+                help_text='Select 0 if not applicable.',
+                verbose_name='Estimated number of weeks',
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='estimated_hours_per_week',
-            field=models.IntegerField(default=1, help_text='Select 0 if not applicable.', verbose_name='Estimated hours per week'),
+            field=models.IntegerField(
+                default=1,
+                help_text='Select 0 if not applicable.',
+                verbose_name='Estimated hours per week',
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='requirements',
-            field=models.TextField(blank=True, null=True, verbose_name='Requirements'),
+            field=models.TextField(
+                blank=True, null=True, verbose_name='Requirements'
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='short_description',
-            field=models.CharField(max_length=200, verbose_name='Short description'),
+            field=models.CharField(
+                max_length=200, verbose_name='Short description'
+            ),
         ),
         migrations.AlterField(
             model_name='task',
             name='volunteers_needed',
-            field=models.IntegerField(default=1, help_text='Select 0 if not applicable.', verbose_name='Number of volunteers needed'),
+            field=models.IntegerField(
+                default=1,
+                help_text='Select 0 if not applicable.',
+                verbose_name='Number of volunteers needed',
+            ),
         ),
     ]
diff --git a/tasks/migrations/0007_task_require_phone.py b/tasks/migrations/0007_task_require_phone.py
index 8e3fb10f..931b78b8 100644
--- a/tasks/migrations/0007_task_require_phone.py
+++ b/tasks/migrations/0007_task_require_phone.py
@@ -15,6 +15,10 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='task',
             name='require_phone',
-            field=models.BooleanField(default=True, help_text='Make users provide their phone numbers in the profiles to partake in the task.', verbose_name='Require phone number from volunteers'),
+            field=models.BooleanField(
+                default=True,
+                help_text='Make users provide their phone numbers in the profiles to partake in the task.',
+                verbose_name='Require phone number from volunteers',
+            ),
         ),
     ]
diff --git a/tasks/migrations/0008_merge_20191019_1939.py b/tasks/migrations/0008_merge_20191019_1939.py
index 7efc2b2e..cc9c610a 100644
--- a/tasks/migrations/0008_merge_20191019_1939.py
+++ b/tasks/migrations/0008_merge_20191019_1939.py
@@ -10,5 +10,4 @@ class Migration(migrations.Migration):
         ('tasks', '0007_task_require_phone'),
     ]
 
-    operations = [
-    ]
+    operations = []
diff --git a/tasks/models.py b/tasks/models.py
index 920a1162..b72ab548 100644
--- a/tasks/models.py
+++ b/tasks/models.py
@@ -39,9 +39,10 @@
     (11, _('Technical development')),
     (12, _('Management')),
     (13, _('Planning and execution')),
-    (14, _('Translating'))
+    (14, _('Translating')),
 )
 
+
 class Task(models.Model):
     polity = models.ForeignKey('polity.Polity', on_delete=CASCADE)
     categories = models.ManyToManyField('tasks.TaskCategory')
@@ -50,9 +51,15 @@ class Task(models.Model):
     name = models.CharField(max_length=128, verbose_name=_('Name'))
     slug = models.SlugField(max_length=128, blank=True)
 
-    short_description = models.CharField(max_length=200, verbose_name=_("Short description"))
-    detailed_description = models.TextField(verbose_name=_("Detailed description"), null=True, blank=True)
-    requirements = models.TextField(verbose_name=_("Requirements"), null=True, blank=True)
+    short_description = models.CharField(
+        max_length=200, verbose_name=_("Short description")
+    )
+    detailed_description = models.TextField(
+        verbose_name=_("Detailed description"), null=True, blank=True
+    )
+    requirements = models.TextField(
+        verbose_name=_("Requirements"), null=True, blank=True
+    )
 
     created_by = models.ForeignKey(
         settings.AUTH_USER_MODEL,
@@ -60,7 +67,7 @@ class Task(models.Model):
         null=True,
         blank=True,
         related_name='task_created_by',
-        on_delete=SET_NULL
+        on_delete=SET_NULL,
     )
     modified_by = models.ForeignKey(
         settings.AUTH_USER_MODEL,
@@ -68,22 +75,44 @@ class Task(models.Model):
         null=True,
         blank=True,
         related_name='task_modified_by',
-        on_delete=SET_NULL
+        on_delete=SET_NULL,
     )
     created = models.DateTimeField(auto_now_add=True)
     modified = models.DateTimeField(auto_now=True)
 
-    volunteers_needed = models.IntegerField(default=1, verbose_name=_('Number of volunteers needed'), help_text=_('Select 0 if not applicable.'))
-    estimated_hours_per_week = models.IntegerField(default=1, verbose_name=_('Estimated hours per week'), help_text=_('Select 0 if not applicable.'))
-    estimated_duration_weeks = models.IntegerField(default=1, verbose_name=_('Estimated number of weeks'), help_text=_('Select 0 if not applicable.'))
+    volunteers_needed = models.IntegerField(
+        default=1,
+        verbose_name=_('Number of volunteers needed'),
+        help_text=_('Select 0 if not applicable.'),
+    )
+    estimated_hours_per_week = models.IntegerField(
+        default=1,
+        verbose_name=_('Estimated hours per week'),
+        help_text=_('Select 0 if not applicable.'),
+    )
+    estimated_duration_weeks = models.IntegerField(
+        default=1,
+        verbose_name=_('Estimated number of weeks'),
+        help_text=_('Select 0 if not applicable.'),
+    )
 
-    require_phone = models.BooleanField(default=True, verbose_name=_('Require phone number from volunteers'), help_text=_('Make users provide their phone numbers in the profiles to partake in the task.'))
+    require_phone = models.BooleanField(
+        default=True,
+        verbose_name=_('Require phone number from volunteers'),
+        help_text=_(
+            'Make users provide their phone numbers in the profiles to partake in the task.'
+        ),
+    )
 
     is_done = models.BooleanField(default=False)
-    is_recruiting = models.BooleanField(default=True, verbose_name=_('Is recruiting'))
+    is_recruiting = models.BooleanField(
+        default=True, verbose_name=_('Is recruiting')
+    )
 
     def accepted_volunteers(self):
-        return self.taskrequest_set.filter(is_accepted=True).select_related('user')
+        return self.taskrequest_set.filter(is_accepted=True).select_related(
+            'user'
+        )
 
     def applied_volunteers(self):
         return self.taskrequest_set.select_related('user')
@@ -98,16 +127,20 @@ class TaskCategory(models.Model):
     def __str__(self):
         return self.name
 
+
 class TaskSkill(models.Model):
     name = models.CharField(max_length=128)
 
     def __str__(self):
         return self.name
 
+
 class TaskRequest(models.Model):
     task = models.ForeignKey('tasks.Task', on_delete=CASCADE)
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE)
     date_offered = models.DateTimeField(auto_now_add=True)
     is_accepted = models.BooleanField(default=False)
     whyme = models.TextField(verbose_name=_("Why me?"))
-    available_time = models.TextField(verbose_name=_('What available time do I have?'))
+    available_time = models.TextField(
+        verbose_name=_('What available time do I have?')
+    )
diff --git a/tasks/urls.py b/tasks/urls.py
index 8b8db066..d079b51f 100644
--- a/tasks/urls.py
+++ b/tasks/urls.py
@@ -12,11 +12,34 @@
 
 urlpatterns = [
     url(r'^polity/(?P\d+)/tasks/$', task_main, name='task_main'),
-    url(r'^polity/(?P\d+)/tasks/(?P\d+)/edit/$', task_add_edit, name='task_edit'),
-    url(r'^polity/(?P\d+)/tasks/(?P\d+)/delete/$', task_delete, name='task_delete'),
-    url(r'^polity/(?P\d+)/tasks/new/$', task_add_edit, name='task_add'),
-    url(r'^polity/(?P\d+)/tasks/(?P\d+)/$', task_detail, name='task_detail'),
-    url(r'^polity/(?P\d+)/tasks/applications/$', task_applications, name='task_applications'),
-
-    url(r'^accounts/profile/(?:(?P[^/]+)/tasks/)?$', task_user_tasks, name='task_user_tasks'),
+    url(
+        r'^polity/(?P\d+)/tasks/(?P\d+)/edit/$',
+        task_add_edit,
+        name='task_edit',
+    ),
+    url(
+        r'^polity/(?P\d+)/tasks/(?P\d+)/delete/$',
+        task_delete,
+        name='task_delete',
+    ),
+    url(
+        r'^polity/(?P\d+)/tasks/new/$',
+        task_add_edit,
+        name='task_add',
+    ),
+    url(
+        r'^polity/(?P\d+)/tasks/(?P\d+)/$',
+        task_detail,
+        name='task_detail',
+    ),
+    url(
+        r'^polity/(?P\d+)/tasks/applications/$',
+        task_applications,
+        name='task_applications',
+    ),
+    url(
+        r'^accounts/profile/(?:(?P[^/]+)/tasks/)?$',
+        task_user_tasks,
+        name='task_user_tasks',
+    ),
 ]
diff --git a/tasks/views.py b/tasks/views.py
index 08b47fe2..22d1419b 100644
--- a/tasks/views.py
+++ b/tasks/views.py
@@ -14,6 +14,7 @@
 from polity.models import Polity
 from tasks.forms import TaskForm
 
+
 def task_main(request, polity_id):
     polity = get_object_or_404(Polity, id=polity_id)
 
@@ -21,14 +22,11 @@ def task_main(request, polity_id):
     tasks = Task.objects.filter(is_recruiting=True, is_done=False)
 
     # Front polity's tasks are always shown.
-    front_tasks = tasks.filter(
-        polity__is_front_polity=True
-    )
+    front_tasks = tasks.filter(polity__is_front_polity=True)
 
     # Sub-polity's tasks are only shown if they exist.
     sub_polity_tasks = tasks.filter(
-        polity_id=polity_id,
-        polity__is_front_polity=False
+        polity_id=polity_id, polity__is_front_polity=False
     )
 
     total_task_count = len(front_tasks) + len(sub_polity_tasks)
@@ -50,13 +48,10 @@ def task_user_tasks(request, username):
     # currently not used but still required to make sure that links are
     # created with this specification in mind.
 
-    taskrequests = TaskRequest.objects.select_related(
-        'task'
-    ).prefetch_related(
-        'task__skills',
-        'task__categories'
-    ).filter(
-        user_id=request.user.id
+    taskrequests = (
+        TaskRequest.objects.select_related('task')
+        .prefetch_related('task__skills', 'task__categories')
+        .filter(user_id=request.user.id)
     )
 
     tasks = [req.task for req in taskrequests]
@@ -70,7 +65,9 @@ def task_user_tasks(request, username):
 @login_required
 def task_add_edit(request, polity_id, task_id=None):
     polity = get_object_or_404(Polity, id=polity_id)
-    if not (polity.is_member(request.user) or polity.is_wrangler(request.user)):
+    if not (
+        polity.is_member(request.user) or polity.is_wrangler(request.user)
+    ):
         raise PermissionDenied()
 
     if task_id:
@@ -146,7 +143,9 @@ def task_detail(request, polity_id, task_id):
 
 def task_applications(request, polity_id):
     polity = get_object_or_404(Polity, id=polity_id)
-    if not (polity.is_member(request.user) or polity.is_wrangler(request.user)):
+    if not (
+        polity.is_member(request.user) or polity.is_wrangler(request.user)
+    ):
         raise PermissionDenied()
 
     done = request.POST.get('done', None)
@@ -192,9 +191,9 @@ def task_applications(request, polity_id):
         # Prefetch the data for the User model that we need to determine statistics and such.
         Prefetch(
             'taskrequest_set__user',
-            queryset=User.objects.annotate_task_stats()
+            queryset=User.objects.annotate_task_stats(),
         ),
-        'taskrequest_set__user__userprofile'
+        'taskrequest_set__user__userprofile',
     ).order_by('-created')
     if not show_done:
         tasks = tasks.filter(is_done=False)
diff --git a/tests/test_views.py b/tests/test_views.py
index 240f45ef..081f2284 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -2,8 +2,8 @@
 from django.urls import reverse
 from django.contrib.auth.models import User
 
-class ViewTests(TestCase):
 
+class ViewTests(TestCase):
     @classmethod
     def setUpTestData(cls):
         # Runs once to set up non-modified data for all class methods.
@@ -15,7 +15,6 @@ def setUp(self):
         user1 = User.objects.create_user(username='user1', password='password')
         user1.save()
 
-
     def test_non_endpoints(self):
         response = self.client.get('/this-is-not-an-endpoint', follow=True)
         self.assertEqual(response.status_code, 404)
@@ -30,7 +29,9 @@ def test_admin_endpoint(self):
 
     def test_redirect_if_not_logged_in(self):
         response = self.client.get('/accounts/profile/')
-        self.assertRedirects(response, '/accounts/login/?next=/accounts/profile/')
+        self.assertRedirects(
+            response, '/accounts/login/?next=/accounts/profile/'
+        )
 
     def test_user_can_log_in(self):
         login = self.client.login(username='user1', password='password')
diff --git a/topic/admin.py b/topic/admin.py
index 5ccd9e97..cdce1050 100644
--- a/topic/admin.py
+++ b/topic/admin.py
@@ -2,6 +2,7 @@
 
 from topic.models import Topic
 
+
 class TopicAdmin(admin.ModelAdmin):
     fieldsets = None
     list_display = ['name', 'slug', 'description', 'polity']
diff --git a/topic/dataviews.py b/topic/dataviews.py
index f722b9fb..23490c6b 100644
--- a/topic/dataviews.py
+++ b/topic/dataviews.py
@@ -33,7 +33,10 @@ def topic_star(request):
         ctx["starred"] = True
 
     topics = topic.polity.topic_set.listing_info(request.user)
-    ctx["html"] = render_to_string("topic/_topic_list_table.html", {"topics": topics, "user": request.user, "polity": topic.polity})
+    ctx["html"] = render_to_string(
+        "topic/_topic_list_table.html",
+        {"topics": topics, "user": request.user, "polity": topic.polity},
+    )
 
     ctx["ok"] = True
 
@@ -55,7 +58,10 @@ def topic_showstarred(request):
         try:
             polity = Polity.objects.get(id=polity)
             topics = polity.topic_set.listing_info(request.user)
-            ctx["html"] = render_to_string("topic/_topic_list_table.html", {"topics": topics, "user": request.user, "polity": polity})
+            ctx["html"] = render_to_string(
+                "topic/_topic_list_table.html",
+                {"topics": topics, "user": request.user, "polity": polity},
+            )
         except Exception as e:
             ctx["error"] = e
 
diff --git a/topic/migrations/0001_initial.py b/topic/migrations/0001_initial.py
index 61ff2afb..a53f3f5d 100644
--- a/topic/migrations/0001_initial.py
+++ b/topic/migrations/0001_initial.py
@@ -20,15 +20,57 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='Topic',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'name',
+                    models.CharField(max_length=128, verbose_name='Name'),
+                ),
                 ('slug', models.SlugField(blank=True, max_length=128)),
-                ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
+                (
+                    'description',
+                    models.TextField(
+                        blank=True, null=True, verbose_name='Description'
+                    ),
+                ),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('modified', models.DateTimeField(auto_now=True)),
-                ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topic_created_by', to=settings.AUTH_USER_MODEL)),
-                ('modified_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topic_modified_by', to=settings.AUTH_USER_MODEL)),
-                ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')),
+                (
+                    'created_by',
+                    models.ForeignKey(
+                        blank=True,
+                        editable=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='topic_created_by',
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                (
+                    'modified_by',
+                    models.ForeignKey(
+                        blank=True,
+                        editable=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='topic_modified_by',
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                (
+                    'polity',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='polity.Polity',
+                    ),
+                ),
             ],
             options={
                 'ordering': ['name'],
@@ -37,9 +79,29 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='UserTopic',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topic.Topic')),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'topic',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='topic.Topic',
+                    ),
+                ),
+                (
+                    'user',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
             ],
         ),
         migrations.AlterUniqueTogether(
diff --git a/topic/migrations/0002_auto_20190822_2006.py b/topic/migrations/0002_auto_20190822_2006.py
index e4f80b18..f7b0416b 100644
--- a/topic/migrations/0002_auto_20190822_2006.py
+++ b/topic/migrations/0002_auto_20190822_2006.py
@@ -15,11 +15,25 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='topic',
             name='created_by',
-            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='topic_created_by', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='topic_created_by',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
         migrations.AlterField(
             model_name='topic',
             name='modified_by',
-            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='topic_modified_by', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='topic_modified_by',
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
     ]
diff --git a/topic/models.py b/topic/models.py
index 6c0b5f12..82d92604 100644
--- a/topic/models.py
+++ b/topic/models.py
@@ -36,9 +36,9 @@ def listing_info(self, user):
                     favorited=Count(
                         Case(
                             When(usertopic__user=user, then=True),
-                            output_field=BooleanField
+                            output_field=BooleanField,
                         ),
-                        distinct=True
+                        distinct=True,
                     )
                 )
 
@@ -46,24 +46,32 @@ def listing_info(self, user):
         topics = topics.annotate(issue_count=Count('issue', distinct=True))
 
         # Annotate usertopic count.
-        topics = topics.annotate(usertopic_count=Count('usertopic', distinct=True))
+        topics = topics.annotate(
+            usertopic_count=Count('usertopic', distinct=True)
+        )
 
         # Annotate counts of issues that are open and/or being voted on.
         topics = topics.annotate(
             issues_open=Count(
                 Case(
                     When(issue__deadline_votes__gte=now, then=True),
-                    output_field=IntegerField()
+                    output_field=IntegerField(),
                 ),
-                distinct=True
+                distinct=True,
             ),
             issues_voting=Count(
                 Case(
-                    When(Q(issue__deadline_votes__gte=now, issue__deadline_proposals__lt=now), then=True),
-                    output_field=IntegerField()
+                    When(
+                        Q(
+                            issue__deadline_votes__gte=now,
+                            issue__deadline_proposals__lt=now,
+                        ),
+                        then=True,
+                    ),
+                    output_field=IntegerField(),
                 ),
-                distinct=True
-            )
+                distinct=True,
+            ),
         )
 
         return topics
@@ -71,12 +79,15 @@ def listing_info(self, user):
 
 class Topic(models.Model):
     """A collection of issues unified categorically."""
+
     objects = TopicQuerySet.as_manager()
 
     name = models.CharField(max_length=128, verbose_name=_('Name'))
     slug = models.SlugField(max_length=128, blank=True)
 
-    description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
+    description = models.TextField(
+        verbose_name=_("Description"), null=True, blank=True
+    )
 
     created_by = models.ForeignKey(
         settings.AUTH_USER_MODEL,
@@ -84,7 +95,7 @@ class Topic(models.Model):
         null=True,
         blank=True,
         related_name='topic_created_by',
-        on_delete=SET_NULL
+        on_delete=SET_NULL,
     )
     modified_by = models.ForeignKey(
         settings.AUTH_USER_MODEL,
@@ -92,7 +103,7 @@ class Topic(models.Model):
         null=True,
         blank=True,
         related_name='topic_modified_by',
-        on_delete=SET_NULL
+        on_delete=SET_NULL,
     )
     created = models.DateTimeField(auto_now_add=True)
     modified = models.DateTimeField(auto_now=True)
@@ -103,7 +114,9 @@ class Meta:
         ordering = ["name"]
 
     def new_comments(self):
-        return Comment.objects.filter(issue__topics=self).order_by("-created")[:10]
+        return Comment.objects.filter(issue__topics=self).order_by("-created")[
+            :10
+        ]
 
     def __str__(self):
         return u'%s' % (self.name)
@@ -111,6 +124,7 @@ def __str__(self):
 
 class UserTopic(models.Model):
     """Whether a user likes a topic."""
+
     topic = models.ForeignKey('topic.Topic', on_delete=CASCADE)
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE)
 
diff --git a/topic/urls.py b/topic/urls.py
index bef5abe2..22b4b9f4 100644
--- a/topic/urls.py
+++ b/topic/urls.py
@@ -9,12 +9,22 @@
 
 
 urlpatterns = [
-    url(r'^polity/(?P\d+)/topic/new/$', topic_add_edit, name='topic_add'),
-    url(r'^polity/(?P\d+)/topic/(?P\d+)/edit/$', topic_add_edit, name='topic_edit'),
-    url(r'^polity/(?P\d+)/topic/(?P\d+)/$', topic_view, name='topic'),
+    url(
+        r'^polity/(?P\d+)/topic/new/$',
+        topic_add_edit,
+        name='topic_add',
+    ),
+    url(
+        r'^polity/(?P\d+)/topic/(?P\d+)/edit/$',
+        topic_add_edit,
+        name='topic_edit',
+    ),
+    url(
+        r'^polity/(?P\d+)/topic/(?P\d+)/$',
+        topic_view,
+        name='topic',
+    ),
     url(r'^polity/(?P\d+)/topics/$', topic_list, name='topics'),
-
-
     url(r'^api/topic/star/$', topic_star),
     url(r'^api/topic/showstarred/$', topic_showstarred),
 ]
diff --git a/topic/views.py b/topic/views.py
index 80dcc011..fd83039d 100644
--- a/topic/views.py
+++ b/topic/views.py
@@ -49,6 +49,7 @@ def topic_view(request, polity_id, topic_id):
     }
     return render(request, 'topic/topic_detail.html', ctx)
 
+
 def topic_list(request, polity_id):
     polity = get_object_or_404(Polity, id=polity_id)
 
diff --git a/urls.py b/urls.py
index 268f8bb2..c093cbd0 100644
--- a/urls.py
+++ b/urls.py
@@ -19,50 +19,79 @@
     # Uncomment the next line to enable the admin:
     url(r'^admin/', admin.site.urls),
     url(r'^admintools/$', core_views.view_admintools, name='admin_tools'),
-    url(r'^admintools/push/$', core_views.view_admintools_push, name='admin_tools_push'),
+    url(
+        r'^admintools/push/$',
+        core_views.view_admintools_push,
+        name='admin_tools_push',
+    ),
     # Enabling i18n language changes per
     # https://docs.djangoproject.com/en/1.4/topics/i18n/translation/#the-set-language-redirect-view
     url(r'^i18n/', include('django.conf.urls.i18n')),
-
     url(r'^', include('election.urls')),
     url(r'^', include('issue.urls')),
     url(r'^', include('core.urls')),
     url(r'^', include('polity.urls')),
     url(r'^', include('topic.urls')),
     url(r'^', include('emailconfirmation.urls')),
-
-    url(r'^accounts/profile/(?:(?P[^/]+)/)?$', core_views.profile, name='profile'),
-    url(r'^accounts/settings/', core_views.view_settings, name='account_settings'),
-    url(r'^accounts/personal-data/fetch/', core_views.personal_data_fetch, name='personal_data_fetch'),
-    url(r'^accounts/personal-data/', core_views.personal_data, name='personal_data'),
-
+    url(
+        r'^accounts/profile/(?:(?P[^/]+)/)?$',
+        core_views.profile,
+        name='profile',
+    ),
+    url(
+        r'^accounts/settings/',
+        core_views.view_settings,
+        name='account_settings',
+    ),
+    url(
+        r'^accounts/personal-data/fetch/',
+        core_views.personal_data_fetch,
+        name='personal_data_fetch',
+    ),
+    url(
+        r'^accounts/personal-data/',
+        core_views.personal_data,
+        name='personal_data',
+    ),
     url(r'^accounts/sso/', core_views.sso),
-    url(r'^accounts/register/$', core_views.Wasa2ilRegistrationView.as_view(), name='registration_register'),
+    url(
+        r'^accounts/register/$',
+        core_views.Wasa2ilRegistrationView.as_view(),
+        name='registration_register',
+    ),
     url(
         r'^accounts/activate/(?P\w+)/$',
         core_views.Wasa2ilActivationView.as_view(),
-        name='registration_activate'
+        name='registration_activate',
     ),
     url(
         r'^accounts/password/reset/$',
         auth_views.PasswordResetView.as_view(
             email_template_name='registration/password_reset_email.txt',
-            html_email_template_name='registration/password_reset_email.html'
+            html_email_template_name='registration/password_reset_email.html',
         ),
-        name='auth_password_reset'
+        name='auth_password_reset',
+    ),
+    url(
+        r'^accounts/reset-password/done/$',
+        auth_views.PasswordResetDoneView.as_view(),
+        name='password_reset_done',
     ),
-    url(r'^accounts/reset-password/done/$', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
-
     # SAML-related URLs.
     url(r'^accounts/verify/', core_views.verify),
-    url(r'^accounts/login-or-saml-redirect/', core_views.login_or_saml_redirect, name='login_or_saml_redirect'),
-
+    url(
+        r'^accounts/login-or-saml-redirect/',
+        core_views.login_or_saml_redirect,
+        name='login_or_saml_redirect',
+    ),
     url(r'^accounts/', include('registration.backends.default.urls')),
-
     url(r'^help/$', TemplateView.as_view(template_name='help/is/index.html')),
     url(r'^help/(?P.*)/$', core_views.help),
-
-    url(r'^static/(?P.*)$', static.serve,  {'document_root': settings.STATIC_ROOT}),
+    url(
+        r'^static/(?P.*)$',
+        static.serve,
+        {'document_root': settings.STATIC_ROOT},
+    ),
 ]
 
 if settings.FEATURES['tasks']:
@@ -73,13 +102,18 @@
 
 if settings.DEBUG:
     urlpatterns += [
-        url(r'^uploads/(?P.*)$', static.serve, {
-            'document_root': settings.MEDIA_ROOT,
-        }),
+        url(
+            r'^uploads/(?P.*)$',
+            static.serve,
+            {
+                'document_root': settings.MEDIA_ROOT,
+            },
+        ),
     ]
     if 'debug_toolbar.apps.DebugToolbarConfig' in settings.INSTALLED_APPS:
         try:
             import debug_toolbar
+
             urlpatterns += [
                 url(r'^__debug__/', include(debug_toolbar.urls)),
             ]
diff --git a/wasa2il/forms.py b/wasa2il/forms.py
index 0e51d451..69f40433 100644
--- a/wasa2il/forms.py
+++ b/wasa2il/forms.py
@@ -13,6 +13,7 @@
 from django.forms.models import ModelChoiceField
 from django.forms.models import ModelMultipleChoiceField
 
+
 class Wasa2ilForm(forms.ModelForm):
     '''
     A custom form base class with functionality intended to be used with forms
@@ -36,12 +37,14 @@ def __init__(self, *args, **kwargs):
 
         # Add save/cancel buttons
         self.helper.add_input(Submit('save', _('Save')))
-        self.helper.add_input(Button(
-            'cancel',
-            _('Cancel'),
-            css_class='btn btn-default',
-            onclick="var $cancel_url = $('#cancel-url').text(); if ($cancel_url) { location.href = $cancel_url } else { window.history.back(); }"
-        ))
+        self.helper.add_input(
+            Button(
+                'cancel',
+                _('Cancel'),
+                css_class='btn btn-default',
+                onclick="var $cancel_url = $('#cancel-url').text(); if ($cancel_url) { location.href = $cancel_url } else { window.history.back(); }",
+            )
+        )
 
         # Automatically make selection fields prettier and easier to use.
         for fieldname in self.fields:
@@ -55,9 +58,15 @@ def __init__(self, *args, **kwargs):
             elif field_type is TypedChoiceField:
                 field.widget = forms.RadioSelect()
                 # Remove ugly and pointless '---------' option.
-                if field.required and len(field.choices) > 0 and field.choices[0][1] == '---------':
+                if (
+                    field.required
+                    and len(field.choices) > 0
+                    and field.choices[0][1] == '---------'
+                ):
                     field.choices.pop(0)
             elif field_type is ModelMultipleChoiceField:
                 field.widget = forms.CheckboxSelectMultiple()
             elif field_type is DateTimeField:
-                field.widget = DateTimeWidget(options=self.dateTimeOptions, bootstrap_version=3)
+                field.widget = DateTimeWidget(
+                    options=self.dateTimeOptions, bootstrap_version=3
+                )
diff --git a/wasa2il/settings.py b/wasa2il/settings.py
index 1b128bc2..0d344648 100644
--- a/wasa2il/settings.py
+++ b/wasa2il/settings.py
@@ -1,4 +1,4 @@
-#coding:utf-8
+# coding:utf-8
 # Django settings for wasa2il project.
 
 import os
@@ -10,53 +10,62 @@
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
 ## Base configuration
-DEBUG=os.environ.get('W2_DEBUG', True)
-ALLOWED_HOSTS=os.environ.get('W2_ALLOWED_HOSTS', 'localhost').split(',')
+DEBUG = os.environ.get('W2_DEBUG', True)
+ALLOWED_HOSTS = os.environ.get('W2_ALLOWED_HOSTS', 'localhost').split(',')
 
-ADMINS=os.environ.get('W2_ADMINS', 'username,user@example.com').split(',')
+ADMINS = os.environ.get('W2_ADMINS', 'username,user@example.com').split(',')
 
 # Contact email address
-CONTACT_EMAIL=os.environ.get('W2_CONTACT_EMAIL', 'contact@example.com')
+CONTACT_EMAIL = os.environ.get('W2_CONTACT_EMAIL', 'contact@example.com')
 
-BALLOT_SAVEFILE_FORMAT=os.environ.get('W2_BALLOT_SAVEFILE_FORMAT', 'elections/ballots-%(voting_system)s-%(election_id)s.json')
+BALLOT_SAVEFILE_FORMAT = os.environ.get(
+    'W2_BALLOT_SAVEFILE_FORMAT',
+    'elections/ballots-%(voting_system)s-%(election_id)s.json',
+)
 
 ## Security settings
 if not os.environ.get('W2_SECRET_KEY'):
-    print("You must configure the environment variable $W2_SECRET_KEY. It must be a long, hard to guess string.")
+    print(
+        "You must configure the environment variable $W2_SECRET_KEY. It must be a long, hard to guess string."
+    )
     print("To generate one, try running 'head /dev/urandom | sha256sum'.")
     exit()
-SECRET_KEY=os.environ.get('W2_SECRET_KEY')
-AUTO_LOGOUT_DELAY=int(os.environ.get('W2_AUTO_LOGOUT_DELAY', 30))
+SECRET_KEY = os.environ.get('W2_SECRET_KEY')
+AUTO_LOGOUT_DELAY = int(os.environ.get('W2_AUTO_LOGOUT_DELAY', 30))
 
 ## Instance identity
-INSTANCE_LOGO=os.environ.get('W2_INSTANCE_LOGO', '')
-INSTANCE_NAME=os.environ.get('W2_INSTANCE_NAME', 'Unconfigured Wasa2il')
-INSTANCE_SLUG=os.environ.get('W2_INSTANCE_SLUG', 'unconfiguredwasa2il')
-INSTANCE_URL=os.environ.get('W2_INSTANCE_URL', '')
-ORGANIZATION_NAME=os.environ.get('W2_ORGANIZATION_NAME', 'orgName')
-ORGANIZATION_NEWS_URL=os.environ.get('W2_ORGANIZATION_NEWS_URL', '')
+INSTANCE_LOGO = os.environ.get('W2_INSTANCE_LOGO', '')
+INSTANCE_NAME = os.environ.get('W2_INSTANCE_NAME', 'Unconfigured Wasa2il')
+INSTANCE_SLUG = os.environ.get('W2_INSTANCE_SLUG', 'unconfiguredwasa2il')
+INSTANCE_URL = os.environ.get('W2_INSTANCE_URL', '')
+ORGANIZATION_NAME = os.environ.get('W2_ORGANIZATION_NAME', 'orgName')
+ORGANIZATION_NEWS_URL = os.environ.get('W2_ORGANIZATION_NEWS_URL', '')
 
 ## Overall instance rules
-AGE_LIMIT=int(os.environ.get('W2_AGE_LIMIT', 16))
-ALLOW_LEAVE_POLITY=os.environ.get('W2_ALLOW_LEAVE_POLITY', False)
-RECENT_ELECTION_DAYS=int(os.environ.get('W2_RECENT_ELECTION_DAYS', 7))
-RECENT_ISSUE_DAYS=int(os.environ.get('W2_RECENT_ISSUE_DAYS', 7))
+AGE_LIMIT = int(os.environ.get('W2_AGE_LIMIT', 16))
+ALLOW_LEAVE_POLITY = os.environ.get('W2_ALLOW_LEAVE_POLITY', False)
+RECENT_ELECTION_DAYS = int(os.environ.get('W2_RECENT_ELECTION_DAYS', 7))
+RECENT_ISSUE_DAYS = int(os.environ.get('W2_RECENT_ISSUE_DAYS', 7))
 
 ## Database configuration
-DATABASE_ENGINE=os.environ.get('W2_DATABASE_ENGINE', 'django.db.backends.mysql')
-DATABASE_HOST=os.environ.get('W2_DATABASE_HOST', '127.0.0.1')
-DATABASE_NAME=os.environ.get('W2_DATABASE_NAME', 'wasa2il')
-DATABASE_PASSWORD=os.environ.get('W2_DATABASE_PASSWORD', 'wasa2il')
-DATABASE_PORT=os.environ.get('W2_DATABASE_PORT', '3306')
-DATABASE_USER=os.environ.get('W2_DATABASE_USER', 'wasa2il')
-DATABASE_EXPORT_DB_NAME=os.environ.get('W2_DATABASE_EXPORT_DB_NAME', '')
+DATABASE_ENGINE = os.environ.get(
+    'W2_DATABASE_ENGINE', 'django.db.backends.mysql'
+)
+DATABASE_HOST = os.environ.get('W2_DATABASE_HOST', '127.0.0.1')
+DATABASE_NAME = os.environ.get('W2_DATABASE_NAME', 'wasa2il')
+DATABASE_PASSWORD = os.environ.get('W2_DATABASE_PASSWORD', 'wasa2il')
+DATABASE_PORT = os.environ.get('W2_DATABASE_PORT', '3306')
+DATABASE_USER = os.environ.get('W2_DATABASE_USER', 'wasa2il')
+DATABASE_EXPORT_DB_NAME = os.environ.get('W2_DATABASE_EXPORT_DB_NAME', '')
 
 ## Locale settings
-DATETIME_FORMAT=os.environ.get('W2_DATETIME_FORMAT', 'd/m/Y H:i:s')
-DATETIME_FORMAT_DJANGO_WIDGET=os.environ.get('W2_DATETIME_FORMAT_DJANGO_WIDGET', 'dd/mm/yyyy hh:ii')
-DATE_FORMAT=os.environ.get('W2_DATE_FORMAT', 'd/m/Y')
-LANGUAGE_CODE=os.environ.get('W2_LANGUAGE_CODE', 'en-US')
-TIME_ZONE=os.environ.get('W2_TIME_ZONE', 'Iceland')
+DATETIME_FORMAT = os.environ.get('W2_DATETIME_FORMAT', 'd/m/Y H:i:s')
+DATETIME_FORMAT_DJANGO_WIDGET = os.environ.get(
+    'W2_DATETIME_FORMAT_DJANGO_WIDGET', 'dd/mm/yyyy hh:ii'
+)
+DATE_FORMAT = os.environ.get('W2_DATE_FORMAT', 'd/m/Y')
+LANGUAGE_CODE = os.environ.get('W2_LANGUAGE_CODE', 'en-US')
+TIME_ZONE = os.environ.get('W2_TIME_ZONE', 'Iceland')
 DATETIME_INPUT_FORMATS = (
     '%Y-%m-%d %H:%M:%S',
     '%Y-%m-%d %H:%M:%S.%f',
@@ -69,13 +78,15 @@
     '%d/%m/%y %H:%M:%S',
     '%d/%m/%y %H:%M:%S.%f',
     '%d/%m/%y %H:%M',
-    '%d/%m/%y'
+    '%d/%m/%y',
 )
 
 ## Email settings
-EMAIL_BACKEND=os.environ.get('W2_EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
-SERVER_EMAIL=os.environ.get('W2_SERVER_EMAIL', '')
-DEFAULT_FROM_EMAIL=os.environ.get('W2_DEFAULT_FROM_EMAIL', '')
+EMAIL_BACKEND = os.environ.get(
+    'W2_EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend'
+)
+SERVER_EMAIL = os.environ.get('W2_SERVER_EMAIL', '')
+DEFAULT_FROM_EMAIL = os.environ.get('W2_DEFAULT_FROM_EMAIL', '')
 EMAIL_HOST = os.environ.get('W2_EMAIL_HOST', 'localhost')
 EMAIL_PORT = int(os.environ.get('W2_EMAIL_PORT', 25))
 EMAIL_USE_TLS = os.environ.get('W2_EMAIL_USE_TLS', False) == '1'
@@ -85,37 +96,44 @@
 EMAIL_SUBJECT_PREFIX = os.environ.get('W2_EMAIL_SUBJECT_PREFIX', '[Wasa2il] ')
 
 ## Push notifications
-GCM_APP_ID=os.environ.get('W2_GCM_APP_ID', '')
-GCM_SENDER_ID=os.environ.get('W2_GCM_SENDER_ID', 0)
-GCM_REST_API_KEY=os.environ.get('W2_GCM_REST_API_KEY', '')
+GCM_APP_ID = os.environ.get('W2_GCM_APP_ID', '')
+GCM_SENDER_ID = os.environ.get('W2_GCM_SENDER_ID', 0)
+GCM_REST_API_KEY = os.environ.get('W2_GCM_REST_API_KEY', '')
 
 ## Facebook integration
-INSTANCE_FACEBOOK_APP_ID=os.environ.get('W2_INSTANCE_FACEBOOK_APP_ID', '')
-INSTANCE_FACEBOOK_IMAGE=os.environ.get('W2_INSTANCE_FACEBOOK_IMAGE', 'https://example.com/full/url/to/image.png')
+INSTANCE_FACEBOOK_APP_ID = os.environ.get('W2_INSTANCE_FACEBOOK_APP_ID', '')
+INSTANCE_FACEBOOK_IMAGE = os.environ.get(
+    'W2_INSTANCE_FACEBOOK_IMAGE', 'https://example.com/full/url/to/image.png'
+)
 
 ## Discourse integration
-DISCOURSE_URL=os.environ.get('W2_DISCOURSE_URL', '')
-DISCOURSE_SECRET=os.environ.get('W2_DISCOURSE_SECRET', '')
+DISCOURSE_URL = os.environ.get('W2_DISCOURSE_URL', '')
+DISCOURSE_SECRET = os.environ.get('W2_DISCOURSE_SECRET', '')
 
 ## IcePirate integration
 ICEPIRATE = {
     'url': os.environ.get('W2_ICEPIRATE_URL', ''),
-    'key': os.environ.get('W2_ICEPIRATE_KEY', '')
+    'key': os.environ.get('W2_ICEPIRATE_KEY', ''),
 }
 
 # SAML (2.0) support
 SAML = {
     'URL': os.environ.get('W2_SAML_URL', ''),
-    'CERT': os.environ.get('W2_SAML_CERT', '')
+    'CERT': os.environ.get('W2_SAML_CERT', ''),
 }
 
 FEATURES = {
     'tasks': os.environ.get('W2_FEATURE_TASKS', False) == '1',
     'topic': os.environ.get('W2_FEATURE_TOPIC', False) == '1',
-    'push_notifications': os.environ.get('W2_FEATURE_PUSH_NOTIFICATIONS', False) == '1',
+    'push_notifications': os.environ.get(
+        'W2_FEATURE_PUSH_NOTIFICATIONS', False
+    )
+    == '1',
 }
 
-if not GCM_APP_ID:  # We cannot have push notifications without a registered app ID.
+if (
+    not GCM_APP_ID
+):  # We cannot have push notifications without a registered app ID.
     FEATURES['push_notifications'] = False
 
 # Get Wasa2il version.
@@ -128,8 +146,9 @@
 
 # Some error checking for local_settings
 if not SECRET_KEY:
-    raise Exception('You need to specify Django SECRET_KEY in the local_settings!')
-
+    raise Exception(
+        'You need to specify Django SECRET_KEY in the local_settings!'
+    )
 
 
 MANAGERS = ADMINS
@@ -137,11 +156,11 @@
 DATABASES = {
     'default': {
         'ENGINE': DATABASE_ENGINE,  # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
-        'NAME': DATABASE_NAME,                      # Or path to database file if using sqlite3.
-        'USER': DATABASE_USER,                      # Not used with sqlite3.
-        'PASSWORD': DATABASE_PASSWORD,                  # Not used with sqlite3.
-        'HOST': DATABASE_HOST,                      # Set to empty string for localhost. Not used with sqlite3.
-        'PORT': DATABASE_PORT,                      # Set to empty string for default. Not used with sqlite3.
+        'NAME': DATABASE_NAME,  # Or path to database file if using sqlite3.
+        'USER': DATABASE_USER,  # Not used with sqlite3.
+        'PASSWORD': DATABASE_PASSWORD,  # Not used with sqlite3.
+        'HOST': DATABASE_HOST,  # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': DATABASE_PORT,  # Set to empty string for default. Not used with sqlite3.
     }
 }
 
@@ -169,6 +188,7 @@
     db_url = os.environ['DATABASE_URL']
     # If exists, confic accordingly
     import dj_database_url
+
     # Update database configuration with $DATABASE_URL.
     db_from_env = dj_database_url.config(conn_max_age=500)
     DATABASES['default'].update(db_from_env)
@@ -178,8 +198,8 @@
 # Language code for this installation. All choices can be found here:
 # http://www.i18nguy.com/unicode/language-identifiers.html
 LANGUAGES = (
-  ('is', 'Íslenska'),
-  ('en', 'English'),
+    ('is', 'Íslenska'),
+    ('en', 'English'),
 )
 
 SITE_ID = 1
@@ -228,11 +248,10 @@
 STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
     'django.contrib.staticfiles.finders.FileSystemFinder',
-#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
+    #    'django.contrib.staticfiles.finders.DefaultStorageFinder',
 )
 
 
-
 MIDDLEWARE = (
     'cookiesdirective.middleware.CookiesDirectiveMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
@@ -281,9 +300,7 @@
 FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
 
 
-LOCALE_PATHS = (
-    here('locale'),
-)
+LOCALE_PATHS = (here('locale'),)
 
 INSTALLED_APPS = (
     'django.contrib.auth',
@@ -293,19 +310,15 @@
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'django.forms',
-
     'django.contrib.admin',
-
     'registration',
     'bootstrapform',
     'diff_match_patch',
     'datetimewidget',
     'crispy_forms',
     'termsandconditions',
-
     'languagecontrol',
     'emailconfirmation',
-
     'core',
     'polity',
     'topic',
@@ -327,12 +340,12 @@
 AUTHENTICATION_BACKENDS = (
     'django.contrib.auth.backends.ModelBackend',
     'core.authentication.SSNAuthenticationBackend',
-#
-# Note: It may make sense to disable the following, so merely compromising
-# an e-mail account isn't sufficient to take over a wasa2il account. However,
-# Icelandic SSNs are such poorly kept secrets (they're effectively public)
-# that this wouldn't improve security for us, it'd just hurt usability.
-#
+    #
+    # Note: It may make sense to disable the following, so merely compromising
+    # an e-mail account isn't sufficient to take over a wasa2il account. However,
+    # Icelandic SSNs are such poorly kept secrets (they're effectively public)
+    # that this wouldn't improve security for us, it'd just hurt usability.
+    #
     'core.authentication.EmailAuthenticationBackend',
 )
 
@@ -367,9 +380,7 @@
     '/accounts/verify/',
 )
 
-SAML_VERIFICATION_EXCLUDE_URL_PREFIX_LIST = (
-    '/terms/',
-)
+SAML_VERIFICATION_EXCLUDE_URL_PREFIX_LIST = ('/terms/',)
 
 AUTH_PROFILE_MODULE = "core.UserProfile"
 ACCOUNT_ACTIVATION_DAYS = 7
@@ -379,15 +390,14 @@
 
 if DEBUG:
     import imp
+
     try:
         imp.find_module('debug_toolbar')
 
         INSTALLED_APPS += ('debug_toolbar.apps.DebugToolbarConfig',)
         MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
         INTERNAL_IPS = ('127.0.0.1',)
-        DEBUG_TOOLBAR_CONFIG = {
-            'JQUERY_URL': ''
-        }
+        DEBUG_TOOLBAR_CONFIG = {'JQUERY_URL': ''}
     except ImportError:
         # Silently continue if django-debug-toolbar isn't installed
         pass
@@ -395,5 +405,7 @@
 if DEBUG:
     print("============ Wasa2il Features ============")
     for feature, enabled in FEATURES.items():
-        print(" - %-25s      %8s" % (feature, ["DISABLED", "ENABLED"][enabled]))
+        print(
+            " - %-25s      %8s" % (feature, ["DISABLED", "ENABLED"][enabled])
+        )
     print("==========================================")
diff --git a/wasa2il/utils.py b/wasa2il/utils.py
index 16da2770..fc11c38a 100644
--- a/wasa2il/utils.py
+++ b/wasa2il/utils.py
@@ -1,3 +1,5 @@
 import os
 
-here = lambda x: os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), x))
+here = lambda x: os.path.abspath(
+    os.path.join(os.path.abspath(os.path.dirname(__file__)), x)
+)
diff --git a/wasa2il/wsgi.py b/wasa2il/wsgi.py
index c22d3aa2..bd6e0e03 100644
--- a/wasa2il/wsgi.py
+++ b/wasa2il/wsgi.py
@@ -23,7 +23,9 @@
 # setting points here.
 from django.core.wsgi import get_wsgi_application
 
-dotenv.read_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env'))
+dotenv.read_dotenv(
+    os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
+)
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wasa2il.settings")
 
 application = get_wsgi_application()