Merge branch 'master' into usage_tracking
This commit is contained in:
40
apiserver/apiserver/api/emails/welcome.html
Normal file
40
apiserver/apiserver/api/emails/welcome.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!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>You just signed up to Spaceport with the username: [username]<br></div>
|
||||
<div><br></div>
|
||||
<div>To manage your Protospace membership go to:<br></div>
|
||||
<div><a href="https://my.protospace.ca">https://my.protospace.ca</a><br></div>
|
||||
<div><br></div>
|
||||
<div>You have automatically been added to our forum Spacebar at:<br></div>
|
||||
<div><a href="https://forum.protospace.ca">https://forum.protospace.ca</a><br></div>
|
||||
<div><br></div>
|
||||
<div>Please introduce yourself here:<br></div>
|
||||
<div><a href="https://forum.protospace.ca/c/chattymcchatface/new-user-introductions/31">https://forum.protospace.ca/c/chattymcchatface/new-user-introductions/31</a><br></div>
|
||||
<div><br></div>
|
||||
<div>If you have any questions, you will get the fastest response there.<br></div>
|
||||
<div><br></div>
|
||||
<div>Your next goal is to become vetted after:<br></div>
|
||||
<div>- paying your member dues<br></div>
|
||||
<div>- being a member for four weeks<br></div>
|
||||
<div>- attending a New Member Orientation<br></div>
|
||||
<div>- finding two members to sponsor (vouch for) you<br></div>
|
||||
<div><br></div>
|
||||
<div>You can meet members Tuesday evenings during our open house.<br></div>
|
||||
<div><br></div>
|
||||
<div>Mark [date] on your calendar as the day you can get vetted.<br></div>
|
||||
<div><br></div>
|
||||
<div>Sign up for a New Member Orientation here:<br></div>
|
||||
<div><a href="https://my.protospace.ca/classes">https://my.protospace.ca/classes</a><br></div>
|
||||
<div><br></div>
|
||||
<div>Good luck,<br></div>
|
||||
<div>Spaceport<br></div>
|
||||
<div><br></div>
|
||||
</body>
|
||||
</html>
|
30
apiserver/apiserver/api/emails/welcome.txt
Normal file
30
apiserver/apiserver/api/emails/welcome.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
Hi [name],
|
||||
|
||||
You just signed up to Spaceport with the username: [username]
|
||||
|
||||
To manage your Protospace membership go to:
|
||||
https://my.protospace.ca
|
||||
|
||||
You have automatically been added to our forum Spacebar at:
|
||||
https://forum.protospace.ca
|
||||
|
||||
Please introduce yourself here:
|
||||
https://forum.protospace.ca/c/chattymcchatface/new-user-introductions/31
|
||||
|
||||
If you have any questions, you will get the fastest response there.
|
||||
|
||||
Your next goal is to become vetted after:
|
||||
- paying your member dues
|
||||
- being a member for four weeks
|
||||
- attending a New Member Orientation
|
||||
- finding two members to sponsor (vouch for) you
|
||||
|
||||
You can meet members Tuesday evenings during our open house.
|
||||
|
||||
Mark [date] on your calendar as the day you can get vetted.
|
||||
|
||||
Sign up for a New Member Orientation here:
|
||||
https://my.protospace.ca/classes
|
||||
|
||||
Good luck,
|
||||
Spaceport
|
@@ -117,6 +117,7 @@ class Course(models.Model):
|
||||
name = models.TextField(blank=True, null=True)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
is_old = models.BooleanField(default=False)
|
||||
tags = models.CharField(max_length=128, blank=True)
|
||||
|
||||
history = HistoricalRecords()
|
||||
|
||||
|
@@ -449,12 +449,13 @@ class StudentTrainingSerializer(TrainingSerializer):
|
||||
class CourseSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.Course
|
||||
fields = ['id', 'name', 'is_old', 'description']
|
||||
fields = ['id', 'name', 'is_old', 'description', 'tags']
|
||||
|
||||
class SessionSerializer(serializers.ModelSerializer):
|
||||
student_count = serializers.SerializerMethodField()
|
||||
course_data = serializers.SerializerMethodField()
|
||||
instructor_name = serializers.SerializerMethodField()
|
||||
instructor_id = serializers.SerializerMethodField()
|
||||
datetime = serializers.DateTimeField()
|
||||
course = serializers.PrimaryKeyRelatedField(queryset=models.Course.objects.all())
|
||||
students = TrainingSerializer(many=True, read_only=True)
|
||||
@@ -479,6 +480,12 @@ class SessionSerializer(serializers.ModelSerializer):
|
||||
name = 'Unknown'
|
||||
return obj.old_instructor or name
|
||||
|
||||
def get_instructor_id(self, obj):
|
||||
if obj.instructor and hasattr(obj.instructor, 'member'):
|
||||
return obj.instructor.member.id
|
||||
else:
|
||||
return None
|
||||
|
||||
class SessionListSerializer(SessionSerializer):
|
||||
students = None
|
||||
|
||||
|
@@ -22,6 +22,8 @@ class LoggingThrottle(throttling.BaseThrottle):
|
||||
pass
|
||||
elif path.startswith('/stats/'):
|
||||
return True
|
||||
elif path == '/sessions/' and user == None:
|
||||
return True
|
||||
|
||||
if request.data:
|
||||
data = request.data.dict()
|
||||
|
@@ -21,7 +21,7 @@ from django.db.models import Sum
|
||||
from django.core.cache import cache
|
||||
from django.utils.timezone import now, pytz
|
||||
|
||||
from . import models, serializers, utils_ldap, utils_stats, utils_auth, utils
|
||||
from . import models, serializers, utils_ldap, utils_stats, utils_auth, utils, utils_email
|
||||
from .. import settings
|
||||
|
||||
STATIC_FOLDER = 'data/static/'
|
||||
@@ -374,6 +374,14 @@ def register_user(data, user):
|
||||
utils.alert_tanner(msg)
|
||||
logger.info(msg)
|
||||
|
||||
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Sending welcome email...')
|
||||
try:
|
||||
utils_email.send_welcome_email(user.member)
|
||||
except BaseException as e: # TODO: remove, just for testing
|
||||
msg = 'Problem sending welcome email: ' + str(e)
|
||||
logger.exception(msg)
|
||||
alert_tanner(msg)
|
||||
|
||||
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Done!')
|
||||
time.sleep(1)
|
||||
|
||||
|
46
apiserver/apiserver/api/utils_email.py
Normal file
46
apiserver/apiserver/api/utils_email.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import os
|
||||
import smtplib
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.core.mail import send_mail
|
||||
|
||||
from . import utils
|
||||
from .. import settings
|
||||
|
||||
EMAIL_DIR = os.path.join(settings.BASE_DIR, 'apiserver/api/emails/')
|
||||
|
||||
def send_welcome_email(member):
|
||||
vetting_date = member.application_date + timedelta(days=28)
|
||||
|
||||
def replace_fields(text):
|
||||
return text.replace(
|
||||
'[name]', member.first_name,
|
||||
).replace(
|
||||
'[username]', member.user.username,
|
||||
).replace(
|
||||
'[date]', vetting_date.strftime('%A, %B %d'),
|
||||
)
|
||||
|
||||
with open(EMAIL_DIR + 'welcome.txt', 'r') as f:
|
||||
email_text = replace_fields(f.read())
|
||||
|
||||
with open(EMAIL_DIR + 'welcome.html', 'r') as f:
|
||||
email_html = replace_fields(f.read())
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
subject='Welcome to Protospace!',
|
||||
message=email_text,
|
||||
from_email=None, # defaults to DEFAULT_FROM_EMAIL
|
||||
recipient_list=[member.user.email, 'portal@tannercollin.com'],
|
||||
html_message=email_html,
|
||||
)
|
||||
|
||||
logger.info('Sent welcome email:\n' + email_text)
|
||||
except smtplib.SMTPException as e:
|
||||
msg = 'Problem sending welcome email to ' + member.user.email + ': ' + str(e)
|
||||
utils.alert_tanner(msg)
|
||||
logger.exception(msg)
|
@@ -353,7 +353,7 @@ def process_paypal_ipn(data):
|
||||
defaults=dict(user=user),
|
||||
)
|
||||
|
||||
if custom_json.get('category', False) in ['Snacks', 'OnAcct', 'Donation']:
|
||||
if custom_json.get('category', False) in ['Snacks', 'OnAcct', 'Donation', 'Consumables']:
|
||||
logger.info('IPN - Category matched')
|
||||
update_ipn(ipn, 'Accepted, category')
|
||||
return create_category_tx(data, member, custom_json)
|
||||
|
@@ -4,7 +4,7 @@ logger = logging.getLogger(__name__)
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.db import transaction
|
||||
from django.db.models import Max, F, Count
|
||||
from django.db.models import Max, F, Count, Q
|
||||
from django.db.utils import OperationalError
|
||||
from django.http import HttpResponse, Http404, FileResponse
|
||||
from django.core.files.base import File
|
||||
@@ -230,7 +230,7 @@ class CardViewSet(Base, Create, Retrieve, Update, Destroy):
|
||||
|
||||
|
||||
class CourseViewSet(Base, List, Retrieve, Create, Update):
|
||||
permission_classes = [AllowMetadata | IsAuthenticated, IsAdminOrReadOnly | IsInstructorOrReadOnly]
|
||||
permission_classes = [AllowMetadata | IsAuthenticatedOrReadOnly, IsAdminOrReadOnly | IsInstructorOrReadOnly]
|
||||
queryset = models.Course.objects.annotate(date=Max('sessions__datetime')).order_by('-date')
|
||||
|
||||
def get_serializer_class(self):
|
||||
@@ -241,11 +241,27 @@ class CourseViewSet(Base, List, Retrieve, Create, Update):
|
||||
|
||||
|
||||
class SessionViewSet(Base, List, Retrieve, Create, Update):
|
||||
permission_classes = [AllowMetadata | IsAuthenticated, IsAdminOrReadOnly | IsInstructorOrReadOnly]
|
||||
permission_classes = [AllowMetadata | IsAuthenticatedOrReadOnly, IsAdminOrReadOnly | IsInstructorOrReadOnly]
|
||||
|
||||
def get_queryset(self):
|
||||
if self.action == 'list':
|
||||
return models.Session.objects.order_by('-datetime')[:50]
|
||||
week_ago = now() - datetime.timedelta(days=7)
|
||||
year_ago = now() - datetime.timedelta(days=365)
|
||||
|
||||
return models.Session.objects.annotate(
|
||||
course_count=Count(
|
||||
'course__sessions',
|
||||
filter=Q(
|
||||
course__sessions__datetime__gte=year_ago,
|
||||
),
|
||||
),
|
||||
).filter(
|
||||
datetime__gte=week_ago,
|
||||
).order_by(
|
||||
'-course_count',
|
||||
'-course_id',
|
||||
'datetime',
|
||||
)
|
||||
else:
|
||||
return models.Session.objects.all()
|
||||
|
||||
@@ -323,7 +339,7 @@ class TrainingViewSet(Base, Retrieve, Create, Update):
|
||||
|
||||
if data.get('member_id', None):
|
||||
if not (is_admin_director(user) or session.instructor == user):
|
||||
raise exceptions.ValidationError('Not allowed to register others')
|
||||
raise exceptions.ValidationError(dict(non_field_errors='Not allowed to register others'))
|
||||
|
||||
member = get_object_or_404(models.Member, id=data['member_id'])
|
||||
user = member.user
|
||||
@@ -338,9 +354,9 @@ class TrainingViewSet(Base, Retrieve, Create, Update):
|
||||
else:
|
||||
training = models.Training.objects.filter(user=user, session=session)
|
||||
if training.exists():
|
||||
raise exceptions.ValidationError('Already registered')
|
||||
raise exceptions.ValidationError(dict(non_field_errors='Already registered'))
|
||||
if user == session.instructor:
|
||||
raise exceptions.ValidationError('You are teaching this session')
|
||||
raise exceptions.ValidationError(dict(non_field_errors='You are teaching this session'))
|
||||
if status == 'Waiting for payment' and session.cost == 0:
|
||||
status = 'Confirmed'
|
||||
serializer.save(user=user, attendance_status=status)
|
||||
@@ -736,12 +752,23 @@ class PasteView(views.APIView):
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
def get(self, request):
|
||||
return Response(dict(paste=cache.get('paste', '')))
|
||||
if request.user.id == 9:
|
||||
key = 'special_paste'
|
||||
logging.info('Using special paste for a special someone.')
|
||||
else:
|
||||
key = 'paste'
|
||||
|
||||
return Response(dict(paste=cache.get(key, '')))
|
||||
|
||||
def post(self, request):
|
||||
if 'paste' in request.data:
|
||||
cache.set('paste', request.data['paste'][:20000])
|
||||
return Response(dict(paste=cache.get('paste', '')))
|
||||
if request.user.id == 9:
|
||||
key = 'special_paste'
|
||||
logging.info('Using special paste for a special someone.')
|
||||
else:
|
||||
key = 'paste'
|
||||
cache.set(key, request.data['paste'][:20000])
|
||||
return Response(dict(paste=cache.get(key, '')))
|
||||
else:
|
||||
raise exceptions.ValidationError(dict(paste='This field is required.'))
|
||||
|
||||
|
@@ -282,7 +282,7 @@ DEFAULT_FROM_EMAIL = 'Protospace Portal <portal@mg.protospace.ca>'
|
||||
if DEBUG: logger.info('Debug mode ON')
|
||||
logger.info('Test logging for each thread')
|
||||
|
||||
APP_VERSION = 1 # TODO: automate this
|
||||
APP_VERSION = 2 # TODO: automate this
|
||||
|
||||
#import logging_tree
|
||||
#logging_tree.printout()
|
||||
|
22
apiserver/export_class_report.py
Normal file
22
apiserver/export_class_report.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import django, sys, os
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'apiserver.settings'
|
||||
django.setup()
|
||||
|
||||
import csv
|
||||
from apiserver.api import models
|
||||
|
||||
sessions = models.Session.objects.filter(datetime__gte='2021-01-01')
|
||||
|
||||
with open('output.csv', 'w', newline='') as csvfile:
|
||||
fields = ['date', 'name', 'num_students']
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fields)
|
||||
|
||||
writer.writeheader()
|
||||
|
||||
for s in sessions:
|
||||
writer.writerow(dict(
|
||||
date=s.datetime.date(),
|
||||
name=s.course.name,
|
||||
num_students=s.students.filter(attendance_status='Attended').count(),
|
||||
))
|
||||
|
104
apiserver/output.csv
Normal file
104
apiserver/output.csv
Normal file
@@ -0,0 +1,104 @@
|
||||
date,name,num_students
|
||||
2021-07-25,New Members: Orientation and Basic Safety,5
|
||||
2021-07-17,Monthly Cleanup and Group Lunch,0
|
||||
2021-08-07,Woodworking Tools 1: Intro to Saws,5
|
||||
2021-08-08,Woodworking Tools 1: Intro to Saws,1
|
||||
2021-08-07,"Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander",6
|
||||
2021-08-08,"Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander",1
|
||||
2021-08-07,Introduction to the Wood Lathe,4
|
||||
2021-08-08,Introduction to the Wood Lathe,0
|
||||
2021-08-07,New Members: Orientation and Basic Safety,3
|
||||
2021-08-22,New Members: Orientation and Basic Safety,6
|
||||
2021-08-21,Woodworking Tools 1: Intro to Saws,3
|
||||
2021-08-21,Monthly Cleanup and Group Lunch,0
|
||||
2021-09-11,Annual General Meeting,0
|
||||
2021-09-11,Monthly Members Meeting,0
|
||||
2021-09-25,Metal: Metal Cutting & Manual Lathe,5
|
||||
2021-08-21,Introduction to the Wood Lathe,5
|
||||
2021-09-02,New Members: Orientation and Basic Safety,3
|
||||
2021-09-12,Basic CNC Wood Router,6
|
||||
2021-09-04,"Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander",2
|
||||
2021-09-12,Woodworking Tools 1: Intro to Saws,4
|
||||
2021-09-12,Introduction to the Wood Lathe,2
|
||||
2021-09-11,New Members: Orientation and Basic Safety,1
|
||||
2021-10-21,Monthly Members Meeting,0
|
||||
2021-09-18,Monthly Cleanup and Group Lunch,0
|
||||
2021-09-23,New Members: Orientation and Basic Safety,5
|
||||
2021-10-03,New Members: Orientation and Basic Safety,1
|
||||
2021-09-18,Basic CNC Wood Router,6
|
||||
2021-10-02,Woodworking Tools 1: Intro to Saws,3
|
||||
2021-10-09,Basic CNC Wood Router,5
|
||||
2021-10-23,Laser: Cutting and Engraving,5
|
||||
2021-11-06,Laser: Cutting and Engraving,3
|
||||
2021-10-23,Laser: Cutting and Engraving,6
|
||||
2021-10-16,Laser II: Trotec Course (For Rabbit-Certified members),0
|
||||
2021-10-16,Laser II: Trotec Course (For Rabbit-Certified members),3
|
||||
2021-10-23,Laser II: Trotec Course (For Rabbit-Certified members),1
|
||||
2021-10-23,Laser II: Trotec Course (For Rabbit-Certified members),0
|
||||
2021-10-30,Laser II: Trotec Course (For Rabbit-Certified members),8
|
||||
2021-10-30,Laser II: Trotec Course (For Rabbit-Certified members),0
|
||||
2021-10-22,New Members: Orientation and Basic Safety,1
|
||||
2021-10-17,Woodworking Tools 1: Intro to Saws,5
|
||||
2021-10-17,"Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander",8
|
||||
2021-10-17,Introduction to the Wood Lathe,3
|
||||
2021-10-16,Monthly Cleanup and Group Lunch,0
|
||||
2021-11-21,Woodworking Tools 1: Intro to Saws,4
|
||||
2021-11-06,CAD/CAM for The Complete Beginner: Fusion 360,4
|
||||
2021-11-07,New Members: Orientation and Basic Safety,6
|
||||
2021-11-07,Laser II: Trotec Course (For Rabbit-Certified members),0
|
||||
2021-11-05,Electronics Night,0
|
||||
2021-11-13,Basic CNC Wood Router,4
|
||||
2021-11-28,Basic CNC Wood Router,4
|
||||
2021-12-03,Electronics Night,0
|
||||
2021-11-25,New Members: Orientation and Basic Safety,7
|
||||
2021-12-11,New Members: Orientation and Basic Safety,5
|
||||
2021-11-19,Monthly Members Meeting,0
|
||||
2021-11-20,Monthly Cleanup and Group Lunch,0
|
||||
2022-01-15,CAD/CAM for The Complete Beginner: Fusion 360,0
|
||||
2021-12-18,Monthly Cleanup and Group Lunch,0
|
||||
2022-01-16,Woodworking Tools 1: Intro to Saws,6
|
||||
2022-03-13,Woodworking Tools 1: Intro to Saws,0
|
||||
2022-05-15,Woodworking Tools 1: Intro to Saws,0
|
||||
2021-11-27,CRT Retro Computing Club,0
|
||||
2021-12-23,New Members: Orientation and Basic Safety,1
|
||||
2021-12-11,Roots2STEM tour,5
|
||||
2021-12-18,Basic CNC Wood Router,4
|
||||
2021-12-11,Woodworking Tools 1: Intro to Saws,3
|
||||
2021-12-11,"Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander",5
|
||||
2021-12-12,Introduction to the Wood Lathe,1
|
||||
2021-12-19,CRT Retro Computing Club,0
|
||||
2022-01-23,3D Printing: Introduction to 3D Printing,7
|
||||
2022-01-09,New Members: Orientation and Basic Safety,5
|
||||
2022-01-22,New Members: Orientation and Basic Safety,4
|
||||
2022-01-29,Basic CNC Wood Router,0
|
||||
2022-01-07,Electronics Night,0
|
||||
2022-02-04,Electronics Night,0
|
||||
2022-02-13,Woodworking Tools 1: Intro to Saws,0
|
||||
2022-02-03,New Members: Orientation and Basic Safety,0
|
||||
2022-02-27,Basic CNC Wood Router,0
|
||||
2022-03-05,Tormach: CAM and Tormach Intro,0
|
||||
2022-02-05,CAD: Introduction to 3D CAD (Fusion),0
|
||||
2022-01-30,Laser: Cutting and Engraving,0
|
||||
2022-01-22,CAD/CAM for The Complete Beginner Part II (Classroom Booking Edition),0
|
||||
2022-02-12,Metal: Metal Cutting & Manual Lathe,0
|
||||
2022-01-21,Monthly Members Meeting,0
|
||||
2022-02-12,New Members: Orientation and Basic Safety,0
|
||||
2022-02-24,New Members: Orientation and Basic Safety,0
|
||||
2022-03-26,Basic CNC Wood Router,0
|
||||
2022-01-29,"Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander",0
|
||||
2022-02-06,Laser: Cutting and Engraving,0
|
||||
2022-01-28,New Members: Orientation and Basic Safety,2
|
||||
2022-01-23,New Members: Orientation and Basic Safety,2
|
||||
2022-01-25,New Members: Orientation and Basic Safety,1
|
||||
2022-01-29,New Members: Orientation and Basic Safety,0
|
||||
2022-02-17,Monthly Members Meeting,0
|
||||
2022-02-19,Monthly Cleanup and Group Lunch,0
|
||||
2022-01-30,Intro to Blade Sharpening,0
|
||||
2022-01-29,Welding 101 - MIG Welding and Safety,0
|
||||
2021-12-30,Woodworking Tools 1: Intro to Saws,0
|
||||
2022-01-27,Woodworking Tools 1: Intro to Saws,2
|
||||
2022-02-03,Woodworking Tools 1: Intro to Saws,0
|
||||
2022-02-05,"Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander",0
|
||||
2022-02-12,Roots 2 STEM classes,0
|
||||
2022-02-12,Laser II: Trotec Course (For Rabbit-Certified members),0
|
||||
2022-02-19,Laser II: Trotec Course (For Rabbit-Certified members),0
|
|
Reference in New Issue
Block a user