Skip to content

Commit

Permalink
Merge pull request #2218 from concord-consortium/186939433-display-ex…
Browse files Browse the repository at this point in the history
…emplars

186939433 display exemplars
  • Loading branch information
scytacki authored Mar 25, 2024
2 parents 986565b + 82c4d86 commit 38ce4db
Show file tree
Hide file tree
Showing 27 changed files with 258 additions and 44 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ There are a number of URL parameters that can aid in testing:
The `unit` parameter can be in 3 forms:
- a valid URL starting with `https:` or `http:` will be treated as an absolute URL.
- a string starting with `./` will be treated as a URL relative to the current page in the browser.
- Everything else is treated as a unit code, these codes are first looked up in a map to remap legacy codes. Then the URL of the unit is created by `${curriculumBaseUrl}/branch/${branchName}/${unitCode}/content.json`.
- `curriculumBaseUrl` defaults to `https://models-resources.concord.org/clue-curriculum`.
- Everything else is treated as a unit code, these codes are first looked up in a map to remap legacy codes. Then the URL of the unit is created by `${curriculumSiteUrl}/branch/${branchName}/${unitCode}/content.json`.
- `curriculumSiteUrl` defaults to `https://models-resources.concord.org/clue-curriculum`.
- `branchName` defaults to `main`.
- To find out more about customizing these values look at `app-config-model.ts`.

Expand Down
65 changes: 49 additions & 16 deletions cypress/e2e/functional/teacher_tests/teacher_sort_work_view_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ let resourcesPanel = new ResourcesPanel;
let dashboard = new TeacherDashboard;
let header = new ClueHeader;
const canvas = new Canvas;
const title = "1.1 Unit Toolbar Configuration";
const copyTitle = "Personal Workspace";
const title = "1.1 Unit Toolbar Configuration";
const copyTitle = "Personal Workspace";
const queryParams1 = `${Cypress.config("clueTestqaConfigSubtabsUnitTeacher6")}`;
const queryParams2 = `${Cypress.config("qaConfigSubtabsUnitTeacher1")}`;

Expand Down Expand Up @@ -53,27 +53,41 @@ describe('SortWorkView Tests', () => {

sortWork.getListItemByGroup().click(); // Select 'Group' sort type
cy.wait(1000);

cy.log('verify opening and closing a document from the sort work view');
sortWork.getSortWorkItem().eq(1).click(); // Open the first document in the list
resourcesPanel.getEditableDocumentContent().should('be.visible');
resourcesPanel.getDocumentCloseButton().click();
sortWork.getSortWorkItem().should('be.visible'); // Verify the document is closed
});


it("should open Sort Work tab and test sorting by group", () => {
const students = ["student:1", "student:2", "student:3", "student:4"]
const studentProblemDocs = [`Student 1: ${title}`, `Student 2: ${title}`, `Student 3: ${title}`,`Student 4: ${title}`];
const studentPersonalDocs = [`Student 1: ${copyTitle}`, `Student 2: ${copyTitle}`, `Student 3: ${copyTitle}`,`Student 4: ${copyTitle}`];
const students = ["student:1", "student:2", "student:3", "student:4"];
const studentProblemDocs = [
`Student 1: ${title}`,
`Student 2: ${title}`,
`Student 3: ${title}`,
`Student 4: ${title}`
];
const studentPersonalDocs = [
`Student 1: ${copyTitle}`,
`Student 2: ${copyTitle}`,
`Student 3: ${copyTitle}`,
`Student 4: ${copyTitle}`
];
const exemplarDocs = [
`Ivan Idea: First Exemplar`
];

cy.log("run CLUE for various students creating their problem and personal documents");
students.forEach(student => {
runClueAsStudent(student);
canvas.copyDocument(copyTitle);
canvas.getPersonalDocTitle().find('span').text().should('contain', copyTitle);
})
});

cy.log("run CLUE as teacher and check student problem and personal documents show in Sort Work");
cy.log("run CLUE as teacher and check student problem, personal, and exemplar docs show in Sort Work");
cy.visit(queryParams2);
cy.waitForLoad();
cy.openTopTab('sort-work');
Expand All @@ -85,29 +99,37 @@ describe('SortWorkView Tests', () => {
sortWork.getSortWorkItem().should('contain', doc);
});

