diff --git a/sspromotions/jinja2/sspromotions/email/bulletin.txt b/sspromotions/jinja2/sspromotions/email/bulletin.txt new file mode 100644 index 0000000..5234039 --- /dev/null +++ b/sspromotions/jinja2/sspromotions/email/bulletin.txt @@ -0,0 +1,23 @@ +{% for event in events %}{{ event.date.strftime('%a %d %b') }}: {{ event.title }}{% if event.link %} <{{ event.link }}>{% endif %} +{% endfor %} +{% for group in groups %}{{ group.name }} +============================== + +{% for item in group.bulletin_items %}{{ item.title }} +-------------------------- + +{{ item.content }} + +{% if item.link %}Read more <{{ item.link }}> + +{% endif %}{% endfor %}{% endfor %}{% if more %}More from MUMUS +============================== + +{% for item in more %}{{ item.title }} +-------------------------- + +{{ item.content }} + +{% if item.link %}Read more <{{ item.link }}> + +{% endif %}{% endfor %}{% endif %}© Copyright {{ import('datetime').datetime.now().strftime('%Y') }} MUMUS Inc. All Rights Reserved. diff --git a/sspromotions/management/commands/sendbulletin.py b/sspromotions/management/commands/sendbulletin.py new file mode 100644 index 0000000..323855c --- /dev/null +++ b/sspromotions/management/commands/sendbulletin.py @@ -0,0 +1,108 @@ +# 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 . + +import boto3 +from botocore.exceptions import ClientError +import premailer + +from django.core.management.base import BaseCommand, CommandError + +from django.conf import settings +from django.template import loader +from django.urls import reverse +from django.utils import timezone + +import ssmembership.models +import sspromotions.utils + +import logging +import time +import urllib.parse + +def send_aws_email(client, email, subject, content_html, content_txt): + def send_mail(**kwargs): + for i in range(0, 10): + try: + client.send_email(**kwargs) + return + except ClientError as e: + if e.response['Error']['Code'] == 'Throttling' and e.response['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') + + send_mail( + Destination={ + 'ToAddresses': [email], + }, + 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), + ) + +class Command(BaseCommand): + help = 'Send bulletin emails' + + def add_arguments(self, parser): + parser.add_argument('ids', nargs='*', type=int, help='Members with ID numbers equal to these values will be emailed (default all)') + parser.add_argument('--render', action='store_true', help='Render to stdout instead of sending emails') + + def handle(self, *args, **options): + template_html = loader.get_template('sspromotions/email/bulletin.html') + template_txt = loader.get_template('sspromotions/email/bulletin.txt') + + members = ssmembership.models.Member.objects.all() + + if len(options['ids']) > 0: + members = [member for member in members if member.id in options['ids']] + else: + raise Exception('Must provide IDs') + + client = boto3.client('ses', aws_access_key_id=settings.AWS_KEY_ID, aws_secret_access_key=settings.AWS_SECRET, region_name=settings.AWS_REGION) + + title = '{} News: {}'.format(settings.ORG_NAME, timezone.now().strftime('%d %m %Y')) + + calbegin, calend, bulbegin, bulend = sspromotions.utils.bulletin_dates(timezone.now()) + events = list(sspromotions.utils.get_calendar_events(calbegin, calend)) + + for member in members: + template_args = sspromotions.utils.bulletin_args(member, events, bulbegin, bulend) + + content_html = premailer.Premailer(template_html.render(template_args), cssutils_logging_level=logging.ERROR, strip_important=False).transform() + content_txt = template_txt.render(template_args) + + if options['render']: + self.stdout.write('Content-Type: multipart/alternative; boundary=boundary\n\n--boundary\nContent-Type: text/html; charset=utf-8\n\n' + content_html + '\n--boundary\nContent-Type: text/plain; charset=utf-8\n\n' + content_txt + '\n--boundary') + else: + self.stdout.write('Emailing {} at {}'.format(member.id, member.email)) + send_aws_email(client, member.email, title, content_html, content_txt) diff --git a/sspromotions/utils.py b/sspromotions/utils.py new file mode 100644 index 0000000..f73e77d --- /dev/null +++ b/sspromotions/utils.py @@ -0,0 +1,78 @@ +# 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 . + +from googleapiclient.discovery import build + +from django.conf import settings +from django.utils import timezone + +from . import models + +import datetime + +def get_calendar_events(calbegin, calend): + service = build('calendar', 'v3', developerKey=settings.GOOGLE_API_KEY) + events = service.events().list( + calendarId=settings.GOOGLE_CALENDAR_ID, + orderBy='startTime', + singleEvents=True, + timeMin=datetime.datetime.combine(calbegin, datetime.time(tzinfo=timezone.get_current_timezone())).isoformat(), + timeMax=datetime.datetime.combine(calend, datetime.time(tzinfo=timezone.get_current_timezone())).isoformat() + ).execute() + for event in events['items']: + yield { + 'date': datetime.datetime.strptime(event['start']['date'], '%Y-%m-%d') if 'date' in event['start'] else datetime.datetime.strptime(event['start']['dateTime'][:-3] + event['start']['dateTime'][-2:], '%Y-%m-%dT%H:%M:%S%z'), + 'title': event['summary'], + 'link': event['htmlLink'] + } + +def bulletin_dates(dt): + calbegin = dt.date() + datetime.timedelta(days=1) # Start tomorrow for calendar + calend = calbegin + datetime.timedelta(days=14) + + bulbegin = dt.date() + bulend = bulbegin + datetime.timedelta(days=7) + + return calbegin, calend, bulbegin, bulend + +def bulletin_args(member, events, bulbegin, bulend): + if member is None: + groups = models.Group.objects.all() + else: + groups = models.Group.get_member_groups(member) + + groups_data = [] + for group in groups: + items = [] + for item in group.bulletinitem_set.filter(date__gte=bulbegin).filter(date__lt=bulend).all(): + # Check also_limit + if member is None or len(item.also_limit) == 0 or any(models.Group.objects.get(id=x).contains_member(member) for x in item.also_limit if models.Group.objects.filter(id=x).count() > 0): + items.append(item) + + if len(items) > 0: + groups_data.append({ + 'group': group, + 'name': group.name, + 'bulletin_items': items + }) + + return { + 'events': events, + 'groups': groups_data, + 'more': [], # TODO + 'bulbegin': bulbegin, + 'bulend': bulend + } diff --git a/sspromotions/views.py b/sspromotions/views.py index 92e77c0..7af339a 100644 --- a/sspromotions/views.py +++ b/sspromotions/views.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from googleapiclient.discovery import build - from django.contrib.auth.decorators import login_required from django.conf import settings @@ -25,30 +23,14 @@ from django.urls import reverse from django.utils import timezone from . import models +from . import utils import datetime -import json @login_required def index(request): return render(request, 'sspromotions/index.html') -def get_calendar_events(calbegin, calend): - service = build('calendar', 'v3', developerKey=settings.GOOGLE_API_KEY) - events = service.events().list( - calendarId=settings.GOOGLE_CALENDAR_ID, - orderBy='startTime', - singleEvents=True, - timeMin=datetime.datetime.combine(calbegin, datetime.time(tzinfo=timezone.get_current_timezone())).isoformat(), - timeMax=datetime.datetime.combine(calend, datetime.time(tzinfo=timezone.get_current_timezone())).isoformat() - ).execute() - for event in events['items']: - yield { - 'date': datetime.datetime.strptime(event['start']['date'], '%Y-%m-%d') if 'date' in event['start'] else datetime.datetime.strptime(event['start']['dateTime'][:-3] + event['start']['dateTime'][-2:], '%Y-%m-%dT%H:%M:%S%z'), - 'title': event['summary'], - 'link': event['htmlLink'] - } - @login_required def bulletin_list(request): items_past = [] @@ -77,19 +59,8 @@ def bulletin_list(request): @login_required def bulletin_preview(request): - calbegin = timezone.now().date() + datetime.timedelta(days=1) # Start tomorrow for calendar - calend = calbegin + datetime.timedelta(days=14) - - bulbegin = timezone.now().date() - bulend = bulbegin + datetime.timedelta(days=7) - - return render(request, 'sspromotions/email/bulletin.html', { - 'events': get_calendar_events(calbegin, calend), - 'groups': models.Group.objects.all(), - 'more': [], - 'bulbegin': bulbegin, - 'bulend': bulend - }) + calbegin, calend, bulbegin, bulend = utils.bulletin_dates(timezone.now()) + return render(request, 'sspromotions/email/bulletin.html', utils.bulletin_args(None, utils.get_calendar_events(calbegin, calend), bulbegin, bulend)) @login_required def bulletin_new(request):