Example #1
0
class Stats(Resource):
    def __init__(self):
        super(Resource, self).__init__()
        self.campaign_table = DynamoTable('campaigns')
        self.donation_table = DynamoTable('donations')

    @swagger.operation(notes='Get global stats for the site',
                       nickname='Get Site Stats',
                       parameters=[])
    def get(self):
        """Get Site Stats

        Response:
            campaign_count - total number of campaigns
            campaign_active_count - total number of active campaigns
            campaign_matched_count - total number of matched campaigns
            campaign_cancelled_count - total number of cancelled campaigns
            campaign_total_cents - total amount of pledged cents (sum of matched and active campaigns only)
            donation_count - total number of donations
            total_donation_cents - total amount of donations in cents

        """
        def campaign_count_func(items, result):
            """Method to count campaigns"""
            result["campaign_count"] += len(items)

            for item in items:
                if item["campaign_status"]["S"] == "active":
                    result["campaign_active_count"] += 1
                    result["campaign_total_cents"] += int(
                        item["match_cents"]["N"])
                elif item["campaign_status"]["S"] == "matched":
                    result["campaign_matched_count"] += 1
                    result["campaign_total_cents"] += int(
                        item["match_cents"]["N"])
                else:
                    result["campaign_cancelled_count"] += 1

        def donation_func(items, result):
            """Method to count campaigns"""
            for item in items:
                result["donation_count"] += 1
                result["total_donation_cents"] += int(
                    item["donation_cents"]["N"])

        result = {
            "campaign_count": 0,
            "campaign_active_count": 0,
            "campaign_matched_count": 0,
            "campaign_cancelled_count": 0,
            "campaign_total_cents": 0,
            "donation_count": 0,
            "total_donation_cents": 0
        }
        self.campaign_table.scan_table(
            campaign_count_func, result,
            "campaign_id, campaign_status, match_cents")
        self.donation_table.scan_table(donation_func, result, "donation_cents")

        return result, 200
Example #2
0
    def test_delete(self):
        """Method to test deleting a charity"""
        data = {
            "charity_id": "aclu",
            "campaigner_name": "John Doeski",
            "campaigner_email": "*****@*****.**",
            "match_cents": 5000
        }
        rv = self.app.post('/campaign', data=data, follow_redirects=True)

        self.assertEqual(rv.status_code, 201)
        response = json.loads(rv.data)
        assert "campaign_id" in response

        # Get charity object
        campaign_table = DynamoTable('campaigns')
        key = {"campaign_id": response["campaign_id"]}
        item = campaign_table.get_item(key)
        assert item is not None
        self.assertEqual(item["campaign_status"], "active")

        # Delete
        rv = self.app.post('/campaign/{}/cancel/{}'.format(
            response["campaign_id"], item["secret_id"]),
                           follow_redirects=True)
        self.assertEqual(rv.status_code, 204)

        # Verify it deleted
        item = campaign_table.get_item(key)
        self.assertEqual(item["campaign_status"], "cancelled")
