Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Implement new API routes to fetch blog posts by authorIds and to update a post #2

Open
wants to merge 79 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
9c54e06
Start route that handles GET request for posts having at least one of…
huaszu Apr 11, 2023
604f094
Update api/posts.py
huaszu Apr 13, 2023
6904073
Update URL of route and update return statement to have proper jsonif…
huaszu Apr 14, 2023
6589caf
Update URL of route and update return statement to have proper jsonif…
huaszu Apr 14, 2023
3774363
Correct URL to match specification. Shorten docstring.
huaszu Apr 15, 2023
68326a5
Make list of author_ids where each author_id is an integer
huaszu Apr 15, 2023
470cbf2
Make list of Post objects to represent in response
huaszu Apr 15, 2023
44f5f61
Make dictionary containing information to include in response. Forma…
huaszu Apr 15, 2023
d660db7
Handle case of user entering duplicate authors.
huaszu Apr 15, 2023
3a6f17b
Comment to explain use of dictionary.
huaszu Apr 15, 2023
9612dc6
Add file to ignore
huaszu Apr 15, 2023
76e5d03
Add key-value pair to inner dictionary of posts_data dictionary to he…
huaszu Apr 15, 2023
2e26c4b
Implement two directions for sorting blog posts.
huaszu Apr 15, 2023
cb60657
Suggest potential refactoring.
huaszu Apr 15, 2023
04b70b7
Handle error when required query parameter is missing. Handle error …
huaszu Apr 15, 2023
c5b84d0
Change error checking of direction by which to sort posts to help wit…
huaszu Apr 15, 2023
648c0f8
Initial commit
huaszu Apr 16, 2023
3a1e6b6
Implement type hints.
huaszu Apr 16, 2023
c208ca4
Update comment on alternative solution.
huaszu Apr 16, 2023
5529ef6
Refactor so as not to make unnecessary list of Post objects.
huaszu Apr 16, 2023
9a4d4c4
Use sets to consider unique author ids and unique posts. Handle case…
huaszu Apr 16, 2023
dbf2ea6
Update variable names for clarity. Make code more concise by removin…
huaszu Apr 17, 2023
24f1be2
Move file for better organization.
huaszu Apr 17, 2023
732a030
Use set comprehension. Save line of code that initialized an empty set.
huaszu Apr 17, 2023
b01a21f
Incorporate set comprehension. Initialize a set intended to be a sup…
huaszu Apr 17, 2023
6773aa4
Use list comprehension.
huaszu Apr 17, 2023
3ac2e08
Update error to warning.
huaszu Apr 22, 2023
dd9e9a9
Ensure that only a logged in can use this route.
huaszu Apr 22, 2023
c3f5764
Start route to update a blog post. Validate that user is logged in. …
huaszu Apr 23, 2023
7f3a782
Add instance method to get a post by post id.
huaszu Apr 23, 2023
c88810a
Enable logged in user to modify authorIds, tags, or text of post. Wh…
huaszu Apr 26, 2023
1da081a
Test use of row_to_dict(row) function.
huaszu Apr 26, 2023
8674425
Prepare format for Response Body.
huaszu Apr 27, 2023
1855fc0
Update response.
huaszu Apr 27, 2023
58917da
Attempt to get response to match exact format of specification, speci…
huaszu Apr 27, 2023
c4aeed8
Attempt differently to create specified order within response.
huaszu Apr 27, 2023
7107958
Set sorting of keys of JSON objects alphabetically to FALSE to achiev…
huaszu Apr 27, 2023
f630b16
Ensure only an author of a post can update the post.
huaszu Apr 27, 2023
9fcc11f
Give useful error message to user.
huaszu Apr 27, 2023
0f103f9
Give warning when database has no post with requested postId. Give e…
huaszu Apr 27, 2023
99c75f5
Handle error when user does not provide authorIds in the format of an…
huaszu Apr 27, 2023
a9fa08b
Handle error when user enters tags not in the format of an array. Ha…
huaszu Apr 27, 2023
03c8bd5
Handle error when user provides text that is not a string.
huaszu Apr 27, 2023
3c0a5f0
Sort tags alphabetically in response based on looking at example in s…
huaszu Apr 27, 2023
24fa5d1
Undo sort of tags in response because that sort made test_posts.py::t…
huaszu Apr 27, 2023
1321643
Remove unnecessary code because, for the Post class, the use of the @…
huaszu Apr 27, 2023
7b9cb9c
Test using db.utils.rows_to_list(rows) helper function to get toward …
huaszu Apr 27, 2023
d90805e
Improve error handling to handle additional error. Refactor implemen…
huaszu Apr 27, 2023
67ca0ef
Give up opportunity to deduplicate tags because, otherwise, the imple…
huaszu Apr 27, 2023
f4e0b34
Add static method for Post class that builds a query joining the post…
huaszu Apr 30, 2023
52bc8b4
Rename file
huaszu Apr 30, 2023
dd7ca19
Move constant to different file. Write helper functions.
huaszu May 1, 2023
68bb986
Write helper function to format results of database query per specifi…
huaszu May 1, 2023
3c3abdc
Eliminate unnecessary code to handle case when there are no posts to …
huaszu May 1, 2023
e13bc1c
Fix typograhical error
huaszu May 1, 2023
1d7f5ca
Remove unnecessary comment.
huaszu May 1, 2023
d160ce1
Rename file for clarity. Update comment for clarity.
huaszu May 1, 2023
f47e531
Use helper function to validate post_id and, if valid, return post.
huaszu May 1, 2023
e15c332
Make route more concise by factoring portions of code into helper fun…
huaszu May 1, 2023
dd272e8
Write helper function to get post and format information about post f…
huaszu May 1, 2023
06088b0
Refactor for extensibility. Map which kind of error handling message…
huaszu May 1, 2023
1d058c0
Rewrite for consistency and understanding.
huaszu May 2, 2023
5437c1e
Check whether user included data in request and give helpful error me…
huaszu May 2, 2023
c712f5a
Remove unnecessary commented out code.
huaszu May 2, 2023
c52b43c
Make repository layer for functions that make queries to the database…
huaszu Jun 25, 2023
d0a273e
Move function that queries database to repository layer. api/util/he…
huaszu Jun 25, 2023
1f55a4f
Pull out another three functions to repository layer that interact wi…
huaszu Jun 25, 2023
65ce818
Remove unnecessary imports. Fix code to refer to updated file name
huaszu Jun 25, 2023
7a69b51
Fix database operation to delete a UserPost record
huaszu Jun 26, 2023
5dc1b3d
Using Controller-Service-Repository pattern, refactor to make reposit…
huaszu Jul 2, 2023
39f41c7
Revise code to use consistent style across API routes
huaszu Jul 2, 2023
56a7666
Initial commit
huaszu Jul 3, 2023
c628c18
Views of SQLite database for reference, with the help of https://inlo…
huaszu Jul 3, 2023
088d1cd
Include prompt for easy reference. Introduce formatting for readabil…
huaszu Jul 3, 2023
1e15e1a
Improve technical correctness, style, organization, and readability
huaszu Jul 5, 2023
327719b
Update formatting so that all footnotes show
huaszu Jul 5, 2023
26dfb89
Edit list indentation for readability
huaszu Jul 5, 2023
9067923
Edit for clarity
huaszu Jul 5, 2023
cb0a60c
Fix spelling
huaszu Jul 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,7 @@ cython_debug/
*.db

