Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix JSON payload size when publishing exam results, and misc fixes #1341

Merged
merged 7 commits into from
Dec 2, 2024

Conversation

nygrenh
Copy link
Member

@nygrenh nygrenh commented Dec 2, 2024

Summary by CodeRabbit

  • New Features

    • Introduced the PartnerEditor and PartnerSave components for enhanced editing and saving of partner-related content in the WordPress block editor.
    • Added a new block configuration for the "Partner Section" to streamline content creation.
    • Expanded ContentRenderer capabilities to include the new PartnersBlock.
    • Updated JoinCoursePage to ensure only signed-in users can access the page and handle errors more effectively.
  • Bug Fixes

    • Improved error handling in the release_grades function to verify user exercise states and grading decisions.
  • Refactor

    • Streamlined grading logic in the GradingPage component for better performance and clarity.
    • Renamed components and types related to sponsorship to align with new terminology, enhancing code readability.

Copy link
Contributor

coderabbitai bot commented Dec 2, 2024

Walkthrough

This pull request introduces several new components and functionalities related to a "Partner" block in a WordPress block editor context. Key additions include the PartnerEditor and PartnerSave components for editing and saving the block, along with a configuration file for the block itself. The changes also include updates to existing components and services to integrate the new "Partner" terminology, replacing previous "Sponsor" references. Additionally, new SQL queries and functions for handling data retrieval in the backend have been added, enhancing the overall functionality of the application.

Changes

File Path Change Summary
services/cms/src/blocks/Partners/PartnersEditor.tsx New component PartnerEditor added for editing the Partner block with internationalization support.
services/cms/src/blocks/Partners/PartnersSave.tsx New component PartnerSave added for saving the Partner block content.
services/cms/src/blocks/Partners/index.tsx New block configuration for "Partner Section" linking PartnerEditor and PartnerSave.
services/cms/src/blocks/index.tsx Added import for PartnersBlock and new entries in block type mappings for pages and front pages.
services/course-material/src/components/ContentRenderer/index.tsx Updated to include PartnersBlock in the block renderer map.
services/course-material/src/components/ContentRenderer/moocfi/PartnersBlock.tsx Renamed component from Sponsor to Partner and updated associated types.
services/headless-lms/models/.sqlx/query-2c8c3495c6e898d23d0a73cb3861d511b64e8a06fb5cb8382c9457d494f03d2d.json New SQL query for retrieving teacher grading decisions.
services/headless-lms/models/src/teacher_grading_decisions.rs New function get_by_ids added for batch retrieval of grading decisions.
services/headless-lms/models/src/user_exercise_states.rs New function get_by_ids added for batch retrieval of user exercise states; existing function updated.
services/headless-lms/server/src/controllers/main_frontend/exams.rs Updated release_grades function to simplify input data structure and improve transaction handling.
services/main-frontend/src/pages/join.tsx Wrapped JoinCoursePage with authentication and error boundary handling.
services/main-frontend/src/pages/manage/exams/[id]/questions.tsx Streamlined GradingPage component logic; removed and refactored several functions.
services/main-frontend/src/services/backend/exams.ts Updated releaseGrades function parameter type to accept grading decision IDs.
shared-module/packages/common/src/components/Partner.tsx Comprehensive renaming from "Sponsor" to "Partner" for associated components and types.
services/headless-lms/models/.sqlx/query-67a13986d2adbce227c6ebbb7402f7e18980990010b4b784f6a6257a76b40b56.json New SQL query for retrieving user exercise states.

Possibly related PRs

  • More element of AI components #1333: The main PR introduces the PartnerEditor component, while the retrieved PR More element of AI components #1333 introduces the AsideWithImageEditor component. Both components are designed for use within the WordPress block editor context and utilize similar structures, including the InnerBlocks feature and the useTranslation hook for internationalization.

🐰 In the garden where partners play,
New blocks and queries come out to stay.
From sponsors to partners, we hop with glee,
Building a world that's vibrant and free!
With each little change, our code blooms bright,
Together we flourish, in day and in night! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Outside diff range and nitpick comments (7)
services/main-frontend/src/pages/manage/exams/[id]/questions.tsx (2)

189-194: Use .forEach instead of .map for side effects

In the totalPublished function, .map is used for side effects without utilizing the returned array. This is not the intended use of .map and can lead to confusion. Use .forEach when you are not using the result array.

Apply this diff to use .forEach instead:

