def setUp(self): super(TestGrading, self).setUp() self.setup_course() message_dict = {'file_contents': {'backup.py': '1'}, 'analytics': {}} self.active_user_ids = [self.user1.id, self.user2.id, self.user3.id] self.active_staff = [self.staff1, self.staff2] self.active_assignments = [self.assignment, self.assignment2] Group.invite(self.user1, self.user2, self.assignment) group = Group.lookup(self.user1, self.assignment) group.accept(self.user2) # Creates 5 submissions for each assignment per user, each spaced two minutes apart for assign in self.active_assignments: time = assign.due_date - datetime.timedelta(minutes=30) for num in range(5): for user_id in self.active_user_ids: num += 1 time += datetime.timedelta(minutes=2) backup = Backup(submitter_id=user_id, assignment=assign, submit=True) messages = [Message(kind=k, backup=backup, contents=m) for k, m in message_dict.items()] backup.created = time db.session.add_all(messages) db.session.add(backup) # Debugging print if tests fails # print("User {} | Assignment {} | Submission {} | Time {}".format( # user_id, assign.id, num, time)) db.session.commit()
def setUp(self): """ Add submissions for 3 users. """ super(TestRevision, self).setUp() self.setup_course() message_dict = {'file_contents': {'backup.py': '1'}, 'analytics': {}} self.active_user_ids = [self.user1.id, self.user2.id, self.user3.id] self.assignment.revisions_allowed = True time = self.assignment.due_date # Set to dt.now(), so future subms are late for user_id in self.active_user_ids: time -= datetime.timedelta(minutes=15) backup = Backup(submitter_id=user_id, assignment=self.assignment, submit=True) # Revisions are submitted on time. backup.created = time messages = [Message(kind=k, backup=backup, contents=m) for k, m in message_dict.items()] db.session.add_all(messages) db.session.add(backup) # Put user 3 in a group with user 4 Group.invite(self.user3, self.user4, self.assignment) group = Group.lookup(self.user3, self.assignment) group.accept(self.user4) okversion = Version(name="ok-client", current_version="v1.5.0", download_link="http://localhost/ok") db.session.add(okversion) db.session.commit()
def gen_backup(user, assignment): seconds_offset = random.randrange(-100000, 100) messages = gen_messages(assignment, seconds_offset) submit = gen_bool(0.3) if submit: messages['file_contents']['submit'] = '' backup = Backup( created=assignment.due_date - datetime.timedelta(seconds=seconds_offset), submitter_id=user.id, assignment_id=assignment.id, submit=submit) backup.messages = [Message(kind=k, contents=m) for k, m in messages.items()] return backup
def gen_backup(user, assignment): seconds_offset = random.randrange(-100000, 100) messages = gen_messages(assignment, seconds_offset) submit = gen_bool(0.3) if submit: messages['file_contents']['submit'] = '' backup = Backup(created=assignment.due_date - datetime.timedelta(seconds=seconds_offset), submitter_id=user.id, assignment_id=assignment.id, submit=submit) backup.messages = [ Message(kind=k, contents=m) for k, m in messages.items() ] return backup
def test_files(self): backup = Backup(submitter_id=self.user1.id, assignment=self.assignment, submit=True) message = Message(kind='file_contents', backup=backup, contents={ 'hog.py': 'def foo():\n return', 'submit': True }) db.session.add(message) db.session.add(backup) db.session.commit() # submit should not show up assert backup.files() == {'hog.py': 'def foo():\n return'}
def download(name, submit, bid, file): backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "view")): abort(404) if backup.submit != submit: return redirect(url_for('.download', name=name, submit=backup.submit, bid=bid, file=file)) try: contents = backup.files()[file] except KeyError: abort(404) response = make_response(contents) inline = 'raw' in request.args content_disposition = "inline" if inline else "attachment" response.headers["Content-Disposition"] = ("{0}; filename={1!s}" .format(content_disposition, file)) response.headers["Content-Security-Policy"] = "default-src 'none';" response.headers["X-Content-Type-Options"] = "nosniff" if file.endswith('.ipynb') and not inline: # Prevent safari from adding a .txt extension to files response.headers["Content-Type"] = "application/octet-stream; charset=UTF-8" else: response.headers["Content-Type"] = "text/plain; charset=UTF-8" return response
def code(name, submit, bid): assign = get_assignment(name) backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "view")): abort(404) if backup.submit != submit: return redirect(url_for('.code', name=name, submit=backup.submit, bid=bid)) diff_type = request.args.get('diff') if diff_type not in (None, 'short', 'full'): return redirect(url_for('.code', name=name, submit=submit, bid=bid)) if not assign.files and diff_type: return abort(404) # sort comments by (filename, line) comments = collections.defaultdict(list) for comment in backup.comments: comments[(comment.filename, comment.line)].append(comment) # highlight files and add comments files = highlight.diff_files(assign.files, backup.files(), diff_type) for filename, source_file in files.items(): for line in source_file.lines: line.comments = comments[(filename, line.line_after)] return render_template('student/assignment/code.html', course=assign.course, assignment=assign, backup=backup, files=files, diff_type=diff_type)
def flag(name, bid): assign = get_assignment(name) user_ids = assign.active_user_ids(current_user.id) flag = 'flag' in request.form next_url = request.form['next'] backup = models.Backup.query.get(bid) if not Backup.can(backup, current_user, "view"): abort(404) if not assign.active: flash('It is too late to change what submission is graded.', 'warning') elif flag: result = assign.flag(bid, user_ids) flash('Flagged submission {}. '.format(result.hashid) + 'This submission will be used for grading', 'success') else: result = assign.unflag(bid, user_ids) flash('Removed flag from {}. '.format(result.hashid) + 'The most recent submission will be used for grading.', 'success') if is_safe_redirect_url(request, next_url): return redirect(next_url) else: flash("Not a valid redirect", "danger") abort(400)
def edit_backup(bid): courses, current_course = get_courses() backup = Backup.query.options(db.joinedload('assignment')).get(bid) if not backup: abort(404) if not Backup.can(backup, current_user, 'grade'): flash("You do not have permission to score this assignment.", "warning") abort(401) form = forms.SubmissionTimeForm() if form.validate_on_submit(): backup.custom_submission_time = form.get_submission_time( backup.assignment) db.session.commit() flash('Submission time saved', 'success') return redirect(url_for('.edit_backup', bid=bid)) else: form.set_submission_time(backup) return render_template( 'staff/grading/edit.html', courses=courses, current_course=current_course, backup=backup, student=backup.submitter, form=form, )
def code(name, submit, bid): assign = get_assignment(name) backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "view")): abort(404) if backup.submit != submit: return redirect( url_for('.code', name=name, submit=backup.submit, bid=bid)) diff_type = request.args.get('diff') if diff_type not in (None, 'short', 'full'): return redirect(url_for('.code', name=name, submit=submit, bid=bid)) if not assign.files and diff_type: return abort(404) # sort comments by (filename, line) comments = collections.defaultdict(list) for comment in backup.comments: comments[(comment.filename, comment.line)].append(comment) # highlight files and add comments files = highlight.diff_files(assign.files, backup.files(), diff_type) for filename, lines in files.items(): for line in lines: line.comments = comments[(filename, line.line_after)] return render_template('student/assignment/code.html', course=assign.course, assignment=assign, backup=backup, files=files, diff_type=diff_type)
def flag(name, bid): assign = get_assignment(name) user_ids = assign.active_user_ids(current_user.id) flag = 'flag' in request.form next_url = request.form['next'] backup = models.Backup.query.get(bid) if not Backup.can(backup, current_user, "view"): abort(404) if not assign.active: flash('It is too late to change what submission is graded.', 'warning') elif flag: result = assign.flag(bid, user_ids) flash( 'Flagged submission {}. '.format(result.hashid) + 'This submission will be used for grading', 'success') else: result = assign.unflag(bid, user_ids) flash( 'Removed flag from {}. '.format(result.hashid) + 'The most recent submission will be used for grading.', 'success') if is_safe_redirect_url(request, next_url): return redirect(next_url) else: flash("Not a valid redirect", "danger") abort(400)
def download(name, submit, bid, file): backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "view")): abort(404) if backup.submit != submit: return redirect( url_for('.download', name=name, submit=backup.submit, bid=bid, file=file)) try: contents = backup.files()[file] except KeyError: abort(404) response = make_response(contents) content_disposition = "inline" if 'raw' in request.args else "attachment" response.headers["Content-Disposition"] = ("{0}; filename={1!s}".format( content_disposition, file)) response.headers["Content-Security-Policy"] = "default-src 'none';" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Content-Type"] = "text/plain; charset=UTF-8" return response
def gen_backup(user, assignment): messages = { 'file_contents': { 'fizzbuzz.py': modified_file, 'moby_dick': 'Call me Ishmael.' }, 'analytics': {} } submit = gen_bool(0.1) if submit: messages['file_contents']['submit'] = '' backup = Backup( created=assignment.due_date - datetime.timedelta(seconds=random.randrange(-100000, 100)), submitter_id=user.id, assignment_id=assignment.id, submit=submit) backup.messages = [Message(kind=k, contents=m) for k, m in messages.items()] return backup
def test_files(self): backup = Backup( submitter_id=self.user1.id, assignment=self.assignment, submit=True) message = Message( kind='file_contents', backup=backup, contents={ 'hog.py': 'def foo():\n return', 'submit': True }) db.session.add(message) db.session.add(backup) db.session.commit() # submit should not show up assert backup.files() == { 'hog.py': 'def foo():\n return' }
def composition(bid): backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "grade")): abort(404) form = forms.CompositionScoreForm() existing = Score.query.filter_by(backup=backup, kind="composition").first() if existing: form.kind.data = "composition" form.message.data = existing.message form.score.data = existing.score return grading_view(backup, form=form)
def gen_backup(user, assignment): messages = { 'file_contents': { 'fizzbuzz.py': modified_file, 'moby_dick': 'Call me Ishmael.' }, 'analytics': {} } submit = gen_bool(0.1) if submit: messages['file_contents']['submit'] = '' backup = Backup(created=assignment.due_date - datetime.timedelta(seconds=random.randrange(-100000, 100)), submitter_id=user.id, assignment_id=assignment.id, submit=submit) backup.messages = [ Message(kind=k, contents=m) for k, m in messages.items() ] return backup
def score_from_csv(assign_id, rows, kind='total', invalid=None, message=None): """ Job for uploading Scores. @param ``rows`` should be a list of records (mappings), with labels `email` and `score` """ log = jobs.get_job_logger() current_user = jobs.get_current_job().user assign = Assignment.query.get(assign_id) message = message or '{} score for {}'.format(kind.title(), assign.display_name) def log_err(msg): log.info('\t! {}'.format(msg)) log.info("Uploading scores for {}:\n".format(assign.display_name)) if invalid: log_err('skipping {} invalid entries on lines:'.format(len(invalid))) for line in invalid: log_err('\t{}'.format(line)) log.info('') success, total = 0, len(rows) for i, row in enumerate(rows, start=1): try: email, score = row['email'], row['score'] user = User.query.filter_by(email=email).one() backup = Backup.query.filter_by(assignment=assign, submitter=user, submit=True).first() if not backup: backup = Backup.create(submitter=user, assignment=assign, submit=True) uploaded_score = Score(grader=current_user, assignment=assign, backup=backup, user=user, score=score, kind=kind, message=message) db.session.add(uploaded_score) uploaded_score.archive_duplicates() except SQLAlchemyError: print_exc() log_err('error: user with email `{}` does not exist'.format(email)) else: success += 1 if i % 100 == 0: log.info('\nUploaded {}/{} Scores\n'.format(i, total)) db.session.commit() log.info('\nSuccessfully uploaded {} "{}" scores (with {} errors)'.format(success, kind, total - success)) return '/admin/course/{cid}/assignments/{aid}/scores'.format( cid=jobs.get_current_job().course_id, aid=assign_id)
def test_backup_owners(self): backup = Backup( submitter_id=self.user1.id, assignment=self.assignment, submit=True) backup2 = Backup( submitter_id=self.user2.id, assignment=self.assignment, submit=True) db.session.add(backup) db.session.add(backup2) db.session.commit() assert backup2.owners() == {self.user2.id} Group.invite(self.user1, self.user2, self.assignment) Group.invite(self.user1, self.user3, self.assignment) group = Group.lookup(self.user1, self.assignment) group.accept(self.user2) assert backup.owners() == {self.user1.id, self.user2.id} assert backup2.owners() == {self.user1.id, self.user2.id}
def download(name, submit, bid, file): backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "view")): abort(404) if backup.submit != submit: return redirect(url_for('.download', name=name, submit=backup.submit, bid=bid, file=file)) try: contents = backup.files()[file] except KeyError: abort(404) response = make_response(contents) response.headers["Content-Disposition"] = "attachment; filename={0!s}".format(file) return response
def setUp(self): """ Add submissions for 3 users. """ super(TestRevision, self).setUp() self.setup_course() message_dict = {'file_contents': {'backup.py': '1'}, 'analytics': {}} self.active_user_ids = [self.user1.id, self.user2.id, self.user3.id] self.assignment.revisions_allowed = True time = self.assignment.due_date # Set to dt.now(), so future subms are late for user_id in self.active_user_ids: time -= datetime.timedelta(minutes=15) backup = Backup(submitter_id=user_id, assignment=self.assignment, submit=True) # Revisions are submitted on time. backup.created = time messages = [ Message(kind=k, backup=backup, contents=m) for k, m in message_dict.items() ] db.session.add_all(messages) db.session.add(backup) # Put user 3 in a group with user 4 Group.invite(self.user3, self.user4, self.assignment) group = Group.lookup(self.user3, self.assignment) group.accept(self.user4) okversion = Version(name="ok-client", current_version="v1.5.0", download_link="http://localhost/ok") db.session.add(okversion) db.session.commit()
def grading(bid): backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "grade")): abort(404) form = forms.GradeForm() existing = Score.query.filter_by(backup=backup).first() if existing and existing.kind in GRADE_TAGS: form = forms.GradeForm(kind=existing.kind) form.kind.data = existing.kind form.message.data = existing.message form.score.data = existing.score return grading_view(backup, form=form)
def autograde_backup(bid): backup = Backup.query.options(db.joinedload('assignment')).get(bid) if not backup: abort(404) if not Backup.can(backup, current_user, 'grade'): flash("You do not have permission to score this assignment.", "warning") abort(401) form = forms.CSRFForm() if form.validate_on_submit(): try: autograder.autograde_backup(backup) flash('Submitted to the autograder', 'success') except ValueError as e: flash(str(e), 'error') return redirect(url_for('.grading', bid=bid))
def grading(bid): backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "grade")): abort(404) form = forms.GradeForm() existing = [s for s in backup.scores if not s.archived] first_score = existing[0] if existing else None if first_score and first_score.kind in GRADE_TAGS: form = forms.GradeForm(kind=first_score.kind) form.kind.data = first_score.kind form.message.data = first_score.message form.score.data = first_score.score return grading_view(backup, form=form)
def _make_assignment(self, uids, assignment): # create a submission every 15 minutes message_dict = {'file_contents': {'backup.py': '1'}, 'analytics': {}} time = assignment.due_date for _ in range(20): for user_id in uids: time -= datetime.timedelta(minutes=15) backup = Backup(submitter_id=user_id, assignment=assignment, submit=True) messages = [ Message(kind=k, backup=backup, contents=m) for k, m in message_dict.items() ] db.session.add_all(messages) db.session.add(backup) db.session.commit()
def submit_assignment(name): assign = get_assignment(name) group = Group.lookup(current_user, assign) user_ids = assign.active_user_ids(current_user.id) fs = assign.final_submission(user_ids) if not assign.uploads_enabled: flash("This assignment cannot be submitted online", 'warning') return redirect(url_for('.assignment', name=assign.name)) if not assign.active: flash("It's too late to submit this assignment", 'warning') return redirect(url_for('.assignment', name=assign.name)) form = UploadSubmissionForm() if form.validate_on_submit(): backup = Backup( submitter=current_user, assignment=assign, submit=True, ) if form.upload_files.upload_backup_files(backup): db.session.add(backup) db.session.commit() if assign.autograding_key: try: submit_continous(backup) except ValueError as e: logger.warning('Web submission did not autograde', exc_info=True) flash('Did not send to autograder: {}'.format(e), 'warning') flash('Uploaded submission', 'success') return redirect( url_for( '.code', name=assign.name, submit=backup.submit, bid=backup.id, )) return render_template('student/assignment/submit.html', assignment=assign, group=group, course=assign.course, form=form)
def download(name, submit, bid, file): backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "view")): abort(404) if backup.submit != submit: return redirect( url_for('.download', name=name, submit=backup.submit, bid=bid, file=file)) try: contents = backup.files()[file] except KeyError: abort(404) response = make_response(contents) response.headers[ "Content-Disposition"] = "attachment; filename={0!s}".format(file) return response
def _add_file(self, filename, contents): self.setup_course() email = '*****@*****.**' self.login(email) self.user = User.lookup(email) self.backup = Backup(submitter=self.user, assignment=self.assignment, submit=True) self.message = Message(backup=self.backup, contents={ filename: contents, 'submit': True }, kind='file_contents') db.session.add(self.backup) db.session.add(self.message) db.session.commit()
def download(name, submit, bid, file): backup = Backup.query.get(bid) if not (backup and Backup.can(backup, current_user, "view")): abort(404) if backup.submit != submit: return redirect(url_for('.download', name=name, submit=backup.submit, bid=bid, file=file)) try: contents = backup.files()[file] except KeyError: abort(404) response = make_response(contents) content_disposition = "inline" if 'raw' in request.args else "attachment" response.headers["Content-Disposition"] = ("{0}; filename={1!s}" .format(content_disposition, file)) response.headers["Content-Security-Policy"] = "default-src 'none';" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Content-Type"] = "text/plain; charset=UTF-8" return response
def test_backup_owners(self): backup = Backup(submitter_id=self.user1.id, assignment=self.assignment, submit=True) backup2 = Backup(submitter_id=self.user2.id, assignment=self.assignment, submit=True) db.session.add(backup) db.session.add(backup2) db.session.commit() assert backup2.owners() == {self.user2.id} Group.invite(self.user1, self.user2, self.assignment) Group.invite(self.user1, self.user3, self.assignment) group = Group.lookup(self.user1, self.assignment) group.accept(self.user2) assert backup.owners() == {self.user1.id, self.user2.id} assert backup2.owners() == {self.user1.id, self.user2.id}
def staff_submit_backup(cid, email, 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'): return abort(404) student = User.lookup(email) if not student: abort(404) user_ids = assign.active_user_ids(student.id) # TODO: DRY - Unify with student upload code - should just be a function form = forms.StaffUploadSubmissionForm() if form.validate_on_submit(): backup = Backup( submitter=student, creator=current_user, assignment=assign, submit=True, custom_submission_time=form.get_submission_time(assign), ) if form.upload_files.upload_backup_files(backup): db.session.add(backup) db.session.commit() if assign.autograding_key: try: autograder.submit_continous(backup) except ValueError as e: flash('Did not send to autograder: {}'.format(e), 'warning') flash('Uploaded submission'.format(backup.hashid), 'success') return redirect(url_for('.grading', bid=backup.id)) return render_template( 'staff/student/submit.html', current_course=current_course, courses=courses, student=student, assignment=assign, upload_form=form, )
def grade(bid): """ Used as a form submission endpoint. """ backup = Backup.query.options(db.joinedload('assignment')).get(bid) if not backup: abort(404) if not Backup.can(backup, current_user, 'grade'): flash("You do not have permission to score this assignment.", "warning") abort(401) form = forms.GradeForm() score_kind = form.kind.data.strip().lower() is_composition = (score_kind == "composition") # TODO: Form should include redirect url instead of guessing based off tag if is_composition: form = forms.CompositionScoreForm() if not form.validate_on_submit(): return grading_view(backup, form=form) score = Score(backup=backup, grader=current_user, assignment_id=backup.assignment_id) form.populate_obj(score) db.session.add(score) db.session.commit() # Archive old scores of the same kind score.archive_duplicates() next_page = None flash_msg = "Added a {0} {1} score.".format(score.score, score_kind) # Find GradingTasks applicable to this score tasks = backup.grading_tasks for task in tasks: task.score = score cache.delete_memoized(User.num_grading_tasks, task.grader) db.session.commit() if len(tasks) == 1: # Go to next task for the current task queue if possible. task = tasks[0] next_task = task.get_next_task() next_route = '.composition' if is_composition else '.grading' # Handle case when the task is on the users queue if next_task: flash_msg += (" There are {0} tasks left. Here's the next submission:" .format(task.remaining)) next_page = url_for(next_route, bid=next_task.backup_id) else: flash_msg += " All done with grading for {}".format(backup.assignment.name) next_page = url_for('.grading_tasks') else: # TODO: Send task id or redirect_url in the grading form # For now, default to grading tasks next_page = url_for('.grading_tasks') flash(flash_msg, 'success') if not next_page: next_page = url_for('.assignment_queues', aid=backup.assignment_id, cid=backup.assignment.course_id) return redirect(next_page)
def restore_object(self, attrs, instance=None): attrs[u'user'] = self.context.get('request').user backup = Backup(**attrs) if backup.file: backup.file.name = backup.name return backup
def submit_assignment(name): # TODO: Unify student & staff upload. assign = get_assignment(name) group = Group.lookup(current_user, assign) user_ids = assign.active_user_ids(current_user.id) fs = assign.final_submission(user_ids) if not assign.uploads_enabled: flash("This assignment cannot be submitted online", 'warning') return redirect(url_for('.assignment', name=assign.name)) extension = None # No need for an extension if not assign.active: extension = Extension.get_extension(current_user, assign) if not extension: flash("It's too late to submit this assignment", 'warning') return redirect(url_for('.assignment', name=assign.name)) if request.method == "POST": backup = Backup.create( submitter=current_user, assignment=assign, submit=True, ) assignment = backup.assignment if extension: backup.custom_submission_time = extension.custom_submission_time templates = assignment.files or [] files = {} def extract_file_index(file_ind): """ Get the index of of file objects. Used because request.files.getlist() does not handle uniquely indexed lists. >>> extract_file_index('file[12']) 12 """ brace_loc = file_ind.find('[') index_str = file_ind[brace_loc+1:-1] return int(index_str) # A list of one element lists sorted_uploads = sorted(list(request.files.items()), key=lambda x: extract_file_index(x[0])) uploads = [v[1] for v in sorted_uploads] full_path_names = list(request.form.listvalues())[0] template_files = assign.files or [] file_names = [os.path.split(f)[1] for f in full_path_names] missing = [t for t in template_files if t not in file_names] if missing: return jsonify({ 'error': ('Missing files: {}. The following files are required: {}' .format(', '.join(missing), ', '.join(template_files))) }), 400 backup_folder_postfix = time.time() for full_path, upload in zip(full_path_names, uploads): data = upload.read() if len(data) > MAX_UPLOAD_FILE_SIZE: # file is too large (over 25 MB) return jsonify({ 'error': ('{} is larger than the maximum file size of {} MB' .format(full_path, MAX_UPLOAD_FILE_SIZE/1024/1024)) }), 400 try: files[full_path] = str(data, 'utf-8') except UnicodeDecodeError: upload.stream.seek(0) # We've already read data, so reset before uploading dest_folder = "uploads/{}/{}/{}/".format(assign.name, current_user.id, backup_folder_postfix) bin_file = ExternalFile.upload(upload.stream, current_user.id, full_path, staff_file=False, prefix=dest_folder, course_id=assign.course.id, backup=backup, assignment_id=assign.id) db.session.add(bin_file) message = Message(kind='file_contents', contents=files) backup.messages.append(message) db.session.add(backup) db.session.commit() # Send to continuous autograder if assign.autograding_key and assign.continuous_autograding: try: submit_continuous(backup) except ValueError as e: flash('Did not send to autograder: {}'.format(e), 'warning') return jsonify({ 'backup': backup.hashid, 'url': url_for('.code', name=assign.name, submit=backup.submit, bid=backup.id) }) return render_template('student/assignment/submit.html', assignment=assign, group=group, course=assign.course)