Skip to content

Commit

Permalink
Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
nygrenh committed Jan 4, 2024
1 parent 575b1f3 commit 226df54
Show file tree
Hide file tree
Showing 14 changed files with 107 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,10 @@ const ModuleWrapper = styled.div`
`

export interface CongratulationsProps {
courseInstanceId: string
modules: Array<UserModuleCompletionStatus>
}

const Congratulations: React.FC<React.PropsWithChildren<CongratulationsProps>> = ({
courseInstanceId,
modules,
}) => {
const Congratulations: React.FC<React.PropsWithChildren<CongratulationsProps>> = ({ modules }) => {
const { t } = useTranslation()

const someModuleCompleted = modules.some((module) => module.completed)
Expand Down Expand Up @@ -126,7 +122,7 @@ const Congratulations: React.FC<React.PropsWithChildren<CongratulationsProps>> =
.map((module) => (
<ModuleCard
key={module.module_id}
courseInstanceId={courseInstanceId}
certificateConfigurationId={module.certificate_configuration_id}
module={module}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ const StyledLink = styled.a`
`

export interface CongratulationsLinksProps {
courseInstanceId: string
certificateConfigurationId: string | null
module: UserModuleCompletionStatus
}

const CongratulationsLinks: React.FC<React.PropsWithChildren<CongratulationsLinksProps>> = ({
courseInstanceId,
certificateConfigurationId,
module,
}) => {
const { t } = useTranslation()
Expand All @@ -49,9 +49,9 @@ const CongratulationsLinks: React.FC<React.PropsWithChildren<CongratulationsLink
</Button>
</a>
)}
{module.certification_enabled && (
{module.certification_enabled && certificateConfigurationId && (
<a
href={`/module-certificate?module=${module.module_id}&instance=${courseInstanceId}`}
href={`/generate-certificate?module=${module.module_id}&ccid=${certificateConfigurationId}`}
aria-label={`Generate certificate for completing ${module.name}`}
>
<Button variant="tertiary" size="large" disabled={!module.completed}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ const StyledFailedIcon = styled(FailedIcon)`
`

export interface ModuleCardProps {
courseInstanceId: string
certificateConfigurationId: string | null
module: UserModuleCompletionStatus
}

const ModuleCard: React.FC<React.PropsWithChildren<ModuleCardProps>> = ({
courseInstanceId,
certificateConfigurationId,
module,
}) => {
const { t } = useTranslation()
Expand All @@ -111,7 +111,10 @@ const ModuleCard: React.FC<React.PropsWithChildren<ModuleCardProps>> = ({
>
{module.name}
</h2>
<CongratulationsLinks courseInstanceId={courseInstanceId} module={module} />
<CongratulationsLinks
certificateConfigurationId={certificateConfigurationId}
module={module}
/>
</Wrapper>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ const CongratulationsBlock: React.FC<React.PropsWithChildren<unknown>> = () => {
{/* This block is only visible after the default module is completed.*/}
{courseInstanceId && getModuleCompletions.data.some((x) => x.default && x.completed) && (
<BreakFromCentered sidebar={false}>
<Congratulations
courseInstanceId={courseInstanceId}
modules={getModuleCompletions.data}
/>
<Congratulations modules={getModuleCompletions.data} />
</BreakFromCentered>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ COMMENT ON COLUMN certificate_configuration_to_requirements.deleted_at IS 'Times
COMMENT ON COLUMN certificate_configuration_to_requirements.certificate_configuration_id IS 'Identifies the certificate this requirement is for.';
COMMENT ON COLUMN certificate_configuration_to_requirements.course_instance_id IS 'If defined, the referred course instance is a requirement for this certificate. If multiple course instances are a requirement for a certificate (multiple rows in this table), the user be on any of those course instances. This is because a user can be on only one course instance at a time.';
COMMENT ON COLUMN certificate_configuration_to_requirements.course_module_id IS 'If defined, the referred course module is a requirement for this certificate. If multiple course modules are a requirement for a certificate (multiple rows in this table), the user has to complete all of those course modules.';
ALTER TABLE certificate_configuration_to_requirements
ADD CONSTRAINT one_requirement_per_row CHECK (
num_nonnulls(
course_instance_id,
course_module_id
) = 1
);
COMMENT ON CONSTRAINT one_requirement_per_row ON certificate_configuration_to_requirements IS 'Each row can define only one requirement. If you want multiple requirements for a certificate, add more rows.';
-- Generated certificates cannot no longer refer directly refer to the requirements as there may be many of them
-- We'll migrate the table to refer to the configuration instead
ALTER TABLE generated_certificates
Expand Down
25 changes: 24 additions & 1 deletion services/headless-lms/models/src/library/progressing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ pub struct UserModuleCompletionStatus {
pub passed: Option<bool>,
pub enable_registering_completion_to_uh_open_university: bool,
pub certification_enabled: bool,
pub certificate_configuration_id: Option<Uuid>,
}

/// Gets course modules with user's completion status for the given instance.
Expand All @@ -711,23 +712,45 @@ pub async fn get_user_module_completion_statuses_for_course_instance(
.into_iter()
.map(|x| (x.course_module_id, x))
.collect();

let all_default_certificate_configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course_instance(conn, course_instance_id).await?;

let course_module_completion_statuses = course_modules
.into_iter()
.map(|module| {
let mut certificate_configuration_id = None;

let completion = course_module_completions.get(&module.id);
let passed = completion.map(|x| x.passed);
if module.certification_enabled && passed == Some(true) {
// If passed, show the user the default certificate configuration id so that they can generate their certificate.
let default_certificate_configuration = all_default_certificate_configurations
.iter()
.find(|x| x.requirements.course_module_ids.contains(&module.id));
dbg!(&default_certificate_configuration);
dbg!(&all_default_certificate_configurations);
if let Some(default_certificate_configuration) = default_certificate_configuration {
certificate_configuration_id = Some(
default_certificate_configuration
.certificate_configuration
.id,
);
}
}
UserModuleCompletionStatus {
completed: completion.is_some(),
default: module.is_default_module(),
module_id: module.id,
name: module.name.unwrap_or_else(|| course.name.clone()),
order_number: module.order_number,
passed: completion.map(|x| x.passed),
passed,
grade: completion.and_then(|x| x.grade),
prerequisite_modules_completed: completion
.map_or(false, |x| x.prerequisite_modules_completed),
enable_registering_completion_to_uh_open_university: module
.enable_registering_completion_to_uh_open_university,
certification_enabled: module.certification_enabled,
certificate_configuration_id,
}
})
.collect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ async fn get_module_completions_for_course_instance(
module_completion_statuses.iter_mut().for_each(|module| {
if !module.prerequisite_modules_completed {
module.completed = false;
module.certificate_configuration_id = None;
}
});
token.authorized_ok(web::Json(module_completion_statuses))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ async fn update_certificate_configuration_inner(
file_store: &dyn FileStore,
user: AuthUser,
) -> Result<Vec<Uuid>, ControllerError> {
let mut tx = conn.begin().await?;
let mut files_to_delete = vec![];

let metadata = payload.metadata.into_inner();
Expand All @@ -147,7 +148,7 @@ async fn update_certificate_configuration_inner(
info!("Saving new background svg file");
// upload new background svg
let (id, path) = file_uploading::upload_certificate_svg(
conn,
&mut tx,
background_svg_file_name,
content,
file_store,
Expand All @@ -163,7 +164,7 @@ async fn update_certificate_configuration_inner(
info!("Saving new overlay svg file");
// upload new overlay svg
let (id, path) = file_uploading::upload_certificate_svg(
conn,
&mut tx,
overlay_svg_file_name,
content,
file_store,
Expand All @@ -187,7 +188,7 @@ async fn update_certificate_configuration_inner(

let existing_configuration =
models::certificate_configurations::get_default_configuration_by_course_module_and_course_instance(
conn,
&mut tx,
metadata.course_module_id,
metadata.course_instance_id,
)
Expand Down Expand Up @@ -280,10 +281,20 @@ async fn update_certificate_configuration_inner(
};
if let Some(existing_configuration) = existing_configuration {
// update existing config
models::certificate_configurations::update(conn, existing_configuration.id, &conf).await?;
models::certificate_configurations::update(&mut tx, existing_configuration.id, &conf)
.await?;
} else {
models::certificate_configurations::insert(conn, &conf).await?;
let inserted_configuration =
models::certificate_configurations::insert(&mut tx, &conf).await?;
models::certificate_configuration_to_requirements::insert(
&mut tx,
inserted_configuration.id,
Some(metadata.course_module_id),
metadata.course_instance_id,
)
.await?;
}
tx.commit().await?;
Ok(files_to_delete)
}

Expand All @@ -294,9 +305,9 @@ pub struct CertificateGenerationRequest {
}

/**
POST `/api/v0/main-frontend/course-modules/generate-certificate`
POST `/api/v0/main-frontend/certificates/generate`
Generates a certificate for completing a course module.
Generates a certificate for a given certificate configuration id.
*/
#[generated_doc]
#[instrument(skip(pool))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,9 @@ fn models() {
prerequisite_modules_completed: false,
enable_registering_completion_to_uh_open_university: true,
certification_enabled: false,
certificate_configuration_id: Some(
Uuid::parse_str("cf48b8ad-0fb5-47a4-a4bc-dcc57d66a439").unwrap()
)
},
UserModuleCompletionStatus {
completed: true,
Expand All @@ -1330,6 +1333,9 @@ fn models() {
prerequisite_modules_completed: false,
enable_registering_completion_to_uh_open_university: false,
certification_enabled: false,
certificate_configuration_id: Some(
Uuid::parse_str("2e831797-328d-4fda-b37a-1e24faaefa06").unwrap()
)
}
]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1958,10 +1958,11 @@ pub async fn seed_sample_course(
overlay_svg_path: None,
overlay_svg_file_upload_id: None,
};
certificate_configurations::insert(&mut conn, &configuration).await?;
let database_configuration =
certificate_configurations::insert(&mut conn, &configuration).await?;
certificate_configuration_to_requirements::insert(
&mut conn,
configuration.id,
database_configuration.id,
Some(default_module.id),
Some(default_instance.id),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useQuery } from "@tanstack/react-query"
import { useQuery, UseQueryResult } from "@tanstack/react-query"
import { TFunction } from "i18next"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"

import { fetchCertificate, generateCertificate } from "../../services/backend/certificates"
import { fetchCourseModule } from "../../services/backend/course-modules"
import { getCourse } from "../../services/backend/courses"
import { Course, CourseModule } from "../../shared-module/bindings"
import Button from "../../shared-module/components/Button"
import ErrorBanner from "../../shared-module/components/ErrorBanner"
import TextField from "../../shared-module/components/InputFields/TextField"
Expand All @@ -15,34 +17,37 @@ import useQueryParameter from "../../shared-module/hooks/useQueryParameter"
import useToastMutation from "../../shared-module/hooks/useToastMutation"
import useUserInfo from "../../shared-module/hooks/useUserInfo"
import dontRenderUntilQueryParametersReady from "../../shared-module/utils/dontRenderUntilQueryParametersReady"
import { assertNotNullOrUndefined } from "../../shared-module/utils/nullability"
import withErrorBoundary from "../../shared-module/utils/withErrorBoundary"

const ModuleCertificate: React.FC<React.PropsWithChildren<void>> = () => {
const { t } = useTranslation()
const router = useRouter()

const certificateConfigurationId = useQueryParameter("ccid")
// Used to generate the right title
const moduleId = useQueryParameter("module")
const courseInstanceId = useQueryParameter("instance")
const userInfo = useUserInfo()
const [nameOnCertificate, setNameOnCertificate] = useState("")
useEffect(() => {
if (!router || !router.isReady) {
return
}
fetchCertificate(moduleId, courseInstanceId).then((certificate) => {
fetchCertificate(certificateConfigurationId).then((certificate) => {
if (certificate !== null) {
// found existing certificate, redirect
router.replace(`/certificates/validate/${certificate.verification_id}`)
}
})
}, [courseInstanceId, moduleId, router])
}, [certificateConfigurationId, router])
const courseAndModule = useQuery({
queryKey: ["course-module", moduleId],
queryFn: async () => {
const courseModule = await fetchCourseModule(moduleId)
const courseModule = await fetchCourseModule(assertNotNullOrUndefined(moduleId))
const course = await getCourse(courseModule.course_id)
return { module: courseModule, course }
},
enabled: !!moduleId,
})

useEffect(() => {
Expand All @@ -54,7 +59,7 @@ const ModuleCertificate: React.FC<React.PropsWithChildren<void>> = () => {
}, [userInfo.isSuccess, userInfo.data, nameOnCertificate])
const generateCertificateMutation = useToastMutation(
() => {
return generateCertificate(moduleId, courseInstanceId, nameOnCertificate)
return generateCertificate(certificateConfigurationId, nameOnCertificate)
},
{ notify: true, method: "POST" },
{
Expand All @@ -71,16 +76,7 @@ const ModuleCertificate: React.FC<React.PropsWithChildren<void>> = () => {
{userInfo.isPending || (courseAndModule.isPending && <Spinner variant={"medium"} />)}
{courseAndModule.isSuccess && (
<>
<h2>
{courseAndModule.data.module.name
? t("generate-a-certificate-for-completing-the-module-of-the-course", {
module: courseAndModule.data.module.name,
course: courseAndModule.data.course.name,
})
: t("generate-a-certificate-for-completing-course", {
course: courseAndModule.data.course.name,
})}
</h2>
<h2>{getHeaderContent(t, courseAndModule, moduleId)}</h2>
<div>{t("certificate-generation-instructions")}</div>
<hr />
<TextField
Expand Down Expand Up @@ -111,6 +107,28 @@ const ModuleCertificate: React.FC<React.PropsWithChildren<void>> = () => {
)
}

function getHeaderContent(
t: TFunction,
courseAndModule: UseQueryResult<{
module: CourseModule
course: Course
}>,
moduleId: string | null | undefined,
): string {
if (moduleId === null || moduleId === undefined || !courseAndModule.data) {
return "todo"
}
if (courseAndModule.data.module.name) {
return t("generate-a-certificate-for-completing-the-module-of-the-course", {
module: courseAndModule.data.module.name,
course: courseAndModule.data.course.name,
})
}
return t("generate-a-certificate-for-completing-course", {
course: courseAndModule.data.course.name,
})
}

export default withErrorBoundary(
withSignedIn(dontRenderUntilQueryParametersReady(ModuleCertificate)),
)
Loading

0 comments on commit 226df54

Please sign in to comment.