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

feat: ItemBankBlock #35553

Merged
merged 5 commits into from
Oct 22, 2024
Merged

feat: ItemBankBlock #35553

merged 5 commits into from
Oct 22, 2024

Conversation

kdmccormick
Copy link
Member

@kdmccormick kdmccormick commented Sep 26, 2024

Based on:

Description

A new XBlock that presents a random subset of its children.

The block does not care if its children are from V1 library, V2 library, or the course itself.

Shares the randomization logic with LegacyLibraryContentBlock. It is also fully backwards-compatible with LegacyLibraryContentBlock. So, once V1 libraries are migrated to V2 libraries, we eventually could point the library_content entry point at ItemBankBlock.

Supporting information

The unit editor view is identical to the LegacyLibraryContentBlock, except that the default title is "Problem Bank":

image

The editor (which Braden's team will replace) is currently just a subset of the settings on the LegacyLibraryContentBlock's editor:

image

Clicking "View" brings you to the detail view. Here's it empty; I haven't tested it with children yet:

image

Deadline / Status

We'd like this to be done and merged before the Sumac cutoff (23 Oct) if possible.

Current status: Partially working. Needs a new React-based editor for picking blocks from a V2 library. Also needs more unit tests.

Other information

...

@kdmccormick kdmccormick changed the title feat: PoolBlock feat: ProblemBankBlock Oct 10, 2024
@kdmccormick kdmccormick changed the title feat: ProblemBankBlock feat: ItemBankBlock Oct 13, 2024
@kdmccormick kdmccormick force-pushed the kdmccormick/pool branch 2 times, most recently from b403f34 to d90f819 Compare October 14, 2024 18:05
@bradenmacdonald
Copy link
Contributor

@kdmccormick Thanks! Here is the corresponding ticket for the UI: openedx/frontend-app-authoring#1385

Copy link
Contributor

@bradenmacdonald bradenmacdonald left a comment

Choose a reason for hiding this comment

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

This is looking great, and working really well in my testing so far. Didn't run into any bugs.

# TODO: Whittle down list of mixins if possible.
# https://github.com/openedx/edx-platform/issues/35686
MakoTemplateBlockBase,
XmlMixin,
Copy link
Contributor

@bradenmacdonald bradenmacdonald Oct 22, 2024

Choose a reason for hiding this comment

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

It seems you can remove XmlMixin now. ItemBankBlock works fine without it AFAICT, although you may have to update some test cases that rely on its weird default behavior of writing the XML to a file in export_fs.

(edit: it may also affect import/export in some way, but I'm not sure)

Feel free to leave it in and get rid of it post-Sumac if that's more sensible.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'll do this in a follow-up

xmodule/item_bank_block.py Outdated Show resolved Hide resolved
Comment on lines +105 to +111
# Read back the itembank OLX
with export_fs.open('{dir}/{file_name}.xml'.format(
dir=self.item_bank.scope_ids.usage_id.block_type,
file_name=self.item_bank.scope_ids.usage_id.block_id
)) as f:
actual_olx_export = f.read()
Copy link
Contributor

@bradenmacdonald bradenmacdonald Oct 22, 2024

Choose a reason for hiding this comment

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

I don't like how this relies weird export behavior coming from XmlMixin, such that add_xml_to_node has a side-effect of writing definitions to the memory filesystem. According to the XmlMixin docstring, this behavior is "specifically for customtag ?".

I was going to say, I'd rather you remove XmlMixin and just use actual_olx_export = etree.tostring(node, encoding="unicode", pretty_print=True) to get the string version, instead of encoding these export nuances into the test case. If you need more advanced serialization (with children and static assets), call serialize_xblock_to_olx(self.item_bank) instead.

BUT I guess you're also relying on the fs-serialization of problem blocks and you're wanting to test code that's as similar as possible to the actual export-import cycle here? If so, that's fine. I just wish our import code wasn't so messy.

Copy link
Member Author

@kdmccormick kdmccormick Oct 22, 2024

Choose a reason for hiding this comment

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

I can try to simplify it.

BUT I guess you're also relying on the fs-serialization of problem blocks and you're wanting to test code that's as similar as possible to the actual export-import cycle here?

yes, but also test_library_content.py still exists and still has the near-identical test case that I cribbed this one from, so I think it's safe to use a cleaner version of the test case in test_item_bank.py

Copy link
Contributor

Choose a reason for hiding this comment

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

OK. Don't spend a ton of time on it though; more important we get this merged ASAP.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, I'd rather do this in a follow-up then

xmodule/tests/test_item_bank.py Outdated Show resolved Hide resolved
xmodule/tests/test_item_bank.py Outdated Show resolved Hide resolved
xmodule/tests/test_item_bank.py Outdated Show resolved Hide resolved
xmodule/tests/test_item_bank.py Outdated Show resolved Hide resolved
xmodule/tests/test_item_bank.py Outdated Show resolved Hide resolved
xmodule/tests/test_item_bank.py Show resolved Hide resolved
"""
rand = random.Random()

selected_keys = {tuple(k) for k in selected} # set of (block_type, block_id) tuples assigned to this student
Copy link
Contributor

Choose a reason for hiding this comment

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

[question]: Do you know what the call to tuple() here is for, if the thing being passed in is already a list of tuples? I'm hesitant to suggest any change to a method that has Obviously Seen Edge Cases, but it just seems weird. Maybe at some point someone just threw a list of lists and it threw things off because they wouldn't hash?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh right, this 😛

It's because we assign lists-of-tuples to self.selected, but when you save the block and then load it back from ModuleStore, self.selected comes back as list-of-2-element-lists. So we need to handle both cases here by converting to tuples. I'll add a comment.

(Back in the last iteration of this project, I had straightened this out and made it type safe, but that never merged. I can't bite that off again right now, but I'd love to do it some time before the Sumac cut.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Left a comment

xmodule/item_bank_block.py Outdated Show resolved Hide resolved
"max_count": self.max_count,
}
event_data.update(kwargs)
self.runtime.publish(self, f"edx.librarycontentblock.content.{event_name}", event_data)
Copy link
Contributor

Choose a reason for hiding this comment

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

[question]: is this still the right event to emit given that it's going to be used in another block as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. I feel like Data WG would be the place to ask, but that won't happen between now and the cut.

I feel comfortable saying that LegacyLibraryContentBlocks should continue emitting edx.librarycontentblock.content.(assigned/removed)... events (for now, at least).

For the MVP, would would be your inclination for ItemBankBlocks? My inclination is either:

  1. Emit no events until we clear the schema with Data WG
  2. Emit edx.itembankblock.content.(assigned/removed) events, but just with reason and usage_key field. i.e., leave out original_usage_key, original_usage_version, and descendents for now.

(placeholder followup issue #35685)

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'm going with (2), LMK if that concerns you for Sumac

xmodule/item_bank_block.py Show resolved Hide resolved
if field.is_set_on(self):
xml_object.set(field_name, str(field.read_from(self)))
return xml_object
block = self.runtime.modulestore.get_item(key, depth=None) # Load the item and all descendants
Copy link
Contributor

Choose a reason for hiding this comment

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

[question] Is this equivalent to using self.runtime.get_block, and an artifact of the fact that this used to be part of a class that had a reference to modulestore? Or is there a deeper difference? (No need to change it–this is really sensitive code and safe-equivalence is fine. I'm just curious to understand better.)

Copy link
Contributor

@bradenmacdonald bradenmacdonald Oct 22, 2024

Choose a reason for hiding this comment

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

I was going to ask the same thing, because the canonical way to iterate over children is just to use self.get_chidlren(). But I saw the "all descendants" - I think the intention here was something more optimized that loads many levels of decsendants at once? However, I don't really know why we care about that when 99% of problem banks will be one level deep.

In any case, this code was already here.

Copy link
Member Author

Choose a reason for hiding this comment

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

an artifact of the fact that this used to be part of a class that had a reference to modulestore?

It's an artifact of that, plus me not remembering that self.runtime.get_block exists 😄

Using self.runtime.modulestore.get_item is already an imperfect substitute, as it spit out blocks with version information in their .children keys, hence the new .replace(version=None, branch=None) clause.

I'll try get_block, and see if it lets me remove the version-stripping.

Copy link
Member Author

Choose a reason for hiding this comment

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

This change didn't immediately work, so I'm going to punt on it for now

@kdmccormick
Copy link
Member Author

Comments are addressed. I still have those 2 unit tests to fix-- I'll ping again when they're ready to go.

@kdmccormick kdmccormick marked this pull request as ready for review October 22, 2024 19:11
),
)
@ddt.unpack
def test_removed_invalid(self, to_select_initial, to_drop, to_select_new):
Copy link
Member Author

Choose a reason for hiding this comment

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

FYI @ormsbee , @bradenmacdonald -- I'm not proud of how long and convoluted this test case is, but I feel very confident that it thoroughly tests the selection & re-selection process, which has consistently shown itself to be the most complex aspect of this block. I'm willing to break it into smaller + more readable test cases in a follow-up, but I don't think it's important right now.

@kdmccormick
Copy link
Member Author

Ready for re-review.

'added': added_block_keys,
'invalid': sorted(invalid_block_keys),
'overlimit': sorted(overlimit_block_keys),
'added': sorted(added_block_keys),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these now sorted? Is it to facilitate testing or fix a bug? I would assume it's better to have added reflect the order in which the blocks are added, but I don't think it matters.

Copy link
Member Author

Choose a reason for hiding this comment

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

To facilitate testing

Copy link
Contributor

@bradenmacdonald bradenmacdonald left a comment

Choose a reason for hiding this comment

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

I have been testing this quite a bit along with my UI branch and it's been working well. Great work @kdmccormick.

@kdmccormick kdmccormick merged commit 08c8e9d into openedx:master Oct 22, 2024
49 checks passed
@kdmccormick kdmccormick deleted the kdmccormick/pool branch October 22, 2024 19:40
@kdmccormick
Copy link
Member Author

Thanks for the fast reviews all!

@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production.

@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX production environment.

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.

4 participants