diff --git a/.github/workflows/build_and_push_all_images.yml b/.github/workflows/build_and_push_all_images.yml index c3b8d71..40e4564 100644 --- a/.github/workflows/build_and_push_all_images.yml +++ b/.github/workflows/build_and_push_all_images.yml @@ -221,6 +221,36 @@ jobs: e2xgrader_branch: ${{ inputs.e2xgrader_branch }} secrets: inherit + wus_notebook: + needs: [datascience_notebook] + uses: ./.github/workflows/build_image.yml + with: + force_build: ${{ inputs.force_build || needs.datascience_notebook.outputs.did_build_image == 'true' }} + registry: ${{ inputs.registry }} + base_image_name: ${{ inputs.registry }}/digiklausur/docker-stacks/datascience-notebook + base_image_tag: ${{ inputs.tag }} + image_path: images/wus-notebook + image_name: wus-notebook + image_tag: ${{ inputs.tag }} + push: ${{ inputs.push }} + secrets: inherit + + e2x_wus_notebook: + needs: [wus_notebook] + uses: ./.github/workflows/build_e2xgrader_images.yml + with: + force_build: ${{ inputs.force_build || needs.wus_notebook.outputs.did_build_image == 'true' }} + registry: ${{ inputs.registry }} + image_name: wus-notebook + image_tag: ${{ inputs.tag }} + base_image_name: ${{ inputs.registry }}/digiklausur/docker-stacks/wus-notebook + base_image_tag: ${{ inputs.tag }} + push: ${{ inputs.push }} + e2xgrader_installation_source: ${{ inputs.e2xgrader_installation_source }} + e2xgrader_version: ${{ inputs.e2xgrader_version }} + e2xgrader_branch: ${{ inputs.e2xgrader_branch }} + secrets: inherit + nlp_notebook: needs: [ml_notebook] uses: ./.github/workflows/build_image.yml diff --git a/images/minimal-notebook/Dockerfile b/images/minimal-notebook/Dockerfile index b89ff9f..341e471 100644 --- a/images/minimal-notebook/Dockerfile +++ b/images/minimal-notebook/Dockerfile @@ -15,22 +15,18 @@ ENV TZ=Europe/Berlin RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ locale-gen -ENV LC_ALL en_US.UTF-8 -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US.UTF-8 - -RUN ln -snf /usr/share/zoneinfo/"$TZ" /etc/localtime && echo "$TZ" > /etc/timezone - -# Create default config for ipython kernel config -RUN mkdir /etc/ipython/ && \ - chown root:"$NB_GID" /etc/ipython/ &&\ - chmod g+rwX /etc/ipython/ &&\ - chmod +6000 /etc/ipython/ - -# Remove default work dir -RUN rm -rf "$HOME"/work - -RUN apt-get update -y \ +ENV LC_ALL=en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US.UTF-8 + +# Set timezone and create default config for ipython kernel config +RUN ln -snf /usr/share/zoneinfo/"$TZ" /etc/localtime && echo "$TZ" > /etc/timezone \ + && mkdir /etc/ipython/ \ + && chown root:"$NB_GID" /etc/ipython/ \ + && chmod g+rwX /etc/ipython/ \ + && chmod +6000 /etc/ipython/ \ + && rm -rf "$HOME"/work \ + && apt-get update -y \ && apt-get install --yes \ ncdu \ vim \ @@ -40,29 +36,23 @@ RUN apt-get update -y \ USER $NB_USER -# Install git and graphviz +# Install requirements and enable extenstions and Jupyter contrib extensions +COPY requirements.txt /tmp/requirements.txt + +# Install git and graphviz and upgrade pip RUN mamba install -y gh --channel conda-forge \ && mamba install -y graphviz --no-update-deps \ && mamba clean -afy \ && npm cache clean --force \ - && find /opt/conda/ -follow -type f -name '*.js.map' -delete - -RUN pip install --no-cache-dir --upgrade pip \ - && find /opt/conda/ -follow -type f -name '*.js.map' -delete - -# Install requirements -COPY requirements.txt /tmp/requirements.txt -RUN pip install --no-cache-dir -r /tmp/requirements.txt \ - && find /opt/conda/ -follow -type f -name '*.js.map' -delete - -# Enable extenstions and Jupyter contrib extensions -RUN cd /tmp \ + && pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r /tmp/requirements.txt \ + && pip uninstall "jupyter-server-terminals" -y \ + && find /opt/conda/ -follow -type f -name '*.js.map' -delete \ + && cd /tmp \ && git clone --depth 1 -b master https://github.com/ipython-contrib/jupyter_contrib_nbextensions.git \ && cd /tmp/jupyter_contrib_nbextensions/src/jupyter_contrib_nbextensions/nbextensions \ && jupyter nbextension install --sys-prefix execute_time \ && jupyter nbextension enable --sys-prefix execute_time/ExecuteTime \ && jupyter nbextension list \ - && rm -rf /tmp/jupyter_contrib_nbextensions - -# Jupyter extension list -RUN jupyter nbextension list \ No newline at end of file + && rm -rf /tmp/jupyter_contrib_nbextensions \ + && jupyter nbextension list \ No newline at end of file diff --git a/images/minimal-notebook/requirements.txt b/images/minimal-notebook/requirements.txt index 093e4d2..d11d770 100644 --- a/images/minimal-notebook/requirements.txt +++ b/images/minimal-notebook/requirements.txt @@ -1 +1,2 @@ -nbresuse \ No newline at end of file +nbresuse +jupyter-server<2.0.0 \ No newline at end of file diff --git a/images/wus-notebook/Dockerfile b/images/wus-notebook/Dockerfile new file mode 100644 index 0000000..cc67b15 --- /dev/null +++ b/images/wus-notebook/Dockerfile @@ -0,0 +1,22 @@ +ARG IMAGE_SOURCE=ghcr.io/digiklausur/docker-stacks/minimal-notebook:latest +FROM $IMAGE_SOURCE + +LABEL maintainer="e2x project H-BRS " +LABEL description="e2x notebook for statistics course" +LABEL org.opencontainers.image.description="e2x notebook for statistics course" +LABEL org.opencontainers.image.authors="e2x project H-BRS " + +USER root +# Copy nbgrader wus config +COPY configs/nbgrader_config.py /opt/conda/etc/jupyter/nbgrader_config.py +RUN chown root:"$NB_GID" /opt/conda/etc/jupyter/nbgrader_config.py &&\ + chmod g+rwX /opt/conda/etc/jupyter/nbgrader_config.py + +USER $NB_USER + +# Disable execute time extension and enable highlight word +RUN jupyter nbextension disable --sys-prefix execute_time/ExecuteTime \ + && pip install --no-cache-dir jupyter_highlight_selected_word \ + && jupyter nbextension install --py --sys-prefix jupyter_highlight_selected_word \ + && jupyter nbextension enable --sys-prefix highlight_selected_word/main \ + && jupyter nbextension list diff --git a/images/wus-notebook/README.md b/images/wus-notebook/README.md new file mode 100644 index 0000000..0446f8c --- /dev/null +++ b/images/wus-notebook/README.md @@ -0,0 +1,54 @@ +# e2x Data Science Notebook + +TThis Docker image is a customized version of the [e2x Data Science Notebook](../datascience-notebook) image, with certain extensions specific to the WuS course at H-BRS installed. + +## Build Args + +The following build arg is available: + +* `IMAGE_SOURCE`: The base image to use for the build. Defaults to `ghcr.io/digiklausur/docker-stacks/datascience-notebook:latest`. + +## Description + +This image is designed to provide a comprehensive Jupyter Notebook environment for data science tasks, building on top of the e2x Minimal Notebook image. It includes: + +* All features from the e2x Minimal Notebook image +* A collection of popular Python data science libraries, including NumPy, Pandas, SciPy, Matplotlib, Scikit-learn, and more + +## Versions + +This images comes as a basic data science image or with `e2xgrader` installed and a specific mode activated. +For more information look at the [E2xGrader Notebook](../e2xgrader-notebook) image and the [e2xgrader](https://github.com/Digiklausur/e2xgrader) package. + +* `wus-notebook` + + Base WuS science image +* `wus-notebook-teacher` + + Base data science image with `e2xgrader` teacher mode activated (includes grading tools) +* `wus-notebook-student` + + Base data science image with `e2xgrader` student mode activated (includes extensions for students) +* `wus-notebook-exam` + + Base data science image with `e2xgrader` student_exam mode activated (provides a restricted notebook for students in an exam) + +## Usage + +### Pull and Run + +To pull and run the image use: + +`docker run -p 8888:8888 ghcr.io/digiklausur/docker-stacks/wus-notebook:latest` + +Available tags are `latest` and `dev`. Available registries are `quay.io` and `ghcr.io`. + +### Build and Run + +To build the image from the standard source, run: + +`docker build -t wus-notebook:dev .` + +To build the image from a custom source, run: + +`docker build -t wus-notebook:dev . --build-arg="IMAGE_SOURCE=:"` + +To run the image, use: + +`docker run -p 8888:8888 wus-notebook:dev` \ No newline at end of file diff --git a/images/wus-notebook/configs/nbgrader_config.py b/images/wus-notebook/configs/nbgrader_config.py new file mode 100644 index 0000000..a0c5822 --- /dev/null +++ b/images/wus-notebook/configs/nbgrader_config.py @@ -0,0 +1,99 @@ +from nbgrader.preprocessors import GetGrades +from nbformat import NotebookNode +from nbconvert.exporters.exporter import ResourcesDict +from nbgrader.api import MissingEntry +from nbconvert.preprocessors import CSSHTMLHeaderPreprocessor +from e2xgrader.preprocessors import FilterTests + +from e2xgrader.graders import MultipleChoiceGrader, CodeGrader, SingleChoiceGrader +from e2xgrader.utils.extra_cells import get_choices, get_instructor_choices +from traitlets import Unicode + +class GetGradesWithUngradedComment(GetGrades): + """ + If a cell has not been graded, this processor adds a comment + saying "Unbewertet" to the cell + """ + + ungraded_comment = Unicode( + "Unbewertet", + help="Comment to add to ungraded cells" + ).tag(config=True) + + def _get_comment(self, cell: NotebookNode, resources: ResourcesDict) -> None: + """Graders can optionally add comments to the student's solutions, so + add the comment information into the database if it doesn't + already exist. It should NOT overwrite existing comments that + might have been added by a grader already. + + """ + comment = self.gradebook.find_comment( + cell.metadata['nbgrader']['grade_id'], + self.notebook_id, + self.assignment_id, + self.student_id) + + needs_manual_grade = False + try: + grade = self.gradebook.find_grade( + cell.metadata['nbgrader']['grade_id'], + self.notebook_id, + self.assignment_id, + self.student_id) + needs_manual_grade = grade.needs_manual_grade + except MissingEntry: + pass + + comment = comment.comment + + if comment is None: + comment = "" + else: + comment += "\n" + + # save it in the notebook + if needs_manual_grade: + cell.metadata.nbgrader['comment'] = comment + self.ungraded_comment + else: + cell.metadata.nbgrader['comment'] = comment + + +class WuSMultipleChoiceGrader(MultipleChoiceGrader): + + def determine_grade(self, cell, log=None): + ''' + Grader for multiple choice questions + + Only give full points if student did select all correct + answers and no incorrect answers + ''' + max_points = float(cell.metadata['nbgrader']['points']) + student_choices = get_choices(cell) + instructor_choices = get_instructor_choices(cell) + + # Return 0 points if the student did not select all correct answers + for choice in instructor_choices: + if choice not in student_choices: + return 0, max_points + + # Return 0 points if the student did select an incorrect answer + for choice in student_choices: + if choice not in instructor_choices: + return 0, max_points + + return max_points, max_points + +c = get_config() # noqa: F821 + +c.GenerateFeedback.preprocessors = [ + GetGradesWithUngradedComment, + FilterTests, + CSSHTMLHeaderPreprocessor +] + +c.SaveAutoGrades.graders = { + 'multiplechoice': WuSMultipleChoiceGrader(), + 'code': CodeGrader(), + 'singlechoice': SingleChoiceGrader() +} +