<commit_before>"""
API functions relating to user management and registration.
"""

import bcrypt
import re

import api

from api.common import check, validate, safe_fail
from api.common import WebException, InternalException
from api.annotations import log_action
from voluptuous import Required, Length, Schema

_check_email_format = lambda email: re.match(r"[A-Za-z0-9\._%+-]+@[A-Za-z0-9\.-]+\.[A-Za-z]{2,4}", email) is not None

user_schema = Schema({
    Required('email'): check(
        ("Email must be between 5 and 50 characters.", [str, Length(min=5, max=50)]),
        ("Your email does not look like an email address.", [_check_email_format])
    ),
    Required('username'): check(
        ("Usernames must be between 3 and 20 characters.", [str, Length(min=3, max=20)]),
        ("This username already exists.", [
            lambda name: safe_fail(get_user, name=name) is None])
    ),
    Required('password'):
        check(("Passwords must be between 3 and 20 characters.", [str, Length(min=3, max=20)]))
}, extra=True)

new_team_schema = Schema({
    Required('team-name-new'): check(
        ("The team name must be between 3 and 40 characters.", [str, Length(min=3, max=40)]),
        ("A team with that name already exists.", [
            lambda name: safe_fail(api.team.get_team, name=name) is None])
    ),
    Required('team-password-new'):
        check(("Team passwords must be between 3 and 20 characters.", [str, Length(min=3, max=20)])),
    Required('team-adv-name-new'):
        check(("Adviser names should be between 3 and 50 characters.", [str, Length(min=3, max=50)])),
    Required('team-adv-email-new'): check(
        ("Adviser emails must be between 5 and 50 characters.", [str, Length(min=5, max=50)]),
        ("Your adviser email does not look like an email address.", [_check_email_format])
    ),
    Required('team-school-new'):
        check(("School names must be between 3 and 100 characters.", [str, Length(min=3, max=100)]))

}, extra=True)

existing_team_schema = Schema({
    Required('team-name-existing'): check(
        ("Existing team names must be between 3 and 50 characters.", [str, Length(min=3, max=50)]),
        ("There is no existing team named that.", [
            lambda name: api.team.get_team(name=name) != None]),
        ("There are too many members on that team for you to join.", [
            lambda name: len(api.team.get_team_uids(name=name)) < api.team.max_team_users
        ])
    ),
    Required('team-password-existing'):
        check(("Team passwords must be between 3 and 50 characters.", [str, Length(min=3, max=50)]))
}, extra=True)

teacher_schema = Schema({
    Required('teacher-school'):
        check(("School names must be between 3 and 100 characters.", [str, Length(min=3, max=100)]))
}, extra=True)

def hash_password(password):
    """
    Hash plaintext password.

    Args:
        password: <PASSWORD>
    Returns:
        Secure hash of password.
    """

    return bcrypt.hashpw(password, bcrypt.gensalt(8))

def get_team(uid=None):
    """
    Retrieve the the corresponding team to the user's uid.

    Args:
        uid: user's userid
    Returns:
        The user's team.
    """

    user = get_user(uid=uid)
    return api.team.get_team(tid=user["tid"])

def get_user(name=None, uid=None):
    """
    Retrieve a user based on a property. If the user is logged in,
    it will return that user object.

    Args:
        name: the user's username
        uid: the user's uid
    Returns:
        Returns the corresponding user object or None if it could not be found
    """

    db = api.common.get_conn()

    match = {}

    if uid is not None:
        match.update({'uid': uid})
    elif name is not None:
        match.update({'username': name})
    elif api.auth.is_logged_in():
        match.update({'uid': api.auth.get_uid()})
    else:
        raise InternalException("Uid or name must be specified for get_user")

    user = db.users.find_one(match)

    if user is None:
        raise InternalException("User does not exist")

    return user

