diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index 176f275..feb57ed 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -82,4 +82,4 @@ class Training(models.Model): member_id = models.IntegerField(blank=True, null=True) attendance_status = models.TextField(blank=True, null=True) sign_up_date = models.DateField(default=date.today, blank=True, null=True) - paid_date = models.DateField(default=date.today, blank=True, null=True) + paid_date = models.DateField(blank=True, null=True) diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index e393798..8d9de8e 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -226,19 +226,32 @@ 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()) + class Meta: + model = models.Training + fields = '__all__' + read_only_fields = ['user', 'sign_up_date', 'paid_date', 'member_id'] + +class StudentTrainingSerializer(TrainingSerializer): + attendance_status = serializers.ChoiceField(['waiting for payment', 'withdrawn']) + + + class SessionSerializer(serializers.ModelSerializer): student_count = serializers.SerializerMethodField() course_name = serializers.SerializerMethodField() instructor_name = serializers.SerializerMethodField() datetime = serializers.DateTimeField() - instructor = serializers.PrimaryKeyRelatedField(queryset=models.User.objects.all()) course = serializers.PrimaryKeyRelatedField(queryset=models.Course.objects.all()) + students = TrainingSerializer(many=True, read_only=True) class Meta: model = models.Session fields = '__all__' - read_only_fields = ['old_instructor'] + read_only_fields = ['old_instructor', 'instructor'] def get_student_count(self, obj): - return len(obj.students.all()) + return len([x for x in obj.students.all() if x.attendance_status != 'withdrawn']) def get_course_name(self, obj): return obj.course.name def get_instructor_name(self, obj): @@ -249,9 +262,7 @@ class SessionSerializer(serializers.ModelSerializer): return obj.old_instructor or name class SessionListSerializer(SessionSerializer): - class Meta: - model = models.Session - fields = '__all__' + students = None diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index 061d35c..77ed9da 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import User, Group +from django.shortcuts import get_object_or_404 from django.db.models import Max from rest_framework import viewsets, views, mixins, generics, exceptions from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS @@ -15,11 +16,21 @@ class AllowMetadata(BasePermission): return request.method in ['OPTIONS', 'HEAD'] def is_admin_director(user): - return user.is_staff or user.member.is_director or user.member.is_staff + return bool(user.is_staff or user.member.is_director or user.member.is_staff) -class IsOwnerOrAdmin(BasePermission): +class IsObjOwnerOrAdmin(BasePermission): def has_object_permission(self, request, view, obj): - return request.user and (obj.user == request.user or is_admin_director(request.user)) + return bool(request.user and (obj.user == request.user or is_admin_director(request.user))) + +class IsSessionInstructorOrAdmin(BasePermission): + def has_object_permission(self, request, view, obj): + return bool(request.user and (obj.session.instructor == request.user or is_admin_director(request.user))) + +class ReadOnly(BasePermission): + def has_permission(self, request, view): + return bool(request.method in SAFE_METHODS) + def has_object_permission(self, request, view, obj): + return bool(request.method in SAFE_METHODS) class IsAdminOrReadOnly(BasePermission): def has_permission(self, request, view): @@ -58,7 +69,7 @@ def gen_search_strings(): search_strings[string] = m.id NUM_SEARCH_RESULTS = 10 -class SearchViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin): +class SearchViewSet(Base, Retrieve): permission_classes = [AllowMetadata | IsAuthenticated] def get_serializer_class(self): @@ -113,7 +124,7 @@ class SearchViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin): class MemberViewSet(Base, Retrieve, Update): - permission_classes = [AllowMetadata | IsAuthenticated, IsOwnerOrAdmin] + permission_classes = [AllowMetadata | IsAuthenticated, IsObjOwnerOrAdmin] queryset = models.Member.objects.all() def get_serializer_class(self): @@ -124,7 +135,7 @@ class MemberViewSet(Base, Retrieve, Update): class CardViewSet(Base, Create, Retrieve, Update, Destroy): - permission_classes = [AllowMetadata | IsAuthenticated, IsOwnerOrAdmin, IsAdminOrReadOnly] + permission_classes = [AllowMetadata | IsAuthenticated, IsObjOwnerOrAdmin, IsAdminOrReadOnly] queryset = models.Card.objects.all() serializer_class = serializers.CardSerializer @@ -142,7 +153,6 @@ class CourseViewSet(Base, List, Retrieve, Create, Update): class SessionViewSet(Base, List, Retrieve, Create, Update): permission_classes = [AllowMetadata | IsAuthenticated, IsAdminOrReadOnly | IsInstructorOrReadOnly] - serializer_class = serializers.SessionSerializer def get_queryset(self): if self.action == 'list': @@ -150,6 +160,37 @@ class SessionViewSet(Base, List, Retrieve, Create, Update): else: return models.Session.objects.all() + def get_serializer_class(self): + if self.action == 'list': + return serializers.SessionListSerializer + else: + return serializers.SessionSerializer + + def perform_create(self, serializer): + serializer.save(instructor=self.request.user) + +class TrainingViewSet(Base, Retrieve, Create, Update): + permission_classes = [AllowMetadata | IsAuthenticated, IsObjOwnerOrAdmin | IsSessionInstructorOrAdmin | ReadOnly] + serializer_class = serializers.TrainingSerializer + queryset = models.Training.objects.all() + + def get_serializer_class(self): + user = self.request.user + if is_admin_director(user) or user.member.is_instructor: + return serializers.TrainingSerializer + else: + return serializers.StudentTrainingSerializer + + def perform_create(self, serializer): + session_id = self.request.data['session'] + session = get_object_or_404(models.Session, id=session_id) + training = models.Training.objects.filter(user=self.request.user, session=session) + if training.exists(): + raise exceptions.ValidationError('You have already registered') + if self.request.user == session.instructor: + raise exceptions.ValidationError('You are teaching this session') + serializer.save(user=self.request.user) + class UserView(views.APIView): permission_classes = [AllowMetadata | IsAuthenticated] diff --git a/apiserver/apiserver/urls.py b/apiserver/apiserver/urls.py index 9de0323..b81cf6c 100644 --- a/apiserver/apiserver/urls.py +++ b/apiserver/apiserver/urls.py @@ -7,11 +7,12 @@ from .api import views router = routers.DefaultRouter() #router.register(r'users', views.UserViewSet) +router.register(r'cards', views.CardViewSet, basename='card') +router.register(r'search', views.SearchViewSet, basename='search') router.register(r'members', views.MemberViewSet, basename='members') router.register(r'courses', views.CourseViewSet, basename='course') router.register(r'sessions', views.SessionViewSet, basename='session') -router.register(r'search', views.SearchViewSet, basename='search') -router.register(r'cards', views.CardViewSet, basename='card') +router.register(r'training', views.TrainingViewSet, basename='training') #router.register(r'me', views.FullMemberView, basename='fullmember') #router.register(r'registration', views.RegistrationViewSet, basename='register') diff --git a/webclient/src/InstructorClasses.js b/webclient/src/InstructorClasses.js index d5340e9..36b1ff1 100644 --- a/webclient/src/InstructorClasses.js +++ b/webclient/src/InstructorClasses.js @@ -116,7 +116,7 @@ export function InstructorClassDetail(props) { }; export function InstructorClassList(props) { - const { course, setCourse, token, user } = props; + const { course, setCourse, token } = props; const [open, setOpen] = useState(false); const [input, setInput] = useState({}); const [error, setError] = useState(false); @@ -126,7 +126,7 @@ export function InstructorClassList(props) { const handleSubmit = (e) => { setLoading(true); setSuccess(false); - const data = { ...input, instructor: user.id, course: course.id }; + const data = { ...input, course: course.id }; requester('/sessions/', 'POST', token, data) .then(res => { setSuccess(res.id); diff --git a/webclient/src/utils.js b/webclient/src/utils.js index 1279072..82ed3d8 100644 --- a/webclient/src/utils.js +++ b/webclient/src/utils.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Table } from 'semantic-ui-react'; export const isAdmin = (user) => user.is_staff || user.member.is_director || user.member.is_staff; -export const isInstructor = (user) => isAdmin(user) || user.member.is_staff; +export const isInstructor = (user) => isAdmin(user) || user.member.is_instructor; export const siteUrl = window.location.protocol + '//' + window.location.hostname; export const apiUrl = window.location.protocol + '//api.' + window.location.hostname;