cy.log("verify that exemplar document shows in Sort Work");
sortWork.getSortWorkItem().eq(0).should('contain', exemplarDocs[0]);

cy.log("open problem doc and make sure Edit button doesn't show and Close button shows");
sortWork.getSortWorkItem().contains(studentProblemDocs[0]).click();
resourcesPanel.getDocumentEditButton().should("not.exist");
resourcesPanel.getDocumentCloseButton().should("exist").click();

cy.log("open personal doc and make sure Edit button doesn't show and Close button shows");
sortWork.getSortWorkItem().contains(studentPersonalDocs[0]).click();
resourcesPanel.getDocumentEditButton().should("not.exist");
resourcesPanel.getDocumentCloseButton().should("exist").click();

cy.log("open exemplar doc and make sure Edit button doesn't show and Close button shows");
sortWork.getSortWorkItem().contains(exemplarDocs[0]).click();
resourcesPanel.getDocumentEditButton().should("not.exist");
resourcesPanel.getDocumentCloseButton().should("exist").click();

cy.log("check all problem and personal docs show in the correct group");
studentProblemDocs.forEach(doc => {
sortWork.checkDocumentInGroup("Group 5", doc);
})
});
studentPersonalDocs.forEach(doc => {
sortWork.checkDocumentInGroup("Group 5", doc);
})
});

cy.log("run CLUE as a student:1 and leave the group");
runClueAsStudent(students[0]);
header.leaveGroup();

