diff --git a/README.rst b/README.rst index 630b8bb..c5dcb99 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,8 @@ Getting Started To get started with Sharpy, simply install it like you would any other python package +.. code:: + pip install sharpy Optionally, you can also install `lxml `_ on your @@ -50,8 +52,19 @@ Code You can checkout and download Sharpy's latest code at `Github `_. +Installing elementtree for Development and Unit Testing +======================================================= +When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. + +.. code:: + + wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip + unzip elementtree-1.2.6-20050316.zip + cd elementtree-1.2.6-20050316/ + pip install . + TODOs ===== * Flesh out the documentation to cover the full API. -* Add support for the various filtering options in the `get_customers` call. \ No newline at end of file +* Add support for the various filtering options in the `get_customers` call. diff --git a/sharpy/__init__.py b/sharpy/__init__.py index 1bc90e9..c07c373 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 8) +VERSION = (0, 9) diff --git a/sharpy/parsers.py b/sharpy/parsers.py index 6d4b417..52c7aa4 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -328,4 +328,69 @@ def parse_subscription_item(self, item_element): item['modified_datetime'] = self.parse_datetime(item_element.findtext('modifiedDatetime')) return item - + + +class PromotionsParser(CheddarOutputParser): + ''' + A utility class for parsing cheddar's xml output for promotions. + ''' + def parse_xml(self, xml_str): + promotions = [] + promotions_xml = XML(xml_str) + for promotion_xml in promotions_xml: + promotion = self.parse_promotion(promotion_xml) + promotions.append(promotion) + + return promotions + + def parse_promotion(self, promotion_element): + promotion = {} + promotion['id'] = promotion_element.attrib['id'] + promotion['name'] = promotion_element.findtext('name') + promotion['description'] = promotion_element.findtext('description') + promotion['created_datetime'] = self.parse_datetime(promotion_element.findtext('createdDatetime')) + + promotion['incentives'] = self.parse_incentives(promotion_element.find('incentives')) + promotion['coupons'] = self.parse_coupons(promotion_element.find('coupons')) + + return promotion + + def parse_incentives(self, incentives_element): + incentives = [] + + if incentives_element is not None: + for incentive_element in incentives_element: + incentives.append(self.parse_incentive(incentive_element)) + + return incentives + + def parse_incentive(self, incentive_element): + incentive = {} + + incentive['id'] = incentive_element.attrib['id'] + incentive['type'] = incentive_element.findtext('type') + incentive['percentage'] = incentive_element.findtext('percentage') + incentive['months'] = incentive_element.findtext('months') + incentive['created_datetime'] = self.parse_datetime(incentive_element.findtext('createdDatetime')) + + return incentive + + def parse_coupons(self, coupons_element): + coupons = [] + + if coupons_element is not None: + for coupon_element in coupons_element: + coupons.append(self.parse_coupon(coupon_element)) + + return coupons + + def parse_coupon(self, coupon_element): + coupon = {} + + coupon['id'] = coupon_element.attrib['id'] + coupon['code'] = coupon_element.attrib['code'] + coupon['max_redemptions'] = coupon_element.findtext('maxRedemptions') + coupon['expiration_datetime'] = self.parse_datetime(coupon_element.findtext('expirationDatetime')) + coupon['created_datetime'] = self.parse_datetime(coupon_element.findtext('createdDatetime')) + + return coupon diff --git a/sharpy/product.py b/sharpy/product.py index cd6c0bc..817260d 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -7,7 +7,7 @@ from sharpy.client import Client from sharpy.exceptions import NotFound -from sharpy.parsers import PlansParser, CustomersParser +from sharpy.parsers import PlansParser, CustomersParser, PromotionsParser class CheddarProduct(object): @@ -263,8 +263,42 @@ def delete_all_customers(self): path='customers/delete-all/confirm/%d' % int(time()), method='POST' ) - - + + def get_all_promotions(self): + ''' + Returns all promotions. + https://cheddargetter.com/developers#promotions + ''' + promotions = [] + + try: + response = self.client.make_request(path='promotions/get') + except NotFound: + response = None + + if response: + promotions_parser = PromotionsParser() + promotions_data = promotions_parser.parse_xml(response.content) + promotions = [Promotion(**promotion_data) for promotion_data in promotions_data] + + return promotions + + def get_promotion(self, code): + ''' + Get the promotion with the specified coupon code. + https://cheddargetter.com/developers#single-promotion + ''' + + response = self.client.make_request( + path='promotions/get', + params={'code': code}, + ) + promotion_parser = PromotionsParser() + promotion_data = promotion_parser.parse_xml(response.content) + + return Promotion(**promotion_data[0]) + + class PricingPlan(object): def __init__(self, name, code, id, description, is_active, is_free, @@ -576,12 +610,12 @@ def __init__(self, id, gateway_token, cc_first_name, cc_last_name, super(Subscription, self).__init__() - def load_data(self, id, gateway_token, cc_first_name, cc_last_name, \ - cc_company, cc_country, cc_address, cc_city, cc_state, \ - cc_zip, cc_type, cc_last_four, cc_expiration_date, customer,\ - cc_email=None, canceled_datetime=None ,created_datetime=None, \ - plans=None, invoices=None, items=None, gateway_account=None, \ - cancel_reason=None, cancel_type=None, redirect_url=None): + def load_data(self, id, gateway_token, cc_first_name, cc_last_name, + cc_company, cc_country, cc_address, cc_city, cc_state, + cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, + cc_email=None, canceled_datetime=None ,created_datetime=None, + plans=None, invoices=None, items=None, gateway_account=None, + cancel_reason=None, cancel_type=None, redirect_url=None): self.id = id self.gateway_token = gateway_token @@ -764,5 +798,36 @@ def set(self, quantity): ) return self.subscription.customer.load_data_from_xml(response.content) - - + + +class Promotion(object): + def __init__(self, id=None, code=None, name=None, description=None, + created_datetime=None, incentives=None, coupons=None): + + self.load_data(code=code, id=id, name=name, description=description, + created_datetime=created_datetime, + incentives=incentives, coupons=coupons) + + super(Promotion, self).__init__() + + def __repr__(self): + return u'Promotion: %s (%s)' % (self.name, self.code,) + + def __unicode__(self): + return u'{0} ({1})'.format(self.name, self.code) + + def load_data(self, id=None, code=None, name=None, description=None, + created_datetime=None, incentives=None, coupons=None): + + self.code = code + self.id = id + self.name = name + self.description = description + self.created = created_datetime + + self.incentives = incentives + self.coupons = coupons + + # Bring coupon code up to parent promotion + if self.code is None and self.coupons and len(self.coupons) > 0: + self.code = self.coupons[0].get('code') diff --git a/tests/client_tests.py b/tests/client_tests.py index 571cc64..f3e612f 100644 --- a/tests/client_tests.py +++ b/tests/client_tests.py @@ -93,8 +93,9 @@ def test_make_request_access_denied(self): client = self.get_client(username=bad_username) client.make_request(path) - @raises(BadRequest) + @raises(NotFound) def test_make_request_bad_request(self): + """ Attempt to grab the plans without adding /get to the url. """ path = 'plans' client = self.get_client() client.make_request(path) @@ -104,7 +105,7 @@ def test_make_request_not_found(self): path = 'things-which-dont-exist' client = self.get_client() client.make_request(path) - + @clear_users def test_post_request(self): path = 'customers/new' @@ -118,7 +119,6 @@ def test_post_request(self): client = self.get_client() client.make_request(path, data=data) - def generate_error_response(self, auxcode=None, path=None, params=None, **overrides): ''' Creates a request to cheddar which should return an error @@ -244,7 +244,9 @@ def test_format_datetime_with_now(self): @clear_users def test_chedder_update_customer_error(self): - # Overriding the zipcode so a customer actually gets created + """ + Test overriding the zipcode so a customer actually gets updated. + """ overrides = { 'subscription[ccZip]': 12345 } diff --git a/tests/product_tests.py b/tests/product_tests.py index 16dcbc9..b834c8d 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -1,3 +1,6 @@ +import random +import string + from copy import copy from datetime import date, datetime, timedelta from decimal import Decimal @@ -116,6 +119,13 @@ def test_plan_initial_bill_date(self): def get_customer(self, **kwargs): customer_data = copy(self.customer_defaults) + # We need to make unique customers with the same data. + # Cheddar recomends we pass a garbage field. + # http://support.cheddargetter.com/discussions/problems/8342-duplicate-post + # http://support.cheddargetter.com/kb/api-8/error-handling#duplicate + random_string = ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.digits) for _ in range(5)) + customer_data.update({'notes': random_string}) customer_data.update(kwargs) product = self.get_product() @@ -137,59 +147,59 @@ def get_customer_with_items(self, **kwargs): customer = self.get_customer(**data) return customer - + @clear_users def test_simple_create_customer(self): self.get_customer() - + @clear_users def test_create_customer_with_company(self): self.get_customer(company='Test Co') - + @clear_users def test_create_customer_with_meta_data(self): self.get_customer(meta_data = {'key_1': 'value_1', 'key2': 'value_2'}) - + @clear_users def test_create_customer_with_true_vat_exempt(self): self.get_customer(is_vat_exempt=True) - + @clear_users def test_create_customer_with_false_vat_exempt(self): self.get_customer(is_vat_exempt=False) - + @clear_users def test_create_customer_with_vat_number(self): self.get_customer(vat_number=12345) - + @clear_users def test_create_customer_with_notes(self): self.get_customer(notes='This is a test note!') - + @clear_users def test_create_customer_with_first_contact_datetime(self): self.get_customer(first_contact_datetime=datetime.now()) - + @clear_users def test_create_customer_with_referer(self): self.get_customer(referer='http://saaspire.com/test.html') - + @clear_users def test_create_customer_with_campaign_term(self): self.get_customer(campaign_term='testing') - + @clear_users def test_create_customer_with_campaign_name(self): self.get_customer(campaign_name='testing') - + @clear_users def test_create_customer_with_campaign_source(self): self.get_customer(campaign_source='testing') - + @clear_users def test_create_customer_with_campaign_content(self): self.get_customer(campaign_content='testing') - + @clear_users def test_create_customer_with_initial_bill_date(self): initial_bill_date = datetime.utcnow() + timedelta(days=60) @@ -201,11 +211,11 @@ def test_create_customer_with_initial_bill_date(self): # if the request is made around UTC midnight diff = initial_bill_date.date() - real_bill_date.date() self.assertLessEqual(diff.days, 1) - + @clear_users def test_create_paid_customer(self): self.get_customer(**self.paid_defaults) - + @clear_users def test_create_paid_customer_with_charges(self): data = copy(self.paid_defaults) @@ -214,7 +224,7 @@ def test_create_paid_customer_with_charges(self): charges.append({'code': 'charge2', 'quantity': 3, 'each_amount': 4}) data['charges'] = charges self.get_customer(**data) - + @clear_users def test_create_paid_customer_with_decimal_charges(self): data = copy(self.paid_defaults) @@ -234,7 +244,6 @@ def test_create_paid_customer_with_items(self): data['plan_code'] = 'TRACKED_MONTHLY' self.get_customer(**data) - @clear_users def test_create_paid_customer_with_decimal_quantity_items(self): data = copy(self.paid_defaults) @@ -260,7 +269,6 @@ def test_update_paypal_customer(self): cancel_url='http://example.com/update-cancel/', ) - @clear_users def test_customer_repr(self): customer = self.get_customer() @@ -269,7 +277,7 @@ def test_customer_repr(self): result = repr(customer) self.assertEquals(expected, result) - + @clear_users def test_subscription_repr(self): customer = self.get_customer() @@ -279,7 +287,7 @@ def test_subscription_repr(self): result = repr(subscription) self.assertIn(expected, result) - + @clear_users def test_pricing_plan_repr(self): customer = self.get_customer() @@ -290,8 +298,7 @@ def test_pricing_plan_repr(self): result = repr(plan) self.assertEquals(expected, result) - - + @clear_users def test_item_repr(self): customer = self.get_customer_with_items() @@ -302,7 +309,7 @@ def test_item_repr(self): result = repr(item) self.assertEquals(expected, result) - + @clear_users def test_get_customers(self): customer1 = self.get_customer() @@ -317,9 +324,8 @@ def test_get_customers(self): product = self.get_product() fetched_customers = product.get_customers() - self.assertEquals(2, len(fetched_customers)) - + @clear_users def test_get_customer(self): created_customer = self.get_customer() @@ -331,7 +337,7 @@ def test_get_customer(self): self.assertEquals(created_customer.first_name, fetched_customer.first_name) self.assertEquals(created_customer.last_name, fetched_customer.last_name) self.assertEquals(created_customer.email, fetched_customer.email) - + @clear_users def test_simple_customer_update(self): new_name = 'Different' @@ -343,7 +349,7 @@ def test_simple_customer_update(self): fetched_customer = product.get_customer(code=customer.code) self.assertEquals(customer.first_name, fetched_customer.first_name) - + @clear_users @raises(NotFound) def test_delete_customer(self): @@ -355,8 +361,8 @@ def test_delete_customer(self): customer.delete() fetched_customer = product.get_customer(code=customer.code) - - + + @clear_users def test_delete_all_customers(self): customer_1 = self.get_customer() @@ -370,7 +376,7 @@ def test_delete_all_customers(self): fetched_customers = product.get_customers() self.assertEquals(0, len(fetched_customers)) - + @clear_users def test_cancel_subscription(self): customer = self.get_customer() @@ -398,19 +404,19 @@ def assert_increment(self, quantity=None): fetched_customer = product.get_customer(code=customer.code) fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + @clear_users def test_simple_increment(self): self.assert_increment() - + @clear_users def test_int_increment(self): self.assert_increment(1) - + @clear_users def test_float_increment(self): self.assert_increment(1.234) - + @clear_users def test_decimal_increment(self): self.assert_increment(Decimal('1.234')) @@ -430,19 +436,19 @@ def assert_decrement(self, quantity=None): fetched_customer = product.get_customer(code=customer.code) fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + @clear_users def test_simple_decrement(self): self.assert_decrement() - + @clear_users def test_int_decrement(self): self.assert_decrement(1) - + @clear_users def test_float_decrement(self): self.assert_decrement(1.234) - + @clear_users def test_decimal_decrement(self): self.assert_decrement(Decimal('1.234')) @@ -461,15 +467,15 @@ def assert_set(self, quantity): fetched_customer = product.get_customer(code=customer.code) fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + @clear_users def test_int_set(self): self.assert_set(1) - + @clear_users def test_float_set(self): self.assert_set(1.234) - + @clear_users def test_decimal_set(self): self.assert_set(Decimal('1.234')) @@ -509,27 +515,23 @@ def assert_charged(self, code, each_amount, quantity=None, self.assertAlmostEqual(Decimal(each_amount), fetched_charge['each_amount'], places=2) self.assertEqual(quantity, fetched_charge['quantity']) self.assertEqual(description, fetched_charge['description']) - + @clear_users def test_add_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1) - - @clear_users - def test_add_float_charge(self): - self.assert_charged(code='TEST-CHARGE', each_amount=2.3, quantity=2) - + @clear_users def test_add_float_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=2.3, quantity=2) - + @clear_users def test_add_decimal_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=Decimal('2.3'), quantity=3) - + @clear_users def test_add_charge_with_descriptions(self): self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1, description="A test charge") - + @clear_users def test_add_credit(self): self.assert_charged(code='TEST-CHARGE', each_amount=-1, quantity=1) @@ -595,7 +597,6 @@ def test_add_one_time_invoice_with_description(self): self.assertOneTimeInvoice(charges) - @clear_users def test_add_one_time_invoice_with_multiple_charges(self): charges = [{ @@ -612,3 +613,43 @@ def test_add_one_time_invoice_with_multiple_charges(self): },] self.assertOneTimeInvoice(charges) + + def test_get_all_promotions(self): + ''' Test get all promotions. ''' + product = self.get_product() + promotions = product.get_all_promotions() + + self.assertEquals(2, len(promotions)) + for promotion in promotions: + assert promotion.coupons[0].get('code') in ('COUPON', 'COUPON2') + + def test_get_promotion(self): + ''' Test get a single promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + self.assertEqual(unicode(promotion), 'Coupon (COUPON)') + self.assertEqual(promotion.name, 'Coupon') + self.assertEqual(promotion.coupons[0].get('code'), 'COUPON') + self.assertEqual(promotion.incentives[0].get('percentage'), '10') + self.assertEqual(promotion.incentives[0].get('expiration_datetime'), None) + + def test_promotion_repr(self): + ''' Test the internal __repr___ method of Promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + expected = 'Promotion: Coupon (COUPON)' + result = repr(promotion) + + self.assertEquals(expected, result) + + def test_promotion_unicode(self): + ''' Test the internal __unicode___ method of Promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + expected = 'Coupon (COUPON)' + result = unicode(promotion) + + self.assertEquals(expected, result) diff --git a/tests/readme.rst b/tests/readme.rst new file mode 100644 index 0000000..325ac3d --- /dev/null +++ b/tests/readme.rst @@ -0,0 +1,74 @@ +Requirements +============ + +Inside a virtualenv, run: + +.. code:: + + pip install -r dev-requirements.txt + +Installing elementtree for Unit Testing +======================================================= +When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. + +.. code:: + + wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip + unzip elementtree-1.2.6-20050316.zip + cd elementtree-1.2.6-20050316/ + pip install . + +CheddarGetter Setup +============= +You will also need to setup the correct plans in cheddar. You may want to set up a product intended just for testing. + + + +The following tracked items are required for unit tests: + ++--------------+--------------+ +| Name | Code | ++==============+==============+ +| Once Item | ONCE_ITEM | ++--------------+--------------+ +| Monthly Item | MONTHLY_ITEM | ++--------------+--------------+ + +The following plan codes are required for unit tests: + ++-----------------+-----------------+---------+-----------+--------------+ +| Plan Name | Code | Price | ONCE_ITEM | MONTHLY_ITEM | ++=================+=================+=========+===========+==============+ +| Free Monthly | FREE_MONTHLY | $0.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ +| Paid Monthly | PAID_MONTHLY | $10.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ +| Tracked Monthly | TRACKED_MONTHLY | $10.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ + + +The following promotions are required for unit tests: + ++----------------+---------------+--------+-----------+ +| Promotion Name | Coupon Code | % Off | Duration | ++================+===============+========+===========+ +| Coupon | COUPON | 10 | Forever | ++----------------+---------------+--------+-----------+ +| Coupon 2 | COUPON2 | 20 | Forever | ++----------------+---------------+--------+-----------+ + +Be sure to turn on the native gateway credit card option in Configuration > Product settings > Gateway Settings. +Be sure to turn on the paypal option in Configuration > Product settings > Gateway Settings or Quick Setup > Billing solution. I checked the "Use standard payments (PayPal account to PayPal account)" checkbox. + +Config +====== + +In the tests folder, copy the config.ini.template to config.ini. Fill in your email, password, and product code. + +Running Tests +============= +Run the test with nosetests. + +.. code:: + + nosetests diff --git a/tests/testing_tools/utils.py b/tests/testing_tools/utils.py index faa96bf..0b5d616 100644 --- a/tests/testing_tools/utils.py +++ b/tests/testing_tools/utils.py @@ -16,5 +16,5 @@ def clear_users(): response, content = h.request(url, 'POST') - if response.status != 200: - raise Exception('Could not clear users. Recieved a response of %s %s ' % (response.status, response.reason)) + if response.status != 200 or 'success' not in content: + raise Exception('Could not clear users. Recieved a response of %s %s \n %s' % (response.status, response.reason, content))