Example #1
0
def course_strange(db):
    orm_thing = Course.find_by_code(db, code="Strange", org_id=1)
    if not orm_thing:
        orm_thing = Course(org_id=1, course_code="Strange", course_title="Damnation Alley")
        db.add(orm_thing)
        db.commit()
    return orm_thing
Example #2
0
def course_charm(db):
    orm_thing = Course.find_by_code(db, code="WEIRD", org_id=1)
    if not orm_thing:
        orm_thing = Course(org_id=1, course_code="WEIRD", course_title="Fable of a Failed Race")
        db.add(orm_thing)
        db.commit()
    return orm_thing
Example #3
0
def test_multiple_courses(db, course_quirk, course_strange, course_charm):
    courses = Course.find_by_org(db, 1)
    assert len(courses) == 2
    courses = Course.find_by_org(db, 2)
    assert len(courses) == 1
    courses = Course.find_by_org(db, 3)
    assert len(courses) == 0
Example #4
0
def course_quirk(db):
    orm_thing = Course.find_by_code(db, code="quirk", org_id=2)
    if not orm_thing:
        orm_thing = Course(org_id=2, course_code="quirk", course_title="Spirit of the Age")
        db.add(orm_thing)
        db.commit()
    return orm_thing
Example #5
0
    def delete(self):

        [course_code, assignment_code,
         purge] = self.get_params(["course_id", "assignment_id", "purge"])

        self.log.debug(
            f"Called DELETE /assignment with arguments: course {course_code}, assignment {assignment_code}, and purge {purge}"  # noqa: E501
        )
        if not (course_code and assignment_code):
            note = "Unreleasing an Assigment requires a course code and an assignment code"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        this_user = self.nbex_user

        if course_code not in this_user["courses"]:
            note = f"User not subscribed to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        if "instructor" not in map(str.casefold,
                                   this_user["courses"][course_code]):
            note = f"User not an instructor to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        note = f"Assignment '{assignment_code}' on course '{course_code}' marked as unreleased"
        with scoped_session() as session:
            course = Course.find_by_code(db=session,
                                         code=course_code,
                                         org_id=this_user["org_id"],
                                         log=self.log)

            assignment = AssignmentModel.find_by_code(db=session,
                                                      code=assignment_code,
                                                      course_id=course.id)

            if not assignment:
                note = f"Missing assignment for {assignment_code} and {course_code}, cannot delete"
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return

            # Set assignment to inactive
            assignment.active = False
            # Delete the associated notebook
            for notebook in assignment.notebooks:
                session.delete(notebook)

            # If we have the purge parameter, we actually delete the data
            # The various 'cascade on delete' settings should clear all the sub-tables
            if purge:
                session.delete(assignment)
                note = f"Assignment '{assignment_code}' on course '{course_code}' deleted and purged from the database"
        self.log.info(f"{note} by user {this_user['id']} ")
        self.finish({"success": True, "note": note})
Example #6
0
def test_course_basic_requirements(db, user_kaylee):
    orm_course = Course(
        org_id=user_kaylee.org_id,
        # course_code = "Strange",
    )
    db.add(orm_course)
    with pytest.raises(IntegrityError):
        db.commit()
    db.rollback()
    orm_course = Course(
        # org_id=user_kaylee.org_id,
        course_code="Strange",
    )
    db.add(orm_course)
    with pytest.raises(IntegrityError):
        db.commit()
    db.rollback()
    orm_course = Course(
        org_id=user_kaylee.org_id,
        course_code="Strange",
    )
    db.add(orm_course)
    db.commit()
    assert orm_course.course_code == "Strange"
    orm_course.course_title = "Damnation Alley"
    db.commit()
Example #7
0
def test_course_params(db, course_strange):
    # confirm named arguments work even when reversed
    found_by_code = Course.find_by_code(org_id=course_strange.org_id, db=db, code=course_strange.course_code)
    assert found_by_code.course_code == course_strange.course_code

    # confirm that putting the positional values the wrong way round will [probably] fail
    with pytest.raises(ValueError):
        found_by_code = Course.find_by_code(db, course_strange.org_id, course_strange.course_code)

    # test for unbexpected param
    with pytest.raises(TypeError):
        Course.find_by_pk(primary_key=course_strange.id, db=db)
    with pytest.raises(TypeError):
        found_by_code = Course.find_by_code(course_code=course_strange.course_code, org_id=course_strange.org_id, db=db)
    with pytest.raises(TypeError):
        found_by_code = Course.find_by_code(code=course_strange.course_code, id=course_strange.org_id, db=db)
    with pytest.raises(TypeError):
        Course.find_by_org(id=course_strange.org_id, db=db)
Example #8
0
    def post(self):
        """
        This endpoint accepts feedback files for a notebook.
        It requires a notebook id, student id, feedback timestamp and
        a checksum.

        The endpoint return {'success': true} for all successful feedback releases.
        """

        [
            course_id,
            assignment_id,
            notebook_id,
            student_id,
            timestamp,
            checksum,
        ] = self.get_params([
            "course_id",
            "assignment_id",
            "notebook",
            "student",
            "timestamp",
            "checksum",
        ])

        if not (course_id and assignment_id and notebook_id and student_id
                and timestamp and checksum):
            note = "Feedback call requires a course id, assignment id, notebook name, student id, checksum and timestamp."
            self.log.debug(note)
            self.finish({"success": False, "note": note})
            return

        this_user = self.nbex_user
        if course_id not in this_user["courses"]:
            note = f"User not subscribed to course {course_id}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        if ("instructor" != this_user["current_role"].casefold()
            ):  # we may need to revisit this
            note = f"User not an instructor to course {course_id}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        with scoped_session() as session:

            # Start building feedback object

            course = Course.find_by_code(db=session,
                                         code=course_id,
                                         org_id=this_user["org_id"],
                                         log=self.log)

            if not course:
                self.log.info(
                    f"Could not find requested resource course {course_id}")
                raise web.HTTPError(
                    404,
                    f"Could not find requested resource course {course_id}")

            assignment = AssignmentModel.find_by_code(
                db=session,
                code=assignment_id,
                course_id=course.id,
                action=AssignmentActions.released.value,
            )

            if not assignment:
                note = f"Could not find requested resource assignment {assignment_id}"
                self.log.info(note)
                raise web.HTTPError(404, note)

            notebook = Notebook.find_by_name(db=session,
                                             name=notebook_id,
                                             assignment_id=assignment.id,
                                             log=self.log)
            if not notebook:
                note = f"Could not find requested resource notebook {notebook_id}"
                self.log.info(note)
                raise web.HTTPError(404, note)

            student = User.find_by_name(db=session,
                                        name=student_id,
                                        log=self.log)

            if not student:
                note = f"Could not find requested resource student {student_id}"
                self.log.info(note)
                raise web.HTTPError(404, note)

            # # raise Exception(f"{res}")
            # self.log.info(f"Notebook: {notebook}")
            # self.log.info(f"Student: {student}")
            # self.log.info(f"Instructor: {this_user}")

            # TODO: check access. Is the user an instructor on the course to which the notebook belongs

            # Check whether there is an HTML file attached to the request
            if not self.request.files:
                self.log.warning(f"Error: No file supplied in upload"
                                 )  # TODO: improve error message
                raise web.HTTPError(412)  # precondition failed

            try:
                # Grab the file
                file_info = self.request.files["feedback"][0]
                filename, content_type = (
                    file_info["filename"],
                    file_info["content_type"],
                )
                note = f"Received file {filename}, of type {content_type}"
                self.log.info(note)
                fbfile = tempfile.NamedTemporaryFile()
                fbfile.write(file_info["body"])
                fbfile.seek(0)

            except Exception as e:
                # Could not grab the feedback file
                self.log.error(f"Error: {e}")
                raise web.HTTPError(412)
            # TODO: should we check the checksum?
            # unique_key = make_unique_key(
            #     course_id,
            #     assignment_id,
            #     notebook_id,
            #     student_id,
            #     str(timestamp).strip(),
            # )
            # check_checksum = notebook_hash(fbfile.name, unique_key)
            #
            # if check_checksum != checksum:
            #     self.log.info(f"Checksum {checksum} does not match {check_checksum}")
            #     raise web.HTTPError(403, f"Checksum {checksum} does not match {check_checksum}")

            # TODO: What is file of the original notebook we are getting the feedback for?
            # assignment_dir = "collected/student_id/assignment_name"
            # nbfile = os.path.join(assignment_dir, "{}.ipynb".format(notebook.name))
            # calc_checksum = notebook_hash(nbfile.name, unique_key)
            # if calc_checksum != checksum:
            #     self.log.info(f"Mismatched checksums {calc_checksum} and {checksum}.")
            #     raise web.HTTPError(412)

            location = "/".join([
                self.base_storage_location,
                str(this_user["org_id"]),
                "feedback",
                notebook.assignment.course.course_code,
                notebook.assignment.assignment_code,
                str(int(time.time())),
            ])

            # This should be abstracted, so it can be overloaded to store in other manners (eg AWS)
            feedback_file = location + "/" + checksum + ".html"

            try:
                # Ensure the directory exists
                os.makedirs(os.path.dirname(feedback_file), exist_ok=True)
                with open(feedback_file, "w+b") as handle:
                    handle.write(file_info["body"])
            except Exception as e:
                self.log.error(f"Could not save file. \n {e}")
                raise web.HTTPError(500)

            feedback = Feedback(
                notebook_id=notebook.id,
                checksum=checksum,
                location=feedback_file,
                student_id=student.id,
                instructor_id=this_user.get("id"),
                timestamp=parser.parse(timestamp),
            )

            session.add(feedback)

            # Add action
            action = Action(
                user_id=this_user["id"],
                assignment_id=notebook.assignment.id,
                action=AssignmentActions.feedback_released,
                location=feedback_file,
            )
            session.add(action)

        self.finish({"success": True, "note": "Feedback released"})
