Exemple #1
0
    def get(self, course_uuid, assignment_uuid, answer_uuid,
            answer_comment_uuid):
        """
        Get an answer comment
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)
        answer_comment = AnswerComment.get_active_by_uuid_or_404(
            answer_comment_uuid)
        require(
            READ,
            answer_comment,
            title="Feedback Unavailable",
            message=
            "Sorry, your role in this course does not allow you to view this feedback."
        )

        restrict_user = not allow(MANAGE, assignment)

        restrict_user = not allow(MANAGE, assignment)

        on_answer_comment_get.send(self,
                                   event_name=on_answer_comment_get.name,
                                   user=current_user,
                                   course_id=course.id,
                                   data={
                                       'assignment_id': assignment.id,
                                       'answer_id': answer.id,
                                       'answer_comment_id': answer_comment.id
                                   })

        return marshal(answer_comment,
                       dataformat.get_answer_comment(restrict_user))
Exemple #2
0
def output_csv(data, code, headers=None):
    fieldnames = [
        'username', 'student_number', 'firstname', 'lastname', 'displayname',
        'group_name'
    ]

    if allow(MANAGE, User) or current_app.config.get(
            'EXPOSE_EMAIL_TO_INSTRUCTOR', False):
        fieldnames.insert(4, 'email')

    if allow(MANAGE, User) or current_app.config.get(
            'EXPOSE_THIRD_PARTY_USERNAMES_TO_INSTRUCTOR', False):
        if current_app.config.get('CAS_LOGIN_ENABLED'):
            fieldnames.insert(1, 'cas_username')
        if current_app.config.get('SAML_LOGIN_ENABLED'):
            fieldnames.insert(1, 'saml_username')

    csv_buffer = BytesIO()
    writer = csv.DictWriter(csv_buffer,
                            fieldnames=fieldnames,
                            extrasaction='ignore')
    writer.writeheader()

    if 'objects' in data:
        writer.writerows(data['objects'])

    response = make_response(csv_buffer.getvalue(), code)
    response.headers.extend(headers or {})
    response.headers[
        'Content-Disposition'] = 'attachment;filename=classlist.csv'
    return response
Exemple #3
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)
Exemple #4
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(
            READ,
            course,
            title="Instructors Unavailable",
            message=
            "Instructors can only be seen here by those enrolled in the course. Please double-check your enrollment in this course."
        )
        restrict_user = not allow(MANAGE, course)

        instructors = User.query \
            .with_entities(User, UserCourse) \
            .join(UserCourse, UserCourse.user_id == User.id) \
            .filter(
                UserCourse.course_id == course.id,
                or_(
                    UserCourse.course_role == CourseRole.instructor,
                    UserCourse.course_role == CourseRole.teaching_assistant
                )
            ) \
            .order_by(User.lastname, User.firstname) \
            .all()

        users = []
        user_course = UserCourse(course_id=course.id)
        for u in instructors:
            if allow(READ, user_course):
                users.append({
                    'id': u.User.uuid,
                    'name': u.User.fullname_sortable,
                    'group_id': u.UserCourse.group_id,
                    'role': u.UserCourse.course_role.value
                })
            else:
                users.append({
                    'id': u.User.uuid,
                    'name': u.User.displayname,
                    'group_id': u.UserCourse.group_id,
                    'role': u.UserCourse.course_role.value
                })

        on_classlist_instructor.send(self,
                                     event_name=on_classlist_instructor.name,
                                     user=current_user,
                                     course_id=course.id)

        return {'objects': users}
Exemple #5
0
    def get(self):
        params = user_course_list_parser.parse_args()

        # Note, start and end dates are optional so default sort is by start_date (course.start_date or min assignment start date), then name
        query = Course.query \
            .filter_by(active=True) \
            .order_by(Course.start_date_order.desc(), Course.name) \

        # we want to list user linked courses only, so only check the association table
        if not allow(MANAGE, Course):
            query = query.join(UserCourse) \
                .filter(and_(
                    UserCourse.user_id == current_user.id,
                    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['period'] != None:
            now = dateutil.parser.parse(datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat())
            if params['period'] == 'upcoming':
                query = query.filter(
                    Course.start_date > now
                )
            elif params['period'] == 'active':
                query = query.filter(and_(
                    or_(Course.start_date == None, Course.start_date <= now),
                    or_(Course.end_date == None, Course.end_date >= now),
                ))
            elif params['period'] == 'past':
                query = query.filter(
                    Course.end_date < now
                )

        page = query.paginate(params['page'], params['perPage'])

        # TODO REMOVE COURSES WHERE COURSE IS UNAVAILABLE?

        on_user_course_get.send(
            self,
            event_name=on_user_course_get.name,
            user=current_user)

        return {"objects": marshal(page.items, dataformat.get_course()),
                "page": page.page, "pages": page.pages,
                "total": page.total, "per_page": page.per_page}
Exemple #6
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,
            assignment,
            title="Help Comments Unavailable",
            message=
            "Help comments can be seen only by those enrolled in the course. Please double-check your enrollment in this course."
        )
        restrict_user = not allow(MANAGE, assignment)

        assignment_comments = AssignmentComment.query \
            .filter_by(
                course_id=course.id,
                assignment_id=assignment.id,
                active=True
            ) \
            .order_by(AssignmentComment.created.asc()).all()

        on_assignment_comment_list_get.send(
            self,
            event_name=on_assignment_comment_list_get.name,
            user=current_user,
            course_id=course.id,
            data={'assignment_id': assignment.id})

        return {
            "objects":
            marshal(assignment_comments,
                    dataformat.get_assignment_comment(restrict_user))
        }
Exemple #7
0
    def get(self, course_uuid, assignment_uuid, answer_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        answer = Answer.get_active_by_uuid_or_404(
            answer_uuid, joinedloads=['file', 'user', 'group', 'score'])
        require(
            READ,
            answer,
            title="Answer Unavailable",
            message=
            "Sorry, your role in this course does not allow you to view this answer."
        )
        restrict_user = not allow(MANAGE, assignment)

        on_answer_get.send(self,
                           event_name=on_answer_get.name,
                           user=current_user,
                           course_id=course.id,
                           data={
                               'assignment_id': assignment.id,
                               'answer_id': answer.id
                           })

        # don't include score/rank unless the user is non-restricted
        include_score = not restrict_user

        return marshal(
            answer,
            dataformat.get_answer(restrict_user, include_score=include_score))
Exemple #8
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, course,
            title="Instructors Unavailable",
            message="Instructors can only be seen here by those enrolled in the course. Please double-check your enrollment in this course.")
        restrict_user = not allow(MANAGE, course)

        instructors = User.query \
            .with_entities(User, UserCourse) \
            .join(UserCourse, UserCourse.user_id == User.id) \
            .filter(
                UserCourse.course_id == course.id,
                or_(
                    UserCourse.course_role == CourseRole.instructor,
                    UserCourse.course_role == CourseRole.teaching_assistant
                )
            ) \
            .order_by(User.lastname, User.firstname) \
            .all()

        users = []
        user_course = UserCourse(course_id=course.id)
        for u in instructors:
            if allow(READ, user_course):
                users.append({
                    'id': u.User.uuid,
                    'name': u.User.fullname_sortable,
                    'group_id': u.UserCourse.group_id,
                    'role': u.UserCourse.course_role.value
                })
            else:
                users.append({
                    'id': u.User.uuid,
                    'name': u.User.displayname,
                    'group_id': u.UserCourse.group_id,
                    'role': u.UserCourse.course_role.value
                })

        on_classlist_instructor.send(
            self,
            event_name=on_classlist_instructor.name,
            user=current_user,
            course_id=course.id
        )

        return { 'objects': users }
Exemple #9
0
    def get(self):
        if allow(MANAGE, Course()):
            courses = Course.query.filter_by(active=True).all()
        else:
            courses = []
            for user_course in current_user.user_courses:
                if user_course.course.active and allow(
                        MANAGE, Assignment(course_id=user_course.course_id)):
                    courses.append(user_course.course)

        course_list = [{'id': c.uuid, 'name': c.name} for c in courses]

        on_teaching_course_get.send(self,
                                    event_name=on_teaching_course_get.name,
                                    user=current_user)

        return {'courses': course_list}
Exemple #10
0
    def get(self):
        params = user_course_list_parser.parse_args()

        # Note, start and end dates are optional so default sort is by start_date (course.start_date or min assignment start date), then name
        query = Course.query \
            .filter_by(active=True) \
            .order_by(Course.start_date_order.desc(), Course.name) \

        # we want to list user linked courses only, so only check the association table
        if not allow(MANAGE, Course):
            query = query.join(UserCourse) \
                .filter(and_(
                    UserCourse.user_id == current_user.id,
                    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['period'] != None:
            now = dateutil.parser.parse(datetime.datetime.utcnow().replace(
                tzinfo=pytz.utc).isoformat())
            if params['period'] == 'upcoming':
                query = query.filter(Course.start_date > now)
            elif params['period'] == 'active':
                query = query.filter(
                    and_(
                        or_(Course.start_date == None,
                            Course.start_date <= now),
                        or_(Course.end_date == None, Course.end_date >= now),
                    ))
            elif params['period'] == 'past':
                query = query.filter(Course.end_date < now)

        page = query.paginate(params['page'], params['perPage'])

        # TODO REMOVE COURSES WHERE COURSE IS UNAVAILABLE?

        on_user_course_get.send(self,
                                event_name=on_user_course_get.name,
                                user=current_user)

        return {
            "objects": marshal(page.items, dataformat.get_course()),
            "page": page.page,
            "pages": page.pages,
            "total": page.total,
            "per_page": page.per_page
        }
Exemple #11
0
def marshal_user_data(user):
    if impersonation.is_impersonating() and current_user.id == user.id:
        # when retrieving the profile of the student being impersonated,
        # don't include full profile (i.e. no email)
        return marshal(user, dataformat.get_user(False))
    elif allow(MANAGE, user) or current_user.id == user.id:
        return marshal(user, dataformat.get_full_user())
    else:
        return marshal(user, dataformat.get_user(is_user_access_restricted(user)))
Exemple #12
0
    def get(self):
        if allow(MANAGE, Course()):
            courses = Course.query.filter_by(active=True).all()
        else:
            courses = []
            for user_course in current_user.user_courses:
                if user_course.course.active and allow(MANAGE, Assignment(course_id=user_course.course_id)):
                    courses.append(user_course.course)

        course_list = [{'id': c.uuid, 'name': c.name} for c in courses]

        on_teaching_course_get.send(
            self,
            event_name=on_teaching_course_get.name,
            user=current_user
        )

        return {'courses': course_list}
Exemple #13
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, assignment)

        now = datetime.datetime.utcnow()
        if assignment.answer_start and not allow(MANAGE, assignment) and not (assignment.answer_start <= now):
            return {"error": "The assignment is unavailable!"}, 403
        restrict_user = not allow(MANAGE, assignment)

        on_assignment_get.send(
            self,
            event_name=on_assignment_get.name,
            user=current_user,
            course_id=course.id,
            data={'id': assignment.id})

        return marshal(assignment, dataformat.get_assignment(restrict_user))
Exemple #14
0
    def get(self, course_uuid, assignment_uuid):
        """
        Get answers submitted to the assignment submitted by current user

        :param course_uuid:
        :param assignment_uuid:
        :return: answers
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        require(
            READ,
            Answer(user_id=current_user.id),
            title="Answers Unavailable",
            message=
            "Sorry, your role in this course does not allow you to view answers for this assignment."
        )
        restrict_user = not allow(MANAGE, assignment)

        params = user_answer_list_parser.parse_args()

        query = Answer.query \
            .options(joinedload('comments')) \
            .options(joinedload('file')) \
            .options(joinedload('user')) \
            .options(joinedload('group')) \
            .options(joinedload('score')) \
            .filter_by(
                active=True,
                assignment_id=assignment.id,
                course_id=course.id,
                draft=params.get('draft')
            )

        # get group and individual answers for user if applicable
        group = current_user.get_course_group(course.id)
        if group:
            query = query.filter(
                or_(Answer.user_id == current_user.id,
                    Answer.group_id == group.id))
        # get just individual answers for user
        else:
            query = query.filter(Answer.user_id == current_user.id)

        answers = query.all()

        on_user_answer_get.send(self,
                                event_name=on_user_answer_get.name,
                                user=current_user,
                                course_id=course.id,
                                data={'assignment_id': assignment.id})

        return {
            "objects": marshal(answers, dataformat.get_answer(restrict_user))
        }