Example #3
0
    def test_list_unrelated_receipts(self):
        """Method to test receipt index"""
        donation_table = DynamoTable('donations')

        # Add a record
        data = {
            "campaign_id": "my_campaign",
            "donation_on": arrow.utcnow().isoformat(),
            "donator_name": "John Doeski",
            "donator_email": "*****@*****.**",
            "donation_cents": 1000,
            "receipt_id": "alsdf3234",
            "email_bucket": "my_bucket",
            "email_key": "key1"
        }
        donation_table.put_item(data)

        data["receipt_id"] = "adsfghdfg"
        donation_table.put_item(data)

        data["receipt_id"] = "hjydfgsdfg"
        donation_table.put_item(data)

        existing_receipts = donation_table.query_hash("receipt_id",
                                                      "jfghghjf",
                                                      index="ReceiptIndex",
                                                      limit=10)

        assert not existing_receipts
    def test_complex_campaign(self):
        """Method to test a more complex campaign with donors"""
        data = {
            "charity_id": "aclu",
            "campaigner_name": "John Doeski2",
            "campaigner_email": "*****@*****.**",
            "match_cents": 2500000
        }
        rv = self.app.post('/campaign', data=data, follow_redirects=True)

        self.assertEqual(rv.status_code, 201)
        response = json.loads(rv.data)
        campaign_id = response["campaign_id"]

        # Add some donors
        dontation_table = DynamoTable('donations')

        donations = []
        for x in range(0, 7):
            donation = {
                "campaign_id": campaign_id,
                "donation_on": arrow.utcnow().isoformat(),
                "donor_name": "Friend {}".format(x),
                "donor_email": "friend{}@gmail.com".format(x),
                "donation_cents": 400000 - (25000 * x),
                "email_object": "https://s3.aws.com/somghasd"
            }
            dontation_table.put_item(donation)
            donations.append(donation)
            time.sleep(1)

        # Get item
        rv = self.app.get('/campaign/{}'.format(campaign_id),
                          follow_redirects=True)
        self.assertEqual(rv.status_code, 200)
        response = json.loads(rv.data)
        self.assertEqual(response["charity_id"], data["charity_id"])
        self.assertEqual(response["campaigner_name"], data["campaigner_name"])
        self.assertEqual(response["campaigner_email"],
                         data["campaigner_email"])
        self.assertEqual(response["match_cents"], data["match_cents"])
        self.assertEqual(response["notified_on"], response["notified_on"])

        for donor, x in zip(response["large_donors"], range(0, 5)):
            self.assertEqual(donor["donor_name"], "Friend {}".format(x))

        for donor, x in zip(response["recent_donors"], range(6, 1, -1)):
            self.assertEqual(donor["donor_name"], "Friend {}".format(x))

        self.assertEqual(response["donation_total_cents"], 2275000)
Example #5
0
    def test_dup_receipts_different_campaigns(self):
        """Method to test receipt index"""
        donation_table = DynamoTable('donations')
        receipt_id = "1234asdf"

        existing_receipts = donation_table.query_hash("receipt_id",
                                                      receipt_id,
                                                      index="ReceiptIndex",
                                                      limit=10)
        assert not existing_receipts

        # Add a record
        data = {
            "campaign_id": "my_campaign",
            "donation_on": arrow.utcnow().isoformat(),
            "donator_name": "John Doeski",
            "donator_email": "*****@*****.**",
            "donation_cents": 1000,
            "receipt_id": receipt_id,
            "email_bucket": "my_bucket",
            "email_key": "key1"
        }
        donation_table.put_item(data)

        data["campaign_id"] = "my_campaign2"
        donation_table.put_item(data)

        existing_receipts = donation_table.query_hash("receipt_id",
                                                      receipt_id,
                                                      index="ReceiptIndex",
                                                      limit=10)

        self.assertEqual(len(existing_receipts), 2)
Example #6
0
class CampaignCancel(Resource):
    def __init__(self):
        super(Resource, self).__init__()
        self.campaign_table = DynamoTable('campaigns')

    @swagger.operation(notes='Cancel a campaign',
                       nickname='Cancel A Campaign',
                       parameters=[{
                           "name": "campaign_id",
                           "description": "UUID of the campaign",
                           "required": True,
                           "allowMultiple": False,
                           "dataType": 'string',
                           "paramType": "path"
                       }, {
                           "name": "secret_key",
                           "description":
                           "Secret key for the campaign to validate request",
                           "required": True,
                           "allowMultiple": False,
                           "dataType": 'string',
                           "paramType": "path"
                       }])
    def post(self, campaign_id, secret_key):
        """Cancel A Campaign"""
        # Get the campaign
        data = {"campaign_id": campaign_id}
        item = None
        try:
            item = self.campaign_table.get_item(data)
        except IOError as e:
            abort(400, description=e.message)

        if not item:
            abort(404,
                  description="Campaign '{}' not found".format(campaign_id))

        if item["secret_id"] == secret_key:
            # Update status to "cancelled"
            key = {"campaign_id": campaign_id}
            self.campaign_table.update_attribute(key, "campaign_status",
                                                 "cancelled")

        else:
            abort(403, description="Invalid Authorization Key")

        return None, 204
