def get(self, id, pair_no): ''' Returns the opaque pair ID for pair with pair nubmer pair_no in this Tournament. Args: id: Tournament ID for this tournament. pair_no: The pair number whose ID has to be looked up. ''' if not is_int(pair_no): SetErrorStatus( self.response, 404, "Invalid Pair Number", "Pair number must be an integer, was {}".format(pair_no)) return tourney = GetTourneyWithIdAndMaybeReturnStatus(self.response, id) if not tourney: return if not CheckUserOwnsTournamentAndMaybeReturnStatus( self.response, users.get_current_user(), tourney): return player_pairs = PlayerPair._query( ndb.GenericProperty('pair_no') == int(pair_no), ancestor=tourney.key).fetch(1, projection=[PlayerPair.id]) if not player_pairs: SetErrorStatus( self.response, 404, "Invalid Id", "Pair pair number {} does not exist in this " + "tournament".format(pair_no)) return self.response.headers['Content-Type'] = 'application/json' self.response.set_status(200) self.response.out.write( json.dumps({'pair_id': player_pairs[0].id}, indent=2))
def _ValidateHandResultMaybeSetStatus(self, board_no, ns_pair, ew_pair, ns_score, ew_score, calls): ''' Validates the proposed hand results as a real Tichu score. Args: board_no: Integer. Hand number. ns_pair: Integer. Pair number of team playing North/South. ew_pair: Integer. Pair number of team playing East/West. ns_score: Integer or String. Score of the North/South team. If string, must be one of AVG, AVG+, AVG++, AVG-, AVG-- allowing for any capitalization. ew_score: Integer or String. Score of the East/West team. If string, must be one of AVG, AVG+, AVG++, AVG-, AVG-- allowing for any capitalization. calls: Dictionary. Holds calls from each team. Can be None. Returns: True iff the proposed score is a valid Tichu score.s ''' error = "Invalid Score" try: HandResult(board_no, ns_pair, ew_pair, ns_score, ew_score, Calls.FromDict(calls)) except InvalidScoreError as err: SetErrorStatus(self.response, 400, error, "These scores are not a valid Tichu score") return False except InvalidCallError as err: SetErrorStatus(self.response, 400, error, "{} are not valid Tichu calls".format(calls)) return False return True
def _CheckValidTournamentInfoAndMaybeSetStatus(self, name, no_pairs, no_boards, players=None): ''' Checks if the input is valid and sane. If not sets the response with the appropriate status and error message. Assumes no_pairs and no_boards are integers. ''' if name == "": SetErrorStatus(self.response, 400, "Invalid input", "Tournament name must be nonempty") return False elif no_pairs < 2: SetErrorStatus( self.response, 400, "Invalid input", "Number of pairs must be > 1, was {}".format(no_pairs)) return False elif no_boards < 1: SetErrorStatus( self.response, 400, "Invalid input", "Number of boards must be > 0, was {}".format(no_boards)) return False elif players: for player in players: if player['pair_no'] < 1 or player['pair_no'] > no_pairs: SetErrorStatus( self.response, 400, "Invalid Input", "Player pair must be between 1 and no_pairs, was {}.". format(player['pair_no'])) return False return BuildMovementAndMaybeSetStatus(self.response, no_pairs, no_boards) is not None
def _ParseRequestInfoAndMaybeSetStatus(self): ''' Parses the body of the request. Checks if the body is valid JSON with all the proper fields set. Checks if the number of boards and number of pairs are valid numbers. If not sets the response with the appropriate status and error message. ''' try: request_dict = json.loads(self.request.body) except ValueError: SetErrorStatus(self.response, 500, "Invalid Input", "Unable to parse request body as JSON object") return None if not isinstance(request_dict.get('no_pairs'), int): SetErrorStatus(self.response, 400, "Invalid Input", "no_pairs must be an integer") return None elif not isinstance(request_dict.get('no_boards'), int): SetErrorStatus(self.response, 400, "Invalid Input", "no_boards must be an integer") return None elif request_dict.get('players'): player_list = request_dict.get('players') for player in player_list: if not player['pair_no']: SetErrorStatus(self.response, 400, "Invalid Input", "Player must have corresponding pair number if present.") return None if not isinstance(player['pair_no'], int): SetErrorStatus(self.response, 400, "Invalid Input", "Player pair number must be an integer, was {}".format( player['pair_no'])) return None return request_dict
def get(self, id, pair_no): ''' Fetch movement for tournament with id and team pair_no. Args: id: String. Tournament id. pair_no: Integer. Pair number for the team whose movement we're getting. See api for request and response documentation. ''' tourney = GetTourneyWithIdAndMaybeReturnStatus(self.response, id) if not tourney: return if not self._CheckValidPairMaybeSetStatus(tourney, pair_no): return player_pair = PlayerPair.GetByPairNo(tourney, int(pair_no)) if not player_pair: SetErrorStatus(self.response, 404, "Invalid Request", "Player pair {} not in tournament".format(pair_no)) return if not self._CheckUserAllowedToSeeMovementMaybeSetStatus( tourney, player_pair): return no_hands_per_round, no_rounds = Movement.NumBoardsPerRoundFromTotal( tourney.no_pairs, tourney.no_boards) try: movement = Movement.CreateMovement( tourney.no_pairs, no_hands_per_round, no_rounds).GetMovement( int(pair_no)) except ValueError: SetErrorStatus(self.response, 500, "Corrupted Data", "No valid movement for this tourney's config") return movement_list = [] for round in movement: movement_list.append( self._GetJsonRoundWithScore(round, tourney, int(pair_no))) combined_dict = { 'name' : tourney.name, 'players' : player_pair.player_list(), 'movement': movement_list } self.response.headers['Content-Type'] = 'application/json' self.response.set_status(200) self.response.out.write(json.dumps(combined_dict, indent=2))
def _CheckUserHasAccessMaybeSetStatus(self, tourney, hand_no): '''Tests if the current user has access to the results of this hand. Uses the pair id code, if any, set in the request header to see if the user is in one of the teams that is set to play this hand in this movement. Directors always have access. Args: tourney: Tournament. Current tournament. hand_no: Integer. Number of the hand. Returns: A (Boolean, Integer, [(Integer, Integer)]) tuple. First member is True iff the user should have access to the hand provided they played it (or is a director). The second member is the pair number of the user (0 for director). Only set if first member is True. The last member is the list of (nw_pair, ew_pair) tuples that correspond to the pairs that play hand_no in this movement. ''' user = users.get_current_user() movement = tourney.GetMovement() all_matchups = movement.GetListOfPlayersForHand(hand_no) if user and tourney.owner_id == user.user_id(): return (True, 0, all_matchups) if tourney.IsUnlocked(): SetErrorStatus( self.response, 403, "Forbidden User", "The tournament is not set up to show hand " + "results to players.") return (False, None, None) error = "Forbidden User" pair_id = GetPairIdFromRequest(self.request) player_pairs = PlayerPair._query( ndb.GenericProperty('id') == pair_id).fetch( 1, projection=[PlayerPair.pair_no]) if not pair_id or not player_pairs: SetErrorStatus( self.response, 403, error, "User does not own tournament and is not authenticated " + "with a pair code to see the results of this hand.") return (False, None, None) pair_no = player_pairs[0].pair_no if not self._PlayerInMatchupList(pair_no, all_matchups): SetErrorStatus(self.response, 403, error, "User does not play this hand.") return (False, None, None) return (True, pair_no, all_matchups)
def get(self, id): ''' Returns tournament and pair number information this pair_id. Args: id: Tournament ID for this tournament Returns: a list of all unique ids in order. Length will necessarily equal to the number of pairs in this tournament. ''' tourney = GetTourneyWithIdAndMaybeReturnStatus(self.response, id) if not tourney: return if not CheckUserOwnsTournamentAndMaybeReturnStatus( self.response, users.get_current_user(), tourney): return player_pairs = PlayerPair._query(ancestor=tourney.key).fetch( projection=[PlayerPair.id, PlayerPair.pair_no]) player_pairs.sort(key=lambda p: p.pair_no) if not player_pairs: SetErrorStatus( self.response, 500, "Corrupted Data", "Could not find any players for this tournament. " + "Consider resetting tournament info.") self.response.headers['Content-Type'] = 'application/json' self.response.set_status(200) self.response.out.write( json.dumps({"pair_ids": [p.id for p in player_pairs]}, indent=2))
def put(self, id): user = users.get_current_user() tourney = GetTourneyWithIdAndMaybeReturnStatus(self.response, id) if not tourney: return if not CheckUserOwnsTournamentAndMaybeReturnStatus( self.response, user, tourney): return request_dict = self._ParseRequestInfoAndMaybeSetStatus() if not request_dict: return name = request_dict['name'] no_pairs = request_dict['no_pairs'] no_boards = request_dict['no_boards'] player_list = request_dict.get('players') if not self._CheckValidTournamentInfoAndMaybeSetStatus( name, no_pairs, no_boards, player_list): return if ((tourney.no_pairs != no_pairs or tourney.no_boards != no_boards) and len(tourney.GetScoredHandList()) != 0): SetErrorStatus(self.response, 400, "Invalid Request", "Tournament already has registered hands") return self.response.set_status(204) tourney.no_pairs = no_pairs tourney.no_boards = no_boards tourney.name = name tourney_key = tourney.put() tourney.PutPlayers(player_list, no_pairs)
def delete(self, id, board_no, ns_pair, ew_pair): ''' Delete hand with these hand number and opponents from this tournament. Args: id: String. Tournament id. board_no: Integer. Hand number. ns_pair: Integer. Pair number of team playing North/South. ew_pair: Integer. Pair number of team playing East/West. See api for request and response documentation. ''' tourney = GetTourneyWithIdAndMaybeReturnStatus(self.response, id) if not tourney: return if not CheckUserOwnsTournamentAndMaybeReturnStatus(self.response, users.get_current_user(), tourney): return if not CheckValidHandPlayersCombinationAndMaybeSetStatus( self.response, tourney, board_no, ns_pair, ew_pair): return hand_score = HandScore.GetByHandParams(tourney, board_no, ns_pair, ew_pair) if not hand_score: SetErrorStatus(self.response, 404, "Invalid Request", "Hand {} between pairs {} and {} is not set".format( board_no, ns_pair, ew_pair)) return hand_score.Delete() self.response.set_status(204)
def _CheckUserAllowedToSeeMovementMaybeSetStatus(self, tourney, player_pair): error = "Forbidden User" user = users.get_current_user() if user and tourney.owner_id == user.user_id(): return True pair_id = GetPairIdFromRequest(self.request) if not pair_id: SetErrorStatus(self.response, 403, error, "User does not own tournament and is not authenticated " + "with a pair code to see this movement") return False if pair_id != player_pair.id: SetErrorStatus(self.response, 403, error, "User does not own tournament and is authenticated with " + "the wrong code for pair {}".format(player_pair.pair_no)) return False return True
def _ParsePutRequestInfoAndMaybeSetStatus(self): ''' Parse the body of the request. Checks if the body is valid JSON with all the proper fields set. If not, sets the response with the appropriate status and error message. Returns: a dict with all the request parameters if the required parameters are set. None if any of the required fields are unset or have the wrong type. ''' try: request_dict = json.loads(self.request.body) except ValueError: SetErrorStatus(self.response, 500, "Invalid Input", "Unable to parse request body as JSON object") return None ns_score = request_dict.get('ns_score') ew_score = request_dict.get('ew_score') if not isinstance(ns_score, int): if not isinstance(ns_score, basestring): SetErrorStatus( self.response, 400, "Invalid Input", "ns_score must be int or string, was " + type(ns_score).__name__) return None if ns_score.strip().upper() not in AVG_VALUES: SetErrorStatus( self.response, 400, "Invalid Input", "ns_score must be an integer or avg, was " + str(ns_score)) return None if isinstance(ew_score, int): SetErrorStatus( self.response, 400, "Invalid Input", "Cannot have one team with an avg score and another " "with a real Tichu value ") return None if not isinstance(ew_score, int): if not isinstance(ew_score, basestring): SetErrorStatus( self.response, 400, "Invalid Input", "ew_score must be int or string, was " + type(ew_score).__name__) return None if ew_score.strip().upper() not in AVG_VALUES: SetErrorStatus( self.response, 400, "Invalid Input", "ew_score must be an integer or avg, was " + str(ew_score)) return None if isinstance(ns_score, int): SetErrorStatus( self.response, 400, "Invalid Input", "Cannot have one team with an avg score and another " "with a real Tichu value") return None return request_dict
def _CheckValidPairMaybeSetStatus(self, tourney, pair_no): ''' Test if the provided pair number is valid for tourney. Args: tourney: Tournament. Tournament the pair number is being validated for. pair_no: Integer. Pair number for the team we are validating. ''' error = "Invalid Input" if (not is_int(pair_no)) or int(pair_no) < 1 or int(pair_no) > tourney.no_pairs: SetErrorStatus(self.response, 404, error, "Pair number {} is invalid".format(pair_no)) return False return True
def _CheckUserHasAccessMaybeSetStatus(self, tourney, ns_pair, ew_pair): ''' Tests if the current user has access to a hand with given players. Uses the pair id code, if any, set in the request header to see if the user is in one of the teams playing the hand. Directors always have access. Args: tourney. Tournament. Current tournament. ns_pair: Integer. Pair number of team playing North/South. ew_pair: Integer. Pair number of team playing East/West. Returns: A (Boolean, Integer) pair. First member is True iff the user has access to the hand between ns_pair and ew_pair. Second member is the pair number of the user. Only set if first member is True. ''' user = users.get_current_user() error = "Forbidden User" if user and tourney.owner_id == user.user_id(): return (True, 0) pair_id = GetPairIdFromRequest(self.request) if not pair_id: SetErrorStatus( self.response, 403, error, "User does not own tournament and is not authenticated " + "with a pair code to overwrite this hand.") return (False, None) player_pairs = PlayerPair.query(ndb.OR( PlayerPair.pair_no == int(ns_pair), PlayerPair.pair_no == int(ew_pair)), ancestor=tourney.key).fetch() if (not player_pairs) or (pair_id not in [p.id for p in player_pairs]): SetErrorStatus( self.response, 403, error, "User does not own tournament and is authenticated with " + "the wrong code for involved pairs") return (False, None) return (True, next(p.pair_no for p in player_pairs if p.id == pair_id))
def _ParseHandsFromRequestAndMaybeSetStatus(self): ''' Parses the hands from the body of the request. Checks if the body is valid JSON with all the proper fields set. Checks if each hand has valid scores. If not sets the response with the appropriate status and error message. Returns: List of hand dicts. If the request or its memebrs were invalid returns None. ''' try: request_dict = json.loads(self.request.body) except ValueError: SetErrorStatus(self.response, 500, "Invalid Input", "Unable to parse request body as JSON object") return None for hand in request_dict.get("hands", []): ns_score = hand.get('ns_score') ew_score = hand.get('ew_score') if not isinstance(ns_score, int): if not isinstance(ns_score, basestring): SetErrorStatus(self.response, 400, "Invalid Input", "ns_score must be int or string, was " + type(ns_score).__name__) return None if ns_score.strip().upper() not in AVG_VALUES: SetErrorStatus(self.response, 400, "Invalid Input", "ns_score must be an integer or avg, was " + str(ns_score)) return None if isinstance(ew_score, int): SetErrorStatus(self.response, 400, "Invalid Input", "Cannot have one team with an avg score and another " "with a real Tichu value ") return None if not isinstance(ew_score, int): if not isinstance(ew_score, basestring): SetErrorStatus(self.response, 400, "Invalid Input", "ew_score must be int or string, was " + type(ew_score).__name__) return None if ew_score.strip().upper() not in AVG_VALUES: SetErrorStatus(self.response, 400, "Invalid Input", "ew_score must be an integer or avg, was " + str(ew_score)) return None if isinstance(ns_score, int): SetErrorStatus(self.response, 400, "Invalid Input", "Cannot have one team with an avg score and another " "with a real Tichu value") return None return request_dict.get("hands", [])
def _CheckValidPositionAndMaybeSetStatus(self, position): '''Returns true if the position is one of allowed positions. If not, set an error in the response. Args: position: String. Position returned by the get header. ''' if position != "N" and position != "E": SetErrorStatus( self.response, 404, "Invalid Request", "Position {} is not valid. Must be one of N or E".format( position)) return False return True
def put(self, id): user = users.get_current_user() tourney = GetTourneyWithIdAndMaybeReturnStatus(self.response, id) if not tourney: return if not CheckUserOwnsTournamentAndMaybeReturnStatus(self.response, user, tourney): return request_dict = self._ParseRequestInfoAndMaybeSetStatus() if not request_dict: return name = request_dict['name'] no_pairs = request_dict['no_pairs'] no_boards = request_dict['no_boards'] player_list = request_dict.get('players') allow_score_overwrites = request_dict.get('allow_score_overwrites', False) if not self._CheckValidTournamentInfoAndMaybeSetStatus(name, no_pairs, no_boards, player_list, tourney.legacy_version_id): return if ((tourney.no_pairs != no_pairs or tourney.no_boards != no_boards) and len(tourney.GetScoredHandList()) != 0): SetErrorStatus(self.response, 400, "Invalid Request", "Tournament already has registered hands") return old_no_pairs = tourney.no_pairs tourney.no_pairs = no_pairs tourney.no_boards = no_boards tourney.name = name if allow_score_overwrites: tourney.Unlock() else: tourney.MakeLockable() tourney.PutPlayers(player_list, old_no_pairs) tourney_key = tourney.put_async() self.response.set_status(204)
def get(self, id, board_no): '''Gets all the scored results for a particular hand in the tournament. Args: id: String. Tournament id. board_no: Integer. Hand number. See api for request and response documentation. ''' tourney = GetTourneyWithIdAndMaybeReturnStatus(self.response, id) if not tourney: return if (not is_int(board_no) ) or int(board_no) < 1 or int(board_no) > tourney.no_boards: SetErrorStatus(self.response, 404, "Invalid Hand Parameters", "Board number {} is invalid".format(board_no)) return has_access, pair_no, all_matchups = self._CheckUserHasAccessMaybeSetStatus( tourney, int(board_no)) if not has_access: return position = self.request.headers.get('X-position') if not self._CheckValidPositionAndMaybeSetStatus(position): return scores = self._GetAllPlayedScores(tourney, int(board_no), all_matchups) if not self._CheckPlayerScoredHandsAndMaybeSetStatus( pair_no, all_matchups, scores): return list_of_results = self._ScoresToJson(position, scores, all_matchups) self.response.headers['Content-Type'] = 'application/json' self.response.set_status(200) self.response.out.write( json.dumps({"results": list_of_results}, indent=2))
def get(self, pair_id): ''' Returns tournament and pair number information this pair_id. Args: pair_id: Opaque secret ID used to look up information for the user. ''' player_pairs = PlayerPair._query( ndb.GenericProperty('id') == pair_id).fetch( projection=[PlayerPair.pair_no]) if not player_pairs: SetErrorStatus(self.response, 404, "Invalid Id", "Pair number with this ID does not exist") return info_dict = { 'tournament_infos': [{ 'pair_no': p.pair_no, 'tournament_id': str(p.key.parent().id()) } for p in player_pairs] } self.response.headers['Content-Type'] = 'application/json' self.response.set_status(200) self.response.out.write(json.dumps(info_dict, indent=2))
def _CheckPlayerScoredHandsAndMaybeSetStatus(self, pair, all_matchups, scores): '''Returns true if pair is present in one of the scored matchups Directors always return true. Args: pair: Integer. Pair number we are checking. all_matchups: List of (Integer, Integer) pairs. All matchups for this board possible in the tournament. scores: List of HandScores. List of all scored hands with their scores. ''' if pair == 0: return True scored_matchups = [] for i in xrange(len(scores)): if scores[i]: scored_matchups.append(all_matchups[i]) if not self._PlayerInMatchupList(pair, scored_matchups): SetErrorStatus( self.response, 403, "Forbidden User", "Pair {} has not yet played this hand.".format(pair)) return False return True
def _CanPutHandAndMaybeSetResponse(self, tourney, board_no, ns_pair, ew_pair): ''' Tests whether the tournament lock stats allows this user to write a hand. The owner is always allowed to write a hand. Pair code players can write a hand if the tournament is UNLOCKED or if it is LOCKABLE and no hand is currently written. Args: tourney: Tournament. Current tournament. board_no: Integer. Hand number. ns_pair: Integer. Pair number of team playing North/South. ew_pair: Integer. Pair number of team playing East/West. Returns: Boolean. True iff the put call is allowed. ''' user = users.get_current_user() if (user and tourney.owner_id == user.user_id()) or tourney.IsUnlocked(): return True if tourney.IsLocked(): SetErrorStatus(self.response, 405, "Forbidden by Tournament Status", "This tournament is locked. No hands can be edited by non-directors") return False hand_score = HandScore.GetByHandParams(tourney, board_no, ns_pair, ew_pair) if not hand_score: return True self.response.headers['Content-Type'] = 'application/json' response = { 'calls' : hand_score.calls_dict(), 'ns_score' : hand_score.get_ns_score(), 'ew_score' : hand_score.get_ew_score(), 'notes' : hand_score.notes, } self.response.set_status(405) self.response.out.write(json.dumps(response, indent=2)) return False