cy.log("check student:1 problem and personal docs show in No Group");
cy.log("check student:1 problem, exemplar, and personal docs show in No Group");
cy.visit(queryParams2);
cy.waitForLoad();
cy.openTopTab('sort-work');
Expand All @@ -120,7 +142,18 @@ describe('SortWorkView Tests', () => {
sortWork.checkDocumentInGroup("Group 5", studentPersonalDocs[1]);
sortWork.checkDocumentNotInGroup("No Group", studentProblemDocs[1]);
sortWork.checkDocumentNotInGroup("No Group", studentPersonalDocs[1]);

sortWork.checkDocumentInGroup("No Group", exemplarDocs[0]);

cy.log("check that problem and exemplar documents can be sorted by name");
sortWork.getSortByMenu().click();
cy.wait(1000);
sortWork.getListItemByName().click();
sortWork.checkSectionHeaderLabelsExist([
"1, Student", "1, Teacher", "2, Student", "3, Student", "4, Student", "Idea, Ivan"
]);
sortWork.checkDocumentInGroup("Idea, Ivan", exemplarDocs[0]);
sortWork.checkDocumentInGroup("1, Student", studentProblemDocs[0]);

cy.log("run CLUE as a student:1 and join group 6");
runClueAsStudent(students[0], 6);

Expand Down Expand Up @@ -148,5 +181,5 @@ describe('SortWorkView Tests', () => {
sortWork.checkDocumentInGroup("No Group", studentProblemDocs[0]);
sortWork.checkDocumentInGroup("No Group", studentPersonalDocs[0]);
sortWork.checkGroupDoesNotExist("Group 6");
})
})
});
});
7 changes: 6 additions & 1 deletion cypress/support/elements/common/SortedWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class SortedWork {
return cy.get('[data-test="list-item-name"]');
}
getListItemByGroup() {
return cy.get('[data-test="list-item-group"]')
return cy.get('[data-test="list-item-group"]');
}
getSortWorkItem() {
return cy.get(".sort-work-view .sorted-sections .list-item .footer .info");
Expand All @@ -20,6 +20,11 @@ class SortedWork {
checkGroupDoesNotExist(group) {
cy.get(".sort-work-view .sorted-sections .section-header-label").should("not.contain", group);
}
checkSectionHeaderLabelsExist(labels){
labels.forEach(label => {
cy.get(".sort-work-view .sorted-sections .section-header-label").should("contain", label);
});
}
}

export default SortedWork;
5 changes: 5 additions & 0 deletions docs/launch-sequence.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ flowchart TB
%% LE.start: Loading curriculum unit
unit(Get unit JSON)
unit --> tiles
unit --> exemplars
unit --> sections
%% LE.end: Loading curriculum uit
Expand All @@ -54,6 +55,10 @@ flowchart TB
sections --> resolveSectionsLoadedPromise([resolve sectionsLoadedPromise])
%% LE.end: Loading curriculum sections
%% LE.start: Loading exemplar documents
exemplars("createAndLoadExemplarDocs")
%% LE.end: Loading exemplar documents
resolveUnitLoadedPromise([resolve unitLoadedPromise])
configStores(Configure some stores)
Expand Down
2 changes: 1 addition & 1 deletion src/clue/curriculum-config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"curriculumBaseUrl": "https://models-resources.concord.org/clue-curriculum",
"curriculumSiteUrl": "https://models-resources.concord.org/clue-curriculum",
"unitCodeMap": {
"bio4community": "bio",
"completely-rational": "cr",
Expand Down
9 changes: 7 additions & 2 deletions src/hooks/use-document-caption.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useFirestoreTeacher } from "./firestore-hooks";
import { useAppConfig, useClassStore, useProblemStore, useUserStore } from "./use-stores";
import { DocumentModelType } from "../models/document/document";
import { isPublishedType, isUnpublishedType } from "../models/document/document-types";
import { ExemplarDocument, isPublishedType, isUnpublishedType } from "../models/document/document-types";
import { getDocumentDisplayTitle } from "../models/document/document-utils";

export function useDocumentCaption(document: DocumentModelType, isStudentWorkspaceDoc?: boolean) {
Expand All @@ -17,7 +17,12 @@ export function useDocumentCaption(document: DocumentModelType, isStudentWorkspa
|| (document.isRemote ? teacher?.name : "")
|| "Unknown User";

const hasNamePrefix = document.isRemote || isPublishedType(type) || isUnpublishedType(type) || isStudentWorkspaceDoc;
const hasNamePrefix = document.isRemote
|| isPublishedType(type)
|| isUnpublishedType(type)
// TODO: ExemplarDocument could be in "isPublishedType" or "isUnpiblishedType" test arrays
|| type === ExemplarDocument
|| isStudentWorkspaceDoc;
const namePrefix = hasNamePrefix ? `${userName}: ` : "";
const dateSuffix = document.isRemote && document.createdAt
? ` (${new Date(document.createdAt).toLocaleDateString()})`
Expand Down
2 changes: 1 addition & 1 deletion src/lib/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const PARTIAL_RAW_OFFERING_INFO = {
activity_url: "https://foo.bar/?problem=3.2"
};

const curriculumConfig = CurriculumConfig.create({curriculumBaseUrl: ""});
const curriculumConfig = CurriculumConfig.create({curriculumSiteUrl: ""});

describe("dev mode", () => {
const originalUrlParams = UrlParams.urlParams;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/portal-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("Portal Offerings", () => {
});

describe("getPortalClassOfferings", () => {
const curriculumConfig = CurriculumConfig.create({curriculumBaseUrl: ""});
const curriculumConfig = CurriculumConfig.create({curriculumSiteUrl: ""});
const mockAppConfig = {
config: { defaultProblemOrdinal: "1.1" }
} as AppConfigModelType;
Expand Down
15 changes: 10 additions & 5 deletions src/models/curriculum/problem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ describe("problem model", () => {
ordinal: 1,
title: "test",
subtitle: "",
sectionsFromSnapshot: []
sectionsFromSnapshot: [],
exemplars: [],
});
expect(problem.fullTitle).toBe("test");
});
Expand Down Expand Up @@ -48,7 +49,8 @@ describe("problem model", () => {
type: "initialChallenge",
}
],
config: {}
config: {},
exemplars: []
});
expect(problem.fullTitle).toBe("test: sub");
});
Expand Down Expand Up @@ -216,7 +218,8 @@ describe("problem model", () => {
config: {
disabledFeatures: ["foo"],
settings: { foo: "bar" }
}
},
exemplars: []
});
});

Expand All @@ -238,7 +241,8 @@ describe("problem model", () => {
config: {
disabledFeatures: ["foo"],
settings: { foo: "bar" }
}
},
exemplars: []
});
});

