Closed mays4 closed 1 year ago
EOD , Today worked on adding functions to route and testing in postman Tomorrow continue working in the route and make sure its getting the correct info
EOD today worked on adding functions to the route and try to find the best way to query the required information Tomorrow keep working to solve other issues I have to get the correct data.
Hi Mays!
I know you've been running into some roadblocks trying to write the code that will resolve this issue, and I'd like to break down some of those problems into smaller, more manageable chunks so you have a better idea of what you'll need to complete before your pull request.
A. Write code in the router file to conform to the logic and flow that is set out in the router test file B. Modify the functions that are in the service file (or write new ones) to conform to updated behavior since the opening of the #515 ticket. C. Write tests for those updated service file functions (or tests for the functions assigned to you by the #515 ticket owner). D. Modify the tests in the router test file, if needed, to update them for any changes to parameters or return types of service file functions you've made since beginning this ticket.
Let's take task A first.
Looking at the code in the #515 branch's router test file for reference, here's what I'm seeing as the logic flow for how your router function should behave:
ProgramAssessment
object corresponding with the matching row in the database.CurriculumAssessment
object corresponding with the matching rows in the database.AssessmentSubmission
object corresponding with the matching rows in the database.As you can see from the test file, every single test for your route follows this logic; it's just that the behaviour of the functions and route differ given different inputs or states. However, I see two issues with this approach:
getProgramIdByProgramAssessmentId
) and consolidate that with step 3, thereby giving this logic:ProgramAssessment
object corresponding with the matching row in the database. (Since that would give you the program ID number and the curriculum assessment ID number.)AssessmentSubmission
object corresponding with the matching rows in the database.CurriculumAssessment
object corresponding with the matching rows in the database.Once we have that, let's figure out our function names and signatures.
Looking through our code in competenciesService.ts
and programsService.ts
, I see the following conventions:
Constructing the functions from that standard:
findProgramAssessment()
.getPrincipalProgramRole()
.getAssessmentSubmission()
.getCurriculumAssessment()
.Back-solving from the function descriptions, this leads us to these function headers:
const findProgramAssessment = (programAssessmentId: number): ProgramAssessment | null => {};
const getPrincipalProgramRole = (principalId: number, programId: number): string | null => {};
const getAssessmentSubmission = (assessmentSubmissionId: number, responsesIncluded?: boolean): AssessmentSubmission | null => {};
const getCurriculumAssessment = (curriculumAssessmentId: number, questionsAndAllAnswersIncluded?: boolean, questionsAndCorrectAnswersIncluded?: boolean): CurriculumAssessment | null => {};
However, since these need to be async
functions, our return types have to be declared as Promises:
const findProgramAssessment = async (programAssessmentId: number): Promise<ProgramAssessment | null> => {};
const getPrincipalProgramRole = async (principalId: number, programId: number): Promise<string | null> => {};
const getAssessmentSubmission = async (assessmentSubmissionId: number, responsesIncluded?: boolean): Promise<AssessmentSubmission | null> => {};
const getCurriculumAssessment = async (curriculumAssessmentId: number, questionsAndAllAnswersIncluded?: boolean, questionsAndCorrectAnswersIncluded?: boolean): Promise<CurriculumAssessment | null> => {};
This should lend itself well for us to begin writing code for our router function:
// get the principal row ID number
// get and parse the program ID number and assessment submission number
// error out if we were passed an invalid program ID number or assessment submission number
// get the program assessment
const programAssessment = await findProgramAssessment(programAssessmentId);
// if program assessment is null, that means there's no matching program assessment. send an error back to the user.
// make sure to call next(new NotFoundError()); return; in order to send the right kind of error back.
// get the principal program role
const programRole = await getPrincipalProgramRole(principalId, programAssessment.program_id);
// if the program role is null, that means the user is not enrolled in the program. send an error back to the user.
// get the assessment submission and responses
const assessmentSubmission = await getAssessmentSubmission(assessmentSubmissionId, true);
// if the assessment submission is null, that means there's no matching assessment submission. send an error back to the user.
// also, if the program role is "Participant" and the principal ID of the AssessmentSubmission doesn't match the logged-in principal ID, we should return an error to the user.
// for this route, we always want to return the questions and all answer options in all cases.
const includeQuestionsAndAllAnswers = true;
// if the program role is facilitator, we should always return the correct answers.
// otherwise, return the correct answers only if the submission is graded
const includeQuestionsAndCorrectAnswers = (programRole === "Facilitator" || assessmentSubmission.assessment_submission_state === "Graded");
// get the curriculum assessment
const curriculumAssessment = await getCurriculumAssessment(programAssessment.assessment_id, includeQuestionsAndAllAnswers, includeQuestionsAndCorrectAnswers);
// let's construct our return value
const assessmentWithSubmission: SavedAssessment = {
curriculum_assessment: curriculumAssessment,
program_assessment: programAssessment,
principal_program_role: programRole,
submission: assessmentSubmission
};
// let's return that to the user
res.json(itemEnvelope(assessmentWithSubmission));
From there, you should be able to fill in the rest of the missing code above.
There are two things that I want to highlight with regard to writing the service functions. For convenience sake, let me paste the function declarations here again:
const findProgramAssessment = async (programAssessmentId: number): Promise<ProgramAssessment | null> => {};
const getPrincipalProgramRole = async (principalId: number, programId: number): Promise<string | null> => {};
const getAssessmentSubmission = async (assessmentSubmissionId: number, responsesIncluded?: boolean): Promise<AssessmentSubmission | null> => {};
const getCurriculumAssessment = async (curriculumAssessmentId: number, questionsAndAllAnswersIncluded?: boolean, questionsAndCorrectAnswersIncluded?: boolean): Promise<CurriculumAssessment | null> => {};
The first is that none of these should be returning an array, nor an object that is exactly the columns and values from the tables from which they are querying. When you query a table, it will naturally want to return an array of objects. So you will need to dereference the array, then transform the returned values into the object type you need to return. You can see some of this going on in the listReflections()
function in reflectionsService.ts
, for instance, or in constructProgramActivities()
in programsService.ts
.
The other note is that you should pay close attention in the Knex documentation about which functions need to go in which order when querying the database.
Let's take findProgramAssessment()
as an example.
/**
* Fetches a single program assessment from the database, or null if not found.
*
* @param {number} programAssessmentId - The row ID of the program_assessments table for a given program assessment
* @returns {ProgramAssessment | null} - The ProgramAssessment representation of that program assessment, or null if it was not found.
*/
export const findProgramAssessment = async (programAssessmentId: number): Promise<ProgramAssessment | null> => {
const matchingProgramAssessmentsRows = await db('program_assessments').select('program_id', 'assessment_id', 'available_after', 'due_date').where('id', programAssessmentId);
if (matchingProgramAssessmentsRows.length === 0) {
return null;
}
const [programAssessmentRow] = matchingProgramAssessmentsRows;
const programAssessment: ProgramAssessment = {
id: programAssessmentId,
program_id: programAssessmentRow.program_id,
assessment_id: programAssessmentRow.assessment_id,
available_after: programAssessmentRow.available_after,
due_date: programAssessmentRow.due_date
};
return programAssessment;
};
As you can see, we handle the case when there's no matching row, and we also construct our return object (which in this case is exactly similar to what we get back from the database, but that won't always be the case) and return that back to the calling function.
When testing the service functions, it's important to keep in mind the calling parameters for mockQuery()
:
const mockQuery = (sql: string, bindings?: unknown[], response?: unknown) => {}
The first is a string representing the actual SQL query that executes whenever we have a db()
function call in our service file functions. The second parameter is an array that represents the bindings, most commonly found when we have a where()
in our db()
function call. And the third is the response object from our database query. So if I have the following function call in my service file function:
db('program_assessments').select('program_id', 'assessment_id', 'available_after', 'due_date').where('id', programAssessmentId);
Knex is going to turn that into the following SQL query:
select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ? trx2
Because we have a single question mark, that means we need to bind one variable, so our bindings array should be [programAssessmentId]
. If we didn't have any bindings in our query, our bindings array should just be an empty array: []
.
The third parameter of mockQuery()
is the response object. In nearly every circumstance, this will be an array, since db().select()
will return an array, even if there's only one matching row. So before we can call mockQuery()
, we need to define what we expect to return from that database query:
const exampleProgramAssessmentId = 4;
const matchingProgramAssessmentsRows = [{
program_id: 1,
assessment_id: 3,
available_after: "2023-03-23 01:23:45",
due_date: "2023-03-23 01:23:45"
}];
mockQuery(
'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ? trx2',
[exampleProgramAssessmentId],
matchingProgramAssessmentsRows
);
Then, you should also specify your return object, which should be explicitly declared as the return type from your function:
const exampleProgramAssessment: ProgramAssessment = {
id: exampleProgramAssessmentId,
program_id: matchingProgramAssessmentsRows[0].program_id,
assessment_id: matchingProgramAssessmentsRows[0].assessment_id,
available_after: matchingProgramAssessmentsRows[0].available_after,
due_date: matchingProgramAssessmentsRows[0].due_date
};
expect(await findProgramAssessment(exampleProgramAssessmentId)).toEqual(exampleProgramAssessment);
Describe the Feature / Enhancement
Implementing functionality for the GET /assessments/:assessmentId/submissions/:submissionId route which currently does have a test in router test.
This issue continues the work from issue https://github.com/OpenTree-Education/rhizone-lms/issues/515.
Additional Details and Resources
This route requires URL parameter of submissionId (the ID of a assessment submission) and expects no body in the request. The response should contain the following:
Facilitator
itemEnvelope({ "curriculum_assessment": CurriculumAssessment, (with 'questions' and 'answers' and correct answers) "program_assessment": ProgramAssessment, "assessment_submission": AssessmentSubmission (with 'responses') }); Participant
itemEnvelope({ "curriculum_assessment": CurriculumAssessment, (with 'questions' and 'answers' and correct answers (only if graded)) "program_assessment": ProgramAssessment, "assessment_submission": AssessmentSubmission (with 'responses') });
Correct Project Selected
Labels