Example #7
0
    def populate(self):
        """Populate the DBs with data to init things.

        Args:

        Returns:
            None
        """
        # Populate campaigns table
        print("Adding data to campaigns table")
        table = DynamoTable('campaigns')
        populate_table(table, "campaigns.json", " - Campaign Added")

        # Populate donations table
        print("Adding data to donations table")
        table = DynamoTable('donations')
        populate_table(table, "donations.json", " - Donation Added")
    def test_large_campaign(self):
        """Method to test a more complex campaign with donors"""
        data = {
            "charity_id": "aclu",
            "campaigner_name": "John Doeski2",
            "campaigner_email": "*****@*****.**",
            "match_cents": 2500000
        }
        rv = self.app.post('/campaign', data=data, follow_redirects=True)

        self.assertEqual(rv.status_code, 201)
        response = json.loads(rv.data)
        campaign_id = response["campaign_id"]

        # Add some donors
        dontation_table = DynamoTable('donations')

        with dontation_table.table.batch_writer() as batch:
            for x in range(0, 5000):
                donation = {
                    "campaign_id": campaign_id,
                    "donation_on": arrow.utcnow().isoformat(),
                    "donor_name": "Friend {}".format(x),
                    "donor_email": "friend{}@gmail.com".format(x),
                    "donation_cents": 1000,
                    "email_object": "https://s3.aws.com/somghasd"
                }
                batch.put_item(Item=donation)

        # Get item
        start_time = time.time()
        rv = self.app.get('/campaign/{}'.format(campaign_id),
                          follow_redirects=True)
        self.assertEqual(rv.status_code, 200)
        response = json.loads(rv.data)
        self.assertEqual(response["charity_id"], data["charity_id"])
        self.assertEqual(response["campaigner_name"], data["campaigner_name"])
        self.assertEqual(response["campaigner_email"],
                         data["campaigner_email"])
        self.assertEqual(response["match_cents"], data["match_cents"])
        self.assertEqual(response["notified_on"], response["notified_on"])

        self.assertEqual(response["donation_total_cents"], 5000000)
        print("Query took {:0.5f}s".format((time.time() - start_time)))
Example #9
0
    def test_delete_item(self):
        """Method to test receipt index"""
        campaign_table = DynamoTable('campaigns')

        # Add a record
        data = {
            "campaign_id": "my_campaign",
            "notified_on": arrow.utcnow().isoformat(),
            "campaign_status": "active"
        }
        campaign_table.put_item(data)

        # Verify write
        key = {"campaign_id": "my_campaign"}
        item = campaign_table.get_item(key)
        assert item is not None
        self.assertEqual(item["campaign_status"], "active")

        # Delete
        campaign_table.delete_item(key)

        # Verify it deleted
        item = campaign_table.get_item(key)
        assert item is None
Example #10
0
    def test_scan_table(self):
        """Method to test scanning a table"""
        def scan_func(items, input_val):
            for item in items:
                if item["campaign_status"]["S"] == "complete":
                    input_val['count'] += 1
            return input_val

        campaign_table = DynamoTable('campaigns')

        # Add a record
        for idx in range(0, 10):
            data = {
                "campaign_id": "my_campaign_{}".format(idx),
                "notified_on": arrow.utcnow().isoformat(),
                "campaign_status": "complete"
            }
            campaign_table.put_item(data)

        # Scan table
        result = {"count": 0}
        campaign_table.scan_table(scan_func, result, "campaign_status")

        self.assertEqual(result["count"], 10)
