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))