# OS X
.DS_Store
.DS_Store

# Scratch work
scratch_work.txt
93 changes: 93 additions & 0 deletions api/posts.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
from db.utils import row_to_dict
from middlewares import auth_required

import crud


POSTS_SORT_BY_OPTIONS: list[str] = ["id", "reads", "likes", "popularity"]
POSTS_SORT_DIRECTION_OPTIONS: list[str] = ["asc", "desc"]


@api.post("/posts")
@auth_required
Expand Down Expand Up @@ -37,3 +43,90 @@ def posts():
db.session.commit()

return row_to_dict(post), 200

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like the function they gave you does validation this way
'''

validation

user = g.get("user")
if user is None:
    return abort(401)

'''
Maybe you can do the same in your function?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so far your suggestion appears to work. committed here.

i added the code you suggested. then i made this request and got this 401:
Screenshot 2023-04-22 at 6 34 04 PM

next i logged in:
Screenshot 2023-04-22 at 6 41 30 PM

now this request returned what i expected and 200 instead of the 401:
Screenshot 2023-04-22 at 6 44 15 PM


@api.route("/posts", methods=["GET"])
@auth_required
def fetch_posts():
"""
Fetch blog posts that have at least one of the authors specified.
"""
author_ids_input: str = request.args.get("authorIds", None)
huaszu marked this conversation as resolved.
Show resolved Hide resolved

if author_ids_input is None:
return jsonify({"error": "Please identify the author(s) whose posts to fetch using the query parameter key `authorIds`."}), 400