Example #11
0
    def test_stats(self):
        """Method to test stats endpoint"""
        rv = self.app.get('/stats', follow_redirects=True)
        self.assertEqual(rv.status_code, 200)
        response = json.loads(rv.data)
        self.assertEqual(response["campaign_count"], 0)
        self.assertEqual(response["donation_count"], 0)
        self.assertEqual(response["total_donation_cents"], 0)

        data = {
            "charity_id": "aclu",
            "campaigner_name": "John Doeski",
            "campaigner_email": "*****@*****.**",
            "match_cents": 5000
        }
        for idx in range(0, 15):
            rv = self.app.post('/campaign', data=data, follow_redirects=True)
            self.assertEqual(rv.status_code, 201)
        response = json.loads(rv.data)

        # Add some cancelled and matched campaignes
        campaign_table = DynamoTable('campaigns')

        for x in range(0, 4):
            camp = {
                "campaign_id": shortuuid.uuid(),
                "notified_on": arrow.utcnow().isoformat(),
                "campaign_status": "cancelled",
                "match_cents": 20000
            }
            campaign_table.put_item(camp)

        for x in range(0, 2):
            camp = {
                "campaign_id": shortuuid.uuid(),
                "notified_on": arrow.utcnow().isoformat(),
                "campaign_status": "matched",
                "match_cents": 20000
            }
            campaign_table.put_item(camp)

        # Add some donations
        dontation_table = DynamoTable('donations')
        donations = []
        for x in range(0, 7):
            donation = {
                "campaign_id": response["campaign_id"],
                "donation_on": arrow.utcnow().isoformat(),
                "donor_name": "Friend {}".format(x),
                "donor_email": "friend{}@gmail.com".format(x),
                "donation_cents": 20000
            }
            dontation_table.put_item(donation)
            donations.append(donation)

        rv = self.app.get('/stats', follow_redirects=True)
        self.assertEqual(rv.status_code, 200)
        response = json.loads(rv.data)

        self.assertEqual(response["campaign_count"], 21)
        self.assertEqual(response["campaign_active_count"], 15)
        self.assertEqual(response["campaign_matched_count"], 2)
        self.assertEqual(response["campaign_cancelled_count"], 4)
        self.assertEqual(response["campaign_total_cents"], 115000)
        self.assertEqual(response["donation_count"], 7)
        self.assertEqual(response["total_donation_cents"], 140000)
Example #12
0
 def __init__(self):
     super(Resource, self).__init__()
     self.table = DynamoTable('campaigns')
Example #13
0
class Campaign(Resource):
    def __init__(self):
        super(Resource, self).__init__()
        self.table = DynamoTable('campaigns')

    @swagger.operation(notes='Service to create a new campaign',
                       nickname='Create a Campaign',
                       parameters=[{
                           "name":
                           "charity_id",
                           "description":
                           "Identifier of charity. Supported: {}".format(
                               ", ".join(
                                   [c["id"] for c in SUPPORTED_CHARITIES])),
                           "required":
                           True,
                           "allowMultiple":
                           False,
                           "dataType":
                           'string',
                           "paramType":
                           "query"
                       }, {
                           "name": "campaigner_name",
                           "description": "Campaigner's name",
                           "required": True,
                           "allowMultiple": False,
                           "dataType": 'string',
                           "paramType": "query"
                       }, {
                           "name": "campaigner_email",
                           "description": "Campaigner's email",
                           "required": True,
                           "allowMultiple": False,
                           "dataType": 'string',
                           "paramType": "query"
                       }, {
                           "name": "match_cents",
                           "description": "Target amount to match in cents",
                           "required": True,
                           "allowMultiple": False,
                           "dataType": 'integer',
                           "paramType": "query"
                       }])
    def post(self):
        """Create a Campaign"""
        args = story_post_parser.parse_args()

        # Verify charity is supported
        if args["charity_id"] not in [c["id"] for c in SUPPORTED_CHARITIES]:
            abort(400,
                  description="Unsupported charity: {}".format(
                      args["charity_id"]))

        # Create campaign id
        while True:
            u = uuid.uuid4()
            s = shortuuid.encode(u)[:5]
            if not self.table.get_item({"campaign_id": s}):
                break
        printable = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        name_str = filter(lambda x: x in printable,
                          args["campaigner_name"].strip().lower())
        name_str = name_str.replace(" ", "-")

        args["campaign_id"] = "{}-{}".format(name_str, s)
        args["campaigner_name"] = args["campaigner_name"].strip()
        args["campaigner_email"] = args["campaigner_email"].strip()
        args["campaign_status"] = "active"
        args['notified_on'] = arrow.utcnow().isoformat()
        args['created_on'] = arrow.utcnow().isoformat()
        args['secret_id'] = shortuuid.uuid()
        args['charity_id'] = args["charity_id"]

        # Put the object
        self.table.put_item(args)

        # Notify the matcher
        dm_email = DonatematesEmail(
            campaigner_address=args["campaigner_email"])
        dm_email.send_campaign_created(args["campaign_id"], args["secret_id"])

        # Return
        return {"campaign_id": args["campaign_id"]}, 201