-      submissions.map((sub) => {
+      submissions.forEach((sub) => {

Line range hint 83-84: Separate statements in onSuccess handler for clarity

The onSuccess handler uses the comma operator to separate statements, which can reduce readability and may lead to unintended behavior.

Consider separating the statements explicitly:

-          getAllSubmissions.refetch(), checkPublishable()
+          getAllSubmissions.refetch()
+          checkPublishable()
services/cms/src/blocks/Partners/index.tsx (1)

9-16: Inconsistent naming: Consider using plural form for consistency

The block is titled "Partner Section" while related files and components use "Partners" (plural). For consistency and clarity, consider renaming the block title to "Partners Section" to match the naming convention used elsewhere.

Apply this diff:

-  title: "Partner Section",
+  title: "Partners Section",
shared-module/packages/common/src/components/Partner.tsx (2)

78-81: Strengthen type safety for Logo interface

Consider making the Logo interface more strict by requiring specific attribute types.

Apply this diff:

 interface Logo {
   clientId: string
   attributes: {
-    url: string
-    alt: string
+    url: string & { __brand: 'ImageURL' }
+    alt: string & { __brand: 'ImageAlt' }
   }
 }

Line range hint 36-48: Add ARIA attributes for better accessibility

The PartnerBox should have appropriate ARIA attributes for better screen reader support.

Apply this diff:

 const PartnerBox = styled.div`
   display: flex;
   flex-wrap: wrap;
   flex-direction: row;
   justify-content: center;
   align-items: center;
   align-content: center;
   gap: 10px;
+  role: "list";
+  aria-label: "Partner logos";
   img {
     display: flex;
     align-self: center;
   }
 `
services/main-frontend/src/services/backend/exams.ts (1)

98-99: LGTM: Optimized payload size for grade release

Excellent optimization! The function now sends only the necessary IDs instead of full submission objects, which will significantly reduce the JSON payload size when releasing grades.

Consider adding JSDoc to document this optimization:

+/**
+ * Releases grades for an exam by sending only the teacher grading decision IDs
+ * instead of full submission objects to optimize payload size.
+ * @param examId - The ID of the exam
+ * @param teacherGradingDecisionIds - Array of teacher grading decision IDs to release
+ */
 export const releaseGrades = async (examId: string, teacherGradingDecisionIds: string[]) => {
services/headless-lms/models/src/teacher_grading_decisions.rs (1)

250-275: LGTM! Consider adding ORDER BY for consistent results.

The implementation is correct and follows the established patterns. For consistency with other query functions in this file, consider adding an ORDER BY clause to ensure deterministic results.

 SELECT id,
   user_exercise_state_id,
   created_at,
   updated_at,
   deleted_at,
   score_given,
   teacher_decision AS "teacher_decision: _",
   justification,
   hidden
 FROM teacher_grading_decisions
 WHERE id = ANY($1)
   AND deleted_at IS NULL
+ORDER BY created_at DESC
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 27fa855 and 7b175c0.

📒 Files selected for processing (15)
  • services/cms/src/blocks/Partners/PartnersEditor.tsx (1 hunks)
  • services/cms/src/blocks/Partners/PartnersSave.tsx (1 hunks)
  • services/cms/src/blocks/Partners/index.tsx (1 hunks)
  • services/cms/src/blocks/index.tsx (3 hunks)
  • services/course-material/src/components/ContentRenderer/index.tsx (2 hunks)
  • services/course-material/src/components/ContentRenderer/moocfi/PartnersBlock.tsx (1 hunks)
  • services/headless-lms/models/.sqlx/query-2c8c3495c6e898d23d0a73cb3861d511b64e8a06fb5cb8382c9457d494f03d2d.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-64749812d6f37a24c02dd98db9bdde37d63c7136d4248f9837c095f27adc8bfd.json (1 hunks)
  • services/headless-lms/models/src/teacher_grading_decisions.rs (1 hunks)
  • services/headless-lms/models/src/user_exercise_states.rs (1 hunks)
  • services/headless-lms/server/src/controllers/main_frontend/exams.rs (2 hunks)
  • services/main-frontend/src/pages/join.tsx (2 hunks)
  • services/main-frontend/src/pages/manage/exams/[id]/questions.tsx (4 hunks)
  • services/main-frontend/src/services/backend/exams.ts (1 hunks)
  • shared-module/packages/common/src/components/Partner.tsx (4 hunks)
🔇 Additional comments (12)
services/cms/src/blocks/Partners/PartnersSave.tsx (1)

1-11: Component PartnerSave is properly implemented

The PartnerSave component correctly uses InnerBlocks.Content to render any child blocks, ensuring that nested content is saved and rendered properly.

services/cms/src/blocks/Partners/PartnersEditor.tsx (2)

13-46: LGTM! Well-structured component with proper styling and translations

The component follows React best practices, properly handles translations, and uses emotion for styling. The BlockWrapper integration ensures proper WordPress block editor functionality.


11-11: Consider allowing additional block types

The current implementation only allows 'core/image' blocks. Consider if other block types might be useful for partners' content, such as links or descriptions.

Let's check what block types are commonly used in similar contexts:

✅ Verification successful

Based on the search results, I can now generate the final response as we have sufficient context about block type restrictions across the codebase:

The restriction to only 'core/image' blocks is appropriate

Looking at similar components in the codebase, the Partners block follows a pattern where blocks with specific visual purposes have intentionally limited block types. For example:

  • AsideWithImage also only allows "core/image"
  • TableBox only allows "core/table"
  • LogoLink allows "core/image" and "core/columns"

The Partners block appears to be a visual component for displaying partner logos, similar to LogoLink and AsideWithImage. The restriction to only image blocks maintains the component's focused purpose of displaying partner logos in a consistent manner.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other block type restrictions in the codebase
rg "ALLOWED.*BLOCKS.*=.*\[.*\]" -A 2

Length of output: 9830

services/headless-lms/models/.sqlx/query-2c8c3495c6e898d23d0a73cb3861d511b64e8a06fb5cb8382c9457d494f03d2d.json (2)

41-44: Verify completeness of teacher_decision_type enum

Please verify that all possible decision types are covered in the enum.

Let's check for other occurrences of these decision types:

✅ Verification successful

Enum values for teacher_decision_type are complete and consistent

Based on the search results, the enum values in the SQL type definition are complete and consistent across the codebase:

  • The enum values are defined in the migration file (20220721192205_teacher_grading_decision.up.sql)
  • These values are used consistently in the Rust models (teacher_grading_decisions.rs)
  • The frontend components (AnswersRequiringAttentionItem.tsx) and translations (in multiple languages) align with these decision types
  • No other decision types were found in the codebase
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other uses of these decision types
rg -A 2 "teacher_decision.*type|full-points|zero-points|custom-points|suspected-plagiarism"

Length of output: 13521


2-3: Consider adding an index for performance

The query filters on id and deleted_at. Consider adding a composite index if one doesn't exist already.

Let's check the existing indexes:

services/main-frontend/src/pages/join.tsx (2)

13-13: LGTM: Enhanced component security and error handling

Good addition of authentication check and error boundary wrapper to protect the join course functionality.

Also applies to: 16-16


88-88: LGTM: Proper implementation of HOC composition

The order of HOC composition is correct - withErrorBoundary wraps withSignedIn, which wraps JoinCoursePage.

services/headless-lms/models/.sqlx/query-64749812d6f37a24c02dd98db9bdde37d63c7136d4248f9837c095f27adc8bfd.json (2)

2-3: LGTM: Efficient batch fetching query design

The query efficiently fetches multiple user exercise states using PostgreSQL's ANY operator with an array parameter, which is ideal for reducing payload size when dealing with multiple records.


51-93: Verify the completeness of enum values

The custom enum types (grading_progress, activity_progress, reviewing_stage) are crucial for exam results. Please verify that all possible states are covered.

✅ Verification successful

Let me gather more information about the enum definitions to ensure completeness.


Let me search for the Rust enum definitions to verify the completeness.


Let me search for one more piece of information to complete the verification.


Let me try one final search to get the complete ReviewingStage enum definition.


Based on all the gathered information, I can now generate the final response:

✓ Enum values are complete and consistent

The enum values in the SQL type definitions match their corresponding Rust implementations:

  • grading_progress: fully-graded, pending, pending-manual, failed, not-ready
  • activity_progress: initialized, started, in-progress, submitted, completed
  • reviewing_stage: not_started, peer_review, self_review, waiting_for_peer_reviews, waiting_for_manual_grading, reviewed_and_locked

The values are used consistently across the codebase, as evidenced by the SQL migrations and Rust implementations. The enums cover all necessary states for exam results tracking.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any enum value references in the codebase that might not be covered

# Search for grading progress references
echo "Checking grading progress values:"
rg -i "grading.*progress.*=.*['\"](\w+)['\"]" --type rust

# Search for activity progress references
echo "Checking activity progress values:"
rg -i "activity.*progress.*=.*['\"](\w+)['\"]" --type rust

# Search for reviewing stage references
echo "Checking reviewing stage values:"
rg -i "reviewing.*stage.*=.*['\"](\w+)['\"]" --type rust

Length of output: 1249


Script:

#!/bin/bash
# Search for enum definitions in Rust files
echo "Searching for enum definitions:"
ast-grep --pattern 'enum $name {
  $$$
}'

# Search for SQL enum type definitions
echo "Searching for SQL enum types:"
fd -e sql . -x cat {}

Length of output: 72644


Script:

#!/bin/bash
# Search for enum definitions in Rust files
echo "Searching for enum definitions in user_exercise_states.rs:"
rg -A 10 "^pub enum (GradingProgress|ActivityProgress|ReviewingStage)" services/headless-lms/models/src/user_exercise_states.rs

# Search for SQL enum type definitions
echo "Searching for SQL enum type definitions:"
rg -A 10 "CREATE TYPE (grading_progress|activity_progress|reviewing_stage) AS ENUM" --type sql

Length of output: 3863


Script:

#!/bin/bash
# Search for the rest of the ReviewingStage enum definition
echo "Searching for complete ReviewingStage enum:"
rg -A 15 "pub enum ReviewingStage" services/headless-lms/models/src/user_exercise_states.rs

# Search for reviewing_stage enum type definition in SQL
echo "Searching for reviewing_stage enum type definition:"
rg -B 5 -A 10 "CREATE TYPE reviewing_stage AS ENUM" --type sql

Length of output: 1454


Script:

#!/bin/bash
# Search for the complete ReviewingStage enum with more context
echo "Searching for complete ReviewingStage enum with more context:"
rg -A 20 "pub enum ReviewingStage" services/headless-lms/models/src/user_exercise_states.rs

# Also search for reviewing_stage in the SQL file
echo "Searching for reviewing_stage in SQL file:"
rg -A 10 "reviewing_stage" services/headless-lms/migrations/20210609111555_add_user_exercise_states.up.sql

Length of output: 1847

services/cms/src/blocks/index.tsx (1)

40-40: LGTM! Block registration looks good.

The PartnersBlock is correctly imported and registered in both block type maps following the established pattern.

Also applies to: 94-94, 117-117

services/course-material/src/components/ContentRenderer/index.tsx (1)

67-67: LGTM! Block renderer registration looks good.

The PartnersBlock is correctly imported and registered in the block renderer map following the established pattern and maintaining alphabetical order.

Also applies to: 160-160

services/headless-lms/models/src/user_exercise_states.rs (1)

681-681: LGTM! The GROUP BY clause addition is correct.

The addition of GROUP BY user_id ensures that points are correctly aggregated per user, maintaining data consistency.

Also applies to: 682-699

Comment on lines 303 to 335
let teacher_grading_decisions =
models::teacher_grading_decisions::get_by_ids(&mut conn, &teacher_grading_decision_ids)
.await?;
teacher_grading_decisions::update_teacher_grading_decision_hidden_field(
&mut conn,
submission.teacher_grading_decision.unwrap().id,
false,
)

let user_exercise_state_mapping = models::user_exercise_states::get_by_ids(
&mut conn,
&teacher_grading_decisions
.iter()
.map(|x| x.user_exercise_state_id)
.collect::<Vec<Uuid>>(),
)
.await?
.into_iter()
.map(|x| (x.id, x))
.collect::<HashMap<Uuid, UserExerciseState>>();

let mut tx = conn.begin().await?;
for teacher_grading_decision in teacher_grading_decisions.iter() {
let user_exercise_state = user_exercise_state_mapping
.get(&teacher_grading_decision.user_exercise_state_id)
.ok_or_else(|| {
ControllerError::new(
ControllerErrorType::InternalServerError,
"User exercise state not found for a teacher grading decision",
None,
)
})?;

teacher_grading_decisions::update_teacher_grading_decision_hidden_field(
&mut tx,
teacher_grading_decision.id,
false,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate that teacher grading decisions belong to the specified exam

Currently, the code retrieves teacher_grading_decisions based on the provided IDs but does not verify that these decisions are associated with the specified exam_id. This could lead to unintended behavior or security issues if incorrect or malicious IDs are provided. Consider adding a validation step to ensure all teacher_grading_decisions belong to the specified exam.

Apply this diff to add the validation:

+        // Validate that all teacher grading decisions belong to the specified exam
+        for decision in &teacher_grading_decisions {
+            if decision.exam_id != *exam_id {
+                return Err(ControllerError::new(
+                    ControllerErrorType::BadRequest,
+                    "Teacher grading decision does not belong to the specified exam.",
+                    None,
+                ));
+            }
+        }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +154 to +164
const totalAnswered = useCallback(
(id: string) => {
const submissions = allSubmissionsList?.[id]
if (submissions) {
return submissions.length
} else {
return "0"
}
},
[allSubmissionsList],
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistent return types in totalAnswered function

The totalAnswered function returns a number when submissions exist but returns a string "0" when there are no submissions. This inconsistency can lead to type-related issues or unexpected behavior.

Apply this diff to return a consistent number:

-      return "0"
+      return 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const totalAnswered = useCallback(
(id: string) => {
const submissions = allSubmissionsList?.[id]
if (submissions) {
return submissions.length
} else {
return "0"
}
},
[allSubmissionsList],
)
const totalAnswered = useCallback(
(id: string) => {
const submissions = allSubmissionsList?.[id]
if (submissions) {
return submissions.length
} else {
return 0
}
},
[allSubmissionsList],
)

Comment on lines +181 to +198
const totalPublished = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return <div>0</div>
}
const submissions = allSubmissionsList?.[id]
let count = 0
if (submissions) {
submissions.map((sub) => {
if (sub.teacher_grading_decision?.hidden === true) {
count = count + 1
}
})
}
return count
},
[allSubmissionsList, getExam.data?.grade_manually],
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistent return types in totalPublished function

The totalPublished function returns a JSX element <div> when grade_manually is false and a number otherwise. This inconsistency can cause issues when the returned value is used elsewhere expecting a consistent type.

Consider returning a number consistently and handle the rendering in the component where needed.

Apply this diff:

-      return <div>0</div>
+      return 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const totalPublished = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return <div>0</div>
}
const submissions = allSubmissionsList?.[id]
let count = 0
if (submissions) {
submissions.map((sub) => {
if (sub.teacher_grading_decision?.hidden === true) {
count = count + 1
}
})
}
return count
},
[allSubmissionsList, getExam.data?.grade_manually],
)
const totalPublished = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return 0
}
const submissions = allSubmissionsList?.[id]
let count = 0
if (submissions) {
submissions.map((sub) => {
if (sub.teacher_grading_decision?.hidden === true) {
count = count + 1
}
})
}
return count
},
[allSubmissionsList, getExam.data?.grade_manually],
)

