diff --git a/travo/dashboards.py b/travo/dashboards.py index 399e40bb64857c40a27b302fdf9500c7cd8cb114..59784f17dd456952bc8c424c230a25c21ada34f8 100644 --- a/travo/dashboards.py +++ b/travo/dashboards.py @@ -20,6 +20,7 @@ import logging import os import subprocess import requests +import textwrap from threading import Thread from typing import Any, Callable, Dict, List, Optional, Tuple, Union @@ -48,6 +49,7 @@ except ImportError: ) from IPython.display import display, Javascript # type: ignore +import traitlets # type: ignore from .assignment import Assignment @@ -507,6 +509,62 @@ class StatusBar(VBox): ).start() +class LinkAsButton(ipywidgets.HTML): + """ + A widget displaying an html link with a button-like style and interface + """ + + disabled = traitlets.Bool(False) + + def __init__( + self, + description: str, + target: Optional[str] = None, + button_style: str = "", + icon: Optional[str] = None, + tooltip: Optional[str] = None, + layout: Optional[Layout] = None, + disabled: bool = False, + ): + super().__init__("") + + self.target = target + + if icon: + description = f'' + description + self.button_content = description + + classes = "lm-Widget jupyter-widgets jupyter-button widget-button" + if button_style: + classes += f" mod-{button_style}" + self.classes = classes + + self.title = tooltip + + if disabled: + self.disabled = disabled # will trigger an update + else: + self.update() + + def update(self) -> None: + opacity = 0.5 if self.disabled else 1 + content = textwrap.dedent( + f""" + + {self.button_content} + + """ + ) + if self.target is not None: + content = f'{content}' + + self.value = content + + @traitlets.observe("disabled") + def _disabled_changed(self, change: dict) -> None: + self.update() + + class AssignmentStudentDashboard(HBox): def __init__( self, @@ -526,10 +584,6 @@ class AssignmentStudentDashboard(HBox): ) self.status_bar = status_bar - from ipylab import JupyterFrontEnd # type: ignore - - self.jupyter_front_end = JupyterFrontEnd() - layout = Layout(width="initial") self.nameUI = HTML(self.name, layout=layout) self.fetchUI = Button( @@ -551,7 +605,7 @@ class AssignmentStudentDashboard(HBox): ) self.submitUI.on_click(lambda event: self.submit()) - self.work_dir_UI = Button( + self.work_dir_UI = LinkAsButton( description=_("open"), button_style="primary", icon="edit", @@ -559,7 +613,6 @@ class AssignmentStudentDashboard(HBox): layout=layout, disabled=True, ) - self.work_dir_UI.on_click(self.open_work_dir_callback) self.scoreUI = HTML("", tooltip=_("browse feedback")) self.updateUI = Button( description="", @@ -602,6 +655,23 @@ class AssignmentStudentDashboard(HBox): Thread(target=self.update).start() def update(self) -> None: + # First take care of the UI for the local work dir for the assignment + # as it does not require authentication + self.work_dir_UI.disabled = True + has_assignment_dir = ( + self.assignment.assignment_dir is not None + and os.path.exists(self.assignment.assignment_dir) + ) + if has_assignment_dir: + assert self.assignment.assignment_dir is not None + index_files = ["index.md", "index.ipynb", "README.md", "README.ipynb"] + for x in index_files: + file = os.path.join(self.assignment.assignment_dir, x) + if os.path.isfile(file): + self.work_dir_UI.target = file + self.work_dir_UI.disabled = False + break + # For now, fetching the assignment status requires the user to # be logged in. Fails gracefuly if this is not yet the case. try: @@ -647,13 +717,7 @@ class AssignmentStudentDashboard(HBox): self.submissionUI.value = "" self.submitUI.tooltip = _("submit assignment", assignment_name=self.name) - self.submitUI.disabled = True - self.work_dir_UI.disabled = True - if self.assignment.assignment_dir is not None and os.path.exists( - self.assignment.assignment_dir - ): - self.work_dir_UI.disabled = False - self.submitUI.disabled = False + self.submitUI.disabled = not has_assignment_dir if status.autograde_status == "success": # these two s/could be provided by autograde_status @@ -698,52 +762,6 @@ class AssignmentStudentDashboard(HBox): on_success=self.update, ) - def open_work_dir_callback(self, event: Any) -> None: - self.work_dir_UI.disabled = True - self.status_bar.run( # run_in_subthread( - action=_("open work dir", assignment_name=self.name), - command=self.open_work_dir, - on_finished=lambda: setattr(self.work_dir_UI, "disabled", False), - ) - - def open_work_dir(self) -> None: - if self.assignment.assignment_dir is None: - raise ValueError(_("cannot open work dir unset")) - if os.path.isabs(self.assignment.assignment_dir): - raise NotImplementedError( - _( - _("cannot open work dir absolute"), - work_dir=self.assignment.assignment_dir, - ) - ) - path = os.path.dirname(self.jupyter_front_end.sessions.current_session["path"]) - # TODO : if README.md does not exist neither, try to open gitlab file browser - index_files = ["index.md", "index.ipynb", "README.md", "README.ipynb"] - for x in index_files: - file = os.path.join(self.assignment.assignment_dir, x) - if os.path.isfile(file): - self.jupyter_front_end.commands.execute( - "docmanager:open", - { - "path": path + "/" + file, # Generalize if there is no index.md - "factory": "Notebook", - # 'options': { - # 'mode': 'split-right' - # }, - "kernelPreference": { - "shutdownOnClose": True, - }, - }, - ) - return - raise FileNotFoundError( - _( - "no index file", - assignment_dir=self.assignment.assignment_dir, - index_files=" ".join(index_files), - ) - ) - def other_action(self) -> None: action = self.other_actionsUI.value if not action: