diff --git a/.gitignore b/.gitignore index 7849784..c538c7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,23 @@ +# OS & Editors folders +.DS_Store +.cache +.directory +.project +.settings +.tmproj +.ropeproject +.pydevproject +nbproject +Thumbs.db +.idea +*.orig +.c9 +.~c9_invoke* + +# Python +__pycache__ *.pyc -*.DS_Store + +# DBs +/*.db +/*.sqlite3 \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..32e4cfc --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.rst +include LICENSE +include AUTHORS +recursive-include django_braintree/templates * diff --git a/django_braintree/__init__.py b/django_braintree/__init__.py index e69de29..fe29afc 100644 --- a/django_braintree/__init__.py +++ b/django_braintree/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +default_app_config = 'django_braintree.apps.Config' diff --git a/django_braintree/apps.py b/django_braintree/apps.py new file mode 100644 index 0000000..73aed3f --- /dev/null +++ b/django_braintree/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class Config(AppConfig): + name = 'django_braintree' diff --git a/django_braintree/forms.py b/django_braintree/forms.py index ce4ac8f..aee9b35 100644 --- a/django_braintree/forms.py +++ b/django_braintree/forms.py @@ -25,17 +25,16 @@ class UserCCDetailsForm(forms.Form): ) __YEAR_CHOICES = ( - (2010, '2010'), - (2011, '2011'), - (2012, '2012'), - (2013, '2013'), - (2014, '2014'), - (2015, '2015'), (2016, '2016'), (2017, '2017'), (2018, '2018'), (2019, '2019'), (2020, '2020'), + (2021, '2021'), + (2022, '2022'), + (2023, '2023'), + (2024, '2024'), + (2025, '2025'), ) name = forms.CharField(max_length=64, label='Name as on card') @@ -55,7 +54,7 @@ def __init__(self, user, post_to_update=False, *args, **kwargs): this form is meant for rendering to the user, hence initialize with braintree data (if any). """ self.__user = user - self.__user_vault = UserVault.objects.get_user_vault_instance_or_none(user) + self.__user_vault = UserVault.objects.for_user(user) if not post_to_update and self.__user_vault and not args: logging.debug('Looking up payment info for vault_id: %s' % self.__user_vault.vault_id) @@ -72,7 +71,7 @@ def __init__(self, user, post_to_update=False, *args, **kwargs): 'zip_code': info.billing_address.postal_code, } super(UserCCDetailsForm, self).__init__(initial=initial, *args, **kwargs) - except Exception, e: + except Exception as e: logging.error('Was not able to get customer from vault. %s' % e) super(UserCCDetailsForm, self).__init__(initial = {'name': '%s %s' % (user.first_name, user.last_name)}, *args, **kwargs) @@ -116,7 +115,7 @@ def save(self, prepend_vault_id=''): response = Customer.find(self.__user_vault.vault_id) cc_info = response.credit_cards[0] return CreditCard.update(cc_info.token, params=cc_details_map) - except Exception, e: + except Exception as e: logging.error('Was not able to get customer from vault. %s' % e) self.__user_vault.delete() # delete the stale instance from our db diff --git a/django_braintree/migrations/0001_initial.py b/django_braintree/migrations/0001_initial.py new file mode 100644 index 0000000..156533f --- /dev/null +++ b/django_braintree/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-05-25 08:03 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='PaymentLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=7)), + ('timestamp', models.DateTimeField(auto_now=True)), + ('transaction_id', models.CharField(max_length=128)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='UserVault', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('vault_id', models.CharField(max_length=64, unique=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/django_braintree/migrations/__init__.py b/django_braintree/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_braintree/models.py b/django_braintree/models.py index a1e260f..dfe0bbd 100644 --- a/django_braintree/models.py +++ b/django_braintree/models.py @@ -2,48 +2,39 @@ from decimal import Decimal from django.db import models -from django.contrib.auth.models import User +from django.conf import settings +from django.utils.six import python_2_unicode_compatible, text_type from braintree import Transaction class UserVaultManager(models.Manager): - def get_user_vault_instance_or_none(self, user): - """Returns a vault_id string or None""" - qset = self.filter(user=user) - if not qset: + def for_user(self, user): + """ Returns UserVault object for user or None""" + try: + return self.get(user=user) + except UserVault.DoesNotExists: return None - - if qset.count() > 1: - raise Exception('This app does not currently support multiple vault ids') - - return qset.get() def is_in_vault(self, user): - return True if self.filter(user=user) else False - - def charge(self, user, vault_id=None): - """If vault_id is not passed this will assume that there is only one instane of user and vault_id in the db.""" - assert self.is_in_vault(user) - if vault_id: - user_vault = self.get(user=user, vault_id=vault_id) - else: - user_vault = self.get(user=user) + return True if self.filter(user=user).count() > 0 else False + +@python_2_unicode_compatible class UserVault(models.Model): """Keeping it open that one user can have multiple vault credentials, hence the FK to User and not a OneToOne.""" - user = models.ForeignKey(User, unique=True) + user = models.OneToOneField(settings.AUTH_USER_MODEL) vault_id = models.CharField(max_length=64, unique=True) objects = UserVaultManager() - def __unicode__(self): - return self.user.username + def __str__(self): + return text_type(self.user) def charge(self, amount): """ - Charges the users credit card, with he passed $amount, if they are in the vault. Returns the payment_log instance - or None (if charge fails etc.) + Charges the users credit card, with he passed $amount, if they are in the vault. + Returns the payment_log instance or None (if charge fails etc.) """ try: result = Transaction.sale( @@ -58,23 +49,28 @@ def charge(self, amount): if result.is_success: # create a payment log - payment_log = PaymentLog.objects.create(user=self.user, amount=amount, transaction_id=result.transaction.id) + payment_log = PaymentLog.objects.create( + user=self.user, amount=amount, + transaction_id=result.transaction.id + ) return payment_log else: - raise Exception('Logical error in CC transaction') - except Exception: + raise ValueError('Logical error in CC transaction') + except ValueError: logging.error('Failed to charge $%s to user: %s with vault_id: %s' % (amount, self.user, self.vault_id)) return None + +@python_2_unicode_compatible class PaymentLog(models.Model): """ Captures raw charges made to a users credit card. Extra info related to this payment should be a OneToOneField referencing this model. """ - user = models.ForeignKey(User) + user = models.ForeignKey(settings.AUTH_USER_MODEL) amount = models.DecimalField(max_digits=7, decimal_places=2) timestamp = models.DateTimeField(auto_now=True) transaction_id = models.CharField(max_length=128) - def __unicode__(self): + def __str__(self): return '%s charged $%s - %s' % (self.user, self.amount, self.transaction_id) diff --git a/django_braintree/views.py b/django_braintree/views.py index 5dd1586..6b72178 100644 --- a/django_braintree/views.py +++ b/django_braintree/views.py @@ -15,6 +15,7 @@ BAD_CC_ERROR_MSG = 'Oops! Doesn\'t seem like your Credit Card details are correct. Please re-check and try again.' + @ssl_required() @login_required def payments_billing(request, template='django_braintree/payments_billing.html'): @@ -39,9 +40,9 @@ def payments_billing(request, template='django_braintree/payments_billing.html') else: if UserVault.objects.is_in_vault(request.user): try: - response = Customer.find(UserVault.objects.get_user_vault_instance_or_none(request.user).vault_id) + response = Customer.find(UserVault.objects.for_user(request.user).vault_id) d['current_cc_info'] = response.credit_cards[0] - except Exception, e: + except Exception as e: logging.error('Unable to get vault information for user from braintree. %s' % e) d['cc_form'] = UserCCDetailsForm(request.user) diff --git a/setup.py b/setup.py index 5795e51..c3ff750 100644 --- a/setup.py +++ b/setup.py @@ -20,26 +20,24 @@ setup( name='tivix-django-braintree', - version='0.1.2', + version='0.1.4', author='Sumit Chachra', author_email='chachra@tivix.com', url='http://github.com/tivix/django-braintree', - description = 'An easy way to integrate with Braintree Payment Solutions from Django.', + description='An easy way to integrate with Braintree Payment Solutions from Django.', long_description=long_description, - keywords = 'django braintree payment', + keywords='django braintree payment', packages=find_packages(), + include_package_data=True, zip_safe=False, install_requires=[ 'Django>=1.4.0', 'South>=0.7.2', 'braintree>=2.10.0', - 'django-common>=0.1', + 'django-common-helpers>=0.8.0', 'fudge==1.0.3' ], - #dependency_links=["git://github.com/Tivix/django-common.git@91e23cd5e0e8b420e8d4#egg=django_common-0.1"], - test_suite = 'django_braintree.tests', - include_package_data=True, - # cmdclass={}, + test_suite='django_braintree.tests', classifiers=[ 'Framework :: Django', 'Intended Audience :: Developers',