def get(self): require(READ, USER_IDENTITY, title="User List Unavailable", message="Sorry, your system role does not allow you to view the list of users.") params = user_list_parser.parse_args() query = User.query if params['search']: # match each word of search for word in params['search'].strip().split(' '): if word != '': search = '%'+word+'%' query = query.filter(or_( User.firstname.like(search), User.lastname.like(search), User.displayname.like(search) )) if params['orderBy']: if params['reverse']: query = query.order_by(desc(params['orderBy'])) else: query = query.order_by(asc(params['orderBy'])) query = query.order_by(User.lastname.asc(), User.firstname.asc()) page = query.paginate(params['page'], params['perPage']) on_user_list_get.send( self, event_name=on_user_list_get.name, user=current_user) return {"objects": marshal(page.items, dataformat.get_user(False)), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}
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, 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 get(self): require(MANAGE, LTIConsumer, title="Consumers Unavailable", message="Sorry, your system role does not allow you to view LTI consumers.") params = consumer_list_parser.parse_args() query = LTIConsumer.query if params['orderBy']: if params['reverse']: query = query.order_by(desc(params['orderBy'])) else: query = query.order_by(asc(params['orderBy'])) query = query.order_by(LTIConsumer.created) page = query.paginate(params['page'], params['perPage']) on_consumer_list_get.send( self, event_name=on_consumer_list_get.name, user=current_user) return {'objects': marshal(page.items, dataformat.get_lti_consumer()), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}
def post(self): params = new_consumer_parser.parse_args() consumer = LTIConsumer() require(CREATE, consumer, title="Consumer Not Saved", message="Sorry, your system role does not allow you to save LTI consumers.") consumer.oauth_consumer_key = params.get("oauth_consumer_key") consumer.oauth_consumer_secret = params.get("oauth_consumer_secret") consumer.global_unique_identifier_param = params.get("global_unique_identifier_param") consumer.student_number_param = params.get("student_number_param") try: db.session.add(consumer) db.session.commit() on_consumer_create.send( self, event_name=on_consumer_create.name, user=current_user, consumer=consumer, data={'consumer': marshal(consumer, dataformat.get_lti_consumer())} ) except exc.IntegrityError: db.session.rollback() abort(409, title="Consumer Not Saved", message="An LTI consumer with the same consumer key already exists. Please double-check the consumer key and try saving again.") return marshal(consumer, dataformat.get_lti_consumer(include_sensitive=True))
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 post(self, consumer_uuid): consumer = LTIConsumer.get_by_uuid_or_404(consumer_uuid) require(EDIT, consumer, title="Consumer Not Saved", message="Sorry, your system role does not allow you to save LTI consumers.") params = existing_consumer_parser.parse_args() # make sure the course id in the url and the course id in the params match if params['id'] != consumer_uuid: abort(400, title="Consumer Not Saved", message="The LTI consumer ID does not match the URL, which is required in order to save the LTI consumer.") consumer.oauth_consumer_key = params.get("oauth_consumer_key") consumer.oauth_consumer_secret = params.get("oauth_consumer_secret") consumer.global_unique_identifier_param = params.get("global_unique_identifier_param") consumer.student_number_param = params.get("student_number_param") consumer.active = params.get("active") try: db.session.commit() on_consumer_update.send( self, event_name=on_consumer_update.name, user=current_user, consumer=consumer ) except exc.IntegrityError: db.session.rollback() abort(409, title="Consumer Not Saved", message="An LTI consumer with the same consumer key already exists. Please double-check the consumer key and try saving again.") return marshal(consumer, dataformat.get_lti_consumer(include_sensitive=True))
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="Help Comments Unavailable", message= "Help comments can be seen only by those enrolled in the course. Please double-check your enrollment in this course." ) restrict_user = not allow(MANAGE, assignment) assignment_comments = AssignmentComment.query \ .filter_by( course_id=course.id, assignment_id=assignment.id, active=True ) \ .order_by(AssignmentComment.created.asc()).all() on_assignment_comment_list_get.send( self, event_name=on_assignment_comment_list_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id}) return { "objects": marshal(assignment_comments, dataformat.get_assignment_comment(restrict_user)) }
def delete(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( DELETE, assignment_comment, title="Help Comment Not Deleted", message= "Sorry, your role in this course does not allow you to delete help comments." ) data = marshal(assignment_comment, dataformat.get_assignment_comment(False)) assignment_comment.active = False db.session.commit() on_assignment_comment_delete.send( self, event_name=on_assignment_comment_delete.name, user=current_user, course_id=course.id, assignment_comment=assignment_comment, data=data) return {'id': assignment_comment.uuid}
def get(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require( READ, course, title="Instructors Unavailable", message= "Instructors can only be seen here by those enrolled in the course. Please double-check your enrollment in this course." ) instructors = UserCourse.query \ .filter(and_( UserCourse.course_id==course.id, UserCourse.course_role.in_([CourseRole.teaching_assistant, CourseRole.instructor]) )) \ .all() instructor_uuids = { u.user_uuid: u.course_role.value for u in instructors } on_classlist_instructor_label.send( self, event_name=on_classlist_instructor_label.name, user=current_user, course_id=course.id) return {'instructors': instructor_uuids}
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, user_uuid): user = User.get_by_uuid_or_404(user_uuid) # anyone who passes checking below should be an instructor or admin require(EDIT, user, title="Password Not Saved", message="Sorry, your system role does not allow you to update passwords for this user.") if not user.uses_compair_login: abort(400, title="Password Not Saved", message="Sorry, you cannot update the password since this user does not use the ComPAIR account login method.") params = update_password_parser.parse_args() oldpassword = params.get('oldpassword') if current_user.id == user.id and not oldpassword: abort(400, title="Password Not Saved", message="Sorry, the old password is required. Please enter the old password and try saving again.") elif current_user.id == user.id and not user.verify_password(oldpassword): abort(400, title="Password Not Saved", message="Sorry, the old password is not correct. Please double-check the old password and try saving again.") elif len(params.get('newpassword')) < 4: abort(400, title="Password Not Saved", message="The new password must be at least 4 characters long.") user.password = params.get('newpassword') db.session.commit() on_user_password_update.send( self, event_name=on_user_password_update.name, user=current_user) return marshal_user_data(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) 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 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, ComparisonExample(course_id=course.id), title="Comparison Example Unavailable", message= "Sorry, your role in this course does not allow you to view practice answers." ) # Get all comparison examples for this assignment comparison_examples = ComparisonExample.query \ .filter_by( active=True, assignment_id=assignment.id ) \ .all() on_comparison_example_list_get.send( self, event_name=on_comparison_example_list_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id}) return { "objects": marshal(comparison_examples, dataformat.get_comparison_example()) }
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 delete(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( DELETE, comparison_example, title="Comparison Example Not Deleted", message= "Sorry, your role in this course does not allow you to delete practice answers." ) formatted_comparison_example = marshal( comparison_example, dataformat.get_comparison_example(with_answers=False)) comparison_example.active = False db.session.add(comparison_example) db.session.commit() on_comparison_example_delete.send( self, event_name=on_comparison_example_delete.name, user=current_user, course_id=course.id, data=formatted_comparison_example) return {'id': comparison_example.uuid}
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 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." ) user_course.group_name = None db.session.commit() on_course_group_user_delete.send( current_app._get_current_object(), event_name=on_course_group_user_delete.name, user=current_user, course_id=course.id, data={'user_id': user.id}) return {'user_id': user.uuid, 'course_id': course.uuid}
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) 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): """ 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): """ 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 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 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, 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 post(self): params = new_consumer_parser.parse_args() consumer = LTIConsumer() require(CREATE, consumer, title="Consumer Not Saved", message="Sorry, your system role does not allow you to save LTI consumers.") consumer.oauth_consumer_key = params.get("oauth_consumer_key") consumer.oauth_consumer_secret = params.get("oauth_consumer_secret") consumer.global_unique_identifier_param = params.get("global_unique_identifier_param") consumer.student_number_param = params.get("student_number_param") consumer.custom_param_regex_sanitizer = params.get("custom_param_regex_sanitizer") try: db.session.add(consumer) db.session.commit() on_consumer_create.send( self, event_name=on_consumer_create.name, user=current_user, consumer=consumer, data={'consumer': marshal(consumer, dataformat.get_lti_consumer())} ) except exc.IntegrityError: db.session.rollback() abort(409, title="Consumer Not Saved", message="An LTI consumer with the same consumer key already exists. Please double-check the consumer key and try saving again.") return marshal(consumer, dataformat.get_lti_consumer(include_sensitive=True))
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="Reply Unavailable", message= "Sorry, your role in this course does not allow you to view this reply." ) 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())
def post(self, consumer_uuid): consumer = LTIConsumer.get_by_uuid_or_404(consumer_uuid) require(EDIT, consumer, title="Consumer Not Saved", message="Sorry, your system role does not allow you to save LTI consumers.") params = existing_consumer_parser.parse_args() # make sure the course id in the url and the course id in the params match if params['id'] != consumer_uuid: abort(400, title="Consumer Not Saved", message="The LTI consumer ID does not match the URL, which is required in order to save the LTI consumer.") consumer.oauth_consumer_key = params.get("oauth_consumer_key") consumer.oauth_consumer_secret = params.get("oauth_consumer_secret") consumer.global_unique_identifier_param = params.get("global_unique_identifier_param") consumer.student_number_param = params.get("student_number_param") consumer.custom_param_regex_sanitizer = params.get("custom_param_regex_sanitizer") consumer.active = params.get("active") try: db.session.commit() on_consumer_update.send( self, event_name=on_consumer_update.name, user=current_user, consumer=consumer ) except exc.IntegrityError: db.session.rollback() abort(409, title="Consumer Not Saved", message="An LTI consumer with the same consumer key already exists. Please double-check the consumer key and try saving again.") return marshal(consumer, dataformat.get_lti_consumer(include_sensitive=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, group_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) group = Group.get_active_by_uuid_or_404(group_uuid) require( READ, UserCourse(course_id=course.id), title="Group Members Unavailable", message= "Group membership can be seen only by those enrolled in the course. Please double-check your enrollment in this course." ) members = User.query \ .join(UserCourse, UserCourse.user_id == User.id) \ .filter(and_( UserCourse.course_id == course.id, UserCourse.course_role != CourseRole.dropped, UserCourse.group_id == group.id )) \ .order_by(User.lastname, User.firstname) \ .all() on_group_user_list_get.send(current_app._get_current_object(), event_name=on_group_user_list_get.name, user=current_user, course_id=course.id, data={'group_id': group.id}) return { 'objects': [{ 'id': u.uuid, 'name': u.fullname_sortable } for u in members] }
def post(self, course_uuid, 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, user_uuid): user = User.get_by_uuid_or_404(user_uuid) require( MANAGE, User, title="User's Third Party Logins Unavailable", message= "Sorry, your system role does not allow you to view third party logins for this user." ) third_party_users = ThirdPartyUser.query \ .filter(ThirdPartyUser.user_id == user.id) \ .order_by( ThirdPartyUser.third_party_type, ThirdPartyUser.unique_identifier ) \ .all() on_user_third_party_users_get.send( self, event_name=on_user_third_party_users_get.name, user=current_user, data={'user_id': user.id}) return { "objects": marshal(third_party_users, dataformat.get_third_party_user()) }
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="Reply Not Deleted", message= "Sorry, your role in this course does not allow you to delete replies 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, user_uuid): user = User.get_by_uuid_or_404(user_uuid) # anyone who passes checking below should be an instructor or admin require( EDIT, user, title="Notifications Not Updated", message= "Sorry, your system role does not allow you to update notification settings for this user." ) if not user.email: abort( 400, title="Notifications Not Updated", message= "Sorry, you cannot update notification settings since this user does not have an email address in ComPAIR." ) params = update_notification_settings_parser.parse_args() email_notification_method = params.get("email_notification_method") check_valid_email_notification_method(email_notification_method) user.email_notification_method = EmailNotificationMethod( email_notification_method) db.session.commit() on_user_notifications_update.send( self, event_name=on_user_notifications_update.name, user=current_user) return marshal(user, dataformat.get_user(is_user_access_restricted(user)))
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, title="Group Not Saved", message= "Sorry, your role in this course does not allow you to save groups." ) 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 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 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 get(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) require( MANAGE, User, title="User's LTI Account Links Unavailable", message= "Sorry, your system role does not allow you to view LTI account links for this user." ) lti_users = user.lti_user_links \ .order_by( LTIUser.lti_consumer_id, LTIUser.user_id ) \ .all() on_user_lti_users_get.send(self, event_name=on_user_lti_users_get.name, user=current_user, data={'user_id': user.id}) return {"objects": marshal(lti_users, dataformat.get_lti_user())}
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 delete(self, user_uuid, lti_user_uuid): """ unlink lti user from compair user """ user = User.get_by_uuid_or_404(user_uuid) lti_user = LTIUser.get_by_uuid_or_404(lti_user_uuid) require( MANAGE, User, title="User's LTI Account Links Unavailable", message= "Sorry, your system role does not allow you to remove LTI account links for this user." ) lti_user.compair_user_id = None db.session.commit() on_user_lti_user_unlink.send(self, event_name=on_user_lti_user_unlink.name, user=current_user, data={ 'user_id': user.id, 'lti_user_id': lti_user.id }) return {'success': True}
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, user_uuid, third_party_user_uuid): user = User.get_by_uuid_or_404(user_uuid) third_party_user = ThirdPartyUser.get_by_uuid_or_404( third_party_user_uuid) require( MANAGE, User, title="User's Third Party Logins Unavailable", message= "Sorry, your system role does not allow you to delete third party connections for this user." ) on_user_third_party_user_delete.send( self, event_name=on_user_third_party_user_delete.name, user=current_user, data={ 'user_id': user.id, 'third_party_type': third_party_user.third_party_type, 'unique_identifier': third_party_user.unique_identifier }) # TODO: consider adding soft delete to thrid_party_user in the future ThirdPartyUser.query.filter( ThirdPartyUser.uuid == third_party_user_uuid).delete() db.session.commit() return {'success': True}
def delete(self, course_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) require( DELETE, course, title="Course Not Deleted", message= "Sorry, your role in this course does not allow you to delete 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 Deleted", message="Sorry, you cannot remove the default demo course." ) course.active = False course.clear_lti_links() 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, 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 get(self, course_uuid, assignment_uuid): # course unused, but we need to call it to check if it's a valid course course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) # permission checks require( MANAGE, assignment, title="Unable to download attachments", message= "Sorry, your system role does not allow downloading all attachments" ) # grab answers so we can see how many has files answers = self.getStudentAnswersByAssignment(assignment) fileIds = [] fileAuthors = {} for answer in answers: if not answer.file_id: continue # answer has an attachment fileIds.append(answer.file_id) # the user who uploaded the file can be different from the answer # author (e.g. instructor can upload on behalf of student), so # we need to use the answer author instead of file uploader author = answer.user_fullname if answer.user_student_number: author += self.DELIM + answer.user_student_number fileAuthors[answer.file_id] = author if not fileIds: return {'msg': 'Assignment has no attachments'} # grab files so we can get the real file location files = self.getFilesByIds(fileIds) # zip up the tmp dir and save it to the report dir zipName = '{} [attachments-{}].zip'.format(assignment.name, assignment.uuid) zipPath = '{}/{}'.format(current_app.config['REPORT_FOLDER'], zipName) # we're using compression level 6 as a compromise between speed & # compression (this is Z_DEFAULT_COMPRESSION in zlib) with zipfile.ZipFile(zipPath, 'w', zipfile.ZIP_DEFLATED, True, 6) as zipFile: for srcFile in files: srcFilePath = '{}/{}'.format( current_app.config['ATTACHMENT_UPLOAD_FOLDER'], srcFile.name) # filename should be 'full name - student number - uuid.ext' # student number is omitted if user doesn't have one srcFileName = fileAuthors[ srcFile.id] + self.DELIM + srcFile.name #current_app.logger.debug("writing " + srcFileName) zipFile.write(srcFilePath, srcFileName) #current_app.logger.debug("Writing zip file") return {'file': 'report/' + zipName, 'filename': zipName}
def post(self): """ Create new course """ require(CREATE, Course, title="Course Not Saved", message="Sorry, your role in the system does not allow you to save courses.") params = new_course_parser.parse_args() new_course = Course( name=params.get("name"), year=params.get("year"), term=params.get("term"), sandbox=params.get("sandbox"), start_date=params.get('start_date'), end_date=params.get('end_date', None) ) if new_course.start_date is not None: new_course.start_date = datetime.datetime.strptime( new_course.start_date, '%Y-%m-%dT%H:%M:%S.%fZ') if new_course.end_date is not None: new_course.end_date = datetime.datetime.strptime( new_course.end_date, '%Y-%m-%dT%H:%M:%S.%fZ') if new_course.start_date and new_course.end_date and new_course.start_date > new_course.end_date: abort(400, title="Course Not Saved", message="Course end time must be after course start time.") try: # create the course db.session.add(new_course) # also need to enrol the user as an instructor new_user_course = UserCourse( course=new_course, user_id=current_user.id, course_role=CourseRole.instructor ) db.session.add(new_user_course) db.session.commit() except exc.SQLAlchemyError as e: db.session.rollback() current_app.logger.error("Failed to add new course. " + str(e)) raise on_course_create.send( self, event_name=on_course_create.name, user=current_user, course=new_course, data=marshal(new_course, dataformat.get_course())) return marshal(new_course, dataformat.get_course())
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 file_retrieve(file_type, file_name): file_dirs = { 'attachment': app.config['ATTACHMENT_UPLOAD_FOLDER'], 'report': app.config['REPORT_FOLDER'] } file_path = '{}/{}'.format(file_dirs[file_type], file_name) params = attachment_download_parser.parse_args() if file_type == 'attachment': attachment = File.get_by_file_name_or_404( file_name, joinedloads=['answers', 'assignments'] ) for answer in attachment.answers: require(READ, answer, title="Attachment Unavailable", message="Sorry, your role does not allow you to view the attachment.") for assignment in attachment.assignments: require(READ, assignment, title="Attachment Unavailable", message="Sorry, your role does not allow you to view the attachment.") # If attachment is in Kaltura, redirect the user if attachment.kaltura_media and KalturaAPI.enabled(): entry_id = attachment.kaltura_media.entry_id download_url = attachment.kaltura_media.download_url if entry_id: # Short-lived session of 60 seconds for user to start the media download kaltura_url = KalturaAPI.get_direct_access_url(entry_id, download_url, 60) return redirect(kaltura_url) if not os.path.exists(file_path): return make_response('invalid file name', 404) # TODO: add bouncer for reports mimetype, encoding = mimetypes.guess_type(file_name) attachment_filename = None as_attachment = False if file_type == 'attachment' and mimetype != "application/pdf": attachment_filename = params.get('name') #optionally set the download file name as_attachment = True on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=current_user, file_type=file_type, file_name=file_name, data={'file_path': file_path, 'mimetype': mimetype}) return send_file(file_path, mimetype=mimetype, attachment_filename=attachment_filename, as_attachment=as_attachment)
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): 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