Beispiel #1
0
    def post(self):
        """
        Create new course
        """
        require(
            CREATE,
            Course,
            title="Course Not Saved",
            message=
            "Sorry, your role in the system does not allow you to save courses."
        )
        params = new_course_parser.parse_args()

        new_course = Course(name=params.get("name"),
                            year=params.get("year"),
                            term=params.get("term"),
                            sandbox=params.get("sandbox"),
                            start_date=params.get('start_date'),
                            end_date=params.get('end_date', None))
        if new_course.start_date is not None:
            new_course.start_date = datetime.datetime.strptime(
                new_course.start_date, '%Y-%m-%dT%H:%M:%S.%fZ')

        if new_course.end_date is not None:
            new_course.end_date = datetime.datetime.strptime(
                new_course.end_date, '%Y-%m-%dT%H:%M:%S.%fZ')

        if new_course.start_date and new_course.end_date and new_course.start_date > new_course.end_date:
            abort(400,
                  title="Course Not Saved",
                  message="Course end time must be after course start time.")

        try:
            # create the course
            db.session.add(new_course)
            # also need to enrol the user as an instructor
            new_user_course = UserCourse(course=new_course,
                                         user_id=current_user.id,
                                         course_role=CourseRole.instructor)
            db.session.add(new_user_course)

            db.session.commit()

        except exc.SQLAlchemyError as e:
            db.session.rollback()
            current_app.logger.error("Failed to add new course. " + str(e))
            raise

        on_course_create.send(self,
                              event_name=on_course_create.name,
                              user=current_user,
                              course=new_course,
                              data=marshal(new_course,
                                           dataformat.get_course()))

        return marshal(new_course, dataformat.get_course())
Beispiel #2
0
    def post(self):
        """
        Create new course
        """
        require(CREATE, Course,
            title="Course Not Saved",
            message="Sorry, your role in the system does not allow you to save courses.")
        params = new_course_parser.parse_args()

        new_course = Course(
            name=params.get("name"),
            year=params.get("year"),
            term=params.get("term"),
            sandbox=params.get("sandbox"),
            start_date=params.get('start_date'),
            end_date=params.get('end_date', None)
        )
        if new_course.start_date is not None:
            new_course.start_date = datetime.datetime.strptime(
                new_course.start_date,
                '%Y-%m-%dT%H:%M:%S.%fZ')

        if new_course.end_date is not None:
            new_course.end_date = datetime.datetime.strptime(
                new_course.end_date,
                '%Y-%m-%dT%H:%M:%S.%fZ')

        if new_course.start_date and new_course.end_date and new_course.start_date > new_course.end_date:
            abort(400, title="Course Not Saved", message="Course end time must be after course start time.")

        try:
            # create the course
            db.session.add(new_course)
            # also need to enrol the user as an instructor
            new_user_course = UserCourse(
                course=new_course,
                user_id=current_user.id,
                course_role=CourseRole.instructor
            )
            db.session.add(new_user_course)

            db.session.commit()

        except exc.SQLAlchemyError as e:
            db.session.rollback()
            current_app.logger.error("Failed to add new course. " + str(e))
            raise

        on_course_create.send(
            self,
            event_name=on_course_create.name,
            user=current_user,
            course=new_course,
            data=marshal(new_course, dataformat.get_course()))

        return marshal(new_course, dataformat.get_course())
Beispiel #3
0
    def post(self):
        """
        Create new course
        """
        require(CREATE, Course)
        params = new_course_parser.parse_args()

        new_course = Course(
            name=params.get("name"),
            year=params.get("year"),
            term=params.get("term"),
            description=params.get("description", None),
            start_date=params.get('start_date', None),
            end_date=params.get('end_date', None)
        )
        if new_course.start_date is not None:
            new_course.start_date = datetime.datetime.strptime(
                new_course.start_date,
                '%Y-%m-%dT%H:%M:%S.%fZ')

        if new_course.end_date is not None:
            new_course.end_date = datetime.datetime.strptime(
                new_course.end_date,
                '%Y-%m-%dT%H:%M:%S.%fZ')

        try:
            # create the course
            db.session.add(new_course)
            # also need to enrol the user as an instructor
            new_user_course = UserCourse(
                course=new_course,
                user_id=current_user.id,
                course_role=CourseRole.instructor
            )
            db.session.add(new_user_course)

            db.session.commit()

        except exc.SQLAlchemyError as e:
            db.session.rollback()
            current_app.logger.error("Failed to add new course. " + str(e))
            raise

        on_course_create.send(
            self,
            event_name=on_course_create.name,
            user=current_user,
            course=new_course,
            data=marshal(new_course, dataformat.get_course()))

        return marshal(new_course, dataformat.get_course())
Beispiel #4
0
    def post(self, course_uuid):
        """
        link current session's lti context with a course
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(EDIT, course,
            title="Course Not Linked",
            message="Sorry, you do not have permission to link this course since you are not enrolled as an instructor in the course.")

        if not sess.get('LTI'):
            abort(400, title="Course Not Linked",
                message="Sorry, your LTI session has expired. Please log in via LTI and try linking again.")

        if not sess.get('lti_context'):
            abort(400, title="Course Not Linked",
                message="Sorry, your LTI link settings have no course context. Please edit your LTI link settings and try linking again.")

        lti_context = LTIContext.query.get_or_404(sess.get('lti_context'))
        lti_context.compair_course_id = course.id
        db.session.commit()

        # automatically fetch membership if enabled for context
        if lti_context.membership_enabled:
            update_lti_course_membership.delay(course.id)

        on_lti_course_link_create.send(
            self,
            event_name=on_lti_course_link_create.name,
            user=current_user,
            data={ 'course_id': course.id, 'lti_context_id': lti_context.id })

        return { 'success': True }
Beispiel #5
0
    def post(self, course_uuid, user_uuid):
        """
        Enrol or update a user enrolment in the course

        The payload for the request has to contain course_role. e.g.
        {"couse_role":"Student"}

        :param course_uuid:
        :param user_uuid:
        :return:
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        user = User.get_by_uuid_or_404(user_uuid)

        user_course = UserCourse.query \
            .filter_by(
                user_id=user.id,
                course_id=course.id
            ) \
            .first()

        if not user_course:
            user_course = UserCourse(
                user_id=user.id,
                course_id=course.id
            )

        require(EDIT, user_course)

        params = new_course_user_parser.parse_args()
        role_name = params.get('course_role')

        course_roles = [
            CourseRole.dropped.value,
            CourseRole.student.value,
            CourseRole.teaching_assistant.value,
            CourseRole.instructor.value
        ]
        if role_name not in course_roles:
            abort(404)
        course_role = CourseRole(role_name)
        if user_course.course_role != course_role:
            user_course.course_role = course_role
            db.session.add(user_course)
            db.session.commit()

        result = {
            'user_id': user.uuid,
            'fullname': user.fullname,
            'course_role': course_role.value
        }

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

        return result
