def finish_game(game_name): """Completes a game and stores any necessary data. Returns: JSON data of the completed game. """ # Ensure all necessary parameters are here if not check_body(["correct", "correct_question_ids", "correct_words", "wrong", "wrong_question_ids", "wrong_words"]): return errors.missing_finish_parameters() correct = request.json["correct"] correct_question_ids = request.json["correct_question_ids"] correct_words = request.json["correct_words"] wrong = request.json["wrong"] wrong_question_ids = request.json["wrong_question_ids"] wrong_words = request.json["wrong_words"] # Save generic game result result = GameResult(current_user.id, g.game, 0) db.session.add(result) db.session.flush() # Save more detailed game result game_result = g.game.result(current_user.id, result.id, correct, wrong, correct_question_ids, wrong_question_ids) db.session.add(game_result) db.session.commit() # Update all masteries with words the user has practiced update_masteries(current_user.id, correct_words, wrong_words) # Return the general game result as JSON data return jsonify(result.serialize())
def create_entry(): """Creates a new vocabulary entry. Body: chinese: The chinese characters for this entry. english: The english translation of this entry. pinyin: The pinyin representation of the chinese characters. Returns: The JSON data for the new entry. """ # Check that all necessary data is in the request body if not check_body(["chinese", "english"]): return errors.missing_create_entry_parameters() chinese = request.json["chinese"] english = request.json["english"] # Add the new entry to the database entry = Entry(chinese, english) db.session.add(entry) db.session.commit() # Return JSON data for the new entry return get_entry(entry.id)
def invite_user(): """Invites a user to register on Storytime. Parameters: email: The email of the user who needs an invitation. Returns: 204 no content. """ # Ensure necessary parameters are here if not check_body(["email"]): return errors.missing_invite_parameters() email = request.json["email"] # See if a user exists with this email address user = User.query.filter_by(email=email).first() if user is not None: # Return 400 if a user exists with this email return errors.user_already_exists() # Remove all existing invitations to this user Invitation.query.filter_by(email=email).delete() # Create invitation and add to database invitation = Invitation(email) db.session.add(invitation) db.session.commit() # Send the invitation email send(Email.INVITE, email_address=email) return ("", 204)
def create_story(): """Creates a story with the provided data. Body: name: The name of this story. description: This story's description, in english. position: The position that this story is in, relative to other stories. Returns: The JSON data for this story. """ # Check that all necessary data is in the request body if not check_body(["name", "description", "position"]): return errors.missing_create_story_parameters() name = request.json["name"] description = request.json["description"] position = request.json["position"] # Create the story and add it to MySQL story = Story(name, description, position) db.session.add(story) db.session.commit() # Return the story JSON data return get_story(story.id)
def reset_password(): """Initiates a forgot password request. Body: username: The username or email address of the user who needs to reset their password. Returns: 204 no content, so account info isn't leaked. """ # Check that all necessary data is in the request body if not check_body(["username"]): return errors.missing_password_reset_parameters() username = request.json["username"] # Retrieve the user with this username or email address user = User.query.filter((User.username == username) | (User.email == username)).first() # Create a password request code and save in MySQL reset = PasswordReset(user) db.session.add(reset) db.session.commit() # Send this user an email with a link to reset their password send(Email.FORGOT_PASSWORD, user, reset) # Return no content so people can't use this endpoint to check if a # particular email address has been used to register an account return ("", 204)
def create_passage(): """Creates a passage with the provided data. Body: name: The name of this passage. description: This passage's description, in english. story_id: The id of the story that this passage corresponds to. Returns: The JSON data for the new passage. """ # Check that all necessary data is in the request body if not check_body(["name", "description", "story_id"]): return errors.missing_create_passage_parameters() name = request.json["name"] description = request.json["description"] story_id = request.json["story_id"] # Create the passage and add it to MySQL passage = Passage(name, description, story_id) db.session.add(passage) # Update the story to add this passage id story = Story.query.filter_by(id=story_id).first() passage_ids = json.loads(story.passage_ids) passage_ids.append(passage.id) story.passage_ids = json.dumps(passage_ids) db.session.commit() # Return the passage JSON data return get_passage(passage.id)
def create_question(game_name): """Creates a question for a game. Body: Whatever the necessary parameters are for this game's question. Returns: JSON data of the new question. """ # Ensure necessary parameters are here arguments = inspect.getargspec(g.game.question.__init__).args arguments.remove("self") if not check_body(arguments): return errors.missing_create_question_parameters() # Generate data for the question constructor data = {key: request.json[key] for key in arguments} # Create the question and store it in MySQL question = g.game.question(**data) db.session.add(question) db.session.commit() # Return JSON data of the question return jsonify(question.serialize())
def update_password(user_id): """Updates a user's password. Args: user_id: The id of the user who is updating their password. Body: current_password: The user's current password. new_password: The password the user would like to change it to. Returns: The user's JSON data. """ # Check that all necessary data is in the request body if not check_body(["current_password", "new_password"]): return errors.missing_password_update_parameters() current_password = request.json["current_password"] new_password = request.json["new_password"] # Retrieve the user who is being updated user = User.query.filter_by(id=user_id).first() if user is None: # Return 404 if this user doesn't exist return errors.user_not_found() elif user.id != current_user.id: # Only the authenticated user can update their password return errors.not_authorized() # Use bcrypt to check if the current password is correct current_password_is_correct = bcrypt.checkpw( current_password.encode("utf-8"), user.password.encode("utf-8")) # Return 400 if the current password is incorrect if not current_password_is_correct: return errors.incorrect_password() # Ensure that this password can be used password_error = validate_password(new_password, [user.username, user.email]) if password_error is not None: return password_error # Hash the new password, this is what will be stored in MySQL hashed_password = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt()) # Make the update to this user and save to MySQL user.password = hashed_password db.session.commit() # Return this user's JSON data return get_user(user.id)
def create_nlp_app(): """Creates an NLP app for a specified passage. Arguments: passage_id: The id of the passage that this app corresponds to. Returns: JSON data about the app that was just created. """ # Check that all necessary data is in the request body if not check_body(["passage_id"]): return errors.missing_create_app_parameters() passage_id = request.json["passage_id"] # Create the name for this NLP app name = "storytime-%d-%s" % (passage_id, str(uuid.uuid4())[0:8]) # Create this app on Wit.ai response = requests.post( url="https://api.wit.ai/apps", params={ "v": "20170307" }, headers={ "Authorization": "Bearer %s" % os.environ["WIT_ACCESS_TOKEN"], "Content-Type": "application/json; charset=utf-8" }, data=json.dumps({ "name": name, "lang": "zh", "private": True }) ) # Retrieve data given back to us by Wit.ai response_json = response.json() access_token = response_json["access_token"] app_id = response_json["app_id"] # Create this app in the database nlp_app = NLPApp(name, passage_id, access_token, app_id) db.session.add(nlp_app) db.session.commit() # Return JSON data about this app return get_nlp_app(nlp_app.id)
def charge(): if not check_body(["plan", "transaction"]): return errors.missing_charge_parameters() plan = int(request.json["plan"]) token = request.json["transaction"]["id"] amount = [499, 2748, 4992][plan] customer = Customer.create(email=current_user.email, source=token) charge = Charge.create(customer=customer.id, amount=amount, currency="usd", description="Flask Charge") return "", 204
def train_model(answer_id): """Deletes an answer from MySQL and uses it to train the model. Body: classification: The classification for this answer. Returns: 204 no content. """ # Ensure necessary parameters are here if not check_body(["classification"]): return errors.missing_train_parameters() classification = request.json["classification"] if classification < 0 or classification > 10: return errors.invalid_classification() # Create client and get the new and old filepaths s3 = boto3.client( "s3", aws_access_key_id=os.environ["S3_AWS_ACCESS_KEY_ID"], aws_secret_access_key=os.environ["S3_AWS_SECRET_ACCESS_KEY"]) answer = WriterAnswer.query.filter_by(id=answer_id).first() new_path = "writer/%d/%s.png" % (classification, answer.name) old_path = "writer/unclassified/%s.png" % answer.name # If the classification = 0, the image is just being deleted. Otherwise, it # should be moved to its new location if classification > 0: s3.copy({ "Bucket": "storytimeai", "Key": old_path }, "storytimeai", new_path) # Delete the image from S3 s3.delete_object(Bucket="storytimeai", Key=old_path) # Delete answer from MySQL WriterAnswer.query.filter_by(id=answer_id).delete() return ("", 204)
def answer_question(): """Scores a user's answer to a question and saves their answer for review. Body: image: A base64 representation of the image drawn by the user. Returns: JSON object with the resulting prediction. """ # Ensure necessary parameters are here if not check_body(["image"]): return errors.missing_answer_parameters() # Resize the image for classification and convert to bytes base64_image = request.json["image"][22:] image = Image.open(BytesIO(base64.b64decode(base64_image))) image = image.resize((28, 28), Image.ANTIALIAS) data = BytesIO() image.save(data, format="PNG") data = data.getvalue() # Create client and save the image to S3 s3 = boto3.client( "s3", aws_access_key_id=os.environ["S3_AWS_ACCESS_KEY_ID"], aws_secret_access_key=os.environ["S3_AWS_SECRET_ACCESS_KEY"]) filename = str(uuid.uuid4()) path = "writer/unclassified/%s.png" % filename s3.put_object(Body=data, Bucket="storytimeai", Key=path) # Save path in S3 to MySQL answer = WriterAnswer(filename) db.session.add(answer) db.session.commit() # Predict the number depicted in the image and return the prediction prediction = make_prediction(session, image) return jsonify(prediction=prediction)
def verify_email(): """Verifies an email address. Body: code: The code sent to the inbox of the email being verified. Returns: 204 no content. """ # Check that all necessary data is in the request body if not check_body(["code"]): return errors.missing_verify_email_parameters() code = request.json["code"] # Look up the verification object with this code verification = EmailVerification.query.filter_by(code=code).first() if verification is None: # Return 400 if the verification code is invalid return errors.invalid_verification_code() user = User.query.filter_by(id=verification.user_id).first() if user is None: # This should never happen, in theory log_error("User from verification code doesn't exist") return errors.invalid_verification_code() # Make sure the user hasn't changed their email address since this verification was initiated if user.pending_email != verification.email: return errors.incorrect_verification_email() # Set the user to verified and delete the verification object user.email = user.pending_email user.pending_email = None db.session.delete(verification) db.session.commit() return ("", 204)
def train_model_directly(): """Trains a model directly, without going through the game. Body: classification: The classification for this image. image: A base64 representation of the image drawn by the user. Returns: 204 no content. """ # Ensure necessary parameters are here if not check_body(["classification", "image"]): return errors.missing_train_parameters() classification = request.json["classification"] image = request.json["image"] # Process image so it can be stored for training base64_image = image[22:] img = Image.open(BytesIO(base64.b64decode(base64_image))) img = img.resize((28, 28), Image.ANTIALIAS) data = BytesIO() img.save(data, format="PNG") data = data.getvalue() # Create S3 client and save the image s3 = boto3.client( "s3", aws_access_key_id=os.environ["S3_AWS_ACCESS_KEY_ID"], aws_secret_access_key=os.environ["S3_AWS_SECRET_ACCESS_KEY"]) filename = str(uuid.uuid4()) path = "writer/%d/%s.png" % (classification, filename) s3.put_object(Body=data, Bucket="storytimeai", Key=path) # Return nothing, the action is done return ("", 204)
def login(): """Logs in a user with the correct credentials. Body: username: The username or email address of the user logging in. password: The user's password. Returns: JSON data for the user who just logged in. """ # Check that all necessary data is in the request body if not check_body(["username", "password"]): return errors.missing_login_parameters() username = request.json["username"] password = request.json["password"] # Retrieve the user with the correct username or email address user = User.query.filter((User.username == username) | (User.email == username)).first() # Return 401 if there is no user with this username or email if user is None: return errors.invalid_credentials() # Check if the password is correct password_is_correct = bcrypt.checkpw(password.encode("utf-8"), user.password.encode("utf-8")) if password_is_correct: # Set the session correctly and return this user's JSON data login_user(user, remember=True) return get_user(user.id) else: # Return 401 if the password is incorrect return errors.invalid_credentials()
def update_user(user_id): """Updates a user's settings. Args: user_id: The id of the user whose settings are being updated. Body: section: The settings section that is being updated. data: The new settings data for the specified section. Returns: The JSON data for the user whose settings have been updated. """ # Check that all necessary data is in the request body if not check_body(["section", "data"]): return errors.missing_update_parameters() section = request.json["section"] data = request.json["data"] # Retrieve the user who is being updated user = User.query.filter_by(id=user_id).first() if user is None: # Return 404 if this user doesn't exist return errors.user_not_found() elif user.id != current_user.id and not current_user.is_admin: # Only the authenticated user and admins can update a user's settings return errors.not_authorized() # Get the settings that need to be updated settings = user.get_settings() if section not in settings: # Ensure that the settings section is valid return errors.invalid_settings_section() elif section == "profile": # Handle username correctly if the profile section is being updated if "username" in data and data["username"] != user.username: username = data["username"] # Ensure that this username can be used username_error = validate_username(username) if username_error is not None: return username_error # Set the user's new username user.username = username # Handle email correctly if the profile section is being updated if "email" in data and data["email"] != user.email: email = data["email"] # Ensure that this email address can be used email_error = validate_email(email) if email_error is not None: return email_error # Set the user's new email address user.pending_email = email # Add an email verification object to the database, to be deleted # when the user clicks the link in their inbox verification = EmailVerification(user.id, email) db.session.add(verification) db.session.commit() # Send the verification email send(Email.VERIFY_EMAIL, user, verification) settings[section] = { "first_name": data["first_name"], "last_name": data["last_name"] } else: # Just set the data for this section for all other sections settings[section] = data # Update the user's settings in MySQL user.settings = json.dumps(settings) db.session.commit() # Return this user's JSON data return get_user(user.id)
def register(): """Creates a new user who can log in and use Storytime. Body: username: The username for the new user. email: The new user's email address. password: The new user's password. Returns: The JSON data for the new user. """ # Check that all necessary data is in the request body if not check_body(["username", "email", "password"]): return errors.missing_registration_parameters() username = request.json["username"] email = request.json["email"] password = request.json["password"] # Ensure that this username can be used username_error = validate_username(username) if username_error is not None: return username_error # Ensure that this email address can be used email_error = validate_email(email) if email_error is not None: return email_error # Ensure that this password can be used password_error = validate_password(password, [username, email]) if password_error is not None: return password_error invitation = Invitation.query.filter_by(email=email).first() # Make sure this user has received an invitation if invitation is None: return errors.registration_is_disabled() else: Invitation.query.filter_by(email=email).delete() # Hash the password with bcrypt, this is what we'll save to MySQL hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) # Create this user in the database user = User(username, email, hashed_password) db.session.add(user) if invitation is None: # Add an email verification object to the database, to be deleted when # the user clicks the link in their inbox verification = EmailVerification(user.id, user.email) db.session.add(verification) # Send the verification email send(Email.VERIFY_EMAIL, user, verification) else: # If the user is accepting an invitation, they are already verified user.pending_email = None db.session.commit() # Log the user into the account they just created login_user(user, remember=True) # Return JSON data about the new account return get_user(user.id)