Exemple #15
0
def marshal_user_data(user):
    if impersonation.is_impersonating() and current_user.id == user.id:
        # when retrieving the profile of the student being impersonated,
        # don't include full profile (i.e. no email)
        return marshal(user, dataformat.get_user(False))
    elif allow(MANAGE, user) or current_user.id == user.id:
        return marshal(user, dataformat.get_full_user())
    else:
        return marshal(user,
                       dataformat.get_user(is_user_access_restricted(user)))
Exemple #16
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, assignment,
            title="Assignment Unavailable",
            message="Assignments can be saved only by those enrolled in the course. Please double-check your enrollment in this course.")

        now = datetime.datetime.utcnow()
        if assignment.answer_start and not allow(MANAGE, assignment) and not (assignment.answer_start <= now):
            abort(403, title="Assignment Unavailable", message="This assignment is not yet open to students. Please check back after the start date the instructor has set.")
        restrict_user = not allow(MANAGE, assignment)

        on_assignment_get.send(
            self,
            event_name=on_assignment_get.name,
            user=current_user,
            course_id=course.id,
            data={'id': assignment.id})

        return marshal(assignment, dataformat.get_assignment(restrict_user))
Exemple #17
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, assignment,
            title="Assignment Unavailable",
            message="Assignments can be saved only by those enrolled in the course. Please double-check your enrollment in this course.")

        now = datetime.datetime.utcnow()
        if assignment.answer_start and not allow(MANAGE, assignment) and not (assignment.answer_start <= now):
            abort(403, title="Assignment Unavailable", message="This assignment is not yet open to students. Please check back after the start date the instructor has set.")
        restrict_user = not allow(MANAGE, assignment)

        on_assignment_get.send(
            self,
            event_name=on_assignment_get.name,
            user=current_user,
            course_id=course.id,
            data={'id': assignment.id})

        return marshal(assignment, dataformat.get_assignment(restrict_user))
Exemple #18
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}
Exemple #19
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, course)
        restrict_user = not allow(MANAGE, course)

        students = User.query \
            .with_entities(User, UserCourse.group_name) \
            .join(UserCourse, UserCourse.user_id == User.id) \
            .filter(
                UserCourse.course_id == course.id,
                UserCourse.course_role == CourseRole.student
            ) \
            .all()

        users = []
        user_course = UserCourse(course_id=course.id)
        for u in students:
            if allow(READ, user_course):
                users.append({
                    'id': u.User.uuid,
                    'name': u.User.fullname if u.User.fullname else u.User.displayname,
                    'group_name': u.group_name
                })
            else:
                name = u.User.displayname
                if u.User.id == current_user.id:
                    name += ' (You)'
                users.append({
                    'id': u.User.uuid,
                    'name': name,
                    'group_name': u.group_name
                })

        on_classlist_student.send(
            self,
            event_name=on_classlist_student.name,
            user=current_user,
            course_id=course.id
        )

        return { 'objects': users }
Exemple #20
0
    def get(self, course_uuid, assignment_uuid):
        """
        Get answers submitted to the assignment submitted by current user

        :param course_uuid:
        :param assignment_uuid:
        :return: answers
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        require(READ, Answer(user_id=current_user.id),
            title="Answers Unavailable",
            message="Sorry, your role in this course does not allow you to view answers for this assignment.")
        restrict_user = not allow(MANAGE, assignment)

        params = user_answer_list_parser.parse_args()

        query = Answer.query \
            .options(joinedload('comments')) \
            .options(joinedload('file')) \
            .options(joinedload('user')) \
            .options(joinedload('group')) \
            .options(joinedload('score')) \
            .filter_by(
                active=True,
                assignment_id=assignment.id,
                course_id=course.id,
                draft=params.get('draft')
            )

        # get group and individual answers for user if applicable
        group = current_user.get_course_group(course.id)
        if group:
            query = query.filter(or_(
                Answer.user_id == current_user.id,
                Answer.group_id == group.id
            ))
        # get just individual answers for user
        else:
            query = query.filter(Answer.user_id == current_user.id)

        answers = query.all()

        on_user_answer_get.send(
            self,
            event_name=on_user_answer_get.name,
            user=current_user,
            course_id=course.id,
            data={'assignment_id': assignment.id})

        return {"objects": marshal(answers, dataformat.get_answer(restrict_user))}
Exemple #21
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, UserCourse(course_id=course.id))
        restrict_user = not allow(READ, USER_IDENTITY)

        # expire current_user from the session. When loading classlist from database, if the
        # user is already in the session, e.g. instructor for the course, the User.user_courses
        # is not loaded from the query below, but from session. In this case, if user has more
        # than one course, User.user_courses will return multiple row. Thus violating the
        # course_role constrain.
        db.session.expire(current_user)

        users = User.query \
            .join(UserCourse, UserCourse.user_id == User.id) \
            .add_columns(UserCourse.course_role, UserCourse.group_name) \
            .filter(and_(
                UserCourse.course_id == course.id,
                UserCourse.course_role != CourseRole.dropped
            )) \
            .order_by(User.firstname) \
            .all()

        if not restrict_user:
            user_ids = [_user.id for (_user, _course_role, _group_name) in users]
            third_party_auths = ThirdPartyUser.query \
                .filter(and_(
                    ThirdPartyUser.user_id.in_(user_ids),
                    ThirdPartyUser.third_party_type == ThirdPartyType.cas
                )) \
                .all()

        class_list = []
        for (_user, _course_role, _group_name) in users:
            _user.course_role = _course_role
            _user.group_name = _group_name

            if not restrict_user:
                third_party_auth = next(
                    (third_party_auth for third_party_auth in third_party_auths if third_party_auth.user_id == _user.id),
                    None
                )
                _user.cas_username = third_party_auth.unique_identifier if third_party_auth else None

            class_list.append(_user)

        on_classlist_get.send(
            self,
            event_name=on_classlist_get.name,
            user=current_user,
            course_id=course.id)

        return {'objects': marshal(class_list, dataformat.get_users_in_course(restrict_user=restrict_user))}
Exemple #22
0
    def post(self, course_uuid, assignment_uuid, answer_uuid):
        """
        Mark an answer as inappropriate or incomplete to instructors
        :param course_uuid:
        :param assignment_uuid:
        :param answer_uuid:
        :return: marked answer
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)

        require(READ, answer,
            title="Answer Not Flagged",
            message="Sorry, your role in this course does not allow you to flag answers.")
        restrict_user = not allow(MANAGE, answer)

        # anyone can flag an answer, but only the original flagger or someone who can manage
        # the answer can unflag it
        if answer.flagged and answer.flagger_user_id != current_user.id and \
                not allow(MANAGE, answer):
            abort(400, title="Answer Not Updated", message="Sorry, your role in this course does not allow you to unflag answers.")

        params = flag_parser.parse_args()
        answer.flagged = params['flagged']
        answer.flagger_user_id = current_user.id
        db.session.add(answer)
        db.session.commit()

        on_answer_flag.send(
            self,
            event_name=on_answer_flag.name,
            user=current_user,
            course_id=course.id,
            assignment_id=assignment.id,
            answer=answer,
            data={'answer_id': answer.id, 'flag': answer.flagged})

        return marshal(answer, dataformat.get_answer(restrict_user))
Exemple #23
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}
Exemple #24
0
def output_csv(data, code, headers=None):
    fieldnames = ['username', 'student_number', 'firstname', 'lastname', 'displayname', 'group_name']

    if allow(MANAGE, User) or current_app.config.get('EXPOSE_EMAIL_TO_INSTRUCTOR', False):
        fieldnames.insert(4, 'email')

    if allow(MANAGE, User) or current_app.config.get('EXPOSE_THIRD_PARTY_USERNAMES_TO_INSTRUCTOR', False):
        if current_app.config.get('CAS_LOGIN_ENABLED'):
            fieldnames.insert(1, 'cas_username')
        if current_app.config.get('SAML_LOGIN_ENABLED'):
            fieldnames.insert(1, 'saml_username')

    csv_buffer = BytesIO()
    writer = csv.DictWriter(csv_buffer, fieldnames=fieldnames, extrasaction='ignore')
    writer.writeheader()

    if 'objects' in data:
        writer.writerows(data['objects'])

    response = make_response(csv_buffer.getvalue(), code)
    response.headers.extend(headers or {})
    response.headers['Content-Disposition'] = 'attachment;filename=classlist.csv'
    return response
