Implement submitting/withdrawing budgets and email sending
This commit is contained in:
parent
2a5bf7ad93
commit
1afc3f3db7
86
ssmain/email.py
Normal file
86
ssmain/email.py
Normal file
@ -0,0 +1,86 @@
|
||||
# Society Self-Service
|
||||
# Copyright © 2018-2019 Yingtong Li (RunasSudo)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
import premailer
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import loader
|
||||
from jinja2 import Markup
|
||||
|
||||
import markdown
|
||||
|
||||
class Emailer:
|
||||
def __init__(self):
|
||||
self.client = boto3.client('ses', aws_access_key_id=settings.AWS_KEY_ID, aws_secret_access_key=settings.AWS_SECRET, region_name=settings.AWS_REGION)
|
||||
self.template = loader.get_template('ssmain/email/rendered.html')
|
||||
|
||||
def markdown(self, x):
|
||||
return markdown.markdown(x, extensions=['mdx_urlize', 'fenced_code'])
|
||||
|
||||
def boto3_send(self, *args, **kwargs):
|
||||
for i in range(0, 10):
|
||||
try:
|
||||
self.client.send_email(*args, **kwargs)
|
||||
return
|
||||
except ClientError as e:
|
||||
if e['Error']['Code'] == 'Throttling' and e['Error']['Message'] == 'Maximum sending rate exceeded.':
|
||||
wait_time = max(10*(2**i), 5000)
|
||||
#self.stdout.write(self.style.NOTICE('Reached maximum sending rate, waiting {} ms'.format(wait_time)))
|
||||
time.sleep(wait_time/1000)
|
||||
else:
|
||||
raise e
|
||||
raise Exception('Reached maximum number of retries')
|
||||
|
||||
def send_raw_mail(self, recipients, subject, content_txt, content_html):
|
||||
self.boto3_send(
|
||||
Destination={
|
||||
'ToAddresses': recipients,
|
||||
},
|
||||
Message={
|
||||
'Body': {
|
||||
'Html': {
|
||||
'Charset': 'utf-8',
|
||||
'Data': content_html,
|
||||
},
|
||||
'Text': {
|
||||
'Charset': 'utf-8',
|
||||
'Data': content_txt,
|
||||
},
|
||||
},
|
||||
'Subject': {
|
||||
'Charset': 'utf-8',
|
||||
'Data': subject,
|
||||
},
|
||||
},
|
||||
Source='{} <{}>'.format(settings.ORG_NAME, settings.AWS_SENDER_EMAIL),
|
||||
)
|
||||
|
||||
def render_mail(self, template_loc, params={}):
|
||||
params['baseurl'] = 'https://' + settings.ALLOWED_HOSTS[0]
|
||||
|
||||
template = loader.get_template(template_loc)
|
||||
content_txt = template.render(params)
|
||||
|
||||
content_markdown = self.markdown(content_txt)
|
||||
content_html = self.template.render({'email_content': Markup(content_markdown)})
|
||||
|
||||
return content_txt, content_html
|
||||
|
||||
def send_mail(self, recipients, subject, template_loc, params):
|
||||
content_txt, content_html = self.render_mail(template_loc, params)
|
||||
self.send_raw_mail(recipients, subject, content_txt, content_html)
|
@ -890,7 +890,7 @@
|
||||
margin: 30px 0;
|
||||
Margin: 30px 0; }
|
||||
pre code {
|
||||
color: #cacaca; }
|
||||
color: #000000; }
|
||||
pre code span.callout {
|
||||
color: #8a8a8a;
|
||||
font-weight: bold; }
|
||||
@ -1424,7 +1424,7 @@
|
||||
<th>
|
||||
<p class="text-center footercopy">
|
||||
{% block footer %}{% endblock %}
|
||||
© Copyright {{ import('datetime').datetime.now().strftime('%Y') }} MUMUS Inc. All Rights Reserved.<br>
|
||||
© Copyright {{ import('datetime').datetime.now().strftime('%Y') }} {{ import('django.conf').settings.ORG_NAME }}. All Rights Reserved.<br>
|
||||
Design by <a href="https://www.sendwithus.com/resources/templates/meow">SendWithUs</a>.
|
||||
</p>
|
||||
</th>
|
48
ssmain/jinja2/ssmain/email/rendered.html
Normal file
48
ssmain/jinja2/ssmain/email/rendered.html
Normal file
@ -0,0 +1,48 @@
|
||||
{% extends 'ssmain/email/base.html' %}
|
||||
|
||||
{#
|
||||
Society Self-Service
|
||||
Copyright © 2018-2019 Yingtong Li (RunasSudo)
|
||||
|
||||
Design by SendWithUs (Apache 2.0 licence)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#}
|
||||
|
||||
{% block content %}
|
||||
<tr> <!-- main Email content -->
|
||||
<th class="small-12 large-12 columns first last">
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
{{ email_content }}
|
||||
</th>
|
||||
<th class="expander"></th>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
<tr> <!-- This container adds whitespace gap at the bottom of main content -->
|
||||
<th class="small-12 large-12 columns first last">
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
 
|
||||
</th>
|
||||
<th class="expander"></th>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
{% endblock content %}
|
@ -1,4 +1,4 @@
|
||||
{% extends 'ssmembership/email/base.html' %}
|
||||
{% extends 'ssmain/email/base.html' %}
|
||||
|
||||
{#
|
||||
Society Self-Service
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends 'sspromotions/email/base.html' %}
|
||||
{% extends 'ssmain/email/base.html' %}
|
||||
|
||||
{#
|
||||
Society Self-Service
|
||||
|
@ -33,7 +33,7 @@
|
||||
{% for revision in budgets %}
|
||||
<tr>
|
||||
<td class="selectable"><a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}">{{ revision.name }}</a></td>
|
||||
<td class="selectable"><a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}">{{ import('sstreasury.models').BudgetState(revision.state).description }}</a></td>
|
||||
<td class="selectable"><a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}">{{ revision.get_state_display() }}</a></td>
|
||||
<td>
|
||||
<a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}" class="ui tiny primary icon button"><i class="eye icon"></i></a>
|
||||
</td>
|
||||
|
@ -24,10 +24,18 @@
|
||||
<h1>{{ revision.name }}</h1>
|
||||
|
||||
{% if is_latest %}
|
||||
<div>
|
||||
<form class="ui form" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
||||
<span class="ui header">Status: {{ revision.get_state_display() }}</span>
|
||||
|
||||
{% if revision.state == import('sstreasury.models').BudgetState.DRAFT.value or revision.state == import('sstreasury.models').BudgetState.RESUBMIT.value %}
|
||||
<button class="ui mini labeled primary icon button" type="submit" name="action" value="Submit" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to submit this budget for Treasury approval? You will not be able to make any additional changes without withdrawing the budget.');"><i class="paper plane icon"></i> Submit</button>
|
||||
<a class="ui mini labeled right floated icon button" href="{{ url('budget_edit', kwargs={'id': revision.budget.id}) }}"><i class="edit icon"></i> Edit</a>
|
||||
</div>
|
||||
{% elif revision.state != import('sstreasury.models').BudgetState.APPROVED.value %}
|
||||
<button class="ui mini labeled basic red icon button" type="submit" name="action" value="Withdraw" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to withdraw this budget from being considered for approval? The budget will be reverted to a draft.');"><i class="undo icon"></i> Withdraw</button>
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="ui warning message">
|
||||
<p>You are viewing an older version of this budget. To make any changes, <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}">click here</a> to return to the current version.</p>
|
||||
@ -136,7 +144,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra text">
|
||||
{{ item.content }}
|
||||
{{ item.content|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -148,7 +156,15 @@
|
||||
<div class="content">
|
||||
<div class="summary">
|
||||
<i class="user circle icon"></i>
|
||||
{% if item.action == import('sstreasury.models').BudgetAction.CREATE.value %}
|
||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> created the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||
{% 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() }}
|
||||
{% 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 %}
|
||||
<div class="date">
|
||||
{{ localtime(item.time) }}
|
||||
</div>
|
||||
|
7
sstreasury/jinja2/sstreasury/email/commented.md
Normal file
7
sstreasury/jinja2/sstreasury/email/commented.md
Normal file
@ -0,0 +1,7 @@
|
||||
{{ comment.author.first_name }} {{ comment.author.last_name }} made a new comment on the budget *{{ revision.name }}*:
|
||||
|
||||
```
|
||||
{{ comment.content }}
|
||||
```
|
||||
|
||||
{{ baseurl }}{{ url('budget_view', kwargs={'id': revision.budget.id}) }}
|
3
sstreasury/jinja2/sstreasury/email/submitted_drafter.md
Normal file
3
sstreasury/jinja2/sstreasury/email/submitted_drafter.md
Normal file
@ -0,0 +1,3 @@
|
||||
Your budget titled *{{ revision.name }}* has been submitted for Treasury review.
|
||||
|
||||
{{ baseurl }}{{ url('budget_view', kwargs={'id': revision.budget.id}) }}
|
@ -0,0 +1,3 @@
|
||||
A budget titled *{{ revision.name }}* has been submitted for your review.
|
||||
|
||||
{{ baseurl }}{{ url('budget_view', kwargs={'id': revision.budget.id}) }}
|
@ -39,7 +39,18 @@ class BudgetState(Enum):
|
||||
AWAIT_REVIEW = 30, 'Awaiting Treasury review'
|
||||
ENDORSED = 40, 'Endorsed by Treasury, awaiting committee approval'
|
||||
APPROVED = 50, 'Approved'
|
||||
CANCELLED = 60, 'Cancelled'
|
||||
#CANCELLED = 60, 'Cancelled'
|
||||
|
||||
def __new__(cls, value, description):
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.description = description
|
||||
return obj
|
||||
|
||||
class BudgetAction(Enum):
|
||||
CREATE = 5, 'Created'
|
||||
EDIT = 10, 'Edited'
|
||||
UPDATE_STATE = 20, 'Updated state'
|
||||
|
||||
def __new__(cls, value, description):
|
||||
obj = object.__new__(cls)
|
||||
@ -66,5 +77,13 @@ class BudgetRevision(models.Model):
|
||||
expense_no_emergency_fund = models.BooleanField()
|
||||
expense_comments = models.TextField()
|
||||
|
||||
action = models.IntegerField(choices=[(v.value, v.description) for v in BudgetAction])
|
||||
|
||||
def copy(self):
|
||||
contributors = list(self.contributors.all())
|
||||
self.pk, self.id = None, None
|
||||
self.save()
|
||||
self.contributors.add(*contributors)
|
||||
|
||||
class Meta:
|
||||
ordering = ['id']
|
||||
|
@ -26,6 +26,7 @@ from django.utils import timezone
|
||||
from django.views import generic
|
||||
|
||||
from . import models
|
||||
from ssmain.email import Emailer
|
||||
|
||||
import itertools
|
||||
import json
|
||||
@ -113,6 +114,7 @@ def budget_new(request):
|
||||
revision = models.BudgetRevision()
|
||||
revision.author = request.user
|
||||
revision.time = timezone.now()
|
||||
revision.action = models.BudgetAction.CREATE.value
|
||||
revision = revision_from_form(budget, revision, request.POST)
|
||||
|
||||
if request.POST['submit'] == 'Save':
|
||||
@ -146,6 +148,7 @@ def budget_edit(request, id):
|
||||
revision = models.BudgetRevision()
|
||||
revision.author = request.user
|
||||
revision.time = timezone.now()
|
||||
revision.action = models.BudgetAction.EDIT.value
|
||||
revision = revision_from_form(budget, revision, request.POST)
|
||||
|
||||
if request.POST['submit'] == 'Save':
|
||||
@ -170,6 +173,7 @@ def budget_action(request, id):
|
||||
raise PermissionDenied
|
||||
|
||||
if request.POST['action'] == 'Comment':
|
||||
with transaction.atomic():
|
||||
comment = models.BudgetComment()
|
||||
comment.budget = budget
|
||||
comment.author = request.user
|
||||
@ -177,4 +181,43 @@ def budget_action(request, id):
|
||||
comment.content = request.POST['comment']
|
||||
comment.save()
|
||||
|
||||
emailer = Emailer()
|
||||
for user in User.objects.filter(groups__name='Treasury'):
|
||||
if user != request.user:
|
||||
emailer.send_mail([user.email], 'New comment on budget: {}'.format(revision.name), 'sstreasury/email/commented.md', {'revision': revision, 'comment': comment})
|
||||
for user in revision.contributors.all():
|
||||
if user != request.user:
|
||||
emailer.send_mail([user.email], 'New comment on budget: {}'.format(revision.name), 'sstreasury/email/commented.md', {'revision': revision, 'comment': comment})
|
||||
|
||||
if request.POST['action'] == 'Submit':
|
||||
if revision.state != models.BudgetState.DRAFT.value and revision.state != models.BudgetState.RESUBMIT.value:
|
||||
raise PermissionDenied
|
||||
|
||||
with transaction.atomic():
|
||||
# Copy revision
|
||||
revision.copy()
|
||||
revision.author = request.user
|
||||
revision.time = timezone.now()
|
||||
revision.state = models.BudgetState.AWAIT_REVIEW.value
|
||||
revision.action = models.BudgetAction.UPDATE_STATE.value
|
||||
revision.save()
|
||||
|
||||
emailer = Emailer()
|
||||
for user in User.objects.filter(groups__name='Treasury'):
|
||||
emailer.send_mail([user.email], 'Budget submitted: {}'.format(revision.name), 'sstreasury/email/submitted_treasurer.md', {'revision': revision})
|
||||
for user in revision.contributors.all():
|
||||
emailer.send_mail([user.email], 'Budget submitted: {}'.format(revision.name), 'sstreasury/email/submitted_drafter.md', {'revision': revision})
|
||||
|
||||
if request.POST['action'] == 'Withdraw':
|
||||
if revision.state == models.BudgetState.DRAFT.value or revision.state == models.BudgetState.RESUBMIT.value or revision.state == models.BudgetState.APPROVED.value:
|
||||
raise PermissionDenied
|
||||
|
||||
# Copy revision
|
||||
revision.copy()
|
||||
revision.author = request.user
|
||||
revision.time = timezone.now()
|
||||
revision.state = models.BudgetState.DRAFT.value
|
||||
revision.action = models.BudgetAction.UPDATE_STATE.value
|
||||
revision.save()
|
||||
|
||||
return redirect(reverse('budget_view', kwargs={'id': budget.id}))
|
||||
|
Loading…
Reference in New Issue
Block a user