Comment on lines +166 to +179
const totalGraded = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return <div>{totalAnswered(id)}</div>
}
const submissions = allSubmissionsList?.[id]
if (submissions) {
return submissions.filter((sub) => sub.teacher_grading_decision).length
} else {
return "0"
}
},
[getExam.data?.grade_manually, allSubmissionsList, totalAnswered],
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistent return types in totalGraded function

The totalGraded function returns a number in some cases and a JSX element <div> in others. This inconsistency can cause rendering issues or unexpected behavior in components that use this function.

Consider returning a consistent type (preferably a number) and handle the rendering separately in the JSX.

Apply this diff:

-      return <div>{totalAnswered(id)}</div>
+      return totalAnswered(id)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const totalGraded = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return <div>{totalAnswered(id)}</div>
}
const submissions = allSubmissionsList?.[id]
if (submissions) {
return submissions.filter((sub) => sub.teacher_grading_decision).length
} else {
return "0"
}
},
[getExam.data?.grade_manually, allSubmissionsList, totalAnswered],
)
const totalGraded = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return totalAnswered(id)
}
const submissions = allSubmissionsList?.[id]
if (submissions) {
return submissions.filter((sub) => sub.teacher_grading_decision).length
} else {
return "0"
}
},
[getExam.data?.grade_manually, allSubmissionsList, totalAnswered],
)

