Adding The Favorite Flag

In the previous Challenge, we hardcoded a favorite property on the return to the calls in the FavoriteDAO. We did this because we could guarantee the value based on the query being executed. This won’t be possible in the MovieDAO.

Your next challenge is to modify the MovieDAO to dynamically determine whether a movie is on the current user’s My Favorite List.

This challenge has two parts:

Open api/dao/auth.py

Running Multiple Queries within a Transaction

So far, you have only used the tx object within the Unit of Work to run a single query.

This will be fine for the majority of cases, but there may also be scenarios where more than one Query may be required.

User Favorites

One way that we could find the user’s favorites would be to run a separate MATCH clause within the same query. But as all of the routes that interact with the MovieDAO are used by both anonymous and logged in users, this could add unwanted complexity to the service.

Instead, it would be cleaner to execute two separate queries within the same transaction; one query to fetch a list of the user’s favorites, and another to retrieve a list of movies.

Fortunately, with only a few minor tweaks to the code, we can create a method that can be used to populate the favorite flag in the every other method in the MovieDAO.

Creating a Reusable Method

At the bottom of the MovieDAO, a placeholder get_user_favorites() method exists which is currently hardcoded to return an empty array.

python
def get_user_favorites(self, tx, user_id):
    return []

The purpose of this function is to run a Cypher statement against the Transaction object passed as the first parameter, which will find all of the user’s favorite movies and return a list of tmdbId properties.

Your challenge is to modify this method to retrieve that list of Movie ID’s and then call this function from the Read Transaction in the all() method.

Finding Favorites

Modify the get_user_favorites() method to run the following query against the tx object to return a list of IDs for each movie that the user has added to their favorites.

Click here to reveal the Cypher statement

This query should only run if a user is logged in for the current request, and therefore the userId parameter is not None.

cypher
MATCH (u:User {userId: $userId})-[:HAS_FAVORITE]->(m)
RETURN m.tmdbId AS id

Working Solution

Click here to reveal the completed get_user_favorites() method

.

python
def get_user_favorites(self, tx, user_id):
    if user_id == None:
        return []

    result = tx.run("""
        MATCH (u:User {userId: $userId})-[:HAS_FAVORITE]->(m)
        RETURN m.tmdbId AS id
    """, userId=user_id)

    return [ record.get("id") for record in result ]

Multiple Transaction Calls

The get_movies function defined in the all method of the MoviesDAO already uses tx.run() to run a Cypher statement within the current transaction.

To use the list of Movie IDs in the query, you can simply add a call to the self.get_user_favorites() method above the cypher variable. The output of the function can then be passed as a named parameter in the existing call to tx.run().

All that is left to do is to add a new item to m { .* } in the Cypher statement to check whether the tmdbId property of the current movie is in the favorites array.

python
# Get User favorites
favorites = self.get_user_favorites(tx, user_id)

# Define the cypher statement
cypher = """
    MATCH (m:Movie)
    WHERE m.`{0}` IS NOT NULL
    RETURN m {{
        .*,
        favorite: m.tmdbId IN $favorites
    }} AS movie
    ORDER BY m.`{0}` {1}
    SKIP $skip
    LIMIT $limit
""".format(sort, order)

# Run the statement within the transaction passed as the first argument
result = tx.run(cypher, limit=limit, skip=skip, user_id=user_id, favorites=favorites)

Comparing Versions

If we take a look at the two versions of the all() method, not much has changed. The favorites array has been passed through as a parameter to the query, and the query now uses the Cypher IN clause to check if the ID is included in the array.

Learn how to interact with Neo4j from Python using the Neo4j Python DriverUpdated Method
python
api/dao/movies.py
# Get User favorites
favorites = self.get_user_favorites(tx, user_id)

# Define the cypher statement
cypher = """
    MATCH (m:Movie)
    WHERE m.`{0}` IS NOT NULL
    RETURN m {{
        .*,
        favorite: m.tmdbId IN $favorites
    }} AS movie
    ORDER BY m.`{0}` {1}
    SKIP $skip
    LIMIT $limit
""".format(sort, order)

# Run the statement within the transaction passed as the first argument
result = tx.run(cypher, limit=limit, skip=skip, user_id=user_id, favorites=favorites)
Learn how to interact with Neo4j from Python using the Neo4j Python DriverPrevious Version
python
api/dao/movies.py
# Define the cypher statement
cypher = """
    MATCH (m:Movie)
    WHERE m.`{0}` IS NOT NULL
    RETURN m {{ .* }} AS movie
    ORDER BY m.`{0}` {1}
    SKIP $skip
    LIMIT $limit
""".format(sort, order)

# Run the statement within the transaction passed as the first argument
result = tx.run(cypher, limit=limit, skip=skip, user_id=user_id)

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/08_favorite_flag__test.py

The test file is located at tests/08_favorite_flag__test.py.

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 08-favorite-flag branch by running:

sh
Check out the 08-favorite-flag branch
git checkout 08-favorite-flag

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.flag@neo4j.com will have added Band of Brothers, the most popular movie in the dataset to their list of favorites.

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.flag@neo4j.com"})-[:HAS_FAVORITE]->(:Movie {title: 'Free Willy'})
RETURN true AS shouldVerify

Solution

The following statement will mimic the behaviour of the test by first finding the movie with the highest .imdbId rating and merging a new :User node into the graph with the email address graphacademy.flag@neo4j.com.

The test then merges a :HAS_FAVORITE relationship between the user and movie.

cypher
MATCH (m:Movie) WITH m 
WHERE m.imdbRating IS NOT NULL
WITH m
ORDER BY m.imdbRating DESC LIMIT 1

MERGE (u:User {userId: '9f965bf6-7e32-4afb-893f-756f502b2c2a'})
SET u.email = 'graphacademy.favorite@neo4j.com'

MERGE (u)-[r:HAS_FAVORITE]->(m)

RETURN *

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

Module Summary

In this module, you have updated the project to read data from, and write data to Neo4j. You have also learned about some of the considerations that we need to make when working with the Cypher type system in our Python application.

In the Project Backlog module, you will continue to implement the remaining missing pieces of functionality. This isn’t strictly required, but will be good practice for your development journey ahead.