Exemple #25
0
    def post(self, course_uuid, assignment_uuid, answer_uuid):
        """
        Mark an answer as inappropriate or incomplete to instructors
        :param course_uuid:
        :param assignment_uuid:
        :param answer_uuid:
        :return: marked answer
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)

        require(READ, answer)
        restrict_user = not allow(MANAGE, answer)

        # anyone can flag an answer, but only the original flagger or someone who can manage
        # the answer can unflag it
        if answer.flagged and answer.flagger_user_id != current_user.id and \
                not allow(MANAGE, answer):
            return {"error": "You do not have permission to unflag this answer."}, 400

        params = flag_parser.parse_args()
        answer.flagged = params['flagged']
        answer.flagger_user_id = current_user.id
        db.session.add(answer)
        db.session.commit()

        on_answer_flag.send(
            self,
            event_name=on_answer_flag.name,
            user=current_user,
            course_id=course.id,
            assignment_id=assignment.id,
            answer=answer,
            data={'answer_id': answer.id, 'flag': answer.flagged})

        return marshal(answer, dataformat.get_answer(restrict_user))
Exemple #26
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)
Exemple #27
0
    def get(self, course_uuid, assignment_uuid, answer_uuid, answer_comment_uuid):
        """
        Get an answer comment
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)
        answer_comment = AnswerComment.get_active_by_uuid_or_404(answer_comment_uuid)
        require(READ, answer_comment,
            title="Feedback Unavailable",
            message="Sorry, your role in this course does not allow you to view this feedback.")

        restrict_user = not allow(MANAGE, assignment)

        restrict_user = not allow(MANAGE, assignment)

        on_answer_comment_get.send(
            self,
            event_name=on_answer_comment_get.name,
            user=current_user,
            course_id=course.id,
            data={'assignment_id': assignment.id, 'answer_id': answer.id, 'answer_comment_id': answer_comment.id})

        return marshal(answer_comment, dataformat.get_answer_comment(restrict_user))
Exemple #28
0
    def get(self):
        restrict_user = not allow(READ, User)

        params = user_list_parser.parse_args()

        query = User.query
        if params['search']:
            search = '%{}%'.format(params['search'])
            query = query.filter(or_(User.firstname.like(search), User.lastname.like(search)))
        page = query.paginate(params['page'], params['perPage'])

        on_user_list_get.send(
            self,
            event_name=on_user_list_get.name,
            user=current_user)

        return {"objects": marshal(page.items, dataformat.get_user(restrict_user)), "page": page.page,
                "pages": page.pages, "total": page.total, "per_page": page.per_page}
Exemple #29
0
    def get(self, course_uuid, assignment_uuid, answer_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        answer = Answer.get_active_by_uuid_or_404(
            answer_uuid,
            joinedloads=['file', 'user', 'scores']
        )
        require(READ, answer)
        restrict_user = not allow(MANAGE, assignment)

        on_answer_get.send(
            self,
            event_name=on_answer_get.name,
            user=current_user,
            course_id=course.id,
            data={'assignment_id': assignment.id, 'answer_id': answer.id})

        return marshal(answer, dataformat.get_answer(restrict_user))
Exemple #30
0
    def get(self, course_uuid, assignment_uuid):
        """
        Get answers submitted to the assignment submitted by current user

        :param course_uuid:
        :param assignment_uuid:
        :return: answers
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        require(READ, Answer(user_id=current_user.id))
        restrict_user = not allow(MANAGE, assignment)

        params = user_answer_list_parser.parse_args()

        query = Answer.query \
            .options(joinedload('comments')) \
            .options(joinedload('file')) \
            .options(joinedload('user')) \
            .options(joinedload('scores')) \
            .filter_by(
                active=True,
                assignment_id=assignment.id,
                course_id=course.id,
                user_id=current_user.id,
                draft=params.get('draft')
            )

        if params.get('unsaved'):
            query = query.filter(Answer.modified == Answer.created)

        answers = query.all()

        on_user_answer_get.send(
            self,
            event_name=on_user_answer_get.name,
            user=current_user,
            course_id=course.id,
            data={'assignment_id': assignment.id})

        return {"objects": marshal( answers, dataformat.get_answer(restrict_user))}
Exemple #31
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)

        require(
            READ,
            course,
            title="Group Unavailable",
            message=
            "Groups can be seen those enrolled in the course. Please double-check your enrollment in this course."
        )

        user_course = UserCourse.query \
            .filter(and_(
                UserCourse.course_id == course.id,
                UserCourse.user_id == current_user.id,
                UserCourse.course_role != CourseRole.dropped
            )) \
            .one_or_none()

        if not user_course:
            # return none for admins who aren't enrolled in the course
            if allow(MANAGE, course):
                return None
            abort(
                400,
                title="Group Unavailable",
                message=
                "You are not currently enrolled in the course. Please double-check your enrollment in this course."
            )

        group = user_course.group

        on_group_user_get.send(current_app._get_current_object(),
                               event_name=on_group_user_get.name,
                               user=current_user,
                               course_id=course.id)

        if group:
            return marshal(group, dataformat.get_group())
        else:
            return None
Exemple #32
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, assignment)
        restrict_user = not allow(MANAGE, assignment)

        assignment_comments = AssignmentComment.query \
            .filter_by(
                course_id=course.id,
                assignment_id=assignment.id,
                active=True
            ) \
            .order_by(AssignmentComment.created.asc()).all()

        on_assignment_comment_list_get.send(
            self,
            event_name=on_assignment_comment_list_get.name,
            user=current_user,
            course_id=course.id,
            data={'assignment_id': assignment.id})

        return {"objects": marshal(assignment_comments, dataformat.get_assignment_comment(restrict_user))}
Exemple #33
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, course,
            title="Assignments Unavailable",
            message="Assignments can be seen only by those enrolled in the course. Please double-check your enrollment in this course.")

        assignment = Assignment(course_id=course.id)
        restrict_user = not allow(MANAGE, assignment)

        # Get all assignments for this course, order by answer_start date, created date
        base_query = Assignment.query \
            .options(joinedload("assignment_criteria").joinedload("criterion")) \
            .options(undefer_group('counts')) \
            .filter(
                Assignment.course_id == course.id,
                Assignment.active == True
            ) \
            .order_by(desc(Assignment.answer_start), desc(Assignment.created))

        if restrict_user:
            now = datetime.datetime.utcnow()
            assignments = base_query \
                .filter(or_(
                    Assignment.answer_start.is_(None),
                    now >= Assignment.answer_start
                ))\
                .all()
        else:
            assignments = base_query.all()

        on_assignment_list_get.send(
            self,
            event_name=on_assignment_list_get.name,
            user=current_user,
            course_id=course.id)

        return {
            "objects": marshal(assignments, dataformat.get_assignment(restrict_user))
        }
Exemple #34
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, course,
            title="Assignments Unavailable",
            message="Assignments can be seen only by those enrolled in the course. Please double-check your enrollment in this course.")

        assignment = Assignment(course_id=course.id)
        restrict_user = not allow(MANAGE, assignment)

        # Get all assignments for this course, order by answer_start date, created date
        base_query = Assignment.query \
            .options(joinedload("assignment_criteria").joinedload("criterion")) \
            .options(undefer_group('counts')) \
            .filter(
                Assignment.course_id == course.id,
                Assignment.active == True
            ) \
            .order_by(desc(Assignment.answer_start), desc(Assignment.created))

        if restrict_user:
            now = datetime.datetime.utcnow()
            assignments = base_query \
                .filter(or_(
                    Assignment.answer_start.is_(None),
                    now >= Assignment.answer_start
                ))\
                .all()
        else:
            assignments = base_query.all()

        on_assignment_list_get.send(
            self,
            event_name=on_assignment_list_get.name,
            user=current_user,
            course_id=course.id)

        return {
            "objects": marshal(assignments, dataformat.get_assignment(restrict_user))
        }
Exemple #35
0
    def get(self):
        params = user_course_list_parser.parse_args()

        # Note, start and end dates are optional so default sort is by start_date (course.start_date or min assignment start date), then name
        query = Course.query \
            .filter_by(active=True) \
            .order_by(Course.start_date_order.desc(), Course.name) \

        # we want to list user linked courses only, so only check the association table
        if not allow(MANAGE, Course):
            query = query.join(UserCourse) \
                .filter(and_(
                    UserCourse.user_id == current_user.id,
                    UserCourse.course_role != CourseRole.dropped
                ))

        if params['search']:
            search_terms = params['search'].split()
            for search_term in search_terms:
                if search_term != "":
                    search = '%{}%'.format(search_term)
                    query = query.filter(or_(
                        Course.name.like(search),
                        Course.year.like(search),
                        Course.term.like(search)
                    ))
        page = query.paginate(params['page'], params['perPage'])

        # TODO REMOVE COURSES WHERE COURSE IS UNAVAILABLE?

        on_user_course_get.send(
            self,
            event_name=on_user_course_get.name,
            user=current_user)

        return {"objects": marshal(page.items, dataformat.get_course()),
                "page": page.page, "pages": page.pages,
                "total": page.total, "per_page": page.per_page}
Exemple #36
0
    def get(self):
        restrict_user = not allow(READ, User)

        params = user_list_parser.parse_args()

        query = User.query
        if params['search']:
            # match each word of search
            for word in params['search'].strip().split(' '):
                if word != '':
                    search = '%' + word + '%'
                    query = query.filter(
                        or_(User.firstname.like(search),
                            User.lastname.like(search),
                            User.displayname.like(search)))

        if params['orderBy']:
            if params['reverse']:
                query = query.order_by(desc(params['orderBy']))
            else:
                query = query.order_by(asc(params['orderBy']))
        query = query.order_by(User.lastname.asc(), User.firstname.asc())

        page = query.paginate(params['page'], params['perPage'])

        on_user_list_get.send(self,
                              event_name=on_user_list_get.name,
                              user=current_user)

        return {
            "objects": marshal(page.items, dataformat.get_user(restrict_user)),
            "page": page.page,
            "pages": page.pages,
            "total": page.total,
            "per_page": page.per_page
        }
Exemple #37
0
    def get(self, course_uuid, assignment_uuid, answer_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        answer = Answer.get_active_by_uuid_or_404(
            answer_uuid,
            joinedloads=['file', 'user', 'group', 'score']
        )
        require(READ, answer,
            title="Answer Unavailable",
            message="Sorry, your role in this course does not allow you to view this answer.")
        restrict_user = not allow(MANAGE, assignment)

        on_answer_get.send(
            self,
            event_name=on_answer_get.name,
            user=current_user,
            course_id=course.id,
            data={'assignment_id': assignment.id, 'answer_id': answer.id})

        # don't include score/rank unless the user is non-restricted
        include_score = not restrict_user

        return marshal(answer, dataformat.get_answer(restrict_user, include_score=include_score))
Exemple #38
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))
Exemple #39
0
    def get(self):
        params = user_course_status_list_parser.parse_args()
        course_uuids = params['ids'].split(',')

        if params['ids'] == '' or len(course_uuids) == 0:
            abort(400, title="Course Status Unavailable", message="Please select a course from the list of courses to see that course's status.")

        query = Course.query \
            .filter(and_(
                Course.uuid.in_(course_uuids),
                Course.active == True,
            )) \
            .add_columns(UserCourse.course_role, UserCourse.group_id) \

        if not allow(MANAGE, Course):
            query = query.join(UserCourse, and_(
                    UserCourse.user_id == current_user.id,
                    UserCourse.course_id == Course.id,
                    UserCourse.course_role != CourseRole.dropped
                ))
        else:
            query = query.outerjoin(UserCourse, and_(
                    UserCourse.user_id == current_user.id,
                    UserCourse.course_id == Course.id
                ))

        results = query.all()

        if len(course_uuids) != len(results):
            abort(400, title="Course Status Unavailable",
                message="Sorry, you are not enrolled in one or more of the selected users' courses yet. Course status is not available until your are enrolled in the course.")

        statuses = {}

        for course, course_role, group_id in results:
            incomplete_assignment_ids = set()
            if not allow(MANAGE, Course) and course_role == CourseRole.student:
                answer_period_assignments = [assignment for assignment in course.assignments if assignment.active and assignment.answer_period]
                compare_period_assignments = [assignment for assignment in course.assignments if assignment.active and assignment.compare_period]

                if len(answer_period_assignments) > 0:
                    answer_period_assignment_ids = [assignment.id for assignment in answer_period_assignments]
                    answers = Answer.query \
                        .filter(and_(
                            or_(
                                and_(Answer.group_id == group_id, Answer.group_id != None),
                                Answer.user_id == current_user.id,
                            ),
                            Answer.assignment_id.in_(answer_period_assignment_ids),
                            Answer.active == True,
                            Answer.practice == False,
                            Answer.draft == False
                        ))
                    for assignment in answer_period_assignments:
                        answer = next(
                            (answer for answer in answers if answer.assignment_id == assignment.id),
                            None
                        )
                        if answer is None:
                            incomplete_assignment_ids.add(assignment.id)

                if len(compare_period_assignments) > 0:
                    compare_period_assignment_ids = [assignment.id for assignment in compare_period_assignments]
                    comparisons = Comparison.query \
                        .filter(and_(
                            Comparison.user_id == current_user.id,
                            Comparison.assignment_id.in_(compare_period_assignment_ids),
                            Comparison.completed == True
                        ))

                    self_evaluations = AnswerComment.query \
                        .join("answer") \
                        .with_entities(
                            Answer.assignment_id,
                            func.count(Answer.assignment_id).label('self_evaluation_count')
                        ) \
                        .filter(and_(
                            or_(
                                and_(Answer.group_id == group_id, Answer.group_id != None),
                                Answer.user_id == current_user.id,
                            ),
                            AnswerComment.active == True,
                            AnswerComment.comment_type == AnswerCommentType.self_evaluation,
                            AnswerComment.draft == False,
                            Answer.active == True,
                            Answer.practice == False,
                            Answer.draft == False,
                            Answer.assignment_id.in_(compare_period_assignment_ids)
                        )) \
                        .group_by(Answer.assignment_id) \
                        .all()

                    for assignment in compare_period_assignments:
                        assignment_comparisons = [comparison for comparison in comparisons if comparison.assignment_id == assignment.id]
                        if len(assignment_comparisons) < assignment.total_comparisons_required:
                            incomplete_assignment_ids.add(assignment.id)

                        if assignment.enable_self_evaluation:
                            self_evaluation_count = next(
                                (result.self_evaluation_count for result in self_evaluations if result.assignment_id == assignment.id),
                                0
                            )
                            if self_evaluation_count == 0:
                                incomplete_assignment_ids.add(assignment.id)

            statuses[course.uuid] = {
                'incomplete_assignments': len(incomplete_assignment_ids)
            }

        on_user_course_status_get.send(
            self,
            event_name=on_user_course_status_get.name,
            user=current_user,
            data=statuses)

        return {"statuses": statuses}
Exemple #40
0
    def get(self, course_uuid, assignment_uuid):
        """
        Get (or create if needed) a comparison set for assignment.
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(
            READ,
            assignment,
            title="Comparisons Unavailable",
            message=
            "Assignments and their comparisons can only be seen here by those enrolled in the course. Please double-check your enrollment in this course."
        )
        require(
            CREATE,
            Comparison,
            title="Comparisons Unavailable",
            message=
            "Comparisons can only be seen here by those enrolled in the course. Please double-check your enrollment in this course."
        )
        restrict_user = not allow(MANAGE, assignment)

        comparison_count = assignment.completed_comparison_count_for_user(
            current_user.id)

        if not assignment.compare_grace:
            abort(
                403,
                title="Comparisons Unavailable",
                message=
                "Sorry, the comparison deadline has passed. No comparisons can be done after the deadline."
            )
        elif not restrict_user and not assignment.educators_can_compare:
            abort(
                403,
                title="Comparisons Unavailable",
                message=
                "Only students can currently compare answers for this assignment. To change these settings to include instructors and teaching assistants, edit the assignment."
            )
        elif restrict_user and comparison_count >= assignment.total_comparisons_required:
            abort(
                400,
                title="Comparisons Completed",
                message=
                "More comparisons aren't available, since you've finished your comparisons for this assignment. Good job!"
            )

        # check if user has a comparison they have not completed yet
        new_pair = False
        comparison = Comparison.query \
            .options(joinedload('comparison_criteria')) \
            .filter_by(
                assignment_id=assignment.id,
                user_id=current_user.id,
                completed=False
            ) \
            .first()

        if comparison:
            on_comparison_get.send(
                self,
                event_name=on_comparison_get.name,
                user=current_user,
                course_id=course.id,
                data=marshal(comparison,
                             dataformat.get_comparison(restrict_user)))
        else:
            # if there isn't an incomplete comparison, assign a new one
            try:
                comparison = Comparison.create_new_comparison(
                    assignment.id,
                    current_user.id,
                    skip_comparison_examples=allow(MANAGE, assignment))
                new_pair = True

                on_comparison_create.send(
                    self,
                    event_name=on_comparison_create.name,
                    user=current_user,
                    course_id=course.id,
                    data=marshal(comparison,
                                 dataformat.get_comparison(restrict_user)))

            except InsufficientObjectsForPairException:
                abort(
                    400,
                    title="Comparisons Unavailable",
                    message=
                    "Not enough answers are available for you to do comparisons right now. Please check back later for more answers."
                )
            except UserComparedAllObjectsException:
                abort(
                    400,
                    title="Comparisons Unavailable",
                    message=
                    "You have compared all the currently available answer pairs. Please check back later for more answers."
                )
            except UnknownPairGeneratorException:
                abort(
                    500,
                    title="Comparisons Unavailable",
                    message=
                    "Generating scored pairs failed, this really shouldn't happen."
                )

        # get evaluation comments for answers by current user
        answer_comments = AnswerComment.query \
            .join("answer") \
            .filter(and_(
                # both draft and completed comments are allowed
                AnswerComment.active == True,
                AnswerComment.comment_type == AnswerCommentType.evaluation,
                Answer.id.in_([comparison.answer1_id, comparison.answer2_id]),
                AnswerComment.user_id == current_user.id
            )) \
            .order_by(AnswerComment.draft, AnswerComment.created) \
            .all()

        comparison.answer1_feedback = [
            c for c in answer_comments if c.answer_id == comparison.answer1_id
        ]
        comparison.answer2_feedback = [
            c for c in answer_comments if c.answer_id == comparison.answer2_id
        ]

        return {
            'comparison':
            marshal(
                comparison,
                dataformat.get_comparison(restrict_user,
                                          include_answer_author=False,
                                          include_score=False,
                                          with_feedback=True)),
            'new_pair':
            new_pair,
            'current':
            comparison_count + 1
        }
Exemple #41
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,
            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 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('score')) \
            .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'] == 'score':
            query = query.join(AnswerScore) \
                .order_by(AnswerScore.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 = 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 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(AnswerScore.score >= score_for_rank - 0.00001)

        else:
            query = query.order_by(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})

        return {"objects": marshal(page.items, dataformat.get_answer(restrict_user)),
                "page": page.page, "pages": page.pages,
                "total": page.total, "per_page": page.per_page}
