def test_score_staff(self): self._test_backup(True) user = User.lookup(self.user1.email) self.login(self.staff1.email) response = self.client.post('/api/v3/score/') self.assert_400(response) assert response.json['code'] == 400 backup = Backup.query.filter(Backup.submitter_id == user.id).first() data = {'bid': encode_id(backup.id), 'kind': 'Total', 'score': 128.2, 'message': 'wow'} response = self.client.post('/api/v3/score/', data=data) self.assert_200(response) assert response.json['code'] == 200 self.logout() self.login(self.admin.email) data = {'bid': encode_id(backup.id), 'kind': 'Total', 'score': 128.2, 'message': 'wow'} response = self.client.post('/api/v3/score/', data=data) self.assert_200(response) assert response.json['code'] == 200
def test_score_staff(self): self._test_backup(True) user = User.lookup(self.user1.email) self.login(self.staff1.email) response = self.client.post('/api/v3/score/') self.assert_400(response) assert response.json['code'] == 400 backup = Backup.query.filter(Backup.submitter_id == user.id).first() data = { 'bid': encode_id(backup.id), 'kind': 'Total', 'score': 128.2, 'message': 'wow' } response = self.client.post('/api/v3/score/', data=data) self.assert_200(response) assert response.json['code'] == 200 self.logout() self.login(self.admin.email) data = { 'bid': encode_id(backup.id), 'kind': 'Total', 'score': 128.2, 'message': 'wow' } response = self.client.post('/api/v3/score/', data=data) self.assert_200(response) assert response.json['code'] == 200
def autograde_assignment(assignment, ag_assign_key, token, autopromotion=True): """ Autograde all enrolled students for this assignment. If ag_assign_key is 'test', the autograder will respond with 'OK' but not grade. @assignment: Assignment object. @ag_assign_key: Autograder ID (from Autograder Dashboard) @token: OK Access Token (from auth) """ students, submissions, no_submissions = assignment.course_submissions() backups_to_grade = [utils.encode_id(bid) for bid in submissions] if autopromotion: # Hunt for backups from those with no_submissions seen = set() for student_uid in no_submissions: if student_uid not in seen: found_backup = assignment.backups([student_uid]).first() if found_backup: seen |= found_backup.owners() backups_to_grade.append(utils.encode_id(found_backup.id)) found_backup.submit = True db.session.commit() data = { 'subm_ids': backups_to_grade, 'assignment': ag_assign_key, 'access_token': token, 'priority': 'default', 'backup_url': url_for('api.backup', _external=True), 'ok-server-version': 'v3', 'testing': token == 'testing', } return send_autograder('/api/ok/v3/grade/batch', data)
def test_hashids(self): """Tests converting hashes in URLs to IDs. Do not change the values in this test. """ assert utils.encode_id(314159) == 'aAPZ9j' assert utils.decode_id('aAPZ9j') == 314159 assert utils.encode_id(11235) == 'b28KJe' assert utils.decode_id('b28KJe') == 11235 self.assertRaises(ValueError, utils.decode_id, 'deadbeef')
def test_hashids(self): """Tests converting hashes in URLs to IDs. Do not change the values in this test. """ assert self.app.url_map.converters['hashid'] == utils.HashidConverter assert utils.encode_id(314159) == 'aAPZ9j' assert utils.decode_id('aAPZ9j') == 314159 assert utils.encode_id(11235) == 'b28KJe' assert utils.decode_id('b28KJe') == 11235 self.assertRaises(ValidationError, utils.decode_id, 'deadbeef')
def post(self, user, key=None): if key is not None: restful.abort(405) try: backup = self.schema.store_backup(user) except ValueError as e: data = {'backup': True} if 'late' in str(e).lower(): data['late'] = True return restful.abort(403, message=str(e), data=data) assignment = backup.assignment # Only accept revision if the assignment has revisions enabled if not assignment.revisions_allowed: return restful.abort(403, message=("Revisions are not enabled for {}" .format(assignment.name)), data={'backup': True, 'late': True}) # Only accept revision if the user has a FS group = assignment.active_user_ids(user.id) fs = assignment.final_submission(group) if not fs: return restful.abort(403, message="No Submission to Revise", data={}) # Get previous revision, (There should only be one) previous_revision = assignment.revision(group) if previous_revision: for score in previous_revision.scores: if score.kind == "revision": score.archive() models.db.session.commit() fs_url = url_for('student.code', name=assignment.name, submit=fs.submit, bid=encode_id(fs.id), _external=True) assignment_creator = models.User.get_by_id(assignment.creator_id) make_score(assignment_creator, backup, 2.0, "Revision for {}".format(fs_url), "revision") backup_url = url_for('student.code', name=assignment.name, submit=backup.submit, bid=encode_id(backup.id), _external=True) return { 'email': current_user.email, 'key': encode_id(backup.id), 'url': backup_url, 'course': assignment.course, 'assignment': assignment.name, }
def post(self, user, key=None): if key is not None: restful.abort(405) try: backup = self.schema.store_backup(user) except ValueError as e: data = {'backup': True} if 'late' in str(e).lower(): data['late'] = True return restful.abort(403, message=str(e), data=data) assignment = backup.assignment # Only accept revision if the assignment has revisions enabled if not assignment.revisions_allowed: return restful.abort(403, message="Revisions are not enabled for this assignment", data={'backup': True, 'late': True}) # Only accept revision if the user has a FS group = assignment.active_user_ids(user.id) fs = assignment.final_submission(group) if not fs: return restful.abort(403, message="No Submission to Revise", data={}) # Get previous revision, (There should only be one) previous_revision = assignment.revision(group) if previous_revision: for score in previous_revision.scores: if score.kind == "revision": score.archive() models.db.session.commit() fs_url = url_for('student.code', name=assignment.name, submit=fs.submit, bid=encode_id(fs.id), _external=True) assignment_creator = models.User.get_by_id(assignment.creator_id) make_score(assignment_creator, backup, 0.0, "Revision for {}".format(fs_url), "revision") backup_url = url_for('student.code', name=assignment.name, submit=backup.submit, bid=encode_id(backup.id), _external=True) return { 'email': current_user.email, 'key': encode_id(backup.id), 'url': backup_url, 'course': assignment.course, 'assignment': assignment.name, }
def test_edit_extension(self): ext = self._make_ext(self.assignment, self.user1) self.login(self.staff1.email) expires = (dt.datetime.utcnow() + dt.timedelta(days=3)).strftime( constants.ISO_DATETIME_FMT) custom_submission_time = dt.datetime.utcnow().strftime( constants.ISO_DATETIME_FMT) message = 'Sickness' data = { 'assignment_id': self.assignment.id, 'email': self.user1.email, 'expires': expires, 'reason': message, 'submission_time': 'other', 'custom_submission_time': custom_submission_time } self.assert200( self.client.post('/admin/course/{}/extensions/{}'.format( self.course.id, utils.encode_id(ext.id)), data=data, follow_redirects=True)) extension = Extension.get_extension(self.user1, self.assignment, time=dt.datetime.utcnow()) self.assertEqual(extension.staff_id, self.staff1.id) self.assertEqual(extension.assignment_id, self.assignment.id) self.assertEqual(extension.message, message) self.assertEqual( utils.local_time_obj(extension.expires, self.course).replace(tzinfo=None), dt.datetime.strptime(expires, '%Y-%m-%d %H:%M:%S')) self.assertEqual( utils.local_time_obj(extension.custom_submission_time, self.course).replace(tzinfo=None), dt.datetime.strptime(custom_submission_time, '%Y-%m-%d %H:%M:%S'))
def test_binary_download(self): self.login(self.staff1.email) encoded_id = utils.encode_id(self.file2.id) url = "/files/{0}".format(encoded_id) headers, data = self.fetch_file(url) self.verify_download_headers(headers, self.file2.filename, "image/svg+xml") self.verify_binary_download(CWD + "/../server/static/img/logo.svg", data)
def test_api_download(self): self.login(self.staff1.email) encoded_id = utils.encode_id(self.file1.id) url = "/api/v3/file/{0}".format(encoded_id) headers, data = self.fetch_file(url) self.verify_download_headers(headers, self.file1.filename, "text/plain; charset=utf-8") self.verify_text_download(CWD + "/files/fizzbuzz_after.py", data)
def test_comment_staff(self): self._test_backup(True) user = User.lookup(self.user1.email) self.login(self.staff1.email) backup = Backup.query.filter(Backup.submitter_id == user.id).first() comment_url = "/api/v3/backups/{}/comment/".format(encode_id( backup.id)) response = self.client.post(comment_url) self.assert_400(response) # Not all fields present assert response.json['code'] == 400 data = {'line': 2, 'filename': 'fizzbuzz.py', 'message': 'wow'} response = self.client.post(comment_url, data=data) self.assert_200(response) assert response.json['code'] == 200 self.logout() self.login(self.admin.email) data = {'line': 2, 'filename': 'fizzbuzz.py', 'message': 'wow'} response = self.client.post(comment_url, data=data) self.assert_200(response) assert response.json['code'] == 200 # Check that another student is not able to comment self.login(self.user2.email) data = {'line': 2, 'filename': 'fizzbuzz.py', 'message': 'wow'} response = self.client.post(comment_url, data=data) self.assert_403(response) assert response.json['code'] == 403
def post(self, user, key=None): if key is not None: restful.abort(405) try: backup = self.schema.store_backup(user) except ValueError as e: data = {'backup': True} if 'late' in str(e).lower(): data['late'] = True return restful.abort(403, message=str(e), data=data) assignment = backup.assignment return { 'email': current_user.email, 'key': encode_id(backup.id), 'url': url_for('student.code', name=assignment.name, submit=backup.submit, bid=backup.id, _external=True), 'course': assignment.course, 'assignment': assignment.name }
def upload_scores(canvas_assignment_id): logger = jobs.get_job_logger() canvas_assignment = CanvasAssignment.query.get(canvas_assignment_id) canvas_course = canvas_assignment.canvas_course assignment = canvas_assignment.assignment course = assignment.course logger.info('Starting bCourses upload') logger.info('bCourses assignment URL: {}'.format(canvas_assignment.url)) logger.info('OK assignment: {}'.format(assignment.display_name)) logger.info('Scores: {}'.format(', '.join(canvas_assignment.score_kinds))) students = api.get_students(canvas_course) old_scores = api.get_scores(canvas_assignment) new_scores = {} stats = collections.Counter() row_format = '{!s:>10} {!s:<55} {!s:<6} {!s:>9} {!s:>9}' logger.info(row_format.format('STUDENT ID', 'EMAIL', 'BACKUP', 'OLD SCORE', 'NEW SCORE')) for student in students: canvas_user_id = student['id'] sid = student['sis_user_id'] enrollments = Enrollment.query.filter_by( course_id=canvas_course.course_id, sid=sid, role=constants.STUDENT_ROLE, ).all() emails = ','.join(enrollment.user.email for enrollment in enrollments) or 'None' scores = [] for enrollment in enrollments: user_ids = assignment.active_user_ids(enrollment.user_id) scores.extend(assignment.scores(user_ids)) scores = [s for s in scores if s.kind in canvas_assignment.score_kinds] old_score = old_scores.get(canvas_user_id) if not scores: new_score = None backup_id = None stats['no_scores'] += 1 else: max_score = max(scores, key=lambda score: score.score) new_score = max_score.score backup_id = encode_id(max_score.backup_id) if old_score != new_score: new_scores[canvas_user_id] = new_score stats['updated'] += 1 else: stats['not_changed'] += 1 logger.info(row_format.format(sid, emails, backup_id, old_score, new_score)) if new_scores: api.put_scores(canvas_assignment, new_scores) stats = ('{updated} updated, {not_changed} not changed, ' '{no_scores} no scores'.format(**stats)) logger.info(stats) return stats
def test_job(duration=0, should_fail=False, make_file=False): logger = jobs.get_job_logger() logger.info('Starting...') time.sleep(duration) if should_fail: 1/0 if make_file: upload = ExternalFile.upload(data(duration+1), user_id=1, course_id=1, name='temp.okfile', prefix='jobs/example/') logger.info("Saved as: {}".format(upload.object_name)) logger.info('File ID: {0}'.format(encode_id(upload.id))) msg = ("Waited for <a href='/files/{0}'> {1} seconds </a>" .format(encode_id(upload.id), duration)) else: msg = "Waited for <b>{}</b> seconds!".format(duration) logger.info('Finished!') return msg
def retry_task(task): if task.retries >= MAX_RETRIES: logger.error('Did not receive a score for backup {} after {} retries'.format( utils.encode_id(task.backup_id), MAX_RETRIES)) task.set_status(GradingStatus.FAILED) else: task.set_status(GradingStatus.QUEUED) task.job_id = autograde_backup(token, assignment, task.backup_id) task.retries += 1
def test_binary_download(self): self.login(self.staff1.email) encoded_id = utils.encode_id(self.file2.id) url = "/files/{0}".format(encoded_id) headers, data = self.fetch_file(url) self.verify_download_headers(headers, self.file2.filename, "image/svg+xml; charset=utf-8") self.verify_binary_download(CWD + "/../server/static/img/logo.svg", data)
def test_binary_download(self): self.login(self.staff1.email) encoded_id = utils.encode_id(self.file2.id) url = "/files/{0}".format(encoded_id) response = self.client.get(url) self.assert200(response) self.assertEquals("attachment; filename={0!s}".format(self.file2.filename), response.headers.get('Content-Disposition')) self.assertEquals(response.headers['Content-Type'], 'image/svg+xml') self.assertEquals(response.headers['X-Content-Type-Options'], 'nosniff')
def test_binary_api_download(self): self.login(self.staff1.email) encoded_id = utils.encode_id(self.file2.id) url = "/api/v3/file/{0}".format(encoded_id) self.assertEqual(self.file2.download_link, url) headers, data = self.fetch_file(url) self.verify_download_headers(headers, self.file2.filename, "image/svg+xml") self.verify_binary_download(CWD + "/../server/static/img/logo.svg", data)
def test_folders(self): filename = "tests/hof.py" contents = "tests = {\nstatus: 'locked'\n}" self._add_file(filename, contents) encoded_id = utils.encode_id(self.backup.id) submit_str = "submissions" if self.backup.submit else "backups" url = "/{0}/{1}/{2}/download/{3}".format(self.assignment.name, submit_str, encoded_id, filename) response = self.client.get(url) self.assert_200(response) self.assertEqual(contents, response.data.decode('UTF-8'))
def retry_task(task): if task.retries >= MAX_RETRIES: logger.error( 'Did not receive a score for backup {} after {} retries'. format(utils.encode_id(task.backup_id), MAX_RETRIES)) task.set_status(GradingStatus.FAILED) else: task.set_status(GradingStatus.QUEUED) task.job_id = autograde_backup(token, assignment, task.backup_id) task.retries += 1
def test_wrong_student(self): filename = "test.py" contents = "x = 4" self._add_file(filename, contents) self.login('*****@*****.**') encoded_id = utils.encode_id(self.backup.id) submit_str = "submissions" if self.backup.submit else "backups" url = "/{0}/{1}/{2}/download/{3}".format(self.assignment.name, submit_str, encoded_id, filename) response = self.client.get(url) self.assert_404(response)
def test_get_backup(self): self._test_backup(False) backup = Backup.query.first() submission_time = (self.assignment.due_date - dt.timedelta(days=random.randrange(0, 10))) backup.custom_submission_time = submission_time response = self.client.get('/api/v3/backups/{}/'.format(backup.hashid)) self.assert_200(response) course = backup.assignment.course user_json = { "email": backup.submitter.email, "id": encode_id(backup.submitter_id), } response_json = response.json['data'] time_threshold = dt.timedelta(seconds=5) self.assertAlmostEqual(dateutil.parser.parse(response_json['created']), backup.created, delta=time_threshold) self.assertAlmostEqual(dateutil.parser.parse(response_json['submission_time']), submission_time, delta=time_threshold) self.assertAlmostEqual(dateutil.parser.parse(response_json['messages'][0]['created']), backup.created, delta=time_threshold) # Unset timestamps already tested. del response_json['created'] del response_json['submission_time'] del response_json['messages'][0]['created'] assert response_json == { "submitter": user_json, "submit": backup.submit, "group": [user_json], "is_late": backup.is_late, "external_files": [], "assignment": { "name": backup.assignment.name, "course": { "id": course.id, "active": course.active, "display_name": course.display_name, "offering": course.offering, "timezone": course.timezone.zone, }, }, "id": backup.hashid, "messages": [ { "kind": "file_contents", "contents": backup.files(), }, ], }
def post(self, user, key=None): if key is not None: restful.abort(405) try: backup = self.schema.store_backup(user) except ValueError as e: data = {'backup': True} if 'late' in str(e).lower(): data['late'] = True return restful.abort(403, message=str(e), data=data) assignment = backup.assignment return { 'email': current_user.email, 'key': encode_id(backup.id), 'url': url_for('student.code', name=assignment.name, submit=backup.submit, bid=encode_id(backup.id), _external=True), 'course': assignment.course, 'assignment': assignment.name }
def test_unauth_download(self): self.login(self.user1.email) encoded_id = utils.encode_id(self.file1.id) url = "/files/{0}".format(encoded_id) response = self.client.get(url) self.assert404(response) # Should also fail via the API url = "/api/v3/file/{0}".format(encoded_id) response = self.client.get(url) self.assert404(response)
def upload_scores(canvas_assignment_id): logger = jobs.get_job_logger() canvas_assignment = CanvasAssignment.query.get(canvas_assignment_id) canvas_course = canvas_assignment.canvas_course assignment = canvas_assignment.assignment course = assignment.course logger.info('Starting bCourses upload') logger.info('bCourses assignment URL: {}'.format(canvas_assignment.url)) logger.info('OK assignment: {}'.format(assignment.display_name)) logger.info('Scores: {}'.format(', '.join(canvas_assignment.score_kinds))) students = api.get_students(canvas_course) old_scores = api.get_scores(canvas_assignment) new_scores = {} stats = collections.Counter() row_format = '{!s:>10} {!s:<55} {!s:<6} {!s:>9} {!s:>9}' logger.info(row_format.format('STUDENT ID', 'EMAIL', 'BACKUP', 'OLD SCORE', 'NEW SCORE')) for student in students: canvas_user_id = student['id'] sid = student['sis_user_id'] enrollments = Enrollment.query.filter_by( course_id=canvas_course.course_id, sid=sid, role=constants.STUDENT_ROLE, ).all() emails = ','.join(enrollment.user.email for enrollment in enrollments) or 'None' scores = [] for enrollment in enrollments: user_ids = assignment.active_user_ids(enrollment.user_id) scores.extend(assignment.scores(user_ids)) scores = [s for s in scores if s.kind in canvas_assignment.score_kinds] old_score = old_scores.get(canvas_user_id) if not scores: new_score = None backup_id = None stats['no_scores'] += 1 else: max_score = max(scores, key=lambda score: score.score) new_score = max_score.score backup_id = encode_id(max_score.backup_id) if old_score != new_score: new_scores[canvas_user_id] = new_score stats['updated'] += 1 else: stats['not_changed'] += 1 logger.info(row_format.format(sid, emails, backup_id, old_score, new_score)) api.put_scores(canvas_assignment, new_scores) logger.info('{updated} updated, {not_changed} not changed, ' '{no_scores} no scores'.format(**stats))
def test_api_download(self): self.login(self.staff1.email) encoded_id = utils.encode_id(self.file1.id) url = "/api/v3/file/{0}".format(encoded_id) response = self.client.get(url) self.assert200(response) self.assertEquals("attachment; filename={0!s}".format(self.file1.filename), response.headers.get('Content-Disposition')) self.assertEquals(response.headers['Content-Type'], 'text/plain; charset=utf-8') self.assertEquals(response.headers['X-Content-Type-Options'], 'nosniff') with open(CWD + "/files/fizzbuzz_after.py", 'r') as f: self.assertEqual(f.read(), response.data.decode('UTF-8'))
def grade_single(backup, ag_assign_key, token): data = { 'subm_ids': [utils.encode_id(backup.id)], 'assignment': ag_assign_key, 'access_token': token, 'priority': 'default', 'backup_url': url_for('api.backup', _external=True), 'ok-server-version': 'v3', 'testing': token == 'testing', } return send_autograder('/api/ok/v3/grade/batch', data)
def test_incorrect_hash(self): filename = "test.py" contents = "x = 4" self._add_file(filename, contents) encoded_id = utils.encode_id(self.backup.id) submit_str = "submissions" if self.backup.submit else "backups" url = "/{0}/{1}/{2}/download/{3}".format(self.assignment.name, submit_str, "xxxxx", filename) response = self.client.get(url) self.assert_404(response) url = "/{0}/{1}/{2}/download/{3}".format(self.assignment.name, submit_str, "123", filename) response = self.client.get(url) self.assert_404(response)
def test_job(duration=0, should_fail=False, make_file=False): logger = jobs.get_job_logger() logger.info('Starting...') time.sleep(duration) if should_fail: 1 / 0 if make_file: upload = ExternalFile.upload(data(duration + 1), user_id=1, course_id=1, name='temp.okfile', prefix='jobs/example/') logger.info("Saved as: {}".format(upload.object_name)) logger.info('File ID: {0}'.format(encode_id(upload.id))) msg = ("Waited for <a href='/files/{0}'> {1} seconds </a>".format( encode_id(upload.id), duration)) else: msg = "Waited for <b>{}</b> seconds!".format(duration) logger.info('Finished!') return msg
def test_unicode(self): filename = "test.py" contents = "⚡️ 🔥 💥 ❄️" self._add_file(filename, contents) encoded_id = utils.encode_id(self.backup.id) submit_str = "submissions" if self.backup.submit else "backups" url = "/{0}/{1}/{2}/download/{3}".format(self.assignment.name, submit_str, encoded_id, filename) response = self.client.get(url) self.assert_200(response) self.assertEqual(contents, response.data.decode('UTF-8'))
def test_raw(self): filename = "test.py" contents = "x = 4" self._add_file(filename, contents) encoded_id = utils.encode_id(self.backup.id) submit_str = "submissions" if self.backup.submit else "backups" url = "/{0}/{1}/{2}/download/{3}?raw=1".format(self.assignment.name, submit_str, encoded_id, filename) response = self.client.get(url) self.assert_200(response) self.assertTrue('inline' in response.headers['Content-Disposition']) self.assertEquals(response.headers['Content-Type'], 'text/plain; charset=UTF-8') self.assertEquals(response.headers['X-Content-Type-Options'], 'nosniff') self.assertEqual(contents, response.data.decode('UTF-8'))
def test_incorrect_submit_boolean(self): filename = "test.py" contents = "x = 4" self._add_file(filename, contents) encoded_id = utils.encode_id(self.backup.id) wrong_submit_str = "backups" if self.backup.submit else "submissions" # intentionally flipped correct_submit_str = "submissions" if self.backup.submit else "backups" url = "/{0}/{1}/{2}/download/{3}" wrong_url = url.format(self.assignment.name, wrong_submit_str, encoded_id, filename) redir_url = url.format(self.assignment.name, correct_submit_str, encoded_id, filename) response = self.client.get(wrong_url) self.assertRedirects(response, redir_url) response = self.client.get(redir_url) self.assertEqual(contents, response.data.decode('UTF-8'))
def test_admin_student_assign_diff_overview(self): self._login(role="admin") backups = [] for _ in range(3): backup = self._gen_backup(self.user1, self.assignment, "hello{}".format(_), _) backups.append(backup) models.db.session.add(backup) models.db.session.commit() bid = utils.encode_id(backups[0].id) self.page_load(self.get_server_url() + "/admin/course/1/{}/{}/{}?student_email={}".format( self.user1.email, self.assignment.id, bid, self.user1.email)) self.assertIn('Diff Overview', self.driver.title)
def test_get_backup(self): self._test_backup(False) backup = Backup.query.first() submission_time = (self.assignment.due_date - dt.timedelta(days=random.randrange(0, 10))) backup.custom_submission_time = submission_time response = self.client.get('/api/v3/backups/{}/'.format(backup.hashid)) self.assert_200(response) course = backup.assignment.course user_json = { "email": backup.submitter.email, "id": encode_id(backup.submitter_id), } assert response.json['data'] == { "submitter": user_json, "submit": backup.submit, "created": backup.created.isoformat(), "submission_time": submission_time.isoformat(), "group": [user_json], "is_late": backup.is_late, "external_files": [], "assignment": { "name": backup.assignment.name, "course": { "id": course.id, "active": course.active, "display_name": course.display_name, "offering": course.offering, "timezone": course.timezone.zone, }, }, "id": backup.hashid, "messages": [ { "kind": "file_contents", "contents": backup.files(), "created": backup.created.isoformat(), }, ], }
def export_grades(): logger = jobs.get_job_logger() current_user = jobs.get_current_job().user course = Course.query.get(jobs.get_current_job().course_id) assignments = course.assignments students = (Enrollment.query.options(db.joinedload('user')).filter( Enrollment.role == STUDENT_ROLE, Enrollment.course == course).all()) headers, assignments = get_headers(assignments) logger.info("Using these headers:") for header in headers: logger.info('\t' + header) logger.info('') total_students = len(students) users = [student.user for student in students] user_ids = [user.id for user in users] all_scores = collect_all_scores(assignments, user_ids) with io.StringIO() as f: writer = csv.writer(f) writer.writerow(headers) # write headers for i, student in enumerate(students, start=1): row = export_student_grades(student, assignments, all_scores) writer.writerow(row) if i % 50 == 0: logger.info('Exported {}/{}'.format(i, total_students)) f.seek(0) created_time = local_time(dt.datetime.now(), course, fmt='%b-%-d %Y at %I-%M%p') csv_filename = '{course_name} Grades ({date}).csv'.format( course_name=course.display_name, date=created_time) # convert to bytes for csv upload csv_bytes = io.BytesIO(bytearray(f.read(), 'utf-8')) upload = ExternalFile.upload(csv_bytes, user_id=current_user.id, name=csv_filename, course_id=course.id, prefix='jobs/exports/{}/'.format( course.offering)) logger.info('\nDone!\n') logger.info("Saved as: {0}".format(upload.object_name)) return "/files/{0}".format(encode_id(upload.id))
def test_raw(self): filename = "test.py" contents = "x = 4" self._add_file(filename, contents) encoded_id = utils.encode_id(self.backup.id) submit_str = "submissions" if self.backup.submit else "backups" url = "/{0}/{1}/{2}/download/{3}?raw=1".format(self.assignment.name, submit_str, encoded_id, filename) response = self.client.get(url) self.assert_200(response) self.assertTrue('inline' in response.headers['Content-Disposition']) self.assertEqual(response.headers['Content-Type'], 'text/plain; charset=UTF-8') self.assertEqual(response.headers['X-Content-Type-Options'], 'nosniff') self.assertEqual(contents, response.data.decode('UTF-8'))
def assign_grading(cid, aid): courses, current_course = get_courses(cid) assign = Assignment.query.filter_by(id=aid, course_id=cid).one_or_none() if not assign or not Assignment.can(assign, current_user, 'grade'): flash('Cannot access assignment', 'error') return abort(404) form = forms.CreateTaskForm() course_staff = sorted(current_course.get_staff(), key=lambda x: x.role) details = lambda e: "{0} - ({1})".format(e.user.email, e.role) form.staff.choices = [(utils.encode_id(e.user_id), details(e)) for e in course_staff] if not form.staff.data: # Select all by default form.staff.default = [u[0] for u in form.staff.choices] form.process() if form.validate_on_submit(): # TODO: Use worker job for this (this is query intensive) selected_users = [] for hash_id in form.staff.data: user = User.get_by_id(utils.decode_id(hash_id)) if user and user.is_enrolled(cid, roles=STAFF_ROLES): selected_users.append(user) # Available backups data = assign.course_submissions() backups = set(b['backup']['id'] for b in data if b['backup']) students = set(b['user']['id'] for b in data if b['backup']) no_submissions = set(b['user']['id'] for b in data if not b['backup']) tasks = GradingTask.create_staff_tasks(backups, selected_users, aid, cid, form.kind.data, form.only_unassigned.data) num_with_submissions = len(students) - len(no_submissions) flash(("Created {0} tasks ({1} students) for {2} staff.".format( len(tasks), num_with_submissions, len(selected_users))), "success") return redirect(url_for('.assignment', cid=cid, aid=aid)) # Return template with options for who has to grade. return render_template('staff/grading/assign_tasks.html', current_course=current_course, assignment=assign, form=form)
def test_assignment_send_backup_to_ag(self): self._login(role="admin") self.assignment.autograding_key = "test" # Autograder will respond with 200 models.db.session.commit() # find a backup backup = models.Backup( submitter_id=self.user1.id, assignment=self.assignment, ) models.db.session.add(backup) models.db.session.commit() bid = utils.encode_id(backup.id) self.page_load(self.get_server_url() + "/admin/grading/" + bid) self.driver.find_element_by_id('autograde-button').click() self.assertIn("Submitted to the autograder", self.driver.page_source)
def send_batch(token, assignment, backup_ids, priority='default'): """Send a batch of backups to the autograder, returning a dict mapping backup ID -> autograder job ID. """ if not assignment.autograding_key: raise ValueError('Assignment has no autograder key') response_json = send_autograder('/api/ok/v3/grade/batch', { 'subm_ids': [utils.encode_id(bid) for bid in backup_ids], 'assignment': assignment.autograding_key, 'access_token': token.access_token, 'priority': priority, 'ok-server-version': 'v3', }, autograder_url=assignment.course.autograder_url) if response_json: return dict(zip(backup_ids, response_json['jobs'])) else: return {}
def send_batch(token, assignment, backup_ids, priority='default'): """Send a batch of backups to the autograder, returning a dict mapping backup ID -> autograder job ID. """ if not assignment.autograding_key: raise ValueError('Assignment has no autograder key') response_json = send_autograder( '/api/ok/v3/grade/batch', { 'subm_ids': [utils.encode_id(bid) for bid in backup_ids], 'assignment': assignment.autograding_key, 'access_token': token.access_token, 'priority': priority, 'ok-server-version': 'v3', }) if response_json: return dict(zip(backup_ids, response_json['jobs'])) else: return {}
def export_assignment(assignment_id, anonymized): """ Generate a zip file of submissions from enrolled students. Final Submission: One submission per student/group Zip Strucutre: cal-cs61a../[email protected]@b.com/abc12d/hog.py Anonymized: Submission without identifying info Zip Strucutre: cal-cs61a../{hash}/hog.py """ logger = jobs.get_job_logger() assignment = Assignment.query.get(assignment_id) requesting_user = jobs.get_current_job().user if not assignment: logger.warning("No assignment found") raise Exception("No Assignment") if not Assignment.can(assignment, requesting_user, "download"): raise Exception("{} does not have enough permission" .format(requesting_user.email)) if anonymized: logger.info("Starting anonymized submission export") else: logger.info("Starting final submission export") course = assignment.course with io.BytesIO() as bio: # Get a handle to the in-memory zip in append mode with zipfile.ZipFile(bio, "w", zipfile.ZIP_DEFLATED, False) as zf: zf.external_attr = 0o655 << 16 export_loop(bio, zf, logger, assignment, anonymized) created_time = local_time(dt.datetime.now(), course, fmt='%m-%d-%I-%M-%p') zip_name = '{}_{}.zip'.format(assignment.name.replace('/', '-'), created_time) bio.seek(0) # Close zf handle to finish writing zipfile logger.info("Uploading...") upload = ExternalFile.upload(bio, user_id=requesting_user.id, name=zip_name, course_id=course.id, prefix='jobs/exports/{}/'.format(course.offering)) logger.info("Saved as: {0}".format(upload.object_name)) msg = "/files/{0}".format(encode_id(upload.id)) return msg
def test_get_comments(self): self._test_backup(True) user = User.lookup(self.user1.email) staff = User.lookup(self.staff1.email) backup = Backup.query.filter(Backup.submitter_id == user.id).first() comment_url = "/api/v3/backups/{}/comment/".format(encode_id(backup.id)) comment1 = Comment( backupid = backup, author_id = staff.id, filename = 'fizzbuzz.py', line = 2, message = 'hello world' ) comment2 = Comment( backupid = backup, author_id = staff.id, filename = 'fizzbuzz.py', line = 5, message = 'wow' ) db.session.add(comment1) db.session.add(comment2) #check to see if student can view comments on own backup's comments self.login(self.user1.email) response = self.client.get(comment_url) self.assert_200(response) self.assertEqual(len(response['data']['comments']), 2) self.assertEqual(response['data']['comments'][0].message, 'hello world') self.assertEqual(response['data']['comments'][1].message, 'wow') self.logout() #check to see if staff can access comments self.login(self.staff1.email) response = self.client.get(comment_url) self.assert_200(response) self.logout() #check to see another student can't see others' backup's comments self.login(self.user2.email) response = self.client.get(comment_url) self.assert_403(response) self.logout()
def assign_grading(cid, aid): courses, current_course = get_courses(cid) assign = Assignment.query.filter_by(id=aid, course_id=cid).one_or_none() if not assign or not Assignment.can(assign, current_user, 'grade'): flash('Cannot access assignment', 'error') return abort(404) form = forms.CreateTaskForm() course_staff = sorted(current_course.get_staff(), key=lambda x: x.role) details = lambda e: "{0} - ({1})".format(e.user.email, e.role) form.staff.choices = [(utils.encode_id(e.user_id), details(e)) for e in course_staff] if not form.staff.data: # Select all by default form.staff.default = [u[0] for u in form.staff.choices] form.process() if form.validate_on_submit(): # TODO: Use worker job for this (this is query intensive) selected_users = [] for hash_id in form.staff.data: user = User.get_by_id(utils.decode_id(hash_id)) if user and user.is_enrolled(cid, roles=STAFF_ROLES): selected_users.append(user) # Available backups: students, backups, no_submissions = assign.course_submissions() tasks = GradingTask.create_staff_tasks(backups, selected_users, aid, cid, form.kind.data, form.only_unassigned.data) num_with_submissions = len(students) - len(no_submissions) flash(("Created {0} tasks ({1} students) for {2} staff." .format(len(tasks), num_with_submissions, len(selected_users))), "success") return redirect(url_for('.assignment', cid=cid, aid=aid)) # Return template with options for who has to grade. return render_template('staff/grading/assign_tasks.html', current_course=current_course, assignment=assign, form=form)
def test_get_comments(self): self._test_backup(True) user = User.lookup(self.user1.email) staff = User.lookup(self.staff1.email) backup = Backup.query.filter( Backup.submitter_id == user.id).first() comment_url = "/api/v3/backups/{}/comment/".format( encode_id(backup.id)) comment1 = Comment(backupid=backup, author_id=staff.id, filename='fizzbuzz.py', line=2, message='hello world') comment2 = Comment(backupid=backup, author_id=staff.id, filename='fizzbuzz.py', line=5, message='wow') db.session.add(comment1) db.session.add(comment2) #check to see if student can view comments on own backup's comments self.login(self.user1.email) response = self.client.get(comment_url) self.assert_200(response) self.assertEqual(len(response['data']['comments']), 2) self.assertEqual(response['data']['comments'][0].message, 'hello world') self.assertEqual(response['data']['comments'][1].message, 'wow') self.logout() #check to see if staff can access comments self.login(self.staff1.email) response = self.client.get(comment_url) self.assert_200(response) self.logout() #check to see another student can't see others' backup's comments self.login(self.user2.email) response = self.client.get(comment_url) self.assert_403(response) self.logout()
def export_grades(): logger = jobs.get_job_logger() current_user = jobs.get_current_job().user course = Course.query.get(jobs.get_current_job().course_id) assignments = course.assignments students = (Enrollment.query .options(db.joinedload('user')) .filter(Enrollment.role == STUDENT_ROLE, Enrollment.course == course) .all()) headers, assignments = get_headers(assignments) logger.info("Using these headers:") for header in headers: logger.info('\t' + header) logger.info('') total_students = len(students) with io.StringIO() as f: writer = csv.writer(f) writer.writerow(headers) # write headers for i, student in enumerate(students, start=1): row = export_student_grades(student, assignments) writer.writerow(row) if i % 50 == 0: logger.info('Exported {}/{}'.format(i, total_students)) f.seek(0) created_time = local_time(dt.datetime.now(), course, fmt='%b-%-d %Y at %I-%M%p') csv_filename = '{course_name} Grades ({date}).csv'.format( course_name=course.display_name, date=created_time) # convert to bytes for csv upload csv_bytes = io.BytesIO(bytearray(f.read(), 'utf-8')) upload = ExternalFile.upload(csv_bytes, user_id=current_user.id, name=csv_filename, course_id=course.id, prefix='jobs/exports/{}/'.format(course.offering)) logger.info('\nDone!\n') logger.info("Saved as: {0}".format(upload.object_name)) return "/files/{0}".format(encode_id(upload.id))
def send_batch(assignment, backup_ids): if not assignment.autograding_key: raise ValueError('Assignment has no autograder key') # Create an access token for this run autograder_client = Client.query.get('autograder') if not autograder_client: autograder_client = Client( name='Autograder', client_id='autograder', client_secret='autograder', redirect_uris=[], is_confidential=False, description='The Autopy autograder system', default_scopes=['all'], ) db.session.add(autograder_client) db.session.commit() token = Token( client=autograder_client, user=current_user, token_type='bearer', access_token=oauthlib.common.generate_token(), expires=datetime.datetime.utcnow() + datetime.timedelta(hours=2), scopes=['all'], ) db.session.add(token) db.session.commit() return send_autograder('/api/ok/v3/grade/batch', { 'subm_ids': [utils.encode_id(bid) for bid in backup_ids], 'assignment': assignment.autograding_key, 'access_token': token.access_token, 'priority': 'default', 'backup_url': url_for('api.backup', _external=True), 'ok-server-version': 'v3', })
def test_edit_extension(self): ext = self._make_ext(self.assignment, self.user1) self.login(self.staff1.email) expires = (dt.datetime.utcnow() + dt.timedelta(days=3)).strftime(constants.ISO_DATETIME_FMT) custom_submission_time = dt.datetime.utcnow().strftime(constants.ISO_DATETIME_FMT) message = 'Sickness' data = { 'assignment_id': self.assignment.id, 'email': self.user1.email, 'expires': expires, 'reason': message, 'submission_time': 'other', 'custom_submission_time': custom_submission_time } self.assert200(self.client.post('/admin/course/{}/extensions/{}'.format(self.course.id, utils.encode_id(ext.id)), data=data, follow_redirects=True)) extension = Extension.get_extension(self.user1, self.assignment, time=dt.datetime.utcnow()) self.assertEqual(extension.staff_id, self.staff1.id) self.assertEqual(extension.assignment_id, self.assignment.id) self.assertEqual(extension.message, message) self.assertEqual(utils.local_time_obj(extension.expires, self.course).replace(tzinfo=None), dt.datetime.strptime(expires, '%Y-%m-%d %H:%M:%S')) self.assertEqual(utils.local_time_obj(extension.custom_submission_time, self.course).replace(tzinfo=None), dt.datetime.strptime(custom_submission_time, '%Y-%m-%d %H:%M:%S'))
def to_url(self, value): return utils.encode_id(value)
def moss_submit(moss_id, submissions, ref_submissions, language, template, review_threshold=101, max_matches=MAX_MATCHES, file_regex='.*', num_results=NUM_RESULTS): """ Sends SUBMISSIONS and REF_SUBMISSIONS to Moss using MOSS_ID, LANGUAGE, and MAX_MATCHES. Stores results involving SUBMISSIONS in database. """ # ISSUE: Does not work for .ipynb files well (maybe just use sources?) logger = jobs.get_job_logger() logger.info('Connecting to Moss...') moss = socket.socket() moss.connect(('moss.stanford.edu', 7690)) moss.send('moss {}\n'.format(moss_id).encode()) moss.send('directory 1\n'.encode()) moss.send('X 0\n'.encode()) moss.send('maxmatches {}\n'.format(max_matches).encode()) moss.send('show {}\n'.format(num_results).encode()) print(num_results) moss.send('language {}\n'.format(language).encode()) moss_success = moss.recv(1024).decode().strip() print(moss_success) moss_success = moss_success == 'yes' if not moss_success: moss.close() logger.info('FAILED to connect to Moss. Common issues:') logger.info('- Make sure your Moss ID is a number, and not your email address.') logger.info('- Check you typed your Moss ID correctly.') return subm_keys = set() hashed_subm_keys = set() for subm in submissions: subm_keys.add(subm['backup']['id']) hashed_subm_keys.add(encode_id(subm['backup']['id'])) for subm in ref_submissions: subm_keys.add(subm['backup']['id']) backup_query = (Backup.query.options(db.joinedload('messages')) .filter(Backup.id.in_(subm_keys)) .order_by(Backup.created.desc()) .all()) match_pattern = re.compile(file_regex) if template: logger.info('Uploading template...') merged_contents = "" for filename in template: if filename == 'submit' or not match_pattern.match(filename): continue merged_contents += template[filename] + '\n' send_file(moss, 'allcode', merged_contents, 0, language) fid = 0 logger.info('Uploading submissions...') for backup in backup_query: file_contents = [m for m in backup.messages if m.kind == 'file_contents'] if not file_contents: logger.info("{} didn't have any file contents".format(backup.hashid)) continue contents = file_contents[0].contents merged_contents = "" for filename in sorted(contents.keys()): if filename == 'submit' or not match_pattern.match(filename): continue merged_contents += contents[filename] + '\n' fid += 1 path = os.path.join(backup.hashid, 'allcode') send_file(moss, path, merged_contents, fid, language) moss.send("query 0 Submitted via okpy.org\n".encode()) logger.info('Awaiting response...') url = moss.recv(1024).decode().strip() moss.send("end\n".encode()) moss.close() logger.info('Moss results at: {}'.format(url)) parse_moss_results(url, hashed_subm_keys, logger, match_pattern, template, review_threshold)
def format(self, value): if type(value) == int: return encode_id(value) else: return decode_id(value)