Beispiel #6
0
    def delete(self, course_uuid, assignment_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(DELETE, assignment,
            title="Assignment Not Deleted",
            message="Sorry, your role in this course does not allow you to delete assignments.")

        if current_app.config.get('DEMO_INSTALLATION', False):
            from data.fixtures import DemoDataFixture
            if assignment.id in DemoDataFixture.DEFAULT_ASSIGNMENT_IDS:
                abort(400, title="Assignment Not Deleted", message="Sorry, you cannot remove the default demo assignments.")

        formatted_assignment = marshal(assignment, dataformat.get_assignment(False))
        # delete file when assignment is deleted
        assignment.active = False
        assignment.clear_lti_links()
        db.session.commit()

        # update course grades
        course.calculate_grades()

        on_assignment_delete.send(
            self,
            event_name=on_assignment_delete.name,
            user=current_user,
            course_id=course.id,
            assignment=assignment,
            data=formatted_assignment)

        return {'id': assignment.uuid}
Beispiel #7
0
    def post(self, course_uuid):
        """
        refresh the course membership if able
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(EDIT, course)

        if not course.lti_linked:
            return {"error": "Course not linked to lti context"}, 400

        try:
            LTIMembership.update_membership_for_course(course)
        except MembershipNoValidContextsException as err:
            return {"error": "LTI membership service is not supported for this course"}, 400
        except MembershipNoResultsException as err:
            return {"error": "LTI membership service did not return any users"}, 400
        except MembershipInvalidRequestException as err:
            return (
                {
                    "error": "LTI membership request was invalid. Please relaunch the ComPAIR course from the LTI consumer and try again"
                },
                400,
            )

        on_lti_course_membership_update.send(
            self, event_name=on_lti_course_membership_update.name, user=current_user, data={"course_id": course.id}
        )

        return {"imported": True}
Beispiel #8
0
    def delete(self, course_uuid, assignment_uuid, comparison_example_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        comparison_example = ComparisonExample.get_active_by_uuid_or_404(
            comparison_example_uuid)
        require(
            DELETE,
            comparison_example,
            title="Comparison Example Not Deleted",
            message=
            "Sorry, your role in this course does not allow you to delete practice answers."
        )

        formatted_comparison_example = marshal(
            comparison_example,
            dataformat.get_comparison_example(with_answers=False))

        comparison_example.active = False
        db.session.add(comparison_example)
        db.session.commit()

        on_comparison_example_delete.send(
            self,
            event_name=on_comparison_example_delete.name,
            user=current_user,
            course_id=course.id,
            data=formatted_comparison_example)

        return {'id': comparison_example.uuid}
Beispiel #9
0
    def post(self, course_uuid, assignment_uuid, answer_uuid):
        """
        Mark an answer as being a top answer
        :param course_uuid:
        :param assignment_uuid:
        :param answer_uuid:
        :return: marked answer
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)

        require(MANAGE, answer,
            title="Answer Not Added",
            message="Your role in this course does not allow you to add to the list of instructor-picked answers.")

        params = top_answer_parser.parse_args()
        answer.top_answer = params.get('top_answer')
        db.session.add(answer)

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

        db.session.commit()

        return marshal(answer, dataformat.get_answer(restrict_user=False))
Beispiel #10
0
    def delete(self, course_uuid, assignment_uuid, assignment_comment_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        assignment_comment = AssignmentComment.get_active_by_uuid_or_404(
            assignment_comment_uuid)
        require(
            DELETE,
            assignment_comment,
            title="Help Comment Not Deleted",
            message=
            "Sorry, your role in this course does not allow you to delete help comments."
        )

        data = marshal(assignment_comment,
                       dataformat.get_assignment_comment(False))
        assignment_comment.active = False
        db.session.commit()

        on_assignment_comment_delete.send(
            self,
            event_name=on_assignment_comment_delete.name,
            user=current_user,
            course_id=course.id,
            assignment_comment=assignment_comment,
            data=data)

        return {'id': assignment_comment.uuid}
Beispiel #11
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."
        )

        instructors = UserCourse.query \
            .filter(and_(
                UserCourse.course_id==course.id,
                UserCourse.course_role.in_([CourseRole.teaching_assistant, CourseRole.instructor])
            )) \
            .all()
        instructor_uuids = {
            u.user_uuid: u.course_role.value
            for u in instructors
        }

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

        return {'instructors': instructor_uuids}
