diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index 1781c61..176f275 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -73,6 +73,7 @@ class Session(models.Model): old_instructor = models.TextField(blank=True, null=True) datetime = models.DateTimeField(blank=True, null=True) cost = models.DecimalField(max_digits=5, decimal_places=2) + max_students = models.IntegerField(blank=True, null=True) class Training(models.Model): user = models.ForeignKey(User, related_name='training', blank=True, null=True, on_delete=models.SET_NULL) diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index 2c50dca..e393798 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -236,6 +236,7 @@ class SessionSerializer(serializers.ModelSerializer): class Meta: model = models.Session fields = '__all__' + read_only_fields = ['old_instructor'] def get_student_count(self, obj): return len(obj.students.all()) def get_course_name(self, obj): diff --git a/webclient/package.json b/webclient/package.json index 9311f2f..34c315d 100644 --- a/webclient/package.json +++ b/webclient/package.json @@ -8,6 +8,7 @@ "@testing-library/user-event": "^7.1.2", "moment": "^2.24.0", "react": "^16.12.0", + "react-datetime": "^2.16.3", "react-dom": "^16.12.0", "react-quill": "^1.3.3", "react-router-dom": "^5.1.2", diff --git a/webclient/src/App.js b/webclient/src/App.js index 59f8c4f..4894f01 100644 --- a/webclient/src/App.js +++ b/webclient/src/App.js @@ -147,14 +147,14 @@ function App() { - + - + diff --git a/webclient/src/Classes.js b/webclient/src/Classes.js index bed8047..38795bb 100644 --- a/webclient/src/Classes.js +++ b/webclient/src/Classes.js @@ -3,8 +3,9 @@ import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-r import './light.css'; import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; import moment from 'moment'; -import { BasicTable, requester } from './utils.js'; +import { isInstructor, BasicTable, requester } from './utils.js'; import { NotFound, PleaseLogin } from './Misc.js'; +import { InstructorClassDetail } from './InstructorClasses.js'; function ClassTable(props) { const { classes } = props; @@ -86,7 +87,7 @@ export function Classes(props) { export function ClassDetail(props) { const [clazz, setClass] = useState(false); const [error, setError] = useState(false); - const { token } = props; + const { token, user } = props; const { id } = useParams(); useEffect(() => { @@ -107,6 +108,10 @@ export function ClassDetail(props) {
Class Details
+ {isInstructor(user) && + + } + diff --git a/webclient/src/Courses.js b/webclient/src/Courses.js index 08d28f5..86f4842 100644 --- a/webclient/src/Courses.js +++ b/webclient/src/Courses.js @@ -5,7 +5,8 @@ import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Me import moment from 'moment'; import { isInstructor, requester } from './utils.js'; import { NotFound, PleaseLogin } from './Misc.js'; -import { InstructorCourseList, InstructorCourseDetail } from './Instructor.js'; +import { InstructorCourseList, InstructorCourseDetail } from './InstructorCourses.js'; +import { InstructorClassList } from './InstructorClasses.js'; export function Courses(props) { const [courses, setCourses] = useState(false); @@ -25,7 +26,7 @@ export function Courses(props) {
Courses
- {isInstructor && + {isInstructor(user) && } @@ -62,7 +63,7 @@ export function Courses(props) { export function CourseDetail(props) { const [course, setCourse] = useState(false); const [error, setError] = useState(false); - const { token } = props; + const { token, user } = props; const { id } = useParams(); useEffect(() => { @@ -83,7 +84,7 @@ export function CourseDetail(props) {
{course.name}
- {isInstructor && + {isInstructor(user) && } @@ -97,6 +98,11 @@ export function CourseDetail(props) { }
Classes
+ + {isInstructor(user) && + + } + diff --git a/webclient/src/InstructorClasses.js b/webclient/src/InstructorClasses.js new file mode 100644 index 0000000..9f5d10d --- /dev/null +++ b/webclient/src/InstructorClasses.js @@ -0,0 +1,170 @@ +import React, { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Switch, Route, Link, useParams, useHistory } from 'react-router-dom'; +import * as Datetime from 'react-datetime'; +import 'react-datetime/css/react-datetime.css'; +import moment from 'moment'; +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 InstructorClassEditor(props) { + const { input, setInput, error, editing } = props; + + const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value }); + const handleUpload = (e, v) => setInput({ ...input, [v.name]: e.target.files[0] }); + const handleChange = (e) => handleValues(e, e.currentTarget); + const handleCheck = (e, v) => setInput({ ...input, [v.name]: v.checked }); + const handleDatetime = (v) => setInput({ ...input, datetime: v.utc().format() }); + + const makeProps = (name) => ({ + name: name, + onChange: handleChange, + value: input[name] || '', + error: error[name], + }); + + return ( +
+ + + + {error.datetime && + + } + + + + + + + {editing && + + + } + +
+ ); +} + +export function InstructorClassDetail(props) { + const { clazz, setClass, token } = props; + const [open, setOpen] = useState(false); + const [input, setInput] = useState(clazz); + const [error, setError] = useState(false); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const { id } = useParams(); + + const handleSubmit = (e) => { + setLoading(true); + setSuccess(false); + requester('/sessions/'+id+'/', 'PUT', token, input) + .then(res => { + setSuccess(true); + setLoading(false); + setError(false); + setOpen(false); + setClass(res); + }) + .catch(err => { + setLoading(false); + console.log(err); + setError(err.data); + }); + }; + + return ( +
+
Instructor Panel
+ + {!open && success &&

Saved!

} + + {open ? +
+
Edit Class
+ + + + + Submit + + + : + + } +
+ ); +}; + +export function InstructorClassList(props) { + const { course, setCourse, token, user } = props; + const [open, setOpen] = useState(false); + const [input, setInput] = useState({}); + const [error, setError] = useState(false); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + + const handleSubmit = (e) => { + setLoading(true); + setSuccess(false); + const data = { ...input, instructor: user.id, course: course.id }; + requester('/sessions/', 'POST', token, data) + .then(res => { + setSuccess(res.id); + setInput({}); + setLoading(false); + setError(false); + setOpen(false); + setCourse({ ...course, sessions: [ res, ...course.sessions ] }); + }) + .catch(err => { + setLoading(false); + console.log(err); + setError(err.data); + }); + }; + + return ( +
+
Instructor Panel
+ + {!open && success &&

Added! View the class.

} + + {open ? +
+
Add a Class
+ + + + + Submit + + + : + + } +
+ ); +}; + diff --git a/webclient/src/Instructor.js b/webclient/src/InstructorCourses.js similarity index 95% rename from webclient/src/Instructor.js rename to webclient/src/InstructorCourses.js index bc4ec0d..9697788 100644 --- a/webclient/src/Instructor.js +++ b/webclient/src/InstructorCourses.js @@ -44,7 +44,7 @@ function InstructorCourseEditor(props) { /> - + { setSuccess(true); setLoading(false); setError(false); setOpen(false); - props.setCourse({...course, ...res}); + setCourse({...course, ...res}); }) .catch(err => { setLoading(false); @@ -129,14 +129,14 @@ export function InstructorCourseList(props) { setLoading(true); setSuccess(false); const data = { ...input, is_old: false }; - requester('/courses/', 'POST', props.token, data) + requester('/courses/', 'POST', token, data) .then(res => { setSuccess(res.id); setInput({}); setLoading(false); setError(false); setOpen(false); - props.setCourses([ ...courses, res ]); + setCourses([ ...courses, res ]); }) .catch(err => { setLoading(false); @@ -169,4 +169,3 @@ export function InstructorCourseList(props) { ); }; - diff --git a/webclient/src/light.css b/webclient/src/light.css index 7f11aba..0423015 100644 --- a/webclient/src/light.css +++ b/webclient/src/light.css @@ -63,7 +63,7 @@ body { padding-bottom: 24rem; } -.course-editor { +.course-editor, .class-editor { margin-bottom: 1rem; } @@ -87,6 +87,11 @@ body { margin-bottom: 1rem !important; } +.rdtTimeToggle { + font-size: 2rem; + padding-top: 1rem; +} + .footer { margin-top: -20rem; diff --git a/webclient/yarn.lock b/webclient/yarn.lock index 3f76ea0..a91637e 100644 --- a/webclient/yarn.lock +++ b/webclient/yarn.lock @@ -2989,7 +2989,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-class@^15.6.0: +create-react-class@^15.5.2, create-react-class@^15.6.0: version "15.6.3" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg== @@ -7000,6 +7000,11 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I= + object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -8337,7 +8342,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" -prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -8534,6 +8539,16 @@ react-app-polyfill@^1.0.5: regenerator-runtime "^0.13.3" whatwg-fetch "^3.0.0" +react-datetime@^2.16.3: + version "2.16.3" + resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.16.3.tgz#7f9ac7d4014a939c11c761d0c22d1fb506cb505e" + integrity sha512-amWfb5iGEiyqjLmqCLlPpu2oN415jK8wX1qoTq7qn6EYiU7qQgbNHglww014PT4O/3G5eo/3kbJu/M/IxxTyGw== + dependencies: + create-react-class "^15.5.2" + object-assign "^3.0.0" + prop-types "^15.5.7" + react-onclickoutside "^6.5.0" + react-dev-utils@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.0.0.tgz#bd2d16426c7e4cbfed1b46fb9e2ac98ec06fcdfa" @@ -8589,6 +8604,11 @@ react-is@^16.6.0, react-is@^16.6.3, react-is@^16.7.0, react-is@^16.8.1, react-is resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== +react-onclickoutside@^6.5.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz#a54bc317ae8cf6131a5d78acea55a11067f37a1f" + integrity sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A== + react-popper@^1.3.4: version "1.3.7" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"