Comment on lines +99 to +152
const gradedCheck = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return (
<div
className={css`
color: #32bea6;
`}
>
{t("status-graded")}
</div>
)
} else if (countGraded === 0) {
return (
<div
className={css`
color: #f76d82;
`}
>
{t("status-ungraded")}
</div>
)
} else if (submissions.length > countGraded) {
return (
<div
className={css`
color: #ffce54;
`}
>
{t("status-in-progress")}
{t("label-graded-automatically")}
</div>
)
}
} else {
return " "
}
}

const totalAnswered = (id: string) => {
const submissions = allSubmissionsList?.[id]
if (submissions) {
return submissions.length
} else {
return "0"
}
}
const submissions = allSubmissionsList?.[id]
if (submissions) {
const countGraded = submissions.filter((sub) => sub.teacher_grading_decision).length
if (submissions.length === countGraded) {
return (
<div
className={css`
color: #32bea6;
`}
>
{t("status-graded")}
</div>
)
} else if (countGraded === 0) {
return (
<div
className={css`
color: #f76d82;
`}
>
{t("status-ungraded")}
</div>
)
} else if (submissions.length > countGraded) {
return (
<div
className={css`
color: #ffce54;
`}
>
{t("status-in-progress")}
</div>
)
}
} else {
return " "
}
},
[getExam.data, allSubmissionsList, t],
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistent return types in gradedCheck function

The gradedCheck function returns JSX elements in most cases but returns a string " " when no submissions are found. This inconsistency can cause rendering issues.

Consider returning a JSX element consistently, such as an empty <div> or <span>.

Apply this diff:

-        return " "
+        return <span>&nbsp;</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const gradedCheck = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return (
<div
className={css`
color: #32bea6;
`}
>
{t("status-graded")}
</div>
)
} else if (countGraded === 0) {
return (
<div
className={css`
color: #f76d82;
`}
>
{t("status-ungraded")}
</div>
)
} else if (submissions.length > countGraded) {
return (
<div
className={css`
color: #ffce54;
`}
>
{t("status-in-progress")}
{t("label-graded-automatically")}
</div>
)
}
} else {
return " "
}
}
const totalAnswered = (id: string) => {
const submissions = allSubmissionsList?.[id]
if (submissions) {
return submissions.length
} else {
return "0"
}
}
const submissions = allSubmissionsList?.[id]
if (submissions) {
const countGraded = submissions.filter((sub) => sub.teacher_grading_decision).length
if (submissions.length === countGraded) {
return (
<div
className={css`
color: #32bea6;
`}
>
{t("status-graded")}
</div>
)
} else if (countGraded === 0) {
return (
<div
className={css`
color: #f76d82;
`}
>
{t("status-ungraded")}
</div>
)
} else if (submissions.length > countGraded) {
return (
<div
className={css`
color: #ffce54;
`}
>
{t("status-in-progress")}
</div>
)
}
} else {
return " "
}
},
[getExam.data, allSubmissionsList, t],
)
const gradedCheck = useCallback(
(id: string) => {
if (!getExam.data?.grade_manually) {
return (
<div
className={css`
color: #32bea6;
`}
>
{t("label-graded-automatically")}
</div>
)
}
const submissions = allSubmissionsList?.[id]
if (submissions) {
const countGraded = submissions.filter((sub) => sub.teacher_grading_decision).length
if (submissions.length === countGraded) {
return (
<div
className={css`
color: #32bea6;
`}
>
{t("status-graded")}
</div>
)
} else if (countGraded === 0) {
return (
<div
className={css`
color: #f76d82;
`}
>
{t("status-ungraded")}
</div>
)
} else if (submissions.length > countGraded) {
return (
<div
className={css`
color: #ffce54;
`}
>
{t("status-in-progress")}
</div>
)
}
} else {
return <span>&nbsp;</span>
}
},
[getExam.data, allSubmissionsList, t],
)

