Implement voting on budgets
This commit is contained in:
parent
e098ed4f01
commit
5994cd38b6
@ -37,6 +37,7 @@ AVAILABLE_APPROVERS = [
|
||||
# Tuples (committee name, description)
|
||||
('Committee', 'Management Committee'),
|
||||
]
|
||||
BUDGET_ENABLE_VOTING = True
|
||||
|
||||
TICKETING_FEE_PROPORTION = 0.0175 # Previous default was (1-1/1.01884)
|
||||
TICKETING_FEE_FIXED = 0.30 # Previous default was 0.8133/1.01884
|
||||
@ -50,6 +51,11 @@ ABA_SRC_ACC = '00000000'
|
||||
# Download from http://bsb.apca.com.au/
|
||||
BSB_FILE_PATH = 'sstreasury/BSBDirectoryMay20-290.csv'
|
||||
|
||||
PRETIX_API_BASE = 'https://example.com'
|
||||
PRETIX_API_TOKEN = 'abcdefg'
|
||||
PRETIX_ORGANIZER = 'societyname'
|
||||
PRETIX_START_YEAR = '2023' # Ignore events before this year
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
@ -218,7 +218,7 @@
|
||||
{% elif item.action == import('sstreasury.models').BudgetAction.EDIT.value %}
|
||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> edited the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||
{% elif item.action == import('sstreasury.models').BudgetAction.UPDATE_STATE.value %}
|
||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> changed the state to: {{ item.get_state_display() }}
|
||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> changed the state to: {{ item.get_state_display() }} <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||
{% else %}
|
||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> modified the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||
{% endif %}
|
||||
@ -228,10 +228,103 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif item.__class__.__name__ == 'BudgetVote' %}
|
||||
<div class="event">
|
||||
<div class="label">
|
||||
<i class="gavel icon"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="summary">
|
||||
<i class="user circle icon"></i>
|
||||
<a href="mailto:{{ item.voter.email }}">{{ item.voter.first_name }} {{ item.voter.last_name }}</a>
|
||||
{% if item.vote_type == import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value %}
|
||||
voted in favour of the budget
|
||||
{% elif item.vote_type == import('sstreasury.models').BudgetVoteType.AGAINST.value %}
|
||||
voted against the budget
|
||||
{% elif item.vote_type == import('sstreasury.models').BudgetVoteType.ABSTAIN.value %}
|
||||
abstained from voting
|
||||
{% endif %}
|
||||
<div class="date">
|
||||
{{ localtime(item.time) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %}
|
||||
<h2>Committee voting</h2>
|
||||
|
||||
<form class="ui three column grid" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
<div class="column">
|
||||
<div class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if revision.can_vote(request.user) %}
|
||||
<button class="ui bottom attached positive button" type="submit" name="action" value="VoteInFavour">
|
||||
Vote in favour
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if revision.can_vote(request.user) %}
|
||||
<button class="ui bottom attached negative button" type="submit" name="action" value="VoteAgainst">
|
||||
Vote against
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if revision.can_vote(request.user) %}
|
||||
<button class="ui bottom attached secondary button" type="submit" name="action" value="VoteAbstain">
|
||||
Abstain
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if claims is not none %}
|
||||
<h2>Reimbursement claims</h2>
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from jsonfield import JSONField
|
||||
@ -219,6 +220,25 @@ class BudgetRevision(models.Model):
|
||||
if self.state != BudgetState.APPROVED.value:
|
||||
return False
|
||||
return True
|
||||
|
||||
def can_vote(self, user):
|
||||
if not settings.BUDGET_ENABLE_VOTING:
|
||||
return False
|
||||
if self.state == BudgetState.ENDORSED.value and user.groups.filter(name=self.approver).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
class BudgetVoteType(DescriptionEnum):
|
||||
IN_FAVOUR = 1, 'In favour'
|
||||
AGAINST = -1, 'Against'
|
||||
ABSTAIN = 0, 'Abstain'
|
||||
|
||||
class BudgetVote(models.Model):
|
||||
revision = models.ForeignKey(BudgetRevision, on_delete=models.CASCADE)
|
||||
voter = models.ForeignKey(User, on_delete=models.PROTECT, related_name='+')
|
||||
time = models.DateTimeField()
|
||||
is_current = models.BooleanField()
|
||||
vote_type = models.IntegerField(choices=[(v.value, v.description) for v in BudgetVoteType])
|
||||
|
||||
class ClaimState(DescriptionEnum):
|
||||
DRAFT = 10, 'Draft'
|
||||
|
@ -259,7 +259,7 @@ def budget_view(request, budget, revision):
|
||||
if not revision.can_view(request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
history = list(itertools.chain(budget.budgetrevision_set.all(), revision.budget.budgetcomment_set.all()))
|
||||
history = list(itertools.chain(budget.budgetrevision_set.all(), budget.budgetcomment_set.all(), *[r.budgetvote_set.all() for r in budget.budgetrevision_set.all()]))
|
||||
history.sort(key=lambda x: x.time, reverse=True)
|
||||
|
||||
if revision.state == models.BudgetState.APPROVED.value and 'revision' not in request.GET:
|
||||
@ -477,6 +477,39 @@ def budget_action(request, budget, revision):
|
||||
with transaction.atomic():
|
||||
revision.update_state(request.user, models.BudgetState.CANCELLED)
|
||||
|
||||
if 'VoteInFavour' in actions or 'VoteAgainst' in actions or 'VoteAbstain' in actions:
|
||||
if not revision.can_vote(request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
if 'VoteInFavour' in actions:
|
||||
vote_type = models.BudgetVoteType.IN_FAVOUR
|
||||
elif 'VoteAgainst' in actions:
|
||||
vote_type = models.BudgetVoteType.AGAINST
|
||||
elif 'VoteAbstain' in actions:
|
||||
vote_type = models.BudgetVoteType.ABSTAIN
|
||||
|
||||
# Already exists?
|
||||
if revision.budgetvote_set.filter(is_current=True, voter=request.user, vote_type=vote_type.value):
|
||||
# No need to create new vote
|
||||
pass
|
||||
else:
|
||||
with transaction.atomic():
|
||||
# Invalidate any existing votes
|
||||
for vote in revision.budgetvote_set.filter(is_current=True, voter=request.user):
|
||||
vote.is_current = False
|
||||
vote.save()
|
||||
|
||||
# Create a new vote
|
||||
vote = models.BudgetVote()
|
||||
vote.revision = revision
|
||||
vote.voter = request.user
|
||||
vote.time = timezone.now()
|
||||
vote.is_current = True
|
||||
vote.vote_type = vote_type.value
|
||||
vote.save()
|
||||
|
||||
# TODO: Check for vote threshold
|
||||
|
||||
return redirect(reverse('budget_view', kwargs={'id': budget.id}))
|
||||
|
||||
@login_required
|
||||
|
Loading…
Reference in New Issue
Block a user