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

Parse question and answer from cell metadata #2

Closed
psychemedia opened this issue Sep 15, 2021 · 17 comments
Closed

Parse question and answer from cell metadata #2

psychemedia opened this issue Sep 15, 2021 · 17 comments

Comments

@psychemedia
Copy link

When authoring a self-contained notebook for rendering as a Jupyter Book, it's presumably easy enough to put question and answer definitions into a removed code cell in the final book output.

However, if the notebook is used as a notebook, the answers are evident in the code cell.

In such a case, it might be useful to pop the question and answer dictionary into the cell metadata as a JSON object and then render that into the multiple choice widget?

It's a bit of a faff for the notebook author having to edit the metadata cell, but it would keep the notebook self-contained and the answer source hidden to some extent.

@psychemedia psychemedia changed the title Add question and answer to cell metadata Parse question and answer from cell metadata Sep 15, 2021
@jmshea
Copy link
Owner

jmshea commented Sep 15, 2021

Shouldn't be too hard to implement. I will look into implementing this soon

@jmshea
Copy link
Owner

jmshea commented Sep 15, 2021

Actually, I can't see how to access cell metadata either from Python or from Javascript. It does not seem to be added to the DOM, for instance. Let me know if you have any ideas.

As a less attractive workaround, I could add an option to take in the data as Base64-encoded JSON, which is easy to create but not human readable. Let me know what you think.

@psychemedia
Copy link
Author

psychemedia commented Sep 15, 2021

Yes, I'd suddenly started wondering too about how the introspection would work. It's easy enough to access cell metadata from eg a notebook extension, but that is changing the scope a bit.

From a quick search around, there's a recipe at https://habr.com/en/post/439570/ that shows how to set cell tags using magic, so presumably a complementary approach along the lines of q_and_a = %get_metadata qna to get a cell.metadata.qna object might work?

@psychemedia
Copy link
Author

psychemedia commented Sep 15, 2021

Hmmm... I wonder if that js / magic approach might be limited where it works. Eg would it work in a notebook edited / run in VS Code?

@psychemedia
Copy link
Author

psychemedia commented Sep 15, 2021

Okay, so this is very hacky and riffs on https://habr.com/en/post/439570/ with added an IPython.notebook.kernel.execute() Javascript function call. It may also only work in classic notebook UI (I still haven't migrated away from that!)

In a code cell, run:

%%javascript
define('getCellMetadata', function() {
    return function(element) {
        var cell_element = element.parents('.cell');
        var index = Jupyter.notebook.get_cell_elements().index(cell_element);
        var cell = Jupyter.notebook.get_cell(index);
        var jsonString = JSON.stringify(cell.metadata);
        IPython.notebook.kernel.execute("cell_metadata = '"+jsonString+"'");
    }
});

Alternatively, that can probably just be wrapped in Python code, eg as:

display(Javascript("""
define('getCellMetadata', function() {
    return function(element) {
        var cell_element = element.parents('.cell');
        var index = Jupyter.notebook.get_cell_elements().index(cell_element);
        var cell = Jupyter.notebook.get_cell(index);
        var jsonString = JSON.stringify(cell.metadata);
        IPython.notebook.kernel.execute("my_out = '"+jsonString+"'");
    }
});
"""))

Then define some magic:

def _get_metadata(key):
    display(Javascript(
        """
        require(['getCellMetadata'], function(getCellMetadata) {
            getCellMetadata(element);
        });
        """
    ))


@register_line_cell_magic
def get_metadata(line, cell=None):
    _get_metadata(line)

Then in a code cell that has metadata attached, run the magic:

%get_metadata

Then in another code cell you can reference the stringified metadata:

cell_metadata

I think this needs to be accessed in a cell separate to line magic cell because of the async way in which things get executed?

@jmshea
Copy link
Owner

jmshea commented Sep 20, 2021

"Jupyter" isn't even defined in JupyterLab. I can't find anything equivalent, and I listed out all the local objects in JS.

@psychemedia
Copy link
Author

psychemedia commented Sep 21, 2021

Ah... I still haven't moved to JuptyerLab/RetroLab: too complicated for me. I'm pretty much locked into classic notebook.

@psychemedia
Copy link
Author

psychemedia commented Sep 21, 2021

If you write an extension, then you can get access to cell metadata (for example, the JupyterLab Collapsible_Headings extension uses cell metadata to identify header cells marked as collapsed).