Example #9
0
    def get(self):

        models = []

        [course_code] = self.get_params(["course_id"])

        if not course_code:
            note = f"Assigment call requires a course id"
            self.log.info(note)
            self.finish({"success": False, "note": note, "value": []})
            return

        # Who is my user?
        this_user = self.nbex_user
        self.log.debug(f"User: {this_user.get('name')}")
        # For what course do we want to see the assignments?
        self.log.debug(f"Course: {course_code}")
        # Is our user subscribed to this course?
        if course_code not in this_user["courses"]:
            note = f"User not subscribed to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note, "value": []})
            return

        # Find the course being referred to
        with scoped_session() as session:
            course = Course.find_by_code(
                db=session, code=course_code, org_id=this_user["org_id"], log=self.log
            )
            if not course:
                note = f"Course {course_code} does not exist"
                self.log.info(note)
                self.finish({"success": False, "note": note, "value": []})
                return

            assignments = AssignmentModel.find_for_course(
                db=session, course_id=course.id, log=self.log
            )

            for assignment in assignments:
                self.log.debug(f"==========")
                self.log.debug(f"Assignment: {assignment}")
                for action in assignment.actions:
                    # For every action that is not "released" checked if the user id matches
                    if (
                        action.action != AssignmentActions.released
                        and this_user.get("id") != action.user_id
                    ):
                        self.log.debug(
                            f"ormuser: {this_user.get('id')} - actionUser {action.user_id}"
                        )
                        self.log.debug("Action does not belong to user, skip action")
                        continue
                    notebooks = []

                    for notebook in assignment.notebooks:
                        feedback_available = False
                        feedback_timestamp = None
                        if action.action == AssignmentActions.submitted:
                            feedback = Feedback.find_notebook_for_student(
                                db=session,
                                notebook_id=notebook.id,
                                student_id=this_user.get("id"),
                                log=self.log,
                            )
                            if feedback:
                                feedback_available = bool(feedback)
                                feedback_timestamp = feedback.timestamp.strftime(
                                    "%Y-%m-%d %H:%M:%S.%f %Z"
                                )

                        notebooks.append(
                            {
                                "notebook_id": notebook.name,
                                "has_exchange_feedback": feedback_available,
                                "feedback_updated": False,  # TODO: needs a real value
                                "feedback_timestamp": feedback_timestamp,
                            }
                        )
                    models.append(
                        {
                            "assignment_id": assignment.assignment_code,
                            "student_id": action.user_id,
                            "course_id": assignment.course.course_code,
                            "status": action.action.value,  # currently called 'action' in our db
                            "path": action.location,
                            "notebooks": notebooks,
                            "timestamp": action.timestamp.strftime(
                                "%Y-%m-%d %H:%M:%S.%f %Z"
                            ),
                        }
                    )

        self.log.debug(f"Assignments: {models}")
        self.finish({"success": True, "value": models})