Exemple #42
0
    def get(self, **kwargs):
        """
        **Example request**:

        .. sourcecode:: http

            GET /api/answer/123/comments HTTP/1.1
            Host: example.com
            Accept: application/json

        .. sourcecode:: http

            GET /api/answer_comments?ids=1,2,3&self_evaluation=only HTTP/1.1
            Host: example.com
            Accept: application/json

        **Example response**:

        .. sourcecode:: http

            HTTP/1.1 200 OK
            Vary: Accept
            Content-Type: application/json
            [{
                'id': 1
                'content': 'comment text',
                'created': '',
                'user_id': 1,
                'user_displayname': 'John',
                'user_fullname': 'John Smith',
                'fullname_sortable': 'Smith, John',
                'user_avatar': '12k3jjh24k32jhjksaf',
                'comment_type': 'self_evaluation',
                'course_id': 1,
            }]

        :query string ids: a comma separated comment uuids to query
        :query string answer_ids: a comma separated answer uuids for answer filter
        :query string assignment_id: filter the answer comments with a assignment uuid
        :query string user_ids: a comma separated user uuids that own the comments
        :query string self_evaluation: indicate whether the result should include self-evaluation comments or self-evaluation only.
                Possible values: true, false or only. Default true.
        :query string evaluation: indicate whether the result should include evaluation comments or evaluation only.
                Possible values: true, false or only. Default true.
        :query string draft: indicate whether the result should include drafts for current user or not.
                Possible values: true, false or only. Default false.
        :reqheader Accept: the response content type depends on :mailheader:`Accept` header
        :resheader Content-Type: this depends on :mailheader:`Accept` header of request
        :statuscode 200: no error
        :statuscode 404: answers don't exist

        """
        params = answer_comment_list_parser.parse_args()
        answer_uuids = []
        if 'answer_uuid' in kwargs:
            answer_uuids.append(kwargs['answer_uuid'])
        elif 'answer_ids' in params and params['answer_ids']:
            answer_uuids.extend(params['answer_ids'].split(','))

        if not answer_uuids and not params['ids'] and not params[
                'assignment_id'] and not params['user_ids']:
            abort(
                404,
                title="Replies Unavailable",
                message=
                "There was a problem getting the replies for this answer. Please try again."
            )

        conditions = []

        answers = Answer.query \
            .filter(
                Answer.active == True,
                Answer.draft == False,
                Answer.uuid.in_(answer_uuids)
            ) \
            .all() if answer_uuids else []
        if answer_uuids and not answers:
            # non-existing answer ids.
            abort(
                404,
                title="Replies Unavailable",
                message=
                "There was a problem getting the replies for this answer. Please try again."
            )

        # build query condition for each answer
        for answer in answers:
            clauses = [AnswerComment.answer_id == answer.id]

            # student can only see the comments for themselves or public ones.
            # since the owner of the answer can access all comments. We only filter
            # on non-owners
            if current_user.get_course_role(answer.course_id) == CourseRole.student \
                    and answer.user_id != current_user.id:
                # public comments or comments owned by current user
                clauses.append(
                    or_(AnswerComment.comment_type == AnswerCommentType.public,
                        AnswerComment.user_id == current_user.id))

            conditions.append(and_(*clauses))

        query = AnswerComment.query. \
            filter(AnswerComment.active==True) .\
            filter(or_(*conditions))

        if params['ids']:
            query = query.filter(
                AnswerComment.uuid.in_(params['ids'].split(',')))

        if params['self_evaluation'] == 'false':
            # do not include self-evaluation
            query = query.filter(
                AnswerComment.comment_type != AnswerCommentType.self_evaluation
            )
        elif params['self_evaluation'] == 'only':
            # only self_evaluation
            query = query.filter(AnswerComment.comment_type ==
                                 AnswerCommentType.self_evaluation)

        if params['evaluation'] == 'false':
            # do not include evalulation comments
            query = query.filter(
                AnswerComment.comment_type != AnswerCommentType.evaluation)
        elif params['evaluation'] == 'only':
            # only evaluation
            query = query.filter(
                AnswerComment.comment_type == AnswerCommentType.evaluation)

        if params['draft'] == 'true':
            # with draft (current_user)
            query = query.filter(
                or_(
                    AnswerComment.draft == False,
                    and_(AnswerComment.draft == True,
                         AnswerComment.user_id == current_user.id)))
        elif params['draft'] == 'only':
            # only draft (current_user)
            query = query.filter(
                and_(AnswerComment.draft == True,
                     AnswerComment.user_id == current_user.id))
        else:
            # do not include draft. Default
            query = query.filter(AnswerComment.draft == False)

        if params['assignment_id']:
            query = query.join(AnswerComment.answer). \
                filter_by(assignment_uuid=params['assignment_id'])

        if params['user_ids']:
            user_ids = params['user_ids'].split(',')
            query = query \
                .join(User, AnswerComment.user_id == User.id) \
                .filter(User.uuid.in_(user_ids))

        answer_comments = query.order_by(AnswerComment.created.desc()).all()

        # checking the permission
        for answer_comment in answer_comments:
            require(
                READ,
                answer_comment.answer,
                title="Replies Unavailable",
                message=
                "Sorry, your role in this course does not allow you to view replies for this answer."
            )

        on_answer_comment_list_get.send(
            self,
            event_name=on_answer_comment_list_get.name,
            user=current_user,
            data={
                'answer_ids': ','.join([str(answer.id) for answer in answers])
            })

        return marshal(
            answer_comments,
            dataformat.get_answer_comment(not allow(READ, USER_IDENTITY)))