A more elaborate way of making use of an extension might be an extension that supports authoring of example answer tests, cf. cell tests as per the nbcelltests JupyterLab extension. The problem then becomes one of how to disable that view to students whilst still supporting the self-test/formative grading when the cell is run.

@jmshea
Copy link
Owner

jmshea commented Sep 24, 2021

TLDR: I plan to close this issue after adding base64 encoding. Using this plus Collapse Selected Code in JupyterLab seems a good compromise in terms of implementing the behavior you want.

An extension is out of my expertise and interest on this project. I have put some time into trying to find a way to access the cell metadata from either cell JavaScript or Python, and unfortunately I haven't figured out how it can be done in JupyterLab. If you have any knowledge about how to do that, let me know.

I will go ahead and add the ability to read in the questions as base64-encoded stringified JSON as a stopgap. In JupyterLab, you can then set the cell containing that to be "hidden" in JupyterLab via View-> Collapse Selected Code. It can be "uncollapsed", but the questions/answers won't be human readable. I don't know if Collapse carries over to Jupyter notebook. I did some experiments at trying to auto-hide things using JS, but basically all that hiding would go away when the document was reloaded, unless the JS cell was explicitly run.

@psychemedia
Copy link
Author

Okay, thanks for exploring this too. I think that there are various discussions around extending the notebook format, as well as proposals regarding the propagation of metadata to kernels in JupyterLab, but actually contributing to those is outside my skillset; I will however maintain a watching brief over them until such a time as they become available and can be easily coopted for use...

@jmshea
Copy link
Owner

jmshea commented Sep 26, 2021

I think I have implemented nearly equivalent behavior to satisfy this request. You can now store a question as either JSON or base64-encoded JSON in a hidden HTML element within a Markdown cell. Then you can show that question using JupyterQuiz by passing the ID of the HTML element.

This works in both Jupyter Notebook and Jupyter Lab.

Please see the notebook HideQuiz.ipynb for examples.

I will update the README.md later to document this feature.

I hope that you find this helpful!

@jmshea jmshea reopened this Sep 26, 2021
@jmshea
Copy link
Owner

jmshea commented Sep 26, 2021

I am reopening this issue since I did make some progress in implementing it and want this open until I get comments back from @psychemedia

@psychemedia
Copy link
Author

Ah, that's neat... To obfuscate a little bit more, I guess the markdown cell with the hidden answer could also be placed anywhere in the notebook. Though to help the author, a conventional location, such as the end of the notebook, might be most convenient.

(TBH, I'm pretty casual about students looking up answers to formative questions, so as long as there is some friction to peaking at the answers, I suspect most won't (or won't know how to!). And whilst it's their loss if they do, it does demonstrate other skills if they know how to decode answers by technical means...!)

To simplify authoring, I guess a way to generate the "release" notebook could be to tag answer cells (containing explicit plaintext answers) with a particular tag and then run the notebook through a simple offline processor to rewrite the cell contents as base64 encoded elements? Another approach would be a simple extension to do that (I reckon I could do that easily enough in classic notebook, but I still haven't got myself into a position where I can write JupyterLab extensions...)

I guess there is a security implication in that the b64 could hide nasty js that could be evaluated (I'm not very well versed in security / sanitising operations, but I've started trying to at least look for things that a security audit might question).

@jmshea
Copy link
Owner

jmshea commented Sep 27, 2021

Thanks. Great comments.

An authoring tool supporting rewriting the source data to base64 would be great. I'll add it as a to-do.

I haven't worried too much about the security implications because of my particular use case, but I see what you are getting at. However, I guess there is little difference from when the JSON is loaded over the network -- the user doesn't get to see the JSON before it is loaded. I guess the cleanest way to resolve this would be to use something like GPB that would enforce a strict grammar. The downside is that we lose the lightweight, extendible nature of JSON. Looking on line, it seems that by using JSON.parse() instead of eval(), I will greatly reduce the security threats, so I will do that shortly.

@jmshea
Copy link
Owner

jmshea commented Sep 27, 2021

@psychemedia OK, I replaced those eval()s with JSON.parse(), so that should be much safer. However, I'm not a JS security expert by any means!

Also, I completely updated HideQuiz.ipynb to provide a more detailed walkthrough for users.

Please let me know if you have any comments or catch any bugs!

@jmshea
Copy link
Owner

jmshea commented Oct 3, 2021

Closing this because similar functionality has been implemented

@jmshea jmshea closed this as completed Oct 3, 2021
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

No branches or pull requests

2 participants