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 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 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 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, 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 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 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 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()) }
class TestUsersModel(ComPAIRTestCase): user = User() def setUp(self): self.user.firstname = "John" self.user.lastname = "Smith" def test_fullname(self): self.assertEqual(self.user.fullname, "John Smith") def test_fullname_sortable(self): self.assertEqual(self.user.fullname_sortable, "Smith, John") def test_avatar(self): # role != student self.user.email = "*****@*****.**" self.assertEqual(self.user.avatar, '0bc83cb571cd1c50ba6f3e8a78ef1346') self.user.email = " [email protected] " self.assertEqual(self.user.avatar, '0bc83cb571cd1c50ba6f3e8a78ef1346', 'Email with leading and trailing whilespace') self.user.email = "*****@*****.**" self.assertEqual(self.user.avatar, '0bc83cb571cd1c50ba6f3e8a78ef1346', 'Email with upper case letters') self.user.system_role = SystemRole.student self.user.uuid = str(base64.urlsafe_b64encode( uuid.uuid4().bytes)).replace('=', '') self.assertNotEqual(self.user.avatar, '0bc83cb571cd1c50ba6f3e8a78ef1346', 'Student based on uuid') def test_set_password(self): self.user.password = '******' self.assertTrue(self.user.verify_password('123456'))
def post(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) # anyone who passes checking below should be an admin or current user if not allow(MANAGE, user) and not user.id == current_user.id and not \ (allow(EDIT, user) and current_app.config.get('EXPOSE_EMAIL_TO_INSTRUCTOR', False)): abort( 403, 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_data(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) 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, user_uuid): if not current_app.config.get('IMPERSONATION_ENABLED', False): abort(404, title="Impersonation not supported", message="Impersonation function not enabled") original_user = User.query.get(current_user.id) impersonate_as_uuid = user_uuid impersonate_as_user = User.get_by_uuid_or_404(impersonate_as_uuid) if not impersonation.can_impersonate(impersonate_as_user.get_id()): abort(400, title="Impersonation Failed", message="Cannot perform impersonation") impersonation_success = impersonation.start_impersonation( impersonate_as_user.get_id()) # if successful, from this point on, current_user is no longer the original user if not impersonation_success: abort(400, title="Impersonation Failed", message="Cannot perform impersonation") on_impersonation_started.send( self, event_name=on_impersonation_started.name, user=original_user, data={'impersonate_as': impersonate_as_user.id}) # user is impersonating. treat as restrict_user when calling dataformat return { 'impersonate_as': marshal(impersonate_as_user, dataformat.get_user(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, 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, 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, user_uuid): user = User.get_by_uuid_or_404(user_uuid) on_user_get.send(self, event_name=on_user_get.name, user=current_user, data={'id': user.id}) return marshal_user_data(user)
def get(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) on_user_get.send(self, event_name=on_user_get.name, user=current_user, data={'id': user.id}) return marshal(user, dataformat.get_user(is_user_access_restricted(user)))
def get(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) on_user_get.send( self, event_name=on_user_get.name, user=current_user, data={'id': user.id} ) return marshal_user_data(user)
def get(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) on_user_get.send( self, event_name=on_user_get.name, user=current_user, data={'id': user.id} ) return marshal(user, dataformat.get_user(is_user_access_restricted(user)))
def get(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) available = allow(EDIT, user) on_user_edit_button_get.send( self, event_name=on_user_edit_button_get.name, user=current_user, data={'user_id': user.id, 'available': available}) return {'available': available}
def get(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) available = can(EDIT, user) on_user_edit_button_get.send( self, event_name=on_user_edit_button_get.name, user=current_user, data={'user_id': user.id, 'available': available}) return {'available': available}
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 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) 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, 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, title="Enrollment Not Updated", message= "Sorry, your role in this course does not allow you to update enrollment." ) 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.course_role = CourseRole.dropped 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 { 'user_id': user.uuid, 'fullname': user.fullname, 'fullname_sortable': user.fullname_sortable, 'course_role': CourseRole.dropped.value }
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 post(self, course_uuid, group_uuid, user_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) group = Group.get_active_by_uuid_or_404(group_uuid) user = User.get_by_uuid_or_404(user_uuid) user_course = UserCourse.query \ .filter(and_( UserCourse.course_id == course.id, UserCourse.user_id == user.id, UserCourse.course_role != CourseRole.dropped )) \ .first_or_404() require( EDIT, user_course, title="Group Not Saved", message= "Sorry, your role in this course does not allow you to save groups." ) if course.groups_locked and user_course.group_id != None and user_course.group_id != group.id: abort( 400, title="Group Not Saved", message= "The course groups are locked. This user is already assigned to a different group." ) user_course.group_id = group.id db.session.commit() on_group_user_create.send(current_app._get_current_object(), event_name=on_group_user_create.name, user=current_user, course_id=course.id, data={'user_id': user.id}) return marshal(group, dataformat.get_group())
def 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 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, 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) if user.uses_compair_login: params = update_password_parser.parse_args() oldpassword = params.get('oldpassword') # if it is not current user changing own password, it must be an instructor or admin # because of above check if current_user.id != user.id or (oldpassword and user.verify_password(oldpassword)): 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, dataformat.get_user(False)) else: return {"error": "The old password is incorrect or you do not have permission to change password."}, 403 else: return {"error": "Cannot update password. User does not use ComPAIR account login authentication method."}, 400
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, 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 post(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) # anyone who passes checking below should be an admin or current user if not allow(MANAGE, user) and not user.id == current_user.id and not \ (allow(EDIT, user) and current_app.config.get('EXPOSE_EMAIL_TO_INSTRUCTOR', False)): abort(403, 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_data(user)
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, title="Enrollment Not Updated", message="Sorry, your role in this course does not allow you to update enrollment.") 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.course_role = CourseRole.dropped 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 { 'user_id': user.uuid, 'fullname': user.fullname, 'fullname_sortable': user.fullname_sortable, 'course_role': CourseRole.dropped.value }
def post(self, user_uuid): if not current_app.config.get('IMPERSONATION_ENABLED', False): abort(404, title="Impersonation not supported", message="Impersonation function not enabled") original_user = User.query.get(current_user.id) impersonate_as_uuid = user_uuid impersonate_as_user = User.get_by_uuid_or_404(impersonate_as_uuid) if not impersonation.can_impersonate(impersonate_as_user.get_id()): abort(400, title="Impersonation Failed", message="Cannot perform impersonation") impersonation_success = impersonation.start_impersonation(impersonate_as_user.get_id()) # if successful, from this point on, current_user is no longer the original user if not impersonation_success: abort(400, title="Impersonation Failed", message="Cannot perform impersonation") on_impersonation_started.send( self, event_name=on_impersonation_started.name, user=original_user, data={ 'impersonate_as': impersonate_as_user.id }) # user is impersonating. treat as restrict_user when calling dataformat return { 'impersonate_as' : marshal(impersonate_as_user, dataformat.get_user(True)) }
def post(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) if is_user_access_restricted(user): abort(403, title="User Not Saved", message="Sorry, your role does not allow you to save this user.") params = existing_user_parser.parse_args() # make sure the user id in the url and the id matches if params['id'] != user_uuid: abort(400, title="User Not Saved", message="The user's ID does not match the URL, which is required in order to save the user.") # only update username if user uses compair login method if user.uses_compair_login: username = params.get("username") if username == None: abort(400, title="User Not Saved", message="A username is required. Please enter a username and try saving again.") username_exists = User.query.filter_by(username=username).first() if username_exists and username_exists.id != user.id: abort(409, title="User Not Saved", message="Sorry, this username already exists and usernames must be unique in ComPAIR. Please enter another username and try saving again.") user.username = username elif allow(MANAGE, user): #admins can optionally set username for users without a username username = params.get("username") if username: username_exists = User.query.filter_by(username=username).first() if username_exists and username_exists.id != user.id: abort(409, title="User Not Saved", message="Sorry, this username already exists and usernames must be unique in ComPAIR. Please enter another username and try saving again.") user.username = username else: user.username = None if allow(MANAGE, user): system_role = params.get("system_role", user.system_role.value) check_valid_system_role(system_role) user.system_role = SystemRole(system_role) if allow(MANAGE, user) or user.id == current_user.id or current_app.config.get('EXPOSE_EMAIL_TO_INSTRUCTOR', False): if current_user.system_role != SystemRole.student or current_app.config.get('ALLOW_STUDENT_CHANGE_EMAIL'): user.email = params.get("email", user.email) email_notification_method = params.get("email_notification_method") check_valid_email_notification_method(email_notification_method) user.email_notification_method = EmailNotificationMethod(email_notification_method) elif params.get("email") or params.get("email_notification_method"): abort(400, title="User Not Saved", message="your role does not allow you to change email settings for this user.") if current_user.system_role != SystemRole.student or current_app.config.get('ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'): # only students should have student numbers if user.system_role == SystemRole.student: student_number = params.get("student_number", user.student_number) student_number_exists = User.query.filter_by(student_number=student_number).first() if student_number is not None and student_number_exists and student_number_exists.id != user.id: abort(409, title="User Not Saved", message="Sorry, this student number already exists and student numbers must be unique in ComPAIR. Please enter another number and try saving again.") else: user.student_number = student_number else: user.student_number = None if current_user.system_role != SystemRole.student or current_app.config.get('ALLOW_STUDENT_CHANGE_NAME'): user.firstname = params.get("firstname", user.firstname) user.lastname = params.get("lastname", user.lastname) if current_user.system_role != SystemRole.student or current_app.config.get('ALLOW_STUDENT_CHANGE_DISPLAY_NAME'): user.displayname = params.get("displayname", user.displayname) model_changes = get_model_changes(user) try: db.session.commit() on_user_modified.send( self, event_name=on_user_modified.name, user=current_user, data={'id': user.id, 'changes': model_changes}) except exc.IntegrityError: db.session.rollback() abort(409, title="User Not Saved", message="Sorry, this ID already exists and IDs must be unique in ComPAIR. Please try addding another user.") return marshal_user_data(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, assignment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) require(READ, Comparison(course_id=course.id), title="Comparisons Unavailable", message="Sorry, your role in this course does not allow you to view all comparisons for this assignment.") restrict_user = is_user_access_restricted(current_user) params = assignment_users_comparison_list_parser.parse_args() # only get users who have at least made one comparison # each paginated item is a user (with a set of comparisons and self-evaluations) user_query = User.query \ .join(UserCourse, and_( User.id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .join(Comparison, and_( Comparison.user_id == User.id, Comparison.assignment_id == assignment.id )) \ .filter(and_( UserCourse.course_role != CourseRole.dropped, Comparison.completed == True )) \ .group_by(User) \ .order_by(User.lastname, User.firstname) self_evaluation_total = AnswerComment.query \ .join("answer") \ .with_entities( func.count(Answer.assignment_id).label('self_evaluation_count') ) \ .filter(and_( AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.draft == False, Answer.active == True, Answer.practice == False, Answer.draft == False, Answer.assignment_id == assignment.id )) comparison_total = Comparison.query \ .with_entities( func.count(Comparison.assignment_id).label('comparison_count') ) \ .filter(and_( Comparison.completed == True, Comparison.assignment_id == assignment.id )) if params['author']: user = User.get_by_uuid_or_404(params['author']) user_query = user_query.filter(User.id == user.id) self_evaluation_total = self_evaluation_total.filter(AnswerComment.user_id == user.id) comparison_total = comparison_total.filter(Comparison.user_id == user.id) elif params['group']: group = Group.get_active_by_uuid_or_404(params['group']) user_query = user_query.filter(UserCourse.group_id == group.id) self_evaluation_total = self_evaluation_total \ .join(UserCourse, and_( AnswerComment.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .filter(UserCourse.group_id == group.id) comparison_total = comparison_total \ .join(UserCourse, and_( Comparison.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .filter(UserCourse.group_id == group.id) page = user_query.paginate(params['page'], params['perPage']) self_evaluation_total = self_evaluation_total.scalar() comparison_total = comparison_total.scalar() comparison_sets = [] if page.total: user_ids = [user.id for user in page.items] # get all comparisons that group of users created comparisons = Comparison.query \ .filter(and_( Comparison.completed == True, Comparison.assignment_id == assignment.id, Comparison.user_id.in_(user_ids) )) \ .all() # retrieve the answer comments user_comparison_answers = {} for comparison in comparisons: user_answers = user_comparison_answers.setdefault(comparison.user_id, set()) user_answers.add(comparison.answer1_id) user_answers.add(comparison.answer2_id) conditions = [] for user_id, answer_set in user_comparison_answers.items(): conditions.append(and_( AnswerComment.comment_type == AnswerCommentType.evaluation, AnswerComment.user_id == user_id, AnswerComment.answer_id.in_(list(answer_set)), AnswerComment.assignment_id == assignment.id )) conditions.append(and_( AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.user_id == user_id, AnswerComment.assignment_id == assignment.id )) answer_comments = AnswerComment.query \ .filter(or_(*conditions)) \ .filter_by(draft=False) \ .all() # add comparison answer evaluation comments to comparison object for comparison in comparisons: comparison.answer1_feedback = [comment for comment in answer_comments if comment.user_id == comparison.user_id and comment.answer_id == comparison.answer1_id and comment.comment_type == AnswerCommentType.evaluation ] comparison.answer2_feedback = [comment for comment in answer_comments if comment.user_id == comparison.user_id and comment.answer_id == comparison.answer2_id and comment.comment_type == AnswerCommentType.evaluation ] for user in page.items: comparison_sets.append({ 'user': user, 'comparisons': [comparison for comparison in comparisons if comparison.user_id == user.id ], 'self_evaluations': [comment for comment in answer_comments if comment.user_id == user.id and comment.comment_type == AnswerCommentType.self_evaluation ] }) on_assignment_users_comparisons_get.send( self, event_name=on_assignment_users_comparisons_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id} ) return {"objects": marshal(comparison_sets, dataformat.get_comparison_set(restrict_user, with_user=True)), "comparison_total": comparison_total, "self_evaluation_total": self_evaluation_total, "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}
def post(self, course_uuid, assignment_uuid, answer_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) if not assignment.answer_grace and not allow(MANAGE, assignment): return {'error': answer_deadline_message}, 403 answer = Answer.get_active_by_uuid_or_404(answer_uuid) require(EDIT, answer) restrict_user = not allow(MANAGE, assignment) params = existing_answer_parser.parse_args() # make sure the answer id in the url and the id matches if params['id'] != answer_uuid: return {"error": "Answer id does not match the URL."}, 400 # modify answer according to new values, preserve original values if values not passed answer.content = params.get("content") user_uuid = params.get("user_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: if not allow(MANAGE, answer) or not answer.draft: return {"error": "Only instructors and teaching assistants can submit an answer on behalf of another user."}, 400 user = User.get_by_uuid_or_404(user_uuid) answer.user_id = user.id user_course = UserCourse.query \ .filter_by( course_id=course.id, user_id=answer.user_id ) \ .one_or_none() if user_course.course_role.value not in [CourseRole.instructor.value, CourseRole.teaching_assistant.value]: # check if there is a previous answer submitted for the student prev_answer = Answer.query \ .filter(Answer.id != answer.id) \ .filter_by( assignment_id=assignment.id, user_id=answer.user_id, active=True ) \ .first() if prev_answer: return {"error": "An answer has already been submitted."}, 400 # can only change draft status while a draft if answer.draft: answer.draft = params.get("draft") uploaded = params.get('uploadFile') file_uuid = params.get('file_id') if file_uuid: answer.file = File.get_by_uuid_or_404(file_uuid) else: answer.file_id = None # non-drafts must have content if not answer.draft and not answer.content and not file_uuid: return {"error": "The answer content is empty!"}, 400 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, assignment=assignment, data=get_model_changes(answer)) # update course & assignment grade for user if answer is fully submitted if not answer.draft: assignment.calculate_grade(answer.user) course.calculate_grade(answer.user) return marshal(answer, dataformat.get_answer(restrict_user))
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, 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 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 get(self, course_uuid, assignment_uuid): """ Return a list of answers for a assignment based on search criteria. The list of the answers are paginated. If there is any answers from instructor or TA, their answers will be on top of the list (unless they are comparable). :param course_uuid: course uuid :param assignment_uuid: assignment uuid :return: list of answers """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) require( READ, assignment, title="Answers Unavailable", message= "Answers are visible only to those enrolled in the course. Please double-check your enrollment in this course." ) restrict_user = not allow(MANAGE, assignment) params = answer_list_parser.parse_args() # if assingment has no rank display limit set, restricted users can't force to retreive by rank if params[ 'orderBy'] == 'score' and restrict_user and not assignment.rank_display_limit: abort( 400, title="Answers Unavailable", message= "Sorry, you cannot cannot see answers by rank for this assignment." ) if restrict_user and not assignment.after_comparing: # only the answer from student himself/herself should be returned params['author'] = current_user.uuid # this query could be further optimized by reduction the selected columns query = Answer.query \ .options(joinedload('file')) \ .options(joinedload('user')) \ .options(joinedload('group')) \ .options(joinedload('score')) \ .options(undefer_group('counts')) \ .outerjoin(UserCourse, and_( Answer.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .add_columns( and_( UserCourse != None, UserCourse.course_role.__eq__(CourseRole.instructor), not Answer.comparable ).label("instructor_role"), and_( UserCourse != None, UserCourse.course_role.__eq__(CourseRole.teaching_assistant), not Answer.comparable ).label("ta_role") ) \ .filter(and_( Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False, or_( and_(UserCourse.course_role != CourseRole.dropped, Answer.user_id != None), Answer.group_id != None ) )) \ .order_by(desc('instructor_role'), desc('ta_role')) if params['author']: user = User.get_by_uuid_or_404(params['author']) group = user.get_course_group(course.id) if group: query = query.filter( or_(Answer.user_id == user.id, Answer.group_id == group.id)) else: query = query.filter(Answer.user_id == user.id) elif params['group']: group = Group.get_active_by_uuid_or_404(params['group']) query = query.filter( or_(UserCourse.group_id == group.id, Answer.group_id == group.id)) if params['ids']: query = query.filter(Answer.uuid.in_(params['ids'].split(','))) if params['top']: query = query.filter(Answer.top_answer == True) if params['orderBy'] == 'score': # use outer join to include comparable answers that are not yet compared (for non-restricted users) query = query.outerjoin(AnswerScore) \ .filter(Answer.comparable == True) \ .order_by(AnswerScore.score.desc(), Answer.submission_date.desc(), Answer.created.desc()) if restrict_user: # when orderd by rank, students won't see answers that are not compared (i.e. no score/rank) query = query.filter(AnswerScore.score.isnot(None)) # limit answers up to rank if rank_display_limit is set and current_user is restricted (student) if assignment.rank_display_limit and restrict_user: score_for_rank = AnswerScore.get_score_for_rank( assignment.id, assignment.rank_display_limit) # display answers with score >= score_for_rank if score_for_rank != None: # will get all answers with a score greater than or equal to the score for a given rank # the '- 0.00001' fixes floating point precision problems query = query.filter( AnswerScore.score >= score_for_rank - 0.00001) else: # when ordered by date, non-comparable answers should be on top of the list query = query.order_by(Answer.comparable, Answer.submission_date.desc(), Answer.created.desc()) page = query.paginate(params['page'], params['perPage'], error_out=False) # remove label entities from results page.items = [ answer for (answer, instructor_role, ta_role) in page.items ] on_answer_list_get.send(self, event_name=on_answer_list_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id}) # only include score/rank info if: # - requesters are non-restricted users (i.e. instructors / TAs); or, # - retrieving answers ordered by score/rank include_score = (not restrict_user) or \ (params['orderBy'] == 'score' and assignment.rank_display_limit) return { "objects": marshal( page.items, dataformat.get_answer(restrict_user, include_score=include_score)), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page }
def post(self, course_uuid, assignment_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) if not assignment.answer_grace and not allow(MANAGE, assignment): abort( 403, title="Answer Not Submitted", message= "Sorry, the answer deadline has passed. No answers can be submitted after the deadline unless the instructor submits the answer for you." ) require( CREATE, Answer(course_id=course.id), title="Answer Not Submitted", message= "Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course." ) restrict_user = not allow(MANAGE, assignment) answer = Answer(assignment_id=assignment.id) params = new_answer_parser.parse_args() answer.content = params.get("content") answer.draft = params.get("draft") file_uuid = params.get('file_id') attachment = None if file_uuid: attachment = File.get_by_uuid_or_404(file_uuid) answer.file_id = attachment.id else: answer.file_id = None # non-drafts must have content if not answer.draft and not answer.content and not file_uuid: abort( 400, title="Answer Not Submitted", message= "Please provide content in the text editor or upload a file and try submitting again." ) user_uuid = params.get("user_id") group_uuid = params.get("group_id") # we allow instructor and TA to submit multiple answers for other users in the class if user_uuid and not allow(MANAGE, Answer(course_id=course.id)): abort( 400, title="Answer Not Submitted", message= "Only instructors and teaching assistants can submit an answer on behalf of another." ) if group_uuid and not assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Group answers are not allowed for this assignment.") if group_uuid and not allow(MANAGE, Answer(course_id=course.id)): abort( 400, title="Answer Not Submitted", message= "Only instructors and teaching assistants can submit an answer on behalf of a group." ) if group_uuid and user_uuid: abort( 400, title="Answer Not Submitted", message= "You cannot submit an answer for a user and a group at the same time." ) user = User.get_by_uuid_or_404( user_uuid) if user_uuid else current_user group = Group.get_active_by_uuid_or_404( group_uuid) if group_uuid else None if restrict_user and assignment.enable_group_answers and not group: group = current_user.get_course_group(course.id) if group == None: abort( 400, title="Answer Not Submitted", message= "You are currently not in any group for this course. Please contact your instructor to be added to a group." ) check_for_existing_answers = False if group and assignment.enable_group_answers: if group.course_id != course.id: abort( 400, title="Answer Not Submitted", message= "Group answers can be submitted to courses they belong in." ) answer.user_id = None answer.group_id = group.id answer.comparable = True check_for_existing_answers = True else: answer.user_id = user.id answer.group_id = None course_role = User.get_user_course_role(answer.user_id, course.id) # only system admin can add answers for themselves to a class without being enrolled in it # required for managing comparison examples as system admin if (not course_role or course_role == CourseRole.dropped ) and current_user.system_role != SystemRole.sys_admin: abort( 400, title="Answer Not Submitted", message= "Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course." ) if course_role == CourseRole.student and assignment.enable_group_answers: abort( 400, title="Answer Not Submitted", message= "Students can only submit group answers for this assignment." ) # we allow instructor and TA to submit multiple answers for their own, # but not for student. Each student can only have one answer. if course_role and course_role == CourseRole.student: check_for_existing_answers = True answer.comparable = True else: # instructor / TA / Sys Admin can mark the answer as non-comparable, unless the answer is for a student answer.comparable = params.get("comparable") if check_for_existing_answers: # check for answers with user_id or group_id prev_answers = Answer.query \ .filter_by( assignment_id=assignment.id, user_id=answer.user_id, group_id=answer.group_id, active=True ) \ .all() # check if there is a previous answer submitted for the student non_draft_answers = [ prev_answer for prev_answer in prev_answers if not prev_answer.draft ] if len(non_draft_answers) > 0: abort( 400, title="Answer Not Submitted", message= "An answer has already been submitted for this assignment by you or on your behalf." ) # check if there is a previous draft answer submitted for the student (soft-delete if present) draft_answers = [ prev_answer for prev_answer in prev_answers if prev_answer.draft ] for draft_answer in draft_answers: draft_answer.active = False # set submission date if answer is being submitted for the first time if not answer.draft and not answer.submission_date: answer.submission_date = datetime.datetime.utcnow() answer.update_attempt(params.get('attempt_uuid'), params.get('attempt_started', None), params.get('attempt_ended', None)) db.session.add(answer) db.session.commit() on_answer_create.send(self, event_name=on_answer_create.name, user=current_user, course_id=course.id, answer=answer, data=marshal( answer, dataformat.get_answer(restrict_user))) if attachment: on_attach_file.send(self, event_name=on_attach_file.name, user=current_user, course_id=course.id, file=attachment, data={ 'answer_id': answer.id, 'file_id': attachment.id }) # update course & assignment grade for user if answer is fully submitted if not answer.draft: if answer.user: assignment.calculate_grade(answer.user) course.calculate_grade(answer.user) elif answer.group: assignment.calculate_group_grade(answer.group) course.calculate_group_grade(answer.group) return marshal(answer, dataformat.get_answer(restrict_user))
def post(self): # login_required when lti_create_user_link not set if not sess.get( 'lti_create_user_link') and not current_user.is_authenticated: return current_app.login_manager.unauthorized() user = User() params = new_user_parser.parse_args() user.student_number = params.get("student_number", None) user.email = params.get("email") user.firstname = params.get("firstname") user.lastname = params.get("lastname") user.displayname = params.get("displayname") email_notification_method = params.get("email_notification_method") check_valid_email_notification_method(email_notification_method) user.email_notification_method = EmailNotificationMethod( email_notification_method) if not current_app.config.get('APP_LOGIN_ENABLED'): # if APP_LOGIN_ENABLED is not enabled, allow blank username and password user.username = None user.password = None else: # else enforce required password and unique username user.password = params.get("password") if user.password == None: abort( 400, title="User Not Saved", message= "A password is required. Please enter a password and try saving again." ) elif len(params.get("password")) < 4: abort( 400, title="User Not Saved", message="The password must be at least 4 characters long.") user.username = params.get("username") if user.username == None: abort( 400, title="User Not Saved", message= "A username is required. Please enter a username and try saving again." ) username_exists = User.query.filter_by( username=user.username).first() if username_exists: abort( 409, title="User Not Saved", message= "Sorry, this username already exists and usernames must be unique in ComPAIR. Please enter another username and try saving again." ) student_number_exists = User.query.filter_by( student_number=user.student_number).first() # if student_number is not left blank and it exists -> 409 error if user.student_number is not None and student_number_exists: abort( 409, title="User Not Saved", message= "Sorry, this student number already exists and student numbers must be unique in ComPAIR. Please enter another number and try saving again." ) # handle lti_create_user_link setup for third party logins if sess.get('lti_create_user_link') and sess.get('LTI'): lti_user = LTIUser.query.get_or_404(sess['lti_user']) lti_user.compair_user = user user.system_role = lti_user.system_role lti_user.update_user_profile() if sess.get('lti_context') and sess.get('lti_user_resource_link'): lti_context = LTIContext.query.get_or_404(sess['lti_context']) lti_user_resource_link = LTIUserResourceLink.query.get_or_404( sess['lti_user_resource_link']) if lti_context.is_linked_to_course(): # create new enrollment new_user_course = UserCourse( user=user, course_id=lti_context.compair_course_id, course_role=lti_user_resource_link.course_role) db.session.add(new_user_course) else: system_role = params.get("system_role") check_valid_system_role(system_role) user.system_role = SystemRole(system_role) require( CREATE, user, title="User Not Saved", message="Sorry, your role does not allow you to save users.") # only students can have student numbers if user.system_role != SystemRole.student: user.student_number = None try: db.session.add(user) db.session.commit() if current_user.is_authenticated: on_user_create.send(self, event_name=on_user_create.name, user=current_user, data=marshal(user, dataformat.get_full_user())) else: on_user_create.send(self, event_name=on_user_create.name, data=marshal(user, dataformat.get_full_user())) except exc.IntegrityError: db.session.rollback() current_app.logger.error("Failed to add new user. Duplicate.") abort( 409, title="User Not Saved", message= "Sorry, this ID already exists and IDs must be unique in ComPAIR. Please try addding another user." ) # handle lti_create_user_link teardown for third party logins if sess.get('lti_create_user_link'): authenticate(user, login_method='LTI') sess.pop('lti_create_user_link') return marshal_user_data(user)
def post(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) if is_user_access_restricted(user): abort( 403, title="User Not Saved", message="Sorry, your role does not allow you to save this user." ) params = existing_user_parser.parse_args() # make sure the user id in the url and the id matches if params['id'] != user_uuid: abort( 400, title="User Not Saved", message= "The user's ID does not match the URL, which is required in order to save the user." ) # only update username if user uses compair login method if user.uses_compair_login: username = params.get("username") if username == None: abort( 400, title="User Not Saved", message= "A username is required. Please enter a username and try saving again." ) username_exists = User.query.filter_by(username=username).first() if username_exists and username_exists.id != user.id: abort( 409, title="User Not Saved", message= "Sorry, this username already exists and usernames must be unique in ComPAIR. Please enter another username and try saving again." ) user.username = username elif allow(MANAGE, user): #admins can optionally set username for users without a username username = params.get("username") if username: username_exists = User.query.filter_by( username=username).first() if username_exists and username_exists.id != user.id: abort( 409, title="User Not Saved", message= "Sorry, this username already exists and usernames must be unique in ComPAIR. Please enter another username and try saving again." ) user.username = username else: user.username = None if allow(MANAGE, user): system_role = params.get("system_role", user.system_role.value) check_valid_system_role(system_role) user.system_role = SystemRole(system_role) if allow(MANAGE, user) or user.id == current_user.id or current_app.config.get( 'EXPOSE_EMAIL_TO_INSTRUCTOR', False): if current_user.system_role != SystemRole.student or current_app.config.get( 'ALLOW_STUDENT_CHANGE_EMAIL'): user.email = params.get("email", user.email) email_notification_method = params.get("email_notification_method") check_valid_email_notification_method(email_notification_method) user.email_notification_method = EmailNotificationMethod( email_notification_method) elif params.get("email") or params.get("email_notification_method"): abort( 400, title="User Not Saved", message= "your role does not allow you to change email settings for this user." ) if current_user.system_role != SystemRole.student or current_app.config.get( 'ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'): # only students should have student numbers if user.system_role == SystemRole.student: student_number = params.get("student_number", user.student_number) student_number_exists = User.query.filter_by( student_number=student_number).first() if student_number is not None and student_number_exists and student_number_exists.id != user.id: abort( 409, title="User Not Saved", message= "Sorry, this student number already exists and student numbers must be unique in ComPAIR. Please enter another number and try saving again." ) else: user.student_number = student_number else: user.student_number = None if current_user.system_role != SystemRole.student or current_app.config.get( 'ALLOW_STUDENT_CHANGE_NAME'): user.firstname = params.get("firstname", user.firstname) user.lastname = params.get("lastname", user.lastname) if current_user.system_role != SystemRole.student or current_app.config.get( 'ALLOW_STUDENT_CHANGE_DISPLAY_NAME'): user.displayname = params.get("displayname", user.displayname) model_changes = get_model_changes(user) try: db.session.commit() on_user_modified.send(self, event_name=on_user_modified.name, user=current_user, data={ 'id': user.id, 'changes': model_changes }) except exc.IntegrityError: db.session.rollback() abort( 409, title="User Not Saved", message= "Sorry, this ID already exists and IDs must be unique in ComPAIR. Please try addding another user." ) return marshal_user_data(user)
def post(self): # login_required when oauth_create_user_link not set if not sess.get('oauth_create_user_link'): if not current_app.login_manager._login_disabled and \ not current_user.is_authenticated: return current_app.login_manager.unauthorized() user = User() params = new_user_parser.parse_args() user.student_number = params.get("student_number", None) user.email = params.get("email") user.firstname = params.get("firstname") user.lastname = params.get("lastname") user.displayname = params.get("displayname") email_notification_method = params.get("email_notification_method") check_valid_email_notification_method(email_notification_method) user.email_notification_method = EmailNotificationMethod( email_notification_method) # if creating a cas user, do not set username or password if sess.get('oauth_create_user_link') and sess.get('LTI') and sess.get( 'CAS_CREATE'): user.username = None user.password = None else: # else enforce required password and unique username user.password = params.get("password") if user.password == None: abort( 400, title="User Not Saved", message= "A password is required. Please enter a password and try saving again." ) user.username = params.get("username") if user.username == None: abort( 400, title="User Not Saved", message= "A username is required. Please enter a username and try saving again." ) username_exists = User.query.filter_by( username=user.username).first() if username_exists: abort( 409, title="User Not Saved", message= "Sorry, this username already exists and usernames must be unique in ComPAIR. Please enter another username and try saving again." ) student_number_exists = User.query.filter_by( student_number=user.student_number).first() # if student_number is not left blank and it exists -> 409 error if user.student_number is not None and student_number_exists: abort( 409, title="User Not Saved", message= "Sorry, this student number already exists and student numbers must be unique in ComPAIR. Please enter another number and try saving again." ) # handle oauth_create_user_link setup for third party logins if sess.get('oauth_create_user_link'): login_method = None if sess.get('LTI'): lti_user = LTIUser.query.get_or_404(sess['lti_user']) lti_user.compair_user = user user.system_role = lti_user.system_role login_method = 'LTI' if sess.get('lti_context') and sess.get( 'lti_user_resource_link'): lti_context = LTIContext.query.get_or_404( sess['lti_context']) lti_user_resource_link = LTIUserResourceLink.query.get_or_404( sess['lti_user_resource_link']) if lti_context.is_linked_to_course(): # create new enrollment new_user_course = UserCourse( user=user, course_id=lti_context.compair_course_id, course_role=lti_user_resource_link.course_role) db.session.add(new_user_course) if sess.get('CAS_CREATE'): thirdpartyuser = ThirdPartyUser( third_party_type=ThirdPartyType.cas, unique_identifier=sess.get('CAS_UNIQUE_IDENTIFIER'), params=sess.get('CAS_PARAMS'), user=user) login_method = ThirdPartyType.cas.value db.session.add(thirdpartyuser) else: system_role = params.get("system_role") check_valid_system_role(system_role) user.system_role = SystemRole(system_role) require( CREATE, user, title="User Not Saved", message="Sorry, your role does not allow you to save users.") # only students can have student numbers if user.system_role != SystemRole.student: user.student_number = None try: db.session.add(user) db.session.commit() if current_user.is_authenticated: on_user_create.send(self, event_name=on_user_create.name, user=current_user, data=marshal(user, dataformat.get_user(False))) else: on_user_create.send(self, event_name=on_user_create.name, data=marshal(user, dataformat.get_user(False))) except exc.IntegrityError: db.session.rollback() current_app.logger.error("Failed to add new user. Duplicate.") abort( 409, title="User Not Saved", message= "Sorry, this ID already exists and IDs must be unique in ComPAIR. Please try addding another user." ) # handle oauth_create_user_link teardown for third party logins if sess.get('oauth_create_user_link'): authenticate(user, login_method=login_method) sess.pop('oauth_create_user_link') if sess.get('CAS_CREATE'): sess.pop('CAS_CREATE') sess.pop('CAS_UNIQUE_IDENTIFIER') sess['CAS_LOGIN'] = True return marshal(user, dataformat.get_user())
def post(self): if not current_app.config.get('DEMO_INSTALLATION', False): abort( 404, title="Demo Accounts Unavailable", message= "Sorry, the system settings do now allow the use of demo accounts." ) params = new_user_demo_parser.parse_args() user = User() user.password = "******" system_role = params.get("system_role") check_valid_system_role(system_role) user.system_role = SystemRole(system_role) user_count = User.query \ .filter_by(system_role=user.system_role) \ .count() user_count += 1 # username while True: if user.system_role == SystemRole.sys_admin: user.username = "******" + str(user_count) elif user.system_role == SystemRole.instructor: user.username = "******" + str(user_count) else: user.username = "******" + str(user_count) username_exists = User.query.filter_by( username=user.username).first() if not username_exists: break else: user_count += 1 if user.system_role == SystemRole.sys_admin: user.firstname = "Admin" user.lastname = str(user_count) user.displayname = "Admin " + str(user_count) elif user.system_role == SystemRole.instructor: user.firstname = "Instructor" user.lastname = str(user_count) user.displayname = "Instructor " + str(user_count) # create new enrollment new_user_course = UserCourse(user=user, course_id=1, course_role=CourseRole.instructor) db.session.add(new_user_course) else: user.firstname = "Student" user.lastname = str(user_count) user.displayname = display_name_generator() while True: user.student_number = random_generator(8, string.digits) student_number_exists = User.query.filter_by( student_number=user.student_number).first() if not student_number_exists: break # create new enrollment new_user_course = UserCourse(user=user, course_id=1, course_role=CourseRole.student) db.session.add(new_user_course) try: db.session.add(user) db.session.commit() on_user_demo_create.send(self, event_name=on_user_demo_create.name, user=current_user, data=marshal(user, dataformat.get_user(False))) except exc.IntegrityError: db.session.rollback() current_app.logger.error("Failed to add new user. Duplicate.") return { 'error': 'A user with the same identifier already exists.' }, 400 authenticate(user, login_method="Demo") return marshal(user, dataformat.get_user())
def post(self): # login_required when lti_create_user_link not set if not sess.get('lti_create_user_link') and not current_user.is_authenticated: return current_app.login_manager.unauthorized() user = User() params = new_user_parser.parse_args() user.student_number = params.get("student_number", None) user.email = params.get("email") user.firstname = params.get("firstname") user.lastname = params.get("lastname") user.displayname = params.get("displayname") email_notification_method = params.get("email_notification_method") check_valid_email_notification_method(email_notification_method) user.email_notification_method = EmailNotificationMethod(email_notification_method) if not current_app.config.get('APP_LOGIN_ENABLED'): # if APP_LOGIN_ENABLED is not enabled, allow blank username and password user.username = None user.password = None else: # else enforce required password and unique username user.password = params.get("password") if user.password == None: abort(400, title="User Not Saved", message="A password is required. Please enter a password and try saving again.") elif len(params.get("password")) < 4: abort(400, title="User Not Saved", message="The password must be at least 4 characters long.") user.username = params.get("username") if user.username == None: abort(400, title="User Not Saved", message="A username is required. Please enter a username and try saving again.") username_exists = User.query.filter_by(username=user.username).first() if username_exists: abort(409, title="User Not Saved", message="Sorry, this username already exists and usernames must be unique in ComPAIR. Please enter another username and try saving again.") student_number_exists = User.query.filter_by(student_number=user.student_number).first() # if student_number is not left blank and it exists -> 409 error if user.student_number is not None and student_number_exists: abort(409, title="User Not Saved", message="Sorry, this student number already exists and student numbers must be unique in ComPAIR. Please enter another number and try saving again.") # handle lti_create_user_link setup for third party logins if sess.get('lti_create_user_link') and sess.get('LTI'): lti_user = LTIUser.query.get_or_404(sess['lti_user']) lti_user.compair_user = user user.system_role = lti_user.system_role lti_user.update_user_profile() if sess.get('lti_context') and sess.get('lti_user_resource_link'): lti_context = LTIContext.query.get_or_404(sess['lti_context']) lti_user_resource_link = LTIUserResourceLink.query.get_or_404(sess['lti_user_resource_link']) if lti_context.is_linked_to_course(): # create new enrollment new_user_course = UserCourse( user=user, course_id=lti_context.compair_course_id, course_role=lti_user_resource_link.course_role ) db.session.add(new_user_course) else: system_role = params.get("system_role") check_valid_system_role(system_role) user.system_role = SystemRole(system_role) require(CREATE, user, title="User Not Saved", message="Sorry, your role does not allow you to save users.") # only students can have student numbers if user.system_role != SystemRole.student: user.student_number = None try: db.session.add(user) db.session.commit() if current_user.is_authenticated: on_user_create.send( self, event_name=on_user_create.name, user=current_user, data=marshal(user, dataformat.get_full_user())) else: on_user_create.send( self, event_name=on_user_create.name, data=marshal(user, dataformat.get_full_user())) except exc.IntegrityError: db.session.rollback() current_app.logger.error("Failed to add new user. Duplicate.") abort(409, title="User Not Saved", message="Sorry, this ID already exists and IDs must be unique in ComPAIR. Please try addding another user.") # handle lti_create_user_link teardown for third party logins if sess.get('lti_create_user_link'): authenticate(user, login_method='LTI') sess.pop('lti_create_user_link') return marshal_user_data(user)
def get(self, course_uuid, assignment_uuid): """ Return a list of answers for a assignment based on search criteria. The list of the answers are paginated. If there is any answers from instructor or TA, their answers will be on top of the list (unless they are comparable). :param course_uuid: course uuid :param assignment_uuid: assignment uuid :return: list of answers """ course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) require(READ, assignment, title="Answers Unavailable", message="Answers are visible only to those enrolled in the course. Please double-check your enrollment in this course.") restrict_user = not allow(MANAGE, assignment) params = answer_list_parser.parse_args() # if assingment has no rank display limit set, restricted users can't force to retreive by rank if params['orderBy'] == 'score' and restrict_user and not assignment.rank_display_limit: abort(400, title="Answers Unavailable", message="Sorry, you cannot cannot see answers by rank for this assignment.") if restrict_user and not assignment.after_comparing: # only the answer from student himself/herself should be returned params['author'] = current_user.uuid # this query could be further optimized by reduction the selected columns query = Answer.query \ .options(joinedload('file')) \ .options(joinedload('user')) \ .options(joinedload('group')) \ .options(joinedload('score')) \ .options(undefer_group('counts')) \ .outerjoin(UserCourse, and_( Answer.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .add_columns( and_( UserCourse != None, UserCourse.course_role.__eq__(CourseRole.instructor), not Answer.comparable ).label("instructor_role"), and_( UserCourse != None, UserCourse.course_role.__eq__(CourseRole.teaching_assistant), not Answer.comparable ).label("ta_role") ) \ .filter(and_( Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False, or_( and_(UserCourse.course_role != CourseRole.dropped, Answer.user_id != None), Answer.group_id != None ) )) \ .order_by(desc('instructor_role'), desc('ta_role')) if params['author']: user = User.get_by_uuid_or_404(params['author']) group = user.get_course_group(course.id) if group: query = query.filter(or_( Answer.user_id == user.id, Answer.group_id == group.id )) else: query = query.filter(Answer.user_id == user.id) elif params['group']: group = Group.get_active_by_uuid_or_404(params['group']) query = query.filter(or_( UserCourse.group_id == group.id, Answer.group_id == group.id )) if params['ids']: query = query.filter(Answer.uuid.in_(params['ids'].split(','))) if params['top']: query = query.filter(Answer.top_answer == True) if params['orderBy'] == 'score': # use outer join to include comparable answers that are not yet compared (for non-restricted users) query = query.outerjoin(AnswerScore) \ .filter(Answer.comparable == True) \ .order_by(AnswerScore.score.desc(), Answer.submission_date.desc(), Answer.created.desc()) if restrict_user: # when orderd by rank, students won't see answers that are not compared (i.e. no score/rank) query = query.filter(AnswerScore.score.isnot(None)) # limit answers up to rank if rank_display_limit is set and current_user is restricted (student) if assignment.rank_display_limit and restrict_user: score_for_rank = AnswerScore.get_score_for_rank(assignment.id, assignment.rank_display_limit) # display answers with score >= score_for_rank if score_for_rank != None: # will get all answers with a score greater than or equal to the score for a given rank # the '- 0.00001' fixes floating point precision problems query = query.filter(AnswerScore.score >= score_for_rank - 0.00001) else: # when ordered by date, non-comparable answers should be on top of the list query = query.order_by(Answer.comparable, Answer.submission_date.desc(), Answer.created.desc()) page = query.paginate(params['page'], params['perPage'], error_out=False) # remove label entities from results page.items = [answer for (answer, instructor_role, ta_role) in page.items] on_answer_list_get.send( self, event_name=on_answer_list_get.name, user=current_user, course_id=course.id, data={'assignment_id': assignment.id}) # only include score/rank info if: # - requesters are non-restricted users (i.e. instructors / TAs); or, # - retrieving answers ordered by score/rank include_score = (not restrict_user) or \ (params['orderBy'] == 'score' and assignment.rank_display_limit) return {"objects": marshal(page.items, dataformat.get_answer(restrict_user, include_score=include_score)), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}
def post(self, course_uuid, assignment_uuid, answer_uuid): """ 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 import_users(import_type, course, users): invalids = [] # invalid entries - eg. invalid # of columns count = 0 # store number of successful enrolments imported_users = [] set_user_passwords = [] # store unique user identifiers - eg. student number - throws error if duplicate in file import_usernames = [] import_student_numbers = [] # store unique user identifiers - eg. student number - throws error if duplicate in file existing_system_usernames = _get_existing_users_by_identifier(import_type, users) existing_system_student_numbers = _get_existing_users_by_student_number(import_type, users) groups = course.groups.all() groups_by_name = {} for group in groups: groups_by_name[group.name] = group # create / update users in file for user_row in users: if len(user_row) < 1: continue # skip empty row user = _parse_user_row(import_type, user_row) # validate unique identifier username = user.get('username') password = user.get('password') #always None for CAS/SAML import, can be None for existing users on ComPAIR import student_number = user.get('student_number') u = existing_system_usernames.get(username, None) if not username: invalids.append({'user': User(username=username), 'message': 'The username is required.'}) continue elif username in import_usernames: invalids.append({'user': User(username=username), 'message': 'This username already exists in the file.'}) continue if u: # overwrite password if user has not logged in yet if u.last_online == None and not password in [None, '*']: set_user_passwords.append((u, password)) else: u = User( username=None, password=None, student_number=user.get('student_number'), firstname=user.get('firstname'), lastname=user.get('lastname'), email=user.get('email') ) if import_type == ThirdPartyType.cas.value or import_type == ThirdPartyType.saml.value: # CAS/SAML login u.third_party_auths.append(ThirdPartyUser( unique_identifier=username, third_party_type=ThirdPartyType(import_type) )) else: # ComPAIR login u.username = username if password in [None, '*']: invalids.append({'user': u, 'message': 'The password is required.'}) continue elif len(password) < 4: invalids.append({'user': u, 'message': 'The password must be at least 4 characters long.'}) continue else: set_user_passwords.append((u, password)) # validate student number (if not None) if student_number: # invalid if already showed up in file if student_number in import_student_numbers: u.username = username invalids.append({'user': u, 'message': 'This student number already exists in the file.'}) continue # invalid if student number already exists in the system elif student_number in existing_system_student_numbers: u.username = username invalids.append({'user': u, 'message': 'This student number already exists in the system.'}) continue u.system_role = SystemRole.student u.displayname = user.get('displayname') if user.get('displayname') else display_name_generator() db.session.add(u) import_usernames.append(username) if student_number: import_student_numbers.append(student_number) imported_users.append( (u, user.get('group')) ) db.session.commit() enroled = UserCourse.query \ .filter_by(course_id=course.id) \ .all() enroled = {e.user_id: e for e in enroled} students = UserCourse.query \ .filter_by( course_id=course.id, course_role=CourseRole.student ) \ .all() students = {s.user_id: s for s in students} # enrol valid users in file for user, group_name in imported_users: enrol = enroled.get(user.id, UserCourse(course_id=course.id, user_id=user.id)) enrol.group = None if group_name: group = groups_by_name.get(group_name) # add new groups if needed if not group: group = Group( course=course, name=group_name ) groups_by_name[group_name] = group db.session.add(group) enrol.group = group # do not overwrite instructor or teaching assistant roles if enrol.course_role not in [CourseRole.instructor, CourseRole.teaching_assistant]: enrol.course_role = CourseRole.student if user.id in students: del students[user.id] count += 1 db.session.add(enrol) db.session.commit() # unenrol users not in file anymore for user_id in students: enrolment = students.get(user_id) # skip users that are already dropped if enrolment.course_role == CourseRole.dropped: continue enrolment.course_role = CourseRole.dropped enrolment.group_id = None db.session.add(enrolment) db.session.commit() # wait until user ids are generated before starting background jobs # perform password update in chunks of 100 chunk_size = 100 chunks = [set_user_passwords[index:index + chunk_size] for index in range(0, len(set_user_passwords), chunk_size)] for chunk in chunks: set_passwords.delay({ user.id: password for (user, password) in chunk }) on_classlist_upload.send( current_app._get_current_object(), event_name=on_classlist_upload.name, user=current_user, course_id=course.id ) return { 'success': count, 'invalids': marshal(invalids, dataformat.get_import_users_results(False)) }
def import_users(import_type, course, users): invalids = [] # invalid entries - eg. invalid # of columns count = 0 # store number of successful enrolments imported_users = [] set_user_passwords = [] # store unique user identifiers - eg. student number - throws error if duplicate in file import_usernames = [] import_student_numbers = [] # store unique user identifiers - eg. student number - throws error if duplicate in file existing_system_usernames = _get_existing_users_by_identifier( import_type, users) existing_system_student_numbers = _get_existing_users_by_student_number( import_type, users) groups = course.groups.all() groups_by_name = {} for group in groups: groups_by_name[group.name] = group # create / update users in file for user_row in users: if len(user_row) < 1: continue # skip empty row user = _parse_user_row(import_type, user_row) # validate unique identifier username = user.get('username') password = user.get( 'password' ) #always None for CAS/SAML import, can be None for existing users on ComPAIR import student_number = user.get('student_number') u = existing_system_usernames.get(username, None) if not username: invalids.append({ 'user': User(username=username), 'message': 'The username is required.' }) continue elif username in import_usernames: invalids.append({ 'user': User(username=username), 'message': 'This username already exists in the file.' }) continue if u: # overwrite password if user has not logged in yet if u.last_online == None and not password in [None, '*']: set_user_passwords.append((u, password)) else: u = User(username=None, password=None, student_number=user.get('student_number'), firstname=user.get('firstname'), lastname=user.get('lastname'), email=user.get('email')) if import_type == ThirdPartyType.cas.value or import_type == ThirdPartyType.saml.value: # CAS/SAML login u.third_party_auths.append( ThirdPartyUser( unique_identifier=username, third_party_type=ThirdPartyType(import_type))) else: # ComPAIR login u.username = username if password in [None, '*']: invalids.append({ 'user': u, 'message': 'The password is required.' }) continue elif len(password) < 4: invalids.append({ 'user': u, 'message': 'The password must be at least 4 characters long.' }) continue else: set_user_passwords.append((u, password)) # validate student number (if not None) if student_number: # invalid if already showed up in file if student_number in import_student_numbers: u.username = username invalids.append({ 'user': u, 'message': 'This student number already exists in the file.' }) continue # invalid if student number already exists in the system elif student_number in existing_system_student_numbers: u.username = username invalids.append({ 'user': u, 'message': 'This student number already exists in the system.' }) continue u.system_role = SystemRole.student u.displayname = user.get('displayname') if user.get( 'displayname') else display_name_generator() db.session.add(u) import_usernames.append(username) if student_number: import_student_numbers.append(student_number) imported_users.append((u, user.get('group'))) db.session.commit() enroled = UserCourse.query \ .filter_by(course_id=course.id) \ .all() enroled = {e.user_id: e for e in enroled} students = UserCourse.query \ .filter_by( course_id=course.id, course_role=CourseRole.student ) \ .all() students = {s.user_id: s for s in students} # enrol valid users in file for user, group_name in imported_users: enrol = enroled.get(user.id, UserCourse(course_id=course.id, user_id=user.id)) enrol.group = None if group_name: group = groups_by_name.get(group_name) # add new groups if needed if not group: group = Group(course=course, name=group_name) groups_by_name[group_name] = group db.session.add(group) enrol.group = group # do not overwrite instructor or teaching assistant roles if enrol.course_role not in [ CourseRole.instructor, CourseRole.teaching_assistant ]: enrol.course_role = CourseRole.student if user.id in students: del students[user.id] count += 1 db.session.add(enrol) db.session.commit() # unenrol users not in file anymore for user_id in students: enrolment = students.get(user_id) # skip users that are already dropped if enrolment.course_role == CourseRole.dropped: continue enrolment.course_role = CourseRole.dropped enrolment.group_id = None db.session.add(enrolment) db.session.commit() # wait until user ids are generated before starting background jobs # perform password update in chunks of 100 chunk_size = 100 chunks = [ set_user_passwords[index:index + chunk_size] for index in range(0, len(set_user_passwords), chunk_size) ] for chunk in chunks: set_passwords.delay({user.id: password for (user, password) in chunk}) on_classlist_upload.send(current_app._get_current_object(), event_name=on_classlist_upload.name, user=current_user, course_id=course.id) return { 'success': count, 'invalids': marshal(invalids, dataformat.get_import_users_results(False)) }
def 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. :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) restrict_user = not allow(MANAGE, assignment) params = answer_list_parser.parse_args() 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('scores')) \ .options(undefer_group('counts')) \ .join(UserCourse, and_( Answer.user_id == UserCourse.user_id, UserCourse.course_id == course.id )) \ .add_columns( UserCourse.course_role.__eq__(CourseRole.instructor).label("instructor_role"), UserCourse.course_role.__eq__(CourseRole.teaching_assistant).label("ta_role") ) \ .filter(and_( Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False, UserCourse.course_role != CourseRole.dropped )) \ .order_by(desc('instructor_role'), desc('ta_role')) if params['author']: user = User.get_by_uuid_or_404(params['author']) query = query.filter(Answer.user_id == user.id) elif params['group']: query = query.filter(UserCourse.group_name == params['group']) 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']: criterion = Criterion.get_active_by_uuid_or_404(params['orderBy']) # order answer ids by one criterion and pagination, in case there are multiple criteria in assignment # does not include answers without a score (Note: ta and instructor never have a score) query = query.join(Score) \ .filter(Score.criterion_id == criterion.id) \ .order_by(Score.score.desc(), Answer.created.desc()) # 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 = Score.get_score_for_rank( assignment.id, criterion.id, assignment.rank_display_limit) # display answers with score >= score_for_rank if score_for_rank != None: # will get all answer 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(Score.score >= score_for_rank - 0.00001) else: query = query.order_by(Answer.created.desc()) page = query.paginate(params['page'], params['perPage']) # 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}) return {"objects": marshal(page.items, dataformat.get_answer(restrict_user)), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}
def get(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) require( MANAGE, User, title="User's Courses Unavailable", message= "Sorry, your system role does not allow you to view courses for this user." ) params = user_id_course_list_parser.parse_args() query = Course.query \ .with_entities(Course, UserCourse) \ .options(joinedload(UserCourse.group)) \ .join(UserCourse, and_( UserCourse.course_id == Course.id, UserCourse.user_id == user.id, )) \ .filter(and_( Course.active == True, UserCourse.course_role != CourseRole.dropped )) if params['search']: search_terms = params['search'].split() for search_term in search_terms: if search_term != "": search = '%' + search_term + '%' query = query.filter( or_(Course.name.like(search), Course.year.like(search), Course.term.like(search))) if params['includeSandbox'] != None: query = query.filter(Course.sandbox == params['includeSandbox']) 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(Course.start_date_order.desc(), Course.name) page = query.paginate(params['page'], params['perPage']) # fix results courses = [] for (_course, _user_course) in page.items: _course.course_role = _user_course.course_role _course.group = _user_course.group _course.group_uuid = _course.group.uuid if _course.group else None courses.append(_course) page.items = courses on_user_course_get.send(self, event_name=on_user_course_get.name, user=user) return { "objects": marshal(page.items, dataformat.get_user_courses()), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page }
def post(self, course_uuid, assignment_uuid, answer_uuid): course = Course.get_active_by_uuid_or_404(course_uuid) assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid) if not assignment.answer_grace and not allow(MANAGE, assignment): abort(403, title="Answer Not Submitted", message="Sorry, the answer deadline has passed. No answers can be submitted after the deadline unless the instructor submits the answer for you.") answer = Answer.get_active_by_uuid_or_404(answer_uuid) old_file = answer.file require(EDIT, answer, title="Answer Not Saved", message="Sorry, your role in this course does not allow you to save this answer.") restrict_user = not allow(MANAGE, assignment) if current_app.config.get('DEMO_INSTALLATION', False): from data.fixtures import DemoDataFixture if assignment.id in DemoDataFixture.DEFAULT_ASSIGNMENT_IDS and answer.user_id in DemoDataFixture.DEFAULT_STUDENT_IDS: abort(400, title="Answer Not Saved", message="Sorry, you cannot edit the default student demo answers.") params = existing_answer_parser.parse_args() # make sure the answer id in the url and the id matches if params['id'] != answer_uuid: abort(400, title="Answer Not Submitted", message="The answer's ID does not match the URL, which is required in order to save the answer.") # modify answer according to new values, preserve original values if values not passed answer.content = params.get("content") user_uuid = params.get("user_id") group_uuid = params.get("group_id") # we allow instructor and TA to submit multiple answers for other users in the class if user_uuid and user_uuid != answer.user_uuid and not allow(MANAGE, answer): abort(400, title="Answer Not Submitted", message="Only instructors and teaching assistants can submit an answer on behalf of another.") if group_uuid and not assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Group answers are not allowed for this assignment.") if group_uuid and group_uuid != answer.group_uuid and not allow(MANAGE, answer): abort(400, title="Answer Not Submitted", message="Only instructors and teaching assistants can submit an answer on behalf of a group.") if group_uuid and user_uuid: abort(400, title="Answer Not Submitted", message="You cannot submit an answer for a user and a group at the same time.") user = User.get_by_uuid_or_404(user_uuid) if user_uuid else answer.user group = Group.get_active_by_uuid_or_404(group_uuid) if group_uuid else answer.group check_for_existing_answers = False if group and assignment.enable_group_answers: if group.course_id != course.id: abort(400, title="Answer Not Submitted", message="Group answers can be submitted to courses they belong in.") answer.user_id = None answer.group_id = group.id answer.comparable = True check_for_existing_answers = True else: answer.user_id = user.id answer.group_id = None course_role = User.get_user_course_role(answer.user_id, course.id) # only system admin can add answers for themselves to a class without being enrolled in it # required for managing comparison examples as system admin if (not course_role or course_role == CourseRole.dropped) and current_user.system_role != SystemRole.sys_admin: abort(400, title="Answer Not Submitted", message="Answers can be submitted only by those enrolled in the course. Please double-check your enrollment in this course.") if course_role == CourseRole.student and assignment.enable_group_answers: abort(400, title="Answer Not Submitted", message="Students can only submit group answers for this assignment.") # we allow instructor and TA to submit multiple answers for their own, # but not for student. Each student can only have one answer. if course_role and course_role == CourseRole.student: check_for_existing_answers = True answer.comparable = True else: # instructor / TA / Sys Admin can mark the answer as non-comparable, unless the answer is for a student answer.comparable = params.get("comparable") if check_for_existing_answers: # check for answers with user_id or group_id prev_answers = Answer.query \ .filter_by( assignment_id=assignment.id, user_id=answer.user_id, group_id=answer.group_id, active=True ) \ .filter(Answer.id != answer.id) \ .all() # check if there is a previous answer submitted for the student non_draft_answers = [prev_answer for prev_answer in prev_answers if not prev_answer.draft] if len(non_draft_answers) > 0: abort(400, title="Answer Not Submitted", message="An answer has already been submitted for this assignment by you or on your behalf.") # check if there is a previous draft answer submitted for the student (soft-delete if present) draft_answers = [prev_answer for prev_answer in prev_answers if prev_answer.draft] for draft_answer in draft_answers: draft_answer.active = False # can only change draft status while a draft if answer.draft: answer.draft = params.get("draft") file_uuid = params.get('file_id') attachment=None if file_uuid: attachment = File.get_by_uuid_or_404(file_uuid) answer.file_id = attachment.id else: answer.file_id = None # non-drafts must have content if not answer.draft and not answer.content and not file_uuid: abort(400, title="Answer Not Submitted", message="Please provide content in the text editor or upload a file and try submitting again.") # set submission date if answer is being submitted for the first time if not answer.draft and not answer.submission_date: answer.submission_date = datetime.datetime.utcnow() answer.update_attempt( params.get('attempt_uuid'), params.get('attempt_started', None), params.get('attempt_ended', None) ) model_changes = get_model_changes(answer) db.session.add(answer) db.session.commit() on_answer_modified.send( self, event_name=on_answer_modified.name, user=current_user, course_id=course.id, answer=answer, data=model_changes) if old_file and (not attachment or old_file.id != attachment.id): on_detach_file.send( self, event_name=on_detach_file.name, user=current_user, course_id=course.id, file=old_file, answer=answer, data={'answer_id': answer.id, 'file_id': old_file.id}) if attachment and (not old_file or old_file.id != attachment.id): on_attach_file.send( self, event_name=on_attach_file.name, user=current_user, course_id=course.id, file=attachment, data={'answer_id': answer.id, 'file_id': attachment.id}) # update course & assignment grade for user if answer is fully submitted if not answer.draft: if answer.user: assignment.calculate_grade(answer.user) course.calculate_grade(answer.user) elif answer.group: assignment.calculate_group_grade(answer.group) course.calculate_group_grade(answer.group) return marshal(answer, dataformat.get_answer(restrict_user))
def post(self, course_uuid, 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, 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): if not current_app.config.get('DEMO_INSTALLATION', False): abort(404, title="Demo Accounts Unavailable", message="Sorry, the system settings do now allow the use of demo accounts.") params = new_user_demo_parser.parse_args() user = User() user.password = "******" system_role = params.get("system_role") check_valid_system_role(system_role) user.system_role = SystemRole(system_role) user_count = User.query \ .filter_by(system_role=user.system_role) \ .count() user_count += 1 # username while True: if user.system_role == SystemRole.sys_admin: user.username="******"+str(user_count) elif user.system_role == SystemRole.instructor: user.username="******"+str(user_count) else: user.username="******"+str(user_count) username_exists = User.query.filter_by(username=user.username).first() if not username_exists: break else: user_count+=1 if user.system_role == SystemRole.sys_admin: user.firstname = "Admin" user.lastname = str(user_count) user.displayname = "Admin "+str(user_count) elif user.system_role == SystemRole.instructor: user.firstname = "Instructor" user.lastname = str(user_count) user.displayname = "Instructor "+str(user_count) # create new enrollment new_user_course = UserCourse( user=user, course_id=1, course_role=CourseRole.instructor ) db.session.add(new_user_course) else: user.firstname = "Student" user.lastname = str(user_count) user.displayname = display_name_generator() while True: user.student_number = random_generator(8, string.digits) student_number_exists = User.query.filter_by(student_number=user.student_number).first() if not student_number_exists: break # create new enrollment new_user_course = UserCourse( user=user, course_id=1, course_role=CourseRole.student ) db.session.add(new_user_course) try: db.session.add(user) db.session.commit() on_user_demo_create.send( self, event_name=on_user_demo_create.name, user=current_user, data=marshal(user, dataformat.get_full_user())) except exc.IntegrityError: db.session.rollback() current_app.logger.error("Failed to add new user. Duplicate.") return {'error': 'A user with the same identifier already exists.'}, 400 authenticate(user, login_method="Demo") return marshal(user, dataformat.get_full_user())
def get(self, user_uuid): user = User.get_by_uuid_or_404(user_uuid) require(MANAGE, User, title="User's Courses Unavailable", message="Sorry, your system role does not allow you to view courses for this user.") params = user_id_course_list_parser.parse_args() query = Course.query \ .with_entities(Course, UserCourse) \ .options(joinedload(UserCourse.group)) \ .join(UserCourse, and_( UserCourse.course_id == Course.id, UserCourse.user_id == user.id, )) \ .filter(and_( Course.active == True, UserCourse.course_role != CourseRole.dropped )) if params['search']: search_terms = params['search'].split() for search_term in search_terms: if search_term != "": search = '%'+search_term+'%' query = query.filter(or_( Course.name.like(search), Course.year.like(search), Course.term.like(search) )) if params['includeSandbox'] != None: query = query.filter( Course.sandbox == params['includeSandbox'] ) 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(Course.start_date_order.desc(), Course.name) page = query.paginate(params['page'], params['perPage']) # fix results courses = [] for (_course, _user_course) in page.items: _course.course_role = _user_course.course_role _course.group = _user_course.group _course.group_uuid = _course.group.uuid if _course.group else None courses.append(_course) page.items = courses on_user_course_get.send( self, event_name=on_user_course_get.name, user=user) return {"objects": marshal(page.items, dataformat.get_user_courses()), "page": page.page, "pages": page.pages, "total": page.total, "per_page": page.per_page}