def check_if_user(donor_dict):
    """See if the donor exists.

    :param donor_dict = {
        "id": None,
        "user_first_name": user_first_name,
        "user_last_name": user_last_name,
        "user_zipcode": user_zipcode,
        "user_address": user_address,
        "user_email_address": user_email_address,
        "user_phone_number": user_phone_number
    }
    :return: 2 for caged and 0 for not caged.
    """

    # A user ID is said to exist and so if one isn't returned there is a problem.
    query_parameters = {
        'search_terms': {
            'ID': {
                'eq': donor_dict['id']
            }
        },
        'sort_terms': []
    }
    user_by_id = find_ultsys_user(query_parameters)
    if user_by_id:
        # We are returning a category here: ( category_weight, [ user_id ] )
        return 2, [user_by_id[0]['ID']]
    raise UltsysUserNotFoundError
Exemple #2
0
def ultsys_user_update( payload ):
    """Function to update an Ultsys user through the Ultsys user service.
    :param dict payload: Ultsys user ID and caged donor ID.
    :return: Ultsys user.
    """

    # Validate Ultsys user.
    ultsys_user_id = payload[ 'ultsys_user_id' ]
    ultsys_user = find_ultsys_user( get_ultsys_user_query( { 'ultsys_user_id': ultsys_user_id } ) )
    if not ultsys_user:
        raise UltsysUserNotFoundError

    # Find the gift ID from the caged donor.
    caged_donor_model = CagedDonorModel.query.filter_by( id=payload[ 'caged_donor_id' ] ).one_or_none()
    if not caged_donor_model:
        raise ModelCagedDonorNotFoundError

    # From the gift ID retrieve the gift and then update its Ultsys user ID.
    gift = GiftModel.query.filter_by( id=caged_donor_model.gift_id ).one_or_none()
    if not gift:
        raise ModelGiftNotFoundError
    gift.user_id = ultsys_user_id

    # The gift will have at least one transaction and may also have multiple transactions.
    # Get the most recent transaction which holds the current gross gift amount.
    transaction = TransactionModel.query.filter_by( gift_id=caged_donor_model.gift_id )\
        .order_by( TransactionModel.date_in_utc )\
        .first()
    if not transaction:
        raise ModelTransactionNotFoundError
    gross_gift_amount = transaction.gross_gift_amount

    # Update the Ultsys user with the new donation and delete the caged donor.
    update_ultsys_user( { 'id': ultsys_user_id }, gross_gift_amount )
    database.session.delete( caged_donor_model )
    database.session.commit()
    updated_ultsys_user = find_ultsys_user( get_ultsys_user_query( { 'ultsys_user_id': ultsys_user_id } ) )

    return updated_ultsys_user[ 0 ]
def build_model_new(user, gross_gift_amount):
    """Given the new user save to model and return their ID.

    :param dict user: User dictionary with necessary model fields, and may have additional fields.
    :param gross_gift_amount: The gross gift amount.
    :return: The user ID
    """

    # Map the front-end keys to the user model keys.
    ultsys_user_json = {
        'firstname': user['user_address']['user_first_name'],
        'lastname': user['user_address']['user_last_name'],
        'zip': user['user_address']['user_zipcode'],
        'address': user['user_address']['user_address'],
        'city': user['user_address']['user_city'],
        'state': user['user_address']['user_state'],
        'email': user['user_address']['user_email_address'],
        'phone': user['user_address']['user_phone_number'],
        'donation_amount': gross_gift_amount
    }

    # If new user, there is no ID, create the DB entry and get it.
    drupal_uid = create_user(ultsys_user_json)

    query_parameters = {
        "action": "find",
        "search_terms": {
            "uid": {
                "eq": drupal_uid
            }
        },
        "sort_terms": []
    }

    ultsys_user = find_ultsys_user(query_parameters)

    user['id'] = ultsys_user[0]['ID']

    return user['id']