Exemple #43
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
        }
Exemple #44
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))
Exemple #45
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))
Exemple #46
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(
            READ,
            UserCourse(course_id=course.id),
            title="Class List Unavailable",
            message=
            "Sorry, your role in this course does not allow you to view the class list."
        )
        restrict_user = not allow(READ, USER_IDENTITY)

        # expire current_user from the session. When loading classlist from database, if the
        # user is already in the session, e.g. instructor for the course, the User.user_courses
        # is not loaded from the query below, but from session. In this case, if user has more
        # than one course, User.user_courses will return multiple row. Thus violating the
        # course_role constrain.
        db.session.expire(current_user)

        users = User.query \
            .with_entities(User, UserCourse) \
            .options(joinedload(UserCourse.group)) \
            .join(UserCourse, and_(
                UserCourse.user_id == User.id,
                UserCourse.course_id == course.id
            )) \
            .filter(
                UserCourse.course_role != CourseRole.dropped
            ) \
            .order_by(User.lastname, User.firstname) \
            .all()

        if not restrict_user:
            user_ids = [_user.id for (_user, _user_course) in users]
            third_party_auths = ThirdPartyUser.query \
                .filter(and_(
                    ThirdPartyUser.user_id.in_(user_ids),
                    or_(
                        ThirdPartyUser.third_party_type == ThirdPartyType.cas,
                        ThirdPartyUser.third_party_type == ThirdPartyType.saml,
                    )
                )) \
                .all()

        class_list = []
        for (_user, _user_course) in users:
            _user.course_role = _user_course.course_role
            _user.group = _user_course.group
            _user.group_uuid = _user.group.uuid if _user.group else None
            _user.group_name = _user.group.name if _user.group else None

            if not restrict_user:
                cas_auth = next(
                    (auth
                     for auth in third_party_auths if auth.user_id == _user.id
                     and auth.third_party_type == ThirdPartyType.cas), None)
                _user.cas_username = cas_auth.unique_identifier if cas_auth else None
                saml_auth = next(
                    (auth
                     for auth in third_party_auths if auth.user_id == _user.id
                     and auth.third_party_type == ThirdPartyType.saml), None)
                _user.saml_username = saml_auth.unique_identifier if saml_auth else None

            class_list.append(_user)

        on_classlist_get.send(self,
                              event_name=on_classlist_get.name,
                              user=current_user,
                              course_id=course.id)

        if allow(MANAGE, User):
            return {
                'objects':
                marshal(class_list, dataformat.get_full_users_in_course())
            }
        else:
            return {
                'objects':
                marshal(
                    class_list,
                    dataformat.get_users_in_course(
                        restrict_user=restrict_user))
            }
Exemple #47
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, course,
            title="Assignment Status Unavailable",
            message="Assignment status can be seen only by those enrolled in the course. Please double-check your enrollment in this course.")

        group = current_user.get_course_group(course.id)
        group_id = group.id if group else None

        assignments = course.assignments \
            .filter_by(active=True) \
            .all()
        assignment_ids = [assignment.id for assignment in assignments]

        answer_counts = Answer.query \
            .with_entities(
                Answer.assignment_id,
                func.count(Answer.assignment_id).label('answer_count')
            ) \
            .filter_by(
                comparable=True,
                active=True,
                practice=False,
                draft=False
            ) \
            .filter(or_(
                Answer.user_id == current_user.id,
                and_(Answer.group_id == group_id, Answer.group_answer == True)
            )) \
            .filter(Answer.assignment_id.in_(assignment_ids)) \
            .group_by(Answer.assignment_id) \
            .all()


        feedback_counts = AnswerComment.query \
            .join("answer") \
            .with_entities(
                Answer.assignment_id,
                func.count(Answer.assignment_id).label('feedback_count')
            ) \
            .filter(and_(
                AnswerComment.active == True,
                AnswerComment.draft == False,
                Answer.active == True,
                Answer.practice == False,
                Answer.draft == False,
                Answer.assignment_id.in_(assignment_ids)
            )) \
            .filter(or_(
                Answer.user_id == current_user.id,
                and_(Answer.group_id == group_id, Answer.group_id != None)
            )) \
            .group_by(Answer.assignment_id) \
            .all()

        # get self evaluation status for assignments with self evaluations enabled
        self_evaluations = AnswerComment.query \
            .join("answer") \
            .with_entities(
                Answer.assignment_id,
                func.count(Answer.assignment_id).label('self_evaluation_count')
            ) \
            .filter(and_(
                AnswerComment.user_id == current_user.id,
                AnswerComment.active == True,
                AnswerComment.comment_type == AnswerCommentType.self_evaluation,
                AnswerComment.draft == False,
                Answer.active == True,
                Answer.practice == False,
                Answer.draft == False,
                Answer.assignment_id.in_(assignment_ids)
            )) \
            .group_by(Answer.assignment_id) \
            .all()

        self_evaluation_drafts = AnswerComment.query \
            .join("answer") \
            .with_entities(
                Answer.assignment_id,
                func.count(Answer.assignment_id).label('self_evaluation_count')
            ) \
            .filter(and_(
                AnswerComment.user_id == current_user.id,
                AnswerComment.active == True,
                AnswerComment.comment_type == AnswerCommentType.self_evaluation,
                AnswerComment.draft == True,
                Answer.active == True,
                Answer.practice == False,
                Answer.draft == False,
                Answer.assignment_id.in_(assignment_ids)
            )) \
            .group_by(Answer.assignment_id) \
            .all()

        query = Answer.query \
            .options(load_only('id', 'assignment_id', 'uuid')) \
            .filter_by(
                active=True,
                practice=False,
                draft=True
            ) \
            .filter(or_(
                and_(Answer.group_id == group_id, Answer.group_id != None),
                Answer.user_id == current_user.id
            )) \
            .filter(Answer.assignment_id.in_(assignment_ids))
        drafts = query.all()

        statuses = {}
        for assignment in assignments:
            answer_count = next(
                (result.answer_count for result in answer_counts if result.assignment_id == assignment.id),
                0
            )
            feedback_count = next(
                (result.feedback_count for result in feedback_counts if result.assignment_id == assignment.id),
                0
            )
            assignment_drafts = [draft for draft in drafts if draft.assignment_id == assignment.id]
            comparison_count = assignment.completed_comparison_count_for_user(current_user.id)
            comparison_draft_count = assignment.draft_comparison_count_for_user(current_user.id)
            other_comparable_answers = assignment.comparable_answer_count - answer_count

            # students can only begin comparing when there there are enough answers submitted that they can do
            # comparisons without seeing the same answer more than once
            comparison_available = other_comparable_answers >= assignment.number_of_comparisons * 2
            # instructors and tas can compare as long as there are new possible comparisons
            if allow(EDIT, assignment):
                comparison_available = comparison_count < other_comparable_answers * (other_comparable_answers - 1) / 2

            statuses[assignment.uuid] = {
                'answers': {
                    'answered': answer_count > 0,
                    'feedback': feedback_count,
                    'count': answer_count,
                    'has_draft': len(assignment_drafts) > 0,
                    'draft_ids': [draft.uuid for draft in assignment_drafts]
                },
                'comparisons': {
                    'available': comparison_available,
                    'count': comparison_count,
                    'left': max(0, assignment.total_comparisons_required - comparison_count),
                    'has_draft': comparison_draft_count > 0
                }
            }

            if assignment.enable_self_evaluation:
                self_evaluation_count = next(
                    (result.self_evaluation_count for result in self_evaluations if result.assignment_id == assignment.id),
                    0
                )
                self_evaluation_draft_count = next(
                    (result.self_evaluation_count for result in self_evaluation_drafts if result.assignment_id == assignment.id),
                    0
                )
                statuses[assignment.uuid]['comparisons']['self_evaluation_completed'] = self_evaluation_count > 0
                statuses[assignment.uuid]['comparisons']['self_evaluation_draft'] = self_evaluation_draft_count > 0

        on_assignment_list_get_status.send(
            self,
            event_name=on_assignment_list_get_status.name,
            user=current_user,
            course_id=course.id,
            data=statuses)

        return {"statuses": statuses}
