@things-factory/learning 10.1.260 → 10.1.261
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/components/learning-material-form.d.ts +41 -0
- package/dist-client/components/learning-material-form.js +202 -0
- package/dist-client/components/learning-material-form.js.map +1 -0
- package/dist-client/components/learning-unit-form.d.ts +3 -0
- package/dist-client/components/learning-unit-form.js +10 -2
- package/dist-client/components/learning-unit-form.js.map +1 -1
- package/dist-client/pages/learning-course-editor.d.ts +1 -0
- package/dist-client/pages/learning-course-editor.js +19 -6
- package/dist-client/pages/learning-course-editor.js.map +1 -1
- package/dist-client/pages/learning-course-list.d.ts +1 -1
- package/dist-client/pages/learning-course-list.js +6 -2
- package/dist-client/pages/learning-course-list.js.map +1 -1
- package/dist-client/pages/learning-enrollment-status.d.ts +28 -16
- package/dist-client/pages/learning-enrollment-status.js +95 -112
- package/dist-client/pages/learning-enrollment-status.js.map +1 -1
- package/dist-client/pages/learning-view.d.ts +11 -0
- package/dist-client/pages/learning-view.js +94 -19
- package/dist-client/pages/learning-view.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/learning-api.d.ts +48 -3
- package/dist-client/utils/learning-api.js +51 -2
- package/dist-client/utils/learning-api.js.map +1 -1
- package/dist-client/utils/material-source.d.ts +12 -0
- package/dist-client/utils/material-source.js +77 -0
- package/dist-client/utils/material-source.js.map +1 -0
- package/dist-server/ai-reads.js +1 -1
- package/dist-server/ai-reads.js.map +1 -1
- package/dist-server/service/course/course-mutation.d.ts +1 -1
- package/dist-server/service/course/course-mutation.js +7 -6
- package/dist-server/service/course/course-mutation.js.map +1 -1
- package/dist-server/service/course/course-query.d.ts +1 -0
- package/dist-server/service/course/course-query.js +14 -3
- package/dist-server/service/course/course-query.js.map +1 -1
- package/dist-server/service/course/course-rules.d.ts +3 -1
- package/dist-server/service/course/course-rules.js +5 -1
- package/dist-server/service/course/course-rules.js.map +1 -1
- package/dist-server/service/course/course-service.d.ts +5 -4
- package/dist-server/service/course/course-service.js +10 -14
- package/dist-server/service/course/course-service.js.map +1 -1
- package/dist-server/service/course/course-type.d.ts +1 -0
- package/dist-server/service/course/course-type.js +4 -0
- package/dist-server/service/course/course-type.js.map +1 -1
- package/dist-server/service/course/course-unit.d.ts +3 -2
- package/dist-server/service/course/course-unit.js +7 -2
- package/dist-server/service/course/course-unit.js.map +1 -1
- package/dist-server/service/course/index.d.ts +2 -1
- package/dist-server/service/course/index.js +2 -1
- package/dist-server/service/course/index.js.map +1 -1
- package/dist-server/service/course/material-audio.d.ts +1 -0
- package/dist-server/service/course/material-audio.js +43 -0
- package/dist-server/service/course/material-audio.js.map +1 -0
- package/dist-server/service/course/material-draft-mutation.d.ts +13 -0
- package/dist-server/service/course/material-draft-mutation.js +55 -0
- package/dist-server/service/course/material-draft-mutation.js.map +1 -0
- package/dist-server/service/course/material-draft-rules.d.ts +38 -0
- package/dist-server/service/course/material-draft-rules.js +83 -0
- package/dist-server/service/course/material-draft-rules.js.map +1 -0
- package/dist-server/service/course/material-draft-service.d.ts +15 -0
- package/dist-server/service/course/material-draft-service.js +162 -0
- package/dist-server/service/course/material-draft-service.js.map +1 -0
- package/dist-server/service/enrollment/enrollment-query.d.ts +5 -4
- package/dist-server/service/enrollment/enrollment-query.js +26 -35
- package/dist-server/service/enrollment/enrollment-query.js.map +1 -1
- package/dist-server/service/enrollment/enrollment-service.js +13 -3
- package/dist-server/service/enrollment/enrollment-service.js.map +1 -1
- package/dist-server/service/enrollment/enrollment-status.d.ts +31 -18
- package/dist-server/service/enrollment/enrollment-status.js +101 -42
- package/dist-server/service/enrollment/enrollment-status.js.map +1 -1
- package/dist-server/service/enrollment/enrollment-type.d.ts +1 -14
- package/dist-server/service/enrollment/enrollment-type.js +6 -54
- package/dist-server/service/enrollment/enrollment-type.js.map +1 -1
- package/dist-server/service/enrollment/enrollment.d.ts +1 -0
- package/dist-server/service/enrollment/enrollment.js +5 -0
- package/dist-server/service/enrollment/enrollment.js.map +1 -1
- package/dist-server/service/enrollment/learning-ai.d.ts +9 -2
- package/dist-server/service/enrollment/learning-ai.js +25 -28
- package/dist-server/service/enrollment/learning-ai.js.map +1 -1
- package/dist-server/service/enrollment/learning-documents.js +3 -4
- package/dist-server/service/enrollment/learning-documents.js.map +1 -1
- package/dist-server/service/enrollment/progress-rules.d.ts +36 -11
- package/dist-server/service/enrollment/progress-rules.js +47 -12
- package/dist-server/service/enrollment/progress-rules.js.map +1 -1
- package/dist-server/service/enrollment/unit-progress.d.ts +4 -0
- package/dist-server/service/enrollment/unit-progress.js +20 -0
- package/dist-server/service/enrollment/unit-progress.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -13
- package/translations/en.json +30 -7
- package/translations/es.json +30 -7
- package/translations/ja.json +30 -7
- package/translations/ko.json +30 -7
- package/translations/ms.json +30 -7
- package/translations/zh.json +30 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"learning-documents.js","sourceRoot":"","sources":["../../../server/service/enrollment/learning-documents.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The documents this module issues through the pdf module (ADR-0194 decisions 9, 10 · ADR-0176): the certificate of a completed\n * enrolment (`learning.certificate`) and the completion register of a course (`learning.completion-register`). The pdf module\n * numbers and prints them with 「우리 회사」 - the company's name, representative and seal as the application keeps them.\n */\nimport { User } from '@things-factory/auth-base'\nimport { registerIssuedDocumentKind } from '@things-factory/pdf'\nimport { getRepository, Refusal } from '@things-factory/shell'\n\nimport { Course } from '../course/course'\nimport { CourseVersion } from '../course/course-version'\nimport { holds } from '../course/course-service'\nimport { Enrollment, EnrollmentStatus } from './enrollment'\nimport { completionRegister } from './enrollment-status'\n\nexport const LEARNING_CERTIFICATE_KIND = 'learning.certificate'\nexport const LEARNING_REGISTER_KIND = 'learning.completion-register'\n\nconst STYLE = `@page { size: A4; margin: 0 }\nbody { margin: 0; font-family: \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif; color: #222 }\n.page { position: relative; box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 30mm 24mm }\n.number { position: absolute; top: 16mm; left: 24mm; font-size: 9.5pt; color: #666 }\nh1 { margin: 6mm 0 16mm; text-align: center; font-size: 26pt; letter-spacing: 12pt }\ntable { width: 100%; border-collapse: collapse; font-size: 11.5pt }\nth, td { border: 1px solid #444; padding: 3.2mm 4mm; text-align: left; vertical-align: middle }\nth { background: #f2f2f2; font-weight: 600; text-align: center }\n.statement { margin: 22mm 0 0; text-align: center; font-size: 13pt; line-height: 2 }\n.date { margin-top: 18mm; text-align: center; font-size: 13pt; letter-spacing: 1pt }\n.issuer { position: relative; margin: 20mm auto 0; width: max-content; font-size: 14pt; line-height: 2; text-align: left; padding-right: 22mm }\n.issuer img { position: absolute; right: 0; top: 2mm; width: 22mm; opacity: .9 }`\n\nconst ISSUER = `<div class=\"issuer\">\n<div>상호 : {{company.name}}</div>\n{{#if company.address}}<div>주소 : {{company.address}}</div>{{/if}}\n<div>대표이사 : {{company.representative}}</div>\n{{#if company.seal}}<img src=\"{{company.seal}}\" alt=\"\">{{/if}}\n</div>`\n\n/** The certificate's template in code - a domain's own `PDFTemplate` of the kind prints in its place. */\nexport const CERTIFICATE_TEMPLATE = `<!doctype html><html lang=\"ko\"><head><meta charset=\"utf-8\"><style>\n${STYLE}\nth { width: 32mm }\n</style></head><body><div class=\"page\">\n<div class=\"number\">발급번호 {{issue.number}}</div>\n<h1>수료증</h1>\n<table>\n<tr><th>성명</th><td>{{learner.name}}</td></tr>\n<tr><th>과정</th><td>{{course.name}} (개정본 {{course.version}})</td></tr>\n<tr><th>이수일</th><td>{{completion.completedOn}}</td></tr>\n<tr><th>학습 시간</th><td>{{completion.minutes}}분</td></tr>\n{{#if completion.score}}<tr><th>평가 점수</th><td>{{completion.score}}점</td></tr>{{/if}}\n<tr><th>수료 번호</th><td>{{completion.certificateNo}}</td></tr>\n</table>\n<p class=\"statement\">위 사람은 위 과정을 이수하였음을 증명합니다.</p>\n<div class=\"date\">{{issue.issuedOn}}</div>\n${ISSUER}\n</div></body></html>`\n\n/** The completion register's template - one row per completion. */\nexport const REGISTER_TEMPLATE = `<!doctype html><html lang=\"ko\"><head><meta charset=\"utf-8\"><style>\n${STYLE}\n.page { padding: 24mm 16mm }\nh1 { font-size: 20pt; letter-spacing: 4pt; margin-bottom: 8mm }\n.meta { margin-bottom: 6mm; font-size: 10.5pt }\ntable { font-size: 9.5pt }\nth, td { padding: 2mm 2.4mm }\n</style></head><body><div class=\"page\">\n<div class=\"number\">발급번호 {{issue.number}}</div>\n<h1>이수 대장</h1>\n<div class=\"meta\">과정 : {{course.name}} · 기준일 : {{issue.asOf}} · 이수자 {{count}}명</div>\n<table>\n<tr><th>번호</th><th>성명</th><th>부서</th><th>개정본</th><th>이수일</th><th>학습 시간(분)</th><th>점수</th><th>수료 번호</th></tr>\n{{#each rows}}<tr><td>{{this.no}}</td><td>{{this.name}}</td><td>{{this.department}}</td><td>{{this.version}}</td><td>{{this.completedOn}}</td><td>{{this.minutes}}</td><td>{{this.score}}</td><td>{{this.certificateNo}}</td></tr>{{/each}}\n</table>\n<div class=\"date\">{{issue.issuedOn}}</div>\n${ISSUER}\n</div></body></html>`\n\nconst dayOf = (at: Date | string | null | undefined, timezone?: string | null) =>\n at ? new Intl.DateTimeFormat('en-CA', { timeZone: timezone || 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(at)) : ''\n\nasync function enrollmentFor(context: any, id: string): Promise<Enrollment | null> {\n return id ? await getRepository(Enrollment, context.state.tx).findOne({ where: { id, domain: { id: context.state.domain.id } } as any }) : null\n}\n\n/** A certificate is issued and read by the person it is of, once completed, and by a manager. */\nasync function mayHaveCertificate(context: any, id: string | null | undefined): Promise<boolean> {\n const enrollment = await enrollmentFor(context, id ?? '')\n if (!enrollment || enrollment.status !== EnrollmentStatus.COMPLETED) return false\n return enrollment.userId === context.state.user?.id || (await holds(context, 'course', 'query'))\n}\n\nexport function registerLearningDocuments(): void {\n registerIssuedDocumentKind({\n kind: LEARNING_CERTIFICATE_KIND,\n prefix: '수료',\n refType: 'Enrollment',\n template: { content_template: CERTIFICATE_TEMPLATE, page_size: 'A4' },\n async dataOf(context, refId) {\n const enrollment = await enrollmentFor(context, refId)\n if (!enrollment || enrollment.status !== EnrollmentStatus.COMPLETED) throw new Refusal('learning-not-completed', {}, 'a certificate is issued for a completed enrolment')\n const tx = context.state.tx\n const course = await getRepository(Course, tx).findOneBy({ id: enrollment.courseId! })\n const version = await getRepository(CourseVersion, tx).findOne({ where: { id: enrollment.versionId! } as any, withDeleted: true })\n const learner = await getRepository(User, tx).findOneBy({ id: enrollment.userId! })\n return {\n subjectName: `${learner?.name ?? ''} · ${course?.name ?? ''}`,\n data: {\n learner: { name: learner?.name ?? '' },\n course: { name: course?.name ?? '', version: version?.number ?? '' },\n completion: {\n completedOn: dayOf(enrollment.completedAt, context.state.domain.timezone),\n minutes: Math.floor((enrollment.learnedSeconds ?? 0) / 60),\n score: enrollment.score ?? null,\n certificateNo: enrollment.certificateNo ?? ''\n }\n }\n }\n },\n async mayIssue(context, refId) {\n return await mayHaveCertificate(context, refId)\n },\n async mayRead(context, row) {\n return await mayHaveCertificate(context, row.refId)\n }\n })\n\n registerIssuedDocumentKind({\n kind: LEARNING_REGISTER_KIND,\n prefix: '이수대장',\n refType: 'Course',\n asOf: true,\n template: { content_template: REGISTER_TEMPLATE, page_size: 'A4' },\n async dataOf(context, refId, input) {\n const course = await getRepository(Course, context.state.tx).findOne({ where: { id: refId, domain: { id: context.state.domain.id } } as any })\n if (!course) throw new Refusal('learning-not-found', {}, 'the record was not found. reload and try again.')\n const timezone = context.state.domain.timezone\n const rows = (await completionRegister(course.id, context)).filter(row => !input.asOf || dayOf(row.enrollment.completedAt, timezone) <= input.asOf)\n return {\n subjectName: course.name,\n data: {\n course: { name: course.name },\n count: rows.length,\n rows: rows.map((row, index) => ({\n no: index + 1,\n name: row.user?.name ?? '',\n department: row.departmentName ?? '',\n version: row.versionNumber ?? '',\n completedOn: dayOf(row.enrollment.completedAt, timezone),\n minutes: Math.floor((row.enrollment.learnedSeconds ?? 0) / 60),\n score: row.enrollment.score ?? '',\n certificateNo: row.enrollment.certificateNo ?? ''\n }))\n }\n }\n },\n async mayIssue(context) {\n return await holds(context, 'course', 'query')\n },\n async mayRead(context) {\n return await holds(context, 'course', 'query')\n }\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"learning-documents.js","sourceRoot":"","sources":["../../../server/service/enrollment/learning-documents.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The documents this module issues through the pdf module (ADR-0194 decisions 9, 10 · ADR-0176): the certificate of a completed\n * enrolment (`learning.certificate`) and the completion register of a course (`learning.completion-register`). The pdf module\n * numbers and prints them with 「우리 회사」 - the company's name, representative and seal as the application keeps them.\n */\nimport { requestHolds, User } from '@things-factory/auth-base'\nimport { registerIssuedDocumentKind } from '@things-factory/pdf'\nimport { getRepository, Refusal } from '@things-factory/shell'\n\nimport { Course } from '../course/course'\nimport { CourseVersion } from '../course/course-version'\nimport { Enrollment, EnrollmentStatus } from './enrollment'\nimport { completionRegister } from './enrollment-status'\n\nexport const LEARNING_CERTIFICATE_KIND = 'learning.certificate'\nexport const LEARNING_REGISTER_KIND = 'learning.completion-register'\n\nconst STYLE = `@page { size: A4; margin: 0 }\nbody { margin: 0; font-family: \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif; color: #222 }\n.page { position: relative; box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 30mm 24mm }\n.number { position: absolute; top: 16mm; left: 24mm; font-size: 9.5pt; color: #666 }\nh1 { margin: 6mm 0 16mm; text-align: center; font-size: 26pt; letter-spacing: 12pt }\ntable { width: 100%; border-collapse: collapse; font-size: 11.5pt }\nth, td { border: 1px solid #444; padding: 3.2mm 4mm; text-align: left; vertical-align: middle }\nth { background: #f2f2f2; font-weight: 600; text-align: center }\n.statement { margin: 22mm 0 0; text-align: center; font-size: 13pt; line-height: 2 }\n.date { margin-top: 18mm; text-align: center; font-size: 13pt; letter-spacing: 1pt }\n.issuer { position: relative; margin: 20mm auto 0; width: max-content; font-size: 14pt; line-height: 2; text-align: left; padding-right: 22mm }\n.issuer img { position: absolute; right: 0; top: 2mm; width: 22mm; opacity: .9 }`\n\nconst ISSUER = `<div class=\"issuer\">\n<div>상호 : {{company.name}}</div>\n{{#if company.address}}<div>주소 : {{company.address}}</div>{{/if}}\n<div>대표이사 : {{company.representative}}</div>\n{{#if company.seal}}<img src=\"{{company.seal}}\" alt=\"\">{{/if}}\n</div>`\n\n/** The certificate's template in code - a domain's own `PDFTemplate` of the kind prints in its place. */\nexport const CERTIFICATE_TEMPLATE = `<!doctype html><html lang=\"ko\"><head><meta charset=\"utf-8\"><style>\n${STYLE}\nth { width: 32mm }\n</style></head><body><div class=\"page\">\n<div class=\"number\">발급번호 {{issue.number}}</div>\n<h1>수료증</h1>\n<table>\n<tr><th>성명</th><td>{{learner.name}}</td></tr>\n<tr><th>과정</th><td>{{course.name}} (개정본 {{course.version}})</td></tr>\n<tr><th>이수일</th><td>{{completion.completedOn}}</td></tr>\n<tr><th>학습 시간</th><td>{{completion.minutes}}분</td></tr>\n{{#if completion.score}}<tr><th>평가 점수</th><td>{{completion.score}}점</td></tr>{{/if}}\n<tr><th>수료 번호</th><td>{{completion.certificateNo}}</td></tr>\n</table>\n<p class=\"statement\">위 사람은 위 과정을 이수하였음을 증명합니다.</p>\n<div class=\"date\">{{issue.issuedOn}}</div>\n${ISSUER}\n</div></body></html>`\n\n/** The completion register's template - one row per completion. */\nexport const REGISTER_TEMPLATE = `<!doctype html><html lang=\"ko\"><head><meta charset=\"utf-8\"><style>\n${STYLE}\n.page { padding: 24mm 16mm }\nh1 { font-size: 20pt; letter-spacing: 4pt; margin-bottom: 8mm }\n.meta { margin-bottom: 6mm; font-size: 10.5pt }\ntable { font-size: 9.5pt }\nth, td { padding: 2mm 2.4mm }\n</style></head><body><div class=\"page\">\n<div class=\"number\">발급번호 {{issue.number}}</div>\n<h1>이수 대장</h1>\n<div class=\"meta\">과정 : {{course.name}} · 기준일 : {{issue.asOf}} · 이수자 {{count}}명</div>\n<table>\n<tr><th>번호</th><th>성명</th><th>부서</th><th>개정본</th><th>이수일</th><th>학습 시간(분)</th><th>점수</th><th>수료 번호</th></tr>\n{{#each rows}}<tr><td>{{this.no}}</td><td>{{this.name}}</td><td>{{this.department}}</td><td>{{this.version}}</td><td>{{this.completedOn}}</td><td>{{this.minutes}}</td><td>{{this.score}}</td><td>{{this.certificateNo}}</td></tr>{{/each}}\n</table>\n<div class=\"date\">{{issue.issuedOn}}</div>\n${ISSUER}\n</div></body></html>`\n\nconst dayOf = (at: Date | string | null | undefined, timezone?: string | null) =>\n at ? new Intl.DateTimeFormat('en-CA', { timeZone: timezone || 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(at)) : ''\n\nasync function enrollmentFor(context: any, id: string): Promise<Enrollment | null> {\n return id ? await getRepository(Enrollment, context.state.tx).findOne({ where: { id, domain: { id: context.state.domain.id } } as any }) : null\n}\n\n/** A certificate is issued and read by the person it is of, once completed, and by a manager. */\nasync function mayHaveCertificate(context: any, id: string | null | undefined): Promise<boolean> {\n const enrollment = await enrollmentFor(context, id ?? '')\n if (!enrollment || enrollment.status !== EnrollmentStatus.COMPLETED) return false\n return enrollment.userId === context.state.user?.id || (await requestHolds(context, 'course', 'query'))\n}\n\nexport function registerLearningDocuments(): void {\n registerIssuedDocumentKind({\n kind: LEARNING_CERTIFICATE_KIND,\n prefix: '수료',\n refType: 'Enrollment',\n template: { content_template: CERTIFICATE_TEMPLATE, page_size: 'A4' },\n async dataOf(context, refId) {\n const enrollment = await enrollmentFor(context, refId)\n if (!enrollment || enrollment.status !== EnrollmentStatus.COMPLETED) throw new Refusal('learning-not-completed', {}, 'a certificate is issued for a completed enrolment')\n const tx = context.state.tx\n const course = await getRepository(Course, tx).findOneBy({ id: enrollment.courseId! })\n const version = await getRepository(CourseVersion, tx).findOne({ where: { id: enrollment.versionId! } as any, withDeleted: true })\n const learner = await getRepository(User, tx).findOneBy({ id: enrollment.userId! })\n return {\n subjectName: `${learner?.name ?? ''} · ${course?.name ?? ''}`,\n data: {\n learner: { name: learner?.name ?? '' },\n course: { name: course?.name ?? '', version: version?.number ?? '' },\n completion: {\n completedOn: dayOf(enrollment.completedAt, context.state.domain.timezone),\n minutes: Math.floor((enrollment.learnedSeconds ?? 0) / 60),\n score: enrollment.score ?? null,\n certificateNo: enrollment.certificateNo ?? ''\n }\n }\n }\n },\n async mayIssue(context, refId) {\n return await mayHaveCertificate(context, refId)\n },\n async mayRead(context, row) {\n return await mayHaveCertificate(context, row.refId)\n }\n })\n\n registerIssuedDocumentKind({\n kind: LEARNING_REGISTER_KIND,\n prefix: '이수대장',\n refType: 'Course',\n asOf: true,\n template: { content_template: REGISTER_TEMPLATE, page_size: 'A4' },\n async dataOf(context, refId, input) {\n const course = await getRepository(Course, context.state.tx).findOne({ where: { id: refId, domain: { id: context.state.domain.id } } as any })\n if (!course) throw new Refusal('learning-not-found', {}, 'the record was not found. reload and try again.')\n const timezone = context.state.domain.timezone\n const rows = (await completionRegister(course.id, context)).filter(row => !input.asOf || dayOf(row.enrollment.completedAt, timezone) <= input.asOf)\n return {\n subjectName: course.name,\n data: {\n course: { name: course.name },\n count: rows.length,\n rows: rows.map((row, index) => ({\n no: index + 1,\n name: row.user?.name ?? '',\n department: row.departmentName ?? '',\n version: row.versionNumber ?? '',\n completedOn: dayOf(row.enrollment.completedAt, timezone),\n minutes: Math.floor((row.enrollment.learnedSeconds ?? 0) / 60),\n score: row.enrollment.score ?? '',\n certificateNo: row.enrollment.certificateNo ?? ''\n }))\n }\n }\n },\n async mayIssue(context) {\n return await requestHolds(context, 'course', 'query')\n },\n async mayRead(context) {\n return await requestHolds(context, 'course', 'query')\n }\n })\n}\n"]}
|
|
@@ -7,43 +7,68 @@ export declare const DEFAULT_UNIT_SECONDS = 60;
|
|
|
7
7
|
export interface ProgressNow {
|
|
8
8
|
learnedSeconds: number;
|
|
9
9
|
watchedSeconds: number;
|
|
10
|
+
/** The furthest page of a PDF arrived at. */
|
|
11
|
+
pagesSeen?: number | null;
|
|
12
|
+
/** The furthest page of a PDF read through - looked at for its seconds. */
|
|
13
|
+
pagesRead?: number | null;
|
|
14
|
+
/** The seconds looked at on the furthest page arrived at, so far. */
|
|
15
|
+
pageSeconds?: number | null;
|
|
16
|
+
/** The page it was on until this tick. */
|
|
17
|
+
pageAt?: number | null;
|
|
10
18
|
lastTickAt?: Date | null;
|
|
11
19
|
}
|
|
12
20
|
export interface Tick {
|
|
13
21
|
/** What the screen says it looked at since its last tick - it may lower what is counted, never raise it. */
|
|
14
22
|
seconds: number;
|
|
15
|
-
/** Where
|
|
23
|
+
/** Where it stands - a video's second, a PDF's page (from 1); 0 for a text. */
|
|
16
24
|
position: number;
|
|
17
25
|
}
|
|
26
|
+
export interface UnitShape {
|
|
27
|
+
kind: string;
|
|
28
|
+
durationSeconds?: number | null;
|
|
29
|
+
pageCount?: number | null;
|
|
30
|
+
minSeconds?: number | null;
|
|
31
|
+
}
|
|
18
32
|
export type TickVerdict = {
|
|
19
33
|
refusal: 'position-ahead';
|
|
20
34
|
allowed: number;
|
|
35
|
+
} | {
|
|
36
|
+
refusal: 'page-ahead';
|
|
37
|
+
allowed: number;
|
|
21
38
|
} | {
|
|
22
39
|
add: number;
|
|
23
40
|
watchedSeconds: number;
|
|
24
41
|
positionSeconds: number;
|
|
42
|
+
pagesSeen: number;
|
|
43
|
+
pagesRead: number;
|
|
44
|
+
pageSeconds: number;
|
|
45
|
+
pageAt: number;
|
|
25
46
|
};
|
|
47
|
+
/** The fewest seconds a PDF page is looked at to count as read - its unit's time shared by its pages, 3 at least. */
|
|
48
|
+
export declare function secondsPerPage(unit: UnitShape): number;
|
|
26
49
|
/**
|
|
27
50
|
* What one tick does (decision 6). The time added is the gap since the last tick, at most 15 seconds and at most what
|
|
28
|
-
* the screen says - a tick with no tick before it starts the clock and adds nothing.
|
|
29
|
-
*
|
|
51
|
+
* the screen says - a tick with no tick before it starts the clock and adds nothing.
|
|
52
|
+
*
|
|
53
|
+
* - A video's position may not be ahead of what was watched by more than the time that passed: a move ahead is refused,
|
|
54
|
+
* a move back is free.
|
|
55
|
+
* - A PDF is read page by page (2026-11-08, the user). The seconds since the last tick were spent on the page it was on;
|
|
56
|
+
* the furthest page arrived at is read once its own seconds reach `secondsPerPage`. A tick may turn only to a page
|
|
57
|
+
* read or the one after the furthest read - two turns at the same moment do not read the page between them; going
|
|
58
|
+
* back is free.
|
|
30
59
|
*/
|
|
31
|
-
export declare function tickVerdict(progress: ProgressNow, tick: Tick, now: Date,
|
|
32
|
-
export interface UnitShape {
|
|
33
|
-
kind: string;
|
|
34
|
-
durationSeconds?: number | null;
|
|
35
|
-
minSeconds?: number | null;
|
|
36
|
-
}
|
|
60
|
+
export declare function tickVerdict(progress: ProgressNow, tick: Tick, now: Date, unit: UnitShape): TickVerdict;
|
|
37
61
|
/** Whether a unit counts toward completion - a link does not: the server cannot know what was watched elsewhere. */
|
|
38
62
|
export declare const counts: (unit: UnitShape) => boolean;
|
|
39
63
|
/**
|
|
40
64
|
* Whether a unit is done (decision 3): a video watched to its end (its length known) **and** its running time counted
|
|
41
|
-
* by the server - a skip or a faster playback reaches neither; a PDF
|
|
42
|
-
* done - it does not count.
|
|
65
|
+
* by the server - a skip or a faster playback reaches neither; a PDF with every page read through - the last one too (its
|
|
66
|
+
* page count known) - and its time; a text looked at for its time. A link is never done - it does not count.
|
|
43
67
|
*/
|
|
44
68
|
export declare function unitDone(unit: UnitShape, progress: {
|
|
45
69
|
learnedSeconds: number;
|
|
46
70
|
watchedSeconds: number;
|
|
71
|
+
pagesRead?: number | null;
|
|
47
72
|
}): boolean;
|
|
48
73
|
export interface CompletionFacts {
|
|
49
74
|
/** The units that count, and the ones of them done. */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.overdue = exports.counts = exports.DEFAULT_UNIT_SECONDS = exports.POSITION_SLACK_SECONDS = exports.TICK_SECONDS = void 0;
|
|
8
|
+
exports.secondsPerPage = secondsPerPage;
|
|
8
9
|
exports.tickVerdict = tickVerdict;
|
|
9
10
|
exports.unitDone = unitDone;
|
|
10
11
|
exports.missingOf = missingOf;
|
|
@@ -16,35 +17,69 @@ exports.TICK_SECONDS = 15;
|
|
|
16
17
|
exports.POSITION_SLACK_SECONDS = 2;
|
|
17
18
|
/** How long a PDF or a text is looked at to be done when its unit says nothing. */
|
|
18
19
|
exports.DEFAULT_UNIT_SECONDS = 60;
|
|
20
|
+
/** The fewest seconds a PDF page is looked at to count as read - its unit's time shared by its pages, 3 at least. */
|
|
21
|
+
function secondsPerPage(unit) {
|
|
22
|
+
return Math.max(3, Math.ceil((unit.minSeconds ?? exports.DEFAULT_UNIT_SECONDS) / Math.max(1, unit.pageCount ?? 1)));
|
|
23
|
+
}
|
|
19
24
|
/**
|
|
20
25
|
* What one tick does (decision 6). The time added is the gap since the last tick, at most 15 seconds and at most what
|
|
21
|
-
* the screen says - a tick with no tick before it starts the clock and adds nothing.
|
|
22
|
-
*
|
|
26
|
+
* the screen says - a tick with no tick before it starts the clock and adds nothing.
|
|
27
|
+
*
|
|
28
|
+
* - A video's position may not be ahead of what was watched by more than the time that passed: a move ahead is refused,
|
|
29
|
+
* a move back is free.
|
|
30
|
+
* - A PDF is read page by page (2026-11-08, the user). The seconds since the last tick were spent on the page it was on;
|
|
31
|
+
* the furthest page arrived at is read once its own seconds reach `secondsPerPage`. A tick may turn only to a page
|
|
32
|
+
* read or the one after the furthest read - two turns at the same moment do not read the page between them; going
|
|
33
|
+
* back is free.
|
|
23
34
|
*/
|
|
24
|
-
function tickVerdict(progress, tick, now,
|
|
35
|
+
function tickVerdict(progress, tick, now, unit) {
|
|
25
36
|
const gap = progress.lastTickAt ? Math.max(0, (now.getTime() - new Date(progress.lastTickAt).getTime()) / 1000) : 0;
|
|
26
37
|
const said = Math.max(0, Number(tick.seconds) || 0);
|
|
27
38
|
const add = progress.lastTickAt ? Math.floor(Math.min(exports.TICK_SECONDS, gap, said)) : 0;
|
|
28
39
|
const position = Math.max(0, Math.floor(Number(tick.position) || 0));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
let seen = progress.pagesSeen ?? 0;
|
|
41
|
+
let read = progress.pagesRead ?? 0;
|
|
42
|
+
let onPage = progress.pageSeconds ?? 0;
|
|
43
|
+
const kept = { pagesSeen: seen, pagesRead: read, pageSeconds: onPage };
|
|
44
|
+
if (unit.kind === 'VIDEO') {
|
|
45
|
+
const allowed = progress.watchedSeconds + Math.min(exports.TICK_SECONDS, gap) + exports.POSITION_SLACK_SECONDS;
|
|
46
|
+
if (position > allowed)
|
|
47
|
+
return { refusal: 'position-ahead', allowed: Math.floor(progress.watchedSeconds) };
|
|
48
|
+
return { add, watchedSeconds: Math.max(progress.watchedSeconds, position), positionSeconds: position, ...kept, pageAt: 0 };
|
|
49
|
+
}
|
|
50
|
+
if (unit.kind === 'PDF') {
|
|
51
|
+
/* The time since the last tick goes to the page it was on - it reads the furthest page only when it was there. */
|
|
52
|
+
const was = progress.pageAt ?? 0;
|
|
53
|
+
if (was > 0 && was === seen) {
|
|
54
|
+
onPage += add;
|
|
55
|
+
if (onPage >= secondsPerPage(unit))
|
|
56
|
+
read = Math.max(read, seen);
|
|
57
|
+
}
|
|
58
|
+
const page = Math.max(1, position);
|
|
59
|
+
if (page > read + 1)
|
|
60
|
+
return { refusal: 'page-ahead', allowed: read };
|
|
61
|
+
if (page > seen) {
|
|
62
|
+
seen = page;
|
|
63
|
+
onPage = 0;
|
|
64
|
+
}
|
|
65
|
+
return { add, watchedSeconds: progress.watchedSeconds, positionSeconds: 0, pagesSeen: seen, pagesRead: read, pageSeconds: onPage, pageAt: page };
|
|
66
|
+
}
|
|
67
|
+
return { add, watchedSeconds: progress.watchedSeconds, positionSeconds: 0, ...kept, pageAt: 0 };
|
|
35
68
|
}
|
|
36
69
|
/** Whether a unit counts toward completion - a link does not: the server cannot know what was watched elsewhere. */
|
|
37
70
|
const counts = (unit) => unit.kind !== 'LINK';
|
|
38
71
|
exports.counts = counts;
|
|
39
72
|
/**
|
|
40
73
|
* Whether a unit is done (decision 3): a video watched to its end (its length known) **and** its running time counted
|
|
41
|
-
* by the server - a skip or a faster playback reaches neither; a PDF
|
|
42
|
-
* done - it does not count.
|
|
74
|
+
* by the server - a skip or a faster playback reaches neither; a PDF with every page read through - the last one too (its
|
|
75
|
+
* page count known) - and its time; a text looked at for its time. A link is never done - it does not count.
|
|
43
76
|
*/
|
|
44
77
|
function unitDone(unit, progress) {
|
|
45
78
|
if (unit.kind === 'VIDEO')
|
|
46
79
|
return !!unit.durationSeconds && progress.watchedSeconds >= unit.durationSeconds - exports.POSITION_SLACK_SECONDS && progress.learnedSeconds >= unit.durationSeconds - exports.TICK_SECONDS;
|
|
47
|
-
if (unit.kind === 'PDF'
|
|
80
|
+
if (unit.kind === 'PDF')
|
|
81
|
+
return !!unit.pageCount && (progress.pagesRead ?? 0) >= unit.pageCount && progress.learnedSeconds >= (unit.minSeconds ?? exports.DEFAULT_UNIT_SECONDS);
|
|
82
|
+
if (unit.kind === 'MARKDOWN')
|
|
48
83
|
return progress.learnedSeconds >= (unit.minSeconds ?? exports.DEFAULT_UNIT_SECONDS);
|
|
49
84
|
return false;
|
|
50
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"progress-rules.js","sourceRoot":"","sources":["../../../server/service/enrollment/progress-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the server decides of learning by itself (ADR-0194 decisions 4, 6, 8) - data in, verdict out, no database: the\n * time a tick adds, where a video may stand, when a unit is done, whether an enrolment is complete, and a quiz's score.\n */\n\n/** The longest a tick is counted for, in seconds - the screen sends one every 15 seconds looked at. */\nexport const TICK_SECONDS = 15\n/** How far ahead of what was watched a position still reads as watching - the video's own step between two updates. */\nexport const POSITION_SLACK_SECONDS = 2\n/** How long a PDF or a text is looked at to be done when its unit says nothing. */\nexport const DEFAULT_UNIT_SECONDS = 60\n\nexport interface ProgressNow {\n learnedSeconds: number\n watchedSeconds: number\n lastTickAt?: Date | null\n}\n\nexport interface Tick {\n /** What the screen says it looked at since its last tick - it may lower what is counted, never raise it. */\n seconds: number\n /** Where the video stands, in seconds. */\n position: number\n}\n\nexport type TickVerdict = { refusal: 'position-ahead'; allowed: number } | { add: number; watchedSeconds: number; positionSeconds: number }\n\n/**\n * What one tick does (decision 6). The time added is the gap since the last tick, at most 15 seconds and at most what\n * the screen says - a tick with no tick before it starts the clock and adds nothing. A video's position may not be ahead\n * of what was watched by more than the time that passed: a move ahead is refused, a move back is free.\n */\nexport function tickVerdict(progress: ProgressNow, tick: Tick, now: Date, video: boolean): TickVerdict {\n const gap = progress.lastTickAt ? Math.max(0, (now.getTime() - new Date(progress.lastTickAt).getTime()) / 1000) : 0\n const said = Math.max(0, Number(tick.seconds) || 0)\n const add = progress.lastTickAt ? Math.floor(Math.min(TICK_SECONDS, gap, said)) : 0\n const position = Math.max(0, Math.floor(Number(tick.position) || 0))\n if (!video) return { add, watchedSeconds: progress.watchedSeconds, positionSeconds: 0 }\n const allowed = progress.watchedSeconds + Math.min(TICK_SECONDS, gap) + POSITION_SLACK_SECONDS\n if (position > allowed) return { refusal: 'position-ahead', allowed: Math.floor(progress.watchedSeconds) }\n return { add, watchedSeconds: Math.max(progress.watchedSeconds, position), positionSeconds: position }\n}\n\nexport interface UnitShape {\n kind: string\n durationSeconds?: number | null\n minSeconds?: number | null\n}\n\n/** Whether a unit counts toward completion - a link does not: the server cannot know what was watched elsewhere. */\nexport const counts = (unit: UnitShape) => unit.kind !== 'LINK'\n\n/**\n * Whether a unit is done (decision 3): a video watched to its end (its length known) **and** its running time counted\n * by the server - a skip or a faster playback reaches neither; a PDF or a text looked at for its time. A link is never\n * done - it does not count.\n */\nexport function unitDone(unit: UnitShape, progress: { learnedSeconds: number; watchedSeconds: number }): boolean {\n if (unit.kind === 'VIDEO')\n return !!unit.durationSeconds && progress.watchedSeconds >= unit.durationSeconds - POSITION_SLACK_SECONDS && progress.learnedSeconds >= unit.durationSeconds - TICK_SECONDS\n if (unit.kind === 'PDF' || unit.kind === 'MARKDOWN') return progress.learnedSeconds >= (unit.minSeconds ?? DEFAULT_UNIT_SECONDS)\n return false\n}\n\nexport interface CompletionFacts {\n /** The units that count, and the ones of them done. */\n units: number\n unitsDone: number\n learnedSeconds: number\n minSeconds: number\n /** The version has a quiz - questions and a pass score. */\n quiz: boolean\n passed: boolean\n /** The version asks for an acknowledgement. */\n acknowledgement: boolean\n acknowledged: boolean\n}\n\nexport type Missing = 'units' | 'time' | 'quiz' | 'acknowledgement'\n\n/**\n * What still stands between an enrolment and its completion (decision 8) - every unit done, the least time, the quiz\n * passed when there is one, the acknowledgement confirmed when one is asked. Complete when nothing is missing.\n */\nexport function missingOf(facts: CompletionFacts): Missing[] {\n const missing: Missing[] = []\n if (facts.unitsDone < facts.units) missing.push('units')\n if (facts.learnedSeconds < facts.minSeconds) missing.push('time')\n if (facts.quiz && !facts.passed) missing.push('quiz')\n if (facts.acknowledgement && !facts.acknowledged) missing.push('acknowledgement')\n return missing\n}\n\nexport interface QuestionKey {\n id: string\n kind: string\n choices: { id: string; correct: boolean }[]\n}\n\n/**\n * A quiz's score, out of 100 (decision 4): each question is right when the choices picked are exactly the right ones -\n * one for SINGLE and TRUE_FALSE, every right one and no other for MULTIPLE.\n */\nexport function scoreOf(questions: QuestionKey[], answers: Record<string, string[]>): number {\n if (!questions.length) return 0\n const right = questions.filter(question => {\n const picked = new Set(answers?.[question.id] ?? [])\n const correct = question.choices.filter(one => one.correct).map(one => one.id)\n return picked.size === correct.length && correct.every(id => picked.has(id))\n }).length\n return Math.round((right / questions.length) * 100)\n}\n\n/** The questions an attempt draws - `count` of them (all when empty), in a shuffled order, each with its choices shuffled. */\nexport function drawQuestions<Q extends { id: string; choices: { id: string }[] }>(questions: Q[], count: number | null | undefined, random: () => number = Math.random) {\n const shuffle = <T>(items: T[]): T[] => {\n const out = [...items]\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(random() * (i + 1))\n ;[out[i], out[j]] = [out[j], out[i]]\n }\n return out\n }\n const picked = shuffle(questions).slice(0, count && count > 0 ? count : questions.length)\n return picked.map(question => ({ questionId: question.id, choiceIds: shuffle(question.choices.map(one => one.id)) }))\n}\n\n/** The day an enrolment reads as overdue: open, and its due day before today. */\nexport const overdue = (enrollment: { status: string; dueOn?: string | null }, today: string) =>\n (enrollment.status === 'ASSIGNED' || enrollment.status === 'IN_PROGRESS') && !!enrollment.dueOn && enrollment.dueOn < today\n"]}
|
|
1
|
+
{"version":3,"file":"progress-rules.js","sourceRoot":"","sources":["../../../server/service/enrollment/progress-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the server decides of learning by itself (ADR-0194 decisions 4, 6, 8) - data in, verdict out, no database: the\n * time a tick adds, where a video may stand, when a unit is done, whether an enrolment is complete, and a quiz's score.\n */\n\n/** The longest a tick is counted for, in seconds - the screen sends one every 15 seconds looked at. */\nexport const TICK_SECONDS = 15\n/** How far ahead of what was watched a position still reads as watching - the video's own step between two updates. */\nexport const POSITION_SLACK_SECONDS = 2\n/** How long a PDF or a text is looked at to be done when its unit says nothing. */\nexport const DEFAULT_UNIT_SECONDS = 60\n\nexport interface ProgressNow {\n learnedSeconds: number\n watchedSeconds: number\n /** The furthest page of a PDF arrived at. */\n pagesSeen?: number | null\n /** The furthest page of a PDF read through - looked at for its seconds. */\n pagesRead?: number | null\n /** The seconds looked at on the furthest page arrived at, so far. */\n pageSeconds?: number | null\n /** The page it was on until this tick. */\n pageAt?: number | null\n lastTickAt?: Date | null\n}\n\nexport interface Tick {\n /** What the screen says it looked at since its last tick - it may lower what is counted, never raise it. */\n seconds: number\n /** Where it stands - a video's second, a PDF's page (from 1); 0 for a text. */\n position: number\n}\n\nexport interface UnitShape {\n kind: string\n durationSeconds?: number | null\n pageCount?: number | null\n minSeconds?: number | null\n}\n\nexport type TickVerdict =\n | { refusal: 'position-ahead'; allowed: number }\n | { refusal: 'page-ahead'; allowed: number }\n | { add: number; watchedSeconds: number; positionSeconds: number; pagesSeen: number; pagesRead: number; pageSeconds: number; pageAt: number }\n\n/** The fewest seconds a PDF page is looked at to count as read - its unit's time shared by its pages, 3 at least. */\nexport function secondsPerPage(unit: UnitShape): number {\n return Math.max(3, Math.ceil((unit.minSeconds ?? DEFAULT_UNIT_SECONDS) / Math.max(1, unit.pageCount ?? 1)))\n}\n\n/**\n * What one tick does (decision 6). The time added is the gap since the last tick, at most 15 seconds and at most what\n * the screen says - a tick with no tick before it starts the clock and adds nothing.\n *\n * - A video's position may not be ahead of what was watched by more than the time that passed: a move ahead is refused,\n * a move back is free.\n * - A PDF is read page by page (2026-11-08, the user). The seconds since the last tick were spent on the page it was on;\n * the furthest page arrived at is read once its own seconds reach `secondsPerPage`. A tick may turn only to a page\n * read or the one after the furthest read - two turns at the same moment do not read the page between them; going\n * back is free.\n */\nexport function tickVerdict(progress: ProgressNow, tick: Tick, now: Date, unit: UnitShape): TickVerdict {\n const gap = progress.lastTickAt ? Math.max(0, (now.getTime() - new Date(progress.lastTickAt).getTime()) / 1000) : 0\n const said = Math.max(0, Number(tick.seconds) || 0)\n const add = progress.lastTickAt ? Math.floor(Math.min(TICK_SECONDS, gap, said)) : 0\n const position = Math.max(0, Math.floor(Number(tick.position) || 0))\n let seen = progress.pagesSeen ?? 0\n let read = progress.pagesRead ?? 0\n let onPage = progress.pageSeconds ?? 0\n const kept = { pagesSeen: seen, pagesRead: read, pageSeconds: onPage }\n if (unit.kind === 'VIDEO') {\n const allowed = progress.watchedSeconds + Math.min(TICK_SECONDS, gap) + POSITION_SLACK_SECONDS\n if (position > allowed) return { refusal: 'position-ahead', allowed: Math.floor(progress.watchedSeconds) }\n return { add, watchedSeconds: Math.max(progress.watchedSeconds, position), positionSeconds: position, ...kept, pageAt: 0 }\n }\n if (unit.kind === 'PDF') {\n /* The time since the last tick goes to the page it was on - it reads the furthest page only when it was there. */\n const was = progress.pageAt ?? 0\n if (was > 0 && was === seen) {\n onPage += add\n if (onPage >= secondsPerPage(unit)) read = Math.max(read, seen)\n }\n const page = Math.max(1, position)\n if (page > read + 1) return { refusal: 'page-ahead', allowed: read }\n if (page > seen) {\n seen = page\n onPage = 0\n }\n return { add, watchedSeconds: progress.watchedSeconds, positionSeconds: 0, pagesSeen: seen, pagesRead: read, pageSeconds: onPage, pageAt: page }\n }\n return { add, watchedSeconds: progress.watchedSeconds, positionSeconds: 0, ...kept, pageAt: 0 }\n}\n\n/** Whether a unit counts toward completion - a link does not: the server cannot know what was watched elsewhere. */\nexport const counts = (unit: UnitShape) => unit.kind !== 'LINK'\n\n/**\n * Whether a unit is done (decision 3): a video watched to its end (its length known) **and** its running time counted\n * by the server - a skip or a faster playback reaches neither; a PDF with every page read through - the last one too (its\n * page count known) - and its time; a text looked at for its time. A link is never done - it does not count.\n */\nexport function unitDone(unit: UnitShape, progress: { learnedSeconds: number; watchedSeconds: number; pagesRead?: number | null }): boolean {\n if (unit.kind === 'VIDEO')\n return !!unit.durationSeconds && progress.watchedSeconds >= unit.durationSeconds - POSITION_SLACK_SECONDS && progress.learnedSeconds >= unit.durationSeconds - TICK_SECONDS\n if (unit.kind === 'PDF') return !!unit.pageCount && (progress.pagesRead ?? 0) >= unit.pageCount && progress.learnedSeconds >= (unit.minSeconds ?? DEFAULT_UNIT_SECONDS)\n if (unit.kind === 'MARKDOWN') return progress.learnedSeconds >= (unit.minSeconds ?? DEFAULT_UNIT_SECONDS)\n return false\n}\n\nexport interface CompletionFacts {\n /** The units that count, and the ones of them done. */\n units: number\n unitsDone: number\n learnedSeconds: number\n minSeconds: number\n /** The version has a quiz - questions and a pass score. */\n quiz: boolean\n passed: boolean\n /** The version asks for an acknowledgement. */\n acknowledgement: boolean\n acknowledged: boolean\n}\n\nexport type Missing = 'units' | 'time' | 'quiz' | 'acknowledgement'\n\n/**\n * What still stands between an enrolment and its completion (decision 8) - every unit done, the least time, the quiz\n * passed when there is one, the acknowledgement confirmed when one is asked. Complete when nothing is missing.\n */\nexport function missingOf(facts: CompletionFacts): Missing[] {\n const missing: Missing[] = []\n if (facts.unitsDone < facts.units) missing.push('units')\n if (facts.learnedSeconds < facts.minSeconds) missing.push('time')\n if (facts.quiz && !facts.passed) missing.push('quiz')\n if (facts.acknowledgement && !facts.acknowledged) missing.push('acknowledgement')\n return missing\n}\n\nexport interface QuestionKey {\n id: string\n kind: string\n choices: { id: string; correct: boolean }[]\n}\n\n/**\n * A quiz's score, out of 100 (decision 4): each question is right when the choices picked are exactly the right ones -\n * one for SINGLE and TRUE_FALSE, every right one and no other for MULTIPLE.\n */\nexport function scoreOf(questions: QuestionKey[], answers: Record<string, string[]>): number {\n if (!questions.length) return 0\n const right = questions.filter(question => {\n const picked = new Set(answers?.[question.id] ?? [])\n const correct = question.choices.filter(one => one.correct).map(one => one.id)\n return picked.size === correct.length && correct.every(id => picked.has(id))\n }).length\n return Math.round((right / questions.length) * 100)\n}\n\n/** The questions an attempt draws - `count` of them (all when empty), in a shuffled order, each with its choices shuffled. */\nexport function drawQuestions<Q extends { id: string; choices: { id: string }[] }>(questions: Q[], count: number | null | undefined, random: () => number = Math.random) {\n const shuffle = <T>(items: T[]): T[] => {\n const out = [...items]\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(random() * (i + 1))\n ;[out[i], out[j]] = [out[j], out[i]]\n }\n return out\n }\n const picked = shuffle(questions).slice(0, count && count > 0 ? count : questions.length)\n return picked.map(question => ({ questionId: question.id, choiceIds: shuffle(question.choices.map(one => one.id)) }))\n}\n\n/** The day an enrolment reads as overdue: open, and its due day before today. */\nexport const overdue = (enrollment: { status: string; dueOn?: string | null }, today: string) =>\n (enrollment.status === 'ASSIGNED' || enrollment.status === 'IN_PROGRESS') && !!enrollment.dueOn && enrollment.dueOn < today\n"]}
|
|
@@ -18,6 +18,10 @@ export declare class UnitProgress {
|
|
|
18
18
|
learnedSeconds: number;
|
|
19
19
|
watchedSeconds: number;
|
|
20
20
|
positionSeconds: number;
|
|
21
|
+
pagesSeen: number;
|
|
22
|
+
pagesRead: number;
|
|
23
|
+
pageSeconds: number;
|
|
24
|
+
pageAt: number;
|
|
21
25
|
lastTickAt?: Date;
|
|
22
26
|
doneAt?: Date;
|
|
23
27
|
createdAt?: Date;
|
|
@@ -63,6 +63,26 @@ tslib_1.__decorate([
|
|
|
63
63
|
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Where the video was left, in seconds.' }),
|
|
64
64
|
tslib_1.__metadata("design:type", Number)
|
|
65
65
|
], UnitProgress.prototype, "positionSeconds", void 0);
|
|
66
|
+
tslib_1.__decorate([
|
|
67
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
68
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'The furthest page of a PDF arrived at.' }),
|
|
69
|
+
tslib_1.__metadata("design:type", Number)
|
|
70
|
+
], UnitProgress.prototype, "pagesSeen", void 0);
|
|
71
|
+
tslib_1.__decorate([
|
|
72
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
73
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'The furthest page of a PDF read through - looked at for its seconds; the next page is the furthest it may turn to.' }),
|
|
74
|
+
tslib_1.__metadata("design:type", Number)
|
|
75
|
+
], UnitProgress.prototype, "pagesRead", void 0);
|
|
76
|
+
tslib_1.__decorate([
|
|
77
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
78
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'The seconds looked at on the furthest page arrived at, so far.' }),
|
|
79
|
+
tslib_1.__metadata("design:type", Number)
|
|
80
|
+
], UnitProgress.prototype, "pageSeconds", void 0);
|
|
81
|
+
tslib_1.__decorate([
|
|
82
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
83
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'The page of a PDF it was left at.' }),
|
|
84
|
+
tslib_1.__metadata("design:type", Number)
|
|
85
|
+
], UnitProgress.prototype, "pageAt", void 0);
|
|
66
86
|
tslib_1.__decorate([
|
|
67
87
|
(0, typeorm_1.Column)({ nullable: true }),
|
|
68
88
|
(0, type_graphql_1.Field)({ nullable: true, description: 'When the last tick came - the next one is counted from it.' }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unit-progress.js","sourceRoot":"","sources":["../../../server/service/enrollment/unit-progress.ts"],"names":[],"mappings":";;;;AAAA,+CAAyD;AACzD,qCAAoJ;AAEpJ,yDAAgD;AAChD,iDAA8C;AAE9C,uDAAkD;AAClD,6CAAyC;AAEzC;;;;GAIG;AAII,IAAM,YAAY,GAAlB,MAAM,YAAY;
|
|
1
|
+
{"version":3,"file":"unit-progress.js","sourceRoot":"","sources":["../../../server/service/enrollment/unit-progress.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, ID, Int, ObjectType } from 'type-graphql'\nimport { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\n\nimport { User } from '@things-factory/auth-base'\nimport { Domain } from '@things-factory/shell'\n\nimport { CourseUnit } from '../course/course-unit'\nimport { Enrollment } from './enrollment'\n\n/**\n * 진도 (ADR-0194 decision 6) - how far one enrolment went through one unit: the seconds the server counted, how far the\n * video was watched, and whether it is done. Never deleted - another device carries on from it. The time is counted from\n * the gap since the last tick, at most 15 seconds each - never a sum the screen sends.\n */\n@Entity({ name: 'learning_unit_progresses' })\n@Index('ix_learning_unit_progress_0', (row: UnitProgress) => [row.domain, row.enrollment, row.unit], { unique: true, where: '\"deleted_at\" IS NULL' })\n@ObjectType({ description: 'How far one enrolment went through one unit - the time the server counted, how far the video was watched, done or not.' })\nexport class UnitProgress {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Progress id.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'The domain.' })\n domain?: Domain\n\n @RelationId((row: UnitProgress) => row.domain)\n domainId?: string\n\n @ManyToOne(type => Enrollment)\n @Field(type => Enrollment, { nullable: true, description: 'The enrolment.' })\n enrollment?: Enrollment\n\n @RelationId((row: UnitProgress) => row.enrollment)\n enrollmentId?: string\n\n @ManyToOne(type => CourseUnit)\n @Field(type => CourseUnit, { nullable: true, description: 'The unit.' })\n unit?: CourseUnit\n\n @RelationId((row: UnitProgress) => row.unit)\n unitId?: string\n\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'The seconds the server counted on it.' })\n learnedSeconds: number\n\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'How far the video was watched, in seconds - it cannot be moved past it.' })\n watchedSeconds: number\n\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'Where the video was left, in seconds.' })\n positionSeconds: number\n\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'The furthest page of a PDF arrived at.' })\n pagesSeen: number\n\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'The furthest page of a PDF read through - looked at for its seconds; the next page is the furthest it may turn to.' })\n pagesRead: number\n\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'The seconds looked at on the furthest page arrived at, so far.' })\n pageSeconds: number\n\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'The page of a PDF it was left at.' })\n pageAt: number\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When the last tick came - the next one is counted from it.' })\n lastTickAt?: Date\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When it was done.' })\n doneAt?: Date\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Creation timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp.' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete timestamp.' })\n deletedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Who made it.' })\n creator?: User\n\n @RelationId((row: UnitProgress) => row.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Who changed it last.' })\n updater?: User\n\n @RelationId((row: UnitProgress) => row.updater)\n updaterId?: string\n}\n"]}
|
|
@@ -10,5 +10,5 @@ export * from './enrollment/learning-outcome';
|
|
|
10
10
|
export { cancelEnrollment, DIRECT_SOURCE_KIND, enroll, learningAddressOf, withdraw, type LearningSource, LEARNING_CERTIFICATE_KIND, LEARNING_REGISTER_KIND } from './enrollment';
|
|
11
11
|
export declare const entities: (typeof import("./course").Course | typeof import("./course").CourseVersion | typeof import("./course").CourseUnit | typeof import("./course").CourseQuestion | typeof import("./enrollment").Enrollment | typeof import("./enrollment").QuizAttempt | typeof import("./enrollment").UnitProgress)[];
|
|
12
12
|
export declare const schema: {
|
|
13
|
-
resolverClasses: (typeof import("./course/course-mutation").CourseMutation | typeof import("./course/course-query").CourseQuery | typeof import("./course/course-query").CourseVersionFields | typeof import("./course/course-query").CourseUnitFields | typeof import("./course/course-query").CourseQuestionFields | typeof import("./enrollment/enrollment-mutation").EnrollmentMutation | typeof import("./enrollment/enrollment-query").EnrollmentQuery)[];
|
|
13
|
+
resolverClasses: (typeof import("./course/material-draft-mutation").MaterialDraftMutation | typeof import("./course/course-mutation").CourseMutation | typeof import("./course/course-query").CourseQuery | typeof import("./course/course-query").CourseVersionFields | typeof import("./course/course-query").CourseUnitFields | typeof import("./course/course-query").CourseQuestionFields | typeof import("./enrollment/enrollment-mutation").EnrollmentMutation | typeof import("./enrollment/enrollment-query").EnrollmentQuery)[];
|
|
14
14
|
};
|