Skip to content

[BB-10108] Refactor Branching XBlock to include Ren'Py like features#10

Merged
pkulkark merged 33 commits intomainfrom
pooja/bb10108-implement-renpy
Mar 12, 2026
Merged

[BB-10108] Refactor Branching XBlock to include Ren'Py like features#10
pkulkark merged 33 commits intomainfrom
pooja/bb10108-implement-renpy

Conversation

@pkulkark
Copy link
Member

@pkulkark pkulkark commented Feb 19, 2026

Description

This PR refactors Branching XBlock to introduce Ren’Py-like branching/storytelling behavior, with updated authoring and learner UX aligned to the design reference in Figma: Figma design file.

Key updates included:

  • Refactored Studio authoring flow into a step-based editor with improved node management and validation UX.
  • Added richer image node support (left_image_url, right_image_url, overlay text, background image settings).
  • Strengthened save-time validation (cycle detection, referenced-node deletion checks, choice score validation).
  • Updated learner runtime for per-choice scoring, undo/reset behavior, and completion report output.

Supporting Information

OpenCraft Internal Jira task: BB-10108

Testing Instructions

  1. Deploy this branch
  2. In Studio, verify:
    • node create/edit/delete flows work
    • deleting referenced nodes is blocked
    • cycles are rejected on save
    • background image accessibility validation works (alt text required unless decorative)
  3. Create a branching scenario with all the options in place
  4. In learner view, verify:
    • scoring accumulates by selected choice scores
    • undo reverses the most recent scored choice
    • reset clears progress and score
    • completion report renders with expected values

Additional notes from author

  • The grade-scale currently implements only Pass/Fail levels and does not support creating multiple grade bands like Studio grading. This was an intentional scoping decision to reduce implementation complexity for this iteration, and can be extended in a future iteration if requirements call for it.
  • This branch and main both changed the same enable_hintsenable_reset_activity areas. Although 3 related commits from main were cherry-picked here (ab2770c, 8e5d707, c05093e), Git still reported conflicts because cherry-picks copy changes but not commit ancestry. To reconcile both ancestry and overlapping edits, origin/main was merged into this branch and conflicts were resolved in merge commit febfb7b.

Copy link
Member

@samuelallan72 samuelallan72 left a comment

Choose a reason for hiding this comment

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

Thanks @pkulkark ! This is a pretty large PR, so I've made a first pass, and left several code review comments inline. I haven't done a deep review of code logic yet, or played around too much with the UI.

Also a comment on UI/UX:

Saving when there's an issue with the main background image is confusing - the error message may be hidden because it's in the body rather than a popup or next to the save button, and the studio still displays the saving animation in the lower right corner:

Image

This is confusing, as you are thinking that it's just taking a long time to save, when in fact validation has failed and you need to scroll down to view the error:

Image

Maybe these errors too need to go in the bottom bar or as a popup message?

return node

@staticmethod
def _coerce_choice_score(raw_score):
Copy link
Member

Choose a reason for hiding this comment

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

Rather than coercing the score everywhere, can we simply validate it in studio_submit (or a function only called from that), and trust the data structures after that?

In general I see a lot of noise working with data in the backend, due to all the validating and normalising happening. It would be nice to validate and normalise in one place, where it's submitted from the user (ie. studio_submit), and then treat it as known data types everywhere else.

If we need validation for the case where the xblock is imported from olx, then perhaps we can use a standard validation method to validate everything - like validate or validate_field_data - ref https://docs.openedx.org/projects/xblock/en/latest/xblock-utils/index.html or https://docs.openedx.org/projects/xblock/en/latest/xblock.html#xblock.core.XBlock.validate . I'm not sure exactly how these interact, but either way, if we can move all validation related things to the boundaries, that will leave things cleaner in the rest of the code.

help="Whether the background image is decorative"
)

