Skip to content

Commit

Permalink
avanzamento proposal
Browse files Browse the repository at this point in the history
* save draft funziona
* validazione funziona
  • Loading branch information
paolini committed Dec 27, 2023
1 parent 05aa667 commit db8557d
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 43 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ api/node_modules
api/env.local
db
attachments-db
nextjs/.next
nextjs/node_modules

50 changes: 39 additions & 11 deletions api/controllers/ProposalsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const ModelController = require('./ModelController');
const Proposal = require('../models/Proposal');
const Curriculum = require('../models/Curriculum');
const Degree = require('../models/Degree');
const { ForbiddenError, BadRequestError } = require('../exceptions/ApiException')
const Exam = require('../models/Exam');
const { ForbiddenError, BadRequestError, ValidationError } = require('../exceptions/ApiException')

const fields = {
"state": {
Expand Down Expand Up @@ -94,6 +95,8 @@ const ProposalsController = {
const user = req.user
const body = req.body
const now = new Date()
const issues = {}
let has_issues = false

// l'autenticazione viene controllata nel router
// dunque l'utente ci deve essere
Expand All @@ -114,6 +117,7 @@ const ProposalsController = {
const degree = await Degree.findOne({_id: new ObjectId(curriculum.degree_id)})
if (!degree) throw new BadRequestError("Degree not found")
const obj = {
state,
curriculum_id: new ObjectId(body.curriculum_id),
curriculum_name: curriculum.name,
degree_academic_year: degree.academic_year,
Expand All @@ -136,13 +140,14 @@ const ProposalsController = {

if (body.exams.length !== curriculum.years.length) throw new BadRequestError(`Years count mismatch`)

issues.exams = []
for (let year=0; year < curriculum.years.length; ++year) {
const year_exams = curriculum.years[year].exams
if (body.exams[year].length<year_exams.length) throw new BadRequestError(`Exam count in year ${year+1} mismatch`)
issues.exams[year] = []
for (let j=0; j<year_exams.length; ++j) {
const e = year_exams[j]
const exam_id = body.exams[year][j]
if (typeof(exam_id) === 'object') throw new BadRequestError(`invalid object id ${exam_id} at year ${year+1} position ${j+1}`)
let exam_id = body.exams[year][j]
let exam = null
if (exam_id && typeof(exam_id)==='string') {
try {
Expand All @@ -153,10 +158,10 @@ const ProposalsController = {
}
}
if (e.__t === 'CompulsoryExam') {
if (e.exam_id !== exam_id) {
if (!e.exam_id.equals(exam_id)) {
throw new BadRequestError(`Exam ${e.exam_id} expected in curriculum year ${year+1} position ${j+1}`)
}
obj.exams[year].append({
obj.exams[year].push({
__t: e.__t,
exam_id,
exam_name: exam.name,
Expand All @@ -167,13 +172,15 @@ const ProposalsController = {
} else if (e.__t === 'CompulsoryGroup' || e.__t === 'FreeChoiceGroup') {
if (exam_id === null) {
if (e.__t === 'CompulsoryGroup' && state !== 'draft' ) {
throw new BadRequestError(`Exam not choosen in compulsory group`)
issues.exams[year][j] = `L'esame di un gruppo obbligatorio deve essere scelto`
has_issues = true
}
} if (exam_id !== null) {
if (!degree.groups[e.group]?.contains(exam_id)) {
obj.exams[year].push(null)
} else {
if (!degree.groups.get(e.group)?.includes(exam_id)) {
throw new BadRequestError(`Exam not in group ${e.group} year ${year+1} position ${j+1}`)
}
obj.exams[year].append({
obj.exams[year].push({
__t: e.__t,
group: e.group,
exam_id,
Expand All @@ -183,6 +190,23 @@ const ProposalsController = {
})
credits += exam.credits
}
} else if (e.__t === 'FreeChoiceExam') {
if (exam_id === null) {
if (state !== 'draft') {
issues.exams[year][j] = `L'esame a scelta libera deve essere scelto`
has_issues = true
}
obj.exams[year].push(null)
} else {
obj.exams[year].push({
__t: e.__t,
exam_id,
exam_name: exam.name,
exam_code: exam.code,
exam_credits: exam.credits,
})
credits += exam.credits
}
} else {
assert(false,`unexpected type ${e.__t} in curriculum`)
}
Expand Down Expand Up @@ -221,6 +245,10 @@ const ProposalsController = {
}
}

if (has_issues) {
throw new ValidationError(issues,'Errore di validazione')
}

if (state === 'submitted') {
// ci sono abbastanza crediti?
const curriculum_credits = curriculum.years.reduce((acc, y) => acc + y.credits, 0)
Expand Down Expand Up @@ -257,8 +285,8 @@ const ProposalsController = {
}
}
}

const proposal = new Proposal(body)
console.log(`creating proposal with body ${JSON.stringify(obj)}`)
const proposal = new Proposal(obj)
return await proposal.save()
},

Expand Down
1 change: 1 addition & 0 deletions api/models/ProposalSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ ProposalSchema = new mongoose.Schema({
state: {
type: String,
enum: ['draft', 'submitted', 'approved', 'rejected'],
required: true,
},
date_modified: {
type: Date,
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/modules/engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export function useIndex<T>(path:string, query={}) {
})
}

export function usePost<T>(path: string) {
export function usePost<T>(path: string, onError?: (err: any) => void) {
// funziona anche per Multipart Post
const queryClient = useQueryClient()
return useMutation({
Expand All @@ -201,6 +201,7 @@ export function usePost<T>(path: string) {
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: [path] })
},
onError,
})
}

Expand Down Expand Up @@ -424,8 +425,8 @@ export function useGetProposal(id:string|undefined) {
return useGet<ProposalGet>('proposals/', id)
}

export function usePostProposal() {
return usePost<ProposalPost>('proposals/')
export function usePostProposal(onError?: (err: any) => void) {
return usePost<ProposalPost>('proposals/', onError)
}

export function useIndexProposal(query={}) {
Expand Down
80 changes: 53 additions & 27 deletions frontend/src/pages/ProposalPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Dispatch, SetStateAction, useState } from 'react'
import React, { Dispatch, SetStateAction, useState, useEffect } from 'react'
import { useParams } from "react-router-dom"
import {Button, ButtonGroup} from 'react-bootstrap'
import assert from 'assert'
Expand All @@ -20,14 +20,16 @@ import {FlashCard} from '../components/Flash'

export function EditProposalPage() {
const { id } = useParams()

const proposalQuery = useGetProposal(id || '')
const curriculumQuery = useGetCurriculum(proposalQuery.data?.curriculum_id || '')

if (proposalQuery.isError) return <LoadingMessage>errore piano di studi...</LoadingMessage>
if (!proposalQuery.data) return <LoadingMessage>caricamento piano di studi...</LoadingMessage>

if (curriculumQuery.isError) return <LoadingMessage>errore curriculum...</LoadingMessage>
if (!curriculumQuery.data) return <LoadingMessage>caricamento curriculum...</LoadingMessage>


return <ProposalForm proposal={proposalQuery.data}/>
return <ProposalForm proposal={proposalQuery.data} curriculum={curriculumQuery.data}/>
}

export default function ProposalPage() {
Expand Down Expand Up @@ -132,27 +134,27 @@ export default function ProposalPage() {
}

function ExamRow({ exam }) {
const query = useGetExam(exam.exam_id || null)
const query = useGetExam(exam ? exam.exam_id : null)
if (query.isLoading) return <tr><td>loading...</td></tr>
if (query.isError) return <tr><td>error...</td></tr>
const real_exam: any = query.data
return <tr>
<td>{exam.exam_code}</td>
<td>{exam.exam_name}
<td>{exam?.exam_code || '---'}</td>
<td>{exam?.exam_name || '---'}
{real_exam && real_exam.tags.map(tag =>
<div key={tag} className="badge ml-1 badge-secondary badge-sm">
{tag}
</div>)}
</td>
<td>{exam.exam_sector}</td>
<td>{exam.exam_credits}</td>
<td>{{
<td>{exam?.exam_sector || '---'}</td>
<td>{exam?.exam_credits || '---'}</td>
<td>{exam ? {
'CompulsoryExam': 'Obbligatorio',
'CompulsoryGroup': exam.group,
'FreeChoiceGroup': 'A scelta libera (G)',
'FreeChoiceExam': 'A scelta libera',
'ExternalExam': 'Esame Esterno',
}[exam.__t]}
}[exam.__t] : '---'}
</td>
</tr>
}
Expand All @@ -172,7 +174,7 @@ export default function ProposalPage() {
</tr>
</thead>
<tbody>
{ exams .map(e => <ExamRow key={e._id} exam={e}/>) }
{ exams.map((e,i) => <ExamRow key={`exam-${number}-${i}`} exam={e}/>) }
</tbody>
</table>
</Card>
Expand Down Expand Up @@ -209,16 +211,20 @@ export default function ProposalPage() {
}
}

function ProposalForm({ proposal }:{
proposal: ProposalGet
function ProposalForm({ proposal, curriculum }:{
proposal: ProposalGet,
curriculum: CurriculumGet,
}) {
// curriculum serve solo per conoscere il degreeId

const engine = useEngine()

const [degreeId, setDegreeId] = useState(proposal.degree_id)
const [issues, setIssues] = useState<any>({})
const [degreeId, setDegreeId] = useState<string|null>(curriculum?.degree_id || null)
const [curriculumId, setCurriculumId] = useState<string|null>(proposal.curriculum_id)

const degreesQuery = useIndexDegree()
const curriculaQuery = useIndexCurriculum(degreeId ? { degree_id: degreeId } : undefined )
const curriculaQuery = useIndexCurriculum(degreeId ? { degree_id: degreeId } : undefined)
const examsQuery = useIndexExam()

if (degreesQuery.isError) return <LoadingMessage>errore corsi di laurea...</LoadingMessage>
Expand Down Expand Up @@ -280,7 +286,10 @@ function ProposalForm({ proposal }:{
</div>
</Card>
{ degreeId && curriculumId &&
<ProposalFormYears proposal={proposal} curriculum={curricula[curriculumId]} degree={degrees[degreeId]} allExams={allExams}/>
<ProposalFormYears proposal={proposal} curriculum={curricula[curriculumId]}
degree={degrees[degreeId]} allExams={allExams}
issues={issues} setIssues={setIssues}
/>
}
</>

Expand All @@ -302,11 +311,13 @@ function ProposalForm({ proposal }:{
}
}

function ProposalFormYears({ proposal, curriculum, allExams, degree }:{
function ProposalFormYears({ proposal, curriculum, allExams, degree, issues, setIssues }:{
proposal: ProposalGet,
curriculum: CurriculumGet,
degree: DegreeGet,
allExams: {[key: string]: ExamGet},
issues: any,
setIssues: Dispatch<SetStateAction<any>>,
}) {
const groups = Object.fromEntries(
Object.entries(degree.groups).map(([group_id, group_exams]) => {
Expand All @@ -319,8 +330,12 @@ function ProposalFormYears({ proposal, curriculum, allExams, degree }:{
// console.log(JSON.stringify({chosenExams,groups}))

return <>
{curriculum.years.map((yearExams, number) => <YearCard key={number} year={yearExams} number={number} allExams={allExams} chosenExams={chosenExams} setChosenExams={setChosenExams} groups={groups}/>)}
<Submit proposal={proposal} curriculum={curriculum} chosenExams={chosenExams}/>
{curriculum.years.map((yearExams, number) =>
<YearCard key={number} year={yearExams} number={number} allExams={allExams}
chosenExams={chosenExams} setChosenExams={setChosenExams} groups={groups}
issues={issues?.exams ? issues.exams[number] : null}
/>)}
<Submit proposal={proposal} curriculum={curriculum} chosenExams={chosenExams} setIssues={setIssues}/>
</>

function initExams(curriculum: CurriculumGet): ProposalExamPost[][] {
Expand Down Expand Up @@ -390,13 +405,14 @@ function ProposalFormYears({ proposal, curriculum, allExams, degree }:{
}
}

function YearCard({ year, number, allExams, chosenExams, setChosenExams, groups }:{
function YearCard({ year, number, allExams, chosenExams, setChosenExams, groups, issues }:{
year: CurriculumGet['years'][0],
number: number,
allExams: {[key: string]: ExamGet},
chosenExams: ProposalExamPost[][],
setChosenExams: Dispatch<SetStateAction<ProposalExamPost[][]>>,
groups: {[key: string]: ExamGet[]},
issues: any,
}) {
const yearName = ["Primo", "Secondo", "Terzo"][number] || `#${number}`

Expand All @@ -419,7 +435,11 @@ function YearCard({ year, number, allExams, chosenExams, setChosenExams, groups
</div>

return <Card customHeader={customHeader} >
{year.exams.map((exam, examNumber) => <ExamSelect key={examNumber} exam={exam} allExams={allExams} groups={groups} chosenExam={chosenExams[number][examNumber]} setExam={examSetter(number, examNumber)}/>)}
{year.exams.map((exam, examNumber) =>
<ExamSelect key={examNumber} exam={exam} allExams={allExams} groups={groups}
chosenExam={chosenExams[number][examNumber]} setExam={examSetter(number, examNumber)}
issues={issues?.[examNumber]}
/>)}
<div className='d-flex flex-row-reverse'>
<button className='btn btn-primary'>Aggiungi esame esterno</button>
<button className='btn btn-primary mr-2'>Aggiungi esame a scelta libera</button>
Expand All @@ -435,12 +455,13 @@ function YearCard({ year, number, allExams, chosenExams, setChosenExams, groups
}
}

function ExamSelect({ exam, allExams, groups, chosenExam, setExam }:{
function ExamSelect({ exam, allExams, groups, chosenExam, setExam, issues }:{
exam: CurriculumExamGet,
allExams: {[key: string]: ExamGet},
groups: {[key: string]: ExamGet[]},
chosenExam: ProposalExamPost,
setExam: (e:string) => void,
issues: any,
}) {
if (exam.__t === "CompulsoryExam") {
const compulsoryExam = allExams[exam.exam_id]
Expand All @@ -459,8 +480,9 @@ function ExamSelect({ exam, allExams, groups, chosenExam, setExam }:{
} else if (exam.__t === "CompulsoryGroup" || exam.__t === "FreeChoiceGroup") {
const options = groups[exam.group]
const exam_id = typeof(chosenExam) === 'string' ? chosenExam : ''
const style=issues?{background:"yellow",padding:"1ex"}:{}

return <li className='form-group exam-input'>
return <li className='form-group exam-input' style={style}>
<div className='row'>
<div className='col-9'>
<select className='form-control'
Expand All @@ -472,6 +494,7 @@ function ExamSelect({ exam, allExams, groups, chosenExam, setExam }:{
<option disabled value="">Un esame a scelta nel gruppo {exam.group}</option>
{options && options.map(opt => <option key={opt._id} value={opt._id}>{opt.name}</option>)}
</select>
{issues && <div>{issues}</div>}
</div>
<div className="col-3">
<input className='form-control col' readOnly value={chosenExam ? allExams[exam_id].credits : ""}/>
Expand Down Expand Up @@ -510,19 +533,22 @@ function FreeChoiceExamSelect({allExams, chosenExam, setExam}:{
</li>
}

function Submit({proposal, curriculum, chosenExams}:{
function Submit({proposal, curriculum, chosenExams, setIssues}:{
proposal: ProposalGet,
curriculum: CurriculumGet,
chosenExams: ProposalExamPost[][],
setIssues: Dispatch<SetStateAction<any>>,
}) {
const poster = usePostProposal()
const poster = usePostProposal((err:any) => {
setIssues(err.response.data.issues)
})

if (poster.isLoading) return <LoadingMessage />

return <>
{poster.isError && <FlashCard
className="danger"
message={`${poster.error} [${JSON.stringify(poster.data)}]`}
message={`${poster.error}`}
onClick={poster.reset}
/>}
<ButtonGroup>
Expand Down
Loading

0 comments on commit db8557d

Please sign in to comment.