Expand All @@ -262,7 +266,8 @@ describe("problem model", () => {
config: {
disabledFeatures: ["foo"],
settings: { foo: "bar" }
}
},
exemplars: []
});
});
});
6 changes: 5 additions & 1 deletion src/models/curriculum/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const ModernProblemModel = types
* with all of the other sections in this problem, or this problem's unit
*/
sectionsFromSnapshot: types.frozen<SectionModelSnapshot[]>(),
exemplars: types.array(types.string),
config: types.maybe(types.frozen<Partial<ProblemConfiguration>>())
})
.volatile(self => ({
Expand Down Expand Up @@ -121,7 +122,10 @@ export const ProblemModel = types.snapshotProcessor(ModernProblemModel, {
const sectionsFromSnapshot = sections || [];
if (isLegacySnapshot(sn)) {
const { disabled: disabledFeatures, settings, ...others } = sn;
return { ...others, sectionsFromSnapshot, config: { disabledFeatures, settings } } as ModernProblemSnapshot;
return { ...others,
sectionsFromSnapshot,
config: { disabledFeatures, settings }
} as ModernProblemSnapshot;
}
if (isAmbiguousSnapshot(sn)) {
const { disabled: disabledFeatures, settings, config, ...others } = sn as any;
Expand Down
5 changes: 3 additions & 2 deletions src/models/document/document-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const ProblemDocument = "problem";
export const PersonalDocument = "personal";
export const PlanningDocument = "planning";
export const LearningLogDocument = "learningLog";
export const ExemplarDocument = "exemplar";
export const ProblemPublication = "publication";
export const PersonalPublication = "personalPublication";
export const LearningLogPublication = "learningLogPublication";
Expand Down Expand Up @@ -38,7 +39,7 @@ export function isPublishedType(type: string) {
.indexOf(type) >= 0;
}
export function isSortableType(type: string){
return [ProblemDocument, PersonalDocument, LearningLogDocument].indexOf(type) >= 0;
return [ProblemDocument, PersonalDocument, LearningLogDocument, ExemplarDocument].indexOf(type) >= 0;
}
// This function uses a bit of a hack to determine if a document is curriculum or not:
// curriculum documents have no ids.
Expand All @@ -50,7 +51,7 @@ export function isCurriculumDocument(documentId?: string) {

export const DocumentTypeEnum = types.enumeration("type",
[SectionDocumentDEPRECATED,
ProblemDocument, PersonalDocument, PlanningDocument, LearningLogDocument,
ProblemDocument, PersonalDocument, PlanningDocument, LearningLogDocument, ExemplarDocument,
ProblemPublication, PersonalPublication, LearningLogPublication, SupportPublication]);
export type DocumentType = Instance<typeof DocumentTypeEnum>;
export type ProblemOrPlanningDocumentType = typeof ProblemDocument | typeof PlanningDocument;
Expand Down
3 changes: 3 additions & 0 deletions src/models/stores/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export const ClassModel = types
initials: user.initials,
}));
});
},
addUser(user: any) {
self.users.put(user);
}
};
})
Expand Down
17 changes: 17 additions & 0 deletions src/models/stores/create-exemplar-docs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createExemplarDocId } from "./create-exemplar-docs";

describe("Create Exemplar Docs", () => {
describe("createExemplarDocId", () => {
test("should encode disallowed characters", async () => {
// We need to ensure that we don't create exemplar doc Ids with
// characters that firebase disallows in keys [, ], #, $, ., /
const result = createExemplarDocId("https://www.example.com/?thing=[1,2,3]$FOO#", "");
expect(result).toBe("curriculum:https%3A%2F%2Fwww%2Eexample%2Ecom%2F%3Fthing%3D%5B1%2C2%2C3%5D%24FOO%23");
});

test("should strip curriculum base url", () => {
const result = createExemplarDocId("https://www.example.com/curriculum/thing", "https://www.example.com/curriculum");
expect(result).toBe("curriculum:%2Fthing");
});
});
});
Loading

0 comments on commit 38ce4db

Please sign in to comment.