Example #10
0
    def post(self):

        # Do a content-length check, before we go any further
        if "Content-Length" in self.request.headers and int(
            self.request.headers["Content-Length"]
        ) > int(self.max_buffer_size):
            note = "File upload oversize, and rejected. Please reduce the contents of the assignment, re-generate, and re-release"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        [course_code, assignment_code] = self.get_params(["course_id", "assignment_id"])
        self.log.debug(
            f"Called POST /assignment with arguments: course {course_code} and  assignment {assignment_code}"
        )
        if not (course_code and assignment_code):
            note = f"Posting an Assigment requires a course code and an assignment code"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        this_user = self.nbex_user
        if not course_code in this_user["courses"]:
            note = f"User not subscribed to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        if (
            not "instructor" == this_user["current_role"].casefold()
        ):  # we may need to revisit this
            note = f"User not an instructor to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        # The course will exist: the user object creates it if it doesn't exist
        #  - and we know the user is subscribed to the course as an instructor (above)
        with scoped_session() as session:
            course = Course.find_by_code(
                db=session, code=course_code, org_id=this_user["org_id"], log=self.log
            )

            # We need to find this assignment, or make a new one.
            assignment = AssignmentModel.find_by_code(
                db=session, code=assignment_code, course_id=course.id
            )

            if assignment is None:
                # Look for inactive assignments
                assignment = AssignmentModel.find_by_code(
                    db=session, code=assignment_code, course_id=course.id, active=False
                )

            if assignment is None:
                self.log.info(
                    f"New Assignment details: assignment_code:{assignment_code}, course_id:{course.id}"
                )
                # defaults active
                assignment = AssignmentModel(
                    assignment_code=assignment_code, course_id=course.id
                )
                session.add(assignment)
                # deliberately no commit: we need to be able to roll-back if there's no data!

            # Set assignment to active
            assignment.active = True

            # storage is dynamically in $path/release/$course_code/$assignment_code/<timestamp>/
            # Note - this means we can have multiple versions of the same release on the system
            release_file = "/".join(
                [
                    self.base_storage_location,
                    str(this_user["org_id"]),
                    AssignmentActions.released.value,
                    course_code,
                    assignment_code,
                    str(int(time.time())),
                ]
            )

            if not self.request.files:
                self.log.warning(
                    f"Error: No file supplies in upload"
                )  # TODO: improve error message
                raise web.HTTPError(412)  # precondition failed

            try:
                # Write the uploaded file to the desired location
                file_info = self.request.files["assignment"][0]

                filename, content_type = (
                    file_info["filename"],
                    file_info["content_type"],
                )
                note = f"Received file {filename}, of type {content_type}"
                self.log.info(note)
                extn = os.path.splitext(filename)[1]
                cname = str(uuid.uuid4()) + extn

                # store to disk.
                # This should be abstracted, so it can be overloaded to store in other manners (eg AWS)
                release_file = release_file + "/" + cname
                # Ensure the directory exists
                os.makedirs(os.path.dirname(release_file), exist_ok=True)
                with open(release_file, "w+b") as handle:
                    handle.write(file_info["body"])

            except Exception as e:  # TODO: exception handling
                self.log.warning(f"Error: {e}")  # TODO: improve error message

                self.log.info(f"Upload failed")
                # error 500??
                raise Exception

            # Check the file exists on disk
            if not (
                os.path.exists(release_file)
                and os.access(release_file, os.R_OK)
                and os.path.getsize(release_file) > 0
            ):
                note = "File upload failed."
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return

            # We shouldn't get here, but a double-check is good
            if os.path.getsize(release_file) > self.max_buffer_size:
                os.remove(release_file)
                note = "File upload oversize, and rejected. Please reduce the contents of the assignment, re-generate, and re-release"
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return

            # now commit the assignment, and get it back to find the id
            assignment = AssignmentModel.find_by_code(
                db=session, code=assignment_code, course_id=course.id
            )

            # Record the notebooks associated with this assignment
            notebooks = self.get_arguments("notebooks")

            for notebook in notebooks:
                self.log.debug(f"Adding notebook {notebook}")
                new_notebook = Notebook(name=notebook)
                assignment.notebooks.append(new_notebook)

            # Record the action.
            # Note we record the path to the files.
            self.log.info(
                f"Adding action {AssignmentActions.released.value} for user {this_user['id']} against assignment {assignment.id}"
            )
            action = Action(
                user_id=this_user["id"],
                assignment_id=assignment.id,
                action=AssignmentActions.released,
                location=release_file,
            )
            session.add(action)
            self.finish({"success": True, "note": "Released"})
