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

R7 json api #74

Closed
wants to merge 12 commits into from
Closed

R7 json api #74

wants to merge 12 commits into from

Conversation

dexw25
Copy link
Member

@dexw25 dexw25 commented Dec 3, 2017

Resolves #38.

Implemented more complete read only JSON api. There is capability to read a full list or single selection from bridge crews, members, and missions. Ranks have also been separated out under /api/rank/<rank_type>/<member_id> to avoid calculating them unless the information is explicitly requested.

There is capability through this to read every static element of the datastore, except for member QR codes (they wouldn't format easily to text). Every endpoint requires a valid apiKey, and the lists support semester-based filtering.

@dexw25 dexw25 added this to the r7 milestone Dec 3, 2017
api.py Outdated
'name': member.name,
'dce': member.dce,
'mailingList': member.mailing_list,
'currentStudent': member.current_student,
'mailing_list': member.mailing_list,
Copy link
Member

Choose a reason for hiding this comment

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

Because this function is formatting a member to be returned as JSON, I think camelCase should be used to align with the JavaScript standard.

Copy link
Member Author

Choose a reason for hiding this comment

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

You make a good point. I have made the case consistent

api.py Outdated
@@ -37,24 +86,26 @@ def get(self):
selected_semester = self.request.get('semester')

if selected_semester:
members = Member.query(Member.show == True, Member.semesters_paid == selected_semester).order(Member.name).fetch(limit=None)
# Avoid throwing error 500 if a bad semester string is supplied
Copy link
Member

Choose a reason for hiding this comment

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

Very minor, but since the file needs edits anyway, would you mind terribly if I requested period at the end of the sentence?

Copy link
Member Author

Choose a reason for hiding this comment

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

corrected

api.py Outdated
else:
members = Member.query(Member.show == True, Member.never_paid == False).order(Member.name).fetch(limit=None)
members = Member.query().order(Member.name).fetch(limit=None)
Copy link
Member

Choose a reason for hiding this comment

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

This appears to expose members who asked to not appear on the website (the purpose of the Member.show == True), as well as non-members in the system beacuse they ran a mission or are on the mailing list (Member.never_paid == False).

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought that it would be simpler to provide access to all members through the API, and do the member filtering in the app that pulls data, since there could be cases where we might want access to data for members that have not paid or don't wish to be seen publicly. Since access to the API is gated by the API key anyway, better to give more information here and avoid limiting uses of this interface later on down the road.

Copy link
Member

Choose a reason for hiding this comment

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

I am pretty sure the database has more non-members than members (between the mailing list and non-members who ran missions). Fetching every member for every API call is going to make a lot of queries unnecessarily slow and put a lot of unnecessary load on the server.

What do you think about adding a URL parameter for including non-members in the system that is False by default?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, makes sense. I will implement a default False parameter to include private or non-paid members, that way we can only pull the whole list when necessary

output = []
for member in members:
output.append(format_member(member))
output = [format_member(member) for member in members]
Copy link
Member

Choose a reason for hiding this comment

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

Good idea; much cleaner! 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Gotta love python list comprehensions

api.py Outdated

self.response.headers['Content-Type'] = 'application/json'
self.response.write(json.dumps(output))

class MemberAPI(webapp2.RequestHandler):
def get(self):
def get(self, id):
Copy link
Member

Choose a reason for hiding this comment

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

Why the move from URL parameter to part of the path?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is a lot cleaner to be part of the path instead of a URL parameter, makes the URL structure more readable, and also allows the ability to select only specific parts of the member object if desired. For instance, for performance reasons it may be desirable someday to just pull /members/<id>/semestersPaid for a large set of members, then this class can just have another optional parameter added with a case for that.

Copy link
Member

@ZMYaro ZMYaro Feb 2, 2018

Choose a reason for hiding this comment

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

When would you use /members/<id>/semesterspaid though? The handling of that API call would be identical to the existing member API, except with format_member stripping out the extra fields. Unless something changed since I last read through the GAE datastore docs, all datastore queries are the equivalent of SELECT * FROM..., so the load on the datastore would presumably be the same.

On the other hand, /members?id=<id> makes it clear that /members is the API URL.

Copy link
Member Author

@dexw25 dexw25 Mar 27, 2018

Choose a reason for hiding this comment

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

Makes sense, code will be simpler if the members endpoint just handles everything. I will strike out MemberAPI and just add handling for id as a parameter to /members, and make it just return a list of one member if filtered by id

Also it's probably gonna be a lot less confusing going forward to remove the distinction between member and members as endpoints (IMO)

api.py Outdated
@@ -71,14 +122,131 @@ def get(self):
self.response.headers['Content-Type'] = 'application/json'
self.response.write(json.dumps(output))

class RankAPI(webapp2.RequestHandler):
def get(self, rank_type, id):
Copy link
Member

Choose a reason for hiding this comment

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

Why parts of the path rather than parameters?

Copy link
Member Author

Choose a reason for hiding this comment

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

See my other comment on URL path structure, I think I will remove the rank_type parameter though, since per your other comment that is probably not the best way to handle that interface.

Copy link
Member

Choose a reason for hiding this comment

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

See my reply above about URL path structure.

api.py Outdated
self.error(400)
return

if rank_type == 'name':
Copy link
Member

Choose a reason for hiding this comment

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

Instead of returning rank types, what about returning a Rank object, like how ranks are defined in ranks.py, that just includes all three? You already set the output type as JSON, so it does not need to just be one string.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. Changed to return an object with all 3 options rather than selecting one

api.py Outdated
output = {
'rankName': member.get_rank_name(selected_semester),
'rankDisp': member.get_rank_disp(selected_semester),
'rankWithName': member.get_name_with_rank(selected_semester)}
Copy link
Member

Choose a reason for hiding this comment

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

Since the API is to fetch the rank, not the member, I am inclined to call the rank fields, name, abbr, and disp (for consistency with ranks.py), and then if we want to include name-with-rank, give that a name that suggests the member name is being appended to the rank, not the other way around.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, will rename to be more consistent. Is rankName good for signifying we are appending the name?

Also I realized looking through this right now that there is no method to access abbr in ranks.py, is it worth adding in this commit?

api.py Outdated

# Offer option to filter by a semester, but by default just send all missions
if selected_semester:
# Avoid throwing error 500 if a bad semester string is supplied
Copy link
Member

Choose a reason for hiding this comment

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

Sentence ending punctuation (sorry)?

Copy link
Member Author

@dexw25 dexw25 Mar 27, 2018

Choose a reason for hiding this comment

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

Will correct (sorry I am bad at formatting comments)

api.py Outdated
else:
members = Member.query(Member.show == True, Member.never_paid == False).order(Member.name).fetch(limit=None)
members = Member.query().order(Member.name).fetch(limit=None)
Copy link
Member

Choose a reason for hiding this comment

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

I am pretty sure the database has more non-members than members (between the mailing list and non-members who ran missions). Fetching every member for every API call is going to make a lot of queries unnecessarily slow and put a lot of unnecessary load on the server.

What do you think about adding a URL parameter for including non-members in the system that is False by default?

@dexw25
Copy link
Member Author

dexw25 commented Mar 27, 2018

I rebased to master (again) and addressed comments. please review again and let me know if it's good
@ZMYaro
@noblelady

api.py Outdated
'youtubeUrl': mission.youtube_url
}

