Skip to content

Commit

Permalink
Fixes 344 (#1259)
Browse files Browse the repository at this point in the history
* Offer certificates to students that are not tied to course instances

* Make sure exercise stays on viewport after changing view
  • Loading branch information
nygrenh authored Apr 8, 2024
1 parent 52cf3d3 commit 617452a
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useQueryClient } from "@tanstack/react-query"
import { CheckCircle, PlusHeart } from "@vectopus/atlas-icons-react"
import { produce } from "immer"
import { useRouter } from "next/router"
import { useContext, useEffect, useId, useReducer, useState } from "react"
import { useContext, useEffect, useId, useReducer, useRef, useState } from "react"
import { useTranslation } from "react-i18next"

import { BlockRendererProps } from "../.."
Expand Down Expand Up @@ -134,6 +134,7 @@ export const getExerciseBlockBeginningScrollingId = (exerciseId: string) => exer
const ExerciseBlock: React.FC<
React.PropsWithChildren<BlockRendererProps<ExerciseBlockAttributes>>
> = (props) => {
const sectionRef = useRef(null)
const exerciseTitleId = useId()
const router = useRouter()
const returnTo = useCurrentPagePathForReturnTo(router.asPath)
Expand Down Expand Up @@ -192,6 +193,7 @@ const ExerciseBlock: React.FC<
signedIn: Boolean(loginState.signedIn),
})
await getCourseMaterialExercise.refetch()
makeSureComponentStaysVisibleAfterChangingView(sectionRef)
},
},
)
Expand All @@ -203,6 +205,7 @@ const ExerciseBlock: React.FC<
// eslint-disable-next-line i18next/no-literal-string
throw new Error("No data for the try again view")
}
makeSureComponentStaysVisibleAfterChangingView(sectionRef)
dispatch({
type: "tryAgain",
payload: data,
Expand All @@ -223,6 +226,7 @@ const ExerciseBlock: React.FC<
})
setAnswers(a)
}
makeSureComponentStaysVisibleAfterChangingView(sectionRef)
},
{
notify: false,
Expand Down Expand Up @@ -326,6 +330,7 @@ const ExerciseBlock: React.FC<
`}
id={getExerciseBlockBeginningScrollingId(id)}
aria-labelledby={exerciseTitleId}
ref={sectionRef}
>
<div>
<div>
Expand Down Expand Up @@ -722,4 +727,27 @@ const ExerciseBlock: React.FC<
)
}

/**
* The previous view might have been a lot taller than the next view, which would cause the browser to jump down as the view changes and the execise block gets a lot shorter. This function makes sure that the exercise block will stay visible upto half a second after the view has changed.
*/
function makeSureComponentStaysVisibleAfterChangingView(ref: React.RefObject<HTMLElement>) {
if (!ref.current) {
return
}
function scrollToViewIfNeeded() {
if (!ref.current) {
return
}
const boundingBox = ref.current.getBoundingClientRect()
if (boundingBox.bottom < 0) {
ref.current.scrollIntoView()
}
}
setTimeout(scrollToViewIfNeeded, 100)
setTimeout(scrollToViewIfNeeded, 200)
setTimeout(scrollToViewIfNeeded, 300)
setTimeout(scrollToViewIfNeeded, 400)
setTimeout(scrollToViewIfNeeded, 500)
}

export default withErrorBoundary(ExerciseBlock)

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ impl CertificateAllRequirements {
self.course_module_ids.len() == 1 && self.course_instance_ids.len() == 1
}

pub fn requires_only_one_course_module_and_does_not_require_course_instance(&self) -> bool {
self.course_module_ids.len() == 1 && self.course_instance_ids.is_empty()
}

/** Checks if the user has completed all requirements to be eligible for a certificate. */
pub async fn has_user_completed_all_requirements(
&self,
Expand Down
59 changes: 59 additions & 0 deletions services/headless-lms/models/src/certificate_configurations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,65 @@ WHERE cctr.course_instance_id = $1
Ok(res)
}

/** Finds all configurations that applies to any instance of a course module and requires only one course module. **/
pub async fn get_all_certifcate_configurations_requiring_only_one_module_and_no_course_instance(
conn: &mut PgConnection,
course_module_ids: &[Uuid],
) -> ModelResult<Vec<CertificateConfigurationAndRequirements>> {
let mut res = Vec::new();
let cadidate_certificate_configurations = sqlx::query_as!(
CertificateConfiguration,
r#"
SELECT cc.id,
cc.created_at,
cc.updated_at,
cc.deleted_at,
cc.certificate_owner_name_y_pos,
cc.certificate_owner_name_x_pos,
cc.certificate_owner_name_font_size,
cc.certificate_owner_name_text_color,
cc.certificate_owner_name_text_anchor as "certificate_owner_name_text_anchor: _",
cc.certificate_validate_url_y_pos,
cc.certificate_validate_url_x_pos,
cc.certificate_validate_url_font_size,
cc.certificate_validate_url_text_color,
cc.certificate_validate_url_text_anchor as "certificate_validate_url_text_anchor: _",
cc.certificate_date_y_pos,
cc.certificate_date_x_pos,
cc.certificate_date_font_size,
cc.certificate_date_text_color,
cc.certificate_date_text_anchor as "certificate_date_text_anchor: _",
cc.certificate_locale,
cc.paper_size as "paper_size: _",
cc.background_svg_path,
cc.background_svg_file_upload_id,
cc.overlay_svg_path,
cc.overlay_svg_file_upload_id
FROM certificate_configurations cc
JOIN certificate_configuration_to_requirements cctr ON cc.id = cctr.certificate_configuration_id
WHERE cctr.course_module_id = ANY($1)
AND cctr.course_instance_id IS NULL
AND cc.deleted_at IS NULL
AND cctr.deleted_at IS NULL
"#,
course_module_ids,
)
.fetch_all(&mut *conn)
.await?;
for certificate_configuration in &cadidate_certificate_configurations {
let requirements =
get_all_requirements_for_certificate_configuration(conn, certificate_configuration.id)
.await?;
if requirements.requires_only_one_course_module_and_does_not_require_course_instance() {
res.push(CertificateConfigurationAndRequirements {
certificate_configuration: certificate_configuration.clone(),
requirements,
});
}
}
Ok(res)
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts_rs", derive(TS))]
pub struct DatabaseCertificateConfiguration {
Expand Down
14 changes: 14 additions & 0 deletions services/headless-lms/models/src/library/progressing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,7 @@ pub async fn get_user_module_completion_statuses_for_course_instance(
let course_id = course_instances::get_course_id(conn, course_instance_id).await?;
let course = courses::get_course(conn, course_id).await?;
let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
let course_module_ids = course_modules.iter().map(|x| x.id).collect::<Vec<_>>();
let course_module_completions: HashMap<Uuid, CourseModuleCompletion> =
course_module_completions::get_all_by_course_instance_and_user_id(
conn,
Expand All @@ -714,6 +715,7 @@ pub async fn get_user_module_completion_statuses_for_course_instance(
.collect();

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

let course_module_completion_statuses = course_modules
.into_iter()
Expand All @@ -733,6 +735,18 @@ pub async fn get_user_module_completion_statuses_for_course_instance(
.certificate_configuration
.id,
);
} else {
// If instance-speficic certificate configuration is not found, try to find a configuration that is not instance-specific.
let matching_certificate_configuration = all_certifcate_configurations_requiring_only_one_module_and_no_course_instance
.iter()
.find(|x| x.requirements.course_module_ids.contains(&module.id));
if let Some(matching_certificate_configuration) = matching_certificate_configuration {
certificate_configuration_id = Some(
matching_certificate_configuration
.certificate_configuration
.id,
);
}
}
}
UserModuleCompletionStatus {
Expand Down
4 changes: 2 additions & 2 deletions shared-module/src/locales/fi/main-frontend.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@
"generate-a-certificate": "Luo sertifikaatti",
"generate-a-certificate-for-completing-course": "Luo sertifikaatti kurssin {{course}} suorituksesta",
"generate-a-certificate-for-completing-the-module-of-the-course": "Luo sertifikaatti moduulin {{module}} suorituksesta kurssilla {{course}}",
"generating-new-certificates-disabled": "Uusien sertifikaattien luonti sallittu",
"generating-new-certificates-enabled": "Uusien sertifikaattien luonti kielletty",
"generating-new-certificates-disabled": "Uusien sertifikaattien luonti kielletty",
"generating-new-certificates-enabled": "Uusien sertifikaattien luonti sallittu",
"give-custom-points-confirmation": "Oletko varma että haluat antaa {{ custom-points }} pistettä?",
"given-enough-peer-reviews": "Annettu tarpeeksi vertaisarviointeja",
"given-number-data": "Annettu numero palaute",
Expand Down

0 comments on commit 617452a

Please sign in to comment.