sort_by_input: str = request.args.get("sortBy", "id")
huaszu marked this conversation as resolved.
Show resolved Hide resolved
if sort_by_input not in POSTS_SORT_BY_OPTIONS:
return jsonify({"error": "Unacceptable value for `sortBy` parameter. We can sort by id, reads, likes, or popularity."}), 400

direction_input: str = request.args.get("direction", "asc")
huaszu marked this conversation as resolved.
Show resolved Hide resolved
if direction_input not in POSTS_SORT_DIRECTION_OPTIONS:
return jsonify({"error": "Unacceptable value for `direction` parameter. We only accept asc or desc."}), 400

author_ids: set[int] = set()
huaszu marked this conversation as resolved.
Show resolved Hide resolved

try:
for author_id_input in author_ids_input.split(","):
huaszu marked this conversation as resolved.
Show resolved Hide resolved
author_id = int(author_id_input)
if crud.check_user_exists(author_id):
author_ids.add(author_id)
except:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure what in your try would hit the except - does crud.check_user_exists return an error?

Copy link
Owner Author

@huaszu huaszu Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, crud.check_user_exists does not return an error. if the user does not exist, the db query in crudwould return None per my understanding and crud.check_user_exists would return False. thanks for the question! i had to remind myself what error i was checking.

i want to hit the except when the request has an invalid authorIds query parameter value, for instance when a character other than "," is used to separate ids or when ids are not typed as numbers (see screenshot). i don't know whether testing for an error in author_ids.split(",") is a great translation of that intent.

Screenshot 2023-04-17 at 3 31 56 AM

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an update: crud.check_user_exists can return an error if int(author_id) returns an error, e.g., in the case of the bottom request in the screenshot. whether author_id can be converted to an integer is also part of checking for intended behavior

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i suppose we could be more specific to the requester whether the error was that the query parameter value had no integers at all (e.g., "all"), or had integers but incorrectly separated (e.g., "1+2"), or so on

return jsonify({"error": "Please provide a query parameter value for `authorIds` as a number or as numbers separated by commas, such as '1,5'."}), 400

if not author_ids: # Also helps to avoid the problem that subsequently
# running `Post.query.with_parent(user).all()` on users that do not
# exist will give an error
return jsonify({"error": "None of the author id(s) you requested exist in the database."}), 200
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a little confusing to return a response that has a 200 status and also an error message. Maybe make the error a warning? Or message? Or maybe there's a more appropriate status code for this kind of situation?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want to use another status code you can look through existing ones and their meaning here - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i looked through the status codes. clarifying that there is not actually an error seems the way to go. does this work: 3ac2e08 ? or did you mean a warning as in the Python warnings module or did you have a technical meaning when you said "message"? @hdenisenko


posts_of_authors: set[Post] = set()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this a set over a regular array?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at first i made this an array. i changed my mind because when i read onward in the project instructions to the Part 2 feature, i noticed that for a post, authorIds is an array of integers. i paid more attention to the responses i got to my test GET requests and anecdotally saw that there exist posts in the seeded db that have more than one author.

if a post can have multiple authors, then here when we query for posts by author, a query for a different author can result in a duplicate post. i wanted the set to help avoid duplication.

what benefits of an array might you be interested in that we lose here?

side note: perhaps i could have made the above observation more easily if i looked over the db first. i have only used PostgreSQL before and haven't figured out how to use SQLite. i looked online and made a cursory attempt that did not work.


for author_id in author_ids:
for post in Post.get_posts_by_user_id(author_id):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe another chance for fancy list comprehension on the inner for loop

Copy link
Owner Author

@huaszu huaszu Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was thinking of

    for parsed_author_id in parsed_author_ids: 
        posts_of_authors: set[Post] = set(post for post in Post.get_posts_by_user_id(parsed_author_id)) 

however, does this mean we recreate the set every time we iterate on a parsed_author_id? by the end of this for loop, posts_of_authors would have only the posts from the last parsed_author_id?

my logic was to have a set that can continue to grow with more posts as we go through more authors and add each author's posts to the set.

or did you have something else in mind using comprehension?

Copy link
Owner Author

@huaszu huaszu Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oooh i came up with a different way that incorporates your feedback to use comprehension and solves the problem i considered in the previous comment.

i committed here.

    posts_of_authors: set[Post] = set()

    for parsed_author_id in parsed_author_ids:
        posts_of_author: set[Post] = set(post for post in Post.get_posts_by_user_id(parsed_author_id))
        posts_of_authors.update(posts_of_author)

