def delete(self, course_uuid, user_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) user = User.get_by_uuid_or_404(user_uuid) user_course = UserCourse.query \ .filter_by( user_id=user.id, course_id=course.id ) \ .first_or_404() require(EDIT, user_course) user_course.course_role = CourseRole.dropped result = { 'user_id': user.uuid, 'fullname': user.fullname, 'course_role': CourseRole.dropped.value } db.session.add(user_course) on_classlist_unenrol.send( self, event_name=on_classlist_unenrol.name, user=current_user, course_id=course.id, data={'user_id': user.id}) db.session.commit() return result
def delete(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) answer = Answer.get_active_by_uuid_or_404(answer_uuid) require(DELETE, answer, title="Answer Not Deleted", message="Sorry, your role in this course does not allow you to delete this answer.") 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 Deleted", message="Sorry, you cannot delete the default student demo answers.") answer.active = False db.session.commit() # update course & assignment grade for user if answer was 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) on_answer_delete.send( self, event_name=on_answer_delete.name, user=current_user, course_id=course.id, answer=answer, data={'assignment_id': assignment.id, 'answer_id': answer.id}) return {'id': answer.uuid}
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) require(CREATE, AssignmentComment(course_id=course.id)) new_assignment_comment = AssignmentComment(assignment_id=assignment.id) params = new_assignment_comment_parser.parse_args() new_assignment_comment.content = params.get("content") if not new_assignment_comment.content: return {"error": "The comment content is empty!"}, 400 new_assignment_comment.user_id = current_user.id db.session.add(new_assignment_comment) db.session.commit() on_assignment_comment_create.send( self, event_name=on_assignment_comment_create.name, user=current_user, course_id=course.id, assignment_comment=new_assignment_comment, data=marshal(new_assignment_comment, dataformat.get_assignment_comment(False))) return marshal(new_assignment_comment, dataformat.get_assignment_comment())
def get(self, course_uuid, group_name): course = Course.get_active_by_uuid_or_404(course_uuid) user_course = UserCourse(course_id=course.id) require(READ, user_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_name == group_name )) \ .all() if len(members) == 0: abort(404) on_course_group_members_get.send( current_app._get_current_object(), event_name=on_course_group_members_get.name, user=current_user, course_id=course.id, data={'group_name': group_name}) return {'students': [{'id': u.uuid, 'name': u.fullname} for u in members]}
def delete(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) answer = Answer.get_active_by_uuid_or_404(answer_uuid) require(DELETE, answer) answer.active = False if answer.file: answer.file.active = False db.session.commit() # update course & assignment grade for user if answer was fully submitted if not answer.draft: assignment.calculate_grade(answer.user) course.calculate_grade(answer.user) on_answer_delete.send( self, event_name=on_answer_delete.name, user=current_user, course_id=course.id, answer=answer, data={'assignment_id': assignment.id, 'answer_id': answer.id}) return {'id': answer.uuid}
def post(self, course_uuid, user_uuid, group_name): course = Course.get_active_by_uuid_or_404(course_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) user_course.group_name = group_name db.session.commit() on_course_group_user_create.send( current_app._get_current_object(), event_name=on_course_group_user_create.name, user=current_user, course_id=course.id, data={"user_id": user.id}, ) return {"group_name": group_name}
def delete(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(DELETE, assignment, title="Assignment Not Deleted", message="Sorry, your role in this course does not allow you to delete assignments.") if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if assignment.id in DemoDataFixture.DEFAULT_ASSIGNMENT_IDS: abort(400, title="Assignment Not Deleted", message="Sorry, you cannot remove the default demo assignments.") formatted_assignment = marshal(assignment, dataformat.get_assignment(False)) # delete file when assignment is deleted assignment.active = False assignment.clear_lti_links() db.session.commit() # update course grades course.calculate_grades() on_assignment_delete.send( self, event_name=on_assignment_delete.name, user=current_user, course_id=course.id, assignment=assignment, data=formatted_assignment) return {'id': assignment.uuid}
def delete(self, course_uuid, assignment_uuid, answer_uuid, answer_comment_uuid): """ Delete an answer comment """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer_comment = AnswerComment.get_active_by_uuid_or_404(answer_comment_uuid) require(DELETE, answer_comment, title="Feedback Not Deleted", message="Sorry, your role in this course does not allow you to delete feedback for this answer.") data = marshal(answer_comment, dataformat.get_answer_comment(False)) answer_comment.active = False db.session.commit() # update course & assignment grade for user if self-evaluation is completed if not answer_comment.draft and answer_comment.comment_type == AnswerCommentType.self_evaluation: assignment.calculate_grade(answer_comment.user) course.calculate_grade(answer_comment.user) on_answer_comment_delete.send( self, event_name=on_answer_comment_delete.name, user=current_user, course_id=course.id, answer_comment=answer_comment, data=data) return {'id': answer_comment.uuid}
def post(self, course_uuid, assignment_uuid, answer_uuid): """ Mark an answer as being a top answer :param course_uuid: :param assignment_uuid: :param answer_uuid: :return: marked answer """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) require(MANAGE, answer) params = top_answer_parser.parse_args() answer.top_answer = params.get('top_answer') db.session.add(answer) on_set_top_answer.send( self, event_name=on_set_top_answer.name, user=current_user, course_id=course.id, assignment_id=assignment.id, data={'answer_id': answer.id, 'top_answer': answer.top_answer}) db.session.commit() return marshal(answer, dataformat.get_answer(restrict_user=False))
def post(self, course_uuid): """ link current session's lti context with a course """ course = Course.get_active_by_uuid_or_404(course_uuid) require(EDIT, course, title="Course Not Linked", message="Sorry, you do not have permission to link this course since you are not enrolled as an instructor in the course.") if not sess.get('LTI'): abort(400, title="Course Not Linked", message="Sorry, your LTI session has expired. Please log in via LTI and try linking again.") if not sess.get('lti_context'): abort(400, title="Course Not Linked", message="Sorry, your LTI link settings have no course context. Please edit your LTI link settings and try linking again.") lti_context = LTIContext.query.get_or_404(sess.get('lti_context')) lti_context.compair_course_id = course.id db.session.commit() # automatically fetch membership if enabled for context if lti_context.membership_enabled: update_lti_course_membership.delay(course.id) on_lti_course_link_create.send( self, event_name=on_lti_course_link_create.name, user=current_user, data={ 'course_id': course.id, 'lti_context_id': lti_context.id }) return { 'success': True }
def delete(self, course_uuid, lti_context_uuid): """ unlink lti context from course """ course = Course.get_active_by_uuid_or_404(course_uuid) lti_context = LTIContext.get_by_uuid_or_404(lti_context_uuid) require(DELETE, lti_context, title="Course Not Unlinked", message="Sorry, your system role does not allow you to unlink LTI courses.") if lti_context.compair_course_id != course.id: abort(400, title="Course Not Unlinked", message="Sorry, The LTI context is already not linked to the course.") lti_context.compair_course_id = None db.session.commit() # automatically refresh membership if it was enabled for the removed context if lti_context.membership_enabled: update_lti_course_membership.delay(course.id) on_lti_course_unlink.send( self, event_name=on_lti_course_unlink.name, user=current_user, data={ 'course_id': course.id, 'lti_context_id': lti_context.id }) return { 'success': True }
def get(self, course_uuid): """ refresh the course membership if able """ course = Course.get_active_by_uuid_or_404(course_uuid) require(EDIT, course) if not course.lti_linked: return {"error": "Course not linked to lti context"}, 400 valid_membership_contexts = [ lti_context for lti_context in course.lti_contexts if lti_context.ext_ims_lis_memberships_url and lti_context.ext_ims_lis_memberships_id ] pending = 0 enabled = len(valid_membership_contexts) > 0 if enabled: lti_context_ids = [lti_context.id for lti_context in valid_membership_contexts] pending = ( LTIMembership.query.join(LTIUser) .filter(and_(LTIUser.compair_user_id == None, LTIMembership.lti_context_id.in_(lti_context_ids))) .count() ) status = {"enabled": enabled, "pending": pending} on_lti_course_membership_status_get.send( self, event_name=on_lti_course_membership_status_get.name, user=current_user, data={"course_id": course.id} ) return {"status": status}
def post(self, course_uuid, assignment_uuid, answer_uuid): """ Mark an answer as being a top answer :param course_uuid: :param assignment_uuid: :param answer_uuid: :return: marked answer """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) require(MANAGE, answer, title="Answer Not Added", message="Your role in this course does not allow you to add to the list of instructor-picked answers.") params = top_answer_parser.parse_args() answer.top_answer = params.get('top_answer') db.session.add(answer) on_set_top_answer.send( self, event_name=on_set_top_answer.name, user=current_user, course_id=course.id, assignment_id=assignment.id, data={'answer_id': answer.id, 'top_answer': answer.top_answer}) db.session.commit() return marshal(answer, dataformat.get_answer(restrict_user=False))
def post(self, course_uuid): """ link current session's lti context with a course """ course = Course.get_active_by_uuid_or_404(course_uuid) require(EDIT, course) if not sess.get("LTI"): return {"error": "Your LTI session has expired."}, 404 if not sess.get("lti_context"): return {"error": "Your LTI session has no context."}, 404 lti_context = LTIContext.query.get_or_404(sess.get("lti_context")) lti_context.compair_course_id = course.id db.session.commit() # automatically fetch membership if enabled for context if lti_context.ext_ims_lis_memberships_url and lti_context.ext_ims_lis_memberships_id: update_lti_course_membership.delay(course.id) on_lti_course_link.send( self, event_name=on_lti_course_link.name, user=current_user, data={"course_id": course.id, "lti_context_id": lti_context.id}, ) return {"success": True}
def post(self, course_uuid): """ refresh the course membership if able """ course = Course.get_active_by_uuid_or_404(course_uuid) require(EDIT, course) if not course.lti_linked: return {"error": "Course not linked to lti context"}, 400 try: LTIMembership.update_membership_for_course(course) except MembershipNoValidContextsException as err: return {"error": "LTI membership service is not supported for this course"}, 400 except MembershipNoResultsException as err: return {"error": "LTI membership service did not return any users"}, 400 except MembershipInvalidRequestException as err: return ( { "error": "LTI membership request was invalid. Please relaunch the ComPAIR course from the LTI consumer and try again" }, 400, ) on_lti_course_membership_update.send( self, event_name=on_lti_course_membership_update.name, user=current_user, data={"course_id": course.id} ) return {"imported": True}
def post(self, course_uuid, assignment_uuid, comparison_example_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) comparison_example = ComparisonExample.get_active_by_uuid_or_404(comparison_example_uuid) require(EDIT, comparison_example) params = existing_comparison_example_parser.parse_args() answer1_uuid = params.get("answer1_id") answer2_uuid = params.get("answer2_id") if answer1_uuid: answer1 = Answer.get_active_by_uuid_or_404(answer1_uuid) answer1.practice = True comparison_example.answer1 = answer1 else: return {"error": "Comparison examples must have 2 answers"}, 400 if answer2_uuid: answer2 = Answer.get_active_by_uuid_or_404(answer2_uuid) answer2.practice = True comparison_example.answer2 = answer2 else: return {"error": "Comparison examples must have 2 answers"}, 400 on_comparison_example_modified.send( self, event_name=on_comparison_example_modified.name, user=current_user, course_id=course.id, data=get_model_changes(comparison_example)) db.session.add(comparison_example) db.session.commit() return marshal(comparison_example, dataformat.get_comparison_example())
def post(self, course_uuid, user_uuid): """ Enrol or update a user enrolment in the course The payload for the request has to contain course_role. e.g. {"couse_role":"Student"} :param course_uuid: :param user_uuid: :return: """ course = Course.get_active_by_uuid_or_404(course_uuid) user = User.get_by_uuid_or_404(user_uuid) user_course = UserCourse.query \ .filter_by( user_id=user.id, course_id=course.id ) \ .first() if not user_course: user_course = UserCourse( user_id=user.id, course_id=course.id ) require(EDIT, user_course) params = new_course_user_parser.parse_args() role_name = params.get('course_role') course_roles = [ CourseRole.dropped.value, CourseRole.student.value, CourseRole.teaching_assistant.value, CourseRole.instructor.value ] if role_name not in course_roles: abort(404) course_role = CourseRole(role_name) if user_course.course_role != course_role: user_course.course_role = course_role db.session.add(user_course) db.session.commit() result = { 'user_id': user.uuid, 'fullname': user.fullname, 'course_role': course_role.value } on_classlist_enrol.send( self, event_name=on_classlist_enrol.name, user=current_user, course_id=course.id, data={'user_id': user.id}) return result
def post(self, course_uuid): """ refresh the course membership if able """ course = Course.get_active_by_uuid_or_404(course_uuid) require(EDIT, course, title="Membership Not Updated", message="Sorry, your role in this course does not allow you to update membership.") if not course.lti_linked: abort(400, title="Membership Not Updated", message="Sorry, your LTI link settings have no course context. Please edit your LTI link settings and try linking again.") try: LTIMembership.update_membership_for_course(course) except MembershipNoValidContextsException as err: abort(400, title="Membership Not Updated", message="The LTI link does not support the membership extension. Please edit your LTI link settings or contact your system administrator and try again.") except MembershipNoResultsException as err: abort(400, title="Membership Not Updated", message="The membership service did not return any users. Please check your LTI course and try again.") except MembershipInvalidRequestException as err: abort(400, title="Membership Not Updated", message="The membership request was invalid. Please relaunch the LTI link and try again.") on_lti_course_membership_update.send( self, event_name=on_lti_course_membership_update.name, user=current_user, data={ 'course_id': course.id }) return { 'imported': True }
def post(self, course_uuid, assignment_uuid, assignment_comment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) assignment_comment = AssignmentComment.get_active_by_uuid_or_404(assignment_comment_uuid) require(EDIT, assignment_comment) params = existing_assignment_comment_parser.parse_args() # make sure the comment id in the rul and the id matches if params['id'] != assignment_comment_uuid: return {"error": "Comment id does not match URL."}, 400 # modify comment according to new values, preserve original values if values not passed if not params.get("content"): return {"error": "The comment content is empty!"}, 400 assignment_comment.content = params.get("content") db.session.add(assignment_comment) on_assignment_comment_modified.send( self, event_name=on_assignment_comment_modified.name, user=current_user, course_id=course.id, assignment_comment=assignment_comment, data=get_model_changes(assignment_comment)) db.session.commit() return marshal(assignment_comment, dataformat.get_assignment_comment())
def delete(self, course_uuid, assignment_uuid, answer_uuid, answer_comment_uuid): """ Delete an answer comment """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer_comment = AnswerComment.get_active_by_uuid_or_404(answer_comment_uuid) require(DELETE, answer_comment) data = marshal(answer_comment, dataformat.get_answer_comment(False)) answer_comment.active = False db.session.commit() # update course & assignment grade for user if self-evaluation is completed if not answer_comment.draft and answer_comment.comment_type == AnswerCommentType.self_evaluation: assignment.calculate_grade(answer_comment.user) course.calculate_grade(answer_comment.user) on_answer_comment_delete.send( self, event_name=on_answer_comment_delete.name, user=current_user, course_id=course.id, answer_comment=answer_comment, data=data, ) return {"id": answer_comment.uuid}
def get(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require(READ, course) on_course_get.send( self, event_name=on_course_get.name, user=current_user, data={'id': course.id}) return marshal(course, dataformat.get_course())
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) user_course = UserCourse(course_id=course.id) require(EDIT, user_course, title="Class List Not Imported", message="Sorry, your role in this course does not allow you to import or otherwise change the class list.") if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if course.id == DemoDataFixture.DEFAULT_COURSE_ID: abort(400, title="Class List Not Imported", message="Sorry, you cannot import users for the default demo course.") params = import_classlist_parser.parse_args() import_type = params.get('import_type') if import_type not in [ThirdPartyType.cas.value, ThirdPartyType.saml.value, None]: abort(400, title="Class List Not Imported", message="Please select a way for students to log in and try importing again.") elif import_type == ThirdPartyType.cas.value and not current_app.config.get('CAS_LOGIN_ENABLED'): abort(400, title="Class List Not Imported", message="Please select another way for students to log in and try importing again. Students are not able to use CWL logins based on the current settings.") elif import_type == ThirdPartyType.saml.value and not current_app.config.get('SAML_LOGIN_ENABLED'): abort(400, title="Class List Not Imported", message="Please select another way for students to log in and try importing again. Students are not able to use CWL logins based on the current settings.") elif import_type is None and not current_app.config.get('APP_LOGIN_ENABLED'): abort(400, title="Class List Not Imported", message="Please select another way for students to log in and try importing again. Students are not able to use the ComPAIR logins based on the current settings.") uploaded_file = request.files['file'] results = {'success': 0, 'invalids': []} if not uploaded_file: abort(400, title="Class List Not Imported", message="No file was found to upload. Please try uploading again.") elif not allowed_file(uploaded_file.filename, current_app.config['UPLOAD_ALLOWED_EXTENSIONS']): abort(400, title="Class List Not Imported", message="Sorry, only CSV files can be imported. Please try again with a CSV file.") unique = str(uuid.uuid4()) filename = unique + secure_filename(uploaded_file.filename) tmp_name = os.path.join(current_app.config['UPLOAD_FOLDER'], filename) uploaded_file.save(tmp_name) current_app.logger.debug("Importing for course " + str(course.id) + " with " + filename) with open(tmp_name, 'rb') as csvfile: spamreader = csv.reader(csvfile) users = [] for row in spamreader: if row: users.append(row) if len(users) > 0: results = import_users(import_type, course, users) on_classlist_upload.send( self, event_name=on_classlist_upload.name, user=current_user, course_id=course.id) os.remove(tmp_name) current_app.logger.debug("Class Import for course " + str(course.id) + " is successful. Removed file.") return results
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require(EDIT, UserCourse(course_id=course.id)) params = update_users_course_role_parser.parse_args() role_name = params.get('course_role') course_roles = [ CourseRole.dropped.value, CourseRole.student.value, CourseRole.teaching_assistant.value, CourseRole.instructor.value ] if role_name not in course_roles: abort(404) course_role = CourseRole(role_name) if len(params.get('ids')) == 0: return {"error": "Please select at least one user below"}, 400 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): return {"error": "One or more users are not enrolled in the course"}, 400 if len(user_courses) == 1 and user_courses[0].user_id == current_user.id: if course_role == CourseRole.dropped: return {"error": "You cannot drop yourself from the course. Please select other users"}, 400 else: return {"error": "You cannot change your own course role. Please select other users"}, 400 for user_course in user_courses: # skip current user if user_course.user_id == current_user.id: continue # update user's role' user_course.course_role = course_role db.session.commit() on_classlist_update_users_course_roles.send( current_app._get_current_object(), event_name=on_classlist_update_users_course_roles.name, user=current_user, course_id=course.id, data={'user_uuids': params.get('ids'), 'course_role': role_name}) return {'course_role': role_name}
def post(self, course_uuid, assignment_uuid, answer_uuid): """ Create comment for an answer """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) require(CREATE, AnswerComment(course_id=course.id)) answer_comment = AnswerComment(answer_id=answer.id) params = new_answer_comment_parser.parse_args() answer_comment.draft = params.get("draft") answer_comment.content = params.get("content") # require content not empty if not a draft if not answer_comment.content and not answer_comment.draft: return {"error": "The comment content is empty!"}, 400 if params.get("user_id") and current_user.system_role == SystemRole.sys_admin: user = User.get_by_uuid_or_404(params.get("user_id")) answer_comment.user_id = user.id else: answer_comment.user_id = current_user.id comment_types = [ AnswerCommentType.public.value, AnswerCommentType.private.value, AnswerCommentType.evaluation.value, AnswerCommentType.self_evaluation.value, ] comment_type = params.get("comment_type") if comment_type not in comment_types: abort(400) answer_comment.comment_type = AnswerCommentType(comment_type) db.session.add(answer_comment) db.session.commit() # update course & assignment grade for user if self-evaluation is completed if not answer_comment.draft and answer_comment.comment_type == AnswerCommentType.self_evaluation: assignment.calculate_grade(answer_comment.user) course.calculate_grade(answer_comment.user) on_answer_comment_create.send( self, event_name=on_answer_comment_create.name, user=current_user, course_id=course.id, answer_comment=answer_comment, data=marshal(answer_comment, dataformat.get_answer_comment(False)), ) return marshal(answer_comment, dataformat.get_answer_comment())
def get(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require(READ, course, title="Course Unavailable", message="Courses can be seen only by those enrolled in them. Please double-check your enrollment in this course.") on_course_get.send( self, event_name=on_course_get.name, user=current_user, data={'id': course.id}) return marshal(course, dataformat.get_course())
def get(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require(READ, UserCourse(course_id=course.id)) restrict_user = not allow(READ, USER_IDENTITY) # expire current_user from the session. When loading classlist from database, if the # user is already in the session, e.g. instructor for the course, the User.user_courses # is not loaded from the query below, but from session. In this case, if user has more # than one course, User.user_courses will return multiple row. Thus violating the # course_role constrain. db.session.expire(current_user) users = User.query \ .join(UserCourse, UserCourse.user_id == User.id) \ .add_columns(UserCourse.course_role, UserCourse.group_name) \ .filter(and_( UserCourse.course_id == course.id, UserCourse.course_role != CourseRole.dropped )) \ .order_by(User.firstname) \ .all() if not restrict_user: user_ids = [_user.id for (_user, _course_role, _group_name) in users] third_party_auths = ThirdPartyUser.query \ .filter(and_( ThirdPartyUser.user_id.in_(user_ids), ThirdPartyUser.third_party_type == ThirdPartyType.cas )) \ .all() class_list = [] for (_user, _course_role, _group_name) in users: _user.course_role = _course_role _user.group_name = _group_name if not restrict_user: third_party_auth = next( (third_party_auth for third_party_auth in third_party_auths if third_party_auth.user_id == _user.id), None ) _user.cas_username = third_party_auth.unique_identifier if third_party_auth else None class_list.append(_user) on_classlist_get.send( self, event_name=on_classlist_get.name, user=current_user, course_id=course.id) return {'objects': marshal(class_list, dataformat.get_users_in_course(restrict_user=restrict_user))}
def get(self, course_uuid, assignment_uuid): """ Get answers submitted to the assignment submitted by current user :param course_uuid: :param assignment_uuid: :return: answers """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) require(READ, Answer(user_id=current_user.id), title="Answers Unavailable", message="Sorry, your role in this course does not allow you to view answers for this assignment.") restrict_user = not allow(MANAGE, assignment) params = user_answer_list_parser.parse_args() query = Answer.query \ .options(joinedload('comments')) \ .options(joinedload('file')) \ .options(joinedload('user')) \ .options(joinedload('group')) \ .options(joinedload('score')) \ .filter_by( active=True, assignment_id=assignment.id, course_id=course.id, draft=params.get('draft') ) # get group and individual answers for user if applicable group = current_user.get_course_group(course.id) if group: query = query.filter(or_( Answer.user_id == current_user.id, Answer.group_id == group.id )) # get just individual answers for user else: query = query.filter(Answer.user_id == current_user.id) answers = query.all() on_user_answer_get.send( self, event_name=on_user_answer_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id}) return {"objects": marshal(answers, dataformat.get_answer(restrict_user))}
def get(self, course_uuid, assignment_uuid, assignment_comment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) assignment_comment = AssignmentComment.get_active_by_uuid_or_404(assignment_comment_uuid) require(READ, assignment_comment) on_assignment_comment_get.send( self, event_name=on_assignment_comment_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id, 'assignment_comment_id': assignment_comment.id}) return marshal(assignment_comment, dataformat.get_assignment_comment())
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require(EDIT, course, title="Course Not Saved", message="Sorry, your role in this course does not allow you to save changes to it.") if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if course.id == DemoDataFixture.DEFAULT_COURSE_ID: abort(400, title="Course Not Updated", message="Sorry, you cannot edit the default demo course.") params = existing_course_parser.parse_args() # make sure the course id in the url and the course id in the params match if params['id'] != course_uuid: abort(400, title="Course Not Saved", message="The course's ID does not match the URL, which is required in order to save the course.") # modify course according to new values, preserve original values if values not passed course.name = params.get("name", course.name) course.year = params.get("year", course.year) course.term = params.get("term", course.term) course.sandbox = params.get("sandbox", course.sandbox) course.start_date = params.get("start_date") if course.start_date is not None: course.start_date = datetime.datetime.strptime( course.start_date, '%Y-%m-%dT%H:%M:%S.%fZ') course.end_date = params.get("end_date", None) if course.end_date is not None: course.end_date = datetime.datetime.strptime( course.end_date, '%Y-%m-%dT%H:%M:%S.%fZ') if course.start_date and course.end_date and course.start_date > course.end_date: abort(400, title="Course Not Saved", message="Course end time must be after course start time.") model_changes = get_model_changes(course) db.session.commit() on_course_modified.send( self, event_name=on_course_modified.name, user=current_user, course=course, data=model_changes) return marshal(course, dataformat.get_course())
def delete(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require(DELETE, course) course.active = False db.session.commit() on_course_delete.send( self, event_name=on_course_delete.name, user=current_user, course=course, data={'id': course.id}) return {'id': course.uuid}
def delete(self, course_uuid, user_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) user = User.get_by_uuid_or_404(user_uuid) user_course = UserCourse.query \ .filter_by( course_id=course.id, user_id=user.id ) \ .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: abort( 400, title="Group Not Saved", message= "The course groups are locked. You may not remove users from the group they are already assigned to." ) user_course.group_id = None db.session.commit() on_group_user_delete.send(current_app._get_current_object(), event_name=on_group_user_delete.name, user=current_user, course_id=course.id, data={'user_id': user.id}) return {'success': True}
def delete(self, course_uuid, lti_context_uuid): """ unlink lti context from course """ course = Course.get_active_by_uuid_or_404(course_uuid) lti_context = LTIContext.get_by_uuid_or_404(lti_context_uuid) require( DELETE, lti_context, title="Course Not Unlinked", message= "Sorry, your system role does not allow you to unlink LTI courses." ) if lti_context.compair_course_id != course.id: abort( 400, title="Course Not Unlinked", message= "Sorry, The LTI context is already not linked to the course.") lti_context.compair_course_id = None db.session.commit() # automatically refresh membership if it was enabled for the removed context if lti_context.membership_enabled: update_lti_course_membership.delay(course.id) on_lti_course_unlink.send(self, event_name=on_lti_course_unlink.name, user=current_user, data={ 'course_id': course.id, 'lti_context_id': lti_context.id }) return {'success': True}
def post(self, course_uuid, assignment_uuid, answer_uuid): """ Mark an answer as being a top answer :param course_uuid: :param assignment_uuid: :param answer_uuid: :return: marked answer """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) require( MANAGE, answer, title="Answer Not Added", message= "Your role in this course does not allow you to add to the list of instructor-picked answers." ) params = top_answer_parser.parse_args() answer.top_answer = params.get('top_answer') db.session.add(answer) on_set_top_answer.send(self, event_name=on_set_top_answer.name, user=current_user, course_id=course.id, assignment_id=assignment.id, data={ 'answer_id': answer.id, 'top_answer': answer.top_answer }) db.session.commit() return marshal(answer, dataformat.get_answer(restrict_user=False))
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 get(self, course_uuid, assignment_uuid, answer_uuid, answer_comment_uuid): """ Get an answer comment """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) answer_comment = AnswerComment.get_active_by_uuid_or_404(answer_comment_uuid) require(READ, answer_comment, title="Feedback Unavailable", message="Sorry, your role in this course does not allow you to view this feedback.") restrict_user = not allow(MANAGE, assignment) restrict_user = not allow(MANAGE, assignment) on_answer_comment_get.send( self, event_name=on_answer_comment_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id, 'answer_id': answer.id, 'answer_comment_id': answer_comment.id}) return marshal(answer_comment, dataformat.get_answer_comment(restrict_user))
def get(self, course_uuid, assignment_uuid, assignment_comment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) assignment_comment = AssignmentComment.get_active_by_uuid_or_404( assignment_comment_uuid) require( READ, assignment_comment, title="Help Comment Unavailable", message= "Sorry, your role in this course does not allow you to view help comments." ) on_assignment_comment_get.send( self, event_name=on_assignment_comment_get.name, user=current_user, course_id=course.id, data={ 'assignment_id': assignment.id, 'assignment_comment_id': assignment_comment.id }) return marshal(assignment_comment, dataformat.get_assignment_comment())
def get(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) answer = Answer.get_active_by_uuid_or_404( answer_uuid, joinedloads=['file', 'user', 'group', 'score'] ) require(READ, answer, title="Answer Unavailable", message="Sorry, your role in this course does not allow you to view this answer.") restrict_user = not allow(MANAGE, assignment) on_answer_get.send( self, event_name=on_answer_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id, 'answer_id': answer.id}) # don't include score/rank unless the user is non-restricted include_score = not restrict_user return marshal(answer, dataformat.get_answer(restrict_user, include_score=include_score))
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, assignment, title="Assignment Status Unavailable", message="Assignment status can be seen only by those enrolled in the course. Please double-check your enrollment in this course.") answer_count = Answer.query \ .filter_by( user_id=current_user.id, assignment_id=assignment.id, active=True, practice=False, draft=False ) \ .count() feedback_count = AnswerComment.query \ .join("answer") \ .filter(and_( AnswerComment.active == True, AnswerComment.draft == False, Answer.user_id == current_user.id, Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False )) \ .count() drafts = Answer.query \ .options(load_only('id')) \ .filter_by( user_id=current_user.id, assignment_id=assignment.id, active=True, practice=False, draft=True, saved=True ) \ .all() comparison_count = assignment.completed_comparison_count_for_user(current_user.id) other_student_answers = assignment.student_answer_count - answer_count comparison_available = comparison_count < other_student_answers * (other_student_answers - 1) / 2 status = { 'answers': { 'answered': answer_count > 0, 'feedback': feedback_count, 'count': answer_count, 'has_draft': len(drafts) > 0, 'draft_ids': [draft.uuid for draft in drafts] }, 'comparisons': { 'available': comparison_available, 'count': comparison_count, 'left': max(0, assignment.total_comparisons_required - comparison_count) } } if assignment.enable_self_evaluation: self_evaluations = AnswerComment.query \ .join("answer") \ .filter(and_( AnswerComment.user_id == current_user.id, AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.draft == False, Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False )) \ .count() status['comparisons']['self_evaluation_completed'] = self_evaluations > 0 on_assignment_get_status.send( self, event_name=on_assignment_get_status.name, user=current_user, course_id=course.id, data=status) return {"status": status}
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) # check permission first before reading parser arguments new_assignment = Assignment(course_id=course.id) require(CREATE, new_assignment, title="Assignment Not Saved", message="Sorry, your role in this course does not allow you to save assignments.") params = new_assignment_parser.parse_args() new_assignment.user_id = current_user.id new_assignment.name = params.get("name") new_assignment.description = params.get("description") new_assignment.peer_feedback_prompt = params.get("peer_feedback_prompt") new_assignment.answer_start = dateutil.parser.parse(params.get('answer_start')) new_assignment.answer_end = dateutil.parser.parse(params.get('answer_end')) new_assignment.educators_can_compare = params.get("educators_can_compare") new_assignment.rank_display_limit = params.get("rank_display_limit", None) if new_assignment.rank_display_limit != None and new_assignment.rank_display_limit <= 0: new_assignment.rank_display_limit = None # make sure that file attachment exists file_uuid = params.get('file_id') if file_uuid: attachment = File.get_by_uuid_or_404(file_uuid) new_assignment.file_id = attachment.id else: new_assignment.file_id = None new_assignment.compare_start = params.get('compare_start', None) if new_assignment.compare_start is not None: new_assignment.compare_start = dateutil.parser.parse(params.get('compare_start', None)) new_assignment.compare_end = params.get('compare_end', None) if new_assignment.compare_end is not None: new_assignment.compare_end = dateutil.parser.parse(params.get('compare_end', None)) # validate answer + comparison period start & end times valid, error_message = Assignment.validate_periods(course.start_date, course.end_date, new_assignment.answer_start, new_assignment.answer_end, new_assignment.compare_start, new_assignment.compare_end) if not valid: abort(400, title="Assignment Not Saved", message=error_message) new_assignment.students_can_reply = params.get('students_can_reply', False) new_assignment.number_of_comparisons = params.get('number_of_comparisons') new_assignment.enable_self_evaluation = params.get('enable_self_evaluation') new_assignment.answer_grade_weight = params.get('answer_grade_weight') new_assignment.comparison_grade_weight = params.get('comparison_grade_weight') new_assignment.self_evaluation_grade_weight = params.get('self_evaluation_grade_weight') pairing_algorithm = params.get("pairing_algorithm", PairingAlgorithm.random) check_valid_pairing_algorithm(pairing_algorithm) new_assignment.pairing_algorithm = PairingAlgorithm(pairing_algorithm) criterion_uuids = [c.get('id') for c in params.criteria] criterion_data = {c.get('id'): c.get('weight', 1) for c in params.criteria} if len(criterion_data) == 0: msg = "Please add at least one criterion to the assignment and save again." abort(400, title="Assignment Not Saved", message=msg) criteria = Criterion.query \ .filter(Criterion.uuid.in_(criterion_uuids)) \ .all() if len(criterion_uuids) != len(criteria): abort(400, title="Assignment Not Saved", message="Please double-check the criteria you selected and save agaiin.") # add criteria to assignment in order for criterion_uuid in criterion_uuids: criterion = next(criterion for criterion in criteria if criterion.uuid == criterion_uuid) new_assignment.assignment_criteria.append(AssignmentCriterion( criterion=criterion, weight=criterion_data.get(criterion.uuid) )) db.session.add(new_assignment) db.session.commit() # need to reorder after insert new_assignment.assignment_criteria.reorder() # update course grades course.calculate_grades() on_assignment_create.send( self, event_name=on_assignment_create.name, user=current_user, course_id=course.id, assignment=new_assignment, data=marshal(new_assignment, dataformat.get_assignment(False))) return marshal(new_assignment, dataformat.get_assignment())
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) assignment_criteria = assignment.assignment_criteria require(EDIT, assignment, title="Assignment Not Saved", message="Sorry, your role in this course does not allow you to save assignments.") if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if assignment.id in DemoDataFixture.DEFAULT_ASSIGNMENT_IDS: abort(400, title="Assignment Not Saved", message="Sorry, you cannot edit the default demo assignments.") params = existing_assignment_parser.parse_args() # make sure the assignment id in the url and the id matches if params['id'] != assignment_uuid: abort(400, title="Assignment Not Saved", message="The assignment's ID does not match the URL, which is required in order to update the assignment.") # make sure that file attachment exists file_uuid = params.get('file_id') if file_uuid: attachment = File.get_by_uuid_or_404(file_uuid) assignment.file_id = attachment.id else: assignment.file_id = None # modify assignment according to new values, preserve original values if values not passed assignment.name = params.get("name", assignment.name) assignment.description = params.get("description", assignment.description) assignment.peer_feedback_prompt = params.get("peer_feedback_prompt", assignment.peer_feedback_prompt) assignment.answer_start = datetime.datetime.strptime( params.get('answer_start', assignment.answer_start), '%Y-%m-%dT%H:%M:%S.%fZ') assignment.answer_end = datetime.datetime.strptime( params.get('answer_end', assignment.answer_end), '%Y-%m-%dT%H:%M:%S.%fZ') # if nothing in request, assume user don't want comparison date assignment.compare_start = params.get('compare_start', None) if assignment.compare_start is not None: assignment.compare_start = datetime.datetime.strptime( assignment.compare_start, '%Y-%m-%dT%H:%M:%S.%fZ') assignment.compare_end = params.get('compare_end', None) if assignment.compare_end is not None: assignment.compare_end = datetime.datetime.strptime( params.get('compare_end', assignment.compare_end), '%Y-%m-%dT%H:%M:%S.%fZ') # validate answer + comparison period start & end times valid, error_message = Assignment.validate_periods(course.start_date, course.end_date, assignment.answer_start, assignment.answer_end, assignment.compare_start, assignment.compare_end) if not valid: abort(400, title="Assignment Not Saved", message=error_message) assignment.students_can_reply = params.get('students_can_reply', False) assignment.number_of_comparisons = params.get( 'number_of_comparisons', assignment.number_of_comparisons) assignment.enable_self_evaluation = params.get( 'enable_self_evaluation', assignment.enable_self_evaluation) assignment.answer_grade_weight = params.get( 'answer_grade_weight', assignment.answer_grade_weight) assignment.comparison_grade_weight = params.get( 'comparison_grade_weight', assignment.comparison_grade_weight) assignment.self_evaluation_grade_weight = params.get( 'self_evaluation_grade_weight', assignment.self_evaluation_grade_weight) pairing_algorithm = params.get("pairing_algorithm") check_valid_pairing_algorithm(pairing_algorithm) if not assignment.compared: assignment.pairing_algorithm = PairingAlgorithm(pairing_algorithm) elif assignment.pairing_algorithm != PairingAlgorithm(pairing_algorithm): msg = 'The answer pair selection algorithm cannot be changed for this assignment ' + \ 'because it has already been used in one or more comparisons.' abort(403, title="Assignment Not Saved", message=msg) assignment.educators_can_compare = params.get("educators_can_compare") assignment.rank_display_limit = params.get("rank_display_limit", None) if assignment.rank_display_limit != None and assignment.rank_display_limit <= 0: assignment.rank_display_limit = None criterion_uuids = [c.get('id') for c in params.criteria] criterion_data = {c.get('id'): c.get('weight', 1) for c in params.criteria} if assignment.compared: active_uuids = [c.uuid for c in assignment.criteria] active_data = {c.uuid: c.weight for c in assignment.criteria} if set(criterion_uuids) != set(active_uuids): msg = 'The criteria cannot be changed for this assignment ' + \ 'because they have already been used in one or more comparisons.' abort(403, title="Assignment Not Saved", message=msg) for criterion in assignment.criteria: if criterion_data.get(criterion.uuid) != criterion.weight: msg = 'The criteria weights cannot be changed for this assignment ' + \ 'because they have already been used in one or more comparisons.' abort(403, title="Assignment Not Saved", message=msg) else: # assignment not compared yet, can change criteria if len(criterion_uuids) == 0: msg = "Please add at least one criterion to the assignment and save again." abort(403, title="Assignment Not Saved", message=msg) existing_uuids = [c.criterion_uuid for c in assignment_criteria] # disable old ones for c in assignment_criteria: c.active = c.criterion_uuid in criterion_uuids if c.active: c.weight = criterion_data.get(c.criterion_uuid) # add the new ones new_uuids = [] for criterion_uuid in criterion_uuids: if criterion_uuid not in existing_uuids: new_uuids.append(criterion_uuid) if len(new_uuids) > 0: new_criteria = Criterion.query.filter(Criterion.uuid.in_(new_uuids)).all() for criterion in new_criteria: assignment_criteria.append(AssignmentCriterion( criterion=criterion, weight=criterion_data.get(criterion.uuid) )) # ensure criteria are in order for index, criterion_uuid in enumerate(criterion_uuids): assignment_criterion = next(assignment_criterion \ for assignment_criterion in assignment_criteria \ if assignment_criterion.criterion_uuid == criterion_uuid) assignment_criteria.remove(assignment_criterion) assignment_criteria.insert(index, assignment_criterion) model_changes = get_model_changes(assignment) on_assignment_modified.send( self, event_name=on_assignment_modified.name, user=current_user, course_id=course.id, assignment=assignment, data=model_changes) db.session.commit() # need to reorder after update assignment_criteria.reorder() # update assignment and course grades if needed if model_changes and (model_changes.get('answer_grade_weight') or model_changes.get('comparison_grade_weight') or model_changes.get('self_evaluation_grade_weight') or model_changes.get('enable_self_evaluation')): assignment.calculate_grades() course.calculate_grades() return marshal(assignment, dataformat.get_assignment())
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_name = params.get('group_name', None) report_type = params.get('type') 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 group_name: # ensure that group_name is valid group_exists = UserCourse.query \ .filter( UserCourse.group_name == group_name, UserCourse.course_id == course.id, UserCourse.course_role != CourseRole.dropped ) \ .first() if group_exists == None: abort(400, title="Report Not Run", message="Please try again with a group from the list of groups provided.") if report_type == "participation_stat": data = participation_stat_report(course, assignments, group_name, assignment_uuid is None) title = [ 'Assignment', 'User UUID', 'Last Name', 'First Name', 'Answer Submitted', 'Answer ID', 'Evaluations Submitted', 'Evaluations Required', 'Evaluation Requirements Met', 'Replies Submitted'] titles = [title] elif report_type == "participation": user_titles = ['Last Name', 'First Name', 'Student No'] data = participation_report(course, assignments, group_name) title_row1 = [""] * len(user_titles) title_row2 = user_titles for assignment in assignments: assignment_criteria = AssignmentCriterion.query \ .filter_by( assignment_id=assignment.id, active=True ) \ .order_by(AssignmentCriterion.position) \ .all() title_row1 += [assignment.name] + [""] * len(assignment_criteria) title_row2.append('Percentage score for answer overall') for assignment_criterion in assignment_criteria: title_row2.append('Percentage score for "' + assignment_criterion.criterion.name + '"') title_row2.append("Evaluations Submitted (" + str(assignment.total_comparisons_required) + ' required)') if assignment.enable_self_evaluation: title_row1 += [""] title_row2.append("Self Evaluation Submitted") titles = [title_row1, title_row2] elif report_type == "peer_feedback": titles1 = [ "", "Feedback Author", "", "", "Answer Author", "", "", "", "" ] titles2 = [ "Assignment", "Last Name", "First Name", "Student No", "Last Name", "First Name", "Student No", "Feedback Type", "Feedback" ] data = peer_feedback_report(course, assignments, group_name) 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_name) 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 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): """ Duplicate a course """ course = Course.get_active_by_uuid_or_404(course_uuid) require( EDIT, course, title="Course Not Duplicated", message= "Sorry, your role in this course does not allow you to duplicate it." ) params = duplicate_course_parser.parse_args() start_date = datetime.datetime.strptime( params.get("start_date"), '%Y-%m-%dT%H:%M:%S.%fZ') if params.get("start_date") else None end_date = datetime.datetime.strptime( params.get("end_date"), '%Y-%m-%dT%H:%M:%S.%fZ') if params.get("end_date") else None if start_date is None: abort(400, title="Course Not Saved", message="Course start time is required.") elif start_date and end_date and start_date > end_date: abort(400, title="Course Not Saved", message="Course end time must be after course start time.") assignments = [ assignment for assignment in course.assignments if assignment.active ] assignments_copy_data = params.get("assignments") if len(assignments) != len(assignments_copy_data): abort( 400, title="Course Not Saved", message= "The course is missing assignments. Please reload the page and try duplicating again." ) for assignment_copy_data in assignments_copy_data: if assignment_copy_data.get('answer_start'): assignment_copy_data[ 'answer_start'] = datetime.datetime.strptime( assignment_copy_data.get('answer_start'), '%Y-%m-%dT%H:%M:%S.%fZ') if assignment_copy_data.get('answer_end'): assignment_copy_data[ 'answer_end'] = datetime.datetime.strptime( assignment_copy_data.get('answer_end'), '%Y-%m-%dT%H:%M:%S.%fZ') if assignment_copy_data.get('compare_start'): assignment_copy_data[ 'compare_start'] = datetime.datetime.strptime( assignment_copy_data.get('compare_start'), '%Y-%m-%dT%H:%M:%S.%fZ') if assignment_copy_data.get('compare_end'): assignment_copy_data[ 'compare_end'] = datetime.datetime.strptime( assignment_copy_data.get('compare_end'), '%Y-%m-%dT%H:%M:%S.%fZ') if 'enable_self_evaluation' not in assignment_copy_data: assignment_copy_data['enable_self_evaluation'] = False if assignment_copy_data.get('self_eval_start'): assignment_copy_data[ 'self_eval_start'] = datetime.datetime.strptime( assignment_copy_data.get('self_eval_start'), '%Y-%m-%dT%H:%M:%S.%fZ') if assignment_copy_data.get('self_eval_end'): assignment_copy_data[ 'self_eval_end'] = datetime.datetime.strptime( assignment_copy_data.get('self_eval_end'), '%Y-%m-%dT%H:%M:%S.%fZ') valid, error_message = Assignment.validate_periods( start_date, end_date, assignment_copy_data.get('answer_start'), assignment_copy_data.get('answer_end'), assignment_copy_data.get('compare_start'), assignment_copy_data.get('compare_end'), assignment_copy_data.get('self_eval_start'), assignment_copy_data.get('self_eval_end')) if not valid: error_message = error_message.replace( ".", "") + " for assignment " + text_type( assignment_copy_data.get('name', '')) + "." abort(400, title="Course Not Saved", message=error_message) # duplicate course duplicate_course = Course(name=params.get("name"), year=params.get("year"), term=params.get("term"), sandbox=params.get("sandbox"), start_date=start_date, end_date=end_date) db.session.add(duplicate_course) # also need to enrol the user as an instructor new_user_course = UserCourse(course=duplicate_course, user_id=current_user.id, course_role=CourseRole.instructor) db.session.add(new_user_course) # duplicate assignments for assignment in assignments: # this should never be null due assignment_copy_data = next( (assignment_copy_data for assignment_copy_data in assignments_copy_data if assignment_copy_data.get('id') == assignment.uuid), None) if not assignment_copy_data: abort(400, title="Course Not Saved", message="Missing information for assignment " + assignment.name + ". Please try duplicating again.") duplicate_assignment = Assignment( course=duplicate_course, user_id=current_user.id, file=assignment.file, name=assignment.name, description=assignment.description, answer_start=assignment_copy_data.get('answer_start'), answer_end=assignment_copy_data.get('answer_end'), compare_start=assignment_copy_data.get('compare_start'), compare_end=assignment_copy_data.get('compare_end'), self_eval_start=assignment_copy_data.get('self_eval_start') if assignment_copy_data.get('enable_self_evaluation', False) else None, self_eval_end=assignment_copy_data.get('self_eval_end') if assignment_copy_data.get('enable_self_evaluation', False) else None, self_eval_instructions=assignment.self_eval_instructions if assignment_copy_data.get('enable_self_evaluation', False) else None, answer_grade_weight=assignment.answer_grade_weight, comparison_grade_weight=assignment.comparison_grade_weight, self_evaluation_grade_weight=assignment. self_evaluation_grade_weight, number_of_comparisons=assignment.number_of_comparisons, students_can_reply=assignment.students_can_reply, enable_self_evaluation=assignment_copy_data.get( 'enable_self_evaluation', False), enable_group_answers=assignment.enable_group_answers, pairing_algorithm=assignment.pairing_algorithm, scoring_algorithm=assignment.scoring_algorithm, peer_feedback_prompt=assignment.peer_feedback_prompt, educators_can_compare=assignment.educators_can_compare, rank_display_limit=assignment.rank_display_limit, ) db.session.add(duplicate_assignment) # duplicate assignment criteria for assignment_criterion in assignment.assignment_criteria: if not assignment_criterion.active: continue duplicate_assignment_criterion = AssignmentCriterion( assignment=duplicate_assignment, criterion_id=assignment_criterion.criterion_id) db.session.add(duplicate_assignment_criterion) # duplicate assignment comparisons examples for comparison_example in assignment.comparison_examples: answer1 = comparison_example.answer1 answer2 = comparison_example.answer2 # duplicate assignment comparisons example answers duplicate_answer1 = Answer(assignment=duplicate_assignment, user_id=current_user.id, file=answer1.file, content=answer1.content, practice=answer1.practice, active=answer1.active, draft=answer1.draft) db.session.add(duplicate_answer1) # duplicate assignment comparisons example answers duplicate_answer2 = Answer(assignment=duplicate_assignment, user_id=current_user.id, file=answer2.file, content=answer2.content, practice=answer2.practice, active=answer2.active, draft=answer2.draft) db.session.add(duplicate_answer2) duplicate_comparison_example = ComparisonExample( assignment=duplicate_assignment, answer1=duplicate_answer1, answer2=duplicate_answer2) db.session.add(duplicate_comparison_example) db.session.commit() on_course_duplicate.send(self, event_name=on_course_duplicate.name, user=current_user, course=duplicate_course, data=marshal(course, dataformat.get_course())) return marshal(duplicate_course, dataformat.get_course())
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) user_course = UserCourse(course_id=course.id) require( EDIT, user_course, title="Class List Not Imported", message= "Sorry, your role in this course does not allow you to import or otherwise change the class list." ) if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if course.id == DemoDataFixture.DEFAULT_COURSE_ID: abort( 400, title="Class List Not Imported", message= "Sorry, you cannot import users for the default demo course." ) params = import_classlist_parser.parse_args() import_type = params.get('import_type') if import_type not in [ ThirdPartyType.cas.value, ThirdPartyType.saml.value, None ]: abort( 400, title="Class List Not Imported", message= "Please select a way for students to log in and try importing again." ) elif import_type == ThirdPartyType.cas.value and not current_app.config.get( 'CAS_LOGIN_ENABLED'): abort( 400, title="Class List Not Imported", message= "Please select another way for students to log in and try importing again. Students are not able to use CWL logins based on the current settings." ) elif import_type == ThirdPartyType.saml.value and not current_app.config.get( 'SAML_LOGIN_ENABLED'): abort( 400, title="Class List Not Imported", message= "Please select another way for students to log in and try importing again. Students are not able to use CWL logins based on the current settings." ) elif import_type is None and not current_app.config.get( 'APP_LOGIN_ENABLED'): abort( 400, title="Class List Not Imported", message= "Please select another way for students to log in and try importing again. Students are not able to use the ComPAIR logins based on the current settings." ) uploaded_file = request.files['file'] results = {'success': 0, 'invalids': []} if not uploaded_file: abort(400, title="Class List Not Imported", message= "No file was found to upload. Please try uploading again.") elif not allowed_file(uploaded_file.filename, current_app.config['UPLOAD_ALLOWED_EXTENSIONS']): abort( 400, title="Class List Not Imported", message= "Sorry, only CSV files can be imported. Please try again with a CSV file." ) unique = str(uuid.uuid4()) filename = unique + secure_filename(uploaded_file.filename) tmp_name = os.path.join(current_app.config['UPLOAD_FOLDER'], filename) uploaded_file.save(tmp_name) current_app.logger.debug("Importing for course " + str(course.id) + " with " + filename) with open(tmp_name, 'rb') as csvfile: spamreader = csv.reader(csvfile) users = [] for row in spamreader: if row: users.append(row) if len(users) > 0: results = import_users(import_type, course, users) on_classlist_upload.send(self, event_name=on_classlist_upload.name, user=current_user, course_id=course.id) os.remove(tmp_name) current_app.logger.debug("Class Import for course " + str(course.id) + " is successful. Removed file.") return results
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']: user_query = user_query.filter(UserCourse.group_name == params['group']) self_evaluation_total = self_evaluation_total \ .join(UserCourse, and_( AnswerComment.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .filter(UserCourse.group_name == params['group']) comparison_total = comparison_total \ .join(UserCourse, and_( Comparison.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .filter(UserCourse.group_name == params['group']) 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, answer_comment_uuid): """ Update an answer comment """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) answer_comment = AnswerComment.get_active_by_uuid_or_404(answer_comment_uuid) require(EDIT, answer_comment, title="Feedback Not Saved", message="Sorry, your role in this course does not allow you to save feedback for this answer.") restrict_user = not allow(MANAGE, assignment) restrict_user = not allow(MANAGE, assignment) was_draft = answer_comment.draft params = existing_answer_comment_parser.parse_args() # make sure the answer comment id in the url and the id matches if params['id'] != answer_comment_uuid: abort(400, title="Feedback Not Saved", message="The feedback's ID does not match the URL, which is required in order to save the feedback.") # modify answer comment according to new values, preserve original values if values not passed answer_comment.content = params.get("content") comment_types = [ AnswerCommentType.public.value, AnswerCommentType.private.value, AnswerCommentType.evaluation.value, AnswerCommentType.self_evaluation.value ] eval_comment_types = [ AnswerCommentType.evaluation.value, AnswerCommentType.self_evaluation.value ] comment_type = params.get("comment_type", AnswerCommentType.private.value) if comment_type not in comment_types: abort(400, title="Feedback Not Saved", message="This feedback type is not recognized. Please contact support for assistance.") # do not allow changing a self-eval into a comment or vise-versa if (answer_comment.comment_type.value in eval_comment_types or comment_type in eval_comment_types) and answer_comment.comment_type.value != comment_type: abort(400, title="Feedback Not Saved", message="Feedback type cannot be changed. Please contact support for assistance.") answer_comment.comment_type = AnswerCommentType(comment_type) if answer_comment.comment_type == AnswerCommentType.self_evaluation and not assignment.self_eval_grace and not allow(MANAGE, assignment): abort(403, title="Self-Evaluation Not Saved", message="Sorry, the self-evaluation deadline has passed and therefore cannot be submitted.") # only update draft param if currently a draft if answer_comment.draft: answer_comment.draft = params.get('draft', answer_comment.draft) # require content not empty if not a draft if not answer_comment.content and not answer_comment.draft: abort(400, title="Feedback Not Saved", message="Please provide content in the text editor and try saving again.") answer_comment.update_attempt( params.get('attempt_uuid'), params.get('attempt_started', None), params.get('attempt_ended', None) ) model_changes = get_model_changes(answer_comment) db.session.add(answer_comment) db.session.commit() on_answer_comment_modified.send( self, event_name=on_answer_comment_modified.name, user=current_user, course_id=course.id, answer_comment=answer_comment, evaluation_number=params.get("evaluation_number"), was_draft=was_draft, data=model_changes) # update course & assignment grade for user if self-evaluation is completed if not answer_comment.draft and answer_comment.comment_type == AnswerCommentType.self_evaluation: assignment.calculate_grade(answer_comment.user) course.calculate_grade(answer_comment.user) return marshal(answer_comment, dataformat.get_answer_comment(restrict_user))
def post(self, course_uuid, assignment_uuid, answer_uuid): """ Create comment for an answer """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) require(CREATE, AnswerComment(course_id=course.id), title="Feedback Not Saved", message="Sorry, your role in this course does not allow you to save feedback for this answer.") restrict_user = not allow(MANAGE, assignment) restrict_user = not allow(MANAGE, assignment) answer_comment = AnswerComment(answer_id=answer.id) params = new_answer_comment_parser.parse_args() answer_comment.draft = params.get('draft') answer_comment.content = params.get("content") # require content not empty if not a draft if not answer_comment.content and not answer_comment.draft: abort(400, title="Feedback Not Saved", message="Please provide content in the text editor and try saving again.") if params.get('user_id') and current_user.system_role == SystemRole.sys_admin: user = User.get_by_uuid_or_404(params.get('user_id')) answer_comment.user_id = user.id else: answer_comment.user_id = current_user.id comment_types = [ AnswerCommentType.public.value, AnswerCommentType.private.value, AnswerCommentType.evaluation.value, AnswerCommentType.self_evaluation.value ] comment_type = params.get("comment_type") if comment_type not in comment_types: abort(400, title="Feedback Not Saved", message="This feedback type is not recognized. Please contact support for assistance.") answer_comment.comment_type = AnswerCommentType(comment_type) if answer_comment.comment_type == AnswerCommentType.self_evaluation and not assignment.self_eval_grace and not allow(MANAGE, assignment): abort(403, title="Self-Evaluation Not Saved", message="Sorry, the self-evaluation deadline has passed and therefore cannot be submitted.") answer_comment.update_attempt( params.get('attempt_uuid'), params.get('attempt_started', None), params.get('attempt_ended', None) ) db.session.add(answer_comment) db.session.commit() # update course & assignment grade for user if self-evaluation is completed if not answer_comment.draft and answer_comment.comment_type == AnswerCommentType.self_evaluation: assignment.calculate_grade(answer_comment.user) course.calculate_grade(answer_comment.user) on_answer_comment_create.send( self, event_name=on_answer_comment_create.name, user=current_user, course_id=course.id, answer_comment=answer_comment, evaluation_number=params.get("evaluation_number"), data=marshal(answer_comment, dataformat.get_answer_comment(restrict_user))) return marshal(answer_comment, dataformat.get_answer_comment(restrict_user))
def get(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require(READ, course, title="Assignment Status Unavailable", message="Assignment status can be seen only by those enrolled in the course. Please double-check your enrollment in this course.") assignments = course.assignments \ .filter_by(active=True) \ .all() assignment_ids = [assignment.id for assignment in assignments] answer_counts = Answer.query \ .with_entities( Answer.assignment_id, func.count(Answer.assignment_id).label('answer_count') ) \ .filter_by( user_id=current_user.id, active=True, practice=False, draft=False ) \ .filter(Answer.assignment_id.in_(assignment_ids)) \ .group_by(Answer.assignment_id) \ .all() feedback_counts = AnswerComment.query \ .join("answer") \ .with_entities( Answer.assignment_id, func.count(Answer.assignment_id).label('feedback_count') ) \ .filter(and_( AnswerComment.active == True, AnswerComment.draft == False, Answer.user_id == current_user.id, Answer.active == True, Answer.practice == False, Answer.draft == False, Answer.assignment_id.in_(assignment_ids) )) \ .group_by(Answer.assignment_id) \ .all() # get self evaluation status for assignments with self evaluations enabled self_evaluations = AnswerComment.query \ .join("answer") \ .with_entities( Answer.assignment_id, func.count(Answer.assignment_id).label('self_evaluation_count') ) \ .filter(and_( AnswerComment.user_id == current_user.id, AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.draft == False, Answer.active == True, Answer.practice == False, Answer.draft == False, Answer.assignment_id.in_(assignment_ids) )) \ .group_by(Answer.assignment_id) \ .all() drafts = Answer.query \ .options(load_only('id', 'assignment_id')) \ .filter_by( user_id=current_user.id, active=True, practice=False, draft=True, saved=True ) \ .all() statuses = {} for assignment in assignments: answer_count = next( (result.answer_count for result in answer_counts if result.assignment_id == assignment.id), 0 ) feedback_count = next( (result.feedback_count for result in feedback_counts if result.assignment_id == assignment.id), 0 ) assignment_drafts = [draft for draft in drafts if draft.assignment_id == assignment.id] comparison_count = assignment.completed_comparison_count_for_user(current_user.id) other_student_answers = assignment.student_answer_count - answer_count comparison_available = comparison_count < other_student_answers * (other_student_answers - 1) / 2 statuses[assignment.uuid] = { 'answers': { 'answered': answer_count > 0, 'feedback': feedback_count, 'count': answer_count, 'has_draft': len(assignment_drafts) > 0, 'draft_ids': [draft.uuid for draft in assignment_drafts] }, 'comparisons': { 'available': comparison_available, 'count': comparison_count, 'left': max(0, assignment.total_comparisons_required - comparison_count) } } if assignment.enable_self_evaluation: self_evaluation_count = next( (result.self_evaluation_count for result in self_evaluations if result.assignment_id == assignment.id), 0 ) statuses[assignment.uuid]['comparisons']['self_evaluation_completed'] = self_evaluation_count > 0 on_assignment_list_get_status.send( self, event_name=on_assignment_list_get_status.name, user=current_user, course_id=course.id, data=statuses) return {"statuses": statuses}
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, assignment, title="Comparisons Unavailable", message="Sorry, your role in this course does not allow you to view comparisons for this assignment.") restrict_user = is_user_access_restricted(current_user) # get comparisons for current user comparisons = Comparison.query \ .filter(and_( Comparison.completed == True, Comparison.assignment_id == assignment.id, Comparison.user_id == current_user.id )) \ .all() # get all self-evaluations and evaluation comments for current user answer_comments = AnswerComment.query \ .join("answer") \ .filter(and_( AnswerComment.active == True, AnswerComment.comment_type.in_([AnswerCommentType.self_evaluation, AnswerCommentType.evaluation]), AnswerComment.draft == False, Answer.active == True, Answer.draft == False, Answer.assignment_id == assignment.id, AnswerComment.user_id == current_user.id )) \ .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 ] on_assignment_user_comparisons_get.send( self, event_name=on_assignment_user_comparisons_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id} ) comparison_set = { 'comparisons': [comparison for comparison in comparisons if comparison.user_id == current_user.id ], 'self_evaluations': [comment for comment in answer_comments if comment.user_id == current_user.id and comment.comment_type == AnswerCommentType.self_evaluation ] } return marshal(comparison_set, dataformat.get_comparison_set(restrict_user, with_user=False))
def post(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require( EDIT, UserCourse(course_id=course.id), title="Enrollment Not Updated", message= "Sorry, your role in this course does not allow you to update enrollment." ) params = update_users_course_role_parser.parse_args() role_name = params.get('course_role') course_roles = [ CourseRole.dropped.value, CourseRole.student.value, CourseRole.teaching_assistant.value, CourseRole.instructor.value ] if role_name not in course_roles: abort( 400, title="Enrollment Not Updated", message= "Please try again with a course role from the list of roles provided." ) course_role = CourseRole(role_name) if len(params.get('ids')) == 0: abort( 400, title="Enrollment Not Updated", message= "Please select at least one user below and then try to update the enrollment 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="Enrollment Not Updated", message= "One or more users selected are not enrolled in the course yet." ) if len(user_courses ) == 1 and user_courses[0].user_id == current_user.id: if course_role == CourseRole.dropped: abort( 400, title="Enrollment Not Updated", message= "Sorry, you cannot drop yourself from the course. Please select only other users and try again." ) else: abort( 400, title="Enrollment Not Updated", message= "Sorry, you cannot change your own course role. Please select only other users and try again." ) for user_course in user_courses: if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if course.id == DemoDataFixture.DEFAULT_COURSE_ID and user.id in DemoDataFixture.DEFAULT_COURSE_USERS: abort( 400, title="Enrollment Not Updated", message= "Sorry, you cannot update course role for the default users in the default demo course." ) # skip current user if user_course.user_id == current_user.id: continue # update user's role user_course.course_role = course_role db.session.commit() on_classlist_update_users_course_roles.send( current_app._get_current_object(), event_name=on_classlist_update_users_course_roles.name, user=current_user, course_id=course.id, data={ 'user_uuids': params.get('ids'), 'course_role': role_name }) return {'course_role': role_name}
def get(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require( READ, UserCourse(course_id=course.id), title="Class List Unavailable", message= "Sorry, your role in this course does not allow you to view the class list." ) restrict_user = not can(READ, USER_IDENTITY) # expire current_user from the session. When loading classlist from database, if the # user is already in the session, e.g. instructor for the course, the User.user_courses # is not loaded from the query below, but from session. In this case, if user has more # than one course, User.user_courses will return multiple row. Thus violating the # course_role constrain. db.session.expire(current_user) users = User.query \ .with_entities(User, UserCourse) \ .options(joinedload(UserCourse.group)) \ .join(UserCourse, and_( UserCourse.user_id == User.id, UserCourse.course_id == course.id )) \ .filter( UserCourse.course_role != CourseRole.dropped ) \ .order_by(User.lastname, User.firstname) \ .all() if not restrict_user: user_ids = [_user.id for (_user, _user_course) in users] third_party_auths = ThirdPartyUser.query \ .filter(and_( ThirdPartyUser.user_id.in_(user_ids), or_( ThirdPartyUser.third_party_type == ThirdPartyType.cas, ThirdPartyUser.third_party_type == ThirdPartyType.saml, ) )) \ .all() class_list = [] for (_user, _user_course) in users: _user.course_role = _user_course.course_role _user.group = _user_course.group _user.group_uuid = _user.group.uuid if _user.group else None _user.group_name = _user.group.name if _user.group else None if not restrict_user: cas_auth = next( (auth for auth in third_party_auths if auth.user_id == _user.id and auth.third_party_type == ThirdPartyType.cas), None) _user.cas_username = cas_auth.unique_identifier if cas_auth else None saml_auth = next( (auth for auth in third_party_auths if auth.user_id == _user.id and auth.third_party_type == ThirdPartyType.saml), None) _user.saml_username = saml_auth.unique_identifier if saml_auth else None class_list.append(_user) on_classlist_get.send(self, event_name=on_classlist_get.name, user=current_user, course_id=course.id) if can(MANAGE, User): return { 'objects': marshal(class_list, dataformat.get_full_users_in_course()) } else: return { 'objects': marshal( class_list, dataformat.get_users_in_course( restrict_user=restrict_user)) }
def get(self, course_uuid, assignment_uuid, **kwargs): """ :query string ids: a comma separated comment uuids to query :query string answer_ids: a comma separated answer uuids for answer filter :query string assignment_id: filter the answer comments with a assignment uuid :query string user_ids: a comma separated user uuids that own the comments :query string self_evaluation: indicate whether the result should include self-evaluation comments or self-evaluation only. Possible values: true, false or only. Default true. :query string evaluation: indicate whether the result should include evaluation comments or evaluation only. Possible values: true, false or only. Default true. :query string draft: indicate whether the result should include drafts for current user or not. Possible values: true, false or only. Default false. :reqheader Accept: the response content type depends on :mailheader:`Accept` header :resheader Content-Type: this depends on :mailheader:`Accept` header of request :statuscode 200: no error :statuscode 404: answers don't exist """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) restrict_user = not allow(MANAGE, assignment) params = answer_comment_list_parser.parse_args() answer_uuids = [] if 'answer_uuid' in kwargs: answer_uuids.append(kwargs['answer_uuid']) elif 'answer_ids' in params and params['answer_ids']: answer_uuids.extend(params['answer_ids'].split(',')) if not answer_uuids and not params['ids'] and not params['assignment_id'] and not params['user_ids']: abort(404, title="Feedback Unavailable", message="There was a problem getting the feedback for this answer. Please try again.") conditions = [] answers = Answer.query \ .filter( Answer.assignment_id == assignment.id, Answer.active == True, Answer.draft == False, Answer.uuid.in_(answer_uuids) ) \ .all() if answer_uuids else [] if answer_uuids and not answers: # non-existing answer ids. abort(404, title="Feedback Unavailable", message="There was a problem getting the feedback for this answer. Please try again.") group = current_user.get_course_group(course.id) course_role = current_user.get_course_role(course.id) # build query condition for each answer for answer in answers: clauses = [AnswerComment.answer_id == answer.id] # student can only see the comments for themselves or public ones. # since the owner of the answer can access all comments. We only filter # on non-owners answer_owner = answer.user_id == current_user.id or (group and group.id == answer.group_id) if course_role == CourseRole.student and not answer_owner: # public comments or comments owned by current user clauses.append(or_( AnswerComment.comment_type == AnswerCommentType.public, AnswerComment.user_id == current_user.id )) conditions.append(and_(*clauses)) query = AnswerComment.query \ .filter( AnswerComment.assignment_id == assignment.id, AnswerComment.active==True, or_(*conditions) ) if params['ids']: query = query.filter(AnswerComment.uuid.in_(params['ids'].split(','))) if params['self_evaluation'] == 'false': # do not include self-evaluation query = query.filter(AnswerComment.comment_type != AnswerCommentType.self_evaluation) elif params['self_evaluation'] == 'only': # only self_evaluation query = query.filter(AnswerComment.comment_type == AnswerCommentType.self_evaluation) if params['evaluation'] == 'false': # do not include evalulation comments query = query.filter(AnswerComment.comment_type != AnswerCommentType.evaluation) elif params['evaluation'] == 'only': # only evaluation query = query.filter(AnswerComment.comment_type == AnswerCommentType.evaluation) if params['draft'] == 'true': # with draft (current_user) query = query.filter(or_( AnswerComment.draft == False, and_( AnswerComment.draft == True, AnswerComment.user_id == current_user.id ) )) elif params['draft'] == 'only': # only draft (current_user) query = query.filter(and_( AnswerComment.draft == True, AnswerComment.user_id == current_user.id )) else: # do not include draft. Default query = query.filter(AnswerComment.draft == False) if params['user_ids']: user_ids = params['user_ids'].split(',') query = query \ .join(User, AnswerComment.user_id == User.id) \ .filter(User.uuid.in_(user_ids)) answer_comments = query.order_by(AnswerComment.created.desc()).all() # checking the permission for answer_comment in answer_comments: require(READ, answer_comment.answer, title="Feedback Unavailable", message="Sorry, your role in this course does not allow you to view feedback for this answer.") on_answer_comment_list_get.send( self, event_name=on_answer_comment_list_get.name, user=current_user, data={'answer_ids': ','.join([str(answer.id) for answer in answers])}) return marshal(answer_comments, dataformat.get_answer_comment(restrict_user))
def post(self, course_uuid, user_uuid): """ Enrol or update a user enrolment in the course The payload for the request has to contain course_role. e.g. {"couse_role":"Student"} :param course_uuid: :param user_uuid: :return: """ course = Course.get_active_by_uuid_or_404(course_uuid) user = User.get_by_uuid_or_404(user_uuid) if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if course.id == DemoDataFixture.DEFAULT_COURSE_ID and user.id in DemoDataFixture.DEFAULT_COURSE_USERS: abort( 400, title="Enrollment Not Updated", message= "Sorry, you cannot update course role for the default users in the default demo course." ) user_course = UserCourse.query \ .filter_by( user_id=user.id, course_id=course.id ) \ .first() if not user_course: user_course = UserCourse(user_id=user.id, course_id=course.id) require( EDIT, user_course, title="Enrollment Not Updated", message= "Sorry, your role in this course does not allow you to update enrollment." ) params = new_course_user_parser.parse_args() role_name = params.get('course_role') course_roles = [ CourseRole.dropped.value, CourseRole.student.value, CourseRole.teaching_assistant.value, CourseRole.instructor.value ] if role_name not in course_roles: abort( 400, title="Enrollment Not Updated", message= "Please try again with a course role from the list of roles provided." ) course_role = CourseRole(role_name) if user_course.course_role != course_role: user_course.course_role = course_role db.session.add(user_course) db.session.commit() on_classlist_enrol.send(self, event_name=on_classlist_enrol.name, user=current_user, course_id=course.id, data={'user_id': user.id}) return { 'user_id': user.uuid, 'fullname': user.fullname, 'fullname_sortable': user.fullname_sortable, 'course_role': course_role.value }
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 post(self, course_uuid): # delete multiple (DELETE request cannot use params in angular) course = Course.get_active_by_uuid_or_404(course_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: abort( 400, title="Group Not Saved", message= "The course groups are locked. You may not remove users from the group they are already assigned to." ) for user_course in user_courses: user_course.group_id = None db.session.commit() on_group_user_list_delete.send( current_app._get_current_object(), event_name=on_group_user_list_delete.name, user=current_user, course_id=course.id, data={ 'user_ids': [user_course.user_id for user_course in user_courses] }) return {'success': True}
def post(self, course_uuid, assignment_uuid, answer_uuid): """ Create comment for an answer """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) require( CREATE, AnswerComment(course_id=course.id), title="Reply Not Saved", message= "Sorry, your role in this course does not allow you to save replies for this answer." ) answer_comment = AnswerComment(answer_id=answer.id) params = new_answer_comment_parser.parse_args() answer_comment.draft = params.get('draft') answer_comment.content = params.get("content") # require content not empty if not a draft if not answer_comment.content and not answer_comment.draft: abort( 400, title="Reply Not Saved", message= "Please provide content in the text editor to reply and try saving again." ) if params.get('user_id' ) and current_user.system_role == SystemRole.sys_admin: user = User.get_by_uuid_or_404(params.get('user_id')) answer_comment.user_id = user.id else: answer_comment.user_id = current_user.id comment_types = [ AnswerCommentType.public.value, AnswerCommentType.private.value, AnswerCommentType.evaluation.value, AnswerCommentType.self_evaluation.value ] comment_type = params.get("comment_type") if comment_type not in comment_types: abort( 400, title="Reply Not Saved", message= "This reply type is not recognized. Please contact support for assistance." ) answer_comment.comment_type = AnswerCommentType(comment_type) db.session.add(answer_comment) db.session.commit() # update course & assignment grade for user if self-evaluation is completed if not answer_comment.draft and answer_comment.comment_type == AnswerCommentType.self_evaluation: assignment.calculate_grade(answer_comment.user) course.calculate_grade(answer_comment.user) on_answer_comment_create.send( self, event_name=on_answer_comment_create.name, user=current_user, course_id=course.id, answer_comment=answer_comment, data=marshal(answer_comment, dataformat.get_answer_comment(False))) return marshal(answer_comment, dataformat.get_answer_comment())
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 post(self, course_uuid, assignment_uuid, answer_uuid, answer_comment_uuid): """ Update an answer comment """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) answer = Answer.get_active_by_uuid_or_404(answer_uuid) answer_comment = AnswerComment.get_active_by_uuid_or_404( answer_comment_uuid) require( EDIT, answer_comment, title="Reply Not Saved", message= "Sorry, your role in this course does not allow you to save replies for this answer." ) was_draft = answer_comment.draft params = existing_answer_comment_parser.parse_args() # make sure the answer comment id in the url and the id matches if params['id'] != answer_comment_uuid: abort( 400, title="Reply Not Saved", message= "The reply's ID does not match the URL, which is required in order to save the reply." ) # modify answer comment according to new values, preserve original values if values not passed answer_comment.content = params.get("content") comment_types = [ AnswerCommentType.public.value, AnswerCommentType.private.value, AnswerCommentType.evaluation.value, AnswerCommentType.self_evaluation.value ] comment_type = params.get("comment_type", AnswerCommentType.private.value) if comment_type not in comment_types: abort( 400, title="Reply Not Saved", message= "This reply type is not recognized. Please contact support for assistance." ) answer_comment.comment_type = AnswerCommentType(comment_type) # only update draft param if currently a draft if answer_comment.draft: answer_comment.draft = params.get('draft', answer_comment.draft) # require content not empty if not a draft if not answer_comment.content and not answer_comment.draft: abort( 400, title="Reply Not Saved", message= "Please provide content in the text editor to reply and try saving again." ) db.session.add(answer_comment) db.session.commit() on_answer_comment_modified.send( self, event_name=on_answer_comment_modified.name, user=current_user, course_id=course.id, answer_comment=answer_comment, was_draft=was_draft, data=get_model_changes(answer_comment)) # update course & assignment grade for user if self-evaluation is completed if not answer_comment.draft and answer_comment.comment_type == AnswerCommentType.self_evaluation: assignment.calculate_grade(answer_comment.user) course.calculate_grade(answer_comment.user) return marshal(answer_comment, dataformat.get_answer_comment())
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): 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}