def assignment(course_name, state_is_hidden, session, request, with_works): course = m.Course.query.filter_by(name=course_name).one() state = ( m.AssignmentStateEnum.hidden if state_is_hidden else m.AssignmentStateEnum.open ) assig = m.Assignment( name='TEST COURSE', state=state, course=course, deadline=DatetimeWithTimezone.utcnow() + datetime.timedelta(days=1 if request.param == 'new' else -1), is_lti=False, ) session.add(assig) session.commit() if with_works: names = ['Student1', 'Student2', 'Student3', 'Œlµo'] if with_works != 'single': names += names for uname in names: user = m.User.query.filter_by(name=uname).one() work = m.Work(assignment=assig, user=user) session.add(work) session.commit() yield assig
def test_delete_submission(assignment_real_works, session, monkeypatch, canvas_lti1p1_provider, stub_function_class, describe): assignment, submission = assignment_real_works passback = stub_function_class(lambda: True) monkeypatch.setattr(psef.lti.v1_1.LTI, '_passback_grade', passback) def do_delete(was_latest, new_latest=None): psef.signals.WORK_DELETED.send( psef.signals.WorkDeletedData( deleted_work=m.Work.query.get(helpers.get_id(submission)), was_latest=was_latest, new_latest=new_latest, )) with describe('deleting submission without lti should work'): do_delete(True) assert not passback.called canvas_lti1p1_provider._delete_submission( (helpers.get_id(submission), assignment.id)) assert not passback.called user_id = submission['user']['id'] assignment.assignment_results[user_id] = m.AssignmentResult( sourcedid='wow', user_id=user_id) m.CourseLTIProvider.create_and_add( lti_context_id=str(uuid.uuid4()), course=assignment.course, lti_provider=canvas_lti1p1_provider, deployment_id='', ) assignment.lti_grade_service_data = 'http://aaa' assignment.is_lti = True session.commit() with describe('deleting newest submission'): do_delete(was_latest=True) assert len(passback.all_args) == 1 assert passback.all_args[0]['grade'] is None passback.reset() with describe('deleting non newest should not delete grade'): sub_new = m.Work(user_id=user_id, assignment=assignment) session.add(sub_new) session.commit() do_delete(was_latest=False, new_latest=None) assert not passback.called with describe('deleting in non existing assignment'): canvas_lti1p1_provider._delete_submission( (helpers.get_id(submission), None)) assert not passback.called with describe('deleting in non existing submissions'): canvas_lti1p1_provider._delete_submission((-1, assignment.id)) assert not passback.called
def test_data(db=None): db = db or psef.models.db if not app.config['DEBUG']: print('You can not add test data in production mode', file=sys.stderr) return 1 seed() db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/courses.json', 'r' ) as c: cs = json.load(c) for c in cs: if m.Course.query.filter_by(name=c['name']).first() is None: db.session.add(m.Course(name=c['name'])) db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/assignments.json', 'r' ) as c: cs = json.load(c) for c in cs: assig = m.Assignment.query.filter_by(name=c['name']).first() if assig is None: db.session.add( m.Assignment( name=c['name'], deadline=datetime.datetime.utcnow() + datetime.timedelta(days=c['deadline']), state=c['state'], description=c['description'], course=m.Course.query.filter_by(name=c['course'] ).first() ) ) else: assig.description = c['description'] assig.state = c['state'] assig.course = m.Course.query.filter_by(name=c['course'] ).first() db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/users.json', 'r' ) as c: cs = json.load(c) for c in cs: u = m.User.query.filter_by(name=c['name']).first() courses = { m.Course.query.filter_by(name=name).first(): role for name, role in c['courses'].items() } perms = { course.id: m.CourseRole.query.filter_by(name=name, course_id=course.id).first() for course, name in courses.items() } username = c['name'].split(' ')[0].lower() if u is not None: u.name = c['name'] u.courses = perms u.email = c['name'].replace(' ', '_').lower() + '@example.com' u.password = c['name'] u.username = username u.role = m.Role.query.filter_by(name=c['role']).first() else: u = m.User( name=c['name'], courses=perms, email=c['name'].replace(' ', '_').lower() + '@example.com', password=c['name'], username=username, role=m.Role.query.filter_by(name=c['role']).first() ) db.session.add(u) for course, role in courses.items(): if role == 'Student': for assig in course.assignments: work = m.Work(assignment=assig, user=u) db.session.add( m.File( work=work, name='Top stub dir', is_directory=True ) ) db.session.add(work) db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/rubrics.json', 'r' ) as c: cs = json.load(c) for c in cs: for row in c['rows']: assignment = m.Assignment.query.filter_by( name=c['assignment'] ).first() if assignment is not None: rubric_row = m.RubricRow.query.filter_by( header=row['header'], description=row['description'], assignment_id=assignment.id ).first() if rubric_row is None: rubric_row = m.RubricRow( header=row['header'], description=row['description'], assignment=assignment ) db.session.add(rubric_row) for item in row['items']: if not db.session.query( m.RubricItem.query.filter_by( rubricrow_id=rubric_row.id, **item, ).exists() ).scalar(): rubric_item = m.RubricItem( description=item['description'] * 5, header=item['header'], points=item['points'], rubricrow=rubric_row ) db.session.add(rubric_item) db.session.commit()
def test_data(db=None): db = psef.models.db if db is None else db if not app.config['DEBUG']: print('You can not add test data in production mode', file=sys.stderr) return 1 if not os.path.isdir(app.config['UPLOAD_DIR']): os.mkdir(app.config['UPLOAD_DIR']) seed() db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/courses.json', 'r') as c: cs = json.load(c) for c in cs: if m.Course.query.filter_by(name=c['name']).first() is None: db.session.add(m.Course.create_and_add(name=c['name'])) db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/assignments.json', 'r') as c: cs = json.load(c) for c in cs: assig = m.Assignment.query.filter_by(name=c['name']).first() if assig is None: db.session.add( m.Assignment( name=c['name'], deadline=cg_dt_utils.now() + datetime.timedelta(days=c['deadline']), state=c['state'], description=c['description'], course=m.Course.query.filter_by( name=c['course']).first(), is_lti=False, )) else: assig.description = c['description'] assig.state = c['state'] assig.course = m.Course.query.filter_by( name=c['course']).first() db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/users.json', 'r') as c: cs = json.load(c) for c in cs: u = m.User.query.filter_by(name=c['name']).first() courses = { m.Course.query.filter_by(name=name).first(): role for name, role in c['courses'].items() } perms = { course.id: m.CourseRole.query.filter_by(name=name, course_id=course.id).first() for course, name in courses.items() } username = c['name'].split(' ')[0].lower() if u is not None: u.name = c['name'] u.courses = perms u.email = c['name'].replace(' ', '_').lower() + '@example.com' u.password = c['name'] u.username = username u.role = m.Role.query.filter_by(name=c['role']).first() else: u = m.User(name=c['name'], courses=perms, email=c['name'].replace(' ', '_').lower() + '@example.com', password=c['name'], username=username, role=m.Role.query.filter_by(name=c['role']).first()) db.session.add(u) for course, role in courses.items(): if role == 'Student': for assig in course.assignments: work = m.Work(assignment=assig, user=u) f = m.File(work=work, name='Top stub dir ({})'.format(u.name), is_directory=True) filename = str(uuid.uuid4()) shutil.copyfile( __file__, os.path.join(app.config['UPLOAD_DIR'], filename)) f.children = [ m.File(work=work, name='manage.py', is_directory=False, filename=filename) ] db.session.add(f) db.session.add(work) db.session.commit() with open( f'{os.path.dirname(os.path.abspath(__file__))}/test_data/rubrics.json', 'r') as c: cs = json.load(c) for c in cs: for row in c['rows']: assignment = m.Assignment.query.filter_by( name=c['assignment']).first() if assignment is not None: rubric_row = m.RubricRow.query.filter_by( header=row['header'], description=row['description'], assignment_id=assignment.id, ).first() if rubric_row is None: rubric_row = m.RubricRow( header=row['header'], description=row['description'], assignment=assignment, rubric_row_type='normal', ) db.session.add(rubric_row) for item in row['items']: if not db.session.query( m.RubricItem.query.filter_by( rubricrow_id=rubric_row.id, **item, ).exists()).scalar(): rubric_item = m.RubricItem( description=item['description'] * 5, header=item['header'], points=item['points'], rubricrow=rubric_row) db.session.add(rubric_item) db.session.commit()
def upload_work(assignment_id: int) -> JSONResponse[models.Work]: """Upload one or more files as :class:`.models.Work` to the given :class:`.models.Assignment`. .. :quickref: Assignment; Create work by uploading a file. :query ignored_files: How to handle ignored files. The options are: ``ignore``: this the default, sipmly do nothing about ignored files, ``delete``: delete the ignored files, ``error``: raise an :py:class:`.APIException` when there are ignored files in the archive. :query author: The username of the user that should be the author of this new submission. Simply don't give this if you want to be the author. :param int assignment_id: The id of the assignment :returns: A JSON serialized work and with the status code 201. :raises APIException: If the request is bigger than the maximum upload size. (REQUEST_TOO_LARGE) :raises APIException: If there was no file in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If some file was under the wrong key or some filename is empty. (INVALID_PARAM) """ files = get_submission_files_from_request(check_size=True) assig = helpers.get_or_404(models.Assignment, assignment_id) given_author = request.args.get('author', None) if given_author is None: author = current_user else: author = helpers.filter_single_or_404( models.User, models.User.username == given_author, ) auth.ensure_can_submit_work(assig, author) work = models.Work(assignment=assig, user_id=author.id) work.divide_new_work() db.session.add(work) try: raise_or_delete = psef.files.IgnoreHandling[request.args.get( 'ignored_files', 'keep', )] except KeyError: # The enum value does not exist raise APIException( 'The given value for "ignored_files" is invalid', ( f'The value "{request.args.get("ignored_files")}" is' ' not in the `IgnoreHandling` enum' ), APICodes.INVALID_PARAM, 400, ) tree = psef.files.process_files( files, force_txt=False, ignore_filter=IgnoreFilterManager(assig.cgignore), handle_ignore=raise_or_delete, ) work.add_file_tree(db.session, tree) db.session.flush() if assig.is_lti: work.passback_grade(initial=True) db.session.commit() work.run_linter() return jsonify(work, status_code=201)
def post_submissions(assignment_id: int) -> EmptyResponse: """Add submissions to the given:class:`.models.Assignment` from a blackboard zip file as :class:`.models.Work` objects. .. :quickref: Assignment; Create works from a blackboard zip. You should upload a file as multiform post request. The key should start with 'file'. Multiple blackboard zips are not supported and result in one zip being chosen at (psuedo) random. :param int assignment_id: The id of the assignment :returns: An empty response with return code 204 :raises APIException: If no assignment with given id exists. (OBJECT_ID_NOT_FOUND) :raises APIException: If there was no file in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If the file parameter name is incorrect or if the given file does not contain any valid submissions. (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user is not allowed to manage the course attached to the assignment. (INCORRECT_PERMISSION) """ assignment = helpers.get_or_404(models.Assignment, assignment_id) auth.ensure_permission('can_upload_bb_zip', assignment.course_id) files = get_submission_files_from_request(check_size=False) try: submissions = psef.files.process_blackboard_zip(files[0]) except Exception: # pylint: disable=broad-except # TODO: Narrow this exception down. submissions = [] if not submissions: raise APIException( "The blackboard zip could not imported or it was empty.", 'The blackboard zip could not' ' be parsed or it did not contain any valid submissions.', APICodes.INVALID_PARAM, 400 ) missing, recalc_missing = assignment.get_divided_amount_missing() sub_lookup = {} for sub in assignment.get_all_latest_submissions(): sub_lookup[sub.user_id] = sub student_course_role = models.CourseRole.query.filter_by( name='Student', course_id=assignment.course_id ).first() global_role = models.Role.query.filter_by(name='Student').first() subs = [] hists = [] found_users = { u.username: u for u in models.User.query.filter( t.cast( models.DbColumn[str], models.User.username, ).in_([si.student_id for si, _ in submissions]) ).options(joinedload(models.User.courses)) } newly_assigned: t.Set[t.Optional[int]] = set() for submission_info, submission_tree in submissions: user = found_users.get(submission_info.student_id, None) if user is None: # TODO: Check if this role still exists user = models.User( name=submission_info.student_name, username=submission_info.student_id, courses={assignment.course_id: student_course_role}, email='', password=None, role=global_role, ) found_users[user.username] = user # We don't need to track the users to insert as we are already # tracking the submissions of them and they are coupled. else: user.courses[assignment.course_id] = student_course_role work = models.Work( assignment=assignment, user=user, created_at=submission_info.created_at, ) subs.append(work) if user.id is not None and user.id in sub_lookup: work.assigned_to = sub_lookup[user.id].assigned_to if work.assigned_to is None: if missing: work.assigned_to = max( missing.keys(), key=lambda k: missing[k] ) missing = recalc_missing(work.assigned_to) sub_lookup[user.id] = work hists.append( work.set_grade( submission_info.grade, current_user, add_to_session=False ) ) work.add_file_tree(db.session, submission_tree) if work.assigned_to is not None: newly_assigned.add(work.assigned_to) assignment.set_graders_to_not_done( list(newly_assigned), send_mail=True, ignore_errors=True, ) db.session.bulk_save_objects(subs) db.session.flush() # TODO: This loop should be eliminated. for hist in hists: hist.work_id = hist.work.id hist.user_id = hist.user.id db.session.bulk_save_objects(hists) db.session.commit() return make_empty_response()