i initialize a set that i intend to be a superset of posts. in each iteration of the outer for loop, make a set of that author's posts. update this set into the superset.

my first time using the update() method!

now i still have the same number of lines of code. i don't know about the benefits of:

  • (before) adding to a set one element at a time versus
  • (now) updating a set by incorporating other sets - aka potentially multiple elements at a time but does that break down behind the scenes into one at a time anyway/ how?

the differences between these benefits may vary depending on how often in the data we expect there to be duplicates filtered out and not added to the final set (e.g., if many posts are co-authored and it is likely when querying by author to get the same posts again and again)?

an argument could be made that now the code is more readable because we can see that:

  • each outer for loop iteration helps us get one author's posts and
  • we aggregate posts from each author into posts from all authors requested.

also, the before implementation may be more extensible if we foresee wanting to do other manipulating of post by individual post and
the now implementation may be more extensible if we foresee wanting to do other manipulating of an author's posts as a group?

posts_of_authors.add(post)

if not posts_of_authors: # If posts_of_authors is empty, the later code to
# populate the `posts_data` dictionary will give an error so let's
# avoid that
return jsonify({"posts": []}), 200

posts_data: dict[int, dict] = {}

for post in posts_of_authors:
posts_data[post.id] = {"id": post.id, # This key-value pair is redundant with the outer dictionary key. What problems are we causing?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you saving the id as both the key and value?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: line numbers have changed with the latest commits so i am referring to the latest code right now.

since we want to be able to sort on id, reads, likes, or popularity, i thought i would make sure all four of these are organized in a way that line 109 can use the function from line 106 to access any of these four as a key to sort on. what do you think would be better to do? @hdenisenko

"likes": post.likes,
"popularity": post.popularity,
"reads": post.reads,
"tags": post._tags.split(","),
"text": post.text}
# Alternative: For each post, make a dictionary in the format of the
# inner dictionary above. Have a list of these dictionaries. Later
# can sort the list as desired. However, generating this outer
# dictionary of dictionaries seems more extensible and has better time
# complexity if we want to look up a post (though arguably we could
# just access the db to get a post's info - depends on the context,
# what the API user might want to do in the future, what access the
# user has, whom and what we are building for, et al)

def sort_posts_on(item):
huaszu marked this conversation as resolved.
Show resolved Hide resolved
return item[1][sort_by_input]

if direction_input == "asc":
reverse_boolean: bool = False
else:
reverse_boolean: bool = True

sorted_posts: list[tuple] = sorted(posts_data.items(), key=sort_posts_on, reverse=reverse_boolean)
huaszu marked this conversation as resolved.
Show resolved Hide resolved
# Alternative: Have SQLAlchemy help sort posts when querying database on line 85.
# Not sure how much this alternative helps because we query database by
# author id and ultimately we want to sort not on author id, but on
# post id, reads, likes, or popularity. There could be some benefit of,
# for each author, sorting by the desired one of the four sort by options
# at the point of querying the database, and then preparing the final sort
# later. That could be investigated.

result: list[dict] = []
for post_response in sorted_posts:
result.append(post_response[1])
huaszu marked this conversation as resolved.
Show resolved Hide resolved

return jsonify({"posts": result}), 200
7 changes: 7 additions & 0 deletions crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from db.shared import db
huaszu marked this conversation as resolved.
Show resolved Hide resolved
from db.models.user import User

def check_user_exists(user_id):
"""Check by user id whether or not a user exists."""

return User.query.get(user_id) is not None
16 changes: 16 additions & 0 deletions rubric.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Instructions: Complete one project to proceed to the next steps. You'll receive tailored feedback on your programming skills and a recommendation on the best next step to get job ready.

The goal of this assessment is to test your backend development skills.

You will be building on top of a simple JSON API using Python (Flask). If you have never written a JSON API using those technologies before, this https://auth0.com/blog/developing-restful-apis-with-python-and-flask/ may be a good resource that may help you.

The assessment involves building two new routes for a blog post API. Detailed instructions will be available once you click start. Your assessment will be graded based on this rubric. https://drive.google.com/file/d/103oOiqjxd_N1JckefKqPJjndqX1qvqdL/view

FEEDBACK:
WHAT YOU CAN EXPECT

You will receive comprehensive review & feedback from our experienced engineers. You will also receive guidance on what to do next on the Feedback page.

HOW LONG IT WILL NEED

3 - 10 days