Download and email iCal files for classes

This commit is contained in:
Tanner Collin 2022-04-07 21:39:38 +00:00
parent eb7d34c92d
commit 6ffce428c5
5 changed files with 138 additions and 3 deletions

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<style type="text/css">p.MsoNormal,p.MsoNoSpacing{margin:0}</style>
</head>
<body>
<div>Hi [name],<br></div>
<div><br></div>
<div>Please find attached the iCalendar file for [class] on [date].<br></div>
<div><br></div>
<div>Spaceport<br></div>
</body>
</html>

View File

@ -0,0 +1,5 @@
Hi [name],
Please find attached the iCalendar file for [class] on [date].
Spaceport

View File

@ -5,7 +5,7 @@ import os
import smtplib import smtplib
from datetime import datetime, timedelta 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 utils
from .. import settings from .. import settings
@ -39,3 +39,29 @@ def send_welcome_email(member):
) )
logger.info('Sent welcome email:\n' + email_text) 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)

View File

@ -18,11 +18,12 @@ from rest_auth.views import PasswordChangeView, PasswordResetView, PasswordReset
from rest_auth.registration.views import RegisterView from rest_auth.registration.views import RegisterView
from fuzzywuzzy import fuzz, process from fuzzywuzzy import fuzz, process
from collections import OrderedDict from collections import OrderedDict
import icalendar
import datetime, time import datetime, time
import requests 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 ( from .permissions import (
is_admin_director, is_admin_director,
AllowMetadata, AllowMetadata,
@ -274,6 +275,45 @@ class SessionViewSet(Base, List, Retrieve, Create, Update):
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(instructor=self.request.user) 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): class TrainingViewSet(Base, Retrieve, Create, Update):
permission_classes = [AllowMetadata | IsAuthenticated, IsObjOwnerOrAdmin | IsSessionInstructorOrAdmin | ReadOnly] permission_classes = [AllowMetadata | IsAuthenticated, IsObjOwnerOrAdmin | IsSessionInstructorOrAdmin | ReadOnly]

View File

@ -3,7 +3,7 @@ import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-r
import './light.css'; import './light.css';
import { Label, Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; 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 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 { NotFound, PleaseLogin } from './Misc.js';
import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js'; import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js';
import { PayPalPayNow } from './PayPal.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 (
<>
<Button compact onClick={handleDownload}>
Download
</Button>
{success ?
<span>&nbsp;&nbsp;Sent!</span>
:
<Button compact loading={loading} onClick={handleEmail}>
Email
</Button>
}
{error && <span>Error.</span>}
</>
);
};
export function ClassDetail(props) { export function ClassDetail(props) {
const [clazz, setClass] = useState(false); const [clazz, setClass] = useState(false);
const [refreshCount, refreshClass] = useReducer(x => x + 1, 0); const [refreshCount, refreshClass] = useReducer(x => x + 1, 0);
@ -389,6 +435,10 @@ export function ClassDetail(props) {
<Table.Cell>Students:</Table.Cell> <Table.Cell>Students:</Table.Cell>
<Table.Cell>{clazz.student_count} {!!clazz.max_students && '/ '+clazz.max_students}</Table.Cell> <Table.Cell>{clazz.student_count} {!!clazz.max_students && '/ '+clazz.max_students}</Table.Cell>
</Table.Row> </Table.Row>
<Table.Row>
<Table.Cell>iCalendar:</Table.Cell>
<Table.Cell><ICalButtons token={token} clazz={clazz} /></Table.Cell>
</Table.Row>
</Table.Body> </Table.Body>
</BasicTable> </BasicTable>