Exemple #4
0
def build_out_user_data(searchable_ids):
    """Build a lookup table from searchable to gift ID, as well as construct a list of Gift IDs to delete.

    :param searchable_ids: The searchable IDs to process.
    :return: User data based upon the user ID on the gift.
    """

    searchable_ids_uuid = [
        UUID(searchable_id) for searchable_id in searchable_ids
    ]
    user_data = {}
    for searchable_id_uuid in searchable_ids_uuid:
        gift = GiftModel.query.filter_by(
            searchable_id=searchable_id_uuid).one_or_none()
        if gift:
            user_id = gift.user_id

            donor_model = {-1: CagedDonorModel, -2: QueuedDonorModel}
            if user_id in [-1, -2]:
                donor = donor_model[user_id].query.filter_by(
                    gift_id=gift.id).one_or_none()
                user = [{
                    'firstname': donor.user_first_name,
                    'lastname': donor.user_last_name,
                    'honorific': '',
                    'suffix': '',
                    'address': donor.user_address,
                    'city': donor.user_city,
                    'state': donor.user_state,
                    'zip': donor.user_zipcode,
                    'email': donor.user_email_address
                }]
            else:
                user_query_terms = {
                    'action': 'find',
                    'search_terms': {
                        'ID': {
                            'eq': user_id
                        }
                    },
                    'sort_terms': []
                }
                user = find_ultsys_user(user_query_terms)

            # Ensure the number of users returned is correct: 1.
            # Need to definitely catch no users found, and might as well protect against multiple users found.
            if not user:
                raise UltsysUserNotFoundError
            if len(user) >= 2:
                raise UltsysUserMultipleFoundError

            user_data[searchable_id_uuid.hex.upper()] = {
                'user_address': {
                    'user_short_first_name': user[0]['firstname'],
                    'user_honorific': user[0]['honorific'],
                    'user_first_name': user[0]['firstname'],
                    'user_last_name': user[0]['lastname'],
                    'user_suffix': user[0]['suffix'],
                    'user_address': user[0]['address'],
                    'user_city': user[0]['city'],
                    'user_state': user[0]['state'],
                    'user_zipcode': user[0]['zip'].zfill(5),
                    'user_email_address': user[0]['email']
                },
                'billing_address': {}
            }

    return user_data
Exemple #5
0
def ultsys_user_create( payload ):
    """Function to create an Ultsys user from a caged donor.

    payload = {
        "caged_donor_id": 1234,
        "ultsys_user_id": Null,
        "user_first_name": "Ralph",
        "user_last_name": "Kramden",
        "user_address": "328 Chauncey St",
        "user_state": "NY",
        "user_city": "Bensonhurst",
        "user_zipcode": "11214",
        "user_email_address": "*****@*****.**",
        "user_phone_number": "9172307441"
    }

    :param dict payload: The required payload
    :return: Ultsys user.
    """

    # Find the gift ID from the caged donor.
    caged_donor_model = CagedDonorModel.query.filter_by( id=payload[ 'caged_donor_id' ] ).one_or_none()
    if not caged_donor_model:
        raise ModelCagedDonorNotFoundError

    caged_donor_json = to_json( CagedDonorSchema(), caged_donor_model ).data
    caged_donor_json.pop( 'id' )

    # Retrieve the completed gift from the transactions to get the gross_gift_amount.
    gift = GiftModel.query.filter_by( id=caged_donor_model.gift_id ).one_or_none()
    if not gift:
        raise ModelGiftNotFoundError

    # The gift will have at least one transaction and may also have multiple transactions.
    # Get the most recent transaction which holds the current gross gift amount.
    transaction = TransactionModel.query.filter_by( gift_id=caged_donor_model.gift_id )\
        .order_by( TransactionModel.date_in_utc )\
        .first()
    if not transaction:
        raise ModelTransactionNotFoundError
    gross_gift_amount = transaction.gross_gift_amount

    # The updating of the caged donor fields does not expect the 2 IDs in the dictionary.

    # Build the payload for the Drupal user.
    caged_donor = {
        'action': 'create',
        'firstname': caged_donor_json[ 'user_first_name' ],
        'lastname': caged_donor_json[ 'user_last_name' ],
        'zip': caged_donor_json[ 'user_zipcode' ],
        'city': caged_donor_json[ 'user_city' ],
        'state': caged_donor_json[ 'user_state' ],
        'email': caged_donor_json[ 'user_email_address' ],
        'phone': str( caged_donor_json[ 'user_phone_number' ] )
    }

    drupal_user_uid = create_user( caged_donor )

    # Use the Drupal ID to retrieve the Ultsys user.
    ultsys_user = find_ultsys_user( get_ultsys_user_query( { 'drupal_user_uid': drupal_user_uid } ) )
    ultsys_user_id = ultsys_user[ 0 ][ 'ID' ]

    # Update the gift with the new Ultsys user ID and the Ultsys user with the gross gift amount.
    gift.user_id = ultsys_user_id
    update_ultsys_user( { 'id': ultsys_user_id }, gross_gift_amount )

    database.session.delete( caged_donor_model )
    database.session.commit()

    return ultsys_user[ 0 ]