Exemple #48
0
    def post(self, course_uuid, assignment_uuid):
        """
        Stores comparison set into the database.
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(READ, assignment,
            title="Comparison Not Saved",
            message="Comparisons can only be saved by those enrolled in the course. Please double-check your enrollment in this course.")
        require(EDIT, Comparison,
            title="Comparison Not Saved",
            message="Comparisons can only be saved by those enrolled in the course. Please double-check your enrollment in this course.")
        restrict_user = not allow(MANAGE, assignment)

        if not assignment.compare_grace:
            abort(403, title="Comparison Not Saved",
                message="Sorry, the comparison deadline has passed. No comparisons can be done after the deadline.")
        elif not restrict_user and not assignment.educators_can_compare:
            abort(403, title="Comparison Not Saved",
                message="Only students can save answer comparisons for this assignment. To change these settings to include instructors and teaching assistants, edit the assignment.")

        comparison = Comparison.query \
            .options(joinedload('comparison_criteria')) \
            .filter_by(
                assignment_id=assignment.id,
                user_id=current_user.id,
                completed=False
            ) \
            .first()

        params = update_comparison_parser.parse_args()
        completed = True

        # check if there are any comparisons to update
        if not comparison:
            abort(400, title="Comparison Not Saved", message="There are no comparisons open for evaluation.")

        is_comparison_example = comparison.comparison_example_id != None

        # check if number of comparison criteria submitted matches number of comparison criteria needed
        if len(comparison.comparison_criteria) != len(params['comparison_criteria']):
            abort(400, title="Comparison Not Saved", message="Please double-check that all criteria were evaluated and try saving again.")

        if params.get('draft'):
            completed = False

        # check if each comparison has a criterion Id and a winner id
        for comparison_criterion_update in params['comparison_criteria']:
            # ensure criterion param is present
            if 'criterion_id' not in comparison_criterion_update:
                abort(400, title="Comparison Not Saved", message="Sorry, the assignment is missing criteria. Please reload the page and try again.")

            # set default values for cotnent and winner
            comparison_criterion_update.setdefault('content', None)
            winner = comparison_criterion_update.setdefault('winner', None)

            # if winner isn't set for any comparison criterion, then the comparison isn't complete yet
            if winner == None:
                completed = False

            # check that we're using criteria that were assigned to the course and that we didn't
            # get duplicate criteria in comparison criteria
            known_criterion = False
            for comparison_criterion in comparison.comparison_criteria:
                if comparison_criterion_update['criterion_id'] == comparison_criterion.criterion_uuid:
                    known_criterion = True

                    # check that the winner id matches one of the answer pairs
                    if winner not in [None, WinningAnswer.answer1.value, WinningAnswer.answer2.value]:
                        abort(400, title="Comparison Not Saved", message="Please select an answer from the two answers provided for each criterion and try saving again.")

                    break
            if not known_criterion:
                abort(400, title="Comparison Not Saved", message="You are attempting to submit a comparison of an unknown criterion. Please remove the unknown criterion and try again.")


        # update comparison criterion
        comparison.completed = completed
        comparison.winner = None

        assignment_criteria = assignment.assignment_criteria
        answer1_weight = 0
        answer2_weight = 0

        for comparison_criterion in comparison.comparison_criteria:
            for comparison_criterion_update in params['comparison_criteria']:
                if comparison_criterion_update['criterion_id'] != comparison_criterion.criterion_uuid:
                    continue

                winner = WinningAnswer(comparison_criterion_update['winner']) if comparison_criterion_update['winner'] != None else None

                comparison_criterion.winner = winner
                comparison_criterion.content = comparison_criterion_update['content']

                if completed:
                    weight = next((
                        assignment_criterion.weight for assignment_criterion in assignment_criteria \
                        if assignment_criterion.criterion_uuid == comparison_criterion.criterion_uuid
                    ), 0)
                    if winner == WinningAnswer.answer1:
                        answer1_weight += weight
                    elif winner == WinningAnswer.answer2:
                        answer2_weight += weight

        if completed:
            if answer1_weight > answer2_weight:
                comparison.winner = WinningAnswer.answer1
            elif answer1_weight < answer2_weight:
                comparison.winner = WinningAnswer.answer2
            else:
                comparison.winner = WinningAnswer.draw
        else:
            # ensure that the comparison is 'touched' when saving a draft
            comparison.modified = datetime.datetime.utcnow()

        comparison.update_attempt(
            params.get('attempt_uuid'),
            params.get('attempt_started', None),
            params.get('attempt_ended', None)
        )

        db.session.commit()

        # update answer scores
        if completed and not is_comparison_example:
            current_app.logger.debug("Doing scoring")
            Comparison.update_scores_1vs1(comparison)
            #Comparison.calculate_scores(assignment.id)

        # update course & assignment grade for user if comparison is completed
        if completed:
            assignment.calculate_grade(current_user)
            course.calculate_grade(current_user)

        on_comparison_update.send(
            self,
            event_name=on_comparison_update.name,
            user=current_user,
            course_id=course.id,
            assignment=assignment,
            comparison=comparison,
            is_comparison_example=is_comparison_example,
            data=marshal(comparison, dataformat.get_comparison(restrict_user)))

        return {'comparison': marshal(comparison, dataformat.get_comparison(restrict_user, include_answer_author=False, include_score=False))}
Exemple #49
0
    def get(self, course_uuid, assignment_uuid):
        """
        Get (or create if needed) a comparison set for assignment.
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(READ, assignment,
            title="Comparisons Unavailable",
            message="Assignments and their comparisons can only be seen here by those enrolled in the course. Please double-check your enrollment in this course.")
        require(CREATE, Comparison,
            title="Comparisons Unavailable",
            message="Comparisons can only be seen here by those enrolled in the course. Please double-check your enrollment in this course.")
        restrict_user = not allow(MANAGE, assignment)

        comparison_count = assignment.completed_comparison_count_for_user(current_user.id)

        if not assignment.compare_grace:
            abort(403, title="Comparisons Unavailable",
                message="Sorry, the comparison deadline has passed. No comparisons can be done after the deadline.")
        elif not restrict_user and not assignment.educators_can_compare:
            abort(403, title="Comparisons Unavailable",
                message="Only students can currently compare answers for this assignment. To change these settings to include instructors and teaching assistants, edit the assignment.")
        elif restrict_user and comparison_count >= assignment.total_comparisons_required:
            abort(400, title="Comparisons Completed",
                message="More comparisons aren't available, since you've finished your comparisons for this assignment. Good job!")

        # check if user has a comparison they have not completed yet
        comparison = Comparison.query \
            .options(joinedload('comparison_criteria')) \
            .filter_by(
                assignment_id=assignment.id,
                user_id=current_user.id,
                completed=False
            ) \
            .first()

        if comparison:
            on_comparison_get.send(
                self,
                event_name=on_comparison_get.name,
                user=current_user,
                course_id=course.id,
                data=marshal(comparison, dataformat.get_comparison(restrict_user)))
        else:
            # if there isn't an incomplete comparison, assign a new one
            try:
                comparison = Comparison.create_new_comparison(assignment.id, current_user.id,
                    skip_comparison_examples=allow(MANAGE, assignment))

                on_comparison_create.send(
                    self,
                    event_name=on_comparison_create.name,
                    user=current_user,
                    course_id=course.id,
                    data=marshal(comparison, dataformat.get_comparison(restrict_user)))

            except InsufficientObjectsForPairException:
                abort(400, title="Comparisons Unavailable", message="Not enough answers are available for you to do comparisons right now. Please check back later for more answers.")
            except UserComparedAllObjectsException:
                abort(400, title="Comparisons Unavailable", message="You have compared all the currently available answer pairs. Please check back later for more answers.")
            except UnknownPairGeneratorException:
                abort(500, title="Comparisons Unavailable", message="Generating scored pairs failed, this really shouldn't happen.")


        # get evaluation comments for answers by current user
        answer_comments = AnswerComment.query \
            .join("answer") \
            .filter(and_(
                # both draft and completed comments are allowed
                AnswerComment.active == True,
                AnswerComment.comment_type == AnswerCommentType.evaluation,
                Answer.id.in_([comparison.answer1_id, comparison.answer2_id]),
                AnswerComment.user_id == current_user.id
            )) \
            .order_by(AnswerComment.draft, AnswerComment.created) \
            .all()

        comparison.answer1_feedback = [c for c in answer_comments if c.answer_id == comparison.answer1_id]
        comparison.answer2_feedback = [c for c in answer_comments if c.answer_id == comparison.answer2_id]

        return {
            'comparison': marshal(comparison, dataformat.get_comparison(restrict_user,
                include_answer_author=False, include_score=False, with_feedback=True)),
            'current': comparison_count+1
        }
