Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ addopts =[
"--ignore=cdk",
"--ignore=node_modules",
"--ruff",
"--ruff-format",
"--ds=ynr.settings.testing"
]

Expand Down
22 changes: 14 additions & 8 deletions ynr/apps/candidates/models/popolo_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,14 +440,14 @@ def cancelled_status_html(self):
@property
def locked_status_text(self):
if self.candidates_locked:
return mark_safe("🔐")
return mark_safe("🔐 Locked")
return None

@property
def locked_status_html(self):
if self.candidates_locked:
return mark_safe(
'<abbr title="Candidates verified and post locked">{}</abbr>'.format(
'<abbr title="Candidates verified and ballot locked">{}</abbr>'.format(
self.locked_status_text
)
)
Expand All @@ -458,7 +458,7 @@ def locked_status_html(self):
@property
def suggested_lock_html(self):
return mark_safe(
'<abbr title="Someone suggested locking this post">🔓</abbr>'
'<abbr title="Someone suggested locking this post">🔑 Lock Suggested</abbr>'
)

@cached_property
Expand All @@ -468,6 +468,16 @@ def has_results(self):
return True
return False

@cached_property
def has_sopn(self):
"""
Return a boolean if the ballot has a related SOPN.

This is needed because accessing `ballot.sopn` without a SOPN will raise
`RelatedObjectDoesNotExist`. This can cause subtle errors in templates.
"""
return hasattr(self, "sopn")

@property
def uncontested(self):
if not self.candidates_locked:
Expand Down Expand Up @@ -602,11 +612,7 @@ def user_can_edit_membership(self, user, allow_if_trusted_to_lock=True):
# can edit the memberships. Also prevent adding via the ballot
# forms when we have a SOPN for this ballot, as the bulk adding forms
# should be used instead.
if (
not self.candidates_locked
and not self.cancelled
and not hasattr(self, "sopn")
):
if not self.candidates_locked and not self.cancelled:
return True

# Special case where elections are cancelled before they are locked
Expand Down
4 changes: 4 additions & 0 deletions ynr/apps/candidates/static/candidates/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ form.signup {
.form-item {
margin-bottom: 1.5em;

.row {
margin: 0;
}

input,
select,
p {
Expand Down
9 changes: 7 additions & 2 deletions ynr/apps/candidates/static/js/ballot.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ $(function() {
}

/* Set up the hide / reveal for the add new candidate form */
$('.show-new-candidate-form').on('click', function(e){
$(document).on('click', '.show-new-candidate-form', function(e){
e.preventDefault();
e.target.classList.add('hide-new-candidate-form')
e.target.classList.remove('show-new-candidate-form')
var newCandidate = getNewCandidateDiv(e.target);
newCandidate.slideDown(function(){
newCandidate.find('input:text').eq(0).focus();
});
});
$('.hide-new-candidate-form').on('click', function(e){
$(document).on('click', '.hide-new-candidate-form', function(e){
e.preventDefault();
e.target.classList.add('show-new-candidate-form')
e.target.classList.remove('hide-new-candidate-form')
var newCandidate = getNewCandidateDiv(e.target);
newCandidate.slideUp();
});
Expand Down
9 changes: 8 additions & 1 deletion ynr/apps/candidates/tests/test_candidacy_add_and_remove.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

from django_webtest import WebTest
from official_documents.models import BallotSOPN
from people.tests.factories import PersonFactory
Expand Down Expand Up @@ -63,9 +65,14 @@ def test_can_see_add_button_with_no_sopn(self):
user=self.user,
)
self.assertContains(response, "Add a new candidate")

# We allow adding candidates when there is a SOPN
# but we mark the button as 'secondary'
BallotSOPN.objects.create(ballot=ballot)
response = self.app.get(
ballot.get_absolute_url(),
user=self.user,
)
self.assertNotContains(response, "Add a new candidate")
button = response.html.find("a", text=re.compile("Add a new candidate"))
self.assertIsNotNone(button)
self.assertIn("secondary", button["class"])
166 changes: 84 additions & 82 deletions ynr/apps/elections/templates/elections/ballot_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,39 +34,60 @@
{% block title %}Candidates for {{ ballot.post.label }} in the {{ ballot.election.name }} on {{ ballot.election.election_date|date:"j F Y" }}{% endblock %}

{% block hero %}
<h1>Candidates for {{ ballot.post.label }} on <br>{{ ballot.election.election_date|date:"j F Y" }}</h1>
<h1>{{ ballot.post.label }}</h1>
<p> <a href="{% url 'election_view' ballot.election.slug %}">{{ ballot.election.name }}
on {{ ballot.election.election_date|date:"j F Y" }}</a>
</p>
{% endblock %}


{% block content %}
<div class="row">
<div class="columns large-9">
<div class="panel">
<h4>
This ballot is part of the <a href="{% url 'election_view' ballot.election.slug %}">{{ ballot.election.name }}</a>
</h4>
</div>
{% if ballot.cancelled %}
❌ The poll for this election was cancelled
{% if ballot.replaced_by %}
and <a href="{{ ballot.replaced_by.get_absolute_url }}">replaced by {{ ballot.replaced_by.ballot_paper_id }}</a>
{% endif %}
{% endif %}
<div class="ballot_status">
{% if candidates.exists %}
{% if ballot.candidates_locked %}
<p>
<strong>{{ ballot.locked_status_html }}</strong>: These {{ ballot.num_candidates }} candidates have been double-checked against the
<a href="{{ ballot.sopn.get_absolute_url }}">official Statement of Persons Nominated</a>
from the council. The ballot is ‘locked’, which means that no new candidates may be added.
</p>
{% endif %}

{% if ballot.replaces %}
🔄 This ballot replaces a <a href="{{ ballot.replaces.get_absolute_url }}">previously cancelled ballot</a>
{% endif %}
{% if ballot.has_lock_suggestion %}
{% if current_user_suggested_lock %}
<p>
<strong>{{ ballot.suggested_lock_html }}</strong> You suggested locking this ballot. Someone else needs to double-check and lock it.
</p>
{% else %}
<p><strong>{{ ballot.suggested_lock_html }}</strong> These candidates have not yet been double-checked against the official candidate list.</p>
{% endif %}

{% if not ballot.candidates_locked and candidates.exists %}
<p>These candidates will not be confirmed until the council publishes the official candidate list on {{ballot.expected_sopn_date}}.
Once nomination papers are published, we will manually verify each candidate.
</p>
{% endif %}
{% endif %}

{% if not ballot.candidates_locked %}
{% if ballot.has_sopn %}
<p>These candidates need to be double-checked against the <a href="{{ ballot.sopn.get_absolute_url }}">official Statement of Persons Nominated</a>.</p>
{% else %}
<p>These candidates will not be confirmed until the council publishes the official candidate list on {{ballot.expected_sopn_date}}.
Once nomination papers are published, we will manually verify each candidate.
</p>
{% endif %}
{% endif %}
{% else %}
{# In this case we show a heading as it needs to indicate that this is the main section of the page #}
{# without the heading the 'previous candidates' section looks like the main section #}
<h2>Candidates unknown</h2>
<p>
We don’t know of any candidates in {{ ballot.post.label }} for the {{ ballot.election.name }} yet.
We expect the official list of candidates to be published on or after
<strong>{{ ballot.expected_sopn_date }}</strong>.</p>
<p class="clearfix">
{% include "elections/includes/_ballot_add_candidate.html" with position='left' %}
</p>
{% endif %}
</div>

{% if ballot.candidates_locked and candidates.exists %}
<p>These {{ ballot.num_candidates }} candidates have been confirmed by the official Statement of Persons Nominated
from the council. The ballot is ‘locked’, which means that no new candidates may be added.</p>
{% endif %}

{% include "elections/includes/_ballot_sopn_links.html" %}

Expand All @@ -89,7 +110,7 @@ <h4>
<th>Rank</th>
{% endif %}
{% if user.is_authenticated %}
<th>Actions</th>
<th class="text-right">Actions</th>
{% endif %}
</tr>
</thead>
Expand Down Expand Up @@ -141,7 +162,7 @@ <h4>
{% endif %}

{% if user.is_authenticated %}
<td>
<td class="text-right">
{% if membership_edits_allowed %}
<a class="button tiny js-toggle-source-confirmation not-standing">
Not actually standing?
Expand Down Expand Up @@ -170,78 +191,59 @@ <h4>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>

{% if user_can_record_results and has_any_winners %}
<form action="{% url 'retract-winner' ballot_paper_id=ballot.ballot_paper_id %}" method="post">
{% csrf_token %}
<input type="submit" class="button alert small" value="Unset the current winners, if incorrect">
</form>
{% endif %}
{% if membership_edits_allowed or not user.is_authenticated and not ballot.has_sopn%}
<tr>
<td colspan="3">
{% include "elections/includes/_ballot_add_candidate.html" %}
</td>
</tr>

{% else %}
<div class="no-candidates">
<p>
<strong>Oh no!</strong>
We don’t know of any candidates in {{ ballot.post.label }} for the {{ ballot.election.name }} yet.
We expect the official list of candidates to be published on or after {{ ballot.expected_sopn_date }}.
</p>
</div>
{% endif %}

<div class="ballot-ctas">
{% endif %}

{% include "elections/includes/_ballot_suggest_locking.html" %}
{% include "elections/includes/_ballot_lock_form.html" %}

{% if user_can_review_photos and ballot.get_absolute_queued_image_review_url %}
<p>This ballot has candidate photos awaiting approval.<a href="{{ ballot.get_absolute_queued_image_review_url }}"> Do you have time to review them?</a></p>
</tbody>
</table>
{% endif %}

{% if membership_edits_allowed %}
<p>
<a class="show-new-candidate-form button" href="{% url 'person-create' ballot_paper_id=ballot.ballot_paper_id %}">
Add a new candidate
</a>
</p>

{% if user.is_authenticated %}
{# You basically can't do anything unless you're logged in #}
<div class="ballot-ctas">
{# Everything you can _do to change_ the ballot or related objects #}

{% elif not user.is_authenticated %}
<p>
<a href="{% url 'wombles:login' %}?next={{ request.path }}" class="button">
Sign in to add a new candidate
</a>
</p>
{% endif %}
</div>
{% if user.is_authenticated and sopn %}
{% if not ballot.has_lock_suggestion and not ballot.candidates_locked %}
<a href="{{ ballot.get_bulk_add_url }}" class="button">
Add candidates from nomination paper in {{ballot.post.label}}
{# Bulk adding #}
{% if ballot.has_sopn and not ballot.has_lock_suggestion and not ballot.candidates_locked and not ballot.cancelled %}
<a href="{{ ballot.get_bulk_add_url }}" class="button small">
Add candidates from SOPN
</a>
{% endif %}
{% endif %}
{% if membership_edits_allowed %}
{% if add_candidate_form %}
<div class="candidates__new" {% if add_candidate_form.errors %}style="display: block"{% endif %}>
<h4>Add a new candidate</h4>
<form id="new-candidate-form" name="new-candidate-form" action="{% url 'person-create' ballot_paper_id=ballot.ballot_paper_id %}" method="post">
{% with form=add_candidate_form identifiers_formset=identifiers_formset %}
{% include 'candidates/_person_form.html' %}
{% endwith %}


{% include "elections/includes/_ballot_lock_form.html" %}

{% if user_can_record_results and has_any_winners %}
<form action="{% url 'retract-winner' ballot_paper_id=ballot.ballot_paper_id %}" method="post">
{% csrf_token %}
<input type="submit" class="button alert small" value="Unset the current winners, if incorrect">
</form>
</div>
{% endif %}
{% endif %}
{% include "elections/includes/_ballot_suggest_locking.html" %}

{# Only include these if the user can alter memberships #}
{% include "elections/includes/_ballot_candidates_might_stand.html" %}
{% include "elections/includes/_ballot_candidates_not_standing.html" %}
{% if user_can_review_photos and ballot.get_absolute_queued_image_review_url %}
<p>This ballot has candidate photos awaiting approval.<a href="{{ ballot.get_absolute_queued_image_review_url }}"> Do you have time to review them?</a></p>
{% endif %}
</div>
{% endif %}

{% if ballot.has_results and ballot.resultset %}
{% include "elections/includes/_ballot_results_table.html" with results=ballot.resultset %}
{% endif %}

{# Only include these if the user can alter memberships #}
{% include "elections/includes/_ballot_candidates_might_stand.html" %}
{% include "elections/includes/_ballot_candidates_not_standing.html" %}


{% if user.is_authenticated and logged_actions %}
<h2 id="history">History for this ballot</h2>
{% for action in logged_actions %}
Expand Down
4 changes: 2 additions & 2 deletions ynr/apps/elections/templates/elections/election_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ <h3>{{ group.grouper }}</h3>
{% elif ballot.candidates_locked %}
{{ ballot.locked_status_html }}
{% else %}
{% if ballot.officialdocument_set.exists and not ballot.suggestedpostlock_set.exists %}
<a href="{{ ballot.get_bulk_add_url }}" class="button tiny">Add candidates</a>
{% if ballot.sopn and not ballot.suggestedpostlock_set.exists %}
SOPN Uploaded <br><a href="{{ ballot.get_bulk_add_url }}">(add candidates)</a>
{% else %}
{% if ballot.suggestedpostlock_set.exists %}
{{ ballot.suggested_lock_html }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% if not user.is_authenticated and not ballot.has_sopn %}
<a href="{% url 'wombles:login' %}?next={{ request.path }}" class="button small right" style="margin:0">
Sign in to add a new candidate
</a>
{% endif %}

{% if membership_edits_allowed %}
<a class="show-new-candidate-form button small {% if not position %}right{% endif %} {% if ballot.has_sopn %}secondary{% else %}success{% endif %}"
style="margin:0" href="{% url 'person-create' ballot_paper_id=ballot.ballot_paper_id %}">
Add a new candidate
</a>

{% if add_candidate_form %}
<div class="candidates__new" {% if add_candidate_form.errors %}style="display: block"{% endif %}>
<h4>Add a new candidate</h4>
<form id="new-candidate-form" name="new-candidate-form"
action="{% url 'person-create' ballot_paper_id=ballot.ballot_paper_id %}" method="post">
{% with form=add_candidate_form identifiers_formset=identifiers_formset %}
{% include 'candidates/_person_form.html' %}
{% endwith %}
</form>
</div>
{% endif %}
{% endif %}
Loading