Example #14
0
 def __init__(self):
     super(Resource, self).__init__()
     self.campaign_table = DynamoTable('campaigns')
     self.donation_table = DynamoTable('donations')
Example #15
0
class CampaignProperties(Resource):
    def __init__(self):
        super(Resource, self).__init__()
        self.campaign_table = DynamoTable('campaigns')
        self.donation_table = DynamoTable('donations')

    @swagger.operation(notes='Get the properties of a campaign by ID',
                       nickname='Get Campaign Details',
                       parameters=[{
                           "name": "campaign_id",
                           "description": "UUID of the campaign",
                           "required": True,
                           "allowMultiple": False,
                           "dataType": 'string',
                           "paramType": "path"
                       }])
    def get(self, campaign_id):
        """Get Campaign Details"""
        # Get the campaign
        data = {"campaign_id": campaign_id}
        item = None
        try:
            item = self.campaign_table.get_item(data)
        except IOError as e:
            abort(400, description=e.message)

        if not item:
            abort(404,
                  description="Campaign '{}' not found".format(campaign_id))

        item["donation_email"] = "{}@donatemates.com".format(
            item["campaign_id"])

        # Get donor amount stats
        amounts = self.donation_table.query_biggest("campaign_id",
                                                    campaign_id,
                                                    5,
                                                    index="DonationIndex")

        # Get donor time stats
        donors = self.donation_table.query_most_recent(
            "campaign_id",
            campaign_id,
            "donation_on",
            arrow.utcnow().isoformat(),
            limit=5)

        item["large_donors"] = [{
            "donor_name": x["donor_name"],
            "donation_cents": float(x["donation_cents"])
        } for x in amounts]
        item["recent_donors"] = [{
            "donor_name": x["donor_name"],
            "donation_cents": float(x["donation_cents"])
        } for x in donors]

        # Sum donors
        item[
            "donation_total_cents"] = self.donation_table.integer_sum_attribute(
                "campaign_id", campaign_id, "donation_cents")

        # Get charity information
        charity = next(c for (i, c) in enumerate(SUPPORTED_CHARITIES)
                       if c["id"] == item["charity_id"])

        item["donation_url"] = charity["donation_url"]
        item["charity_name"] = charity["conversational_name"]
        del item["secret_id"]

        item = clean_dynamo_response(item)

        return item, 200

    def put(self, campaign_id):
        abort(403, description="Missing Authorization Key")

    def post(self, campaign_id):
        abort(403, description="Missing Authorization Key")
