This challenge has three parts:
The My Favorites Feature
Clicking the My Favorites link in the main navigation will take you to page which contains a list of Movies that you can revisit later.
When a logged in user hovers their mouse cursor over a Movie on the website, a bookmark icon appears in the top right hand corner. Clicking this icon will either add or remove the Movie from the user’s Favorites list.
When adding a Movie to the list, a POST
request it sent to the /api/favorites/{id}
endpoint. When this happens, the following chain of events will occur:
-
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 -
The route handler creates an instance of the
FavoriteDAO
. -
The
add()
method is then called on theFavoriteDAO
instance with the user’s ID taken from the JWT token, and the ID of the movie that has been extracted from the request URL. -
It is then the responsibility of the
FavoriteDAO
to find the:User
and:Movie
nodes and create the:HAS_FAVORITE
relationship between them.
Likewise, when it is clicked for a Movie that has already been favorited, a DELETE
request is sent to the same URL, and the Movie is removed from the list.
Adding a Movie to My Favorites
For the first part of this challenge, modify the add()
method to open a new database session, run the Cypher statement to create the :HAS_FAVORITE
relationship, close the session and return the movie details along with an additional favorite
property.
api/dao/auth.py
→
1. Define a new Transaction Function
Define a new transaction function to execute the Cypher statement below to create the :HAS_FAVORITE
relationship between the User and the Movie.
Click here to reveal the Cypher statement
MATCH (u:User {userId: $userId})
MATCH (m:Movie {tmdbId: $movieId})
MERGE (u)-[r:HAS_FAVORITE]->(m)
ON CREATE SET r.createdAt = datetime()
RETURN m {
.*,
favorite: true
} AS movie
The function should expect a transaction object as the first parameter and also accept the user_id
and movie_id
as named parameters to the function.
There should only be one result, so you can call the single()
method to instantly consume the first result.
# Define a new transaction function to create a HAS_FAVORITE relationship
def add_to_favorites(tx, user_id, movie_id):
row = tx.run("""
MATCH (u:User {userId: $userId})
MATCH (m:Movie {tmdbId: $movieId})
MERGE (u)-[r:HAS_FAVORITE]->(m)
ON CREATE SET u.createdAt = datetime()
RETURN m {
.*,
favorite: true
} AS movie
""", userId=user_id, movieId=movie_id).single()
If no records are returned, you can safely assume that the either the User or Movie do not exist.
In this case, raise a NotFoundException
with an appropriate error message.
# If no rows are returnedm throw a NotFoundException
if row == None:
raise NotFoundException()
Otherwise, use the get()
method on the record to return the movie
value from the first row.
return row.get("movie")
2. Call the Transaction Function
In a new session, call the new method in a new Write Transaction and return the results.
with self.driver.session() as session:
return session.execute_write(add_to_favorites, user_id, movie_id)
Working Solution
Click here to reveal the completed add()
method
def add(self, user_id, movie_id):
# Define a new transaction function to create a HAS_FAVORITE relationship
def add_to_favorites(tx, user_id, movie_id):
row = tx.run("""
MATCH (u:User {userId: $userId})
MATCH (m:Movie {tmdbId: $movieId})
MERGE (u)-[r:HAS_FAVORITE]->(m)
ON CREATE SET u.createdAt = datetime()
RETURN m {
.*,
favorite: true
} AS movie
""", userId=user_id, movieId=movie_id).single()
# If no rows are returnedm throw a NotFoundException
if row == None:
raise NotFoundException()
return row.get("movie")
with self.driver.session() as session:
return session.execute_write(add_to_favorites, user_id, movie_id)
Removing a Movie from My Favorites
The second part of this challenge is to write the code to remove a movie from the My Favorites list.
The code for deleting the :HAS_FAVORITE
relationship will be similar, only the Cypher statement will change.
Instead of two separate MATCH
clauses, we can instead attempt to find the pattern within a single clause.
If the relationship (with an alias of r
) exists, we will delete it and then return the movie information with favorite
set to false
.
Click here to reveal the Cypher statement
MATCH (u:User {userId: $userId})-[r:HAS_FAVORITE]->(m:Movie {tmdbId: $movieId})
DELETE r
RETURN m {
.*,
favorite: false
} AS movie
Use the code from the add()
method above to implement the remove()
function.
If you get stuck, you can reveal the completed method below.
Working Solution
Click here to reveal the completed remove()
method
def remove(self, user_id, movie_id):
# Define a transaction function to delete the HAS_FAVORITE relationship within a Write Transaction
def remove_from_favorites(tx, user_id, movie_id):
row = tx.run("""
MATCH (u:User {userId: $userId})-[r:HAS_FAVORITE]->(m:Movie {tmdbId: $movieId})
DELETE r
RETURN m {
.*,
favorite: false
} AS movie
""", userId=user_id, movieId=movie_id).single()
# If no rows are returnedm throw a NotFoundException
if row == None:
raise NotFoundException()
return row.get("movie")
# Execute the transaction function within a Write Transaction
with self.driver.session() as session:
# Return movie details and `favorite` property
return session.execute_write(remove_from_favorites, user_id, movie_id)
Listing My Favorites
Finally, the all()
method in the FavoriteDAO
currently returns a hardcoded list of Movies.
def all(self, user_id, sort = 'title', order = 'ASC', limit = 6, skip = 0):
# TODO: Open a new session
# TODO: Retrieve a list of movies favorited by the user
return popular
Update this method to return a paginated list of movies that the user has added to their My Favorites list.
Click here to reveal the Cypher statement
MATCH (u:User {userId: $userId})
MATCH (m:Movie {tmdbId: $movieId})
MERGE (u)-[r:HAS_FAVORITE]->(m)
ON CREATE SET r.createdAt = datetime()
RETURN m {
.*,
favorite: true
} AS movie
This time, as the query is simpler, you can use a lambda function to represent the transaction function.
# Retrieve a list of movies favorited by the user
movies = session.execute_read(lambda tx: tx.run("""
MATCH (u:User {{userId: $userId}})-[r:HAS_FAVORITE]->(m:Movie)
RETURN m {{
.*,
favorite: true
}} AS movie
ORDER BY m.`{0}` {1}
SKIP $skip
LIMIT $limit
""".format(sort, order), userId=user_id, limit=limit, skip=skip).value("movie"))
Escaped Braces
You may have noticed that the code block above features double curly braces ({{
and }}
) within the MATCH
clause rather than the single braces used within the Cypher statement.
MATCH (u:User {{userId: $userId}})
Braces need to be escaped within a Python string, and we do this by using double quotes.
Click here to reveal the completed all()
method
def all(self, user_id, sort = 'title', order = 'ASC', limit = 6, skip = 0):
# Open a new session
with self.driver.session() as session:
# Retrieve a list of movies favorited by the user
movies = session.execute_read(lambda tx: tx.run("""
MATCH (u:User {{userId: $userId}})-[r:HAS_FAVORITE]->(m:Movie)
RETURN m {{
.*,
favorite: true
}} AS movie
ORDER BY m.`{0}` {1}
SKIP $skip
LIMIT $limit
""".format(sort, order), userId=user_id, limit=limit, skip=skip).value("movie"))
return movies
Testing
To test that this functionality has been correctly implemented, run the following code in a new terminal session:
pytest tests/07_favorites_list__test.py
The test file is located at tests/07_favorites_list__test.py
.
Are you stuck? Click here for help
If you get stuck, you can see a working solution by checking out the 07-favorites-list
branch by running:
git checkout 07-favorites-list
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.favorite@neo4j.com
will have added the movie Toy Story 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.
MATCH (u:User {email: "graphacademy.favorite@neo4j.com"})-[:HAS_FAVORITE]->(:Movie {title: 'Toy Story'})
RETURN true AS shouldVerify
Solution
The following statement will mimic the behaviour of the test, merging a new :User
node with the email address graphacademy.favorite@neo4j.com
and ensuring that a node exists for the movie Toy Story.
The test then merges a :HAS_FAVORITE
relationship between the user and movie nodes.
MERGE (u:User {userId: '9f965bf6-7e32-4afb-893f-756f502b2c2a'})
SET u.email = 'graphacademy.favorite@neo4j.com'
MERGE (m:Movie {tmdbId: '862'})
SET m.title = 'Toy Story'
MERGE (u)-[r:HAS_FAVORITE]->(m)
RETURN *
Once you have run this statement, click Try again… to complete the challenge.
Module Summary
In this Challenge, you have written the code to find or create a :HAS_FAVORITE
relationship between a User
and a Movie
within a write transaction.
In the next Challenge, you will write code to execute multiple queries in the same transaction.