diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index 8d9de8e..b2bdb46 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -202,23 +202,6 @@ class CardSerializer(serializers.ModelSerializer): -class UserTrainingSerializer(serializers.ModelSerializer): - class Meta: - model = models.Training - exclude = ['user'] - depth = 2 - -class UserSerializer(serializers.ModelSerializer): - training = UserTrainingSerializer(many=True) - member = MemberSerializer() - - class Meta: - model = User - fields = ['id', 'username', 'member', 'transactions', 'cards', 'training', 'is_staff'] - depth = 1 - - - class TransactionSerializer(serializers.ModelSerializer): class Meta: model = models.Transaction @@ -229,10 +212,18 @@ class TransactionSerializer(serializers.ModelSerializer): class TrainingSerializer(serializers.ModelSerializer): attendance_status = serializers.ChoiceField(['waiting for payment', 'withdrawn', 'rescheduled', 'no-show', 'attended', 'confirmed']) session = serializers.PrimaryKeyRelatedField(queryset=models.Session.objects.all()) + student_name = serializers.SerializerMethodField() class Meta: model = models.Training fields = '__all__' read_only_fields = ['user', 'sign_up_date', 'paid_date', 'member_id'] + def get_student_name(self, obj): + if obj.user: + member = obj.user.member + else: + member = models.Member.objects.get(id=obj.member_id) + return member.preferred_name + ' ' + member.last_name + class StudentTrainingSerializer(TrainingSerializer): attendance_status = serializers.ChoiceField(['waiting for payment', 'withdrawn']) @@ -281,6 +272,24 @@ class CourseDetailSerializer(serializers.ModelSerializer): +class UserTrainingSerializer(serializers.ModelSerializer): + session = SessionListSerializer() + class Meta: + model = models.Training + exclude = ['user'] + depth = 2 + +class UserSerializer(serializers.ModelSerializer): + training = UserTrainingSerializer(many=True) + member = MemberSerializer() + + class Meta: + model = User + fields = ['id', 'username', 'member', 'transactions', 'cards', 'training', 'is_staff'] + depth = 1 + + + class RegistrationSerializer(RegisterSerializer): first_name = serializers.CharField(max_length=32) last_name = serializers.CharField(max_length=32) diff --git a/webclient/src/App.js b/webclient/src/App.js index 4894f01..e5c6231 100644 --- a/webclient/src/App.js +++ b/webclient/src/App.js @@ -154,7 +154,7 @@ function App() { - + diff --git a/webclient/src/Classes.js b/webclient/src/Classes.js index 8a600b6..1bae213 100644 --- a/webclient/src/Classes.js +++ b/webclient/src/Classes.js @@ -1,11 +1,11 @@ import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; import './light.css'; -import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; +import { Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; import moment from 'moment'; -import { isInstructor, BasicTable, requester } from './utils.js'; +import { isAdmin, isInstructor, BasicTable, requester } from './utils.js'; import { NotFound, PleaseLogin } from './Misc.js'; -import { InstructorClassDetail } from './InstructorClasses.js'; +import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js'; function ClassTable(props) { const { classes } = props; @@ -66,8 +66,7 @@ export function Classes(props) { return (
Class List
- -
Upcoming
+
Upcoming
{classes ? x.datetime > now)} /> : @@ -87,8 +86,9 @@ export function Classes(props) { export function ClassDetail(props) { const [clazz, setClass] = useState(false); const [error, setError] = useState(false); - const { token, user } = props; + const { token, user, setUserCache } = props; const { id } = useParams(); + const userTraining = user.training.find(x => x.session.id == id); useEffect(() => { requester('/sessions/'+id+'/', 'GET', token) @@ -101,6 +101,42 @@ export function ClassDetail(props) { }); }, []); + const handleSignup = () => { + const data = { attendance_status: 'waiting for payment', session: id }; + requester('/training/', 'POST', token, data) + .then(res => { + // bad code: + const newClass = { ...clazz, student_count: clazz.student_count+1 }; + setUserCache({ ...user, training: [...user.training, {...res, session: newClass }] }); + setClass(newClass); + }) + .catch(err => { + console.log(err); + setError(true); + }); + }; + + const handleToggle = (newStatus) => { + const data = { attendance_status: newStatus, session: id }; + requester('/training/'+userTraining.id+'/', 'PUT', token, data) + .then(res => { + // bad code: + const studentChange = newStatus === 'withdrawn' ? -1 : 1 + const newClass = { ...clazz, student_count: clazz.student_count + studentChange }; + const trainingIndex = user.training.indexOf(userTraining); + const newTraining = user.training; + newTraining[trainingIndex] = {...res, session: newClass }; + setUserCache({ ...user, training: newTraining }); + setClass(newClass); + }) + .catch(err => { + console.log(err); + setError(true); + }); + }; + + // TODO: calculate yesterday and lock signups + return ( {!error ? @@ -150,6 +186,37 @@ export function ClassDetail(props) {
Attendance
+ + {(isAdmin(user) || clazz.instructor === user.id) && + + + + } + + {clazz.instructor != user.id && + (userTraining ? +
+

Status: {userTraining.attendance_status}

+ {userTraining.attendance_status === 'withdrawn' ? + + : + + } +
+ : + ((clazz.max_students && clazz.student_count >= clazz.max_students) ? +

The course is full.

+ : + + ) + ) + } :

Loading...

diff --git a/webclient/src/InstructorClasses.js b/webclient/src/InstructorClasses.js index 36b1ff1..2701a89 100644 --- a/webclient/src/InstructorClasses.js +++ b/webclient/src/InstructorClasses.js @@ -7,6 +7,83 @@ import './light.css'; import { Button, Container, Checkbox, Divider, Dropdown, Form, Grid, Header, Icon, Image, Label, Menu, Message, Segment, Table } from 'semantic-ui-react'; import { BasicTable, staticUrl, requester } from './utils.js'; +function AttendanceRow(props) { + const { student, token } = props; + const [training, setTraining] = useState(student); + const [error, setError] = useState(false); + + const handleMark = (newStatus) => { + const data = { attendance_status: newStatus }; + requester('/training/'+training.id+'/', 'PATCH', token, data) + .then(res => { + setTraining(res); + setError(false); + }) + .catch(err => { + console.log(err); + setError(true); + }); + }; + + // 'withdrawn', 'rescheduled', 'no-show', 'attended', 'confirmed' + + const makeProps = (name) => ({ + onClick: () => handleMark(name), + toggle: true, + active: training.attendance_status === name, + }); + + return ( +
+

{training.student_name}:

+ + + + + + + + + + + + {error &&

Error: something went wrong!

} + +
+ ); +} + +export function InstructorClassAttendance(props) { + const { clazz, token } = props; + const [error, setError] = useState(false); + + return ( +
+
Instructor Panel
+ +
Mark Attendance
+ + {clazz.students.length ? + clazz.students.map((x, i) => +

+ ) + : +

No students yet.

+ } +
+ ); +}; + function InstructorClassEditor(props) { const { input, setInput, error, editing } = props; diff --git a/webclient/src/Training.js b/webclient/src/Training.js index 452cfc0..b16d7b9 100644 --- a/webclient/src/Training.js +++ b/webclient/src/Training.js @@ -27,14 +27,12 @@ export function Training(props) { {user.training.map((x, i) => - - {x.session.course.name} - + {x.session.course_name} {moment(x.session.datetime).format('MMMM Do YYYY')} {x.attendance_status} - {x.session.old_instructor} + {x.session.instructor_name} )}