Implement voting on budgets
This commit is contained in:
parent
e098ed4f01
commit
5994cd38b6
@ -37,6 +37,7 @@ AVAILABLE_APPROVERS = [
|
|||||||
# Tuples (committee name, description)
|
# Tuples (committee name, description)
|
||||||
('Committee', 'Management Committee'),
|
('Committee', 'Management Committee'),
|
||||||
]
|
]
|
||||||
|
BUDGET_ENABLE_VOTING = True
|
||||||
|
|
||||||
TICKETING_FEE_PROPORTION = 0.0175 # Previous default was (1-1/1.01884)
|
TICKETING_FEE_PROPORTION = 0.0175 # Previous default was (1-1/1.01884)
|
||||||
TICKETING_FEE_FIXED = 0.30 # Previous default was 0.8133/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/
|
# Download from http://bsb.apca.com.au/
|
||||||
BSB_FILE_PATH = 'sstreasury/BSBDirectoryMay20-290.csv'
|
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
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
@ -218,7 +218,7 @@
|
|||||||
{% elif item.action == import('sstreasury.models').BudgetAction.EDIT.value %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
@ -228,10 +228,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</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 %}
|
{% if claims is not none %}
|
||||||
<h2>Reimbursement claims</h2>
|
<h2>Reimbursement claims</h2>
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
@ -219,6 +220,25 @@ class BudgetRevision(models.Model):
|
|||||||
if self.state != BudgetState.APPROVED.value:
|
if self.state != BudgetState.APPROVED.value:
|
||||||
return False
|
return False
|
||||||
return True
|
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):
|
class ClaimState(DescriptionEnum):
|
||||||
DRAFT = 10, 'Draft'
|
DRAFT = 10, 'Draft'
|
||||||
|
@ -259,7 +259,7 @@ def budget_view(request, budget, revision):
|
|||||||
if not revision.can_view(request.user):
|
if not revision.can_view(request.user):
|
||||||
raise PermissionDenied
|
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)
|
history.sort(key=lambda x: x.time, reverse=True)
|
||||||
|
|
||||||
if revision.state == models.BudgetState.APPROVED.value and 'revision' not in request.GET:
|
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():
|
with transaction.atomic():
|
||||||
revision.update_state(request.user, models.BudgetState.CANCELLED)
|
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}))
|
return redirect(reverse('budget_view', kwargs={'id': budget.id}))
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
Loading…
Reference in New Issue
Block a user