From c897a26d72f34af7a2ae898b0c7574017ca0540c Mon Sep 17 00:00:00 2001 From: Ashar Fuadi Date: Sun, 6 Oct 2024 21:34:44 +0700 Subject: [PATCH] Explicitly separate problem set problems in a chapter (#667) --- .../problem/ChapterProblemsResponse.java | 1 + .../programming/ChapterProblemWorksheet.java | 2 + .../problem/ChapterProblemResource.java | 14 ++- .../problem/ProblemSetProblemStore.java | 34 ++++++- .../ChapterProblemStatementPage.jsx | 13 +++ .../ChapterProblemStatementPage.scss | 7 +- .../ChapterProblemCard/ChapterProblemCard.jsx | 22 ++++- .../ChapterProblemCard.scss | 6 +- .../ChapterResourcesPage.jsx | 98 +++++++++++++------ .../ChapterResourcesPage.scss | 8 ++ .../ChapterResourcesPage.test.jsx | 1 + 11 files changed, 170 insertions(+), 36 deletions(-) diff --git a/judgels-backends/judgels-server-api/src/main/java/judgels/jerahmeel/api/chapter/problem/ChapterProblemsResponse.java b/judgels-backends/judgels-server-api/src/main/java/judgels/jerahmeel/api/chapter/problem/ChapterProblemsResponse.java index b7d6aebd7..4c9961a30 100644 --- a/judgels-backends/judgels-server-api/src/main/java/judgels/jerahmeel/api/chapter/problem/ChapterProblemsResponse.java +++ b/judgels-backends/judgels-server-api/src/main/java/judgels/jerahmeel/api/chapter/problem/ChapterProblemsResponse.java @@ -12,6 +12,7 @@ public interface ChapterProblemsResponse { List getData(); Map getProblemsMap(); + Map>> getProblemSetProblemPathsMap(); Map getProblemProgressesMap(); class Builder extends ImmutableChapterProblemsResponse.Builder {} diff --git a/judgels-backends/judgels-server-api/src/main/java/judgels/jerahmeel/api/chapter/problem/programming/ChapterProblemWorksheet.java b/judgels-backends/judgels-server-api/src/main/java/judgels/jerahmeel/api/chapter/problem/programming/ChapterProblemWorksheet.java index 33a4f2e15..55d252d46 100644 --- a/judgels-backends/judgels-server-api/src/main/java/judgels/jerahmeel/api/chapter/problem/programming/ChapterProblemWorksheet.java +++ b/judgels-backends/judgels-server-api/src/main/java/judgels/jerahmeel/api/chapter/problem/programming/ChapterProblemWorksheet.java @@ -1,6 +1,7 @@ package judgels.jerahmeel.api.chapter.problem.programming; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; import java.util.Optional; import java.util.Set; import judgels.gabriel.api.SubmissionSource; @@ -18,6 +19,7 @@ public interface ChapterProblemWorksheet extends judgels.jerahmeel.api.chapter.p Set getSkeletons(); Optional getLastSubmission(); Optional getLastSubmissionSource(); + List> getProblemSetProblemPaths(); ProblemProgress getProgress(); Optional getEditorial(); diff --git a/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/chapter/problem/ChapterProblemResource.java b/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/chapter/problem/ChapterProblemResource.java index 35d537218..5371de50f 100644 --- a/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/chapter/problem/ChapterProblemResource.java +++ b/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/chapter/problem/ChapterProblemResource.java @@ -34,6 +34,7 @@ import judgels.jerahmeel.api.problem.ProblemProgress; import judgels.jerahmeel.chapter.ChapterStore; import judgels.jerahmeel.chapter.resource.ChapterResourceStore; +import judgels.jerahmeel.problemset.problem.ProblemSetProblemStore; import judgels.jerahmeel.role.RoleChecker; import judgels.jerahmeel.stats.StatsStore; import judgels.jerahmeel.submission.JerahmeelSubmissionStore; @@ -53,7 +54,8 @@ public class ChapterProblemResource { @Inject protected RoleChecker roleChecker; @Inject protected ChapterStore chapterStore; @Inject protected ChapterResourceStore resourceStore; - @Inject protected ChapterProblemStore problemStore; + @Inject protected ChapterProblemStore chapterProblemStore; + @Inject protected ProblemSetProblemStore problemSetProblemStore; @Inject protected StatsStore statsStore; @Inject @JerahmeelSubmissionStore protected SubmissionStore submissionStore; @Inject protected SubmissionSourceBuilder submissionSourceBuilder; @@ -90,7 +92,7 @@ public void setProblems( .build()) .collect(Collectors.toList()); - problemStore.setProblems(chapterJid, setData); + chapterProblemStore.setProblems(chapterJid, setData); } @GET @@ -103,15 +105,17 @@ public ChapterProblemsResponse getProblems( String actorJid = actorChecker.check(authHeader); checkFound(chapterStore.getChapterByJid(chapterJid)); - List problems = problemStore.getProblems(chapterJid); + List problems = chapterProblemStore.getProblems(chapterJid); var problemJids = Lists.transform(problems, ChapterProblem::getProblemJid); Map problemsMap = sandalphonClient.getProblems(problemJids); + Map>> problemSetProblemPathsMap = problemSetProblemStore.getProblemSetProblemPathsMap(problemJids); Map problemProgressesMap = statsStore.getProblemProgressesMap(actorJid, problemJids); return new ChapterProblemsResponse.Builder() .data(problems) .problemsMap(problemsMap) + .problemSetProblemPathsMap(problemSetProblemPathsMap) .problemProgressesMap(problemProgressesMap) .build(); } @@ -131,7 +135,7 @@ public ChapterProblemWorksheet getProblemWorksheet( String actorJid = actorChecker.check(authHeader); checkFound(chapterStore.getChapterByJid(chapterJid)); - ChapterProblem problem = checkFound(problemStore.getProblemByAlias(chapterJid, problemAlias)); + ChapterProblem problem = checkFound(chapterProblemStore.getProblemByAlias(chapterJid, problemAlias)); String problemJid = problem.getProblemJid(); ProblemInfo problemInfo = sandalphonClient.getProblem(problemJid); @@ -141,6 +145,7 @@ public ChapterProblemWorksheet getProblemWorksheet( List> previousAndNextResourcePaths = resourceStore.getPreviousAndNextResourcePathsForProblem(chapterJid, problemAlias); + List> problemSetProblemPaths = problemSetProblemStore.getProblemSetProblemPaths(problemJid); ProblemProgress progress = statsStore.getProblemProgressesMap(actorJid, Set.of(problemJid)).get(problemJid); Optional editorial = progress.getVerdict().equals(Verdict.ACCEPTED.getCode()) ? sandalphonClient.getProblemEditorial(problemJid, uriInfo.getBaseUri(), language) @@ -169,6 +174,7 @@ public ChapterProblemWorksheet getProblemWorksheet( .skeletons(sandalphonClient.getProgrammingProblemSkeletons(problemJid)) .lastSubmission(lastSubmission) .lastSubmissionSource(lastSubmissionSource) + .problemSetProblemPaths(problemSetProblemPaths) .progress(progress) .editorial(editorial) .build(); diff --git a/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/problemset/problem/ProblemSetProblemStore.java b/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/problemset/problem/ProblemSetProblemStore.java index fc161b592..cc0d5c8c6 100644 --- a/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/problemset/problem/ProblemSetProblemStore.java +++ b/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/problemset/problem/ProblemSetProblemStore.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -15,6 +16,8 @@ import judgels.jerahmeel.api.problemset.problem.ProblemSetProblem; import judgels.jerahmeel.persistence.ProblemContestDao; import judgels.jerahmeel.persistence.ProblemContestModel; +import judgels.jerahmeel.persistence.ProblemSetDao; +import judgels.jerahmeel.persistence.ProblemSetModel; import judgels.jerahmeel.persistence.ProblemSetProblemDao; import judgels.jerahmeel.persistence.ProblemSetProblemModel; import judgels.jerahmeel.persistence.ProblemSetProblemModel_; @@ -22,11 +25,17 @@ import judgels.sandalphon.api.problem.ProblemType; public class ProblemSetProblemStore { + private final ProblemSetDao problemSetDao; private final ProblemSetProblemDao problemDao; private final ProblemContestDao problemContestDao; @Inject - public ProblemSetProblemStore(ProblemSetProblemDao problemDao, ProblemContestDao problemContestDao) { + public ProblemSetProblemStore( + ProblemSetDao problemSetDao, + ProblemSetProblemDao problemDao, + ProblemContestDao problemContestDao) { + + this.problemSetDao = problemSetDao; this.problemDao = problemDao; this.problemContestDao = problemContestDao; } @@ -128,6 +137,29 @@ public ProblemSetProblem upsertProblem( } } + public Map>> getProblemSetProblemPathsMap(Collection problemJids) { + List models = problemDao.selectAllByProblemJids(problemJids); + + List problemSetJids = Lists.transform(models, m -> m.problemSetJid); + Map problemSetModelsMap = problemSetDao.selectByJids(problemSetJids); + + Map>> problemSetProblemPathsMap = new HashMap<>(); + for (ProblemSetProblemModel m : models) { + if (!problemSetModelsMap.containsKey(m.problemSetJid)) { + continue; + } + ProblemSetModel problemSetModel = problemSetModelsMap.get(m.problemSetJid); + List path = List.of(problemSetModel.slug, m.alias); + problemSetProblemPathsMap.putIfAbsent(m.problemJid, new ArrayList<>()); + problemSetProblemPathsMap.get(m.problemJid).add(path); + } + return Map.copyOf(problemSetProblemPathsMap); + } + + public List> getProblemSetProblemPaths(String problemJid) { + return getProblemSetProblemPathsMap(List.of(problemJid)).getOrDefault(problemJid, List.of()); + } + private void upsertProblemContests(String problemJid, List contestJids) { problemContestDao.selectAllByProblemJid(problemJid).forEach(problemContestDao::delete); problemContestDao.flush(); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx index f6e39c542..be6e79697 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx @@ -41,6 +41,18 @@ function ChapterProblemStatementPage({ worksheet }) { ); }; + const renderProblemSetProblemPaths = () => { + const { problemSetProblemPaths } = worksheet; + if (!problemSetProblemPaths) { + return null; + } + return ( + + {problemSetProblemPaths.map(p => p.join('/')).join(', ')} + + ); + }; + const renderStatementHeader = () => { const { defaultLanguage, languages } = worksheet; if (!defaultLanguage || !languages) { @@ -54,6 +66,7 @@ function ChapterProblemStatementPage({ worksheet }) {
{renderLimits()} + {renderProblemSetProblemPaths()}
); }; diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.scss index 39be05426..a52e8f4bb 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.scss +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.scss @@ -20,11 +20,16 @@ margin-left: auto; margin-right: auto; - .statement-header__limits { + &__limits { line-height: 26px !important; margin-bottom: 5px; white-space: nowrap; } + + &__problem-set-problem-paths { + flex: 1; + line-height: 26px !important; + } } .chapter-problem-editorial { diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.jsx index 0c3c77000..b89f83372 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.jsx @@ -9,7 +9,26 @@ import { ProblemType } from '../../../../../../../../modules/api/sandalphon/prob import './ChapterProblemCard.scss'; -export function ChapterProblemCard({ course, chapter, problem, progress, problemName, isFuture }) { +export function ChapterProblemCard({ + course, + chapter, + problem, + problemName, + problemSetProblemPaths, + progress, + isFuture, +}) { + const renderProblemSetProblemPaths = () => { + if (!problemSetProblemPaths) { + return null; + } + return ( +
+ {problemSetProblemPaths.map(p => p.join('/')).join(', ')} +
+ ); + }; + const renderProgress = () => { if (!progress) { return null; @@ -29,6 +48,7 @@ export function ChapterProblemCard({ course, chapter, problem, progress, problem

{problem.alias}. {problemName}

+ {renderProblemSetProblemPaths()} {renderProgress()} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.scss index 09f9eeff1..7009323ea 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.scss +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.scss @@ -21,10 +21,14 @@ height: 21px; } + &__problem-set-problem-paths { + font-weight: 600; + } + &--future { background-color: inherit; - h4, + > *, .chapter-problem-card__heading > .bp5-icon { font-weight: normal !important; color: #bcc0c2; diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.jsx index ddfc160ed..a3244da35 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.jsx @@ -36,9 +36,23 @@ export class ChapterResourcesPage extends Component { this.setState({ response: undefined, }); + const response = await this.props.onGetResources(this.props.chapter.jid); + const [lessonsResponse, problemsResponse] = response; + const { data: lessons, lessonsMap } = lessonsResponse; + const { data: problems, problemsMap, problemSetProblemPathsMap, problemProgressesMap } = problemsResponse; + + const firstUnsolvedProblemIndex = this.getFirstUnsolvedProblemIndex(problems, problemProgressesMap); + this.setState({ response, + lessons, + lessonsMap, + problems, + problemsMap, + problemSetProblemPathsMap, + problemProgressesMap, + firstUnsolvedProblemIndex, }); }; @@ -68,15 +82,11 @@ export class ChapterResourcesPage extends Component { }; renderResources = () => { - const { response } = this.state; + const { response, lessons, problems, problemSetProblemPathsMap } = this.state; if (!response) { return ; } - const [lessonsResponse, problemsResponse] = response; - const { data: lessons, lessonsMap } = lessonsResponse; - const { data: problems, problemsMap, problemProgressesMap } = problemsResponse; - if (lessons.length === 0 && problems.length === 0) { return (

@@ -85,34 +95,66 @@ export class ChapterResourcesPage extends Component { ); } - const firstUnsolvedProblemIndex = this.getFirstUnsolvedProblemIndex(problems, problemProgressesMap); + const chapterProblems = problems.filter(p => !problemSetProblemPathsMap[p.problemJid]); + const problemSetProblems = problems.filter(p => !!problemSetProblemPathsMap[p.problemJid]); + + let chapterResources = null; + if (lessons.length > 0 || chapterProblems.length > 0) { + chapterResources = ( +

+ {lessons.map(this.renderLesson)} + {chapterProblems.map(this.renderProblem)} +
+ ); + } + + let problemSetResources = null; + if (problemSetProblems.length > 0) { + problemSetResources = ( +
+

Practice Problems

+
+ {problemSetProblems.map((p, idx) => this.renderProblem(p, idx + chapterProblems.length))} +
+
+ ); + } return ( - <> - {lessons.map(lesson => { - const props = { - course: this.props.course, - chapter: this.props.chapter, - lesson, - lessonName: getLessonName(lessonsMap[lesson.lessonJid], undefined), - }; - return ; - })} - {problems.map((problem, idx) => { - const props = { - course: this.props.course, - chapter: this.props.chapter, - problem, - problemName: getProblemName(problemsMap[problem.problemJid], undefined), - progress: problemProgressesMap[problem.problemJid], - isFuture: idx > firstUnsolvedProblemIndex, - }; - return ; - })} - +
+ {chapterResources} + {problemSetResources} +
); }; + renderLesson = lesson => { + const { lessonsMap } = this.state; + + const props = { + course: this.props.course, + chapter: this.props.chapter, + lesson, + lessonName: getLessonName(lessonsMap[lesson.lessonJid], undefined), + }; + return ; + }; + + renderProblem = (problem, idx) => { + const { problemsMap, problemSetProblemPathsMap, problemProgressesMap, firstUnsolvedProblemIndex } = this.state; + + const props = { + course: this.props.course, + chapter: this.props.chapter, + problem, + problemName: getProblemName(problemsMap[problem.problemJid], undefined), + problemSetProblemPaths: problemSetProblemPathsMap[problem.problemJid], + progress: problemProgressesMap[problem.problemJid], + isFuture: idx > firstUnsolvedProblemIndex, + }; + return ; + }; + getFirstUnsolvedProblemIndex = (problems, problemProgressesMap) => { for (let i = problems.length - 1; i >= 0; i--) { const progress = problemProgressesMap[problems[i].problemJid]; diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.scss index 08dfe2f9b..093a30330 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.scss +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.scss @@ -3,7 +3,15 @@ .chapter-resources-page { flex: 1 1; max-width: 865px; +} + +.chapter-resources-page__sections { + display: flex; + flex-direction: column; + gap: 30px; +} +.chapter-resources-page__resources { .content-card { margin-bottom: 1px; diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.test.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.test.jsx index d93fb9bdb..fec7c95ba 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.test.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.test.jsx @@ -50,6 +50,7 @@ describe('ChapterResourcesPage', () => { defaultLanguage: 'en', }, }, + problemSetProblemPathsMap: {}, problemProgressesMap: { problemJid1: { verdict: 'AC', score: 100 }, },