def categorize_donor(donor_dict):
    """The main function that queries the database and matches each user against the donor to get a category.

    donor_dict = {
        "id": None,
        "user_first_name": user_first_name,
        "user_last_name": user_last_name,
        "user_zipcode": user_zipcode,
        "user_address": user_address,
        "user_email_address": user_email_address,
        "user_phone_number": user_phone_number
    }

    A query is made to the database for all users with the donor's last name. Then a loop is made over all the users
    returned and matches made against the fields used for caging:
        category = [ first_name, last_name, zipcode, street_address, email, phone_number ]
    A complete match would look like [ 1, 1, 1, 1, 1, 1, 1 ], and in this case this would indicate the donor exists.

    The first three items in the list [ first_name, last_name, zipcode ] are the base characteristics. The last
    three [ street_address, email, phone_number ] are the discriminators. Given the matches for a particular user
    the category matrix is passed to the function:

        category_weight( category_test_matrix )

    which is a simple, and yet a flexible/configurable function, for determining the category of a donor. For example,
    if the category matrix looks like [ 1, 1, 1, 0, 0, 0 ] the weighting function uses the base fields to determine
    what the discriminators should sum to to assign a category. In this case the sum is 0 and would suggest that, from
    extensive studies on caging across the Ultsys user database, that the donor should be caged. A full explanation
    with supporting data is given on the project Wiki.

    The matrix can be extended to include weighting to each field if needed. Currently, the weighting is strict and
    requires a match on all fields for the user to be categorized as existing. An alternative may be to match either
    on the email address or phone number and street.

    :param donor_dict: The donor dictionary from the front-end.
    :return: A category: new, cage, or exists.
    """

    category_definitions = {0: 'new', 1: 'cage', 2: 'exists', 3: 'caged'}

    # Check to see if the donor has a user ID.
    if 'id' in donor_dict and donor_dict['id']:
        # Function is_user returns category_weight = 2 if found ( exists ), along with user[ 1 ] = [ user_id ]
        # If  category_weight = 1 it might be because it found duplicate users: = [ user_id1, user_id2 ]
        is_user = check_if_user(donor_dict)
        return category_definitions[is_user[0]], is_user[1]

    # Check to see if the donor has a registered email and if so pull user ID.
    if 'user_email_address' in donor_dict and donor_dict['user_email_address']:
        query_parameters = {
            'search_terms': {
                'email': {
                    'eq': donor_dict['user_email_address']
                }
            },
            'sort_terms': []
        }
        users_with_given_email = find_ultsys_user(query_parameters)
        if users_with_given_email:
            ultsys_user = users_with_given_email[0]
            return category_definitions[2], [ultsys_user['ID']]

    # Check to see if the donor has already been caged.
    if check_if_caged(donor_dict) == 3:
        return category_definitions[check_if_caged(donor_dict)], []

    # If they don't already exist and are not previously caged: cage the donor.
    query_parameters = {
        'search_terms': {
            'lastname': {
                'eq': donor_dict['user_last_name']
            }
        },
        'sort_terms': []
    }
    users_by_last_name = find_ultsys_user(query_parameters)

    # If no last names exist this is a new donor.
    if not users_by_last_name:
        return category_definitions[0], []

    donor_street = munge_address(donor_dict['user_address'])

    user_ids = []
    exists_user_ids = []
    maximum_weight = 0
    for user in users_by_last_name:
        # The identifier in Drupal is uppercase.
        if user['ID'] not in user_ids:
            # Capture the user so that it isn't considered more than once.
            user_ids.append(user['ID'])

            # Initialize the category matrix to no match on any fields: [ 0, 0, 0, 0, 0, 0 ].
            category_match_matrix = [0] * 6

            # Set a match on last name since query matches here.
            category_match_matrix[1] = 1

            # Do some basic transformations to the address: set lowercase, remove whitespace and punctuation.
            user_street = munge_address(user['address'])

            # Find matches across match matrix.
            if donor_dict['user_first_name'].lower(
            ) == user['firstname'].lower():
                category_match_matrix[0] = 1
            if donor_dict[ 'user_zipcode' ] == user[ 'zip' ] and \
                    donor_dict[ 'user_zipcode' ] != 0:
                category_match_matrix[2] = 1
            if donor_street == user_street and donor_street != '':
                category_match_matrix[3] = 1
            if donor_dict[ 'user_email_address' ].lower() == user[ 'email' ].lower() and \
                    donor_dict[ 'user_email_address' ] != '':
                category_match_matrix[4] = 1
            if donor_dict[ 'user_phone_number' ] == user[ 'phone' ] and \
                    donor_dict[ 'user_phone_number' ] != '0':
                category_match_matrix[5] = 1

            # After matching the user then categorize them as new ( 0 ), cage ( 1 ) or exists ( 2 ).
            weight = category_weight(category_match_matrix)

            # Keep track of the maximum weight found.
            maximum_weight = track_maximum_weight(weight, maximum_weight,
                                                  exists_user_ids, user['ID'])

    return category_definitions[maximum_weight], exists_user_ids
