Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/articles/ArticlePreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export default function ArticlePreview({
onUnhideArticle,
isHiddenView = false,
inSavedView = false,
// A student in several classes gets one merged classroom list, so each text
// says which class it belongs to.
showClassNames = false,
// Someone is being shown this card rather than browsing it -- a teacher
// previewing their class's feed. The card must look the same but do nothing
// on the teacher's own account, so the personal controls come off.
Expand Down Expand Up @@ -289,6 +292,15 @@ export default function ArticlePreview({
// tags; elsewhere publish time sits at the tail. `dontShowPublishingTime`
// suppresses publish time only — the saved-time path is the replacement,
// so it isn't gated on the same flag.
// Which class a text came from. Only for a student in more than one class:
// with a single class it is the same answer on every row, and the tab itself
// already says it. Computed here because both layouts below render a strip.
const classTags = showClassNames
? (article.from_classes || []).map((each) => (
<MetaTag key={`class-${each.id}`}>{each.name}</MetaTag>
))
: null;

let savedTag = null;
let publishedTimeSlot = null;
if (inSavedView && article.personal_copy_saved_at) {
Expand Down Expand Up @@ -384,6 +396,7 @@ export default function ArticlePreview({
// layouts so the meta/image markup isn't duplicated across the two.
const metaStrip = (
<MetaStrip>
{classTags}
{article.topics_list &&
article.topics_list.map(([topicTitle]) => <MetaTag key={topicTitle}>{topicTitle}</MetaTag>)}
{article.matched_searches &&
Expand Down Expand Up @@ -625,6 +638,7 @@ export default function ArticlePreview({
Saved · source · time. State badges (Simplified/Saved) get a subtle
accent color; source/time stay muted. All on one row, small. */}
<MetaStrip>
{classTags}
{article.topics_list &&
article.topics_list.map(([topicTitle]) => <MetaTag key={topicTitle}>{topicTitle}</MetaTag>)}
{article.matched_searches &&
Expand Down
10 changes: 10 additions & 0 deletions src/articles/ClassroomArticles.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ export default function ClassroomArticles() {
);
}

// The class tags need to know how many classes this student has, and that
// arrives on a separate request. Waiting is better than rendering the list
// untagged and having the tags pop in when getStudent lands -- the empty
// path above waits for the same reason.
if (student === null) {
return <LoadingAnimation delay={300} />;
}

const inMoreThanOneClass = student.cohorts.length > 1;

return (
<>
<br />
Expand Down
30 changes: 30 additions & 0 deletions src/teacher/myClassesPage/cohortsPage/ClassTexts.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ export default function ClassTexts() {

const { cohort, texts, student_count } = overview;

// A text in another language can be shared with this class, and then no
// student ever sees it: they only get the texts in the language they are
// learning. Seven classes are in this state today. The teacher is the one
// who can fix it, so the count goes to them rather than to the students.
//
// Only meaningful once the class has a language: without one there is
// nothing for a text to be "off", and treating null as a language would
// declare every text unreachable -- both alarming and untrue, since the
// student filter compares the article's language to the student's own and
// never consults the class's.
const offLanguage = (cohort.language ? overview.texts_by_language || [] : []).filter(
(each) => each.code !== cohort.language,
);
const offLanguageCount = offLanguage.reduce((total, each) => total + each.count, 0);

const nothingShared = texts.length === 0;
const emptyForEveryone = nothingShared && cohort.only_classroom_texts;

Expand All @@ -69,6 +84,21 @@ export default function ClassTexts() {
</FullWidthErrorMsg>
)}

{offLanguageCount > 0 && (
<FullWidthInfoMsg>
<span>
{offLanguageCount === 1 ? "One text here is" : `${offLanguageCount} texts here are`} not
in {languageName(cohort.language)} —{" "}
{offLanguage
.map((each) => `${each.count} in ${languageName(each.code)}`)
.join(", ")}
. Your students only ever see the texts in the language they are
learning, so nobody in this class can open{" "}
{offLanguageCount === 1 ? "it" : "them"}.
</span>
</FullWidthInfoMsg>
)}

{nothingShared && !emptyForEveryone && (
<FullWidthInfoMsg>No texts shared with this class yet.</FullWidthInfoMsg>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export default function StudentsActivityOverview() {
//Extracting the cohort data for the page title - for showing "no students" guidance and for deleting students from the cohort.
useEffect(() => {
api.getCohortsInfo((res) => {
const currentCohortArray = res.filter((cohort) => cohort.id === cohortID);
// cohortID comes from the route, so it is a string; the payload's id is
// a number. Compare as strings rather than relying on either shape.
const currentCohortArray = res.filter(
(cohort) => String(cohort.id) === String(cohortID),
);
setCohort(currentCohortArray[0]);
});
//eslint-disable-next-line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export default function StudentReadingInsights() {

useEffect(() => {
api.getCohortsInfo((cohortInfo) => {
let currentCohort = cohortInfo.find((each) => each.id === cohortID);
// Route params are strings; the payload's cohort id is a number.
let currentCohort = cohortInfo.find(
(each) => String(each.id) === String(cohortID),
);
setCohortLang(currentCohort.language_name);
});
api.getStudentInfo(
Expand Down
16 changes: 16 additions & 0 deletions test/teacher/textFilters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,19 @@ describe("sharedClassesOf", () => {
expect(sharedClassesOf({ cohorts: ["nana"] })).toEqual([]);
});
});

describe("cohort ids arriving from two endpoints", () => {
// /teacher_texts sends shared_with with numeric ids; the share dialog builds
// its objects from /cohorts_info. Those used to be stringified, so a text
// shared in this session and the same text after a reload produced two
// different chips for one class.
test("a class shared just now and the same class after a reload are one chip", () => {
const justShared = { id: 3, shared_with: [{ id: 7, name: "nana" }] };
const afterReload = { id: 4, shared_with: [{ id: 7, name: "nana" }] };

const filters = buildClassFilters([justShared, afterReload]);

expect(filters.filter((each) => each.name === "nana")).toHaveLength(1);
expect(filters.find((each) => each.name === "nana").count).toBe(2);
});
});