-
Notifications
You must be signed in to change notification settings - Fork 4
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
R7 json api #74
Conversation
api.py
Outdated
'name': member.name, | ||
'dce': member.dce, | ||
'mailingList': member.mailing_list, | ||
'currentStudent': member.current_student, | ||
'mailing_list': member.mailing_list, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea; much cleaner! 👍
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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': |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sentence ending punctuation (sorry)?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
I rebased to |
api.py
Outdated
'youtubeUrl': mission.youtube_url | ||
} | ||
|
||
def format_bridgecrew(crew): |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
.
f876cce
to
77517a2
Compare
rebased and addressed comments @ZMYaro please re-review when you can |
…r to get mission IDs run
…ation of semester logic. Also basic semester validation, avoid error 500 if the parameter cannot be cast to a float
…unless semester filter is applied
…ess explicitly specified otherwise
…name bridge_crew local variable. MODELS: Remove extraneous datastore property mission_ids and associated method
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.