From a36870024823c503a8d79cd6756101d1a3e3629f Mon Sep 17 00:00:00 2001 From: Brad Miller Date: Sun, 18 Dec 2022 07:56:51 -0600 Subject: [PATCH] Initial support for viewing attachments when grading --- controllers/admin.py | 34 +++++++- docker-compose.yml | 2 + .../nginx/sites-available/runestone.template | 2 + poetry.lock | 80 +++++++++++++++++-- production/docker-compose.yml | 3 + pyproject.toml | 1 + static/js/admin.js | 11 ++- views/assignments/doAssignment.html | 4 - 8 files changed, 124 insertions(+), 13 deletions(-) diff --git a/controllers/admin.py b/controllers/admin.py index 84026c082..2e65df408 100644 --- a/controllers/admin.py +++ b/controllers/admin.py @@ -22,6 +22,7 @@ # Third Party library # ------------------- +import boto3, botocore from dateutil.parser import parse from rs_grading import _get_assignment, send_lti_grades from runestone import cmap @@ -1618,7 +1619,38 @@ def htmlsrc(): htmlsrc and htmlsrc[0:2] == "\\x" ): # Workaround Python3/Python2 SQLAlchemy/DAL incompatibility with text columns htmlsrc = htmlsrc.decode("hex") - return json.dumps(htmlsrc) + + result = {"htmlsrc": htmlsrc} + logger.debug("htmlsrc = {htmlsrc}") + if "data-attachment" in htmlsrc: + # get the URL for the attachment, but we need the course, the user and the divid + session = boto3.session.Session() + client = session.client( + "s3", + config=botocore.config.Config(s3={"addressing_style": "virtual"}), + region_name=settings.region, + endpoint_url="https://nyc3.digitaloceanspaces.com", + aws_access_key_id=settings.spaces_key, + aws_secret_access_key=settings.spaces_secret, + ) + + prepath = f"{auth.user.course_name}/{acid}/{studentId}" + logger.debug(f"checking path {prepath}") + response = client.list_objects(Bucket=settings.bucket, Prefix=prepath) + logger.debug(f"response = {response}") + if response and "Contents" in response: + obj = response["Contents"][0] + logger.debug("key = {obj['Key']}") + url = client.generate_presigned_url( + ClientMethod="get_object", + Params={"Bucket": settings.bucket, "Key": obj["Key"]}, + ExpiresIn=300, + ) + + else: + url = "" + result["attach_url"] = url + return json.dumps(result) @auth.requires( diff --git a/docker-compose.yml b/docker-compose.yml index 270c3fe28..e868fc446 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,5 +57,7 @@ services: ADS_FILE: '${ADS_FILE}' QUICK_START: ${QUICK_START} WORKER_NAME: ${HOSTNAME} + SPACES_KEY: ${SPACES_KEY} + SPACES_SECRET: ${SPACES_SECRET} links: - jobe diff --git a/docker/nginx/sites-available/runestone.template b/docker/nginx/sites-available/runestone.template index 75ad44d67..51422c46c 100644 --- a/docker/nginx/sites-available/runestone.template +++ b/docker/nginx/sites-available/runestone.template @@ -77,6 +77,8 @@ server { # # _`gunicorn socket`: This matches the ``--bind`` parameter when `gunicorn is run `. proxy_pass http://unix:/run/fastapi.sock:/; + # For file uploads we need a larger limit than 1M for whiteboard pictures and pdf uploads + client_max_body_size 25M; } location / { diff --git a/poetry.lock b/poetry.lock index f74a29e8e..afde1dfdf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -267,7 +267,7 @@ webencodings = "*" [[package]] name = "bookserver" -version = "1.3.11" +version = "1.3.14" description = "A new Runestone Server Framework" category = "main" optional = false @@ -303,7 +303,7 @@ url = "../../../BookServer" [[package]] name = "bookserver-dev" -version = "1.3.11" +version = "1.3.14" description = "A new Runestone Server Framework" category = "dev" optional = false @@ -332,6 +332,38 @@ tox = "^3.0.0" type = "directory" url = "../../../bookserver-dev" +[[package]] +name = "boto3" +version = "1.26.30" +description = "The AWS SDK for Python" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.29.30,<1.30.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.29.30" +description = "Low-level, data-driven core of boto 3." +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.15.3)"] + [[package]] name = "brotli" version = "1.0.9" @@ -1040,6 +1072,14 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "json2xml" version = "3.20.0" @@ -1952,7 +1992,7 @@ url = "rsmanage" [[package]] name = "runestone" -version = "6.3.24" +version = "6.3.25" description = "Sphinx extensions for writing interactive documents." category = "main" optional = false @@ -1976,7 +2016,7 @@ url = "../../../RunestoneComponents" [[package]] name = "runestone-dev" -version = "6.3.24" +version = "6.3.25" description = "Sphinx extensions for writing interactive documents." category = "dev" optional = false @@ -2012,6 +2052,20 @@ click = "*" type = "directory" url = "docker" +[[package]] +name = "s3transfer" +version = "0.6.0" +description = "An Amazon S3 Transfer Manager" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + [[package]] name = "selenium" version = "3.141.0" @@ -2583,7 +2637,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = ">= 3.8.5, < 4.0.0" -content-hash = "ef04dbd90f52d12c820d2c20db14ac3426428c40852507b1d10a850638d17f4e" +content-hash = "7095a43fed2328453c2ee540d6737d24bf9558a35885c6c0c4f8a27a675cde2a" [metadata.files] aiofiles = [ @@ -2716,6 +2770,14 @@ bleach = [ ] bookserver = [] bookserver-dev = [] +boto3 = [ + {file = "boto3-1.26.30-py3-none-any.whl", hash = "sha256:e222714a6a841f318d3b6557d915dcc3729ff286e9aa3d03b5d26d6bfce3a3bd"}, + {file = "boto3-1.26.30.tar.gz", hash = "sha256:13ba1d98ab5e2591be2dd19c779d67aa4210f126a827c9a376532ace435d8df9"}, +] +botocore = [ + {file = "botocore-1.29.30-py3-none-any.whl", hash = "sha256:6bfe917c022b92c093da448aae71b18f7dcbbbc69403f57ee39ca4775b2888e6"}, + {file = "botocore-1.29.30.tar.gz", hash = "sha256:9364417f53842167f8bcf72b9ab3c78457c7df613051101952b2470d9de7ea31"}, +] brotli = [ {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, @@ -3370,6 +3432,10 @@ jinja2 = [ {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, ] +jmespath = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] json2xml = [ {file = "json2xml-3.20.0-py3-none-any.whl", hash = "sha256:9399af5fa627631f4b7d1049b723ef6f2217d56507980f767328d6dff928400d"}, {file = "json2xml-3.20.0.tar.gz", hash = "sha256:ace0dc2d981964195d6e7a37ff2b4b199ab3a464c0ebb92bef8091aa238c6b8f"}, @@ -4236,6 +4302,10 @@ rsmanage = [] runestone = [] runestone-dev = [] runestone-docker-tools = [] +s3transfer = [ + {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, + {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, +] selenium = [ {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"}, {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"}, diff --git a/production/docker-compose.yml b/production/docker-compose.yml index c318d41d2..dbc130dc9 100644 --- a/production/docker-compose.yml +++ b/production/docker-compose.yml @@ -51,4 +51,7 @@ services: LOGIN_URL: "/runestone/default/user" ADS_FILE: '${ADS_FILE}' WORKER_NAME: ${HOSTNAME} + SPACES_KEY: ${SPACES_KEY} + SPACES_SECRET: ${SPACES_SECRET} + diff --git a/pyproject.toml b/pyproject.toml index 744b6e898..33b70bef5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ pretext = "^1.0.0" # Development dependencies # ======================== +boto3 = "^1.26.30" [tool.poetry.dev-dependencies] black = "~= 22.0" bookserver = { path = "../BookServer", develop = true } diff --git a/static/js/admin.js b/static/js/admin.js index 284f02965..39cde33d7 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -355,7 +355,8 @@ function createGradingPanel(element, acid, studentId, multiGrader) { } } $.getJSON("/runestone/admin/htmlsrc", data, async function (result) { - var htmlsrc = result; + var htmlsrc = result.htmlsrc; + const attachURL = result.attach_url; var enforceDeadline = $("#enforceDeadline").is(":checked"); var dl = showDeadline(); await renderRunestoneComponent(htmlsrc, elementID + ">#questiondisplay", { @@ -367,6 +368,7 @@ function createGradingPanel(element, acid, studentId, multiGrader) { tzoff: new Date().getTimezoneOffset() / 60, multiGrader: multiGrader, gradingContainer: elementID, + attachURL: attachURL, }); }); @@ -1816,6 +1818,8 @@ function create_question(formdata) { } // Given a question ID, preview it. +// This is NOT the function used to generate the grading panel on the grades page +// this is used in other places. function preview_question_id(question_id, preview_div, sid, gradeit) { if (arguments.length == 1) { preview_div = "component-preview"; @@ -1823,7 +1827,8 @@ function preview_question_id(question_id, preview_div, sid, gradeit) { // Request the preview HTML from the server. $.getJSON("/runestone/admin/htmlsrc", { acid: question_id, - }).done(function (html_src) { + }).done(function (jsonData) { + html_src = jsonData.htmlsrc // Render it. data = { acid: question_id }; if (sid) { @@ -2058,7 +2063,7 @@ async function renderRunestoneComponent(componentSrc, whereDiv, moreOpts) { } // $(`#${whereDiv}`).css("background-color", "white"); } - MathJax.Hub.Queue(["Typeset", MathJax.Hub]); + MathJax.typeset([document.querySelector(`#${whereDiv}`)]) } // Called by the "Search" button in the "Search question bank" panel. diff --git a/views/assignments/doAssignment.html b/views/assignments/doAssignment.html index 21167fd62..1b084d754 100644 --- a/views/assignments/doAssignment.html +++ b/views/assignments/doAssignment.html @@ -217,10 +217,6 @@
Not yet graded
selfGrade({{=assignment['id']}}) }); - $(document).ready(function(){ - $('[data-toggle="tooltip"]').tooltip(); - }); -