Exemple #50
0
    def post(self, course_uuid, assignment_uuid):
        """
        Stores comparison set into the database.
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(READ, assignment)
        require(CREATE, Comparison)
        restrict_user = not allow(MANAGE, assignment)

        if not assignment.compare_grace:
            return {'error': comparison_deadline_message}, 403
        elif not restrict_user and not assignment.educators_can_compare:
            return {'error': educators_can_not_compare_message}, 403

        comparisons = Comparison.query \
            .filter_by(
                assignment_id=assignment.id,
                user_id=current_user.id,
                completed=False
            ) \
            .all()

        params = update_comparison_parser.parse_args()
        completed = True

        # check if there are any comparisons to update
        if len(comparisons) == 0:
            return {"error": "There are no comparisons open for evaluation."}, 400

        is_comparison_example = comparisons[0].comparison_example_id != None

        # check if number of comparisons submitted matches number of comparisons needed
        if len(comparisons) != len(params['comparisons']):
            return {"error": "Not all criteria were evaluated."}, 400

        # check if each comparison has a criterion Id and a winner id
        for comparison_to_update in params['comparisons']:
            # check if saving a draft
            if 'draft' in comparison_to_update and comparison_to_update['draft']:
                completed = False

            # ensure criterion param is present
            if 'criterion_id' not in comparison_to_update:
                return {"error": "Missing criterion_id in evaluation."}, 400

            # set default values for cotnent and winner
            comparison_to_update.setdefault('content', None)
            winner_uuid = comparison_to_update.setdefault('winner_id', None)

            # if winner isn't set for any comparisons, then the comparison set isn't complete yet
            if winner_uuid == None:
                completed = False

            # check that we're using criteria that were assigned to the course and that we didn't
            # get duplicate criteria in comparisons
            known_criterion = False
            for comparison in comparisons:
                if comparison_to_update['criterion_id'] == comparison.criterion_uuid:
                    known_criterion = True

                    # check that the winner id matches one of the answer pairs
                    if winner_uuid not in [comparison.answer1_uuid, comparison.answer2_uuid, None]:
                        return {"error": "Selected answer does not match the available answers in comparison."}, 400

                    break
            if not known_criterion:
                return {"error": "Unknown criterion submitted!"}, 400


        # update comparisons
        for comparison in comparisons:
            comparison.completed = completed

            for comparison_to_update in params['comparisons']:
                if comparison_to_update['criterion_id'] != comparison.criterion_uuid:
                    continue

                if comparison_to_update['winner_id'] == comparison.answer1_uuid:
                    comparison.winner_id = comparison.answer1_id
                elif comparison_to_update['winner_id'] == comparison.answer2_uuid:
                    comparison.winner_id = comparison.answer2_id
                else:
                    comparison.winner_id = None
                comparison.content = comparison_to_update['content']

        db.session.commit()

        # update answer scores
        if completed and not is_comparison_example:
            current_app.logger.debug("Doing scoring")
            Comparison.update_scores_1vs1(comparisons)
            #Comparison.calculate_scores(assignment.id)

        # update course & assignment grade for user if comparison is completed
        if completed:
            assignment.calculate_grade(current_user)
            course.calculate_grade(current_user)

        on_comparison_update.send(
            self,
            event_name=on_comparison_update.name,
            user=current_user,
            course_id=course.id,
            assignment=assignment,
            comparisons=comparisons,
            is_comparison_example=is_comparison_example,
            data=marshal(comparisons, dataformat.get_comparison(restrict_user)))

        return {'objects': marshal(comparisons, dataformat.get_comparison(restrict_user))}
Exemple #51
0
    def get(self, course_uuid, assignment_uuid):
        """
        Get (or create if needed) a comparison set for assignment.
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(READ, assignment)
        require(CREATE, Comparison)
        restrict_user = not allow(MANAGE, assignment)

        if not assignment.compare_grace:
            return {'error': comparison_deadline_message}, 403
        elif not restrict_user and not assignment.educators_can_compare:
            return {'error': educators_can_not_compare_message}, 403

        # check if user has comparisons they have not completed yet
        comparisons = Comparison.query \
            .filter_by(
                assignment_id=assignment.id,
                user_id=current_user.id,
                completed=False
            ) \
            .all()

        new_pair = False

        if len(comparisons) > 0:
            on_comparison_get.send(
                self,
                event_name=on_comparison_get.name,
                user=current_user,
                course_id=course.id,
                data=marshal(comparisons, dataformat.get_comparison(restrict_user)))
        else:
            # if there aren't incomplete comparisons, assign a new one
            try:
                comparisons = Comparison.create_new_comparison_set(assignment.id, current_user.id,
                    skip_comparison_examples=allow(MANAGE, assignment))
                new_pair = True

                on_comparison_create.send(
                    self,
                    event_name=on_comparison_create.name,
                    user=current_user,
                    course_id=course.id,
                    data=marshal(comparisons, dataformat.get_comparison(restrict_user)))

            except InsufficientObjectsForPairException:
                return {"error": "Not enough answers are available for a comparison."}, 400
            except UserComparedAllObjectsException:
                return {"error": "You have compared all the currently available answers."}, 400
            except UnknownPairGeneratorException:
                return {"error": "Generating scored pairs failed, this really shouldn't happen."}, 500

        comparison_count = assignment.completed_comparison_count_for_user(current_user.id)

        return {
            'objects': marshal(comparisons, dataformat.get_comparison(restrict_user)),
            'new_pair': new_pair,
            'current': comparison_count+1
        }
Exemple #52
0
    def post(self, course_uuid, assignment_uuid):
        """
        Stores comparison set into the database.
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(
            READ,
            assignment,
            title="Comparison Not Saved",
            message=
            "Comparisons can only be saved by those enrolled in the course. Please double-check your enrollment in this course."
        )
        require(
            EDIT,
            Comparison,
            title="Comparison Not Saved",
            message=
            "Comparisons can only be saved by those enrolled in the course. Please double-check your enrollment in this course."
        )
        restrict_user = not allow(MANAGE, assignment)

        if not assignment.compare_grace:
            abort(
                403,
                title="Comparison Not Saved",
                message=
                "Sorry, the comparison deadline has passed. No comparisons can be done after the deadline."
            )
        elif not restrict_user and not assignment.educators_can_compare:
            abort(
                403,
                title="Comparison Not Saved",
                message=
                "Only students can save answer comparisons for this assignment. To change these settings to include instructors and teaching assistants, edit the assignment."
            )

        comparison = Comparison.query \
            .options(joinedload('comparison_criteria')) \
            .filter_by(
                assignment_id=assignment.id,
                user_id=current_user.id,
                completed=False
            ) \
            .first()

        params = update_comparison_parser.parse_args()
        completed = True

        # check if there are any comparisons to update
        if not comparison:
            abort(400,
                  title="Comparison Not Saved",
                  message="There are no comparisons open for evaluation.")

        is_comparison_example = comparison.comparison_example_id != None

        # check if number of comparison criteria submitted matches number of comparison criteria needed
        if len(comparison.comparison_criteria) != len(
                params['comparison_criteria']):
            abort(
                400,
                title="Comparison Not Saved",
                message=
                "Please double-check that all criteria were evaluated and try saving again."
            )

        if params.get('draft'):
            completed = False

        # check if each comparison has a criterion Id and a winner id
        for comparison_criterion_update in params['comparison_criteria']:
            # ensure criterion param is present
            if 'criterion_id' not in comparison_criterion_update:
                abort(
                    400,
                    title="Comparison Not Saved",
                    message=
                    "Sorry, the assignment is missing criteria. Please reload the page and try again."
                )

            # set default values for cotnent and winner
            comparison_criterion_update.setdefault('content', None)
            winner = comparison_criterion_update.setdefault('winner', None)

            # if winner isn't set for any comparison criterion, then the comparison isn't complete yet
            if winner == None:
                completed = False

            # check that we're using criteria that were assigned to the course and that we didn't
            # get duplicate criteria in comparison criteria
            known_criterion = False
            for comparison_criterion in comparison.comparison_criteria:
                if comparison_criterion_update[
                        'criterion_id'] == comparison_criterion.criterion_uuid:
                    known_criterion = True

                    # check that the winner id matches one of the answer pairs
                    if winner not in [
                            None, WinningAnswer.answer1.value,
                            WinningAnswer.answer2.value
                    ]:
                        abort(
                            400,
                            title="Comparison Not Saved",
                            message=
                            "Please select an answer from the two answers provided for each criterion and try saving again."
                        )

                    break
            if not known_criterion:
                abort(
                    400,
                    title="Comparison Not Saved",
                    message=
                    "You are attempting to submit a comparison of an unknown criterion. Please remove the unknown criterion and try again."
                )

        # update comparison criterion
        comparison.completed = completed
        comparison.winner = None

        assignment_criteria = assignment.assignment_criteria
        answer1_weight = 0
        answer2_weight = 0

        for comparison_criterion in comparison.comparison_criteria:
            for comparison_criterion_update in params['comparison_criteria']:
                if comparison_criterion_update[
                        'criterion_id'] != comparison_criterion.criterion_uuid:
                    continue

                winner = WinningAnswer(
                    comparison_criterion_update['winner']
                ) if comparison_criterion_update['winner'] != None else None

                comparison_criterion.winner = winner
                comparison_criterion.content = comparison_criterion_update[
                    'content']

                if completed:
                    weight = next((
                        assignment_criterion.weight for assignment_criterion in assignment_criteria \
                        if assignment_criterion.criterion_uuid == comparison_criterion.criterion_uuid
                    ), 0)
                    if winner == WinningAnswer.answer1:
                        answer1_weight += weight
                    elif winner == WinningAnswer.answer2:
                        answer2_weight += weight

        if completed:
            if answer1_weight > answer2_weight:
                comparison.winner = WinningAnswer.answer1
            elif answer1_weight < answer2_weight:
                comparison.winner = WinningAnswer.answer2
            else:
                comparison.winner = WinningAnswer.draw
        else:
            # ensure that the comparison is 'touched' when saving a draft
            comparison.modified = datetime.datetime.utcnow()

        db.session.commit()

        # update answer scores
        if completed and not is_comparison_example:
            current_app.logger.debug("Doing scoring")
            Comparison.update_scores_1vs1(comparison)
            #Comparison.calculate_scores(assignment.id)

        # update course & assignment grade for user if comparison is completed
        if completed:
            assignment.calculate_grade(current_user)
            course.calculate_grade(current_user)

        on_comparison_update.send(
            self,
            event_name=on_comparison_update.name,
            user=current_user,
            course_id=course.id,
            assignment=assignment,
            comparison=comparison,
            is_comparison_example=is_comparison_example,
            data=marshal(comparison, dataformat.get_comparison(restrict_user)))

        return {
            'comparison':
            marshal(
                comparison,
                dataformat.get_comparison(restrict_user,
                                          include_answer_author=False,
                                          include_score=False))
        }
Exemple #53
0
    def post(self, course_uuid, assignment_uuid, answer_uuid, answer_comment_uuid):
        """
        Update an answer comment
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)
        answer_comment = AnswerComment.get_active_by_uuid_or_404(answer_comment_uuid)
        require(EDIT, answer_comment,
            title="Feedback Not Saved",
            message="Sorry, your role in this course does not allow you to save feedback for this answer.")

        restrict_user = not allow(MANAGE, assignment)

        restrict_user = not allow(MANAGE, assignment)

        was_draft = answer_comment.draft

        params = existing_answer_comment_parser.parse_args()
        # make sure the answer comment id in the url and the id matches
        if params['id'] != answer_comment_uuid:
            abort(400, title="Feedback Not Saved", message="The feedback's ID does not match the URL, which is required in order to save the feedback.")

        # modify answer comment according to new values, preserve original values if values not passed
        answer_comment.content = params.get("content")

        comment_types = [
            AnswerCommentType.public.value,
            AnswerCommentType.private.value,
            AnswerCommentType.evaluation.value,
            AnswerCommentType.self_evaluation.value
        ]

        eval_comment_types = [
            AnswerCommentType.evaluation.value,
            AnswerCommentType.self_evaluation.value
        ]

        comment_type = params.get("comment_type", AnswerCommentType.private.value)
        if comment_type not in comment_types:
            abort(400, title="Feedback Not Saved", message="This feedback type is not recognized. Please contact support for assistance.")

        # do not allow changing a self-eval into a comment or vise-versa
        if (answer_comment.comment_type.value in eval_comment_types or comment_type in eval_comment_types) and answer_comment.comment_type.value != comment_type:
            abort(400, title="Feedback Not Saved", message="Feedback type cannot be changed. Please contact support for assistance.")

        answer_comment.comment_type = AnswerCommentType(comment_type)

        if answer_comment.comment_type == AnswerCommentType.self_evaluation and not assignment.self_eval_grace and not allow(MANAGE, assignment):
            abort(403, title="Self-Evaluation Not Saved", message="Sorry, the self-evaluation deadline has passed and therefore cannot be submitted.")

        # only update draft param if currently a draft
        if answer_comment.draft:
            answer_comment.draft = params.get('draft', answer_comment.draft)

        # require content not empty if not a draft
        if not answer_comment.content and not answer_comment.draft:
            abort(400, title="Feedback Not Saved", message="Please provide content in the text editor and try saving again.")

        answer_comment.update_attempt(
            params.get('attempt_uuid'),
            params.get('attempt_started', None),
            params.get('attempt_ended', None)
        )

        model_changes = get_model_changes(answer_comment)
        db.session.add(answer_comment)
        db.session.commit()

        on_answer_comment_modified.send(
            self,
            event_name=on_answer_comment_modified.name,
            user=current_user,
            course_id=course.id,
            answer_comment=answer_comment,
            evaluation_number=params.get("evaluation_number"),
            was_draft=was_draft,
            data=model_changes)

        # update course & assignment grade for user if self-evaluation is completed
        if not answer_comment.draft and answer_comment.comment_type == AnswerCommentType.self_evaluation:
            assignment.calculate_grade(answer_comment.user)
            course.calculate_grade(answer_comment.user)

        return marshal(answer_comment, dataformat.get_answer_comment(restrict_user))
