Example #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 can(MANAGE, assignment)

        restrict_user = not can(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))
Example #2
0
def output_csv(data, code, headers=None):
    fieldnames = [
        'username', 'student_number', 'firstname', 'lastname', 'displayname',
        'group_name'
    ]

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

    if can(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
Example #3
0
    def if_can_delete_attachment_reference(file):
        for assignment in file.assignments.all():
            if can(DELETE, assignment):
                return True

        for answer in file.answers.all():
            if can(DELETE, answer):
                return True

        return False
Example #4
0
def context_processor():
    actions = {
        "blogs": {
            "create":
            flask_bouncer.can(action=flask_bouncer.CREATE,
                              subject=blog_models.Blog)
        },
        "categories": {
            "create":
            flask_bouncer.can(action=flask_bouncer.CREATE,
                              subject=category_models.Category)
        },
    }
    return dict(link_actions=actions)
Example #5
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 can(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 can(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}
Example #6
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 can(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))
Example #7
0
File: users.py Project: ubc/compair
    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 can(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}
Example #8
0
def is_user_access_restricted(user):
    """
    determine if the current user has full view of another user
    This provides a measure of anonymity among students, while instructors and above can see real names.
    Also restrict access during impersonation
    """
    # Determine if the logged in user can view full info on the target user
    access_restricted = not can(READ, user) or impersonation.is_impersonating()
    if access_restricted:
        enrolments = UserCourse.query.filter_by(user_id=user.id).all()
        # if the logged in user can edit the target user's enrolments, then we let them see full info
        for enrolment in enrolments:
            if can(EDIT, enrolment):
                access_restricted = False
                break

    return access_restricted
Example #9
0
File: users.py Project: ubc/compair
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 can(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)))
Example #10
0
File: users.py Project: ubc/compair
    def get(self):
        if can(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 can(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}
Example #11
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 can(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))
        }
Example #12
0
File: users.py Project: ubc/compair
    def get(self, user_uuid):
        user = User.get_by_uuid_or_404(user_uuid)
        available = can(EDIT, user)

        on_user_edit_button_get.send(
            self,
            event_name=on_user_edit_button_get.name,
            user=current_user,
            data={'user_id': user.id, 'available': available})

        return {'available': available}
Example #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,
            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 can(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 can(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))
Example #14
0
def edit_post_with_can(post_id):

    # Find an article form a db -- faking for testing
    mary = User(name='mary', admin=False)
    article = Article(author_id=mary.id)

    # bounce them out if they do not have access
    if not can(EDIT, article):
        return abort(401)

    # edit the post
    return "successfully edited post"
Example #15
0
def edit_post_with_can(post_id):

    # Find an article form a db -- faking for testing
    mary = User(name='mary', admin=False)
    article = Article(author_id=mary.id)

    # bounce them out if they do not have access
    if not can(EDIT, article):
        return abort(401)

    # edit the post
    return "successfully edited post"
Example #16
0
File: users.py Project: ubc/compair
    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 can(MANAGE, user) and not user.id == current_user.id and not \
                (can(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)
Example #17
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 can(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
Example #18
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 can(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))
        }
Example #19
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 can(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 content 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))}
Example #20
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 can(READ, USER_IDENTITY)

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

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

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

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

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

            class_list.append(_user)

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

        if can(MANAGE, User):
            return {
                'objects':
                marshal(class_list, dataformat.get_full_users_in_course())
            }
        else:
            return {
                'objects':
                marshal(
                    class_list,
                    dataformat.get_users_in_course(
                        restrict_user=restrict_user))
            }
Example #21
0
    def post(self, course_uuid, assignment_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        if not assignment.answer_grace and not can(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 can(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 can(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 can(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))
Example #22
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 can(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))
Example #23
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 can(MANAGE, assignment)

        restrict_user = not can(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 can(
                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))
Example #24
0
    def post(self, course_uuid, assignment_uuid, answer_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        if not assignment.answer_grace and not can(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 can(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 can(
                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 can(
                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))
Example #25
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 can(MANAGE, assignment)

        restrict_user = not can(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 can(
                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))
Example #26
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 can(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
        }
Example #27
0
def get_logged_in_user_permissions():
    user = User.query.get(current_user.id)
    require(READ, user)

    is_admin = user.system_role == SystemRole.sys_admin
    permissions = {}
    models = {
        User.__name__: user,
    }
    operations = [MANAGE, READ, EDIT, CREATE, DELETE]

    # global models
    for model_name, model in models.items():
        # create entry if not already exists
        permissions.setdefault(model_name, {})
        # if not model_name in permissions:
        # permissions[model_name] = {}
        # obtain permission values for each operations

        global_permissions = set()
        for operation in operations:
            if can(operation, model):
                global_permissions.add(operation)
        permissions[model_name]['global'] = list(global_permissions)

    # course model
    # model_name / courseId OR global / operation
    permissions['Course'] = {}
    mod_operations = [MANAGE, READ, EDIT, DELETE]
    course_global_permissions = set()

    for user_course in user.user_courses:
        if user_course.course_role == CourseRole.dropped:
            continue
        course_permissions = set()
        for operation in mod_operations:
            if can(operation, Course(id=user_course.course_id)):
                course_permissions.add(operation)
                course_global_permissions.add(operation)
        permissions['Course'][user_course.course_uuid] = list(
            course_permissions)

    if can(CREATE, Course):
        course_global_permissions.add(CREATE)
    if is_admin:
        for operation in mod_operations:
            course_global_permissions.add(operation)
    permissions['Course']['global'] = list(course_global_permissions)

    # assignment model
    # model_name / courseId OR global / operation
    permissions['Assignment'] = {}
    assignment_global_permissions = set()

    for user_course in user.user_courses:
        if user_course.course_role == CourseRole.dropped:
            continue
        assignment_permissions = set()
        for operation in operations:
            if can(operation, Assignment(course_id=user_course.course_id)):
                assignment_permissions.add(operation)
                assignment_global_permissions.add(operation)
        permissions['Assignment'][user_course.course_uuid] = list(
            assignment_permissions)

    if is_admin:
        for operation in operations:
            assignment_global_permissions.add(operation)
    permissions['Assignment']['global'] = list(assignment_global_permissions)

    return permissions
Example #28
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 can(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}
Example #29
0
File: users.py Project: ubc/compair
    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 can(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 can(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 can(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)
Example #30
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 can(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=can(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
        }
Example #31
0
File: users.py Project: ubc/compair
    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 can(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 can(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}