def create_user(username, email, password_hash, tid, teacher=False):
    """
    This inserts a user directly into the database. It assumes all data is valid.

    Args:
        username: user's username
        email: user's email
        password_hash: a hash of the user's password
        tid: the team id to join
        teacher: whether this account is a teacher
    Returns:
        Returns the uid of the newly created user
    """

    db = api.common.get_conn()
    uid = api.common.token()

    if safe_fail(get_user, name=username) is not None:
        raise InternalException("User already exists!")

    user = {
        'uid': uid,
        'username': username,
        'email': email,
        'password_hash': password_<PASSWORD>,
        'tid': tid,
        'teacher': teacher,
        'avatar': 3,
        'eventid': 0,
        'level': 'Not Started'
    }

    db.users.insert(user)

    return uid

def get_all_users(show_teachers=False):
    """
    Returns:
        Returns the uid, username, and email of all users.
    """

    db = api.common.get_conn()

    match = {}
    projection = {"uid": 1, "username": 1, "email": 1, "tid": 1}

    if not show_teachers:
        match.update({"teacher": False})
        projection.update({"teacher": 1})

    return list(db.users.find(match, projection))

@log_action
def create_user_request(params):
    """
    Registers a new user and creates/joins a team. Validates all fields.
    Assume arguments to be specified in a dict.

    Args:
        username: user's username
        password: <PASSWORD>
        email: user's email
        create-new-teacher:
            boolean "true" indicating whether or not the user is a teacher
        create-new-team:
            boolean "true" indicating whether or not the user is creating a new team or
            joining an already existing team.

        team-name-existing: Name of existing team to join.
        team-password-existing: Password of existing team to join.

        team-name-new: Name of new team.
        team-adv-name-new: Name of adviser.
        team-adv-email-new: Adviser's email address.
        team-school-new: Name of the team's school.
        team-password-new: Password to join team.

    """

    validate(user_schema, params)

    if params.get("create-new-teacher", "false") == "true":
        validate(teacher_schema, params)
        tid = api.team.create_team({"eligible": False})

        return create_user(
            params["username"],
            params["email"],
            hash_password(params["password"]),
            tid,
            teacher=True<commit_msg>Some team invariants were invalidated causing the logger to throw errors. We need to be careful to avoid this.<commit_after>"""
API functions relating to user management and registration.
"""

import bcrypt
import re

import api

from api.common import check, validate, safe_fail
from api.common import WebException, InternalException
from api.annotations import log_action
from voluptuous import Required, Length, Schema

_check_email_format = lambda email: re.match(r"[A-Za-z0-9\._%+-]+@[A-Za-z0-9\.-]+\.[A-Za-z]{2,4}", email) is not None

user_schema = Schema({
    Required('email'): check(
        ("Email must be between 5 and 50 characters.", [str, Length(min=5, max=50)]),
        ("Your email does not look like an email address.", [_check_email_format])
    ),
    Required('username'): check(
        ("Usernames must be between 3 and 20 characters.", [str, Length(min=3, max=20)]),
        ("This username already exists.", [
            lambda name: safe_fail(get_user, name=name) is None])
    ),
    Required('password'):
        check(("Passwords must be between 3 and 20 characters.", [str, Length(min=3, max=20)]))
}, extra=True)

new_team_schema = Schema({
    Required('team-name-new'): check(
        ("The team name must be between 3 and 40 characters.", [str, Length(min=3, max=40)]),
        ("A team with that name already exists.", [
            lambda name: safe_fail(api.team.get_team, name=name) is None])
    ),
    Required('team-password-new'):
        check(("Team passwords must be between 3 and 20 characters.", [str, Length(min=3, max=20)])),
    Required('team-adv-name-new'):
        check(("Adviser names should be between 3 and 50 characters.", [str, Length(min=3, max=50)])),
    Required('team-adv-email-new'): check(
        ("Adviser emails must be between 5 and 50 characters.", [str, Length(min=5, max=50)]),
        ("Your adviser email does not look like an email address.", [_check_email_format])
    ),
    Required('team-school-new'):
        check(("School names must be between 3 and 100 characters.", [str, Length(min=3, max=100)]))

}, extra=True)

existing_team_schema = Schema({
    Required('team-name-existing'): check(
        ("Existing team names must be between 3 and 50 characters.", [str, Length(min=3, max=50)]),
        ("There is no existing team named that.", [
            lambda name: api.team.get_team(name=name) != None]),
        ("There are too many members on that team for you to join.", [
            lambda name: len(api.team.get_team_uids(name=name)) < api.team.max_team_users
        ])
    ),
    Required('team-password-existing'):
        check(("Team passwords must be between 3 and 50 characters.", [str, Length(min=3, max=50)]))
}, extra=True)

teacher_schema = Schema({
    Required('teacher-school'):
        check(("School names must be between 3 and 100 characters.", [str, Length(min=3, max=100)]))
}, extra=True)

def hash_password(password):
    """
    Hash plaintext password.

    Args:
        password: <PASSWORD>
    Returns:
        Secure hash of password.
    """

    return bcrypt.hashpw(password, bcrypt.gensalt(8))

def get_team(uid=None):
    """
    Retrieve the the corresponding team to the user's uid.

    Args:
        uid: user's userid
    Returns:
        The user's team.
    """

    user = get_user(uid=uid)
    return api.team.get_team(tid=user["tid"])

def get_user(name=None, uid=None):
    """
    Retrieve a user based on a property. If the user is logged in,
    it will return that user object.

    Args:
        name: the user's username
        uid: the user's uid
    Returns:
        Returns the corresponding user object or None if it could not be found
    """

    db = api.common.get_conn()

    match = {}

    if uid is not None:
        match.update({'uid': uid})
    elif name is not None:
        match.update({'username': name})
    elif api.auth.is_logged_in():
        match.update({'uid': api.auth.get_uid()})
    else:
        raise InternalException("Uid or name must be specified for get_user")

    user = db.users.find_one(match)

    if user is None:
        raise InternalException("User does not exist")

    return user

def create_user(username, email, password_hash, tid, teacher=False):
    """
    This inserts a user directly into the database. It assumes all data is valid.

    Args:
        username: user's username
        email: user's email
        password_hash: <PASSWORD> the <PASSWORD>
        tid: the team id to join
        teacher: whether this account is a teacher
    Returns:
        Returns the uid of the newly created user
    """

    db = api.common.get_conn()
    uid = api.common.token()

    if safe_fail(get_user, name=username) is not None:
        raise InternalException("User already exists!")

    user = {
        'uid': uid,
        'username': username,
        'email': email,
        'password_hash': password_hash,
        'tid': tid,
        'teacher': teacher,
        'avatar': 3,
        'eventid': 0,
        'level': 'Not Started'
    }

    db.users.insert(user)

    return uid

def get_all_users(show_teachers=False):
    """
    Returns:
        Returns the uid, username, and email of all users.
    """

    db = api.common.get_conn()

    match = {}
    projection = {"uid": 1, "username": 1, "email": 1, "tid": 1}

    if not show_teachers:
        match.update({"teacher": False})
        projection.update({"teacher": 1})

    return list(db.users.find(match, projection))

@log_action
def create_user_request(params):
    """
    Registers a new user and creates/joins a team. Validates all fields.
    Assume arguments to be specified in a dict.

    Args:
        username: user's username
        password: <PASSWORD>
        email: user's email
        create-new-teacher:
            boolean "true" indicating whether or not the user is a teacher
        create-new-team:
            boolean "true" indicating whether or not the user is creating a new team or
            joining an already existing team.

        team-name-existing: Name of existing team to join.
        team-password-existing: Password of existing team to join.

        team-name-new: Name of new team.
        team-adv-name-new: Name of adviser.
        team-adv-email-new: Adviser's email address.
        team-school-new: Name of the team's school.
        team-password-new: Password to join team.

    """

    validate(user_schema, params)

    #Why are these strings? :o
    if params.get("create-new-teacher", "false") == "true":
        validate(teacher_schema, params)

        tid = api.team.create_team({
            "eligible": False,
            "school": params["teacher-school"],
            "team_name": api.common.token()
        })

        return create_user(
            params["username"],
            params["email"],
            hash_password(params["password"]),
            tid,
            teacher=True
{'fraction_non_alphanumeric': 0.12354843451418492, 'fraction_numerical': 0.012347493752756136, 'mean_word_length': 2.6646916240237006, 'pattern_counts': {'":': 18, '<': 12, '<?xml version=': 0, '>': 10, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '713337', 'n_tokens_mistral': 4159, 'n_tokens_neox': 3948, 'n_words': 1320}