Rating Movies

The challenges in this course come thick and fast!

In this challenge, you will modify the add() method in the RatingDTO to save ratings into Neo4j.

The Request Lifecycle

Rating a Movie

Before we start, let’s take a look at the request lifecycle when saving a review. If you prefer, you can skip to Saving a Rating.

On every Movie page, the user is invited to rate a movie on scale of 1 to 5. The form pictured to the right gives the user the ability to select a rating between 1 and 5 and click submit to save the rating.

When the form is submitted, the website sends a request to /api/account/ratings/{movieId} and the following will happen:

  1. The server directs the request to the route handler in api/routes/account.py, which verifies the user’s JWT token before handling the request.

  2. The route handler creates an instance of the RatingDAO.

  3. The add() method is called on the RatingDAO, and is passed the ID of the current user, the ID of the movie and a rating from the request body.

  4. It is then the responsibility of the add() method to save this information to the database and return an appropriate response.

A rating is represented in the graph a relationship going from a :User to a :Movie node with the type :RATED. The relationship has two properties; the rating (an integer) and a timestamp to represent when the relationship was created.

After the data is saved, the UI expects the movie details to be returned, with an additional property called rating, which will be the rating that the user has given for the movie.

Let’s take a look at the existing method in the RatingDAO.

python
api/dao/ratings.py
def add(self, user_id, movie_id, rating):
    # TODO: Create function to save the rating in the database
    # TODO: Call the function within a write transaction
    # TODO: Return movie details along with a rating

    return {
        **goodfellas,
        "rating": rating
    }

Your challenge is to replace the TODO comments in this method with working code.

Saving a Rating

Complete the following steps to complete the challenge:

Open api/dao/ratings.py

Define a Transaction Function

The first step is to define a new transaction function. The function should call the run() method on the transaction object passed as the first parameter, using the additional parameters passed to the function as named parameters.

The query should only ever return a single row, so the single() method can be called to instantly consume and return the first row.

python
Unit of Work
# Create function to save the rating in the database
def create_rating(tx, user_id, movie_id, rating):
    return tx.run("""
    MATCH (u:User {userId: $user_id})
    MATCH (m:Movie {tmdbId: $movie_id})
    MERGE (u)-[r:RATED]->(m)
    SET r.rating = $rating,
        r.timestamp = timestamp()
    RETURN m {
        .*,
        rating: r.rating
    } AS movie
    """, user_id=user_id, movie_id=movie_id, rating=rating).single()

By using the MERGE keyword here, we will overwrite an existing rating if one already exists. This way we don’t need to worry about duplicates or deleting existing records.

Run the Function in a Write Transaction

Within a new database session, call the create_rating() function, passing the three parameters passed to the method: user_id, movie_id and rating.

python
Call the Transaction Function
with self.driver.session() as session:
    record = session.execute_write(create_rating, user_id=user_id, movie_id=movie_id, rating=rating)

Check User and Movie Exist

As the transaction function calls the single() method, the returned value will be either a Record or None.

If the returned value is None, either the User or Movie could not be found, raise a NotFoundException. This will be handled by a Flask middleware.

python
if record is None:
    raise NotFoundException()

Return the Results

Otherwise, use the brackets method to extract the movie value from the record and return it.

python
return record["movie"]

Working Solution

Click here to reveal the Working Solution.
python
api/dao/ratings.py
def add(self, user_id, movie_id, rating):
    # Create function to save the rating in the database
    def create_rating(tx, user_id, movie_id, rating):
        return tx.run("""
        MATCH (u:User {userId: $user_id})
        MATCH (m:Movie {tmdbId: $movie_id})
        MERGE (u)-[r:RATED]->(m)
        SET r.rating = $rating,
            r.timestamp = timestamp()
        RETURN m {
            .*,
            rating: r.rating
        } AS movie
        """, user_id=user_id, movie_id=movie_id, rating=rating).single()

    with self.driver.session() as session:
        record = session.execute_write(create_rating, user_id=user_id, movie_id=movie_id, rating=rating)

        if record is None:
            raise NotFoundException()

        return record["movie"]

Testing

To test that this functionality has been correctly implemented, run the following code in a new terminal session:

sh
Running the test
pytest tests/06_rating_movies__test.py

The test file is located at tests/06_rating_movies__test.py.

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 06-rating-movies branch by running:

sh
Check out the 06-rating-movies branch
git checkout 06-rating-movies

You may have to commit or stash your changes before checking out this branch. You can also click here to expand the Support pane.

Verifying the Test

If the test has run successfully, a user with the email address graphacademy.reviewer@neo4j.com will have given the movie Goodfellas a rating of 5.

That number should have a type of INTEGER

Hint

You can run the following query to check for the user within the database. If the shouldVerify value returns true, the verification should be successful.

cypher
MATCH (u:User {email: "graphacademy.reviewer@neo4j.com"})-[r:RATED]->(m:Movie {title: "Goodfellas"})
RETURN u.email, m.title, r.rating,
    r.rating = 5 as shouldVerify

Solution

The following statement will mimic the behaviour of the test, merging a new :User node with the email address graphacademy.reviewer@neo4j.com and a :Movie node with a .tmdbId property of '769'.

The test then merges a relationship between the user and movie nodes in the graph, giving the relationship a .rating property.

cypher
MERGE (u:User {userId: '1185150b-9e81-46a2-a1d3-eb649544b9c4'})
SET u.email = 'graphacademy.reviewer@neo4j.com'
MERGE (m:Movie {tmdbId: '769'})
MERGE (u)-[r:RATED]->(m)
SET r.rating = 5,
    r.timestamp = timestamp()
RETURN m {
    .*,
    rating: r.rating
} AS movie

Once you have run this statement, click Try again…​ to complete the challenge.

Lesson Summary

In this Challenge, you have updated the RatingDAO to save a relationship between a User and Movie to represent a Rating.

In the next Challenge, you will implement the My Favorites feature.