Example #11
0
    def get(self):  # def get(self, course_code, assignment_code=None):

        [course_code, assignment_code] = self.get_params(["course_id", "assignment_id"])

        if not (course_code and assignment_code):
            note = "Assigment call requires both a course code and an assignment code!!"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        this_user = self.nbex_user

        if not course_code in this_user["courses"]:
            note = f"User not subscribed to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        # Find the course being referred to
        with scoped_session() as session:
            course = Course.find_by_code(
                db=session, code=course_code, org_id=this_user["org_id"], log=self.log
            )
            if course is None:
                note = f"Course {course_code} does not exist"
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return  # needs a proper 'fail' here

            note = ""
            self.log.debug(f"Course:{course_code} assignment:{assignment_code}")

            # The location for the data-object is actually held in the 'released' action for the given assignment
            # We want the last one...
            assignment = AssignmentModel.find_by_code(
                db=session,
                code=assignment_code,
                course_id=course.id,
                action=AssignmentActions.released.value,
            )

            if assignment is None:
                note = f"Assignment {assignment_code} does not exist"
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return  # needs a proper 'fail' here

            self._headers = httputil.HTTPHeaders(
                {
                    "Content-Type": "application/gzip",
                    "Date": httputil.format_timestamp(time.time()),
                }
            )

            data = b""

            release_file = None

            action = Action.find_most_recent_action(
                db=session,
                assignment_id=assignment.id,
                action=AssignmentActions.released,
                log=self.log,
            )
            release_file = action.location

            if release_file:
                try:
                    with open(release_file, "r+b") as handle:
                        data = handle.read()
                except Exception as e:  # TODO: exception handling
                    self.log.warning(f"Error: {e}")  # TODO: improve error message
                    self.log.info(f"Unable to open file")

                    # error 500??
                    raise Exception

                self.log.info(
                    f"Adding action {AssignmentActions.fetched.value} for user {this_user['id']} against assignment {assignment.id}"
                )
                action = Action(
                    user_id=this_user["id"],
                    assignment_id=assignment.id,
                    action=AssignmentActions.fetched,
                    location=release_file,
                )
                session.add(action)
                self.log.info("record of fetch action committed")
                self.finish(data)
            else:
                self.log.info("no release file found")
                raise Exception
Example #12
0
    def nbex_user(self):

        hub_user = self.get_current_user()
        hub_username = hub_user.get("name")

        full_name = hub_user.get("full_name")
        current_course = hub_user.get("course_id")
        current_role = hub_user.get("course_role")
        course_title = hub_user.get("course_title", "no_title")
        org_id = hub_user.get("org_id", 1)

        if not (current_course and current_role):
            return

        self.org_id = org_id

        with scoped_session() as session:
            user = User.find_by_name(db=session,
                                     name=hub_username,
                                     log=self.log)
            if user is None:
                self.log.debug(
                    f"New user details: name:{hub_username}, org_id:{org_id}")
                user = User(name=hub_username, org_id=org_id)
                session.add(user)
            if user.full_name != full_name:
                user.full_name = full_name

            course = Course.find_by_code(db=session,
                                         code=current_course,
                                         org_id=org_id,
                                         log=self.log)
            if course is None:
                self.log.debug(
                    f"New course details: code:{current_course}, org_id:{org_id}"
                )
                course = Course(org_id=org_id, course_code=current_course)
                if course_title:
                    self.log.debug(f"Adding title {course_title}")
                    course.course_title = course_title
                session.add(course)

            # Check to see if we have a subscription (for this course)
            self.log.debug(
                f"Looking for subscription for: user:{user.id}, course:{course.id}, role:{current_role}"
            )

            subscription = Subscription.find_by_set(db=session,
                                                    user_id=user.id,
                                                    course_id=course.id,
                                                    role=current_role)
            if subscription is None:
                self.log.debug(
                    f"New subscription details: user:{user.id}, course:{course.id}, role:{current_role}"
                )
                subscription = Subscription(user_id=user.id,
                                            course_id=course.id,
                                            role=current_role)
                session.add(subscription)

            courses = {}

            for subscription in user.courses:
                if not subscription.course.course_code in courses:
                    courses[subscription.course.course_code] = {}
                courses[subscription.course.course_code][subscription.role] = 1

            model = {
                "kind": "user",
                "id": user.id,
                "name": user.name,
                "org_id": user.org_id,
                "current_course": current_course,
                "current_role": current_role,
                "courses": courses,
            }
        return model
Example #13
0
def test_course(db, course_strange):
    assert course_strange.course_code == "Strange"
    assert course_strange.course_title == "Damnation Alley"

    with pytest.raises(TypeError):
        found_by_pk = Course.find_by_pk()
    with pytest.raises(TypeError):
        found_by_pk = Course.find_by_pk(db)
    with pytest.raises(ValueError):
        found_by_pk = Course.find_by_pk(db, None)
    with pytest.raises(TypeError):
        found_by_pk = Course.find_by_pk(db, "abc")

    found_by_pk = Course.find_by_pk(db, course_strange.id)
    assert found_by_pk.id == course_strange.id
    found_by_pk = Course.find_by_pk(db, course_strange.id + 10)
    assert found_by_pk is None

    with pytest.raises(TypeError):
        found_by_code = Course.find_by_code(db, course_strange.course_code)

    with pytest.raises(ValueError):
        found_by_code = Course.find_by_code(db, None, course_strange.org_id)
    with pytest.raises(ValueError):
        found_by_code = Course.find_by_code(db, course_strange.course_code, None)

    found_by_code = Course.find_by_code(db, course_strange.course_code, course_strange.org_id)
    assert found_by_code.course_code == course_strange.course_code
    assert str(found_by_code) == f"Course/{course_strange.course_code} {course_strange.course_title}"

    # in real code, org_id is probably a string, so lets confirm that works
    found_by_code = Course.find_by_code(db, course_strange.course_code, "1")
    assert found_by_code.course_code == course_strange.course_code

    found_by_code = Course.find_by_code(db, "SANE", course_strange.org_id)
    assert found_by_code is None
    found_by_code = Course.find_by_code(db, course_strange.course_code, course_strange.org_id + 10)
    assert found_by_code is None