def format_bridgecrew(crew):
Copy link
Member

Choose a reason for hiding this comment

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

“Bridge Crew” is two words, so this should be “format_bridge_crew”.

api.py Outdated
def get(self):
if not check_authentication(self):
return
bridgecrews = BridgeCrew.query().fetch(limit=None)
Copy link
Member

Choose a reason for hiding this comment

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

Same as above—“bridge_crews”.

api.py Outdated
('/api/missions/?', MissionListAPI),
('/api/mission/([a-z0-9]+)', MissionAPI),
('/api/bridgecrews/?', BridgeCrewListAPI),
('/api/bridgecrew/([a-z0-9]+)?', BridgeCrewAPI),
Copy link
Member

Choose a reason for hiding this comment

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

Either /rank should not accept A-Z, or /mission and /bridgecrew should accept. I am inclined to say the latter so the error comes from it being an invalid ID requested rather than appearing to be an invalid API endpoint.

But then might it be better to just make those each (.+) (or ([^/]+)) and have the functions handle the error states?

models.py Outdated

def get_mission_ids(self):
missions = Mission.query(Mission.runners == self.id).order(Mission.start).fetch(limit=None)
return [mission.id for mission in missions]
Copy link
Member

Choose a reason for hiding this comment

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

What is the benefit of this function existing? I think it would make more sense to do the list comprehension in the one place you use Member.mission_ids.

Copy link
Member

Choose a reason for hiding this comment

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

I am also concerned that the get_mission_ids function existing might suggest to someone new to the codebase that get_mission_ids is somehow less resource- or time-intensive than get_missions.

@dexw25 dexw25 force-pushed the r7-json-api branch 2 times, most recently from f876cce to 77517a2 Compare June 8, 2018 19:16
@dexw25
Copy link
Member Author

dexw25 commented Jun 8, 2018

rebased and addressed comments @ZMYaro please re-review when you can

@dexw25 dexw25 closed this Aug 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add more functions to the JSON API
2 participants