def ultsys_user(payload):
    """Controller to handle request to the Ultsys user endpoint.

    There are 3 endpoints for the Ultsys user:

    create_user = {
        "action": "create",
        "id": null,
        "firstname: "Joe"
        "lastname: "Baker"
        "zip: "62918"
        "address: "1300 Crush Rd"
        "city: "BellaVerde"
        "state: "AK"
        "email: "*****@*****.**"
        "phone: "6189853333"
    }

    update_ultsys_user = {
        "action": "update",
        "user": {
            "id": "321234"
        },
        "gift": {
            "gross_gift_amount": "100.00"
        }
    }

    The update endpoint will attach the current date to the payload on its way out.

    An example find_ultsys_user query string: donate/user?lastname=contains:Baker&sort=firstname:asc

    Gets deserialized into a dictionary, which then gets transformed to something like:

    find_ultsys_user = {
        "action": "find",
        "search_terms": {
            "lastname": { "like": "%Baker%" },
        },
        "sort_terms": [
            { "firstname": "ascending" }
        ]
    }

    :param dict payload: The JSON to pass on to the relevant function.
    :return: The requested data.
    """

    if 'action' in payload and payload['action'] == 'create':
        # This is the POST for creation of user through Drupal.
        request = create_user(payload)
    elif 'action' in payload and payload['action'] == 'update':
        # This is the PUT for the update of an Ultsys user.
        request = update_ultsys_user(payload['user'], payload['gift'])
    else:
        # This is the GET a user from Ultsys with query string.

        # The payload is request.args and contains the query string within an ImmutableMultiDict.
        # The request.args is deserialized using filter_serialize_deserialize.py into query_dict.
        # The function filter_serialize_deserialize.py was built for NUSA applications and not Ultsys.
        # The document string in filter_serialize_deserialize.py has more detailed information.
        query_dict = build_filter_from_request_args(payload)
        query_parameters = transform_to_ultsys_query(query_dict)
        request = find_ultsys_user(query_parameters)

    return request