Exemple #54
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))
Exemple #55
0
    def get(self, course_uuid, assignment_uuid, **kwargs):
        """
        :query string ids: a comma separated comment uuids to query
        :query string answer_ids: a comma separated answer uuids for answer filter
        :query string assignment_id: filter the answer comments with a assignment uuid
        :query string user_ids: a comma separated user uuids that own the comments
        :query string self_evaluation: indicate whether the result should include self-evaluation comments or self-evaluation only.
                Possible values: true, false or only. Default true.
        :query string evaluation: indicate whether the result should include evaluation comments or evaluation only.
                Possible values: true, false or only. Default true.
        :query string draft: indicate whether the result should include drafts for current user or not.
                Possible values: true, false or only. Default false.
        :reqheader Accept: the response content type depends on :mailheader:`Accept` header
        :resheader Content-Type: this depends on :mailheader:`Accept` header of request
        :statuscode 200: no error
        :statuscode 404: answers don't exist

        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        restrict_user = not allow(MANAGE, assignment)

        params = answer_comment_list_parser.parse_args()
        answer_uuids = []
        if 'answer_uuid' in kwargs:
            answer_uuids.append(kwargs['answer_uuid'])
        elif 'answer_ids' in params and params['answer_ids']:
            answer_uuids.extend(params['answer_ids'].split(','))

        if not answer_uuids and not params['ids'] and not params['assignment_id'] and not params['user_ids']:
            abort(404, title="Feedback Unavailable", message="There was a problem getting the feedback for this answer. Please try again.")

        conditions = []

        answers = Answer.query \
            .filter(
                Answer.assignment_id == assignment.id,
                Answer.active == True,
                Answer.draft == False,
                Answer.uuid.in_(answer_uuids)
            ) \
            .all() if answer_uuids else []
        if answer_uuids and not answers:
            # non-existing answer ids.
            abort(404, title="Feedback Unavailable", message="There was a problem getting the feedback for this answer. Please try again.")

        group = current_user.get_course_group(course.id)
        course_role = current_user.get_course_role(course.id)

        # build query condition for each answer
        for answer in answers:
            clauses = [AnswerComment.answer_id == answer.id]

            # student can only see the comments for themselves or public ones.
            # since the owner of the answer can access all comments. We only filter
            # on non-owners
            answer_owner = answer.user_id == current_user.id or (group and group.id == answer.group_id)
            if course_role == CourseRole.student and not answer_owner:
                # public comments or comments owned by current user
                clauses.append(or_(
                    AnswerComment.comment_type == AnswerCommentType.public,
                    AnswerComment.user_id == current_user.id
                ))

            conditions.append(and_(*clauses))

        query = AnswerComment.query \
            .filter(
                AnswerComment.assignment_id == assignment.id,
                AnswerComment.active==True,
                or_(*conditions)
            )

        if params['ids']:
            query = query.filter(AnswerComment.uuid.in_(params['ids'].split(',')))

        if params['self_evaluation'] == 'false':
            # do not include self-evaluation
            query = query.filter(AnswerComment.comment_type != AnswerCommentType.self_evaluation)
        elif params['self_evaluation'] == 'only':
            # only self_evaluation
            query = query.filter(AnswerComment.comment_type == AnswerCommentType.self_evaluation)

        if params['evaluation'] == 'false':
            # do not include evalulation comments
            query = query.filter(AnswerComment.comment_type != AnswerCommentType.evaluation)
        elif params['evaluation'] == 'only':
            # only evaluation
            query = query.filter(AnswerComment.comment_type == AnswerCommentType.evaluation)

        if params['draft'] == 'true':
            # with draft (current_user)
            query = query.filter(or_(
                AnswerComment.draft == False,
                and_(
                    AnswerComment.draft == True,
                    AnswerComment.user_id == current_user.id
                )
            ))
        elif params['draft'] == 'only':
            # only draft (current_user)
            query = query.filter(and_(
                AnswerComment.draft == True,
                AnswerComment.user_id == current_user.id
            ))
        else:
            # do not include draft. Default
            query = query.filter(AnswerComment.draft == False)

        if params['user_ids']:
            user_ids = params['user_ids'].split(',')
            query = query \
                .join(User, AnswerComment.user_id == User.id) \
                .filter(User.uuid.in_(user_ids))

        answer_comments = query.order_by(AnswerComment.created.desc()).all()

        # checking the permission
        for answer_comment in answer_comments:
            require(READ, answer_comment.answer,
                title="Feedback Unavailable",
                message="Sorry, your role in this course does not allow you to view feedback for this answer.")

        on_answer_comment_list_get.send(
            self,
            event_name=on_answer_comment_list_get.name,
            user=current_user,
            data={'answer_ids': ','.join([str(answer.id) for answer in answers])})

        return marshal(answer_comments, dataformat.get_answer_comment(restrict_user))
Exemple #56
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)
Exemple #57
0
    def get(self):
        params = user_course_status_list_parser.parse_args()
        course_uuids = params['ids'].split(',')

        if params['ids'] == '' or len(course_uuids) == 0:
            abort(
                400,
                title="Course Status Unavailable",
                message=
                "Please select a course from the list of courses to see that course's status."
            )

        query = Course.query \
            .filter(and_(
                Course.uuid.in_(course_uuids),
                Course.active == True,
            )) \
            .add_columns(UserCourse.course_role, UserCourse.group_id) \

        if not allow(MANAGE, Course):
            query = query.join(
                UserCourse,
                and_(UserCourse.user_id == current_user.id,
                     UserCourse.course_id == Course.id,
                     UserCourse.course_role != CourseRole.dropped))
        else:
            query = query.outerjoin(
                UserCourse,
                and_(UserCourse.user_id == current_user.id,
                     UserCourse.course_id == Course.id))

        results = query.all()

        if len(course_uuids) != len(results):
            abort(
                400,
                title="Course Status Unavailable",
                message=
                "Sorry, you are not enrolled in one or more of the selected users' courses yet. Course status is not available until your are enrolled in the course."
            )

        statuses = {}

        for course, course_role, group_id in results:
            incomplete_assignment_ids = set()
            if not allow(MANAGE, Course) and course_role == CourseRole.student:
                answer_period_assignments = [
                    assignment for assignment in course.assignments
                    if assignment.active and assignment.answer_period
                ]
                compare_period_assignments = [
                    assignment for assignment in course.assignments
                    if assignment.active and assignment.compare_period
                ]

                if len(answer_period_assignments) > 0:
                    answer_period_assignment_ids = [
                        assignment.id
                        for assignment in answer_period_assignments
                    ]
                    answers = Answer.query \
                        .filter(and_(
                            or_(
                                and_(Answer.group_id == group_id, Answer.group_id != None),
                                Answer.user_id == current_user.id,
                            ),
                            Answer.assignment_id.in_(answer_period_assignment_ids),
                            Answer.active == True,
                            Answer.practice == False,
                            Answer.draft == False
                        ))
                    for assignment in answer_period_assignments:
                        answer = next(
                            (answer for answer in answers
                             if answer.assignment_id == assignment.id), None)
                        if answer is None:
                            incomplete_assignment_ids.add(assignment.id)

                if len(compare_period_assignments) > 0:
                    compare_period_assignment_ids = [
                        assignment.id
                        for assignment in compare_period_assignments
                    ]
                    comparisons = Comparison.query \
                        .filter(and_(
                            Comparison.user_id == current_user.id,
                            Comparison.assignment_id.in_(compare_period_assignment_ids),
                            Comparison.completed == True
                        ))

                    self_evaluations = AnswerComment.query \
                        .join("answer") \
                        .with_entities(
                            Answer.assignment_id,
                            func.count(Answer.assignment_id).label('self_evaluation_count')
                        ) \
                        .filter(and_(
                            or_(
                                and_(Answer.group_id == group_id, Answer.group_id != None),
                                Answer.user_id == current_user.id,
                            ),
                            AnswerComment.active == True,
                            AnswerComment.comment_type == AnswerCommentType.self_evaluation,
                            AnswerComment.draft == False,
                            Answer.active == True,
                            Answer.practice == False,
                            Answer.draft == False,
                            Answer.assignment_id.in_(compare_period_assignment_ids)
                        )) \
                        .group_by(Answer.assignment_id) \
                        .all()

                    for assignment in compare_period_assignments:
                        assignment_comparisons = [
                            comparison for comparison in comparisons
                            if comparison.assignment_id == assignment.id
                        ]
                        if len(assignment_comparisons
                               ) < assignment.total_comparisons_required:
                            incomplete_assignment_ids.add(assignment.id)

                        if assignment.enable_self_evaluation:
                            self_evaluation_count = next(
                                (result.self_evaluation_count
                                 for result in self_evaluations
                                 if result.assignment_id == assignment.id), 0)
                            if self_evaluation_count == 0:
                                incomplete_assignment_ids.add(assignment.id)

            statuses[course.uuid] = {
                'incomplete_assignments': len(incomplete_assignment_ids)
            }

        on_user_course_status_get.send(
            self,
            event_name=on_user_course_status_get.name,
            user=current_user,
            data=statuses)

        return {"statuses": statuses}