Example #14
0
    def get(self):

        models = []

        [course_code, assignment_code,
         user_id] = self.get_params(["course_id", "assignment_id", "user_id"])

        if not (course_code and assignment_code):
            note = "Collections call requires both a course code and an assignment code"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        # Who is my user?
        this_user = self.nbex_user

        self.log.debug(f"User: {this_user.get('name')}")
        # For what course do we want to see the assignments?
        self.log.debug(f"Course: {course_code}")
        # Is our user subscribed to this course?
        if course_code not in this_user["courses"]:
            note = f"User not subscribed to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return
        if not "instructor" == this_user["current_role"].casefold(
        ):  # we may need to revisit this
            note = f"User not an instructor to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        # Find the course being referred to
        with scoped_session() as session:
            course = Course.find_by_code(db=session,
                                         code=course_code,
                                         org_id=this_user["org_id"],
                                         log=self.log)
            if not course:
                note = f"Course {course_code} does not exist"
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return

            assignment = AssignmentModel.find_by_code(
                db=session,
                course_id=course.id,
                log=self.log,
                code=assignment_code,
                action=AssignmentActions.submitted.value,
            )

            if not assignment:
                note = f"Assignment {assignment_code} does not exist"
                self.log.info(note)
                self.finish({"success": True, "value": []})
                return

            self.log.debug(f"Assignment: {assignment}")

            filters = [
                Action.assignment_id == assignment.id,
                Action.action == AssignmentActions.submitted.value,
            ]

            if user_id:
                student = session.query(User).filter(
                    User.name == user_id).first()
                filters.append(Action.user_id == student.id)

            actions = session.query(Action).filter(*filters)

            for action in actions:
                models.append({
                    "student_id":
                    action.user.name,
                    "full_name":
                    action.user.full_name,
                    "assignment_id":
                    assignment.assignment_code,
                    "course_id":
                    assignment.course.course_code,
                    "status":
                    action.action.value,  # currently called 'action' in our db
                    "path":
                    action.location,
                    # 'name' in db, 'notebook_id' id nbgrader
                    "notebooks": [{
                        "notebook_id": x.name
                    } for x in assignment.notebooks],
                    "timestamp":
                    action.timestamp.strftime("%Y-%m-%d %H:%M:%S.%f %Z"),
                })

            self.log.debug(f"Assignments: {models}")
        self.finish({"success": True, "value": models})
Example #15
0
    def get(self):

        [course_code, assignment_code,
         path] = self.get_params(["course_id", "assignment_id", "path"])

        if not (course_code and assignment_code and path):
            note = "Collection call requires a course code, an assignment code, and a path"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        # Who is my user?
        this_user = self.nbex_user

        self.log.debug(f"User: {this_user.get('name')}")
        # For what course do we want to see the assignments?
        self.log.debug(f"Course: {course_code}")
        # Is our user subscribed to this course?
        if course_code not in this_user["courses"]:
            note = f"User not subscribed to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return
        self.log.info(f"user: {this_user}")

        if not "instructor" == this_user["current_role"].casefold(
        ):  # we may need to revisit this
            note = f"User not an instructor to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        # Find the course being referred to
        with scoped_session() as session:
            course = Course.find_by_code(db=session,
                                         code=course_code,
                                         org_id=this_user["org_id"],
                                         log=self.log)
            if not course:
                note = f"Course {course_code} does not exist"
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return

            # We need to key off the assignment, but we're actually looking
            # for the action with a action and a specific path
            assignments = AssignmentModel.find_for_course(
                db=session,
                course_id=course.id,
                log=self.log,
                action=AssignmentActions.submitted.value,
                path=path,
            )

            self.set_header("Content-Type", "application/gzip")

            # I do not want to assume there will just be one.
            for assignment in assignments:
                self.log.debug(f"Assignment: {assignment}")

                try:
                    with open(path, "r+b") as handle:
                        data = handle.read()
                except Exception as e:  # TODO: exception handling
                    self.log.warning(
                        f"Error: {e}")  # TODO: improve error message

                    # error 500??
                    raise Exception

                self.log.info(
                    f"Adding action {AssignmentActions.collected.value} for user {this_user['id']} against assignment {assignment.id}"  # noqa: E501
                )
                action = Action(
                    user_id=this_user["id"],
                    assignment_id=assignment.id,
                    action=AssignmentActions.collected,
                    location=path,
                )
                session.add(action)

                self.finish(data)
                return
