From 6ffce428c58fde5e1020f2c1a10ce855add78c88 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Thu, 7 Apr 2022 21:39:38 +0000 Subject: [PATCH] Download and email iCal files for classes --- apiserver/apiserver/api/emails/ical.html | 14 +++++++ apiserver/apiserver/api/emails/ical.txt | 5 +++ apiserver/apiserver/api/utils_email.py | 28 ++++++++++++- apiserver/apiserver/api/views.py | 42 ++++++++++++++++++- webclient/src/Classes.js | 52 +++++++++++++++++++++++- 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 apiserver/apiserver/api/emails/ical.html create mode 100644 apiserver/apiserver/api/emails/ical.txt diff --git a/apiserver/apiserver/api/emails/ical.html b/apiserver/apiserver/api/emails/ical.html new file mode 100644 index 0000000..2ef2b9a --- /dev/null +++ b/apiserver/apiserver/api/emails/ical.html @@ -0,0 +1,14 @@ + + + + + + + +
Hi [name],
+

+
Please find attached the iCalendar file for [class] on [date].
+

+
Spaceport
+ + diff --git a/apiserver/apiserver/api/emails/ical.txt b/apiserver/apiserver/api/emails/ical.txt new file mode 100644 index 0000000..9016a24 --- /dev/null +++ b/apiserver/apiserver/api/emails/ical.txt @@ -0,0 +1,5 @@ +Hi [name], + +Please find attached the iCalendar file for [class] on [date]. + +Spaceport diff --git a/apiserver/apiserver/api/utils_email.py b/apiserver/apiserver/api/utils_email.py index 0aae5d2..319ca22 100644 --- a/apiserver/apiserver/api/utils_email.py +++ b/apiserver/apiserver/api/utils_email.py @@ -5,7 +5,7 @@ import os import smtplib from datetime import datetime, timedelta -from django.core.mail import send_mail +from django.core.mail import send_mail, EmailMultiAlternatives from . import utils from .. import settings @@ -39,3 +39,29 @@ def send_welcome_email(member): ) logger.info('Sent welcome email:\n' + email_text) + +def send_ical_email(member, session, ical_file): + def replace_fields(text): + return text.replace( + '[name]', member.first_name, + ).replace( + '[class]', session.course.name, + ).replace( + '[date]', session.datetime.strftime('%A, %B %d'), + ) + + with open(EMAIL_DIR + 'ical.txt', 'r') as f: + email_text = replace_fields(f.read()) + + with open(EMAIL_DIR + 'ical.html', 'r') as f: + email_html = replace_fields(f.read()) + + subject = 'Protospace ' + session.course.name + from_email = None # defaults to DEFAULT_FROM_EMAIL + to = member.user.email + msg = EmailMultiAlternatives(subject, email_text, from_email, [to]) + msg.attach_alternative(email_html, "text/html") + msg.attach('event.ics', ical_file, 'text/calendar') + msg.send() + + logger.info('Sent ical email:\n' + email_text) diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index 1ffbc40..251fd6e 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -18,11 +18,12 @@ from rest_auth.views import PasswordChangeView, PasswordResetView, PasswordReset from rest_auth.registration.views import RegisterView from fuzzywuzzy import fuzz, process from collections import OrderedDict +import icalendar import datetime, time import requests -from . import models, serializers, utils, utils_paypal, utils_stats, utils_ldap +from . import models, serializers, utils, utils_paypal, utils_stats, utils_ldap, utils_email from .permissions import ( is_admin_director, AllowMetadata, @@ -274,6 +275,45 @@ class SessionViewSet(Base, List, Retrieve, Create, Update): def perform_create(self, serializer): serializer.save(instructor=self.request.user) + def generate_ical(self, session): + cal = icalendar.Calendar() + cal.add('prodid', '-//Protospace//Spaceport//') + cal.add('version', '2.0') + + event = icalendar.Event() + event.add('summary', session.course.name) + event.add('dtstart', session.datetime) + event.add('dtend', session.datetime + datetime.timedelta(hours=1)) + event.add('dtstamp', now()) + + cal.add_component(event) + + return cal.to_ical() + + @action(detail=True, methods=['get']) + def download_ical(self, request, pk=None): + session = get_object_or_404(models.Session, id=pk) + user = self.request.user + + ical_file = self.generate_ical(session).decode() + + response = FileResponse(ical_file, filename='event.ics') + response['Content-Type'] = 'text/calendar' + response['Content-Disposition'] = 'attachment; filename="event.ics"' + + return response + + @action(detail=True, methods=['post']) + def email_ical(self, request, pk=None): + session = get_object_or_404(models.Session, id=pk) + user = self.request.user + + ical_file = self.generate_ical(session).decode() + + utils_email.send_ical_email(user.member, session, ical_file) + + return Response(200) + class TrainingViewSet(Base, Retrieve, Create, Update): permission_classes = [AllowMetadata | IsAuthenticated, IsObjOwnerOrAdmin | IsSessionInstructorOrAdmin | ReadOnly] diff --git a/webclient/src/Classes.js b/webclient/src/Classes.js index d639e57..c71914c 100644 --- a/webclient/src/Classes.js +++ b/webclient/src/Classes.js @@ -3,7 +3,7 @@ import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-r import './light.css'; import { Label, Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; import moment from 'moment-timezone'; -import { isAdmin, isInstructor, getInstructor, BasicTable, requester } from './utils.js'; +import { apiUrl, isAdmin, isInstructor, getInstructor, BasicTable, requester } from './utils.js'; import { NotFound, PleaseLogin } from './Misc.js'; import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js'; import { PayPalPayNow } from './PayPal.js'; @@ -287,6 +287,52 @@ export function Classes(props) { ); }; + +export function ICalButtons(props) { + const { token, clazz } = props; + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(false); + + const handleDownload = (e) => { + e.preventDefault(); + window.location = apiUrl + '/sessions/' + clazz.id + '/download_ical/'; + } + + const handleEmail = (e) => { + e.preventDefault(); + if (loading) return; + setLoading(true); + setSuccess(false); + requester('/sessions/' + clazz.id + '/email_ical/', 'POST', token, {}) + .then(res => { + setLoading(false); + setSuccess(true); + }) + .catch(err => { + setLoading(false); + console.log(err); + setError(true); + }); + }; + + return ( + <> + + {success ? +   Sent! + : + + } + {error && Error.} + + ); +}; + export function ClassDetail(props) { const [clazz, setClass] = useState(false); const [refreshCount, refreshClass] = useReducer(x => x + 1, 0); @@ -389,6 +435,10 @@ export function ClassDetail(props) { Students: {clazz.student_count} {!!clazz.max_students && '/ '+clazz.max_students} + + iCalendar: + +