-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
feat: ItemBankBlock #35553
Conversation
b403f34
to
d90f819
Compare
@kdmccormick Thanks! Here is the corresponding ticket for the UI: openedx/frontend-app-authoring#1385 |
d90f819
to
546fb28
Compare
199e0b1
to
6912ddb
Compare
There was a problem hiding this 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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
# 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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
""" | ||
rand = random.Random() | ||
|
||
selected_keys = {tuple(k) for k in selected} # set of (block_type, block_id) tuples assigned to this student |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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
"max_count": self.max_count, | ||
} | ||
event_data.update(kwargs) | ||
self.runtime.publish(self, f"edx.librarycontentblock.content.{event_name}", event_data) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- Emit no events until we clear the schema with Data WG
- Emit
edx.itembankblock.content.(assigned/removed)
events, but just withreason
andusage_key
field. i.e., leave outoriginal_usage_key
,original_usage_version
, anddescendents
for now.
(placeholder followup issue #35685)
There was a problem hiding this comment.
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
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 |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
934bf45
to
b28e11a
Compare
Comments are addressed. I still have those 2 unit tests to fix-- I'll ping again when they're ready to go. |
), | ||
) | ||
@ddt.unpack | ||
def test_removed_invalid(self, to_select_initial, to_drop, to_select_new): |
There was a problem hiding this comment.
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.
Ready for re-review. |
'added': added_block_keys, | ||
'invalid': sorted(invalid_block_keys), | ||
'overlimit': sorted(overlimit_block_keys), | ||
'added': sorted(added_block_keys), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To facilitate testing
There was a problem hiding this 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.
Thanks for the fast reviews all! |
2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production. |
2U Release Notice: This PR has been deployed to the edX production environment. |
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":
The editor (which Braden's team will replace) is currently just a subset of the settings on the LegacyLibraryContentBlock's editor:
Clicking "View" brings you to the detail view. Here's it empty; I haven't tested it with children yet:
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
...