Example #16
0
    def get(self):

        [course_id,
         assignment_id] = self.get_params(["course_id", "assignment_id"])

        if not assignment_id or not course_id:
            note = "Feedback call requires an assignment id and a course id"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        self.log.debug(
            f"checking for feedback for {assignment_id} on {course_id}")

        this_user = self.nbex_user

        with scoped_session() as session:

            course = Course.find_by_code(db=session,
                                         code=course_id,
                                         org_id=this_user["org_id"],
                                         log=self.log)
            if not course:
                note = f"Course {course_id} not found"
                self.log.info(note)
                # self.finish({"success": False, "note": note, "value": []})
                # return
                raise web.HTTPError(404, note)

            assignment = AssignmentModel.find_by_code(db=session,
                                                      code=assignment_id,
                                                      course_id=course.id,
                                                      log=self.log)
            if not assignment:
                note = f"Assignment {assignment_id} for Course {course_id} not found"
                self.log.info(note)
                # self.finish({"success": False, "note": note, "value": []})
                # return
                raise web.HTTPError(404, note)

            student = User.find_by_name(db=session,
                                        name=this_user["name"],
                                        log=self.log)

            res = Feedback.find_all_for_student(
                db=session,
                student_id=student.id,
                assignment_id=assignment.id,
                log=self.log,
            )
            feedbacks = []
            for r in res:
                f = {}
                notebook = Notebook.find_by_pk(db=session,
                                               pk=r.notebook_id,
                                               log=self.log)
                if notebook is not None:
                    feedback_name = "{0}.html".format(notebook.name)
                else:
                    feedback_name = os.path.basename(r.location)
                with open(r.location, "r+b") as fp:
                    f["content"] = base64.b64encode(fp.read()).decode("utf-8")
                f["filename"] = feedback_name
                # This matches self.timestamp_format
                f["timestamp"] = r.timestamp.strftime(
                    "%Y-%m-%d %H:%M:%S.%f %Z")
                f["checksum"] = r.checksum
                feedbacks.append(f)

                # Add action
                action = Action(
                    user_id=this_user["id"],
                    assignment_id=assignment.id,
                    action=AssignmentActions.feedback_fetched,
                    location=r.location,
                )
                session.add(action)
            self.finish({"success": True, "feedback": feedbacks})
Example #17
0
    def nbex_user(self):

        hub_user = self.get_current_user()
        hub_username = hub_user.get("name")

        full_name = hub_user.get("full_name")
        current_course = hub_user.get("course_id")
        current_role = hub_user.get("course_role")
        course_title = hub_user.get("course_title", "no_title")
        org_id = hub_user.get("org_id", 1)

        # Raising an error appears to have no detrimental affect when running.
        if not (current_course and current_role):
            note = f"Both current_course ('{current_course}') and current_role ('{current_role}') must have values. User was '{hub_username}'"  # noqa: E501
            self.log.info(note)
            raise ValueError(note)

        self.org_id = org_id

        with scoped_session() as session:
            user = User.find_by_name(db=session,
                                     name=hub_username,
                                     log=self.log)
            if user is None:
                self.log.debug(
                    f"New user details: name:{hub_username}, org_id:{org_id}")
                user = User(name=hub_username, org_id=org_id)
                session.add(user)
            if user.full_name != full_name:
                user.full_name = full_name

            course = Course.find_by_code(db=session,
                                         code=current_course,
                                         org_id=org_id,
                                         log=self.log)
            if course is None:
                self.log.debug(
                    f"New course details: code:{current_course}, org_id:{org_id}"
                )
                course = Course(org_id=org_id, course_code=current_course)
                if course_title:
                    self.log.debug(f"Adding title {course_title}")
                    course.course_title = course_title
                session.add(course)

            # Check to see if we have a subscription (for this course)
            self.log.debug(
                f"Looking for subscription for: user:{user.id}, course:{course.id}, role:{current_role}"
            )

            subscription = Subscription.find_by_set(db=session,
                                                    user_id=user.id,
                                                    course_id=course.id,
                                                    role=current_role)
            if subscription is None:
                self.log.debug(
                    f"New subscription details: user:{user.id}, course:{course.id}, role:{current_role}"
                )
                subscription = Subscription(user_id=user.id,
                                            course_id=course.id,
                                            role=current_role)
                session.add(subscription)

            courses = {}

            for subscription in user.courses:
                if subscription.course.course_code not in courses:
                    courses[subscription.course.course_code] = {}
                courses[subscription.course.course_code][subscription.role] = 1

            model = {
                "kind": "user",
                "id": user.id,
                "name": user.name,
                "org_id": user.org_id,
                "current_course": current_course,
                "current_role": current_role,
                "courses": courses,
            }
        return model
Example #18
0
    def post(self):

        if "Content-Length" in self.request.headers and int(
                self.request.headers["Content-Length"]) > int(
                    self.max_buffer_size):
            note = "File upload oversize, and rejected. Please reduce the files in your submission and try again."
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        [course_code,
         assignment_code] = self.get_params(["course_id", "assignment_id"])
        self.log.debug(
            f"Called POST /submission with arguments: course {course_code} and  assignment {assignment_code}"
        )
        if not (course_code and assignment_code):
            note = f"Submission call requires both a course code and an assignment code"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        this_user = self.nbex_user
        if not course_code in this_user["courses"]:
            note = f"User not subscribed to course {course_code}"
            self.log.info(note)
            self.finish({"success": False, "note": note})
            return

        # The course will exist: the user object creates it if it doesn't exist
        #  - and we know the user is subscribed to the course as an instructor (above)
        with scoped_session() as session:
            course = Course.find_by_code(db=session,
                                         code=course_code,
                                         org_id=this_user["org_id"],
                                         log=self.log)

            # We need to find this assignment, or make a new one.
            assignment = Assignment.find_by_code(db=session,
                                                 code=assignment_code,
                                                 course_id=course.id)
            if assignment is None:
                note = f"User not fetched assignment {assignment_code}"
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return

            # storage is dynamically in $path/submitted/$course_code/$assignment_code/$username/<timestamp>/
            # Note - this means that a user can submit multiple times, and we have all copies
            release_file = "/".join([
                self.base_storage_location,
                str(this_user["org_id"]),
                AssignmentActions.submitted.value,
                course_code,
                assignment_code,
                this_user["name"],
                str(int(time.time())),
            ])

            if not self.request.files:
                self.log.warning(f"Error: No file supplies in upload"
                                 )  # TODO: improve error message
                raise web.HTTPError(412)  # precondition failed

            try:
                # Write the uploaded file to the desired location
                file_info = self.request.files["assignment"][0]

                filename, content_type = (
                    file_info["filename"],
                    file_info["content_type"],
                )
                note = f"Received file {filename}, of type {content_type}"
                self.log.info(note)
                extn = os.path.splitext(filename)[1]
                cname = str(uuid.uuid4()) + extn

                # store to disk.
                # This should be abstracted, so it can be overloaded to store in other manners (eg AWS)
                release_file = release_file + "/" + cname
                # Ensure the directory exists
                os.makedirs(os.path.dirname(release_file), exist_ok=True)
                with open(release_file, "w+b") as handle:
                    handle.write(file_info["body"])

            except Exception as e:  # TODO: exception handling
                self.log.warning(f"Error: {e}")  # TODO: improve error message

                self.log.info(f"Upload failed")
                # error 500??
                raise web.HTTPError(418)

            # Check the file exists on disk
            if not (os.path.exists(release_file)
                    and os.access(release_file, os.R_OK)
                    and os.path.getsize(release_file) > 0):
                note = "File upload failed."
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return

            # We shouldn't need this, but it's good to double-check
            if os.path.getsize(release_file) > self.max_buffer_size:
                os.remove(release_file)
                note = "File upload oversize, and rejected. Please reduce the files in your submission and try again."
                self.log.info(note)
                self.finish({"success": False, "note": note})
                return

            # now commit the assignment, and get it back to find the id
            assignment = Assignment.find_by_code(db=session,
                                                 code=assignment_code,
                                                 course_id=course.id)

            # Record the action.
            # Note we record the path to the files.
            self.log.info(
                f"Adding action {AssignmentActions.submitted.value} for user {this_user['id']} against assignment {assignment.id}"
            )
            action = Action(
                user_id=this_user["id"],
                assignment_id=assignment.id,
                action=AssignmentActions.submitted,
                location=release_file,
            )
            session.add(action)
        self.finish({"success": True, "note": "Submitted"})