props,
) => {
return (
<BreakFromCentered sidebar={false}>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Sponsor logos={props.data.innerBlocks as any} />
<Partner logos={props.data.innerBlocks as any} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid using any type assertion to maintain type safety

Casting props.data.innerBlocks to any defeats the benefits of TypeScript's type checking. It can lead to runtime errors that TypeScript is designed to prevent. Define the appropriate type for props.data.innerBlocks to maintain type safety.

For example:

-          <Partner logos={props.data.innerBlocks as any} />
+          <Partner logos={props.data.innerBlocks as LogoProps[]} />

Ensure that LogoProps is correctly defined and imported, and that innerBlocks conforms to this type.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +100 to +102
<PartnerLogo width={width} key={clientId}>
<img src={attributes.url} alt={attributes.alt} />
</SponsorLogo>
</PartnerLogo>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add loading and error handling for images

Consider adding loading="lazy" for better performance and fallback handling for failed image loads.

Apply this diff:

-              <img src={attributes.url} alt={attributes.alt} />
+              <img 
+                src={attributes.url} 
+                alt={attributes.alt} 
+                loading="lazy"
+                onError={(e) => {
+                  e.currentTarget.style.display = 'none';
+                }}
+              />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PartnerLogo width={width} key={clientId}>
<img src={attributes.url} alt={attributes.alt} />
</SponsorLogo>
</PartnerLogo>
<PartnerLogo width={width} key={clientId}>
<img
src={attributes.url}
alt={attributes.alt}
loading="lazy"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
</PartnerLogo>

Comment on lines 652 to 680
pub async fn get_by_ids(
conn: &mut PgConnection,
ids: &[Uuid],
) -> ModelResult<Vec<UserExerciseState>> {
let res = sqlx::query_as!(
UserExerciseState,
r#"
SELECT id,
user_id,
exercise_id,
course_instance_id,
exam_id,
created_at,
updated_at,
deleted_at,
score_given,
grading_progress AS "grading_progress: _",
activity_progress AS "activity_progress: _",
reviewing_stage AS "reviewing_stage: _",
selected_exercise_slide_id
FROM user_exercise_states
WHERE id = ANY($1)
"#,
&ids
)
.fetch_all(conn)
.await?;
Ok(res)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add deleted_at check to prevent returning soft-deleted records.

The query should include a WHERE clause to filter out soft-deleted records, consistent with other functions in this file.

Apply this diff to add the check:

 pub async fn get_by_ids(
     conn: &mut PgConnection,
     ids: &[Uuid],
 ) -> ModelResult<Vec<UserExerciseState>> {
     let res = sqlx::query_as!(
         UserExerciseState,
         r#"
 SELECT id,
   user_id,
   exercise_id,
   course_instance_id,
   exam_id,
   created_at,
   updated_at,
   deleted_at,
   score_given,
   grading_progress AS "grading_progress: _",
   activity_progress AS "activity_progress: _",
   reviewing_stage AS "reviewing_stage: _",
   selected_exercise_slide_id
 FROM user_exercise_states
-WHERE id = ANY($1)
+WHERE id = ANY($1)
+  AND deleted_at IS NULL
 "#,
         &ids
     )
     .fetch_all(conn)
     .await?;
     Ok(res)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub async fn get_by_ids(
conn: &mut PgConnection,
ids: &[Uuid],
) -> ModelResult<Vec<UserExerciseState>> {
let res = sqlx::query_as!(
UserExerciseState,
r#"
SELECT id,
user_id,
exercise_id,
course_instance_id,
exam_id,
created_at,
updated_at,
deleted_at,
score_given,
grading_progress AS "grading_progress: _",
activity_progress AS "activity_progress: _",
reviewing_stage AS "reviewing_stage: _",
selected_exercise_slide_id
FROM user_exercise_states
WHERE id = ANY($1)
"#,
&ids
)
.fetch_all(conn)
.await?;
Ok(res)
}
pub async fn get_by_ids(
conn: &mut PgConnection,
ids: &[Uuid],
) -> ModelResult<Vec<UserExerciseState>> {
let res = sqlx::query_as!(
UserExerciseState,
r#"
SELECT id,
user_id,
exercise_id,
course_instance_id,
exam_id,
created_at,
updated_at,
deleted_at,
score_given,
grading_progress AS "grading_progress: _",
activity_progress AS "activity_progress: _",
reviewing_stage AS "reviewing_stage: _",
selected_exercise_slide_id
FROM user_exercise_states
WHERE id = ANY($1)
AND deleted_at IS NULL
"#,
&ids
)
.fetch_all(conn)
.await?;
Ok(res)
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (3)
services/headless-lms/models/.sqlx/query-67a13986d2adbce227c6ebbb7402f7e18980990010b4b784f6a6257a76b40b56.json (1)

100-102: Consider adding a batch size limit.

The query accepts an array of UUIDs without a size limit. Consider adding a LIMIT clause or implementing application-level batching to prevent potential performance issues with large arrays.

services/headless-lms/server/src/controllers/main_frontend/exams.rs (2)

289-296: Documentation needs to be more specific about the response.

The function documentation should specify the expected response status codes and error conditions.

-POST `/api/v0/main-frontend/exam/:exam_id/release-grades` - Publishes grading results of an exam by updating user_exercise_states according to teacher_grading_decisons and changes teacher_grading_decisions hidden field to false. Takes teacher grading decision ids as input.
+POST `/api/v0/main-frontend/exam/:exam_id/release-grades` - Publishes grading results of an exam by updating user_exercise_states according to teacher_grading_decisons and changes teacher_grading_decisions hidden field to false.
+
+Input: Array of teacher grading decision IDs
+Returns: 200 OK on success
+Errors:
+- 400 Bad Request: If any teacher grading decision doesn't belong to the specified exam
+- 500 Internal Server Error: If user exercise state is not found for a teacher grading decision

319-346: Batch update operations for better performance.

Instead of updating records one by one in a loop, consider using batch operations for better performance.

     let mut tx = conn.begin().await?;
+    // Validate all decisions belong to the exam first
+    for teacher_grading_decision in teacher_grading_decisions.iter() {
+        let user_exercise_state = user_exercise_state_mapping
+            .get(&teacher_grading_decision.user_exercise_state_id)
+            .ok_or_else(|| {
+                ControllerError::new(
+                    ControllerErrorType::InternalServerError,
+                    "User exercise state not found for a teacher grading decision",
+                    None,
+                )
+            })?;
+
+        if user_exercise_state.exam_id != Some(*exam_id) {
+            return Err(ControllerError::new(
+                ControllerErrorType::BadRequest,
+                "Teacher grading decision does not belong to the specified exam.",
+                None,
+            ));
+        }
+    }
+
+    // Batch update teacher grading decisions
+    teacher_grading_decisions::batch_update_hidden_field(
+        &mut tx,
+        &teacher_grading_decision_ids,
+        false,
+    ).await?;
+
+    // Batch update user exercise states
+    user_exercise_state_updater::batch_update(
+        &mut tx,
+        &teacher_grading_decisions
+            .iter()
+            .map(|x| x.user_exercise_state_id)
+            .collect::<Vec<Uuid>>(),
+    ).await?;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 7b175c0 and 16f6ed6.

📒 Files selected for processing (3)
  • services/headless-lms/models/.sqlx/query-67a13986d2adbce227c6ebbb7402f7e18980990010b4b784f6a6257a76b40b56.json (1 hunks)
  • services/headless-lms/models/src/user_exercise_states.rs (1 hunks)
  • services/headless-lms/server/src/controllers/main_frontend/exams.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/headless-lms/models/src/user_exercise_states.rs
🔇 Additional comments (1)
services/headless-lms/server/src/controllers/main_frontend/exams.rs (1)

331-337: Validation matches previous review comment.

The code now includes the suggested validation to ensure teacher grading decisions belong to the specified exam.

@@ -0,0 +1,120 @@
{
"db_name": "PostgreSQL",
"query": "\nSELECT id,\n user_id,\n exercise_id,\n course_instance_id,\n exam_id,\n created_at,\n updated_at,\n deleted_at,\n score_given,\n grading_progress AS \"grading_progress: _\",\n activity_progress AS \"activity_progress: _\",\n reviewing_stage AS \"reviewing_stage: _\",\n selected_exercise_slide_id\nFROM user_exercise_states\nWHERE id = ANY($1)\nAND deleted_at IS NULL\n",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Consider adding an index on the id column for optimal query performance

The query filters by id = ANY($1) but the table's primary key is a composite key on (user_id, exercise_id, course_instance_id). While there are several indexes on the table:

  • Composite index on (user_id, exercise_id)
  • Composite index on (deleted_at, user_id)
  • Composite index on (deleted_at, exercise_id, reviewing_stage)
  • Single index on (course_instance_id)

None of these indexes optimally support the query's id = ANY($1) filter. The deleted_at IS NULL condition is covered by existing indexes, but for optimal performance, consider adding a dedicated index on the id column.

🔗 Analysis chain

Verify index usage for optimal query performance.

The query filters on id and deleted_at columns. To ensure optimal performance, verify that appropriate indexes exist.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for indexes on the user_exercise_states table
# Expected: Index on id (primary key) and potentially on deleted_at

# Search for CREATE INDEX statements
rg -A 2 'CREATE.*INDEX.*user_exercise_states'

# Search for table definition to verify primary key
rg -A 20 'CREATE TABLE.*user_exercise_states'

Length of output: 3274

Comment on lines +307 to +317
let user_exercise_state_mapping = models::user_exercise_states::get_by_ids(
&mut conn,
&teacher_grading_decisions
.iter()
.map(|x| x.user_exercise_state_id)
.collect::<Vec<Uuid>>(),
)
.await?
.into_iter()
.map(|x| (x.id, x))
.collect::<HashMap<Uuid, UserExerciseState>>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider handling empty input gracefully.

The code should handle the case when an empty array of IDs is provided.

+    if teacher_grading_decision_ids.is_empty() {
+        return token.authorized_ok(web::Json(()));
+    }
+
     let user_exercise_state_mapping = models::user_exercise_states::get_by_ids(

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant