otter-service-stdalone 1.0.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/PKG-INFO +1 -1
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/setup.cfg +1 -1
- otter_service_stdalone-1.1.0/src/otter_service_stdalone/__init__.py +1 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/app.py +73 -16
- otter_service_stdalone-1.1.0/src/otter_service_stdalone/grade_notebooks.py +75 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/index.html +40 -5
- otter_service_stdalone-1.1.0/src/otter_service_stdalone/scripts/web_socket.js +127 -0
- otter_service_stdalone-1.1.0/src/otter_service_stdalone/static_files/README_DO_NOT_DISTRIBUTE.txt +9 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone.egg-info/PKG-INFO +1 -1
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone.egg-info/SOURCES.txt +2 -0
- otter_service_stdalone-1.0.0/src/otter_service_stdalone/__init__.py +0 -1
- otter_service_stdalone-1.0.0/src/otter_service_stdalone/grade_notebooks.py +0 -105
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/README.md +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/pyproject.toml +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/access_sops_keys.py +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/fs_logging.py +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/secrets/gh_key.dev.yaml +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/secrets/gh_key.local.yaml +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/secrets/gh_key.prod.yaml +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/secrets/gh_key.staging.yaml +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/static_templates/403.html +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/static_templates/500.html +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/upload_handle.py +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/user_auth.py +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone.egg-info/dependency_links.txt +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone.egg-info/entry_points.txt +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone.egg-info/top_level.txt +0 -0
- {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/tests/test_upload_handle.py +0 -0
@@ -23,7 +23,7 @@ python_requires = >=3.8
|
|
23
23
|
where = src
|
24
24
|
|
25
25
|
[options.package_data]
|
26
|
-
otter_service_stdalone = index.html, secrets/*.yaml, static_templates/*.html
|
26
|
+
otter_service_stdalone = index.html, secrets/*.yaml, static_templates/*.html, static_files/*, scripts/*
|
27
27
|
|
28
28
|
[options.entry_points]
|
29
29
|
console_scripts =
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "1.1.0"
|
{otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/app.py
RENAMED
@@ -4,10 +4,12 @@ import tornado.web
|
|
4
4
|
import tornado.auth
|
5
5
|
import os
|
6
6
|
import uuid
|
7
|
+
import tornado.websocket
|
7
8
|
from otter_service_stdalone import fs_logging as log
|
8
9
|
from otter_service_stdalone import user_auth as u_auth
|
9
10
|
from otter_service_stdalone import grade_notebooks
|
10
11
|
from zipfile import ZipFile, ZIP_DEFLATED
|
12
|
+
import queue
|
11
13
|
|
12
14
|
|
13
15
|
__UPLOADS__ = "/tmp/uploads"
|
@@ -16,6 +18,53 @@ log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
|
|
16
18
|
log_http = f'{os.environ.get("ENVIRONMENT")}-http-error'
|
17
19
|
|
18
20
|
authorization_states = {} # used to protect against cross-site request forgery attacks.
|
21
|
+
session_queues = {}
|
22
|
+
session_messages = {}
|
23
|
+
session_callbacks = {}
|
24
|
+
|
25
|
+
|
26
|
+
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
27
|
+
"""This handles updates to the application from otter-grader
|
28
|
+
"""
|
29
|
+
def open(self):
|
30
|
+
"""if logged in, start a callback once a second to see if there messages for the
|
31
|
+
logged in user
|
32
|
+
"""
|
33
|
+
if self.get_secure_cookie("user"):
|
34
|
+
user_id = self.get_secure_cookie("user").decode('utf-8')
|
35
|
+
if user_id not in session_callbacks:
|
36
|
+
session_callbacks[user_id] = tornado.ioloop.PeriodicCallback(lambda: self.send_results(user_id), 1000)
|
37
|
+
session_callbacks[user_id].start()
|
38
|
+
|
39
|
+
def on_message(self, message):
|
40
|
+
pass # No action needed on incoming message
|
41
|
+
|
42
|
+
def on_close(self):
|
43
|
+
"""stop the periodic classback on close
|
44
|
+
"""
|
45
|
+
if self.get_secure_cookie("user"):
|
46
|
+
user_id = self.get_secure_cookie("user").decode('utf-8')
|
47
|
+
if user_id in session_callbacks and session_callbacks[user_id].callback:
|
48
|
+
session_callbacks[user_id].stop()
|
49
|
+
session_callbacks.pop(user_id)
|
50
|
+
|
51
|
+
def send_results(self, user_id):
|
52
|
+
"""if there are messages for this user, send them to application by submission.
|
53
|
+
We save the messages in the session so if refresh on client side they still
|
54
|
+
have the updates
|
55
|
+
"""
|
56
|
+
try:
|
57
|
+
if user_id in session_queues:
|
58
|
+
user_queue_dict = session_queues[user_id]
|
59
|
+
user_messages_dict = session_messages[user_id]
|
60
|
+
if user_queue_dict:
|
61
|
+
for result_id, q in user_queue_dict.items():
|
62
|
+
if not q.empty():
|
63
|
+
while not q.empty():
|
64
|
+
user_messages_dict[result_id].append(q.get())
|
65
|
+
self.write_message({"messages": user_messages_dict})
|
66
|
+
except tornado.websocket.WebSocketClosedError:
|
67
|
+
log.write_logs("ws-error", "Web Socket Problem", "", "", log_error)
|
19
68
|
|
20
69
|
|
21
70
|
class HealthHandler(tornado.web.RequestHandler):
|
@@ -136,27 +185,26 @@ class Download(BaseHandler):
|
|
136
185
|
msg += "Please check the code or upload your notebooks "
|
137
186
|
msg += "and autograder.zip for grading again."
|
138
187
|
self.render("index.html", download_message=msg)
|
139
|
-
elif not os.path.exists(f"{directory}/
|
188
|
+
elif not os.path.exists(f"{directory}/final_grades.csv"):
|
140
189
|
m = "Download: Results Not Ready"
|
141
190
|
log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
|
142
191
|
msg = "The results of your download are not ready yet. "
|
143
192
|
msg += "Please check back."
|
144
193
|
self.render("index.html", download_message=msg, dcode=download_code)
|
145
194
|
else:
|
146
|
-
if not os.path.isfile(f"{directory}/final_grades.csv"):
|
147
|
-
m = "Download: final_grades.csv does not exist"
|
148
|
-
t = "Problem grading notebooks see stack trace"
|
149
|
-
log.write_logs(download_code, m, t, "debug", log_debug)
|
150
|
-
with open(f"{directory}/final_grades.csv", "a") as f:
|
151
|
-
m = "There was a problem grading your notebooks. Please see grading-logs.txt"
|
152
|
-
f.write(m)
|
153
|
-
f.close()
|
154
195
|
m = "Download Success: Creating results.zip"
|
155
196
|
log.write_logs(download_code, m, "", "debug", log_debug)
|
156
197
|
with ZipFile(f"{directory}/results.zip", 'w') as zipF:
|
157
|
-
|
158
|
-
|
159
|
-
|
198
|
+
final_file = "final_grades.csv"
|
199
|
+
if os.path.isfile(f"{directory}/{final_file}"):
|
200
|
+
zipF.write(f"{directory}/{final_file}", final_file, compress_type=ZIP_DEFLATED)
|
201
|
+
for filename in os.listdir(f"{directory}/grading-summaries"):
|
202
|
+
file_path = os.path.join(f"{directory}/grading-summaries", filename)
|
203
|
+
if os.path.isfile(file_path):
|
204
|
+
f_path = f"grading-summaries-do-not-distribute/{filename}"
|
205
|
+
zipF.write(f"{file_path}", f_path, compress_type=ZIP_DEFLATED)
|
206
|
+
read_me = os.path.join(os.path.dirname(__file__), "static_files", "README_DO_NOT_DISTRIBUTE.txt")
|
207
|
+
zipF.write(read_me, "README_DO_NOT_DISTRIBUTE.txt", compress_type=ZIP_DEFLATED)
|
160
208
|
|
161
209
|
self.set_header('Content-Type', 'application/octet-stream')
|
162
210
|
self.set_header("Content-Description", "File Transfer")
|
@@ -179,7 +227,7 @@ class Upload(BaseHandler):
|
|
179
227
|
|
180
228
|
Args:
|
181
229
|
tornado (tornado.web.RequestHandler): The upload request handler
|
182
|
-
"""
|
230
|
+
"""
|
183
231
|
@tornado.web.authenticated
|
184
232
|
def get(self):
|
185
233
|
# this just redirects to login and displays main page
|
@@ -189,6 +237,11 @@ class Upload(BaseHandler):
|
|
189
237
|
async def post(self):
|
190
238
|
"""this handles the post request and asynchronously launches the grader
|
191
239
|
"""
|
240
|
+
user = self.get_current_user()
|
241
|
+
user_id = user.decode('utf-8')
|
242
|
+
if user_id not in session_queues:
|
243
|
+
session_queues[user_id] = {}
|
244
|
+
session_messages[user_id] = {}
|
192
245
|
g = grade_notebooks.GradeNotebooks()
|
193
246
|
files = self.request.files
|
194
247
|
results_path = str(uuid.uuid4())
|
@@ -219,11 +272,13 @@ class Upload(BaseHandler):
|
|
219
272
|
fh.write(notebooks['body'])
|
220
273
|
m = "Step 3: Uploaded Files Written to Disk"
|
221
274
|
log.write_logs(results_path, m, f"Results Code: {results_path}", "debug", log_debug)
|
222
|
-
m = "Please save this code
|
223
|
-
m += f"in the \"Results\" section to the right: {results_path}"
|
275
|
+
m = "Please save this code; it appears in the \"Notebook Grading Progress\" section below. You can "
|
276
|
+
m += f"retrieve your files by submitting this code in the \"Results\" section to the right: {results_path}"
|
224
277
|
self.render("index.html", message=m)
|
225
278
|
try:
|
226
|
-
|
279
|
+
session_queues[user_id][results_path] = queue.Queue()
|
280
|
+
session_messages[user_id][results_path] = []
|
281
|
+
await g.grade(auto_p, notebooks_path, results_path, session_queues[user_id].get(results_path))
|
227
282
|
except Exception as e:
|
228
283
|
log.write_logs(results_path, "Grading Problem", str(e), "error", log_error)
|
229
284
|
else:
|
@@ -244,8 +299,10 @@ application = tornado.web.Application([
|
|
244
299
|
(r"/login", LoginHandler),
|
245
300
|
(r"/upload", Upload),
|
246
301
|
(r"/download", Download),
|
302
|
+
(r"/update", WebSocketHandler),
|
247
303
|
(r"/oauth_callback", GitHubOAuthHandler),
|
248
304
|
(r"/otterhealth", HealthHandler),
|
305
|
+
(r"/scripts/(.*)", tornado.web.StaticFileHandler, {"path": os.path.join(os.path.dirname(__file__), "scripts")}),
|
249
306
|
], **settings, debug=False)
|
250
307
|
|
251
308
|
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import asyncio
|
2
|
+
from otter_service_stdalone import fs_logging as log, upload_handle as uh
|
3
|
+
import os
|
4
|
+
from otter.grade import main as grade
|
5
|
+
|
6
|
+
log_debug = f'{os.environ.get("ENVIRONMENT")}-debug'
|
7
|
+
log_count = f'{os.environ.get("ENVIRONMENT")}-count'
|
8
|
+
log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
|
9
|
+
|
10
|
+
|
11
|
+
class GradeNotebooks():
|
12
|
+
"""The class contains the async grade method for executing
|
13
|
+
otter grader as well as a function for logging the number of
|
14
|
+
notebooks to be graded
|
15
|
+
"""
|
16
|
+
def count_ipynb_files(self, directory, extension):
|
17
|
+
"""this count the files for logging purposes"""
|
18
|
+
count = 0
|
19
|
+
for filename in os.listdir(directory):
|
20
|
+
if filename.endswith(extension):
|
21
|
+
count += 1
|
22
|
+
return count
|
23
|
+
|
24
|
+
async def grade(self, p, notebooks_path, results_id, user_queue):
|
25
|
+
"""Calls otter grade asynchronously and writes the various log files
|
26
|
+
and results of grading generating by otter-grader
|
27
|
+
|
28
|
+
Args:
|
29
|
+
p (str): the path to autograder.zip -- the solutions
|
30
|
+
notebooks_path (str): the path to the folder of notebooks to be graded
|
31
|
+
results_id (str): used for identifying logs
|
32
|
+
|
33
|
+
Raises:
|
34
|
+
Exception: Timeout Exception is raised if async takes longer than 20 min
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
boolean: True is the process completes; otherwise an Exception is thrown
|
38
|
+
"""
|
39
|
+
try:
|
40
|
+
notebook_folder = uh.handle_upload(notebooks_path, results_id)
|
41
|
+
notebook_count = self.count_ipynb_files(notebook_folder, ".ipynb")
|
42
|
+
log.write_logs(results_id, f"{notebook_count}",
|
43
|
+
"",
|
44
|
+
"info",
|
45
|
+
f'{os.environ.get("ENVIRONMENT")}-count')
|
46
|
+
log.write_logs(results_id, "Step 5: Notebook Folder configured and grading started",
|
47
|
+
f"Notebook Folder: {notebook_folder}",
|
48
|
+
"debug",
|
49
|
+
log_debug)
|
50
|
+
|
51
|
+
await grade(
|
52
|
+
name='grader',
|
53
|
+
autograder=p,
|
54
|
+
paths=(notebook_folder,),
|
55
|
+
containers=10,
|
56
|
+
timeout=300,
|
57
|
+
ext="ipynb",
|
58
|
+
output_dir=notebook_folder,
|
59
|
+
result_queue=user_queue
|
60
|
+
)
|
61
|
+
user_queue.put("Results available for download")
|
62
|
+
|
63
|
+
log.write_logs(results_id, "Step 6: Grading: Finished",
|
64
|
+
f"{notebook_folder}",
|
65
|
+
"debug",
|
66
|
+
log_debug)
|
67
|
+
log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
|
68
|
+
"",
|
69
|
+
"info",
|
70
|
+
log_error)
|
71
|
+
return True
|
72
|
+
except asyncio.TimeoutError:
|
73
|
+
raise Exception(f'Grading timed out for {notebook_folder}')
|
74
|
+
except Exception as e:
|
75
|
+
raise e
|
{otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.0}/src/otter_service_stdalone/index.html
RENAMED
@@ -4,6 +4,39 @@
|
|
4
4
|
<title>Upload Form</title>
|
5
5
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
6
6
|
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
7
|
+
<style>
|
8
|
+
/* Styles to make the div scrollable after a certain height */
|
9
|
+
#messages {
|
10
|
+
max-height: 400px;
|
11
|
+
overflow-y: auto; /* Enable vertical scrolling */
|
12
|
+
padding: 10px;
|
13
|
+
width: auto;
|
14
|
+
}
|
15
|
+
/* Styles for the collapsible div */
|
16
|
+
.collapsible {
|
17
|
+
border: 1px solid #ccc;
|
18
|
+
width: auto;
|
19
|
+
margin-bottom: 10px;
|
20
|
+
}
|
21
|
+
|
22
|
+
/* Styles for the header */
|
23
|
+
.header {
|
24
|
+
background-color: #f1f1f1;
|
25
|
+
padding: 10px;
|
26
|
+
cursor: pointer;
|
27
|
+
}
|
28
|
+
|
29
|
+
/* Styles for the content */
|
30
|
+
.content {
|
31
|
+
max-height: 300px;
|
32
|
+
overflow-y: auto; /* Enable vertical scrolling */
|
33
|
+
padding: 10px;
|
34
|
+
display: none;
|
35
|
+
}
|
36
|
+
|
37
|
+
|
38
|
+
</style>
|
39
|
+
<script src="/scripts/web_socket.js"></script>
|
7
40
|
</head>
|
8
41
|
<body>
|
9
42
|
<section class="container">
|
@@ -14,7 +47,7 @@
|
|
14
47
|
Cloudbank Otter Grading - Beta
|
15
48
|
</h1>
|
16
49
|
<h2 class="subtitle">
|
17
|
-
|
50
|
+
Upload your notebooks, grade them, and download results.
|
18
51
|
</h2>
|
19
52
|
</div>
|
20
53
|
</div>
|
@@ -84,15 +117,17 @@
|
|
84
117
|
{% end %}
|
85
118
|
</form>
|
86
119
|
</div>
|
87
|
-
|
120
|
+
</div>
|
121
|
+
<div id="messages">
|
122
|
+
<p class="title is-3">Notebook Grading Progress</p>
|
123
|
+
<p id="none-msg">No submissions are being graded</p>
|
124
|
+
</div>
|
88
125
|
</div>
|
89
126
|
</div>
|
90
127
|
<div class="hero is-info welcome is-small">
|
91
128
|
<div class="hero-body">
|
92
129
|
<div class="container">
|
93
|
-
<
|
94
|
-
Questions? Problems? email: sean.smorris at berkeley.edu
|
95
|
-
</h2>
|
130
|
+
<span class="has-text-right is-pulled-right">email: sean.smorris@berkeley.edu</span>
|
96
131
|
</div>
|
97
132
|
</div>
|
98
133
|
</div>
|
@@ -0,0 +1,127 @@
|
|
1
|
+
/**
|
2
|
+
* This is main function of the script. It sets up the web sockets and handles messages
|
3
|
+
* coming from the application. The last line of the script calls this function
|
4
|
+
*/
|
5
|
+
function connectWebSocket() {
|
6
|
+
var loc = window.location;
|
7
|
+
var wsStart = loc.protocol === "https:" ? "wss://" : "ws://";
|
8
|
+
var wsUrl = wsStart + loc.host + "/update";
|
9
|
+
|
10
|
+
var ws = new WebSocket(wsUrl);
|
11
|
+
submission_divs = {}
|
12
|
+
reconnectInterval = 3000;
|
13
|
+
ws.onopen = function() {
|
14
|
+
console.log("WebSocket is open now.");
|
15
|
+
reconnectInterval = 3000;
|
16
|
+
};
|
17
|
+
|
18
|
+
ws.onmessage = function(event) {
|
19
|
+
var messagesDiv = document.getElementById("messages");
|
20
|
+
var noneMsgP = document.getElementById("none-msg");
|
21
|
+
noneMsgP.style.display = "none"
|
22
|
+
json_msgs = JSON.parse(event.data)["messages"];
|
23
|
+
console.log(json_msgs)
|
24
|
+
Object.entries(json_msgs).forEach(([submission_key, messages], index) => {
|
25
|
+
if (!(submission_key in submission_divs)){
|
26
|
+
messagesDiv.appendChild(_setUpNewSubmission(submission_key, index, messages))
|
27
|
+
} else {
|
28
|
+
_updateSubmission(submission_key, index, messages)
|
29
|
+
}
|
30
|
+
|
31
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
32
|
+
});
|
33
|
+
};
|
34
|
+
|
35
|
+
ws.onclose = function() {
|
36
|
+
//if closes attempts re-connect every few seconds
|
37
|
+
console.log("WebSocket is closed now.");
|
38
|
+
setTimeout(connectWebSocket, 3000);
|
39
|
+
};
|
40
|
+
|
41
|
+
ws.onerror = function(error) {
|
42
|
+
console.error('WebSocket error:', error);
|
43
|
+
ws.close();
|
44
|
+
};
|
45
|
+
};
|
46
|
+
|
47
|
+
/**
|
48
|
+
* This function sets up the div that will report progress on the given submission
|
49
|
+
* in the application
|
50
|
+
* @param {*} submission_key doanload code - unique to submission
|
51
|
+
* @param {*} index - the submission number in this session for the user
|
52
|
+
* @param {*} messages - all the messages that need to be added to progress
|
53
|
+
*/
|
54
|
+
function _setUpNewSubmission(submission_key, index, messages){
|
55
|
+
var newMessage = document.createElement("div");
|
56
|
+
newMessage.id = submission_key
|
57
|
+
var newMessageHeader = document.createElement("div");
|
58
|
+
var newMessageContent = document.createElement("div");
|
59
|
+
newMessageContent.id = "msg-" + submission_key
|
60
|
+
newMessageHeader.id = "header-" + submission_key
|
61
|
+
var toggleSign = document.createElement("span");
|
62
|
+
var submissionHeader = document.createElement("span");
|
63
|
+
submissionHeader.innerHTML = " Submission Progress: Submission #" + (index+1)
|
64
|
+
submissionHeader.id = "sub-header-" + submission_key
|
65
|
+
newMessage.className = "collapsible"
|
66
|
+
newMessageHeader.className = "header"
|
67
|
+
toggleSign.innerHTML = '<i class="fas fa-minus-circle"></i>';
|
68
|
+
|
69
|
+
newMessageHeader.appendChild(toggleSign)
|
70
|
+
newMessageHeader.appendChild(submissionHeader)
|
71
|
+
newMessageContent.className = "content"
|
72
|
+
newMessageContent.style.display = "block"
|
73
|
+
newMessage.appendChild(newMessageHeader)
|
74
|
+
newMessage.appendChild(newMessageContent)
|
75
|
+
submission_divs[submission_key] = newMessage
|
76
|
+
const li = document.createElement('li');
|
77
|
+
li.textContent = "Download Code: " + submission_key;
|
78
|
+
newMessageContent.appendChild(li);
|
79
|
+
for (const m of messages) {
|
80
|
+
const li = document.createElement('li');
|
81
|
+
li.textContent = m;
|
82
|
+
newMessageContent.appendChild(li);
|
83
|
+
}
|
84
|
+
// Toggle the visibility of the scrollable content
|
85
|
+
newMessageHeader.addEventListener('click', () => {
|
86
|
+
if (newMessageContent.style.display == 'none') {
|
87
|
+
newMessageContent.style.display = 'block';
|
88
|
+
toggleSign.innerHTML = '<i class="fas fa-minus-circle"></i>';
|
89
|
+
} else {
|
90
|
+
newMessageContent.style.display = 'none';
|
91
|
+
toggleSign.innerHTML = '<i class="fas fa-plus-circle"></i>';
|
92
|
+
}
|
93
|
+
});
|
94
|
+
return newMessage;
|
95
|
+
}
|
96
|
+
|
97
|
+
/**
|
98
|
+
* This updates the progress div for this submission
|
99
|
+
*
|
100
|
+
* @param {*} submission_key doanload code - unique to submission
|
101
|
+
* @param {*} index - the submission number in this session for the user
|
102
|
+
* @param {*} messages - all the messages that need to be added to progress
|
103
|
+
*/
|
104
|
+
function _updateSubmission(submission_key, index, messages){
|
105
|
+
var newMessageContent = document.getElementById("msg-" + submission_key);
|
106
|
+
var items = newMessageContent.getElementsByTagName("li");
|
107
|
+
var msgs = []
|
108
|
+
for (var i = 0; i < items.length; ++i) {
|
109
|
+
msgs.push(items[i].textContent)
|
110
|
+
}
|
111
|
+
for (const m of messages) {
|
112
|
+
if(!(msgs.includes(m))){
|
113
|
+
const li = document.createElement('li');
|
114
|
+
li.textContent = m;
|
115
|
+
newMessageContent.appendChild(li);
|
116
|
+
}
|
117
|
+
}
|
118
|
+
if(messages.includes("Results available for download")){
|
119
|
+
var header = document.getElementById("header-" + submission_key)
|
120
|
+
header.style.backgroundColor = '#7BAE37';
|
121
|
+
var subheader = document.getElementById("sub-header-" + submission_key)
|
122
|
+
subheader.innerHTML = " Submission Progress: Submission #" + (index+1) + "</br>Download Code: " + submission_key;
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
|
127
|
+
connectWebSocket()
|
otter_service_stdalone-1.1.0/src/otter_service_stdalone/static_files/README_DO_NOT_DISTRIBUTE.txt
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Please do not distribute the grade summaries in the folder, grading-summaries-do-not-distribute!
|
2
|
+
|
3
|
+
These summaries contain the answers to all questions, including the hidden tests
|
4
|
+
run by the grader.
|
5
|
+
|
6
|
+
We have included these files so you can see what errors were made by a student
|
7
|
+
on their notebook without having to re-run the notebook yourself.
|
8
|
+
|
9
|
+
Thanks!
|
@@ -14,10 +14,12 @@ src/otter_service_stdalone.egg-info/SOURCES.txt
|
|
14
14
|
src/otter_service_stdalone.egg-info/dependency_links.txt
|
15
15
|
src/otter_service_stdalone.egg-info/entry_points.txt
|
16
16
|
src/otter_service_stdalone.egg-info/top_level.txt
|
17
|
+
src/otter_service_stdalone/scripts/web_socket.js
|
17
18
|
src/otter_service_stdalone/secrets/gh_key.dev.yaml
|
18
19
|
src/otter_service_stdalone/secrets/gh_key.local.yaml
|
19
20
|
src/otter_service_stdalone/secrets/gh_key.prod.yaml
|
20
21
|
src/otter_service_stdalone/secrets/gh_key.staging.yaml
|
22
|
+
src/otter_service_stdalone/static_files/README_DO_NOT_DISTRIBUTE.txt
|
21
23
|
src/otter_service_stdalone/static_templates/403.html
|
22
24
|
src/otter_service_stdalone/static_templates/500.html
|
23
25
|
tests/test_upload_handle.py
|
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "1.0.0"
|
@@ -1,105 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
import async_timeout
|
3
|
-
from otter_service_stdalone import fs_logging as log, upload_handle as uh
|
4
|
-
import os
|
5
|
-
|
6
|
-
log_debug = f'{os.environ.get("ENVIRONMENT")}-debug'
|
7
|
-
log_count = f'{os.environ.get("ENVIRONMENT")}-count'
|
8
|
-
log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
|
9
|
-
|
10
|
-
|
11
|
-
class GradeNotebooks():
|
12
|
-
"""The class contains the async grade method for executing
|
13
|
-
otter grader as well as a function for logging the number of
|
14
|
-
notebooks to be graded
|
15
|
-
"""
|
16
|
-
|
17
|
-
def count_ipynb_files(self, directory, extension):
|
18
|
-
"""this count the files for logging purposes"""
|
19
|
-
count = 0
|
20
|
-
for filename in os.listdir(directory):
|
21
|
-
if filename.endswith(extension):
|
22
|
-
count += 1
|
23
|
-
return count
|
24
|
-
|
25
|
-
async def grade(self, p, notebooks_path, results_id):
|
26
|
-
"""Calls otter grade asynchronously and writes the various log files
|
27
|
-
and results of grading generating by otter-grader
|
28
|
-
|
29
|
-
Args:
|
30
|
-
p (str): the path to autograder.zip -- the solutions
|
31
|
-
notebooks_path (str): the path to the folder of notebooks to be graded
|
32
|
-
results_id (str): used for identifying logs
|
33
|
-
|
34
|
-
Raises:
|
35
|
-
Exception: Timeout Exception is raised if async takes longer than 20 min
|
36
|
-
|
37
|
-
Returns:
|
38
|
-
boolean: True is the process completes; otherwise an Exception is thrown
|
39
|
-
"""
|
40
|
-
try:
|
41
|
-
notebook_folder = uh.handle_upload(notebooks_path, results_id)
|
42
|
-
notebook_count = self.count_ipynb_files(notebook_folder, ".ipynb")
|
43
|
-
log.write_logs(results_id, f"{notebook_count}",
|
44
|
-
"",
|
45
|
-
"info",
|
46
|
-
f'{os.environ.get("ENVIRONMENT")}-count')
|
47
|
-
log.write_logs(results_id, "Step 5: Notebook Folder configured for grader",
|
48
|
-
f"Notebook Folder: {notebook_folder}",
|
49
|
-
"debug",
|
50
|
-
log_debug)
|
51
|
-
command = [
|
52
|
-
'otter', 'grade',
|
53
|
-
'-n', 'grader',
|
54
|
-
'-a', p,
|
55
|
-
notebook_folder,
|
56
|
-
"--ext", "ipynb",
|
57
|
-
"--containers", "10",
|
58
|
-
"--timeout", "15",
|
59
|
-
"-o", notebook_folder,
|
60
|
-
"-v"
|
61
|
-
]
|
62
|
-
log.write_logs(results_id, f"Step 6: Grading Start: {notebook_folder}",
|
63
|
-
" ".join(command),
|
64
|
-
"debug",
|
65
|
-
log_debug)
|
66
|
-
process = await asyncio.create_subprocess_exec(
|
67
|
-
*command,
|
68
|
-
stdin=asyncio.subprocess.PIPE,
|
69
|
-
stdout=asyncio.subprocess.PIPE,
|
70
|
-
stderr=asyncio.subprocess.PIPE
|
71
|
-
)
|
72
|
-
|
73
|
-
# this is waiting for communication back from the process
|
74
|
-
# some images are quite big and take some time to build the first
|
75
|
-
# time through - like 20 min for otter-grader
|
76
|
-
async with async_timeout.timeout(2000):
|
77
|
-
stdout, stderr = await process.communicate()
|
78
|
-
|
79
|
-
with open(f"{notebook_folder}/grading-output.txt", "w") as f:
|
80
|
-
for line in stdout.decode().splitlines():
|
81
|
-
f.write(line + "\n")
|
82
|
-
log.write_logs(results_id, "Step 7: Grading: Finished: Write: grading-output.txt",
|
83
|
-
f"{notebook_folder}/grading-output.txt",
|
84
|
-
"debug",
|
85
|
-
log_debug)
|
86
|
-
with open(f"{notebook_folder}/grading-logs.txt", "w") as f:
|
87
|
-
for line in stderr.decode().splitlines():
|
88
|
-
f.write(line + "\n")
|
89
|
-
log.write_logs(results_id, "Step 8: Grading: Finished: Write grading-logs.txt",
|
90
|
-
f"{notebook_folder}/grading-logs.txt",
|
91
|
-
"debug",
|
92
|
-
log_debug)
|
93
|
-
log.write_logs(results_id, f"Step 9: Grading: Finished: {notebook_folder}",
|
94
|
-
" ".join(command),
|
95
|
-
"debug",
|
96
|
-
log_debug)
|
97
|
-
log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
|
98
|
-
" ".join(command),
|
99
|
-
"info",
|
100
|
-
log_error)
|
101
|
-
return True
|
102
|
-
except asyncio.TimeoutError:
|
103
|
-
raise Exception(f'Grading timed out for {notebook_folder}')
|
104
|
-
except Exception as e:
|
105
|
-
raise e
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|