def get(self, course_uuid, group_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) group = Group.get_active_by_uuid_or_404(group_uuid) require( READ, UserCourse(course_id=course.id), title="Group Members Unavailable", message= "Group membership can be seen only by those enrolled in the course. Please double-check your enrollment in this course." ) members = User.query \ .join(UserCourse, UserCourse.user_id == User.id) \ .filter(and_( UserCourse.course_id == course.id, UserCourse.course_role != CourseRole.dropped, UserCourse.group_id == group.id )) \ .order_by(User.lastname, User.firstname) \ .all() on_group_user_list_get.send(current_app._get_current_object(), event_name=on_group_user_list_get.name, user=current_user, course_id=course.id, data={'group_id': group.id}) return { 'objects': [{ 'id': u.uuid, 'name': u.fullname_sortable } for u in members] }
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) new_group = Group(course_id=course.id) require(CREATE, new_group, title="Group Not Saved", message="Sorry, your role in this course does not allow you to save groups.") params = new_group_parser.parse_args() new_group.name = params.get("name") # check if group name is unique group_name_exists = Group.query \ .filter( Group.course_id == course.id, Group.name == new_group.name ) \ .first() if group_name_exists: abort(400, title="Group Not Added", message="Sorry, the group name you have entered already exists. Please choose a different name.") db.session.add(new_group) db.session.commit() on_group_create.send( current_app._get_current_object(), event_name=on_group_create.name, user=current_user, course_id=course.id, data=marshal(new_group, dataformat.get_group()) ) return marshal(new_group, dataformat.get_group())
def delete(self, course_uuid, group_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) group = Group.get_active_by_uuid_or_404(group_uuid) require(DELETE, group, title="Group Not Deleted", message="Sorry, your role in this course does not allow you to delete groups.") # check if group has submitted any answers group_answer_exists = Answer.query \ .filter_by( group_id=group.id, practice=False, active=True, draft=False ) \ .first() if group_answer_exists: abort(400, title="Group Not Deleted", message="Sorry, you cannot remove groups that have submitted answers.") group.active = False # remove members from the group user_courses = UserCourse.query \ .filter_by(group_id=group.id) \ .all() for user_course in user_courses: user_course.group_id = None db.session.commit() on_group_delete.send( current_app._get_current_object(), event_name=on_group_delete.name, user=current_user, course_id=course.id ) return {'id': group.uuid }
def post(self, course_uuid, group_uuid, user_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) group = Group.get_active_by_uuid_or_404(group_uuid) user = User.get_by_uuid_or_404(user_uuid) user_course = UserCourse.query \ .filter(and_( UserCourse.course_id == course.id, UserCourse.user_id == user.id, UserCourse.course_role != CourseRole.dropped )) \ .first_or_404() require( EDIT, user_course, title="Group Not Saved", message= "Sorry, your role in this course does not allow you to save groups." ) if course.groups_locked and user_course.group_id != None and user_course.group_id != group.id: abort( 400, title="Group Not Saved", message= "The course groups are locked. This user is already assigned to a different group." ) user_course.group_id = group.id db.session.commit() on_group_user_create.send(current_app._get_current_object(), event_name=on_group_user_create.name, user=current_user, course_id=course.id, data={'user_id': user.id}) return marshal(group, dataformat.get_group())
def get(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require(READ, Group(course_id=course.id), title="Groups Unavailable", message="Groups can be seen only by those enrolled in the course. Please double-check your enrollment in this course.") groups = Group.query \ .filter_by( course_id=course.id, active=True ) \ .order_by(Group.name) \ .all() on_group_get.send( current_app._get_current_object(), event_name=on_group_get.name, user=current_user, course_id=course.id ) return {'objects': marshal(groups, dataformat.get_group()) }
def post(self, course_uuid, group_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) group = Group.get_active_by_uuid_or_404(group_uuid) require(EDIT, group, title="Group Not Saved", message="Sorry, your role in this course does not allow you to save groups.") params = existing_group_parser.parse_args() # check if group name is unique group_name_exists = Group.query \ .filter( Group.id != group.id, Group.course_id == group.course_id, Group.name == params.get("name", group.name) ) \ .first() if group_name_exists: abort(400, title="Group Not Saved", message="Sorry, the group name you have entered already exists. Please choose a different name.") group.name = params.get("name", group.name) model_changes = get_model_changes(group) db.session.commit() on_group_edit.send( current_app._get_current_object(), event_name=on_group_edit.name, user=current_user, course_id=course.id, data=model_changes ) return marshal(group, dataformat.get_group())
def post(self, course_uuid, assignment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) if not assignment.answer_grace and not allow(MANAGE, assignment): abort( 403, title="Answer Not Submitted", message= "Sorry, the answer deadline has passed. No answers can be submitted after the deadline unless the instructor submits the answer for you." ) require( CREATE, Answer(course_id=course.id), title="Answer Not Submitted", message= "Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course." ) restrict_user = not allow(MANAGE, assignment) answer = Answer(assignment_id=assignment.id) params = new_answer_parser.parse_args() answer.content = params.get("content") answer.draft = params.get("draft") file_uuid = params.get('file_id') attachment = None if file_uuid: attachment = File.get_by_uuid_or_404(file_uuid) answer.file_id = attachment.id else: answer.file_id = None # non-drafts must have content if not answer.draft and not answer.content and not file_uuid: abort( 400, title="Answer Not Submitted", message= "Please provide content in the text editor or upload a file and try submitting again." ) user_uuid = params.get("user_id") group_uuid = params.get("group_id") # we allow instructor and TA to submit multiple answers for other users in the class if user_uuid and not allow(MANAGE, Answer(course_id=course.id)): abort( 400, title="Answer Not Submitted", message= "Only instructors and teaching assistants can submit an answer on behalf of another." ) if group_uuid and not assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Group answers are not allowed for this assignment.") if group_uuid and not allow(MANAGE, Answer(course_id=course.id)): abort( 400, title="Answer Not Submitted", message= "Only instructors and teaching assistants can submit an answer on behalf of a group." ) if group_uuid and user_uuid: abort( 400, title="Answer Not Submitted", message= "You cannot submit an answer for a user and a group at the same time." ) user = User.get_by_uuid_or_404( user_uuid) if user_uuid else current_user group = Group.get_active_by_uuid_or_404( group_uuid) if group_uuid else None if restrict_user and assignment.enable_group_answers and not group: group = current_user.get_course_group(course.id) if group == None: abort( 400, title="Answer Not Submitted", message= "You are currently not in any group for this course. Please contact your instructor to be added to a group." ) check_for_existing_answers = False if group and assignment.enable_group_answers: if group.course_id != course.id: abort( 400, title="Answer Not Submitted", message= "Group answers can be submitted to courses they belong in." ) answer.user_id = None answer.group_id = group.id answer.comparable = True check_for_existing_answers = True else: answer.user_id = user.id answer.group_id = None course_role = User.get_user_course_role(answer.user_id, course.id) # only system admin can add answers for themselves to a class without being enrolled in it # required for managing comparison examples as system admin if (not course_role or course_role == CourseRole.dropped ) and current_user.system_role != SystemRole.sys_admin: abort( 400, title="Answer Not Submitted", message= "Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course." ) if course_role == CourseRole.student and assignment.enable_group_answers: abort( 400, title="Answer Not Submitted", message= "Students can only submit group answers for this assignment." ) # we allow instructor and TA to submit multiple answers for their own, # but not for student. Each student can only have one answer. if course_role and course_role == CourseRole.student: check_for_existing_answers = True answer.comparable = True else: # instructor / TA / Sys Admin can mark the answer as non-comparable, unless the answer is for a student answer.comparable = params.get("comparable") if check_for_existing_answers: # check for answers with user_id or group_id prev_answers = Answer.query \ .filter_by( assignment_id=assignment.id, user_id=answer.user_id, group_id=answer.group_id, active=True ) \ .all() # check if there is a previous answer submitted for the student non_draft_answers = [ prev_answer for prev_answer in prev_answers if not prev_answer.draft ] if len(non_draft_answers) > 0: abort( 400, title="Answer Not Submitted", message= "An answer has already been submitted for this assignment by you or on your behalf." ) # check if there is a previous draft answer submitted for the student (soft-delete if present) draft_answers = [ prev_answer for prev_answer in prev_answers if prev_answer.draft ] for draft_answer in draft_answers: draft_answer.active = False # set submission date if answer is being submitted for the first time if not answer.draft and not answer.submission_date: answer.submission_date = datetime.datetime.utcnow() answer.update_attempt(params.get('attempt_uuid'), params.get('attempt_started', None), params.get('attempt_ended', None)) db.session.add(answer) db.session.commit() on_answer_create.send(self, event_name=on_answer_create.name, user=current_user, course_id=course.id, answer=answer, data=marshal( answer, dataformat.get_answer(restrict_user))) if attachment: on_attach_file.send(self, event_name=on_attach_file.name, user=current_user, course_id=course.id, file=attachment, data={ 'answer_id': answer.id, 'file_id': attachment.id }) # update course & assignment grade for user if answer is fully submitted if not answer.draft: if answer.user: assignment.calculate_grade(answer.user) course.calculate_grade(answer.user) elif answer.group: assignment.calculate_group_grade(answer.group) course.calculate_group_grade(answer.group) return marshal(answer, dataformat.get_answer(restrict_user))
def import_users(import_type, course, users): invalids = [] # invalid entries - eg. invalid # of columns count = 0 # store number of successful enrolments imported_users = [] set_user_passwords = [] # store unique user identifiers - eg. student number - throws error if duplicate in file import_usernames = [] import_student_numbers = [] # store unique user identifiers - eg. student number - throws error if duplicate in file existing_system_usernames = _get_existing_users_by_identifier( import_type, users) existing_system_student_numbers = _get_existing_users_by_student_number( import_type, users) groups = course.groups.all() groups_by_name = {} for group in groups: groups_by_name[group.name] = group # create / update users in file for user_row in users: if len(user_row) < 1: continue # skip empty row user = _parse_user_row(import_type, user_row) # validate unique identifier username = user.get('username') password = user.get( 'password' ) #always None for CAS/SAML import, can be None for existing users on ComPAIR import student_number = user.get('student_number') u = existing_system_usernames.get(username, None) if not username: invalids.append({ 'user': User(username=username), 'message': 'The username is required.' }) continue elif username in import_usernames: invalids.append({ 'user': User(username=username), 'message': 'This username already exists in the file.' }) continue if u: # overwrite password if user has not logged in yet if u.last_online == None and not password in [None, '*']: set_user_passwords.append((u, password)) else: u = User(username=None, password=None, student_number=user.get('student_number'), firstname=user.get('firstname'), lastname=user.get('lastname'), email=user.get('email')) if import_type == ThirdPartyType.cas.value or import_type == ThirdPartyType.saml.value: # CAS/SAML login u.third_party_auths.append( ThirdPartyUser( unique_identifier=username, third_party_type=ThirdPartyType(import_type))) else: # ComPAIR login u.username = username if password in [None, '*']: invalids.append({ 'user': u, 'message': 'The password is required.' }) continue elif len(password) < 4: invalids.append({ 'user': u, 'message': 'The password must be at least 4 characters long.' }) continue else: set_user_passwords.append((u, password)) # validate student number (if not None) if student_number: # invalid if already showed up in file if student_number in import_student_numbers: u.username = username invalids.append({ 'user': u, 'message': 'This student number already exists in the file.' }) continue # invalid if student number already exists in the system elif student_number in existing_system_student_numbers: u.username = username invalids.append({ 'user': u, 'message': 'This student number already exists in the system.' }) continue u.system_role = SystemRole.student u.displayname = user.get('displayname') if user.get( 'displayname') else display_name_generator() db.session.add(u) import_usernames.append(username) if student_number: import_student_numbers.append(student_number) imported_users.append((u, user.get('group'))) db.session.commit() enroled = UserCourse.query \ .filter_by(course_id=course.id) \ .all() enroled = {e.user_id: e for e in enroled} students = UserCourse.query \ .filter_by( course_id=course.id, course_role=CourseRole.student ) \ .all() students = {s.user_id: s for s in students} # enrol valid users in file for user, group_name in imported_users: enrol = enroled.get(user.id, UserCourse(course_id=course.id, user_id=user.id)) enrol.group = None if group_name: group = groups_by_name.get(group_name) # add new groups if needed if not group: group = Group(course=course, name=group_name) groups_by_name[group_name] = group db.session.add(group) enrol.group = group # do not overwrite instructor or teaching assistant roles if enrol.course_role not in [ CourseRole.instructor, CourseRole.teaching_assistant ]: enrol.course_role = CourseRole.student if user.id in students: del students[user.id] count += 1 db.session.add(enrol) db.session.commit() # unenrol users not in file anymore for user_id in students: enrolment = students.get(user_id) # skip users that are already dropped if enrolment.course_role == CourseRole.dropped: continue enrolment.course_role = CourseRole.dropped enrolment.group_id = None db.session.add(enrolment) db.session.commit() # wait until user ids are generated before starting background jobs # perform password update in chunks of 100 chunk_size = 100 chunks = [ set_user_passwords[index:index + chunk_size] for index in range(0, len(set_user_passwords), chunk_size) ] for chunk in chunks: set_passwords.delay({user.id: password for (user, password) in chunk}) on_classlist_upload.send(current_app._get_current_object(), event_name=on_classlist_upload.name, user=current_user, course_id=course.id) return { 'success': count, 'invalids': marshal(invalids, dataformat.get_import_users_results(False)) }
def post(self, course_uuid, group_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) group = Group.get_active_by_uuid_or_404(group_uuid) require( EDIT, UserCourse(course_id=course.id), title="Group Not Saved", message= "Sorry, your role in this course does not allow you to save groups." ) params = user_list_parser.parse_args() if len(params.get('ids')) == 0: abort( 400, title="Group Not Saved", message= "Please select at least one user below and then try to apply the group again." ) user_courses = UserCourse.query \ .join(User, UserCourse.user_id == User.id) \ .filter(and_( UserCourse.course_id == course.id, User.uuid.in_(params.get('ids')), UserCourse.course_role != CourseRole.dropped )) \ .all() if len(params.get('ids')) != len(user_courses): abort( 400, title="Group Not Saved", message= "One or more users selected are not enrolled in the course yet." ) for user_course in user_courses: if course.groups_locked and user_course.group_id != None and user_course.group_id != group.id: abort( 400, title="Group Not Saved", message= "The course groups are locked. One or more users are already assigned to a different group." ) for user_course in user_courses: user_course.group_id = group.id db.session.commit() on_group_user_list_create.send( current_app._get_current_object(), event_name=on_group_user_list_create.name, user=current_user, course_id=course.id, data={ 'user_ids': [user_course.user_id for user_course in user_courses] }) return marshal(group, dataformat.get_group())
def get(self, course_uuid, assignment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) require(READ, Comparison(course_id=course.id), title="Comparisons Unavailable", message="Sorry, your role in this course does not allow you to view all comparisons for this assignment.") restrict_user = is_user_access_restricted(current_user) params = assignment_users_comparison_list_parser.parse_args() # only get users who have at least made one comparison # each paginated item is a user (with a set of comparisons and self-evaluations) user_query = User.query \ .join(UserCourse, and_( User.id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .join(Comparison, and_( Comparison.user_id == User.id, Comparison.assignment_id == assignment.id )) \ .filter(and_( UserCourse.course_role != CourseRole.dropped, Comparison.completed == True )) \ .group_by(User) \ .order_by(User.lastname, User.firstname) self_evaluation_total = AnswerComment.query \ .join("answer") \ .with_entities( func.count(Answer.assignment_id).label('self_evaluation_count') ) \ .filter(and_( AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.draft == False, Answer.active == True, Answer.practice == False, Answer.draft == False, Answer.assignment_id == assignment.id )) comparison_total = Comparison.query \ .with_entities( func.count(Comparison.assignment_id).label('comparison_count') ) \ .filter(and_( Comparison.completed == True, Comparison.assignment_id == assignment.id )) if params['author']: user = User.get_by_uuid_or_404(params['author']) user_query = user_query.filter(User.id == user.id) self_evaluation_total = self_evaluation_total.filter(AnswerComment.user_id == user.id) comparison_total = comparison_total.filter(Comparison.user_id == user.id) elif params['group']: group = Group.get_active_by_uuid_or_404(params['group']) user_query = user_query.filter(UserCourse.group_id == group.id) self_evaluation_total = self_evaluation_total \ .join(UserCourse, and_( AnswerComment.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .filter(UserCourse.group_id == group.id) comparison_total = comparison_total \ .join(UserCourse, and_( Comparison.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .filter(UserCourse.group_id == group.id) page = user_query.paginate(params['page'], params['perPage']) self_evaluation_total = self_evaluation_total.scalar() comparison_total = comparison_total.scalar() comparison_sets = [] if page.total: user_ids = [user.id for user in page.items] # get all comparisons that group of users created comparisons = Comparison.query \ .filter(and_( Comparison.completed == True, Comparison.assignment_id == assignment.id, Comparison.user_id.in_(user_ids) )) \ .all() # retrieve the answer comments user_comparison_answers = {} for comparison in comparisons: user_answers = user_comparison_answers.setdefault(comparison.user_id, set()) user_answers.add(comparison.answer1_id) user_answers.add(comparison.answer2_id) conditions = [] for user_id, answer_set in user_comparison_answers.items(): conditions.append(and_( AnswerComment.comment_type == AnswerCommentType.evaluation, AnswerComment.user_id == user_id, AnswerComment.answer_id.in_(list(answer_set)), AnswerComment.assignment_id == assignment.id )) conditions.append(and_( AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.user_id == user_id, AnswerComment.assignment_id == assignment.id )) answer_comments = AnswerComment.query \ .filter(or_(*conditions)) \ .filter_by(draft=False) \ .all() # add comparison answer evaluation comments to comparison object for comparison in comparisons: comparison.answer1_feedback = [comment for comment in answer_comments if comment.user_id == comparison.user_id and comment.answer_id == comparison.answer1_id and comment.comment_type == AnswerCommentType.evaluation ] comparison.answer2_feedback = [comment for comment in answer_comments if comment.user_id == comparison.user_id and comment.answer_id == comparison.answer2_id and comment.comment_type == AnswerCommentType.evaluation ] for user in page.items: comparison_sets.append({ 'user': user, 'comparisons': [comparison for comparison in comparisons if comparison.user_id == user.id ], 'self_evaluations': [comment for comment in answer_comments if comment.user_id == user.id and comment.comment_type == AnswerCommentType.self_evaluation ] }) on_assignment_users_comparisons_get.send( self, event_name=on_assignment_users_comparisons_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id} ) return {"objects": marshal(comparison_sets, dataformat.get_comparison_set(restrict_user, with_user=True)), "comparison_total": comparison_total, "self_evaluation_total": self_evaluation_total, "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}
def post(self, course_uuid, assignment_uuid, answer_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) if not assignment.answer_grace and not allow(MANAGE, assignment): abort(403, title="Answer Not Submitted", message="Sorry, the answer deadline has passed. No answers can be submitted after the deadline unless the instructor submits the answer for you.") answer = Answer.get_active_by_uuid_or_404(answer_uuid) old_file = answer.file require(EDIT, answer, title="Answer Not Saved", message="Sorry, your role in this course does not allow you to save this answer.") restrict_user = not allow(MANAGE, assignment) if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if assignment.id in DemoDataFixture.DEFAULT_ASSIGNMENT_IDS and answer.user_id in DemoDataFixture.DEFAULT_STUDENT_IDS: abort(400, title="Answer Not Saved", message="Sorry, you cannot edit the default student demo answers.") params = existing_answer_parser.parse_args() # make sure the answer id in the url and the id matches if params['id'] != answer_uuid: abort(400, title="Answer Not Submitted", message="The answer's ID does not match the URL, which is required in order to save the answer.") # modify answer according to new values, preserve original values if values not passed answer.content = params.get("content") user_uuid = params.get("user_id") group_uuid = params.get("group_id") # we allow instructor and TA to submit multiple answers for other users in the class if user_uuid and user_uuid != answer.user_uuid and not allow(MANAGE, answer): abort(400, title="Answer Not Submitted", message="Only instructors and teaching assistants can submit an answer on behalf of another.") if group_uuid and not assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Group answers are not allowed for this assignment.") if group_uuid and group_uuid != answer.group_uuid and not allow(MANAGE, answer): abort(400, title="Answer Not Submitted", message="Only instructors and teaching assistants can submit an answer on behalf of a group.") if group_uuid and user_uuid: abort(400, title="Answer Not Submitted", message="You cannot submit an answer for a user and a group at the same time.") user = User.get_by_uuid_or_404(user_uuid) if user_uuid else answer.user group = Group.get_active_by_uuid_or_404(group_uuid) if group_uuid else answer.group check_for_existing_answers = False if group and assignment.enable_group_answers: if group.course_id != course.id: abort(400, title="Answer Not Submitted", message="Group answers can be submitted to courses they belong in.") answer.user_id = None answer.group_id = group.id answer.comparable = True check_for_existing_answers = True else: answer.user_id = user.id answer.group_id = None course_role = User.get_user_course_role(answer.user_id, course.id) # only system admin can add answers for themselves to a class without being enrolled in it # required for managing comparison examples as system admin if (not course_role or course_role == CourseRole.dropped) and current_user.system_role != SystemRole.sys_admin: abort(400, title="Answer Not Submitted", message="Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course.") if course_role == CourseRole.student and assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Students can only submit group answers for this assignment.") # we allow instructor and TA to submit multiple answers for their own, # but not for student. Each student can only have one answer. if course_role and course_role == CourseRole.student: check_for_existing_answers = True answer.comparable = True else: # instructor / TA / Sys Admin can mark the answer as non-comparable, unless the answer is for a student answer.comparable = params.get("comparable") if check_for_existing_answers: # check for answers with user_id or group_id prev_answers = Answer.query \ .filter_by( assignment_id=assignment.id, user_id=answer.user_id, group_id=answer.group_id, active=True ) \ .filter(Answer.id != answer.id) \ .all() # check if there is a previous answer submitted for the student non_draft_answers = [prev_answer for prev_answer in prev_answers if not prev_answer.draft] if len(non_draft_answers) > 0: abort(400, title="Answer Not Submitted", message="An answer has already been submitted for this assignment by you or on your behalf.") # check if there is a previous draft answer submitted for the student (soft-delete if present) draft_answers = [prev_answer for prev_answer in prev_answers if prev_answer.draft] for draft_answer in draft_answers: draft_answer.active = False # can only change draft status while a draft if answer.draft: answer.draft = params.get("draft") file_uuid = params.get('file_id') attachment=None if file_uuid: attachment = File.get_by_uuid_or_404(file_uuid) answer.file_id = attachment.id else: answer.file_id = None # non-drafts must have content if not answer.draft and not answer.content and not file_uuid: abort(400, title="Answer Not Submitted", message="Please provide content in the text editor or upload a file and try submitting again.") # set submission date if answer is being submitted for the first time if not answer.draft and not answer.submission_date: answer.submission_date = datetime.datetime.utcnow() answer.update_attempt( params.get('attempt_uuid'), params.get('attempt_started', None), params.get('attempt_ended', None) ) model_changes = get_model_changes(answer) db.session.add(answer) db.session.commit() on_answer_modified.send( self, event_name=on_answer_modified.name, user=current_user, course_id=course.id, answer=answer, data=model_changes) if old_file and (not attachment or old_file.id != attachment.id): on_detach_file.send( self, event_name=on_detach_file.name, user=current_user, course_id=course.id, file=old_file, answer=answer, data={'answer_id': answer.id, 'file_id': old_file.id}) if attachment and (not old_file or old_file.id != attachment.id): on_attach_file.send( self, event_name=on_attach_file.name, user=current_user, course_id=course.id, file=attachment, data={'answer_id': answer.id, 'file_id': attachment.id}) # update course & assignment grade for user if answer is fully submitted if not answer.draft: if answer.user: assignment.calculate_grade(answer.user) course.calculate_grade(answer.user) elif answer.group: assignment.calculate_group_grade(answer.group) course.calculate_group_grade(answer.group) return marshal(answer, dataformat.get_answer(restrict_user))
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment(course_id=course.id) require(MANAGE, assignment, title="Report Not Run", message="Sorry, your system role does not allow you to run reports.") params = report_parser.parse_args() group_uuid = params.get('group_id', None) report_type = params.get('type') group = Group.get_active_by_uuid_or_404(group_uuid) if group_uuid else None assignments = [] assignment_uuid = params.get('assignment', None) if assignment_uuid: assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) assignments = [assignment] else: assignments = Assignment.query \ .filter_by( course_id=course.id, active=True ) \ .all() if report_type == "participation_stat": data = participation_stat_report(course, assignments, group, assignment_uuid is None) title = [ 'Assignment', 'Last Name', 'First Name','Student Number', 'User UUID', 'Answer', 'Answer ID', 'Answer Deleted', 'Answer Submission Date', 'Answer Last Modified', 'Answer Score (Normalized)', 'Overall Rank', 'Comparisons Submitted', 'Comparisons Required', 'Comparison Requirements Met', 'Self-Evaluation Submitted', 'Feedback Submitted (During Comparisons)', 'Feedback Submitted (Outside Comparisons)'] titles = [title] elif report_type == "participation": user_titles = ['Last Name', 'First Name', 'Student Number'] data = participation_report(course, assignments, group) title_row1 = [""] * len(user_titles) title_row2 = user_titles for assignment in assignments: title_row1 += [assignment.name] title_row2.append('Participation Grade') title_row1 += [""] title_row2.append('Answer') title_row1 += [""] title_row2.append('Attachment') title_row1 += [""] title_row2.append('Answer Score (Normalized)') title_row1 += [""] title_row2.append("Comparisons Submitted (" + str(assignment.total_comparisons_required) + ' required)') if assignment.enable_self_evaluation: title_row1 += [""] title_row2.append("Self-Evaluation Submitted") title_row1 += [""] title_row2.append("Feedback Submitted (During Comparisons)") title_row1 += [""] title_row2.append("Feedback Submitted (Outside Comparisons)") titles = [title_row1, title_row2] elif report_type == "peer_feedback": titles1 = [ "", "Feedback Author", "", "", "Answer Author", "", "", "", "" ] titles2 = [ "Assignment", "Last Name", "First Name", "Student Number", "Last Name", "First Name", "Student Number", "Feedback Type", "Feedback", "Feedback Character Count" ] data = peer_feedback_report(course, assignments, group) titles = [titles1, titles2] else: abort(400, title="Report Not Run", message="Please try again with a report type from the list of report types provided.") name = name_generator(course, report_type, group) tmp_name = os.path.join(current_app.config['REPORT_FOLDER'], name) with open(tmp_name, 'wb') as report: out = csv.writer(report) for t in titles: out.writerow(t) for s in data: out.writerow(s) on_export_report.send( self, event_name=on_export_report.name, user=current_user, course_id=course.id, data={'type': report_type, 'filename': name}) return {'file': 'report/' + name}
def get(self, course_uuid, assignment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) require(READ, Comparison(course_id=course.id), title="Comparisons Unavailable", message="Sorry, your role in this course does not allow you to view all comparisons for this assignment.") restrict_user = is_user_access_restricted(current_user) params = assignment_users_comparison_list_parser.parse_args() # get users who have at least made one comparison or finished self-eval. # each paginated item is a user (with a set of comparisons and self-evaluations) user_query = User.query \ .join(UserCourse, and_( User.id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .outerjoin(Comparison, and_( Comparison.user_id == User.id, Comparison.assignment_id == assignment.id )) \ .outerjoin(AnswerComment, and_( AnswerComment.user_id == User.id, AnswerComment.assignment_id == assignment.id, AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, )) \ .filter(and_( or_( AnswerComment.draft == False, Comparison.completed == True ), UserCourse.course_role != CourseRole.dropped \ )) \ .group_by(User) \ .order_by(User.lastname, User.firstname) self_evaluation_total = AnswerComment.query \ .join(Answer) \ .with_entities( func.count(Answer.assignment_id).label('self_evaluation_count') ) \ .filter(and_( AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.draft == False, # self-eval on deleted answer should also be counted. so NOT checking the active flag # Answer.active == True, Answer.practice == False, Answer.draft == False, Answer.assignment_id == assignment.id )) comparison_total = Comparison.query \ .with_entities( func.count(Comparison.assignment_id).label('comparison_count') ) \ .filter(and_( Comparison.completed == True, Comparison.assignment_id == assignment.id )) if params['author']: user = User.get_by_uuid_or_404(params['author']) user_query = user_query.filter(User.id == user.id) self_evaluation_total = self_evaluation_total.filter(AnswerComment.user_id == user.id) comparison_total = comparison_total.filter(Comparison.user_id == user.id) elif params['group']: group = Group.get_active_by_uuid_or_404(params['group']) user_query = user_query.filter(UserCourse.group_id == group.id) self_evaluation_total = self_evaluation_total \ .join(UserCourse, and_( AnswerComment.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .filter(UserCourse.group_id == group.id) comparison_total = comparison_total \ .join(UserCourse, and_( Comparison.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .filter(UserCourse.group_id == group.id) page = user_query.paginate(params['page'], params['perPage']) self_evaluation_total = self_evaluation_total.scalar() comparison_total = comparison_total.scalar() comparison_sets = [] if page.total: user_ids = [user.id for user in page.items] # get all comparisons that group of users created comparisons = Comparison.query \ .filter(and_( Comparison.completed == True, Comparison.assignment_id == assignment.id, Comparison.user_id.in_(user_ids) )) \ .all() # retrieve the answer comments user_comparison_answers = {} for comparison in comparisons: user_answers = user_comparison_answers.setdefault(comparison.user_id, set()) user_answers.add(comparison.answer1_id) user_answers.add(comparison.answer2_id) conditions = [] for user_id, answer_set in user_comparison_answers.items(): conditions.append(and_( AnswerComment.comment_type == AnswerCommentType.evaluation, AnswerComment.user_id == user_id, AnswerComment.answer_id.in_(list(answer_set)), AnswerComment.assignment_id == assignment.id )) conditions.append(and_( AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.user_id == user_id, AnswerComment.assignment_id == assignment.id )) answer_comments = AnswerComment.query \ .filter_by(assignment_id=assignment.id) \ .filter(or_(*conditions)) \ .filter_by(draft=False) \ .all() # add comparison answer evaluation comments to comparison object for comparison in comparisons: comparison.answer1_feedback = [comment for comment in answer_comments if comment.user_id == comparison.user_id and comment.answer_id == comparison.answer1_id and comment.comment_type == AnswerCommentType.evaluation ] comparison.answer2_feedback = [comment for comment in answer_comments if comment.user_id == comparison.user_id and comment.answer_id == comparison.answer2_id and comment.comment_type == AnswerCommentType.evaluation ] for user in page.items: comparison_sets.append({ 'user': user, 'comparisons': [comparison for comparison in comparisons if comparison.user_id == user.id ], 'self_evaluations': [comment for comment in answer_comments if comment.user_id == user.id and comment.comment_type == AnswerCommentType.self_evaluation ] }) on_assignment_users_comparisons_get.send( self, event_name=on_assignment_users_comparisons_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id} ) return {"objects": marshal(comparison_sets, dataformat.get_comparison_set(restrict_user, with_user=True)), "comparison_total": comparison_total, "self_evaluation_total": self_evaluation_total, "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}
def get(self, course_uuid, assignment_uuid): """ Return a list of answers for a assignment based on search criteria. The list of the answers are paginated. If there is any answers from instructor or TA, their answers will be on top of the list (unless they are comparable). :param course_uuid: course uuid :param assignment_uuid: assignment uuid :return: list of answers """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) require(READ, assignment, title="Answers Unavailable", message="Answers are visible only to those enrolled in the course. Please double-check your enrollment in this course.") restrict_user = not allow(MANAGE, assignment) params = answer_list_parser.parse_args() # if assingment has no rank display limit set, restricted users can't force to retreive by rank if params['orderBy'] == 'score' and restrict_user and not assignment.rank_display_limit: abort(400, title="Answers Unavailable", message="Sorry, you cannot cannot see answers by rank for this assignment.") if restrict_user and not assignment.after_comparing: # only the answer from student himself/herself should be returned params['author'] = current_user.uuid # this query could be further optimized by reduction the selected columns query = Answer.query \ .options(joinedload('file')) \ .options(joinedload('user')) \ .options(joinedload('group')) \ .options(joinedload('score')) \ .options(undefer_group('counts')) \ .outerjoin(UserCourse, and_( Answer.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .add_columns( and_( UserCourse != None, UserCourse.course_role.__eq__(CourseRole.instructor), not Answer.comparable ).label("instructor_role"), and_( UserCourse != None, UserCourse.course_role.__eq__(CourseRole.teaching_assistant), not Answer.comparable ).label("ta_role") ) \ .filter(and_( Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False, or_( and_(UserCourse.course_role != CourseRole.dropped, Answer.user_id != None), Answer.group_id != None ) )) \ .order_by(desc('instructor_role'), desc('ta_role')) if params['author']: user = User.get_by_uuid_or_404(params['author']) group = user.get_course_group(course.id) if group: query = query.filter(or_( Answer.user_id == user.id, Answer.group_id == group.id )) else: query = query.filter(Answer.user_id == user.id) elif params['group']: group = Group.get_active_by_uuid_or_404(params['group']) query = query.filter(or_( UserCourse.group_id == group.id, Answer.group_id == group.id )) if params['ids']: query = query.filter(Answer.uuid.in_(params['ids'].split(','))) if params['top']: query = query.filter(Answer.top_answer == True) if params['orderBy'] == 'score': # use outer join to include comparable answers that are not yet compared (for non-restricted users) query = query.outerjoin(AnswerScore) \ .filter(Answer.comparable == True) \ .order_by(AnswerScore.score.desc(), Answer.submission_date.desc(), Answer.created.desc()) if restrict_user: # when orderd by rank, students won't see answers that are not compared (i.e. no score/rank) query = query.filter(AnswerScore.score.isnot(None)) # limit answers up to rank if rank_display_limit is set and current_user is restricted (student) if assignment.rank_display_limit and restrict_user: score_for_rank = AnswerScore.get_score_for_rank(assignment.id, assignment.rank_display_limit) # display answers with score >= score_for_rank if score_for_rank != None: # will get all answers with a score greater than or equal to the score for a given rank # the '- 0.00001' fixes floating point precision problems query = query.filter(AnswerScore.score >= score_for_rank - 0.00001) else: # when ordered by date, non-comparable answers should be on top of the list query = query.order_by(Answer.comparable, Answer.submission_date.desc(), Answer.created.desc()) page = query.paginate(params['page'], params['perPage'], error_out=False) # remove label entities from results page.items = [answer for (answer, instructor_role, ta_role) in page.items] on_answer_list_get.send( self, event_name=on_answer_list_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id}) # only include score/rank info if: # - requesters are non-restricted users (i.e. instructors / TAs); or, # - retrieving answers ordered by score/rank include_score = (not restrict_user) or \ (params['orderBy'] == 'score' and assignment.rank_display_limit) return {"objects": marshal(page.items, dataformat.get_answer(restrict_user, include_score=include_score)), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}
def post(self, course_uuid, assignment_uuid, answer_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) if not assignment.answer_grace and not allow(MANAGE, assignment): abort( 403, title="Answer Not Submitted", message= "Sorry, the answer deadline has passed. No answers can be submitted after the deadline unless the instructor submits the answer for you." ) answer = Answer.get_active_by_uuid_or_404(answer_uuid) old_file = answer.file require( EDIT, answer, title="Answer Not Saved", message= "Sorry, your role in this course does not allow you to save this answer." ) restrict_user = not allow(MANAGE, assignment) if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if assignment.id in DemoDataFixture.DEFAULT_ASSIGNMENT_IDS and answer.user_id in DemoDataFixture.DEFAULT_STUDENT_IDS: abort( 400, title="Answer Not Saved", message= "Sorry, you cannot edit the default student demo answers.") params = existing_answer_parser.parse_args() # make sure the answer id in the url and the id matches if params['id'] != answer_uuid: abort( 400, title="Answer Not Submitted", message= "The answer's ID does not match the URL, which is required in order to save the answer." ) # modify answer according to new values, preserve original values if values not passed answer.content = params.get("content") user_uuid = params.get("user_id") group_uuid = params.get("group_id") # we allow instructor and TA to submit multiple answers for other users in the class if user_uuid and user_uuid != answer.user_uuid and not allow( MANAGE, answer): abort( 400, title="Answer Not Submitted", message= "Only instructors and teaching assistants can submit an answer on behalf of another." ) if group_uuid and not assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Group answers are not allowed for this assignment.") if group_uuid and group_uuid != answer.group_uuid and not allow( MANAGE, answer): abort( 400, title="Answer Not Submitted", message= "Only instructors and teaching assistants can submit an answer on behalf of a group." ) if group_uuid and user_uuid: abort( 400, title="Answer Not Submitted", message= "You cannot submit an answer for a user and a group at the same time." ) user = User.get_by_uuid_or_404(user_uuid) if user_uuid else answer.user group = Group.get_active_by_uuid_or_404( group_uuid) if group_uuid else answer.group check_for_existing_answers = False if group and assignment.enable_group_answers: if group.course_id != course.id: abort( 400, title="Answer Not Submitted", message= "Group answers can be submitted to courses they belong in." ) answer.user_id = None answer.group_id = group.id answer.comparable = True check_for_existing_answers = True else: answer.user_id = user.id answer.group_id = None course_role = User.get_user_course_role(answer.user_id, course.id) # only system admin can add answers for themselves to a class without being enrolled in it # required for managing comparison examples as system admin if (not course_role or course_role == CourseRole.dropped ) and current_user.system_role != SystemRole.sys_admin: abort( 400, title="Answer Not Submitted", message= "Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course." ) if course_role == CourseRole.student and assignment.enable_group_answers: abort( 400, title="Answer Not Submitted", message= "Students can only submit group answers for this assignment." ) # we allow instructor and TA to submit multiple answers for their own, # but not for student. Each student can only have one answer. if course_role and course_role == CourseRole.student: check_for_existing_answers = True answer.comparable = True else: # instructor / TA / Sys Admin can mark the answer as non-comparable, unless the answer is for a student answer.comparable = params.get("comparable") if check_for_existing_answers: # check for answers with user_id or group_id prev_answers = Answer.query \ .filter_by( assignment_id=assignment.id, user_id=answer.user_id, group_id=answer.group_id, active=True ) \ .filter(Answer.id != answer.id) \ .all() # check if there is a previous answer submitted for the student non_draft_answers = [ prev_answer for prev_answer in prev_answers if not prev_answer.draft ] if len(non_draft_answers) > 0: abort( 400, title="Answer Not Submitted", message= "An answer has already been submitted for this assignment by you or on your behalf." ) # check if there is a previous draft answer submitted for the student (soft-delete if present) draft_answers = [ prev_answer for prev_answer in prev_answers if prev_answer.draft ] for draft_answer in draft_answers: draft_answer.active = False # can only change draft status while a draft if answer.draft: answer.draft = params.get("draft") file_uuid = params.get('file_id') attachment = None if file_uuid: attachment = File.get_by_uuid_or_404(file_uuid) answer.file_id = attachment.id else: answer.file_id = None # non-drafts must have content if not answer.draft and not answer.content and not file_uuid: abort( 400, title="Answer Not Submitted", message= "Please provide content in the text editor or upload a file and try submitting again." ) # set submission date if answer is being submitted for the first time if not answer.draft and not answer.submission_date: answer.submission_date = datetime.datetime.utcnow() answer.update_attempt(params.get('attempt_uuid'), params.get('attempt_started', None), params.get('attempt_ended', None)) model_changes = get_model_changes(answer) db.session.add(answer) db.session.commit() on_answer_modified.send(self, event_name=on_answer_modified.name, user=current_user, course_id=course.id, answer=answer, data=model_changes) if old_file and (not attachment or old_file.id != attachment.id): on_detach_file.send(self, event_name=on_detach_file.name, user=current_user, course_id=course.id, file=old_file, answer=answer, data={ 'answer_id': answer.id, 'file_id': old_file.id }) if attachment and (not old_file or old_file.id != attachment.id): on_attach_file.send(self, event_name=on_attach_file.name, user=current_user, course_id=course.id, file=attachment, data={ 'answer_id': answer.id, 'file_id': attachment.id }) # update course & assignment grade for user if answer is fully submitted if not answer.draft: if answer.user: assignment.calculate_grade(answer.user) course.calculate_grade(answer.user) elif answer.group: assignment.calculate_group_grade(answer.group) course.calculate_group_grade(answer.group) return marshal(answer, dataformat.get_answer(restrict_user))
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment(course_id=course.id) require( MANAGE, assignment, title="Report Not Run", message="Sorry, your system role does not allow you to run reports." ) params = report_parser.parse_args() group_uuid = params.get('group_id', None) report_type = params.get('type') group = Group.get_active_by_uuid_or_404( group_uuid) if group_uuid else None assignments = [] assignment_uuid = params.get('assignment', None) if assignment_uuid: assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) assignments = [assignment] else: assignments = Assignment.query \ .filter_by( course_id=course.id, active=True ) \ .all() if report_type == "participation_stat": data = participation_stat_report(course, assignments, group, assignment_uuid is None) title = [ 'Assignment', 'Last Name', 'First Name', 'Student Number', 'User UUID', 'Answer', 'Answer ID', 'Answer Deleted', 'Answer Submission Date', 'Answer Last Modified', 'Answer Score (Normalized)', 'Overall Rank', 'Comparisons Submitted', 'Comparisons Required', 'Comparison Requirements Met', 'Self-Evaluation Submitted', 'Feedback Submitted (During Comparisons)', 'Feedback Submitted (Outside Comparisons)' ] titles = [title] elif report_type == "participation": user_titles = ['Last Name', 'First Name', 'Student Number'] data = participation_report(course, assignments, group) title_row1 = [""] * len(user_titles) title_row2 = user_titles for assignment in assignments: title_row1 += [assignment.name] title_row2.append('Participation Grade') title_row1 += [""] title_row2.append('Answer') title_row1 += [""] title_row2.append('Attachment') title_row1 += [""] title_row2.append('Answer Score (Normalized)') title_row1 += [""] title_row2.append("Comparisons Submitted (" + str(assignment.total_comparisons_required) + ' required)') if assignment.enable_self_evaluation: title_row1 += [""] title_row2.append("Self-Evaluation Submitted") title_row1 += [""] title_row2.append("Feedback Submitted (During Comparisons)") title_row1 += [""] title_row2.append("Feedback Submitted (Outside Comparisons)") titles = [title_row1, title_row2] elif report_type == "peer_feedback": titles1 = [ "", "Feedback Author", "", "", "Answer Author", "", "", "", "" ] titles2 = [ "Assignment", "Last Name", "First Name", "Student Number", "Last Name", "First Name", "Student Number", "Feedback Type", "Feedback", "Feedback Character Count" ] data = peer_feedback_report(course, assignments, group) titles = [titles1, titles2] else: abort( 400, title="Report Not Run", message= "Please try again with a report type from the list of report types provided." ) name = name_generator(course, report_type, group) tmp_name = os.path.join(current_app.config['REPORT_FOLDER'], name) with open(tmp_name, 'wb') as report: out = csv.writer(report) for t in titles: out.writerow(t) for s in data: out.writerow(s) on_export_report.send(self, event_name=on_export_report.name, user=current_user, course_id=course.id, data={ 'type': report_type, 'filename': name }) return {'file': 'report/' + name}
def get(self, course_uuid, assignment_uuid): """ Return a list of answers for a assignment based on search criteria. The list of the answers are paginated. If there is any answers from instructor or TA, their answers will be on top of the list (unless they are comparable). :param course_uuid: course uuid :param assignment_uuid: assignment uuid :return: list of answers """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) require( READ, assignment, title="Answers Unavailable", message= "Answers are visible only to those enrolled in the course. Please double-check your enrollment in this course." ) restrict_user = not allow(MANAGE, assignment) params = answer_list_parser.parse_args() # if assingment has no rank display limit set, restricted users can't force to retreive by rank if params[ 'orderBy'] == 'score' and restrict_user and not assignment.rank_display_limit: abort( 400, title="Answers Unavailable", message= "Sorry, you cannot cannot see answers by rank for this assignment." ) if restrict_user and not assignment.after_comparing: # only the answer from student himself/herself should be returned params['author'] = current_user.uuid # this query could be further optimized by reduction the selected columns query = Answer.query \ .options(joinedload('file')) \ .options(joinedload('user')) \ .options(joinedload('group')) \ .options(joinedload('score')) \ .options(undefer_group('counts')) \ .outerjoin(UserCourse, and_( Answer.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .add_columns( and_( UserCourse != None, UserCourse.course_role.__eq__(CourseRole.instructor), not Answer.comparable ).label("instructor_role"), and_( UserCourse != None, UserCourse.course_role.__eq__(CourseRole.teaching_assistant), not Answer.comparable ).label("ta_role") ) \ .filter(and_( Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False, or_( and_(UserCourse.course_role != CourseRole.dropped, Answer.user_id != None), Answer.group_id != None ) )) \ .order_by(desc('instructor_role'), desc('ta_role')) if params['author']: user = User.get_by_uuid_or_404(params['author']) group = user.get_course_group(course.id) if group: query = query.filter( or_(Answer.user_id == user.id, Answer.group_id == group.id)) else: query = query.filter(Answer.user_id == user.id) elif params['group']: group = Group.get_active_by_uuid_or_404(params['group']) query = query.filter( or_(UserCourse.group_id == group.id, Answer.group_id == group.id)) if params['ids']: query = query.filter(Answer.uuid.in_(params['ids'].split(','))) if params['top']: query = query.filter(Answer.top_answer == True) if params['orderBy'] == 'score': # use outer join to include comparable answers that are not yet compared (for non-restricted users) query = query.outerjoin(AnswerScore) \ .filter(Answer.comparable == True) \ .order_by(AnswerScore.score.desc(), Answer.submission_date.desc(), Answer.created.desc()) if restrict_user: # when orderd by rank, students won't see answers that are not compared (i.e. no score/rank) query = query.filter(AnswerScore.score.isnot(None)) # limit answers up to rank if rank_display_limit is set and current_user is restricted (student) if assignment.rank_display_limit and restrict_user: score_for_rank = AnswerScore.get_score_for_rank( assignment.id, assignment.rank_display_limit) # display answers with score >= score_for_rank if score_for_rank != None: # will get all answers with a score greater than or equal to the score for a given rank # the '- 0.00001' fixes floating point precision problems query = query.filter( AnswerScore.score >= score_for_rank - 0.00001) else: # when ordered by date, non-comparable answers should be on top of the list query = query.order_by(Answer.comparable, Answer.submission_date.desc(), Answer.created.desc()) page = query.paginate(params['page'], params['perPage'], error_out=False) # remove label entities from results page.items = [ answer for (answer, instructor_role, ta_role) in page.items ] on_answer_list_get.send(self, event_name=on_answer_list_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id}) # only include score/rank info if: # - requesters are non-restricted users (i.e. instructors / TAs); or, # - retrieving answers ordered by score/rank include_score = (not restrict_user) or \ (params['orderBy'] == 'score' and assignment.rank_display_limit) return { "objects": marshal( page.items, dataformat.get_answer(restrict_user, include_score=include_score)), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page }
def post(self, course_uuid, assignment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) if not assignment.answer_grace and not allow(MANAGE, assignment): abort(403, title="Answer Not Submitted", message="Sorry, the answer deadline has passed. No answers can be submitted after the deadline unless the instructor submits the answer for you.") require(CREATE, Answer(course_id=course.id), title="Answer Not Submitted", message="Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course.") restrict_user = not allow(MANAGE, assignment) answer = Answer(assignment_id=assignment.id) params = new_answer_parser.parse_args() answer.content = params.get("content") answer.draft = params.get("draft") file_uuid = params.get('file_id') attachment = None if file_uuid: attachment = File.get_by_uuid_or_404(file_uuid) answer.file_id = attachment.id else: answer.file_id = None # non-drafts must have content if not answer.draft and not answer.content and not file_uuid: abort(400, title="Answer Not Submitted", message="Please provide content in the text editor or upload a file and try submitting again.") user_uuid = params.get("user_id") group_uuid = params.get("group_id") # we allow instructor and TA to submit multiple answers for other users in the class if user_uuid and not allow(MANAGE, Answer(course_id=course.id)): abort(400, title="Answer Not Submitted", message="Only instructors and teaching assistants can submit an answer on behalf of another.") if group_uuid and not assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Group answers are not allowed for this assignment.") if group_uuid and not allow(MANAGE, Answer(course_id=course.id)): abort(400, title="Answer Not Submitted", message="Only instructors and teaching assistants can submit an answer on behalf of a group.") if group_uuid and user_uuid: abort(400, title="Answer Not Submitted", message="You cannot submit an answer for a user and a group at the same time.") user = User.get_by_uuid_or_404(user_uuid) if user_uuid else current_user group = Group.get_active_by_uuid_or_404(group_uuid) if group_uuid else None if restrict_user and assignment.enable_group_answers and not group: group = current_user.get_course_group(course.id) if group == None: abort(400, title="Answer Not Submitted", message="You are currently not in any group for this course. Please contact your instructor to be added to a group.") check_for_existing_answers = False if group and assignment.enable_group_answers: if group.course_id != course.id: abort(400, title="Answer Not Submitted", message="Group answers can be submitted to courses they belong in.") answer.user_id = None answer.group_id = group.id answer.comparable = True check_for_existing_answers = True else: answer.user_id = user.id answer.group_id = None course_role = User.get_user_course_role(answer.user_id, course.id) # only system admin can add answers for themselves to a class without being enrolled in it # required for managing comparison examples as system admin if (not course_role or course_role == CourseRole.dropped) and current_user.system_role != SystemRole.sys_admin: abort(400, title="Answer Not Submitted", message="Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course.") if course_role == CourseRole.student and assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Students can only submit group answers for this assignment.") # we allow instructor and TA to submit multiple answers for their own, # but not for student. Each student can only have one answer. if course_role and course_role == CourseRole.student: check_for_existing_answers = True answer.comparable = True else: # instructor / TA / Sys Admin can mark the answer as non-comparable, unless the answer is for a student answer.comparable = params.get("comparable") if check_for_existing_answers: # check for answers with user_id or group_id prev_answers = Answer.query \ .filter_by( assignment_id=assignment.id, user_id=answer.user_id, group_id=answer.group_id, active=True ) \ .all() # check if there is a previous answer submitted for the student non_draft_answers = [prev_answer for prev_answer in prev_answers if not prev_answer.draft] if len(non_draft_answers) > 0: abort(400, title="Answer Not Submitted", message="An answer has already been submitted for this assignment by you or on your behalf.") # check if there is a previous draft answer submitted for the student (soft-delete if present) draft_answers = [prev_answer for prev_answer in prev_answers if prev_answer.draft] for draft_answer in draft_answers: draft_answer.active = False # set submission date if answer is being submitted for the first time if not answer.draft and not answer.submission_date: answer.submission_date = datetime.datetime.utcnow() answer.update_attempt( params.get('attempt_uuid'), params.get('attempt_started', None), params.get('attempt_ended', None) ) db.session.add(answer) db.session.commit() on_answer_create.send( self, event_name=on_answer_create.name, user=current_user, course_id=course.id, answer=answer, data=marshal(answer, dataformat.get_answer(restrict_user))) if attachment: on_attach_file.send( self, event_name=on_attach_file.name, user=current_user, course_id=course.id, file=attachment, data={'answer_id': answer.id, 'file_id': attachment.id}) # update course & assignment grade for user if answer is fully submitted if not answer.draft: if answer.user: assignment.calculate_grade(answer.user) course.calculate_grade(answer.user) elif answer.group: assignment.calculate_group_grade(answer.group) course.calculate_group_grade(answer.group) return marshal(answer, dataformat.get_answer(restrict_user))