Download and email iCal files for classes
This commit is contained in:
parent
eb7d34c92d
commit
6ffce428c5
14
apiserver/apiserver/api/emails/ical.html
Normal file
14
apiserver/apiserver/api/emails/ical.html
Normal 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>
|
5
apiserver/apiserver/api/emails/ical.txt
Normal file
5
apiserver/apiserver/api/emails/ical.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Hi [name],
|
||||||
|
|
||||||
|
Please find attached the iCalendar file for [class] on [date].
|
||||||
|
|
||||||
|
Spaceport
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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> 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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user