Beispiel #1
0
    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}
Beispiel #2
0
    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)
Beispiel #3
0
    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
Beispiel #4
0
    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
Beispiel #5
0
    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)
Beispiel #6
0
    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)))
Beispiel #7
0
    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())
Beispiel #8
0
    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}
Beispiel #9
0
    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}
Beispiel #10
0
    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 }
Beispiel #11
0
    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())}
Beispiel #12
0
    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 }
Beispiel #13
0
    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
Beispiel #14
0
    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())}
Beispiel #15
0
    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
        }
Beispiel #16
0
    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)) }
Beispiel #17
0
    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)
Beispiel #18
0
    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))
Beispiel #19
0
    def post(self, user_uuid):
        user = User.get_by_uuid_or_404(user_uuid)

        if is_user_access_restricted(user):
            return {'error': "Sorry, you don't have permission for this action."}, 403

        params = existing_user_parser.parse_args()

        # make sure the user id in the url and the id matches
        if params['id'] != user_uuid:
            return {"error": "User id does not match URL."}, 400

        # only update username if user uses compair login method
        if user.uses_compair_login:
            username = params.get("username")
            if username == None:
                return {"error": "Missing required parameter: username."}, 400
            username_exists = User.query.filter_by(username=username).first()
            if username_exists and username_exists.id != user.id:
                return {"error": "This username already exists. Please pick another."}, 409

            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:
                    return {"error": "This username already exists. Please pick another."}, 409
            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)

        # 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:
                return {"error": "This student number already exists. Please pick another."}, 409
            else:
                user.student_number = student_number
        else:
            user.student_number = None

        user.firstname = params.get("firstname", user.firstname)
        user.lastname = params.get("lastname", user.lastname)
        user.displayname = params.get("displayname", user.displayname)

        user.email = params.get("email", user.email)
        changes = get_model_changes(user)

        restrict_user = not allow(EDIT, user)

        try:
            db.session.commit()
            on_user_modified.send(
                self,
                event_name=on_user_modified.name,
                user=current_user,
                data={'id': user.id, 'changes': changes})
        except exc.IntegrityError:
            db.session.rollback()
            current_app.logger.error("Failed to edit user. Duplicate.")
            return {'error': 'A user with the same identifier already exists.'}, 409

        return marshal(user, dataformat.get_user(restrict_user))
Beispiel #20
0
    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')
        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")
        # 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 user_uuid:
            user = User.get_by_uuid_or_404(user_uuid)
            answer.user_id = user.id
        else:
            answer.user_id = current_user.id

        user_course = UserCourse.query \
            .filter_by(
                course_id=course.id,
                user_id=answer.user_id
            ) \
            .one_or_none()

        # we allow instructor and TA to submit multiple answers for their own,
        # but not for student. Each student can only have one answer.
        instructors_and_tas = [CourseRole.instructor.value, CourseRole.teaching_assistant.value]
        if user_course == None:
            # 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 current_user.id != answer.user_id or 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.")

        elif user_course.course_role.value not in instructors_and_tas:
            # check if there is a previous answer submitted for the student
            prev_answer = Answer.query. \
                filter_by(
                    assignment_id=assignment.id,
                    user_id=answer.user_id,
                    active=True
                ). \
                first()
            if prev_answer:
                abort(400, title="Answer Not Submitted", message="An answer has already been submitted for this assignment by you or on your behalf.")

        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,
            data=marshal(answer, dataformat.get_answer(restrict_user)))

        # 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))
Beispiel #21
0
    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):
            return {'error': answer_deadline_message}, 403

        require(CREATE, Answer(course_id=course.id))
        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')
        if file_uuid:
            uploaded_file = File.get_by_uuid_or_404(file_uuid)
            answer.file_id = uploaded_file.id
        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

        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 not allow(MANAGE, Answer(course_id=course.id)):
            return {"error": "Only instructors and teaching assistants can submit an answer on behalf of another user."}, 400

        if user_uuid:
            user = User.get_by_uuid_or_404(user_uuid)
            answer.user_id = user.id
        else:
            answer.user_id = current_user.id

        user_course = UserCourse.query \
            .filter_by(
                course_id=course.id,
                user_id=answer.user_id
            ) \
            .one_or_none()

        # we allow instructor and TA to submit multiple answers for their own,
        # but not for student. Each student can only have one answer.
        instructors_and_tas = [CourseRole.instructor.value, CourseRole.teaching_assistant.value]
        if user_course == None:
            # 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 current_user.id != answer.user_id or current_user.system_role != SystemRole.sys_admin:
                return {"error": "You are not enrolled in the course."}, 400

        elif user_course.course_role.value not in instructors_and_tas:
            # check if there is a previous answer submitted for the student
            prev_answer = Answer.query. \
                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

        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,
            data=marshal(answer, dataformat.get_answer(restrict_user)))

        # 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))
Beispiel #22
0
    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)
        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 Saved", 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")
        # 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:
                abort(400, title="Answer Not Saved",
                    message="Only instructors and teaching assistants can update an answer on behalf of another.")
            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:
                    abort(400, title="Answer Not Saved", message="An answer has already been submitted for this assignment by you or on your behalf.")

        # 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:
            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.")

        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))
Beispiel #23
0
    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}
Beispiel #24
0
    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))
Beispiel #25
0
    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}
Beispiel #26
0
    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)
Beispiel #27
0
    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
        }
Beispiel #28
0
    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}
Beispiel #29
0
    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}
Beispiel #30
0
    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))
Beispiel #31
0
    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))