Beispiel #1
0
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())
Beispiel #2
0
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)
Beispiel #3
0
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)
Beispiel #4
0
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)
Beispiel #5
0
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)
Beispiel #6
0
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)
Beispiel #7
0
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())
Beispiel #8
0
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)
Beispiel #9
0
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)
Beispiel #10
0
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
Beispiel #11
0
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)
Beispiel #12
0
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)
Beispiel #13
0
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)
Beispiel #14
0
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)
Beispiel #15
0
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()
Beispiel #16
0
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)
Beispiel #17
0
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)