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:
+
+