Beispiel #12
0
    def get(self, course_uuid, assignment_uuid, answer_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

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

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

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

        return marshal(
            answer,
            dataformat.get_answer(restrict_user, include_score=include_score))
Beispiel #13
0
    def delete(self, course_uuid, user_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        user = User.get_by_uuid_or_404(user_uuid)
        user_course = UserCourse.query \
            .filter_by(
                user_id=user.id,
                course_id=course.id
            ) \
            .first_or_404()
        require(EDIT, user_course)

        user_course.course_role = CourseRole.dropped
        result = {
            'user_id': user.uuid,
            'fullname': user.fullname,
            'course_role': CourseRole.dropped.value
        }

        db.session.add(user_course)

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

        db.session.commit()
        return result
Beispiel #14
0
    def delete(self, course_uuid, assignment_uuid, answer_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)
        require(DELETE, answer,
            title="Answer Not Deleted",
            message="Sorry, your role in this course does not allow you to delete this answer.")

        if current_app.config.get('DEMO_INSTALLATION', False):
            from data.fixtures import DemoDataFixture
            if assignment.id in DemoDataFixture.DEFAULT_ASSIGNMENT_IDS and answer.user_id in DemoDataFixture.DEFAULT_STUDENT_IDS:
                abort(400, title="Answer Not Deleted", message="Sorry, you cannot delete the default student demo answers.")

        answer.active = False
        db.session.commit()

        # update course & assignment grade for user if answer was fully submitted
        if not answer.draft:
            if answer.user:
                assignment.calculate_grade(answer.user)
                course.calculate_grade(answer.user)
            elif answer.group:
                assignment.calculate_group_grade(answer.group)
                course.calculate_group_grade(answer.group)

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

        return {'id': answer.uuid}
Beispiel #15
0
    def get(self, course_uuid, assignment_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(
            READ,
            assignment,
            title="Help Comments Unavailable",
            message=
            "Help comments can be seen only by those enrolled in the course. Please double-check your enrollment in this course."
        )
        restrict_user = not allow(MANAGE, assignment)

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

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

        return {
            "objects":
            marshal(assignment_comments,
                    dataformat.get_assignment_comment(restrict_user))
        }
Beispiel #16
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="Reply Unavailable",
            message=
            "Sorry, your role in this course does not allow you to view this reply."
        )

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

        return marshal(answer_comment, dataformat.get_answer_comment())
Beispiel #17
0
    def post(self, course_uuid, user_uuid, group_name):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        user = User.get_by_uuid_or_404(user_uuid)

        user_course = UserCourse.query.filter(
            and_(
                UserCourse.course_id == course.id,
                UserCourse.user_id == user.id,
                UserCourse.course_role != CourseRole.dropped,
            )
        ).first_or_404()

        require(EDIT, user_course)

        user_course.group_name = group_name
        db.session.commit()

        on_course_group_user_create.send(
            current_app._get_current_object(),
            event_name=on_course_group_user_create.name,
            user=current_user,
            course_id=course.id,
            data={"user_id": user.id},
        )

        return {"group_name": group_name}
Beispiel #18
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,
            ComparisonExample(course_id=course.id),
            title="Comparison Example Unavailable",
            message=
            "Sorry, your role in this course does not allow you to view practice answers."
        )

        # Get all comparison examples for this assignment
        comparison_examples = ComparisonExample.query \
            .filter_by(
                active=True,
                assignment_id=assignment.id
            ) \
            .all()

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

        return {
            "objects":
            marshal(comparison_examples, dataformat.get_comparison_example())
        }
Beispiel #19
0
    def delete(self, course_uuid, assignment_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        require(DELETE, assignment,
            title="Assignment Not Deleted",
            message="Sorry, your role in this course does not allow you to delete assignments.")

        if current_app.config.get('DEMO_INSTALLATION', False):
            from data.fixtures import DemoDataFixture
            if assignment.id in DemoDataFixture.DEFAULT_ASSIGNMENT_IDS:
                abort(400, title="Assignment Not Deleted", message="Sorry, you cannot remove the default demo assignments.")

        formatted_assignment = marshal(assignment, dataformat.get_assignment(False))
        # delete file when assignment is deleted
        assignment.active = False
        assignment.clear_lti_links()
        db.session.commit()

        # update course grades
        course.calculate_grades()

        on_assignment_delete.send(
            self,
            event_name=on_assignment_delete.name,
            user=current_user,
            course_id=course.id,
            assignment=assignment,
            data=formatted_assignment)

        return {'id': assignment.uuid}
Beispiel #20
0
    def delete(self, course_uuid, assignment_uuid, answer_uuid, answer_comment_uuid):
        """
        Delete an answer comment
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer_comment = AnswerComment.get_active_by_uuid_or_404(answer_comment_uuid)
        require(DELETE, answer_comment,
            title="Feedback Not Deleted",
            message="Sorry, your role in this course does not allow you to delete feedback for this answer.")

        data = marshal(answer_comment, dataformat.get_answer_comment(False))
        answer_comment.active = False
        db.session.commit()

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

        on_answer_comment_delete.send(
            self,
            event_name=on_answer_comment_delete.name,
            user=current_user,
            course_id=course.id,
            answer_comment=answer_comment,
            data=data)

        return {'id': answer_comment.uuid}
Beispiel #21
0
    def get(self, course_uuid, group_name):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        user_course = UserCourse(course_id=course.id)
        require(READ, user_course)

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

        if len(members) == 0:
            abort(404)

        on_course_group_members_get.send(
            current_app._get_current_object(),
            event_name=on_course_group_members_get.name,
            user=current_user,
            course_id=course.id,
            data={'group_name': group_name})

        return {'students': [{'id': u.uuid, 'name': u.fullname} for u in members]}
Beispiel #22
0
    def post(self, course_uuid):
        """
        link current session's lti context with a course
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(EDIT, course)

        if not sess.get("LTI"):
            return {"error": "Your LTI session has expired."}, 404

        if not sess.get("lti_context"):
            return {"error": "Your LTI session has no context."}, 404

        lti_context = LTIContext.query.get_or_404(sess.get("lti_context"))
        lti_context.compair_course_id = course.id
        db.session.commit()

        # automatically fetch membership if enabled for context
        if lti_context.ext_ims_lis_memberships_url and lti_context.ext_ims_lis_memberships_id:
            update_lti_course_membership.delay(course.id)

        on_lti_course_link.send(
            self,
            event_name=on_lti_course_link.name,
            user=current_user,
            data={"course_id": course.id, "lti_context_id": lti_context.id},
        )

        return {"success": True}
Beispiel #23
0
    def delete(self, course_uuid, assignment_uuid, answer_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)
        require(DELETE, answer)

        answer.active = False
        if answer.file:
            answer.file.active = False
        db.session.commit()

        # update course & assignment grade for user if answer was fully submitted
        if not answer.draft:
            assignment.calculate_grade(answer.user)
            course.calculate_grade(answer.user)

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

        return {'id': answer.uuid}
Beispiel #24
0
    def get(self, course_uuid):
        """
        refresh the course membership if able
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(EDIT, course)

        if not course.lti_linked:
            return {"error": "Course not linked to lti context"}, 400

        valid_membership_contexts = [
            lti_context
            for lti_context in course.lti_contexts
            if lti_context.ext_ims_lis_memberships_url and lti_context.ext_ims_lis_memberships_id
        ]

        pending = 0
        enabled = len(valid_membership_contexts) > 0
        if enabled:
            lti_context_ids = [lti_context.id for lti_context in valid_membership_contexts]

            pending = (
                LTIMembership.query.join(LTIUser)
                .filter(and_(LTIUser.compair_user_id == None, LTIMembership.lti_context_id.in_(lti_context_ids)))
                .count()
            )

        status = {"enabled": enabled, "pending": pending}

        on_lti_course_membership_status_get.send(
            self, event_name=on_lti_course_membership_status_get.name, user=current_user, data={"course_id": course.id}
        )

        return {"status": status}
Beispiel #25
0
    def post(self, course_uuid, assignment_uuid, answer_uuid):
        """
        Mark an answer as being a top answer
        :param course_uuid:
        :param assignment_uuid:
        :param answer_uuid:
        :return: marked answer
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)

        require(MANAGE, answer)

        params = top_answer_parser.parse_args()
        answer.top_answer = params.get('top_answer')
        db.session.add(answer)

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

        db.session.commit()
        return marshal(answer, dataformat.get_answer(restrict_user=False))
Beispiel #26
0
    def delete(self, course_uuid, assignment_uuid, answer_uuid, answer_comment_uuid):
        """
        Delete an answer comment
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer_comment = AnswerComment.get_active_by_uuid_or_404(answer_comment_uuid)
        require(DELETE, answer_comment)

        data = marshal(answer_comment, dataformat.get_answer_comment(False))
        answer_comment.active = False
        db.session.commit()

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

        on_answer_comment_delete.send(
            self,
            event_name=on_answer_comment_delete.name,
            user=current_user,
            course_id=course.id,
            answer_comment=answer_comment,
            data=data,
        )

        return {"id": answer_comment.uuid}
Beispiel #27
0
    def delete(self, course_uuid, assignment_uuid, answer_uuid,
               answer_comment_uuid):
        """
        Delete an answer comment
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer_comment = AnswerComment.get_active_by_uuid_or_404(
            answer_comment_uuid)
        require(
            DELETE,
            answer_comment,
            title="Reply Not Deleted",
            message=
            "Sorry, your role in this course does not allow you to delete replies for this answer."
        )

        data = marshal(answer_comment, dataformat.get_answer_comment(False))
        answer_comment.active = False
        db.session.commit()

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

        on_answer_comment_delete.send(self,
                                      event_name=on_answer_comment_delete.name,
                                      user=current_user,
                                      course_id=course.id,
                                      answer_comment=answer_comment,
                                      data=data)

        return {'id': answer_comment.uuid}
Beispiel #28
0
    def post(self, course_uuid):
        """
        refresh the course membership if able
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(EDIT, course,
            title="Membership Not Updated",
            message="Sorry, your role in this course does not allow you to update membership.")

        if not course.lti_linked:
            abort(400, title="Membership Not Updated",
                message="Sorry, your LTI link settings have no course context. Please edit your LTI link settings and try linking again.")

        try:
            LTIMembership.update_membership_for_course(course)
        except MembershipNoValidContextsException as err:
            abort(400, title="Membership Not Updated",
                message="The LTI link does not support the membership extension. Please edit your LTI link settings or contact your system administrator and try again.")
        except MembershipNoResultsException as err:
            abort(400, title="Membership Not Updated",
                message="The membership service did not return any users. Please check your LTI course and try again.")
        except MembershipInvalidRequestException as err:
            abort(400, title="Membership Not Updated",
                message="The membership request was invalid. Please relaunch the LTI link and try again.")

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

        return { 'imported': True }
Beispiel #29
0
    def get(self, course_uuid, group_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        group = Group.get_active_by_uuid_or_404(group_uuid)

        require(
            READ,
            UserCourse(course_id=course.id),
            title="Group Members Unavailable",
            message=
            "Group membership can be seen only by those enrolled in the course. Please double-check your enrollment in this course."
        )

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

        on_group_user_list_get.send(current_app._get_current_object(),
                                    event_name=on_group_user_list_get.name,
                                    user=current_user,
                                    course_id=course.id,
                                    data={'group_id': group.id})

        return {
            'objects': [{
                'id': u.uuid,
                'name': u.fullname_sortable
            } for u in members]
        }
Beispiel #30
0
    def delete(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(
            DELETE,
            course,
            title="Course Not Deleted",
            message=
            "Sorry, your role in this course does not allow you to delete it.")

        if current_app.config.get('DEMO_INSTALLATION', False):
            from data.fixtures import DemoDataFixture
            if course.id == DemoDataFixture.DEFAULT_COURSE_ID:
                abort(
                    400,
                    title="Course Not Deleted",
                    message="Sorry, you cannot remove the default demo course."
                )

        course.active = False
        course.clear_lti_links()
        db.session.commit()

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

        return {'id': course.uuid}
Beispiel #31
0
    def delete(self, course_uuid, lti_context_uuid):
        """
        unlink lti context from course
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        lti_context = LTIContext.get_by_uuid_or_404(lti_context_uuid)
        require(DELETE, lti_context,
            title="Course Not Unlinked",
            message="Sorry, your system role does not allow you to unlink LTI courses.")

        if lti_context.compair_course_id != course.id:
            abort(400, title="Course Not Unlinked", message="Sorry, The LTI context is already not linked to the course.")

        lti_context.compair_course_id = None
        db.session.commit()

        # automatically refresh membership if it was enabled for the removed context
        if lti_context.membership_enabled:
            update_lti_course_membership.delay(course.id)

        on_lti_course_unlink.send(
            self,
            event_name=on_lti_course_unlink.name,
            user=current_user,
            data={ 'course_id': course.id, 'lti_context_id': lti_context.id })

        return { 'success': True }
Beispiel #32
0
    def post(self, course_uuid, assignment_uuid, comparison_example_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        comparison_example = ComparisonExample.get_active_by_uuid_or_404(comparison_example_uuid)
        require(EDIT, comparison_example)

        params = existing_comparison_example_parser.parse_args()
        answer1_uuid = params.get("answer1_id")
        answer2_uuid = params.get("answer2_id")

        if answer1_uuid:
            answer1 = Answer.get_active_by_uuid_or_404(answer1_uuid)
            answer1.practice = True
            comparison_example.answer1 = answer1
        else:
            return {"error": "Comparison examples must have 2 answers"}, 400

        if answer2_uuid:
            answer2 = Answer.get_active_by_uuid_or_404(answer2_uuid)
            answer2.practice = True
            comparison_example.answer2 = answer2
        else:
            return {"error": "Comparison examples must have 2 answers"}, 400

        on_comparison_example_modified.send(
            self,
            event_name=on_comparison_example_modified.name,
            user=current_user,
            course_id=course.id,
            data=get_model_changes(comparison_example))

        db.session.add(comparison_example)
        db.session.commit()

        return marshal(comparison_example, dataformat.get_comparison_example())
Beispiel #33
0
    def post(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(
            EDIT,
            course,
            title="Course Not Saved",
            message=
            "Sorry, your role in this course does not allow you to save changes to it."
        )

        if current_app.config.get('DEMO_INSTALLATION', False):
            from data.fixtures import DemoDataFixture
            if course.id == DemoDataFixture.DEFAULT_COURSE_ID:
                abort(
                    400,
                    title="Course Not Updated",
                    message="Sorry, you cannot edit the default demo course.")

        params = existing_course_parser.parse_args()

        # make sure the course id in the url and the course id in the params match
        if params['id'] != course_uuid:
            abort(
                400,
                title="Course Not Saved",
                message=
                "The course's ID does not match the URL, which is required in order to save the course."
            )

        # modify course according to new values, preserve original values if values not passed
        course.name = params.get("name", course.name)
        course.year = params.get("year", course.year)
        course.term = params.get("term", course.term)
        course.sandbox = params.get("sandbox", course.sandbox)

        course.start_date = params.get("start_date")
        if course.start_date is not None:
            course.start_date = datetime.datetime.strptime(
                course.start_date, '%Y-%m-%dT%H:%M:%S.%fZ')

        course.end_date = params.get("end_date", None)
        if course.end_date is not None:
            course.end_date = datetime.datetime.strptime(
                course.end_date, '%Y-%m-%dT%H:%M:%S.%fZ')

        if course.start_date and course.end_date and course.start_date > course.end_date:
            abort(400,
                  title="Course Not Saved",
                  message="Course end time must be after course start time.")

        model_changes = get_model_changes(course)
        db.session.commit()

        on_course_modified.send(self,
                                event_name=on_course_modified.name,
                                user=current_user,
                                course=course,
                                data=model_changes)

        return marshal(course, dataformat.get_course())
Beispiel #34
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)
        require(CREATE, AssignmentComment(course_id=course.id))

        new_assignment_comment = AssignmentComment(assignment_id=assignment.id)

        params = new_assignment_comment_parser.parse_args()

        new_assignment_comment.content = params.get("content")
        if not new_assignment_comment.content:
            return {"error": "The comment content is empty!"}, 400

        new_assignment_comment.user_id = current_user.id

        db.session.add(new_assignment_comment)
        db.session.commit()

        on_assignment_comment_create.send(
            self,
            event_name=on_assignment_comment_create.name,
            user=current_user,
            course_id=course.id,
            assignment_comment=new_assignment_comment,
            data=marshal(new_assignment_comment, dataformat.get_assignment_comment(False)))

        return marshal(new_assignment_comment, dataformat.get_assignment_comment())
Beispiel #35
0
    def post(self, course_uuid, assignment_uuid, assignment_comment_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        assignment_comment = AssignmentComment.get_active_by_uuid_or_404(assignment_comment_uuid)
        require(EDIT, assignment_comment)

        params = existing_assignment_comment_parser.parse_args()
        # make sure the comment id in the rul and the id matches
        if params['id'] != assignment_comment_uuid:
            return {"error": "Comment id does not match URL."}, 400

        # modify comment according to new values, preserve original values if values not passed
        if not params.get("content"):
            return {"error": "The comment content is empty!"}, 400

        assignment_comment.content = params.get("content")
        db.session.add(assignment_comment)

        on_assignment_comment_modified.send(
            self,
            event_name=on_assignment_comment_modified.name,
            user=current_user,
            course_id=course.id,
            assignment_comment=assignment_comment,
            data=get_model_changes(assignment_comment))

        db.session.commit()
        return marshal(assignment_comment, dataformat.get_assignment_comment())
Beispiel #36
0
    def delete(self, course_uuid, user_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        user = User.get_by_uuid_or_404(user_uuid)
        user_course = UserCourse.query \
            .filter_by(
                course_id=course.id,
                user_id=user.id
            ) \
            .first_or_404()
        require(
            EDIT,
            user_course,
            title="Group Not Saved",
            message=
            "Sorry, your role in this course does not allow you to save groups."
        )

        user_course.group_name = None
        db.session.commit()

        on_course_group_user_delete.send(
            current_app._get_current_object(),
            event_name=on_course_group_user_delete.name,
            user=current_user,
            course_id=course.id,
            data={'user_id': user.id})

        return {'user_id': user.uuid, 'course_id': course.uuid}
Beispiel #37
0
    def post(self, course_uuid, user_uuid, group_name):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        user = User.get_by_uuid_or_404(user_uuid)

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

        require(
            EDIT,
            user_course,
            title="Group Not Saved",
            message=
            "Sorry, your role in this course does not allow you to save groups."
        )

        user_course.group_name = group_name
        db.session.commit()

        on_course_group_user_create.send(
            current_app._get_current_object(),
            event_name=on_course_group_user_create.name,
            user=current_user,
            course_id=course.id,
            data={'user_id': user.id})

        return {'group_name': group_name}
Beispiel #38
0
    def get(self, course_uuid, assignment_uuid):
        # course unused, but we need to call it to check if it's a valid course
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        # permission checks
        require(
            MANAGE,
            assignment,
            title="Unable to download attachments",
            message=
            "Sorry, your system role does not allow downloading all attachments"
        )

        # grab answers so we can see how many has files
        answers = self.getStudentAnswersByAssignment(assignment)
        fileIds = []
        fileAuthors = {}
        for answer in answers:
            if not answer.file_id:
                continue
            # answer has an attachment
            fileIds.append(answer.file_id)
            # the user who uploaded the file can be different from the answer
            # author (e.g. instructor can upload on behalf of student), so
            # we need to use the answer author instead of file uploader
            author = answer.user_fullname
            if answer.user_student_number:
                author += self.DELIM + answer.user_student_number
            fileAuthors[answer.file_id] = author

        if not fileIds:
            return {'msg': 'Assignment has no attachments'}

        # grab files so we can get the real file location
        files = self.getFilesByIds(fileIds)

        # zip up the tmp dir and save it to the report dir
        zipName = '{} [attachments-{}].zip'.format(assignment.name,
                                                   assignment.uuid)
        zipPath = '{}/{}'.format(current_app.config['REPORT_FOLDER'], zipName)
        # we're using compression level 6 as a compromise between speed &
        # compression (this is Z_DEFAULT_COMPRESSION in zlib)
        with zipfile.ZipFile(zipPath, 'w', zipfile.ZIP_DEFLATED, True,
                             6) as zipFile:
            for srcFile in files:
                srcFilePath = '{}/{}'.format(
                    current_app.config['ATTACHMENT_UPLOAD_FOLDER'],
                    srcFile.name)
                # filename should be 'full name - student number - uuid.ext'
                # student number is omitted if user doesn't have one
                srcFileName = fileAuthors[
                    srcFile.id] + self.DELIM + srcFile.name
                #current_app.logger.debug("writing " + srcFileName)
                zipFile.write(srcFilePath, srcFileName)
        #current_app.logger.debug("Writing zip file")

        return {'file': 'report/' + zipName, 'filename': zipName}
Beispiel #39
0
    def get(self, course_uuid, assignment_uuid):
        """
        Get answers submitted to the assignment submitted by current user

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

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

        params = user_answer_list_parser.parse_args()

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

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

        answers = query.all()

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

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

        on_course_get.send(
            self,
            event_name=on_course_get.name,
            user=current_user,
            data={'id': course.id})
        return marshal(course, dataformat.get_course())
Beispiel #41
0
    def post(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(EDIT, UserCourse(course_id=course.id))

        params = update_users_course_role_parser.parse_args()

        role_name = params.get('course_role')
        course_roles = [
            CourseRole.dropped.value,
            CourseRole.student.value,
            CourseRole.teaching_assistant.value,
            CourseRole.instructor.value
        ]
        if role_name not in course_roles:
            abort(404)
        course_role = CourseRole(role_name)

        if len(params.get('ids')) == 0:
            return {"error": "Please select at least one user below"}, 400

        user_courses = UserCourse.query \
            .join(User, UserCourse.user_id == User.id) \
            .filter(and_(
                UserCourse.course_id == course.id,
                User.uuid.in_(params.get('ids')),
                UserCourse.course_role != CourseRole.dropped
            )) \
            .all()

        if len(params.get('ids')) != len(user_courses):
            return {"error": "One or more users are not enrolled in the course"}, 400

        if len(user_courses) == 1 and user_courses[0].user_id == current_user.id:
            if course_role == CourseRole.dropped:
                return {"error": "You cannot drop yourself from the course. Please select other users"}, 400
            else:
                return {"error": "You cannot change your own course role. Please select other users"}, 400

        for user_course in user_courses:
            # skip current user
            if user_course.user_id == current_user.id:
                continue
            # update user's role'
            user_course.course_role = course_role

        db.session.commit()

        on_classlist_update_users_course_roles.send(
            current_app._get_current_object(),
            event_name=on_classlist_update_users_course_roles.name,
            user=current_user,
            course_id=course.id,
            data={'user_uuids': params.get('ids'), 'course_role': role_name})

        return {'course_role': role_name}
Beispiel #42
0
    def post(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        user_course = UserCourse(course_id=course.id)
        require(EDIT, user_course,
            title="Class List Not Imported",
            message="Sorry, your role in this course does not allow you to import or otherwise change the class list.")

        if current_app.config.get('DEMO_INSTALLATION', False):
            from data.fixtures import DemoDataFixture
            if course.id == DemoDataFixture.DEFAULT_COURSE_ID:
                abort(400, title="Class List Not Imported", message="Sorry, you cannot import users for the default demo course.")

        params = import_classlist_parser.parse_args()
        import_type = params.get('import_type')

        if import_type not in [ThirdPartyType.cas.value, ThirdPartyType.saml.value, None]:
            abort(400, title="Class List Not Imported", message="Please select a way for students to log in and try importing again.")
        elif import_type == ThirdPartyType.cas.value and not current_app.config.get('CAS_LOGIN_ENABLED'):
            abort(400, title="Class List Not Imported", message="Please select another way for students to log in and try importing again. Students are not able to use CWL logins based on the current settings.")
        elif import_type == ThirdPartyType.saml.value and not current_app.config.get('SAML_LOGIN_ENABLED'):
            abort(400, title="Class List Not Imported", message="Please select another way for students to log in and try importing again. Students are not able to use CWL logins based on the current settings.")
        elif import_type is None and not current_app.config.get('APP_LOGIN_ENABLED'):
            abort(400, title="Class List Not Imported", message="Please select another way for students to log in and try importing again. Students are not able to use the ComPAIR logins based on the current settings.")

        uploaded_file = request.files['file']
        results = {'success': 0, 'invalids': []}

        if not uploaded_file:
            abort(400, title="Class List Not Imported", message="No file was found to upload. Please try uploading again.")
        elif not allowed_file(uploaded_file.filename, current_app.config['UPLOAD_ALLOWED_EXTENSIONS']):
            abort(400, title="Class List Not Imported", message="Sorry, only CSV files can be imported. Please try again with a CSV file.")

        unique = str(uuid.uuid4())
        filename = unique + secure_filename(uploaded_file.filename)
        tmp_name = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
        uploaded_file.save(tmp_name)
        current_app.logger.debug("Importing for course " + str(course.id) + " with " + filename)
        with open(tmp_name, 'rb') as csvfile:
            spamreader = csv.reader(csvfile)
            users = []
            for row in spamreader:
                if row:
                    users.append(row)

            if len(users) > 0:
                results = import_users(import_type, course, users)

            on_classlist_upload.send(
                self,
                event_name=on_classlist_upload.name,
                user=current_user,
                course_id=course.id)
        os.remove(tmp_name)
        current_app.logger.debug("Class Import for course " + str(course.id) + " is successful. Removed file.")
        return results
Beispiel #43
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)
        require(
            CREATE,
            ComparisonExample(assignment=Assignment(course_id=course.id)),
            title="Comparison Example Not Saved",
            message=
            "Sorry, your role in this course does not allow you to save practice answers."
        )

        new_comparison_example = ComparisonExample(assignment_id=assignment.id)

        params = new_comparison_example_parser.parse_args()
        answer1_uuid = params.get("answer1_id")
        answer2_uuid = params.get("answer2_id")

        if answer1_uuid:
            answer1 = Answer.get_active_by_uuid_or_404(answer1_uuid)
            answer1.practice = True
            new_comparison_example.answer1 = answer1
        else:
            abort(
                400,
                title="Comparison Example Not Saved",
                message=
                "Please add two answers with content to the practice answers and try again."
            )

        if answer2_uuid:
            answer2 = Answer.get_active_by_uuid_or_404(answer2_uuid)
            answer2.practice = True
            new_comparison_example.answer2 = answer2
        else:
            abort(
                400,
                title="Comparison Example Not Saved",
                message=
                "Please add two answers with content to the practice answers and try again."
            )

        on_comparison_example_create.send(
            self,
            event_name=on_comparison_example_create.name,
            user=current_user,
            course_id=course.id,
            data=marshal(
                new_comparison_example,
                dataformat.get_comparison_example(with_answers=False)))

        db.session.add(new_comparison_example)
        db.session.commit()

        return marshal(new_comparison_example,
                       dataformat.get_comparison_example())
Beispiel #44
0
    def post(self, course_uuid, assignment_uuid, answer_uuid):
        """
        Create comment for an answer
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        answer = Answer.get_active_by_uuid_or_404(answer_uuid)
        require(CREATE, AnswerComment(course_id=course.id))

        answer_comment = AnswerComment(answer_id=answer.id)

        params = new_answer_comment_parser.parse_args()
        answer_comment.draft = params.get("draft")
        answer_comment.content = params.get("content")
        # require content not empty if not a draft
        if not answer_comment.content and not answer_comment.draft:
            return {"error": "The comment content is empty!"}, 400

        if params.get("user_id") and current_user.system_role == SystemRole.sys_admin:
            user = User.get_by_uuid_or_404(params.get("user_id"))
            answer_comment.user_id = user.id
        else:
            answer_comment.user_id = current_user.id

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

        comment_type = params.get("comment_type")
        if comment_type not in comment_types:
            abort(400)
        answer_comment.comment_type = AnswerCommentType(comment_type)

        db.session.add(answer_comment)
        db.session.commit()

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

        on_answer_comment_create.send(
            self,
            event_name=on_answer_comment_create.name,
            user=current_user,
            course_id=course.id,
            answer_comment=answer_comment,
            data=marshal(answer_comment, dataformat.get_answer_comment(False)),
        )

        return marshal(answer_comment, dataformat.get_answer_comment())
Beispiel #45
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, course,
            title="Course Unavailable",
            message="Courses can be seen only by those enrolled in them. Please double-check your enrollment in this course.")

        on_course_get.send(
            self,
            event_name=on_course_get.name,
            user=current_user,
            data={'id': course.id})
        return marshal(course, dataformat.get_course())
Beispiel #46
0
    def post(self, course_uuid, assignment_uuid, comparison_example_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)
        comparison_example = ComparisonExample.get_active_by_uuid_or_404(
            comparison_example_uuid)
        require(
            EDIT,
            comparison_example,
            title="Comparison Example Not Saved",
            message=
            "Sorry, your role in this course does not allow you to save practice answers."
        )

        params = existing_comparison_example_parser.parse_args()
        answer1_uuid = params.get("answer1_id")
        answer2_uuid = params.get("answer2_id")

        if answer1_uuid:
            answer1 = Answer.get_active_by_uuid_or_404(answer1_uuid)
            answer1.practice = True
            comparison_example.answer1 = answer1
        else:
            abort(
                400,
                title="Comparison Example Not Saved",
                message=
                "Please add two answers with content to the practice answers and try again."
            )

        if answer2_uuid:
            answer2 = Answer.get_active_by_uuid_or_404(answer2_uuid)
            answer2.practice = True
            comparison_example.answer2 = answer2
        else:
            abort(
                400,
                title="Comparison Example Not Saved",
                message=
                "Please add two answers with content to the practice answers and try again."
            )

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

        on_comparison_example_modified.send(
            self,
            event_name=on_comparison_example_modified.name,
            user=current_user,
            course_id=course.id,
            data=model_changes)

        return marshal(comparison_example, dataformat.get_comparison_example())
Beispiel #47
0
    def get(self, course_uuid, assignment_uuid):
        # course unused, but we need to call it to check if it's a valid course
        course = Course.get_active_by_uuid_or_404(course_uuid)
        assignment = Assignment.get_active_by_uuid_or_404(assignment_uuid)

        # permission checks
        require(
            MANAGE,
            assignment,
            title="Unable to download attachments",
            message=
            "Sorry, your system role does not allow downloading all attachments"
        )

        # grab answers so we can see how many has files
        answers = self.getAnswersByAssignment(assignment)
        fileIds = []
        for answer in answers:
            if not answer.file_id:
                continue
            # answer has an attachment
            fileIds.append(answer.file_id)

        if not fileIds:
            return {'msg': 'Assignment has no attachments'}

        # grab files so we can get the real file location
        files = self.getFilesByIds(fileIds)

        # zip up the tmp dir and save it to the report dir
        zipName = '{} [attachments-{}].zip'.format(assignment.name,
                                                   assignment.uuid)
        zipPath = '{}/{}'.format(current_app.config['REPORT_FOLDER'], zipName)
        # we're using compression level 6 as a compromise between speed &
        # compression (this is Z_DEFAULT_COMPRESSION in zlib)
        with zipfile.ZipFile(zipPath, 'w', zipfile.ZIP_DEFLATED, True,
                             6) as zipFile:
            for srcFile in files:
                srcFilePath = '{}/{}'.format(
                    current_app.config['ATTACHMENT_UPLOAD_FOLDER'],
                    srcFile.name)
                # set filename to 'full name - student number - uuid.ext'
                # omit student number or extension if not exist
                delim = ' - '
                srcFileName = srcFile.user.fullname
                if srcFile.user.student_number:
                    srcFileName += delim + srcFile.user.student_number
                srcFileName += delim + srcFile.name
                #current_app.logger.debug("writing " + srcFileName)
                zipFile.write(srcFilePath, srcFileName)
        #current_app.logger.debug("Writing zip file")

        return {'file': 'report/' + zipName, 'filename': zipName}
Beispiel #48
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, course,
            title="Course Unavailable",
            message="Courses can be seen only by those enrolled in them. Please double-check your enrollment in this course.")

        on_course_get.send(
            self,
            event_name=on_course_get.name,
            user=current_user,
            data={'id': course.id})
        return marshal(course, dataformat.get_course())
Beispiel #49
0
    def get(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(READ, UserCourse(course_id=course.id))
        restrict_user = not allow(READ, USER_IDENTITY)

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

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

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

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

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

            class_list.append(_user)

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

        return {'objects': marshal(class_list, dataformat.get_users_in_course(restrict_user=restrict_user))}
Beispiel #50
0
    def get(self, course_uuid, assignment_uuid):
        """
        Get answers submitted to the assignment submitted by current user

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

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

        params = user_answer_list_parser.parse_args()

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

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

        answers = query.all()

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

        return {"objects": marshal(answers, dataformat.get_answer(restrict_user))}
Beispiel #51
0
    def get(self, course_uuid):
        """
        refresh the course membership if able
        """
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(
            EDIT,
            course,
            title="Membership Status Unavailable",
            message=
            "Sorry, your role in this course does not allow you to view LTI membership status."
        )

        if not course.lti_linked:
            abort(
                400,
                title="Membership Status Unavailable",
                message=
                "The course is not linked to an LTI context yet. Launch an LTI link to link this course first, then check the status."
            )

        valid_membership_contexts = [
            lti_context for lti_context in course.lti_contexts \
            if lti_context.membership_enabled
        ]

        pending = 0
        enabled = len(valid_membership_contexts) > 0
        if enabled:
            lti_context_ids = [
                lti_context.id for lti_context in valid_membership_contexts
            ]

            pending = LTIMembership.query \
                .join(LTIUser) \
                .filter(and_(
                    LTIUser.compair_user_id == None,
                    LTIMembership.lti_context_id.in_(lti_context_ids)
                )) \
                .count()

        status = {'enabled': enabled, 'pending': pending}

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

        return {'status': status}
Beispiel #52
0
    def post(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(
            EDIT,
            UserCourse(course_id=course.id),
            title="Group Not Saved",
            message=
            "Sorry, your role in this course does not allow you to save groups."
        )

        params = user_list_parser.parse_args()

        if len(params.get('ids')) == 0:
            abort(
                400,
                title="Group Not Saved",
                message=
                "Please select at least one user below and then try to apply the group again."
            )

        user_courses = UserCourse.query \
            .join(User, UserCourse.user_id == User.id) \
            .filter(and_(
                UserCourse.course_id == course.id,
                User.uuid.in_(params.get('ids')),
                UserCourse.course_role != CourseRole.dropped
            )) \
            .all()

        if len(params.get('ids')) != len(user_courses):
            abort(
                400,
                title="Group Not Saved",
                message=
                "One or more users selected are not enrolled in the course yet."
            )

        for user_course in user_courses:
            user_course.group_name = None

        db.session.commit()

        on_course_group_user_list_delete.send(
            current_app._get_current_object(),
            event_name=on_course_group_user_list_delete.name,
            user=current_user,
            course_id=course.id,
            data={'user_uuids': params.get('ids')})

        return {'course_id': course.uuid}
Beispiel #53
0
    def post(self, course_uuid):
        course = Course.get_active_by_uuid_or_404(course_uuid)
        require(EDIT, course,
            title="Course Not Saved",
            message="Sorry, your role in this course does not allow you to save changes to it.")

        if current_app.config.get('DEMO_INSTALLATION', False):
            from data.fixtures import DemoDataFixture
            if course.id == DemoDataFixture.DEFAULT_COURSE_ID:
                abort(400, title="Course Not Updated", message="Sorry, you cannot edit the default demo course.")

        params = existing_course_parser.parse_args()

        # make sure the course id in the url and the course id in the params match
        if params['id'] != course_uuid:
            abort(400, title="Course Not Saved",
                message="The course's ID does not match the URL, which is required in order to save the course.")

        # modify course according to new values, preserve original values if values not passed
        course.name = params.get("name", course.name)
        course.year = params.get("year", course.year)
        course.term = params.get("term", course.term)
        course.sandbox = params.get("sandbox", course.sandbox)

        course.start_date = params.get("start_date")
        if course.start_date is not None:
            course.start_date = datetime.datetime.strptime(
                course.start_date,
                '%Y-%m-%dT%H:%M:%S.%fZ')

        course.end_date = params.get("end_date", None)
        if course.end_date is not None:
            course.end_date = datetime.datetime.strptime(
                course.end_date,
                '%Y-%m-%dT%H:%M:%S.%fZ')

        if course.start_date and course.end_date and course.start_date > course.end_date:
            abort(400, title="Course Not Saved", message="Course end time must be after course start time.")

        model_changes = get_model_changes(course)
        db.session.commit()

        on_course_modified.send(
            self,
            event_name=on_course_modified.name,
            user=current_user,
            course=course,
            data=model_changes)

        return marshal(course, dataformat.get_course())
Beispiel #54
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}
Beispiel #55
0
    def get(self):
        if allow(MANAGE, Course()):
            courses = Course.query.filter_by(active=True).all()
        else:
            courses = []
            for user_course in current_user.user_courses:
                if user_course.course.active and allow(
                        MANAGE, Assignment(course_id=user_course.course_id)):
                    courses.append(user_course.course)

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

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

        return {'courses': course_list}