Example #16
0
def process_email_handler(event, context):
    logger = logging.getLogger("boto3")
    logger.setLevel(logging.WARN)

    print("Received event: " + json.dumps(event, indent=2))

    # Check if S3 event or CloudWatch invocation. If just keeping things hot, exit.
    if "Records" in event:
        if "s3" in event["Records"][0]:
            key = event["Records"][0]["s3"]["object"]["key"]
            bucket = event["Records"][0]["s3"]["bucket"]["name"]
    else:
        print("Not an email. Move along...")
        return

    # Load message from S3
    s3 = boto3.resource('s3')
    email_obj = s3.Object(bucket, key)
    email_mime = email_obj.get()['Body'].read().decode('utf-8')

    # Detect Charity
    for sup_charity in SUPPORTED_CHARITIES:
        class_ = getattr(charity, sup_charity["class"])
        charity_class = class_(email_mime)

        # Detect charity
        if charity_class.is_receipt():
            # Found the charity

            # Get campaign ID and campaign data
            campaign_id = charity_class.get_campaign_id()
            print("CAMPAIGN ID: {}".format(campaign_id))
            campaign_table = DynamoTable('campaigns')
            campaign_key = {"campaign_id": campaign_id}
            campaign = campaign_table.get_item(campaign_key)

            if not campaign:
                print("WARNING: **** CAMPAIGN DOES NOT EXIST ****")
                dm_email = DonatematesEmail(charity_class.from_email)
                dm_email.send_campaign_does_not_exist()
                return True

            # Setup email sender
            dm_email = DonatematesEmail(charity_class.from_email,
                                        campaign["campaigner_email"])

            # Get donation receipt
            data = charity_class.parse_email()
            data["receipt_id"] = data["receipt_id"].strip()

            # Validate this is a new donation
            donation_table = DynamoTable('donations')
            existing_receipts = donation_table.query_hash("receipt_id",
                                                          data["receipt_id"],
                                                          index="ReceiptIndex",
                                                          limit=10)
            if existing_receipts:
                # This receipt already exists!
                print(
                    "WARNING: **** Duplicate receipt detected - Campaign: {} - Receipt: {} - Bucket: {} - Key: {} ****"
                    .format(campaign_id, data["receipt_id"], bucket, key))
                # Notify user we didn't process it
                dm_email.send_duplicate_receipt(campaign_id,
                                                data["receipt_id"], key)
                return True

            # Add donation record
            data["campaign_id"] = campaign_id
            data["donation_on"] = arrow.utcnow().isoformat()
            data["email_bucket"] = bucket
            data["email_key"] = key
            print("DONATION DATA:")
            print(data)
            store_donation(data)

            # Get updated total donation
            donation_total_cents = donation_table.integer_sum_attribute(
                "campaign_id", campaign_id, "donation_cents")

            # Notify the Donor
            if campaign["campaign_status"] == "cancelled":
                # If cancelled, only notify donor and let them know the campaign isn't going on.
                dm_email.send_campaign_cancelled()
            else:
                # Send standard confirmation to donor
                dm_email.send_donation_confirmation(data["donation_cents"])

            # Notify the campaigner if the campaign is active only
            if campaign["campaign_status"] == "active":
                # Update notification time (for future possible digest emails)
                campaign_table.update_attribute(campaign_key, "notified_on",
                                                arrow.utcnow().isoformat())

                if donation_total_cents >= campaign["match_cents"]:
                    # Update campaign status to "matched"
                    campaign_table.update_attribute(campaign_key,
                                                    "campaign_status",
                                                    "matched")

                    # Send campaign completion email!
                    dm_email.send_campaign_matched(data["donor_name"],
                                                   data["donation_cents"],
                                                   donation_total_cents,
                                                   campaign["match_cents"])
                else:
                    # Send normal update
                    dm_email.send_campaign_update(data["donor_name"],
                                                  data["donation_cents"],
                                                  donation_total_cents,
                                                  campaign["match_cents"])

            # Exit
            return True

    # If you get here, you didn't successfully parse the email or it was unsupported
    # Save email to error bucket
    s3.Object('parse-fail-donatemates', '{}'.format(
        shortuuid.uuid())).copy_from(CopySource='{}/{}'.format(bucket, key))

    # Reply to user
    print(
        "WARNING: **** Failed to detect a supported charity - Email Key: {} ****"
        .format(key))