max_score = Float(
Copy link
Member

Choose a reason for hiding this comment

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

Score fields appear to be treated as integers everywhere else, so can these be integer fields instead?

return "Final grade range must end at 100."
return None

def _normalize_grade_ranges(self, raw_grade_ranges, strict=False):
Copy link
Member

Choose a reason for hiding this comment

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

This overlaps with _validate_grade_ranges above. Can we simply drop this and use something like:

if error := _validate_grade_ranges(raw_ranges):
  raise ValidationError(error)
else:
  self.grade_ranges = raw_ranges

I'm not sure why we need so much normalising of values throughout? Can we just validate, and ensure the frontend editor always submits the expected types of values?

new_id = f"node-{uuid.uuid4().hex[:6]}"
else:
new_id = old_id
new_id = f"node-{uuid.uuid4().hex[:6]}" if old_id.startswith('temp-') or not old_id else old_id
Copy link
Member

Choose a reason for hiding this comment

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

It would be interesting to explore letting the frontend provide unique ids for the nodes, to avoid this node id re-assigning and mapping.

@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch from a31f6f6 to 69cfac0 Compare March 2, 2026 14:59
@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch 3 times, most recently from 717966f to aaf609b Compare March 2, 2026 16:54
- move studio save validation to a single backend validation path
- return structured field/global/node errors for UI rendering
- remove frontend normalization/coercion and rely on backend responses
@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch from aaf609b to aa3e3e7 Compare March 2, 2026 18:36
@pkulkark
Copy link
Member Author

pkulkark commented Mar 2, 2026

Thanks @pkulkark ! This is a pretty large PR, so I've made a first pass, and left several code review comments inline. I haven't done a deep review of code logic yet, or played around too much with the UI.

Also a comment on UI/UX:

Saving when there's an issue with the main background image is confusing - the error message may be hidden because it's in the body rather than a popup or next to the save button, and the studio still displays the saving animation in the lower right corner:

Image This is confusing, as you are thinking that it's just taking a long time to save, when in fact validation has failed and you need to scroll down to view the error: Image Maybe these errors too need to go in the bottom bar or as a popup message?

Good catch. I've fixed this now.

@samuelallan72
Copy link
Member

Thanks @pkulkark, I see your responses ; let me know when it's ready for another review pass.


def _normalize_scenario_nodes(self) -> None:
"""
Normalize stored scenario node payloads once per nodes object.
Copy link
Member

Choose a reason for hiding this comment

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

Could you expand these docs with information about why this is required, what it actually does, and a note about potentially removing it in the future when we no longer need to normalise old data?

Also, maybe we can run the normalising in the init method, so we don't need to remember to use it in the various view methods?

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added more information in the doc strings - let me know if that's good enough.

I don't think we should move the normalising in an init method, because we'd risk invalid stored values being auto-corrected during learner requests, which hides data issues instead of surfacing them through authoring validation. Keeping normalization in Studio/save flow will avoid that. Alternative option is to drop the whole normalizing and assume the old saved values wouldn't be needed to be preserved but i'm a bit hesitant to do that since this xblock rolls in the old branching xblock too.

Copy link
Member

@samuelallan72 samuelallan72 Mar 11, 2026

Choose a reason for hiding this comment

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

Hmm if it's in init, then any invalid stored values will be automatically corrected whenever they're saved next. Currently, existing components may be in a slightly broken state after upgrade, until they are edited and re-saved in studio.

Alternative option is to drop the whole normalizing and assume the old saved values wouldn't be needed to be preserved but i'm a bit hesitant to do that since this xblock rolls in the old branching xblock too.

Is this definitely to be a in-place update to the branching xblock? If not, and if we don't need to have backward compatibility, then perhaps a better option might be to change the xblock label (to register it as a different/new xblock) and maybe even the package name, and drop normalising altogether.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes it was part of the requirement to have this as an in-place update.

@samuelallan72
Copy link
Member

@pkulkark I'm still seeing the red general error feedback text within the main content:

image

I'm not sure where that should go though, as it's not in the designs.

Copy link
Member

@samuelallan72 samuelallan72 left a comment

Choose a reason for hiding this comment

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

@pkulkark in general, it works as expected functionally, thanks! I flagged a few things though - please see my inline comments. :)

@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch from b191692 to 16a805a Compare March 11, 2026 16:00
@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch from 16a805a to 855077c Compare March 11, 2026 16:03
@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch from c58cc0d to ab9c996 Compare March 11, 2026 19:42
@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch from faf8448 to c1540a9 Compare March 11, 2026 20:13
Copy link
Member

@samuelallan72 samuelallan72 left a comment

Choose a reason for hiding this comment

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

@pkulkark everything looks pretty good to me. 👍 I have left a couple of comments/suggestions, but otherwise feel free to response as you see fit and merge.

This was a very large change, so I may have missed things.

  • I tested this: read the code, tested manually on the sandbox
  • I read through the code
  • I checked for accessibility issues (my screenreader wasn't working, so I didn't test that. I did test keyboard controls though.)
  • Includes documentation

Co-authored-by: Samuel Allan <samuel.allan@fastmail.com>
@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch from 4e989aa to f5c81e1 Compare March 12, 2026 15:06
@pkulkark pkulkark force-pushed the pooja/bb10108-implement-renpy branch from f5c81e1 to 2f3b7a0 Compare March 12, 2026 15:33
@pkulkark pkulkark merged commit 5d20f81 into main Mar 12, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants