Skip to content

Commit

Permalink
Merge pull request #159 from makeopensource/62-edit-course-add/drop-s…
Browse files Browse the repository at this point in the history
…tudents

62 edit course add/drop students
  • Loading branch information
NeemZ16 authored Oct 28, 2024
2 parents a61de37 + 7ae8fad commit c4fae27
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 22 deletions.
3 changes: 2 additions & 1 deletion devU-api/src/entities/user/user.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const Router = express.Router()
* '200':
* description: OK
*/
Router.get('/', isAuthorized('admin'), UserController.get)
Router.get('/', /*isAuthorized('admin'),*/ UserController.get)
// Router.get('/', isAuthorized('admin'), UserController.get)

/**
* @swagger
Expand Down
17 changes: 17 additions & 0 deletions devU-api/src/entities/userCourse/userCourse.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export async function checkEnroll(req: Request, res: Response, next: NextFunctio
export async function _delete(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.courseId)
console.log("DELETE PARAMS: ", req.params)
const currentUser = req.currentUser?.userId
if (!currentUser) return res.status(401).json({ message: 'Unauthorized' })

Expand All @@ -135,6 +136,21 @@ export async function _delete(req: Request, res: Response, next: NextFunction) {
}
}


export async function _deleteUser(req: Request, res: Response, next: NextFunction) {
try {
const courseID = parseInt(req.params.courseId)
console.log("DELETE PARAMS2: ", req.params)
// const currentUser = req.currentUser?.userId
const userID = parseInt(req.params.id)
if (!userID) return res.status(401).json({ message: 'Unauthorized' })

const results = await UserCourseService._delete(courseID, userID)

if (!results.affected) return res.status(404).json(NotFound)

res.status(204).send()

export async function addStudents(req: Request, res: Response, next: NextFunction) {
try {
const userEmails = req.body['users'] as string[]
Expand Down Expand Up @@ -170,6 +186,7 @@ export default {
post,
put,
_delete,
_deleteUser,
checkEnroll,
addStudents,
dropStudents,
Expand Down
24 changes: 22 additions & 2 deletions devU-api/src/entities/userCourse/userCourse.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,9 @@ Router.put('/:id', isAuthorized('userCourseEditAll'), asInt(), validator, UserCo

/**
* @swagger
* /course/:courseId/user-courses/{id}:
* /course/:courseId/user-courses:
* delete:
* summary: Delete a user-course association
* summary: Delete a user-course association for current user
* tags:
* - UserCourses
* responses:
Expand All @@ -243,4 +243,24 @@ Router.put('/:id', isAuthorized('userCourseEditAll'), asInt(), validator, UserCo
*/
Router.delete('/', UserCourseController._delete)
// TODO: eventually add authorization to this. For now, everyone can remove anyone

/**
* @swagger
* /course/:courseId/user-courses/{id}:
* delete:
* summary: Delete a user-course association given a specific user
* tags:
* - UserCourses
* responses:
* '200':
* description: OK
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: integer
*/
Router.delete('/:id', UserCourseController._deleteUser)
// TODO: eventually add authorization to this. For now, everyone can remove anyone
export default Router
185 changes: 166 additions & 19 deletions devU-client/src/components/pages/forms/courses/courseUpdatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import { useHistory, useParams } from 'react-router-dom'
import PageWrapper from 'components/shared/layouts/pageWrapper'

import RequestService from 'services/request.service'
// import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'

import { ExpressValidationError } from 'devu-shared-modules'

import { useActionless } from 'redux/hooks'
import TextField from 'components/shared/inputs/textField'
// import Button from '@mui/material/Button'
import { SET_ALERT } from 'redux/types/active.types'
import {
applyMessageToErrorFields,
Expand All @@ -20,10 +18,25 @@ import {

import formStyles from './coursesFormPage.scss'


type UrlParams = {
courseId: string
}

/*
copied from devU-shared>src>types>user.types.ts and edited from id? to id
to ensure number type instead of number|undefined
*/
type User = {
id: number
externalId: string // School's unique identifier (the thing that links to the schools auth)
email: string
createdAt?: string
updatedAt?: string
preferredName?: string
}


const CourseUpdatePage = ({ }) => {
const [setAlert] = useActionless(SET_ALERT)
const history = useHistory()
Expand All @@ -32,8 +45,10 @@ const CourseUpdatePage = ({ }) => {
number: '',
semester: '',
})
const [startDate, setStartDate] = useState(new Date().toISOString().split("T")[0])
const [endDate, setEndDate] = useState(new Date().toISOString().split("T")[0])
const [startDate, setStartDate] = useState(new Date().toISOString())
const [endDate, setEndDate] = useState(new Date().toISOString())
const [studentEmail, setStudentEmail] = useState("")
const [emails, setEmails] = useState<string[]>([])
const [invalidFields, setInvalidFields] = useState(new Map<string, string>())

const { courseId } = useParams() as UrlParams
Expand All @@ -52,12 +67,20 @@ const CourseUpdatePage = ({ }) => {
});
}
}, []);
const handleChange = (value: String, e: React.ChangeEvent<HTMLInputElement>) => {

const handleChange = (value: string, e: React.ChangeEvent<HTMLInputElement>) => {
const key = e.target.id
const newInvalidFields = removeClassFromField(invalidFields, key)
setInvalidFields(newInvalidFields)
setFormData(prevState => ({ ...prevState, [key]: value }))

// Update form data based on input field
if (key === 'studentEmail') {
setStudentEmail(value)
} else {
setFormData(prevState => ({ ...prevState, [key]: value }))
}
}

const handleStartDateChange = (event: React.ChangeEvent<HTMLInputElement>) => { setStartDate(event.target.value) }
const handleEndDateChange = (event: React.ChangeEvent<HTMLInputElement>) => { setEndDate(event.target.value) }

Expand All @@ -66,8 +89,8 @@ const CourseUpdatePage = ({ }) => {
name: formData.name,
number: formData.number,
semester: formData.semester,
startDate: startDate,
endDate: endDate,
startDate: startDate + "T16:02:41.849Z",
endDate: endDate + "T16:02:41.849Z",
}

RequestService.put(`/api/courses/${courseId}`, finalFormData)
Expand All @@ -86,17 +109,139 @@ const CourseUpdatePage = ({ }) => {
})
}

// update value of file and update parsed values if file uploaded
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const uploadedFile = event.target.files?.[0] || null;
if (uploadedFile) {
handleFileUpload(uploadedFile);
}
};

// set array of parsed emails from csv file
const handleFileUpload = (uploadedFile: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target?.result as string;
const parsedEmails = parseCSV(text);
setEmails(parsedEmails); // Store the parsed emails in state
};
reader.readAsText(uploadedFile); // Read the file
};

// return array of emails
const parseCSV = (text: string): string[] => {
const lines = text.split('\n');
const emails: string[] = [];
const headers = lines[0].toLowerCase().split(',');

// Find the index of the email-related fields
const emailIndex = headers.findIndex(header =>
['email', 'e-mail', 'email address', 'e-mail address'].includes(header.trim())
);

if (emailIndex === -1) {
console.error("Email field not found in CSV file");
setAlert({ autoDelete: false, type: 'error', message: "Email field not found in CSV file" })
return [];
}

// Extract emails
for (let i = 1; i < lines.length; i++) {
const fields = lines[i].split(',');
const email = fields[emailIndex].trim();
if (email) {
emails.push(email);
}
}

console.log("Parsed emails: ", emails);
return emails;
};

const getUserId = async (email: string) => {
// default return value 0 because userIDs start from 1
try {
const res: User[] = await RequestService.get("/api/users/");
const user: User | undefined = res.find((user: User) => user.email === email);

if (user) {
return user.id;
} else {
console.log("User not found");
return 0;
}
} catch (error) {
console.error("Error fetching users:", error);
return 0;
}
}

const addSingleStudent = async (email: string) => {
const id = await getUserId(email)

if (id == 0) {
setAlert({ autoDelete: false, type: 'error', message: "userID not found" })
return
}

const userCourseData = {
userId: id,
courseId: courseId,
role: 'student',
dropped: false
}

try {
await RequestService.post(`/api/course/${courseId}/user-courses`, userCourseData)
setAlert({ autoDelete: true, type: 'success', message: `${email} added to course` })
} catch (error: any) { // Use any if the error type isn't strictly defined
const message = error.message || "An unknown error occurred"
setAlert({ autoDelete: false, type: 'error', message })
}
}

const dropSingleStudent = async (email: string) => {
const userID = await getUserId(email)
if (!userID) { return }

try {
await RequestService.delete(`/api/course/${courseId}/user-courses/${userID}`)
setAlert({ autoDelete: true, type: 'success', message: `${email} dropped from course` })
} catch (error: any) { // Use any if the error type isn't strictly defined
const message = error.message || "An unknown error occurred"
setAlert({ autoDelete: false, type: 'error', message })
}
}

const handleAddStudent = () => {
// TODO: get user id by getting email and calling /users --> search through /users -->
// RequestService.post(`/api/courses/${courseId}/users-courses/${id}:`,
console.log("emails: ", emails);
if (emails.length<1) {
// if no file inputted then addSingleStudent with email
console.log("adding single user")
addSingleStudent(studentEmail)
} else {
// if file inputted then for each email parsed from csv addSingleStudent
console.log("adding multiple users")
emails.forEach(email => {
addSingleStudent(email)
})
}
}

const handleDropStudent = () => {
// get user id by getting email and calling /users --> search through /users -->
// RequestService.delete(`/api/courses/${courseId}/users-courses/${id}:`,
if (emails.length<1) {
// if no file inputted then dropSingleStudent with email
console.log("dropping single user")
dropSingleStudent(studentEmail)
} else {
// if file inputted then for each email parsed from csv dropSingleStudent
console.log("dropping multiple users")
emails.forEach(email => {
dropSingleStudent(email)
})
}
}


return (
<PageWrapper>
<h1>Update Course Form</h1>
Expand All @@ -116,11 +261,11 @@ const CourseUpdatePage = ({ }) => {
<div className={formStyles.datepickerContainer}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', gap: '5px' }}>
<label htmlFor='start-date'>Start Date *</label>
<input type="date" id="start-date" value={startDate} onChange={handleStartDateChange}/>
<input type="date" id="start-date" value={startDate} onChange={handleStartDateChange} />
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', gap: '5px' }}>
<label htmlFor='end-date'>End Date *</label>
<input type="date" id="end-date" value={endDate} onChange={handleEndDateChange}/>
<input type="date" id="end-date" value={endDate} onChange={handleEndDateChange} />
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
Expand All @@ -129,10 +274,12 @@ const CourseUpdatePage = ({ }) => {
</div>
<div className={formStyles.addDropForm}>
<h2>Add/Drop Students</h2>
<TextField id='ubit' label={"UBIT*"} onChange={handleChange}
placeholder='e.g. hartloff' invalidated={!!invalidFields.get("ubit")} helpText={invalidFields.get("ubit")} />
<input type="file" id="addDropFile" />
<div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column', marginTop: 'auto', gap: '1rem'}}>
<TextField id='studentEmail' label={"Email"} onChange={handleChange}
placeholder='e.g. [email protected]' invalidated={!!invalidFields.get("studentEmail")} helpText={invalidFields.get("studentEmail")} />
<label htmlFor="addDropFile">Add multiple students by uploading a CSV file below</label>
{/* csv should be a good standard filetype */}
<input type="file" accept='.csv' id="addDropFile" onChange={handleFileChange} />
<div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column', marginTop: 'auto', gap: '1rem' }}>
<button className='btnPrimary' onClick={handleAddStudent}>Add Student</button>
<button className='btnDelete' onClick={handleDropStudent}>Drop Student</button>
</div>
Expand Down

0 comments on commit c4fae27

Please sign in to comment.