diff --git a/core/fixtures/initial_data.json b/core/fixtures/initial_data.json index aa5e4dec..70f557cc 100644 --- a/core/fixtures/initial_data.json +++ b/core/fixtures/initial_data.json @@ -82,5 +82,24 @@ "description": "Your username, profile URL, and profile tagline under \"bibliophiles\"", "created": "2011-11-17 22:03:37" } + }, + { + "pk": 150, + "model": "core.premium", + "fields": { + "campaign": null, + "amount": 0, + "type": "00", + "description": "No premium, thanks! I just want to help unglue.", + "created": "2011-11-17 22:03:37" + } + }, + { + "pk": 1, + "model": "core.badge", + "fields": { + "name": "pledger", + "description": "has made a pledge in at least one ungluing campaign" + } } ] diff --git a/core/migrations/0040_auto__add_badge.py b/core/migrations/0040_auto__add_badge.py new file mode 100644 index 00000000..35492227 --- /dev/null +++ b/core/migrations/0040_auto__add_badge.py @@ -0,0 +1,249 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Badge' + db.create_table('core_badge', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=72, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(default='', null=True)), + )) + db.send_create_signal('core', ['Badge']) + + # Adding M2M table for field badges on 'UserProfile' + db.create_table('core_userprofile_badges', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('userprofile', models.ForeignKey(orm['core.userprofile'], null=False)), + ('badge', models.ForeignKey(orm['core.badge'], null=False)) + )) + db.create_unique('core_userprofile_badges', ['userprofile_id', 'badge_id']) + + + def backwards(self, orm): + + # Deleting model 'Badge' + db.delete_table('core_badge') + + # Removing M2M table for field badges on 'UserProfile' + db.delete_table('core_userprofile_badges') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 17, 15, 54, 11, 910150)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 17, 15, 54, 11, 910015)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'core.author': { + 'Meta': {'object_name': 'Author'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'editions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authors'", 'symmetrical': 'False', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '500'}) + }, + 'core.badge': { + 'Meta': {'object_name': 'Badge'}, + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '72', 'blank': 'True'}) + }, + 'core.campaign': { + 'Meta': {'object_name': 'Campaign'}, + 'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deadline': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('ckeditor.fields.RichTextField', [], {'null': 'True'}), + 'details': ('ckeditor.fields.RichTextField', [], {'null': 'True', 'blank': 'True'}), + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'null': 'True', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'license': ('django.db.models.fields.CharField', [], {'default': "'CC BY-NC-ND'", 'max_length': '255'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}), + 'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"}) + }, + 'core.campaignaction': { + 'Meta': {'object_name': 'CampaignAction'}, + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'actions'", 'to': "orm['core.Campaign']"}), + 'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '15'}) + }, + 'core.celerytask': { + 'Meta': {'object_name': 'CeleryTask'}, + 'active': ('django.db.models.fields.NullBooleanField', [], {'default': 'True', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 17, 15, 54, 11, 494504)', 'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True'}), + 'function_args': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'function_name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'task_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tasks'", 'null': 'True', 'to': "orm['auth.User']"}) + }, + 'core.claim': { + 'Meta': {'object_name': 'Claim'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rights_holder': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'claim'", 'to': "orm['core.RightsHolder']"}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '7'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'claim'", 'to': "orm['auth.User']"}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'claim'", 'to': "orm['core.Work']"}) + }, + 'core.ebook': { + 'Meta': {'object_name': 'Ebook'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ebooks'", 'to': "orm['core.Edition']"}), + 'format': ('django.db.models.fields.CharField', [], {'max_length': '25'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'rights': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '1024'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'core.edition': { + 'Meta': {'object_name': 'Edition'}, + 'cover_image': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_domain': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), + 'unglued': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"}) + }, + 'core.identifier': { + 'Meta': {'unique_together': "(('type', 'value'),)", 'object_name': 'Identifier'}, + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'identifiers'", 'null': 'True', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '31'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'identifiers'", 'to': "orm['core.Work']"}) + }, + 'core.key': { + 'Meta': {'object_name': 'Key'}, + 'encrypted_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'core.premium': { + 'Meta': {'object_name': 'Premium'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '0'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'premiums'", 'null': 'True', 'to': "orm['core.Campaign']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '2'}) + }, + 'core.rightsholder': { + 'Meta': {'object_name': 'RightsHolder'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rights_holder'", 'to': "orm['auth.User']"}), + 'rights_holder_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'core.subject': { + 'Meta': {'ordering': "['name']", 'object_name': 'Subject'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}), + 'works': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'subjects'", 'symmetrical': 'False', 'to': "orm['core.Work']"}) + }, + 'core.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + 'badges': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'holders'", 'symmetrical': 'False', 'to': "orm['core.Badge']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'facebook_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'goodreads_auth_secret': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'goodreads_auth_token': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'goodreads_user_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'goodreads_user_link': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'goodreads_user_name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'home_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'librarything_id': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}), + 'pic_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'tagline': ('django.db.models.fields.CharField', [], {'max_length': '140', 'blank': 'True'}), + 'twitter_id': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'core.waswork': { + 'Meta': {'object_name': 'WasWork'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'moved': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'was': ('django.db.models.fields.IntegerField', [], {'unique': 'True'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Work']"}) + }, + 'core.wishes': { + 'Meta': {'object_name': 'Wishes', 'db_table': "'core_wishlist_works'"}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), + 'wishlist': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Wishlist']"}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wishes'", 'to': "orm['core.Work']"}) + }, + 'core.wishlist': { + 'Meta': {'object_name': 'Wishlist'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'wishlist'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'works': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'wishlists'", 'symmetrical': 'False', 'through': "orm['core.Wishes']", 'to': "orm['core.Work']"}) + }, + 'core.work': { + 'Meta': {'ordering': "['title']", 'object_name': 'Work'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}), + 'num_wishes': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}) + } + } + + complete_apps = ['core'] diff --git a/core/models.py b/core/models.py index 19be025d..d5b13820 100755 --- a/core/models.py +++ b/core/models.py @@ -111,6 +111,16 @@ class Premium(models.Model): t_model=get_model('payment','Transaction') return self.limit - t_model.objects.filter(premium=self).count() +class PledgeExtra: + premium=None + anonymous=False + ack_name='' + ack_dedication='' + def __init__(self,premium=None,anonymous=False,ack_name='',ack_dedication=''): + self.premium=premium + self.anonymous=anonymous + self.ack_name=ack_name + self.ack_dedication=ack_dedication class CampaignAction(models.Model): timestamp = models.DateTimeField(auto_now_add=True) @@ -131,8 +141,8 @@ class CCLicense(): ) CHOICES = CCCHOICES+(('PD-US', 'Public Domain, US'),) - @classmethod - def url(klass, license): + @staticmethod + def url(license): if license == 'PD-US': return 'http://creativecommons.org/publicdomain/mark/1.0/' elif license == 'CC0': @@ -152,8 +162,8 @@ class CCLicense(): else: return '' - @classmethod - def badge(klass,license): + @staticmethod + def badge(license): if license == 'PD-US': return 'https://i.creativecommons.org/p/mark/1.0/88x31.png' elif license == 'CC0': @@ -264,8 +274,13 @@ class Campaign(models.Model): def transactions(self, **kwargs): p = PaymentManager() - return p.query_campaign(self, summary=False, campaign_total=True, **kwargs) + # handle default parameter values + kw = {'summary':False, 'campaign_total':True} + kw.update(kwargs) + + return p.query_campaign(self, **kw) + def activate(self): status = self.status @@ -368,6 +383,35 @@ class Campaign(models.Model): return ungluers + def ungluer_transactions(self): + """ + returns a list of authorized transactions for campaigns in progress, + or completed transactions for successful campaigns + used to build the acks page -- because ack_name, _link, _dedication adhere to transactions, + it's easier to return transactions than ungluers + """ + p = PaymentManager() + ungluers={"all":[],"supporters":[], "patrons":[], "bibliophiles":[]} + anons = 0 + if self.status == "ACTIVE": + translist = p.query_campaign(self, summary=False, pledged=True, authorized=True) + elif self.status == "SUCCESSFUL": + translist = p.query_campaign(self, summary=False, pledged=True, completed=True) + else: + translist = [] + for transaction in translist: + ungluers['all'].append(transaction.user) + if transaction.anonymous: + anons += 1 + if transaction.amount >= Premium.TIERS["bibliophile"]: + ungluers['bibliophiles'].append(transaction) + elif transaction.amount >= Premium.TIERS["patron"]: + ungluers['patrons'].append(transaction) + elif transaction.amount >= Premium.TIERS["supporter"]: + ungluers['supporters'].append(transaction) + + return ungluers + def effective_premiums(self): """returns the available premiums for the Campaign including any default premiums""" q = Q(campaign=self) | Q(campaign__isnull=True) @@ -375,7 +419,7 @@ class Campaign(models.Model): def custom_premiums(self): """returns only the active custom premiums for the Campaign""" - return Premium.objects.filter(campaign=self).filter(type='CU') + return Premium.objects.filter(campaign=self).filter(type='CU').order_by('amount') @property def rightsholder(self): @@ -428,8 +472,8 @@ class Identifier(models.Model): other.delete() return identifier - @classmethod - def get_or_add(klass, type='goog', value=None, edition=None, work=None): + @staticmethod + def get_or_add( type='goog', value=None, edition=None, work=None): try: return Identifier.objects.get(type=type, value=value) except Identifier.DoesNotExist: @@ -652,6 +696,13 @@ class Work(models.Model): return edition.publication_date return '' + @property + def publication_date_year(self): + try: + return self.publication_date[:4] + except IndexError: + return 'unknown' + def __unicode__(self): return self.title @@ -752,8 +803,8 @@ class Edition(models.Model): except IndexError: return '' - @classmethod - def get_by_isbn(klass, isbn): + @staticmethod + def get_by_isbn( isbn): if len(isbn)==10: isbn=regluit.core.isbn.convert_10_to_13(isbn) try: @@ -791,8 +842,8 @@ class Ebook(models.Model): return CCLicense.badge('PD-US') return CCLicense.badge(self.rights) - @classmethod - def infer_provider(klass, url): + @staticmethod + def infer_provider( url): if not url: return None # provider derived from url. returns provider value. remember to call save() afterward @@ -859,13 +910,18 @@ class UserProfile(models.Model): twitter_id = models.CharField(max_length=15, blank=True) facebook_id = models.PositiveIntegerField(null=True) librarything_id = models.CharField(max_length=31, blank=True) + badges = models.ManyToManyField('Badge', related_name='holders') goodreads_user_id = models.CharField(max_length=32, null=True, blank=True) goodreads_user_name = models.CharField(max_length=200, null=True, blank=True) goodreads_auth_token = models.TextField(null=True, blank=True) goodreads_auth_secret = models.TextField(null=True, blank=True) goodreads_user_link = models.CharField(max_length=200, null=True, blank=True) - + +class Badge(models.Model): + name = models.CharField(max_length=72, blank=True) + description = models.TextField(default='', null=True) + #class CampaignSurveyResponse(models.Model): # # generic # campaign = models.ForeignKey("Campaign", related_name="surveyresponse", null=False) diff --git a/core/signals.py b/core/signals.py index 919289aa..fdab4099 100644 --- a/core/signals.py +++ b/core/signals.py @@ -174,6 +174,7 @@ def handle_pledge_modified(sender, transaction=None, up_or_down=None, **kwargs): # we need to know if pledges were modified up or down because Amazon handles the # transactions in different ways, resulting in different user-visible behavior; # we need to set expectations appropriately + # up_or_down is 'increased', 'decreased', or 'canceled' if transaction==None or up_or_down==None: return notification.queue([transaction.user], "pledge_status_change", { diff --git a/frontend/forms.py b/frontend/forms.py index 0abcf188..0b050cab 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -13,7 +13,7 @@ from decimal import Decimal as D from selectable.forms import AutoCompleteSelectMultipleWidget,AutoCompleteSelectMultipleField from selectable.forms import AutoCompleteSelectWidget,AutoCompleteSelectField -from regluit.core.models import UserProfile, RightsHolder, Claim, Campaign, Premium, Ebook, Edition +from regluit.core.models import UserProfile, RightsHolder, Claim, Campaign, Premium, Ebook, Edition, PledgeExtra from regluit.core.lookups import OwnerLookup from regluit.utils.localdatetime import now @@ -187,6 +187,25 @@ class OpenCampaignForm(forms.ModelForm): fields = 'name', 'work', 'managers' widgets = { 'work': forms.HiddenInput } +def getTransferCreditForm(maximum, data=None, *args, **kwargs ): + class TransferCreditForm(forms.Form): + recipient = AutoCompleteSelectField( + OwnerLookup, + label='Recipient', + widget=AutoCompleteSelectWidget(OwnerLookup), + required=True, + error_messages={'required': 'Please ensure the recipient is a valid Unglue.it account.'}, + ) + amount = forms.IntegerField( + required=True, + min_value=1, + max_value=maximum, + label="Transfer Amount", + error_messages={'min_value': 'Transfer amount must be positive', 'max_value': 'You only have %(limit_value)s available for transfer'}, + ) + return TransferCreditForm( data=data ) + + class EditManagersForm(forms.ModelForm): managers = AutoCompleteSelectMultipleField( OwnerLookup, @@ -285,59 +304,71 @@ class CampaignPledgeForm(forms.Form): decimal_places=2, label="Pledge Amount", ) - anonymous = forms.BooleanField(required=False, label=_("Don't display my username in the supporters list")) + anonymous = forms.BooleanField(required=False, label=_("Don't display my name in the acknowledgements")) + ack_name = forms.CharField(required=False, max_length=64, label=_("What name should we display?")) + ack_dedication = forms.CharField(required=False, max_length=140, label=_("Your dedication:")) premium_id = forms.IntegerField(required=False) + premium=None + @property + def pledge_extra(self): + return PledgeExtra( anonymous=self.cleaned_data['anonymous'], + ack_name=self.cleaned_data['ack_name'], + ack_dedication=self.cleaned_data['ack_dedication'], + premium=self.premium) + def clean_preapproval_amount(self): - data = self.cleaned_data['preapproval_amount'] - if data is None: + preapproval_amount = self.cleaned_data['preapproval_amount'] + if preapproval_amount is None: raise forms.ValidationError(_("Please enter a pledge amount.")) - return data + return preapproval_amount - # should we do validation on the premium_id here? - # can see whether it corresponds to a real premium -- do that here? - # can also figure out moreover whether it's one of the allowed premiums for that campaign.... + def clean_premium_id(self): + premium_id = self.cleaned_data['premium_id'] + try: + self.premium= Premium.objects.get(id=premium_id) + if self.premium.limit>0: + if self.premium.limit<=self.premium.premium_count: + raise forms.ValidationError(_("Sorry, that premium is fully subscribed.")) + except Premium.DoesNotExist: + raise forms.ValidationError(_("Sorry, that premium is not valid.")) def clean(self): - cleaned_data = self.cleaned_data # check on whether the preapproval amount is < amount for premium tier. If so, put an error message try: - preapproval_amount = cleaned_data.get("preapproval_amount") - premium_id = int(cleaned_data.get("premium_id")) - premium_amount = Premium.objects.get(id=premium_id).amount - logger.info("preapproval_amount: {0}, premium_id: {1}, premium_amount:{2}".format(preapproval_amount, premium_id, premium_amount)) - if preapproval_amount < premium_amount: + preapproval_amount = self.cleaned_data.get("preapproval_amount") + logger.info("preapproval_amount: {0}, premium_id: {1}, premium_amount:{2}".format(preapproval_amount, self.premium.id, self.premium.amount)) + if preapproval_amount < self.premium.amount: logger.info("raising form validating error") - raise forms.ValidationError(_("Sorry, you must pledge at least $%s to select that premium." % (premium_amount))) - try: - premium= Premium.objects.get(id=premium_id) - if premium.limit>0: - if premium.limit<=premium.premium_count: - raise forms.ValidationError(_("Sorry, that premium is fully subscribed.")) - except Premium.DoesNotExist: - raise forms.ValidationError(_("Sorry, that premium is not valid.")) + raise forms.ValidationError(_("Sorry, you must pledge at least $%s to select that premium." % (self.premium.amount))) except Exception, e: if isinstance(e, forms.ValidationError): raise e - return cleaned_data + return self.cleaned_data -class DonateForm(forms.Form): - donation_amount = forms.DecimalField( +class CCForm(forms.Form): + username = forms.CharField(max_length=30, required=True ) + work_id = forms.IntegerField(required=False, widget=forms.HiddenInput() ) + stripe_token = forms.CharField(required=False, widget=forms.HiddenInput()) + preapproval_amount= forms.DecimalField( required=False, min_value=D('1.00'), max_value=D('100000.00'), decimal_places=2, - label="Donation", + label="Pledge", ) - anonymous = forms.BooleanField(required=False, label=_("Don't display my username in the donors' list")) - - def clean(self): - cleaned_data = self.cleaned_data - return cleaned_data + retain_cc_info = forms.BooleanField(required=False, initial=True, label=_("Keep my credit card on record")) +class DonateForm(forms.Form): + preapproval_amount = forms.DecimalField( widget=forms.HiddenInput() ) + username = forms.CharField(max_length=30, required=True, widget=forms.HiddenInput() ) + work_id = forms.IntegerField(required=False, widget=forms.HiddenInput() ) + title = forms.CharField(max_length=30, required=False, widget=forms.HiddenInput() ) + + class GoodreadsShelfLoadingForm(forms.Form): goodreads_shelf_name_number = forms.CharField(widget=forms.Select(choices=( ('all','all'), diff --git a/frontend/templates/campaign_detail.html b/frontend/templates/campaign_detail.html index c652c25c..dcf791fc 100644 --- a/frontend/templates/campaign_detail.html +++ b/frontend/templates/campaign_detail.html @@ -2,22 +2,6 @@ {% load humanize %} {% url privacy as privacyurl %} -{% block extra_head %} - - - {% if embedded %} - - {% endif %} - -{% endblock %} {% block content %} @@ -52,13 +36,5 @@

No associated transactions

{% endif %} - {% if embedded %} - - {% endif %} {% endblock %} diff --git a/frontend/templates/donate.html b/frontend/templates/donate.html deleted file mode 100644 index 6eb537e9..00000000 --- a/frontend/templates/donate.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "basedocumentation.html" %} - -{% block title %}Donate{% endblock %} - -{% block extra_extra_head %} -{% endblock %} - -{% block doccontent %} - -

Donate

-

Wonderful: We're glad that you would like to donate to our partnering non-profit.

- -
- {% csrf_token %} - {{form.as_p}} - -
- -{% endblock %} - - diff --git a/frontend/templates/donation.html b/frontend/templates/donation.html new file mode 100644 index 00000000..fff44d20 --- /dev/null +++ b/frontend/templates/donation.html @@ -0,0 +1,61 @@ +{% extends "basedocumentation.html" %} +{% load humanize %} + +{% block title %}Donations{% endblock %} + +{% block extra_extra_head %} + + +{{ transfer_form.media.css }} + + +{{ transfer_form.media.js }} +{% endblock %} + +{% block doccontent %} +

Donation Credits

+

+You have a balance of {{ user.credit.balance }} donation credits.
+You have pledged {{ user.credit.pledged }} donation credits to ungluing campaigns.
+You have {{ user.credit.available }} donation credits available to pledge or transfer.
+

+
+

Donation Credit Transfers

+{% if transfer_message %} +

{{ transfer_message }} +{% if transfer_amount %} +
Recipient: {{ recipient }} +
Amount: {{ transfer_amount }} donation credits +{% endif %} +

+{% endif %} +

+You may transfer up to {{ user.credit.available }} donation credits to another Unglue.it user.
+

+
+ {% csrf_token %} + {{ transfer_form.as_p }} + +
+
+
+

About Donation Credits

+{% if nonprofit.is_on %} +

Unglue.it uses donation credits to cooperate with a non-profit charity, {{ nonprofit.name }} in the ungluing of books. When you make a donation to {{ nonprofit.name }}, you can get credits for your donation. Then you can direct how {{ nonprofit.name }} uses your donation in support of ungluing campaigns. You can also transfer your credits to other ungluers. To make a donation, and receive credits, click the button! +

+ +
+{% else %} +

Donation credits are not turned on yet.

+{% endif %} +{% endblock %} + + diff --git a/frontend/templates/donation_credit.html b/frontend/templates/donation_credit.html new file mode 100644 index 00000000..c96302e1 --- /dev/null +++ b/frontend/templates/donation_credit.html @@ -0,0 +1,45 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}You Have Donation Credits{% endblock %} + +{% block extra_extra_head %} + + + +{% endblock %} + +{% block doccontent %} +
+ +
+
+ +
+

Donation Credited

+

{% if error %} + Your donation credit of ${{ envelope.amount }}.{{ envelope.cents }} has already been registered! {% if work %} If you want to contribute more to {{ work.title }}, you can! {% endif %} + {% else %} + Congratulations, your donation credit of ${{ envelope.amount }}.{{ envelope.cents }} has been registered! {% if transaction.campaign %} ${{transaction.amount}} of that had been pledged to {{ transaction.campaign.name }}. If you want to contribute more to {{ work.title }}, you can! {% endif %} + {% endif %} +

+ + + +
+

Your donation credits

+

+You have a balance of {{ request.user.credit.balance }} donation credits.
+You have pledged {{ request.user.credit.pledged }} donation credits to ungluing campaigns.
+You have {{ request.user.credit.available }} donation credits available to pledge or transfer.
+

+ + +
+
+ +
+
+{% endblock %} + + diff --git a/frontend/templates/donation_user_error.html b/frontend/templates/donation_user_error.html new file mode 100644 index 00000000..1951a2d8 --- /dev/null +++ b/frontend/templates/donation_user_error.html @@ -0,0 +1,29 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}Please Log in as...{% endblock %} + +{% block extra_extra_head %} + + + +{% endblock %} + +{% block doccontent %} +
+ +
+
+ +

Wrong user for donation credit

+
+

Unglue.it would like to process your donation credit, but you are currently logged in as {{request.user.username}}. Your donation credit from {{nonprofit.name}} for ${{ envelope.amount }}.{{ envelope.cents }} is designated for {{ envelope.username }}. Do record your credit, you need to log out, and then log in as {{ envelope.username }}. If you have any problem, don't hesitate to contact unglue.it support. +

+
+
+ +
+
+{% endblock %} + + diff --git a/frontend/templates/faq_pledge.html b/frontend/templates/faq_pledge.html index 0a0f62a7..0ca0c268 100644 --- a/frontend/templates/faq_pledge.html +++ b/frontend/templates/faq_pledge.html @@ -6,21 +6,28 @@
  • How do I pledge? - Enter your pledge amount and select a premium. (You may select a premium at any level up to and including the amount you pledge.) After you click Pledge, you'll be directed through Amazon to complete the transaction. + Enter your pledge amount and select a premium. (You may select a premium at any level up to and including the amount you pledge.) If you pledge enough, you're also eligible to be credited in the unglued ebook and to include a dedication, and toward the bottom of this page you can specify what you'd like those to say. After you click Pledge, you'll be directed through Amazon to complete the transaction.
  • Do I need an Amazon account to pledge? - No. While you can use yours if you have one, any major credit card will do. + Yes. We know this is an obstacle for some users, and we apologize. Unfortunately we cannot offer other options at this time. There are more details at our blog.
  • +
  • + Can I use PayPal or some other non-Amazon payment system? + + We're sorry, but no. At this time PayPal and Amazon are the only payment processors which support pledges (rather than immediate charges). While we're working on adding PayPal support, we don't yet have all the approvals we need to do so. There are more details at our blog. + +
  • +
  • When will I be charged? - If this campaign reaches its target price, you'll be charged at that time. If it does not, your pledge will expire on {{ campaign.deadline }} (Eastern US time) and you will not be charged. + Campaigns succeed if they reach their target price by their deadline. If this campaign succeeds, you'll be charged within a day of when it reaches its target. If it does not, your pledge will expire on {{ campaign.deadline }} (Eastern US time) and you will not be charged.
  • diff --git a/frontend/templates/front_matter.html b/frontend/templates/front_matter.html index 20cddd22..48539ae7 100644 --- a/frontend/templates/front_matter.html +++ b/frontend/templates/front_matter.html @@ -1,5 +1,5 @@ {% extends "basedocumentation.html" %} -{% block title %}Open Access eBooks{% endblock %} +{% block title %}Ungluers supporting the {{ campaign }}{% endblock %} {% block extra_extra_head %} {% endblock %} @@ -9,43 +9,45 @@

    © {{ campaign.work.preferred_edition.publication_date }} by {{ campaign.work.author }}

    ISBN: {{ campaign.work.preferred_edition.isbn_13 }} .

    -

    URI: https://unglue.it/work/{{ campaign.work.id }}/ (this work).

    +

    URI: https://unglue.it/work/{{ campaign.work.id }}/ (this work).

    unglue.it logo

    This unglued edition is distributed under the terms of the Creative Commons ({{ campaign.license }}) license. The Creative Commons licensing is made possible by the support of readers like you. - Become an ungluer at https://unglue.it/ + Become an ungluer at https://unglue.it/ .

    This is an unglued ebook

    + {% with campaign.ungluer_transactions as transactions %}

    - Unglued ebooks are made possible through the Unglue.it website by contributions from {{ campaign.ungluers.all|length }} readers like you.

    - {% if campaign.ungluers.supporters %} -

    Supporters of this edition include ungluers: - {% for ungluer in campaign.ungluers.supporters %} - {% if forloop.last %}and {% endif %}{{ ungluer.username }}{% if forloop.last %}. {% else %}, {% endif %} + Unglued ebooks are made possible through the Unglue.it website by contributions from {{ transactions.all|length }} readers like you.

    + {% if transactions.supporters %} +

    Supporters of this edition: + {% for transaction in transactions.supporters %} + {% if forloop.last %}{% if transactions.supporters|length > 1 %}and {% endif %}{% endif %}{{ transaction.ack_name }}{% if forloop.last %}. {% else %}, {% endif %} {% endfor %}

    {% endif %} - {% if campaign.ungluers.patrons %} -

    Benefactors of this edition include ungluers: - {% for ungluer in campaign.ungluers.patrons %} - {% if forloop.last %}and {% endif %}{{ ungluer.username }}{% if forloop.last %}. {% else %}, {% endif %} + {% if transactions.patrons %} +

    Benefactors of this edition: + {% for transaction in transactions.patrons %} + {% if forloop.last %}{% if transactions.patrons|length > 1 %}and {% endif %}{% endif %}{% if transaction.ack_link %}{{ transaction.ack_name }}{% else %}{{ transaction.ack_name }}{% endif %}{% if forloop.last %}. {% else %}, {% endif %} {% endfor %}

    {% endif %} - {% if campaign.ungluers.bibliophiles %} -

    Bibliophiles of this edition include ungluers:

    - {% for ungluer in campaign.ungluers.bibliophiles %} -
    {{ ungluer.username }}
    {{ ungluer.profile.tagline }}
    + {% if transactions.bibliophiles %} +

    Bibliophiles of this edition:

    + {% for transaction in transactions.bibliophiles %} +
    {% if transaction.ack_link %}{{ transaction.ack_name }}{% else %}{{ transaction.ack_name }}{% endif %}
    {{ transaction.ack_dedication }}
    {% endfor %}

    {% endif %} + {% endwith %}

    - You can say thank you by supporting the ungluing of more books at https://unglue.it/ + You can say thank you by supporting the ungluing of more books at https://unglue.it/ .

    diff --git a/frontend/templates/fund_the_pledge.html b/frontend/templates/fund_the_pledge.html new file mode 100644 index 00000000..d49a44c3 --- /dev/null +++ b/frontend/templates/fund_the_pledge.html @@ -0,0 +1,84 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}Fund Your Pledge{% endblock %} + +{% block extra_extra_head %} + + + + + + + +{% endblock %} + +{% block doccontent %} +
    + +
    +
    + +

    Funding Your Pledge

    +
    We're so happy that you've decided to {% if modified %}increase your pledge{% else %}join{% endif %} this campaign. + {% if nonprofit.is_on %} + We have two ways we can fund your pledge of ${{preapproval_amount}}. +
      +
    1. You can make a donation now to {{nonprofit.name}}, a non-profit charity that's working with Unglue.it to give books to the world.
    2. +
    3. You can give us your credit card information now; we'll charge your card only if the campaign succeeds.
    4. +
    + {% else %} + To fund your pledge of ${{preapproval_amount}}, you can give us your credit card information now; we'll charge your card only if the campaign succeeds. + {% endif %} +
    +
    + {% if nonprofit.is_on %} + + {% endif %} +
    +

    Pledge by Credit Card

    +

    Unglue.it uses Stripe to securely manage your Credit Card information. +

    + {% if request.user.credit.available %}

    Although you have ${{request.user.credit.available}} in donation credits, you can't support a campaign with a mixture of credit card pledges and donations.{% endif %} +

    + +
    + {% csrf_token %} + {{ form.non_field_errors }} + {{ form.as_p }} + + +
    +
    +
    + +
    +
    + + +{% endblock %} + + diff --git a/frontend/templates/manage_campaign.html b/frontend/templates/manage_campaign.html index c14f6145..33c36a6b 100644 --- a/frontend/templates/manage_campaign.html +++ b/frontend/templates/manage_campaign.html @@ -72,7 +72,7 @@ Please fix the following before launching your campaign:

    {{ work.author }}

    -

    {{ pubdate }}

    +

    {{ work.publication_date_year }}

    diff --git a/frontend/templates/nonprofit.html b/frontend/templates/nonprofit.html new file mode 100644 index 00000000..8f74c5f8 --- /dev/null +++ b/frontend/templates/nonprofit.html @@ -0,0 +1,48 @@ +{% extends "nonprofit_base.html" %} +{% load humanize %} + +{% block title %}Donate to {{nonprofit.name}}{% endblock %} + +{% block extra_extra_head %} + + + + + +{% endblock %} + +{% block doccontent %} +
    + +
    +
    + +

    Supporting {{ get.title }} through {{nonprofit.name}}

    +

    {{nonprofit.name}} is cooperating with Unglue.it to make books free to the world. On this page, you can make a donation to us, and we'll hold the money until the ungluing campaign you've selected succeeds. Then we'll make a payment to the rightsholder along side unglue.it. Once you've made the donation, we'll ad donation credits to your unglue.it account, {{get.username}}. +

    +

    + To fund the pledge of ${{get.preapproval_amount|intcomma}} that you've made at unglue.it, you'll need to make a donation of at least that amount. You can donate more than that of course- you'll get additional donation credits to use on other campaigns. The larger your donation, the less percentage-wise that gets eaten up by processing fees. +

    +

    Any interest we earn will be used for our public purpose of helping libraries remain relevant into the future. If your unglue.it donation credits don't get used for 5 years, we'll use it in the same way. Your donation to {{nonprofit.name}} is deductible as a Charitable donation in the US.

    +
    +
    + + +
    +
    +{% endblock %} + + diff --git a/frontend/templates/nonprofit_base.html b/frontend/templates/nonprofit_base.html new file mode 100644 index 00000000..9e1b29cf --- /dev/null +++ b/frontend/templates/nonprofit_base.html @@ -0,0 +1,200 @@ + + + + + + + + + +{% block title %}Join the Movement | Library Renewal{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% block extra_extra_head %}{% endblock %} + + + + + + + + +
    +
    + + + + +
    + +{% block doccontent %}{% endblock %} + +
    +
      +
    • Delicious
    • +
    • Digg
    • +
    • E-Mail
    • +
    • Facebook
    • +
    • Google Buzz
    • +
    • LinkedIn
    • +
    • StumbleUpon
    • +
    • Twitter
    • + +
    +
    + +
    + +
    +
    + + + + + + + +
    + +
    + + + +
    + +
    + + + + + + + + + + + + + + + + + + diff --git a/frontend/templates/notification/pledge_donation_credit/full.txt b/frontend/templates/notification/pledge_donation_credit/full.txt new file mode 100644 index 00000000..e82cbbcf --- /dev/null +++ b/frontend/templates/notification/pledge_donation_credit/full.txt @@ -0,0 +1,11 @@ +{% if amount > 0 %} +{{ user.username }}, {{ amount }} donation credits have been added to your unglue.it donation credit account. +{% else %} +{{ user.username }}, {{ minus_amount }} donation credits have been deducted from your unglue.it donation credit account. +{% endif %} +You have a balance of {{ user.credit.balance }} donation credits. +You have pledged {{ user.credit.pledged }} donation credits to ungluing campaigns. +You have {{ user.credit.available }} donation credits available to pledge or transfer. +You can manage your donation credit account at https://unglue.it/donation/ + +Donation credits can be used in support of ungluing campaigns. \ No newline at end of file diff --git a/frontend/templates/notification/pledge_donation_credit/notice.html b/frontend/templates/notification/pledge_donation_credit/notice.html new file mode 100644 index 00000000..a6972bcb --- /dev/null +++ b/frontend/templates/notification/pledge_donation_credit/notice.html @@ -0,0 +1,27 @@ +{% extends "notification/notice_template.html" %} + +{% block comments_graphical %} +{% if amount > 0 %} +{{ user.username }}, {{ amount }} donation credits have been added to your unglue.it donation credit account. +{% else %} +{{ user.username }}, {{ minus_amount }} donation credits have been deducted from your unglue.it donation credit account. +{% endif %} +{% endblock %} +{% block comments_textual %} +

    +You have a balance of {{ user.credit.balance }} donation credits. +

    +

    +You have pledged {{ user.credit.pledged }} donation credits to ungluing campaigns. +

    +

    +You have {{ user.credit.available }} donation credits available to pledge or transfer. +

    +

    +You can manage your donation credit account here +

    +

    + +Donation credits can be used in support of ungluing campaigns. +

    +{% endblock %} \ No newline at end of file diff --git a/frontend/templates/notification/pledge_donation_credit/short.txt b/frontend/templates/notification/pledge_donation_credit/short.txt new file mode 100644 index 00000000..d5917996 --- /dev/null +++ b/frontend/templates/notification/pledge_donation_credit/short.txt @@ -0,0 +1 @@ +{% if amount > 0 %}{{ amount }} donation credits have been added to your unglue.it donation credit account.{% else %}{{ minus_amount }} donation credits have been deducted from your unglue.it donation credit account.{% endif %} \ No newline at end of file diff --git a/frontend/templates/pledge.html b/frontend/templates/pledge.html index adf94005..e6274f7c 100644 --- a/frontend/templates/pledge.html +++ b/frontend/templates/pledge.html @@ -23,7 +23,7 @@

    {{ work.author }}

    -

    {{ pubdate }}

    +

    {{ work.publication_date_year }}

    @@ -52,8 +52,8 @@ {% if faqmenu == 'modify' %}

    You've already pledged to this campaign:

    - Amount: ${{preapproval_amount|intcomma}}.
    - Your premium: {% if premium_description %}{{ premium_description }}{% else %}You did not request a premium for this campaign.{% endif %}
    + Amount: ${{transaction.amount|intcomma}}.
    + Your premium: {% if transaction.premium %}{{ transaction.premium.description }}{% else %}You did not request a premium for this campaign.{% endif %}

    You can modify your pledge below.
    @@ -68,11 +68,6 @@ {{ form.non_field_errors }}
    {{ form.preapproval_amount.label_tag }}: {{ form.preapproval_amount.errors }}${{ form.preapproval_amount }}
    - {% comment %} - not supported yet; don't display - {{ form.anonymous.label_tag }}: {{ form.anonymous.errors }}{{ form.anonymous }} - {% endcomment %} -
    Choose your premium:
    - - - {% comment %} - When the pledge amount and premium are in an inconsistent state, the real button is disabled and (via css) hidden; instead we display this fake button with a helpful message. It's a button so we can reuse all the existing CSS for buttons, so that it looks like the real button has just changed in appearance. It's hidden and the other one un-disabled and un-hidden when the pledge & premium return to a correct state. People without javascript enabled will miss out on the front-end corrections but form validation will catch it. - {% endcomment %} +
    +
    Depending on your pledge amount, you'll also get these acknowledgements.
    +
    Any amount
    The unglued ebook will be delivered to your inbox.
    +
    $25+
    You'll be listed on the acknowledgements page of the unglued ebook. {{ form.ack_name.label_tag }} {{ form.ack_name.errors }}{{ form.ack_name }}
    {{ form.anonymous.label_tag }} {{ form.anonymous.errors }}{{ form.anonymous }}
    + +
    $100+
    Your acknowledgement can include a dedication (140 characters max). {{ form.ack_dedication.label_tag }} {{ form.ack_dedication.errors }}{{ form.ack_dedication }}
    +
    + + + {% comment %} + When the pledge amount and premium are in an inconsistent state, the real button is disabled and (via css) hidden; instead we display this fake button with a helpful message. It's a button so we can reuse all the existing CSS for buttons, so that it looks like the real button has just changed in appearance. It's hidden and the other one un-disabled and un-hidden when the pledge & premium return to a correct state. People without javascript enabled will miss out on the front-end corrections but form validation will catch it. + {% endcomment %} -
    (You will be sent to {{ payment_processor|capfirst }} -{% if faqmenu == 'modify' %}, if needed, to modify your pledge. We hope you won't, but of course you're also free to cancel your pledge{% else %} to complete your pledge.{% endif %}.)
    - -{% ifequal work.id 81724 %} - - - - -{% endifequal %} - +
    +{% if faqmenu == 'modify' %}We hope you won't, but of course you're also free to cancel your pledge.{% endif %}
    + +{% if transaction.ack_name %} + +{% else %} + +{% endif %} + + + {% endblock %} diff --git a/frontend/templates/pledge_complete.html b/frontend/templates/pledge_complete.html index ddb1ba7b..442c9af6 100644 --- a/frontend/templates/pledge_complete.html +++ b/frontend/templates/pledge_complete.html @@ -7,6 +7,8 @@ + + @@ -29,7 +31,14 @@

    Thank you!

    -

    You've just pledged ${{ transaction.amount|intcomma }} to {{ work.title }}. If it reaches its goal of ${{ campaign.target|intcomma }} by {{ campaign.deadline|date:"M d Y"}}, it will be unglued for all to enjoy.

    +

    You've just {% if modified %}modified your pledge for{% else %}pledged{% endif %} ${{ transaction.amount|intcomma }} to {{ work.title }}. If it reaches its goal of ${{ campaign.target|intcomma }} by {{ campaign.deadline|date:"M d Y"}}, it will be unglued for all to enjoy.

    +
    +
    + Amount: ${{transaction.amount|intcomma}}.
    + Your premium: {% if transaction.premium %}{{ transaction.premium.description }}{% else %}You did not request a premium for this campaign.{% endif %}
    +
    +
    +

    You can help even more by sharing this campaign with your friends:

    Copy/paste this into your site:
    diff --git a/frontend/templates/work.html b/frontend/templates/work.html index 25703094..31248ea7 100644 --- a/frontend/templates/work.html +++ b/frontend/templates/work.html @@ -130,7 +130,7 @@ $j(document).ready(function(){

    {{ work.author }}

    -

    {{ pubdate }}

    +

    {{ work.publication_date_year }}

    {% if status == 'ACTIVE' %} {% if pledged %} diff --git a/frontend/urls.py b/frontend/urls.py index 4a5421bb..d59392cd 100644 --- a/frontend/urls.py +++ b/frontend/urls.py @@ -3,12 +3,14 @@ from django.views.generic.simple import direct_to_template from django.views.generic.base import TemplateView from django.views.generic import ListView, DetailView from django.contrib.auth.decorators import login_required +from django.views.decorators.csrf import csrf_exempt from django.conf import settings from regluit.core.feeds import SupporterWishlistFeed from regluit.core.models import Campaign -from regluit.frontend.views import GoodreadsDisplayView, LibraryThingView, PledgeView, PledgeCompleteView, PledgeModifyView, PledgeCancelView, PledgeNeverMindView, PledgeRechargeView, FAQView -from regluit.frontend.views import CampaignListView, DonateView, WorkListView, UngluedListView, InfoPageView, InfoLangView +from regluit.frontend.views import GoodreadsDisplayView, LibraryThingView, PledgeView, PledgeCompleteView, PledgeCancelView, PledgeRechargeView, FAQView +from regluit.frontend.views import CampaignListView, WorkListView, UngluedListView, InfoPageView, InfoLangView, DonationView, FundPledgeView +from regluit.frontend.views import NonprofitCampaign, DonationCredit, PledgeModifiedView urlpatterns = patterns( "regluit.frontend.views", @@ -53,12 +55,16 @@ urlpatterns = patterns( url(r"^new_edition/(?P)(?P)$", "new_edition", name="new_edition"), url(r"^new_edition/(?P\d*)/(?P\d*)$", "new_edition", name="new_edition"), url(r"^googlebooks/(?P.+)/$", "googlebooks", name="googlebooks"), + url(r"^donation/$", login_required(DonationView.as_view()), name="donation"), + url(r"^donation/credit/(?P.+)/$", login_required(DonationCredit.as_view()), name="donation_credit"), url(r"^pledge/(?P\d+)/$", login_required(PledgeView.as_view()), name="pledge"), url(r"^pledge/cancel/(?P\d+)$", login_required(PledgeCancelView.as_view()), name="pledge_cancel"), url(r"^pledge/complete/$", login_required(PledgeCompleteView.as_view()), name="pledge_complete"), - url(r"^pledge/nevermind/$", login_required(PledgeNeverMindView.as_view()), name="pledge_nevermind"), - url(r"^pledge/modify/(?P\d+)$", login_required(PledgeModifyView.as_view()), name="pledge_modify"), + url(r"^pledge/modified/$", login_required(PledgeModifiedView.as_view()), name="pledge_modified"), + url(r"^pledge/modify/(?P\d+)$", login_required(PledgeView.as_view()), name="pledge_modify"), + url(r"^pledge/fund/(?P\d+)$", login_required(FundPledgeView.as_view()), name="fund_pledge"), url(r"^pledge/recharge/(?P\d+)$", login_required(PledgeRechargeView.as_view()), name="pledge_recharge"), + url(r"^donate_to_campaign/$", csrf_exempt(NonprofitCampaign.as_view()), name="nonprofit"), url(r"^subjects/$", "subjects", name="subjects"), url(r"^librarything/$", LibraryThingView.as_view(), name="librarything"), url(r"^librarything/load/$","librarything_load", name="librarything_load"), @@ -85,6 +91,5 @@ if settings.DEBUG: "regluit.frontend.views", url(r"^goodreads/$", login_required(GoodreadsDisplayView.as_view()), name="goodreads_display"), url(r"^goodreads/clear_wishlist/$","clear_wishlist", name="clear_wishlist"), - url(r"^donate/$", DonateView.as_view(), name="donate"), url(r"^celery/clear/$","clear_celery_tasks", name="clear_celery_tasks"), ) \ No newline at end of file diff --git a/frontend/views.py b/frontend/views.py index 232e4c87..4bd38d3e 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -17,6 +17,7 @@ import oauth2 as oauth from django import forms from django.conf import settings from django.contrib.auth.models import User +from django.core import signing from django.core.urlresolvers import reverse from django.core.exceptions import ObjectDoesNotExist from django.contrib import messages @@ -47,16 +48,19 @@ from regluit.frontend.forms import UserData, UserEmail, ProfileForm, CampaignPle from regluit.frontend.forms import RightsHolderForm, UserClaimForm, LibraryThingForm, OpenCampaignForm from regluit.frontend.forms import getManageCampaignForm, DonateForm, CampaignAdminForm, EmailShareForm, FeedbackForm from regluit.frontend.forms import EbookForm, CustomPremiumForm, EditManagersForm, EditionForm, PledgeCancelForm +from regluit.frontend.forms import getTransferCreditForm, CCForm from regluit.payment.manager import PaymentManager -from regluit.payment.models import Transaction -from regluit.payment.parameters import TARGET_TYPE_CAMPAIGN, TARGET_TYPE_DONATION, PAYMENT_TYPE_AUTHORIZATION -from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_CANCELED, TRANSACTION_STATUS_ERROR, TRANSACTION_STATUS_FAILED, TRANSACTION_STATUS_INCOMPLETE -from regluit.payment.paypal import Preapproval +from regluit.payment.models import Transaction, Account +from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_CANCELED, TRANSACTION_STATUS_ERROR, TRANSACTION_STATUS_FAILED, TRANSACTION_STATUS_INCOMPLETE, TRANSACTION_STATUS_NONE, TRANSACTION_STATUS_MODIFIED +from regluit.payment.parameters import PAYMENT_TYPE_AUTHORIZATION, PAYMENT_TYPE_INSTANT +from regluit.payment.parameters import PAYMENT_HOST_STRIPE +from regluit.payment.credit import credit_transaction from regluit.core import goodreads from tastypie.models import ApiKey -from regluit.payment.models import Transaction +from regluit.payment.models import Transaction, Sent, CreditLog from notification import models as notification +from regluit.payment import stripelib logger = logging.getLogger(__name__) @@ -154,6 +158,7 @@ def work(request, work_id, action='display'): except: pledged = None + logger.info("pledged: {0}".format(pledged)) countdown = "" try: @@ -185,11 +190,6 @@ def work(request, work_id, action='display'): if action == 'preview': work.last_campaign_status = 'ACTIVE' - try: - pubdate = work.publication_date[:4] - except IndexError: - pubdate = 'unknown' - if not request.user.is_anonymous(): claimform = UserClaimForm( request.user, data={'claim-work':work.pk, 'claim-user': request.user.id}, prefix = 'claim') for edition in editions: @@ -210,8 +210,9 @@ def work(request, work_id, action='display'): claimform = None if campaign: - # pull up premiums explicitly tied to the campaign or generic premiums - premiums = campaign.effective_premiums() + # pull up premiums explicitly tied to the campaign + # mandatory premiums are only displayed in pledge process + premiums = campaign.custom_premiums() else: premiums = None @@ -242,7 +243,6 @@ def work(request, work_id, action='display'): 'wishers': wishers, 'base_url': base_url, 'editions': editions, - 'pubdate': pubdate, 'pledged': pledged, 'activetab': activetab, 'alert': alert, @@ -416,11 +416,6 @@ def manage_campaign(request, id): new_premium_form = CustomPremiumForm(data={'campaign': campaign}) work = campaign.work - - try: - pubdate = work.publication_date[:4] - except IndexError: - pubdate = 'unknown' return render(request, 'manage_campaign.html', { 'campaign': campaign, @@ -429,7 +424,6 @@ def manage_campaign(request, id): 'alerts': alerts, 'premiums' : campaign.effective_premiums(), 'premium_form' : new_premium_form, - 'pubdate': pubdate, 'work': work, 'activetab': activetab, }) @@ -572,276 +566,351 @@ class CampaignListView(FilterableListView): context['ungluers'] = userlists.campaign_list_users(qs,5) context['facet'] =self.kwargs['facet'] return context + +class DonationView(TemplateView): + template_name = "donation.html" + + def get(self, request, *args, **kwargs): + context = self.get_context_data() + context['transfer_form']=getTransferCreditForm(self.request.user.credit.available) + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + context = self.get_context_data() + transfer_form=getTransferCreditForm(self.request.user.credit.available, data=self.request.POST) + if transfer_form.is_valid(): + if self.request.user.credit.transfer_to(transfer_form.cleaned_data['recipient'], transfer_form.cleaned_data['amount']): + #successful transfer + context['transfer_message'] = 'Your transfer has been successfully executed.' + context['recipient']= transfer_form.cleaned_data['recipient'] + context['transfer_amount'] = transfer_form.cleaned_data['amount'] + context['transfer_form']=getTransferCreditForm(self.request.user.credit.available) + else: + #unsuccessful transfer + context['transfer_message'] = 'Your transfer was not successful.' + context['transfer_form']=transfer_form + else: + #not valid + context['transfer_form']=transfer_form + return self.render_to_response(context) + + def get_context_data(self, *args, **kwargs): + context = {'user' : self.request.user,'nonprofit': settings.NONPROFIT} + context['donate_form'] = DonateForm(initial={'username':self.request.user.username}) + return context class PledgeView(FormView): template_name="pledge.html" form_class = CampaignPledgeForm - embedded = False + transaction = None + campaign = None + work = None + premiums = None + data = None - def get(self, request, *args, **kwargs): - # change the default behavior from https://code.djangoproject.com/browser/django/tags/releases/1.3.1/django/views/generic/edit.py#L129 - # don't automatically bind the data to the form on GET, only on POST - # compare with https://code.djangoproject.com/browser/django/tags/releases/1.3.1/django/views/generic/edit.py#L34 - form_class = self.get_form_class() - form = form_class() - - context_data = self.get_context_data(form=form) - # if there is already an active campaign pledge for user, redirect to the pledge modify page - if context_data.get('redirect_to_modify_pledge'): - work = context_data['work'] - return HttpResponseRedirect(reverse('pledge_modify', args=[work.id])) - else: - return self.render_to_response(context_data) - - def get_context_data(self, **kwargs): - """set up the pledge page""" - - # the following should be true since PledgeModifyView.as_view is wrapped in login_required - assert self.request.user.is_authenticated() - user = self.request.user - - context = super(PledgeView, self).get_context_data(**kwargs) - - work = get_object_or_404(models.Work, id=self.kwargs["work_id"]) - campaign = work.last_campaign() - - # if there is no campaign or if campaign is not active, we should raise an error - - if campaign is None or campaign.status != 'ACTIVE': - raise Http404 - - premiums = campaign.effective_premiums() - premium_id = self.request.REQUEST.get('premium_id', None) + def get_preapproval_amount(self): preapproval_amount = self.request.REQUEST.get('preapproval_amount', None) - - if premium_id is not None and preapproval_amount is None: + if preapproval_amount: + return preapproval_amount + premium_id = self.request.REQUEST.get('premium_id', None) + if premium_id != None: try: preapproval_amount = D(models.Premium.objects.get(id=premium_id).amount) except: preapproval_amount = None - - data = {'preapproval_amount':preapproval_amount, 'premium_id':premium_id} - - form_class = self.get_form_class() - - # no validation errors, please, when we're only doing a GET - # to avoid validation errors, don't bind the form - - if preapproval_amount is not None: - form = form_class(data) - else: - form = form_class() - - try: - pubdate = work.publication_date[:4] - except IndexError: - pubdate = 'unknown' - - context.update({ - 'redirect_to_modify_pledge':False, - 'work':work,'campaign':campaign, - 'premiums':premiums, 'form':form, - 'premium_id':premium_id, - 'faqmenu': 'pledge', - 'pubdate':pubdate, - 'payment_processor':settings.PAYMENT_PROCESSOR, - }) - - # check whether the user already has an ACTIVE transaction for the given campaign. - # if so, we should redirect the user to modify pledge page - # BUGBUG: but what about Completed Transactions? - transactions = campaign.transactions().filter(user=user, status=TRANSACTION_STATUS_ACTIVE) - if transactions.count() > 0: - context.update({'redirect_to_modify_pledge':True}) - else: - context.update({'redirect_to_modify_pledge':False}) + if self.transaction: + if preapproval_amount: + preapproval_amount = preapproval_amount if preapproval_amount>self.transaction.amount else self.transaction.amount + else: + preapproval_amount = self.transaction.amount + return preapproval_amount - return context - - def form_valid(self, form): - work_id = self.kwargs["work_id"] - preapproval_amount = form.cleaned_data["preapproval_amount"] - anonymous = form.cleaned_data["anonymous"] - - # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval - campaign = models.Work.objects.get(id=int(work_id)).last_campaign() - - premium_id = form.cleaned_data["premium_id"] - # confirm that the premium_id is a valid one for the campaign in question - try: - premium = models.Premium.objects.get(id=premium_id) - if not (premium.campaign is None or premium.campaign == campaign): - premium = None - except models.Premium.DoesNotExist, e: - premium = None - - p = PaymentManager(embedded=self.embedded) - - # PledgeView is wrapped in login_required -- so in theory, user should never be None -- but I'll keep this logic here for now. - if self.request.user.is_authenticated(): - user = self.request.user - else: - user = None - - if not self.embedded: - - return_url = None - nevermind_url = None - - # the recipients of this authorization is not specified here but rather by the PaymentManager. - # set the expiry date based on the campaign deadline - expiry = campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) - - paymentReason = "Unglue.it Pledge for {0}".format(campaign.name) - t, url = p.authorize('USD', TARGET_TYPE_CAMPAIGN, preapproval_amount, expiry=expiry, campaign=campaign, list=None, user=user, - return_url=return_url, nevermind_url=nevermind_url, anonymous=anonymous, premium=premium, - paymentReason=paymentReason) - else: # embedded view -- which we're not actively using right now. - # embedded view triggerws instant payment: send to the partnering RH - receiver_list = [{'email':settings.PAYPAL_NONPROFIT_PARTNER_EMAIL, 'amount':preapproval_amount}] - - return_url = None - nevermind_url = None - - t, url = p.pledge('USD', TARGET_TYPE_CAMPAIGN, receiver_list, campaign=campaign, list=None, user=user, - return_url=return_url, nevermind_url=nevermind_url, anonymous=anonymous, premium=premium) - - if url: - logger.info("PledgeView url: " + url) - return HttpResponseRedirect(url) - else: - logger.error("Attempt to produce transaction id {0} failed".format(t.id)) - return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") - -class PledgeModifyView(FormView): - """ - A view to handle request to change an existing pledge - """ - - template_name="pledge.html" - form_class = CampaignPledgeForm - embedded = False - - def get_context_data(self, **kwargs): - - context = super(PledgeModifyView, self).get_context_data(**kwargs) - - # the following should be true since PledgeModifyView.as_view is wrapped in login_required + def get_form_kwargs(self): assert self.request.user.is_authenticated() - user = self.request.user - - work = get_object_or_404(models.Work, id=self.kwargs["work_id"]) + self.work = get_object_or_404(models.Work, id=self.kwargs["work_id"]) + # if there is no campaign or if campaign is not active, we should raise an error try: - campaign = work.last_campaign() - premiums = campaign.effective_premiums() - - # which combination of campaign and transaction status required? + self.campaign = self.work.last_campaign() + # TODO need to sort the premiums + self.premiums = self.campaign.custom_premiums() | models.Premium.objects.filter(id=150) # Campaign must be ACTIVE - assert campaign.status == 'ACTIVE' - - transactions = campaign.transactions().filter(user=user, status=TRANSACTION_STATUS_ACTIVE) - assert transactions.count() == 1 - transaction = transactions[0] - assert transaction.type == PAYMENT_TYPE_AUTHORIZATION and transaction.status == TRANSACTION_STATUS_ACTIVE - + assert self.campaign.status == 'ACTIVE' except Exception, e: raise e - - # what stuff do we need to pull out to populate form? - # preapproval_amount, premium_id (which we don't have stored yet) - if transaction.premium is not None: - premium_id = transaction.premium.id - premium_description = transaction.premium.description + + transactions = self.campaign.transactions().filter(user=self.request.user, status=TRANSACTION_STATUS_ACTIVE, type=PAYMENT_TYPE_AUTHORIZATION) + premium_id = self.request.REQUEST.get('premium_id', None) + if transactions.count() == 0: + ack_name='' + ack_dedication='' + anonymous='' else: - premium_id = None - premium_description = None + self.transaction = transactions[0] + if premium_id == None and self.transaction.premium is not None: + premium_id = self.transaction.premium.id + ack_name=self.transaction.ack_name + ack_dedication=self.transaction.ack_dedication + anonymous=self.transaction.anonymous + + self.data = {'preapproval_amount':self.get_preapproval_amount(), 'premium_id':premium_id, + 'ack_name':ack_name, 'ack_dedication':ack_dedication, 'anonymous':anonymous} + if self.request.method == 'POST': + self.data.update(self.request.POST.dict()) + return {'data':self.data} + else: + return {'initial':self.data} - # is there a Transaction for an ACTIVE campaign for this - # should make sure Transaction is modifiable. + def get_context_data(self, **kwargs): + """set up the pledge page""" - preapproval_amount = transaction.amount - data = {'preapproval_amount':preapproval_amount, 'premium_id':premium_id} - - # initialize form with the current state of the transaction if the current values empty - form = kwargs['form'] - - if not(form.is_bound): - form_class = self.get_form_class() - form = form_class(initial=data) - + context = super(PledgeView, self).get_context_data(**kwargs) + context.update({ - 'work':work, - 'campaign':campaign, - 'premiums':premiums, - 'form':form, - 'preapproval_amount':preapproval_amount, - 'premium_id':premium_id, - 'premium_description': premium_description, - 'faqmenu': 'modify', - 'tid': transaction.id, - 'payment_processor':settings.PAYMENT_PROCESSOR, - }) + 'work':self.work, + 'campaign':self.campaign, + 'premiums':self.premiums, + 'premium_id':self.data['premium_id'], + 'faqmenu': 'modify' if self.transaction else 'pledge', + 'transaction': self.transaction, + 'tid': self.transaction.id if self.transaction else None, + }) + return context - - def form_invalid(self, form): - logger.info("form.non_field_errors: {0}".format(form.non_field_errors())) - response = self.render_to_response(self.get_context_data(form=form)) - return response - def form_valid(self, form): + # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval - # What are the situations we need to deal with? - # 2 main situations: if the new amount is less than max_amount, no need to go out to PayPal again - # if new amount is greater than max_amount...need to go out and get new approval. - # to start with, we can use the standard pledge_complete, pledge_cancel machinery - # might have to modify the pledge_complete, pledge_cancel because the messages are going to be - # different because we're modifying a pledge rather than a new one. - - work_id = self.kwargs["work_id"] - preapproval_amount = form.cleaned_data["preapproval_amount"] - anonymous = form.cleaned_data["anonymous"] - - assert self.request.user.is_authenticated() - user = self.request.user - - # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval - campaign = models.Work.objects.get(id=int(work_id)).last_campaign() - assert campaign.status == 'ACTIVE' - - premium_id = form.cleaned_data["premium_id"] - # confirm that the premium_id is a valid one for the campaign in question - try: - premium = models.Premium.objects.get(id=premium_id) - if not (premium.campaign is None or premium.campaign == campaign): - premium = None - except models.Premium.DoesNotExist, e: - premium = None - - transactions = campaign.transactions().filter(user=user, status=TRANSACTION_STATUS_ACTIVE) - assert transactions.count() == 1 - transaction = transactions[0] - assert transaction.type == PAYMENT_TYPE_AUTHORIZATION and transaction.status == TRANSACTION_STATUS_ACTIVE - - p = PaymentManager(embedded=self.embedded) - paymentReason = "Unglue.it Pledge for {0}".format(campaign.name) - status, url = p.modify_transaction(transaction=transaction, amount=preapproval_amount, premium=premium, - paymentReason=paymentReason) - - logger.info("status: {0}, url:{1}".format(status, url)) - - if status and url is not None: - logger.info("PledgeModifyView paypal: " + url) - return HttpResponseRedirect(url) - elif status and url is None: - # let's use the pledge_complete template for now and maybe look into customizing it. - return HttpResponseRedirect("{0}?tid={1}".format(reverse('pledge_complete'), transaction.id)) + p = PaymentManager() + if self.transaction: + # modifying the transaction... + assert self.transaction.type == PAYMENT_TYPE_AUTHORIZATION and self.transaction.status == TRANSACTION_STATUS_ACTIVE + status, url = p.modify_transaction(self.transaction, form.cleaned_data["preapproval_amount"], + paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), + pledge_extra=form.pledge_extra + ) + logger.info("status: {0}, url:{1}".format(status, url)) + + if status and url is not None: + logger.info("PledgeView (Modify): " + url) + return HttpResponseRedirect(url) + elif status and url is None: + return HttpResponseRedirect("{0}?tid={1}".format(reverse('pledge_modified'), self.transaction.id)) + else: + return HttpResponse("No modification made") else: - return HttpResponse("No modification made") + t, url = p.process_transaction('USD', form.cleaned_data["preapproval_amount"], + host = None, + campaign=self.campaign, + user=self.request.user, + paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), + pledge_extra=form.pledge_extra + ) + if url: + logger.info("PledgeView url: " + url) + return HttpResponseRedirect(url) + else: + logger.error("Attempt to produce transaction id {0} failed".format(t.id)) + return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") +class FundPledgeView(FormView): + template_name="fund_the_pledge.html" + form_class = CCForm + transaction = None + def get_form_kwargs(self): + kwargs = super(FundPledgeView, self).get_form_kwargs() + + assert self.request.user.is_authenticated() + if self.transaction is None: + self.transaction = get_object_or_404(Transaction, id=self.kwargs["t_id"]) + + if kwargs.has_key('data'): + data = kwargs['data'].copy() + else: + data = {} + + data.update( + {'preapproval_amount':self.transaction.max_amount, + 'username':self.request.user.username, + 'work_id':self.transaction.campaign.work.id, + 'title':self.transaction.campaign.work.title} + ) + + kwargs['data'] = data + + return kwargs + def get_context_data(self, **kwargs): + context = super(FundPledgeView, self).get_context_data(**kwargs) + context['modified'] = self.transaction.status==TRANSACTION_STATUS_MODIFIED + context['preapproval_amount']=self.transaction.max_amount + context['needed'] = self.transaction.max_amount - self.request.user.credit.available + context['transaction']=self.transaction + context['nonprofit'] = settings.NONPROFIT + context['STRIPE_PK'] = stripelib.STRIPE_PK + # note that get_form_kwargs() will already have been called once + donate_args=self.get_form_kwargs() + donate_args['data']['preapproval_amount']=context['needed'] + context['donate_form'] = DonateForm(**donate_args) + return context + + def post(self, request, *args, **kwargs): + logger.info('request.POST: {0}'.format(request.POST)) + return super(FundPledgeView, self).post(request, *args, **kwargs) + + def form_valid(self, form): + """ note desire to pledge; make sure there is a credit card to charge""" + + # first pass -- we have a token -- also do more direct coupling to stripelib -- then move to + # abstraction of payment.manager / payment.baseprocessor + + # demonstrate two possibilities: 1) token -> charge or 2) token->customer->charge + + stripe_token = form.cleaned_data["stripe_token"] + preapproval_amount = form.cleaned_data["preapproval_amount"] + retain_cc_info = form.cleaned_data["retain_cc_info"] + + sc = stripelib.StripeClient() + + # let's figure out what part of transaction can be used to store info + # try placing charge id in transaction.pay_key + # need to set amount + # how does max_amount get set? -- coming from /pledge/xxx/? + # max_amount is set -- but I don't think we need it for stripe + + if retain_cc_info: + # create customer and charge id and then charge the customer + customer = sc.create_customer(card=stripe_token, description=self.request.user.username, + email=self.request.user.email) + + account = Account(host = PAYMENT_HOST_STRIPE, + account_id = customer.id, + card_last4 = customer.active_card.last4, + card_type = customer.active_card.type, + card_exp_month = customer.active_card.exp_month, + card_exp_year = customer.active_card.exp_year, + card_fingerprint = customer.active_card.fingerprint, + card_country = customer.active_card.country, + user = self.request.user + ) + + account.save() + + charge = sc.create_charge(preapproval_amount, customer=customer, description="${0} for test / retain cc".format(preapproval_amount)) + + else: + customer = None + + charge = sc.create_charge(preapproval_amount, card=stripe_token, description="${0} for test / cc not retained".format(preapproval_amount)) + + # set True for now -- wondering whether we should actually wait for a webhook -- don't think so. + + ## settings to apply to transaction for TRANSACTION_STATUS_COMPLETE + #self.transaction.type = PAYMENT_TYPE_INSTANT + #self.transaction.approved = True + #self.transaction.status = TRANSACTION_STATUS_COMPLETE + #self.transaction.pay_key = charge.id + + # settings to apply to transaction for TRANSACTION_STATUS_ACTIVE + # should approved be set to False and wait for a webhook? + self.transaction.type = PAYMENT_TYPE_AUTHORIZATION + self.transaction.approved = True + self.transaction.status = TRANSACTION_STATUS_ACTIVE + self.transaction.preapproval_key = charge.id + + self.transaction.currency = 'USD' + self.transaction.amount = preapproval_amount + self.transaction.date_payment = now() + + self.transaction.save() + + return HttpResponse("charge id: {0} / customer: {1}".format(charge.id, customer)) + + +class NonprofitCampaign(FormView): + template_name="nonprofit.html" + form_class = CCForm + + def get_context_data(self, **kwargs): + context = super(NonprofitCampaign, self).get_context_data(**kwargs) + context['nonprofit'] = settings.NONPROFIT + context['get'] = self.request.GET + return context + + def get_form_kwargs(self): + if self.request.method == 'POST': + return {'data':self.request.POST} + else: + return {'initial':self.request.GET } + + + + def form_valid(self, form): + username=form.cleaned_data['username'] + forward={'username':username} + forward['work_id']= form.cleaned_data['work_id'] + amount=form.cleaned_data['preapproval_amount'] + forward['cents']=int(100*(amount-int(amount))) + forward['amount']= int(amount) + forward['sent']= Sent.objects.create(user=username,amount=form.cleaned_data['preapproval_amount']).pk + token=signing.dumps(forward) + return HttpResponseRedirect(settings.BASE_URL + reverse('donation_credit',kwargs={'token':token})) + +class DonationCredit(TemplateView): + template_name="donation_credit.html" + + def get_context_data(self, **kwargs): + context = super(DonationCredit, self).get_context_data(**kwargs) + context['faqmenu']="donation" + context['nonprofit'] = settings.NONPROFIT + try: + envelope=signing.loads(kwargs['token']) + context['envelope']=envelope + except signing.BadSignature: + self.template_name="donation_error.html" + return context + try: + work = models.Work.objects.get(id=envelope['work_id']) + campaign=work.last_campaign() + except models.Work.DoesNotExist: + campaign = None + context['work']=work + try: + user = User.objects.get(username=envelope['username']) + except User.DoesNotExist: + self.template_name="donation_user_error.html" + context['error']='user does not exist' + return context + if user != self.request.user: + self.template_name="donation_user_error.html" + context['error']='wrong user logged in' + return context + try: + # check token not used + CreditLog.objects.get(sent=envelope['sent']) + context['error']='credit already registered' + return context + except CreditLog.DoesNotExist: + #not used yet! + amount=envelope['amount']+envelope['cents']/D(100) + CreditLog.objects.create(user=user,amount=amount,action='deposit',sent=envelope['sent']) + ts=Transaction.objects.filter(user=user,campaign=campaign,status=TRANSACTION_STATUS_NONE).order_by('-pk') + if ts.count()==0: + ts=Transaction.objects.filter(user=user,campaign=campaign,status=TRANSACTION_STATUS_MODIFIED).order_by('-pk') + if ts.count()>0: + t=ts[0] + credit_transaction(t,user, amount) + for t in ts[1:]: + t.status=TRANSACTION_STATUS_CANCELED + t.save() + context['transaction']=t + return context + else: + user.credit.add_to_balance(amount) + return context + + class PledgeRechargeView(TemplateView): """ a view to allow for recharge of a transaction for failed transactions or ones with errors @@ -852,7 +921,7 @@ class PledgeRechargeView(TemplateView): context = super(PledgeRechargeView, self).get_context_data(**kwargs) - # the following should be true since PledgeModifyView.as_view is wrapped in login_required + # the following should be true since PledgeView.as_view is wrapped in login_required assert self.request.user.is_authenticated() user = self.request.user @@ -870,16 +939,11 @@ class PledgeRechargeView(TemplateView): nevermind_url = None if transaction is not None: - # the recipients of this authorization is not specified here but rather by the PaymentManager. - # set the expiry date based on the campaign deadline - expiry = campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) - + # the recipients of this authorization is not specified here but rather by the PaymentManager. paymentReason = "Unglue.it Recharge for {0}".format(campaign.name) - p = PaymentManager(embedded=False) - t, url = p.authorize('USD', TARGET_TYPE_CAMPAIGN, transaction.amount, expiry=expiry, campaign=campaign, list=None, user=user, - return_url=return_url, nevermind_url=nevermind_url, anonymous=transaction.anonymous, premium=transaction.premium, - paymentReason=paymentReason) + p = PaymentManager() + t, url = p.authorize(transaction, return_url=return_url, paymentReason=paymentReason) logger.info("Recharge url: {0}".format(url)) else: url = None @@ -900,11 +964,6 @@ class PledgeCompleteView(TemplateView): after pledging, supporter receives email including thanks, work pledged, amount, expiry date, any next steps they should expect; others? study other confirmation emails for their contents - after pledging, supporters are returned to a thank-you screen - should have prominent "thank you" or "congratulations" message - should have prominent share options - should suggest other works for supporters to explore (on what basis?) - link to work page? or to page on which supporter entered the process? (if the latter, how does that work with widgets?) should note that a confirmation email has been sent to $email from $sender should briefly note next steps (e.g. if this campaign succeeds you will be emailed on date X) @@ -915,10 +974,6 @@ class PledgeCompleteView(TemplateView): def get_context_data(self): # pick up all get and post parameters and display context = super(PledgeCompleteView, self).get_context_data() - - output = "pledge complete" - output += self.request.method + "\n" + str(self.request.REQUEST.items()) - context["output"] = output if self.request.user.is_authenticated(): user = self.request.user @@ -938,12 +993,8 @@ class PledgeCompleteView(TemplateView): work = None # we need to check whether the user tied to the transaction is indeed the authenticated user. - - correct_user = False try: - if user.id == transaction.user.id: - correct_user = True - else: + if user.id != transaction.user.id: # should be 403 -- but let's try 404 for now -- 403 exception coming in Django 1.4 raise Http404 except Exception, e: @@ -961,7 +1012,7 @@ class PledgeCompleteView(TemplateView): correct_transaction_type = False # add the work corresponding to the Transaction on the user's wishlist if it's not already on the wishlist - if user is not None and correct_user and correct_transaction_type and (campaign is not None) and (work is not None): + if user is not None and correct_transaction_type and (campaign is not None) and (work is not None): # ok to overwrite Wishes.source? user.wishlist.add_work(work, 'pledging') @@ -970,8 +1021,6 @@ class PledgeCompleteView(TemplateView): works2 = worklist[4:8] context["transaction"] = transaction - context["correct_user"] = correct_user - context["correct_transaction_type"] = correct_transaction_type context["work"] = work context["campaign"] = campaign context["faqmenu"] = "complete" @@ -980,7 +1029,12 @@ class PledgeCompleteView(TemplateView): context["site"] = Site.objects.get_current() return context - + +class PledgeModifiedView(PledgeCompleteView): + def get_context_data(self): + context = super(PledgeModifiedView, self).get_context_data() + context['modified']=True + return context class PledgeCancelView(FormView): """A view for allowing a user to cancel the active transaction for specified campaign""" @@ -1074,114 +1128,7 @@ class PledgeCancelView(FormView): return HttpResponse("Our attempt to cancel your transaction failed. We have logged this error.") except Exception, e: logger.error("Exception from attempt to cancel pledge for campaign id {0} for username {1}: {2}".format(campaign_id, user.username, e)) - return HttpResponse("Sorry, something went wrong in canceling your campaign pledge. We have logged this error.") - - -class PledgeNeverMindView(TemplateView): - """A callback for PayPal to tell unglue.it that a payment transaction has been canceled by the user""" - template_name="pledge_nevermind.html" - - def get_context_data(self): - context = super(PledgeNeverMindView, self).get_context_data() - - if self.request.user.is_authenticated(): - user = self.request.user - else: - user = None - - # pull out the transaction id and try to get the corresponding Transaction - transaction_id = self.request.REQUEST.get("tid") - transaction = Transaction.objects.get(id=transaction_id) - - # work and campaign in question - try: - campaign = transaction.campaign - work = campaign.work - except Exception, e: - campaign = None - work = None - - # we need to check whether the user tied to the transaction is indeed the authenticated user. - - correct_user = False - try: - if user.id == transaction.user.id: - correct_user = True - except Exception, e: - pass - - # check that the user had not already approved the transaction - # do we need to first run PreapprovalDetails to check on the status - - # is it of type=PAYMENT_TYPE_AUTHORIZATION and status is NONE or ACTIVE (but approved is false) - - if transaction.type == PAYMENT_TYPE_AUTHORIZATION: - correct_transaction_type = True - else: - correct_transaction_type = False - - # status? - - # give the user an opportunity to approved the transaction again - # provide a URL to click on. - # https://www.sandbox.paypal.com/?cmd=_ap-preapproval&preapprovalkey=PA-6JV656290V840615H - try_again_url = '%s?cmd=_ap-preapproval&preapprovalkey=%s' % (settings.PAYPAL_PAYMENT_HOST, transaction.preapproval_key) - - context["transaction"] = transaction - context["correct_user"] = correct_user - context["correct_transaction_type"] = correct_transaction_type - context["try_again_url"] = try_again_url - context["work"] = work - context["campaign"] = campaign - context["faqmenu"] = "cancel" - - return context - - -class DonateView(FormView): - template_name="donate.html" - form_class = DonateForm - embedded = False - - #def get_context_data(self, **kwargs): - # context = super(DonateView, self).get_context_data(**kwargs) - # - # form = CampaignPledgeForm(data) - # - # context.update({'work':work,'campaign':campaign, 'premiums':premiums, 'form':form, 'premium_id':premium_id}) - # return context - - def form_valid(self, form): - donation_amount = form.cleaned_data["donation_amount"] - anonymous = form.cleaned_data["anonymous"] - - # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval - campaign = None - - p = PaymentManager(embedded=self.embedded) - - # we should force login at this point -- or if no account, account creation, login, and return to this spot - if self.request.user.is_authenticated(): - user = self.request.user - else: - user = None - - # instant payment: send to the partnering RH - receiver_list = [{'email':settings.PAYPAL_NONPROFIT_PARTNER_EMAIL, 'amount':donation_amount}] - - #redirect the page back to campaign page on success - return_url = self.request.build_absolute_uri(reverse('donate')) - - t, url = p.pledge('USD', TARGET_TYPE_DONATION, receiver_list, campaign=campaign, list=None, user=user, - return_url=return_url, anonymous=anonymous) - - if url: - return HttpResponseRedirect(url) - else: - response = t.reference - logger.info("PledgeView paypal: Error " + str(t.reference)) - return HttpResponse(response) - + return HttpResponse("Sorry, something went wrong in canceling your campaign pledge. We have logged this error.") def claim(request): if request.method == 'GET': @@ -2101,4 +2048,5 @@ def download(request, work_id): def about(request, facet): template = "about_" + facet + ".html" - return render(request, template) \ No newline at end of file + return render(request, template) + diff --git a/payment/amazon.py b/payment/amazon.py deleted file mode 100644 index 0349f075..00000000 --- a/payment/amazon.py +++ /dev/null @@ -1,870 +0,0 @@ -from regluit.payment.parameters import * -from django.core.urlresolvers import reverse -from django.conf import settings -from regluit.payment.models import Transaction, PaymentResponse -from boto.fps.connection import FPSConnection -from django.http import HttpResponse, HttpRequest, HttpResponseRedirect, HttpResponseBadRequest, HttpResponseForbidden -from datetime import timedelta -from regluit.utils.localdatetime import now, zuluformat -from boto import handler -from boto.resultset import ResultSet -from boto.exception import FPSResponseError -import xml.sax - -import traceback -import datetime -import logging -import urlparse -import time -import urllib - -logger = logging.getLogger(__name__) - -AMAZON_STATUS_SUCCESS_ABT = 'SA' -AMAZON_STATUS_SUCCESS_ACH = 'SB' -AMAZON_STATUS_SUCCESS_CREDIT = 'SC' -AMAZON_STATUS_ERROR = 'SE' -AMAZON_STATUS_ADBANDONED = 'A' -AMAZON_STATUS_EXCEPTION = 'CE' -AMAZON_STATUS_PAYMENT_MISMATCH = 'PE' -AMAZON_STATUS_INCOMPLETE = 'NP' -AMAZON_STATUS_NOT_REGISTERED = 'NM' - -AMAZON_STATUS_CANCELED = 'Canceled' -AMAZON_STATUS_FAILURE = 'Failure' -AMAZON_STATUS_PENDING = 'Pending' -AMAZON_STATUS_RESERVED = 'Reserved' -AMAZON_STATUS_SUCCESS = 'Success' - -AMAZON_IPN_STATUS_CANCELED = 'CANCELED' -AMAZON_IPN_STATUS_FAILURE = 'FAILURE' -AMAZON_IPN_STATUS_PENDING = 'PENDING' -AMAZON_IPN_STATUS_RESERVED = 'RESERVED' -AMAZON_IPN_STATUS_SUCCESS = 'SUCCESS' - -AMAZON_NOTIFICATION_TYPE_STATUS = 'TransactionStatus' -AMAZON_NOTIFICATION_TYPE_CANCEL = 'TokenCancellation' - -AMAZON_OPERATION_TYPE_PAY = 'PAY' -AMAZON_OPERATION_TYPE_REFUND = 'REFUND' -AMAZON_OPERATION_TYPE_CANCEL = 'CANCEL' - -# load FPS_ACCESS_KEY and FPS_SECRET_KEY from the database if possible -try: - from regluit.core.models import Key - FPS_ACCESS_KEY = Key.objects.get(name="FPS_ACCESS_KEY").value - FPS_SECRET_KEY = Key.objects.get(name="FPS_SECRET_KEY").value - logger.info('Successful loading of FPS_*_KEYs') -except Exception, e: - logger.info('EXCEPTION: unsuccessful loading of FPS_*_KEYs: {0}'.format(e)) - -def get_ipn_url(): - - if settings.IPN_SECURE_URL: - return settings.BASE_URL_SECURE + reverse('HandleIPN', args=["amazon"]) - else: - return settings.BASE_URL + reverse('HandleIPN', args=["amazon"]) - -def ProcessIPN(request): - ''' - IPN handler for amazon. Here is a litle background on amazon IPNS - http://docs.amazonwebservices.com/AmazonFPS/latest/FPSAdvancedGuide/APPNDX_IPN.html - - notificationType: Can either be TransactionStatus of TokenCancellation - status: One of the defined IPN status codes - operation: The type of operation - callerReference: The reference to find the transaction - - The IPN is called for the following cases: - - A payment or reserve succeeds - A payment or reserve fails - A payment or reserve goes into a pending state - A reserved payment is settled successfully - A reserved payment is not settled successfully - A refund succeeds - A refund fails - A refund goes into a pending state - A payment is canceled - A reserve is canceled - A token is canceled successfully - - ''' - try: - logging.debug("Amazon IPN called") - logging.debug(request.POST) - - uri = request.build_absolute_uri() - parsed_url = urlparse.urlparse(uri) - - connection = FPSConnection(FPS_ACCESS_KEY, FPS_SECRET_KEY, host=settings.AMAZON_FPS_HOST) - - # Check the validity of the IPN - resp = connection.verify_signature("%s://%s%s" %(parsed_url.scheme, - parsed_url.netloc, - parsed_url.path), - request.raw_post_data) - - if not resp[0].VerificationStatus == "Success": - # Error, ignore this IPN - logging.error("Amazon IPN cannot be verified with post data: ") - logging.error(request.raw_post_data) - return HttpResponseForbidden() - - logging.debug("Amazon IPN post data:") - logging.debug(request.POST) - - reference = request.POST['callerReference'] - type = request.POST['notificationType'] - - # In the case of cancelling a token, there is no transaction, so this info is not set - transactionId = request.POST.get('transactionId', None) - date = request.POST.get('transactionDate', None) - operation = request.POST.get('operation', None) - status = request.POST.get('transactionStatus', None) - - logging.info("Received Amazon IPN with the following data:") - logging.info("type = %s" % type) - logging.info("operation = %s" % operation) - logging.info("reference = %s" % reference) - logging.info("status = %s" % status) - - # We should always find the transaction by the token - transaction = Transaction.objects.get(secret=reference) - - if type == AMAZON_NOTIFICATION_TYPE_STATUS: - - - # status update for the token, save the actual value - transaction.local_status = status - - # Now map our local status to the global status codes - if operation == AMAZON_OPERATION_TYPE_PAY: - - - if status == AMAZON_IPN_STATUS_SUCCESS: - transaction.status = TRANSACTION_STATUS_COMPLETE - - elif status == AMAZON_IPN_STATUS_PENDING: - - if transaction.status == TRANSACTION_STATUS_CREATED: - # - # Per the amazon documentation: - # If your IPN receiving service is down for some time, it is possible that our retry mechanism will deliver the IPNs out of order. - # If you receive an IPN for TransactionStatus (IPN), as SUCCESS or FAILURE or RESERVED, - # then after that time ignore any IPN that gives the PENDING status for the transaction - # - transaction.status = TRANSACTION_STATUS_PENDING - else: - transaction.status = TRANSACTION_STATUS_ERROR - - elif operation == AMAZON_OPERATION_TYPE_REFUND: - - if status == AMAZON_IPN_STATUS_SUCCESS: - transaction.status = TRANSACTION_STATUS_REFUNDED - elif status == AMAZON_IPN_STATUS_PENDING: - transaction.status = TRANSACTION_STATUS_PENDING - else: - transaction.status = TRANSACTION_STATUS_ERROR - - elif operation == AMAZON_OPERATION_TYPE_CANCEL: - - if status == AMAZON_IPN_STATUS_SUCCESS: - transaction.status = TRANSACTION_STATUS_COMPLETE - else: - transaction.status = TRANSACTION_STATUS_ERROR - - - elif type == AMAZON_NOTIFICATION_TYPE_CANCEL: - # - # The cancel IPN does not have a transaction ID or transaction status, so make them up - # - transaction.local_status = AMAZON_IPN_STATUS_CANCELED - transaction.status = TRANSACTION_STATUS_CANCELED - status = AMAZON_IPN_STATUS_CANCELED - - - transaction.save() - - # - # This is currently not done in paypal land, but log this IPN since the amazon IPN has good info - # - PaymentResponse.objects.create(api="IPN", - correlation_id = transactionId, - timestamp = date, - info = str(request.POST), - status=status, - transaction=transaction) - - return HttpResponse("Complete") - - except: - traceback.print_exc() - return HttpResponseForbidden() - - -def amazonPaymentReturn(request): - ''' - This is the complete view called after the co-branded API completes. It is called whenever the user - approves a preapproval or a pledge. This URL is set via the PAY api. - ''' - try: - transaction = None - - # pick up all get and post parameters and display - output = "payment complete" - output += request.method + "\n" + str(request.REQUEST.items()) - - - signature = request.GET['signature'] - status = request.GET['status'] - reference = request.GET['callerReference'] - token = request.GET['tokenID'] - - # validate the signature - - uri = request.build_absolute_uri() - parsed_url = urlparse.urlparse(uri) - - connection = FPSConnection(FPS_ACCESS_KEY, FPS_SECRET_KEY, host=settings.AMAZON_FPS_HOST) - - # Check the validity of the IPN - resp = connection.verify_signature("%s://%s%s" %(parsed_url.scheme, - parsed_url.netloc, - parsed_url.path), - urllib.urlencode(request.GET)) - - if not resp[0].VerificationStatus == "Success": - # Error, ignore this - logging.error("amazonPaymentReturn cannot be verified with get data: ") - logging.error(request.GET) - return HttpResponseForbidden() - - logging.debug("amazonPaymentReturn sig verified:") - logging.debug(request.GET) - - # validation of signature ok - # Find the transaction by reference, there should only be one - try: - transaction = Transaction.objects.get(secret=reference) - except: - logging.info("transaction with secret {0}".format(reference)) - return HttpResponseForbidden() - - logging.info("Amazon Co-branded Return URL called for transaction id: %d" % transaction.id) - logging.info(request.GET) - - # - # BUGBUG, for now lets map amazon status code to paypal, just to keep things uninform - # - if transaction.type == PAYMENT_TYPE_INSTANT: - # Instant payments need to be executed now - - # Log the authorize transaction - r = PaymentResponse.objects.create(api="Authorize", - correlation_id = "None", - timestamp = str(datetime.datetime.now()), - info = str(request.GET), - status=status, - transaction=transaction) - - if status == AMAZON_STATUS_SUCCESS_ABT or status == AMAZON_STATUS_SUCCESS_ACH or status == AMAZON_STATUS_SUCCESS_CREDIT: - # The above status code are unique to the return URL and are different than the pay API codes - - # Store the token, we need this for the IPN. - transaction.pay_key = token - - # - # BUGBUG, need to handle multiple recipients - # Send the pay request now to ourselves - # - e = Execute(transaction=transaction) - - if e.success() and not e.error(): - # Success case, save the ID. Our IPN will update the status - print "Amazon Execute returned succesfully" - - else: - logging.error("Amazon payment execution failed: ") - logging.error(e.envelope()) - transaction.status = TRANSACTION_STATUS_ERROR - - # Log the pay transaction - r = PaymentResponse.objects.create(api="Pay", - correlation_id = e.correlation_id(), - timestamp = e.timestamp(), - info = e.envelope(), - status = e.status, - transaction=transaction) - - else: - # We may never see an IPN, set the status here - logging.error("Amazon payment authorization failed: ") - logging.error(request.GET) - transaction.status = TRANSACTION_STATUS_ERROR - - - elif transaction.type == PAYMENT_TYPE_AUTHORIZATION: - # - # Future payments, we only need to store the token. The authorization was requested with the default expiration - # date set in our settings. When we are ready, we can call execute on this - # - transaction.local_status = status - - if status == AMAZON_STATUS_SUCCESS_ABT or status == AMAZON_STATUS_SUCCESS_ACH or status == AMAZON_STATUS_SUCCESS_CREDIT: - - # The above status code are unique to the return URL and are different than the pay API codes - transaction.status = TRANSACTION_STATUS_ACTIVE - transaction.approved = True - transaction.pay_key = token - transaction.save() - - print "Calling CANCEL RELATED" - - # clear out any other active transactions for this user and this campaign - from regluit.payment.manager import PaymentManager - p = PaymentManager() - p.cancel_related_transaction(transaction, status=TRANSACTION_STATUS_ACTIVE, campaign=transaction.campaign) - - else: - # We may never see an IPN, set the status here - transaction.status = TRANSACTION_STATUS_ERROR - - # Log the trasaction - r = PaymentResponse.objects.create(api="Authorize", - correlation_id = "None", - timestamp = str(datetime.datetime.now()), - info = str(request.GET), - status = status, - transaction=transaction) - - else: - # Corrupt transaciton, unknown type - transaction.status = TRANSACTION_STATUS_ERROR - - transaction.save() - - if transaction.status == TRANSACTION_STATUS_ERROR: - # We failed, redirect to a page to allow the user to try again - return_path = "{0}?{1}".format(reverse('pledge_nevermind'), - urllib.urlencode({'tid':transaction.id})) - return_url = urlparse.urljoin(settings.BASE_URL, return_path) - return HttpResponseRedirect(return_url) - - else: - # Not a failure, exact status will be updated by IPN - # Redirect to our pledge success URL - return_path = "{0}?{1}".format(reverse('pledge_complete'), - urllib.urlencode({'tid':transaction.id})) - return_url = urlparse.urljoin(settings.BASE_URL, return_path) - return HttpResponseRedirect(return_url) - - except Exception, e: - logging.error("Amazon co-branded return-url FAILED with exception:") - traceback.print_exc() - - - if transaction: - - # We failed, redirect to a page to allow the user to try again - return_path = "{0}?{1}".format(reverse('pledge_nevermind'), - urllib.urlencode({'tid':transaction.id})) - return_url = urlparse.urljoin(settings.BASE_URL, return_path) - return HttpResponseRedirect(return_url) - - else: - - # - # If we are here, amazon did not give us a caller reference, so we don't know what transaction was cancelled. - # Some kind of cleanup is required. This can happen if there is an error in the amazon API or if the user clicks - # the cancel button. If the case where the user closes the co-branded window or hits the back button, we will never arrive here. - # - return HttpResponseRedirect(settings.BASE_URL) - -class AmazonRequest: - ''' - Handles common information that is processed from the response envelope of the amazon request. - - ''' - - # Global values for the class - response = None - raw_response = None - errorMessage = None - status = None - url = None - - def ack( self ): - return None - - def success(self): - - if self.errorMessage: - return False - else: - return True - - def error(self): - if self.errorMessage: - return True - else: - return False - - def error_data(self): - return None - - def error_id(self): - return None - - def error_string(self): - - return self.errorMessage - - def envelope(self): - - # The envelope is used to store info about this request - if self.response: - return str(self.response) - else: - return None - - def correlation_id(self): - # The correlation ID is unique to each API call - if self.response: - return self.response.TransactionId - else: - return None - - def timestamp(self): - return str(datetime.datetime.now()) - - -class Pay( AmazonRequest ): - - ''' - The pay function generates a redirect URL to approve the transaction - ''' - - def __init__( self, transaction, return_url=None, nevermind_url=None, amount=None, paymentReason=""): - - try: - logging.debug("Amazon PAY operation for transaction ID %d" % transaction.id) - - # Replace our return URL with a redirect through our internal URL - self.original_return_url = return_url - return_url = settings.BASE_URL + reverse('AmazonPaymentReturn') - - self.connection = FPSConnection(FPS_ACCESS_KEY, FPS_SECRET_KEY, host=settings.AMAZON_FPS_HOST) - - receiver_list = [] - receivers = transaction.receiver_set.all() - - if not amount: - amount = 0 - for r in receivers: - amount += r.amount - - logger.info(receiver_list) - - # Data fields for amazon - - expiry = now() + timedelta( days=settings.PREAPPROVAL_PERIOD ) - - data = { - 'amountType':'Maximum', # The transaction amount is the maximum amount - 'callerReference': transaction.secret, - 'currencyCode': 'USD', - 'globalAmountLimit': str(amount), - 'validityExpiry': str(int(time.mktime(expiry.timetuple()))), # use the preapproval date by default - } - - self.url = self.connection.make_url(return_url, paymentReason, "MultiUse", str(amount), **data) - - logging.debug("Amazon PAY redirect url was: %s" % self.url) - - except FPSResponseError as (responseStatus, responseReason, body): - logging.error("Amazon PAY api failed with status: %s, reason: %s and data:" % (responseStatus, responseReason)) - logging.error(body) - self.errorMessage = body - - except: - logging.error("Amazon PAY FAILED with exception:") - traceback.print_exc() - self.errorMessage = "Error: Server Error" - - def api(self): - return "Amazon Co-branded PAY request" - - def exec_status( self ): - return None - - def amount( self ): - return None - - def key( self ): - return None - - def next_url( self ): - return self.url - - def embedded_url(self): - return None - -class Preapproval(Pay): - - def __init__( self, transaction, amount, expiry=None, return_url=None, nevermind_url=None, paymentReason=""): - - # set the expiration date for the preapproval if not passed in. This is what the paypal library does - now_val = now() - if expiry is None: - expiry = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD ) - transaction.date_authorized = now_val - transaction.date_expired = expiry - transaction.save() - - # Call into our parent class - Pay.__init__(self, transaction, return_url=return_url, nevermind_url=nevermind_url, amount=amount, paymentReason=paymentReason) - - -class Execute(AmazonRequest): - - ''' - The Execute function sends an existing token(generated via the URL from the pay operation), and collects - the money. - ''' - - def __init__(self, transaction=None): - - try: - logging.debug("Amazon EXECUTE action for transaction id: %d" % transaction.id) - - # Use the boto class top open a connection - self.connection = FPSConnection(FPS_ACCESS_KEY, FPS_SECRET_KEY, host=settings.AMAZON_FPS_HOST) - self.transaction = transaction - - # BUGBUG, handle multiple receivers! For now we just send the money to ourselves - global_params = {"OverrideIPNURL": get_ipn_url()} - self.raw_response = self.connection.pay(transaction.amount, - transaction.pay_key, - recipientTokenId=None, - callerReference=transaction.secret, - senderReference=None, - recipientReference=None, - senderDescription=None, - recipientDescription=None, - callerDescription=None, - metadata=None, - transactionDate=None, - reserve=False, - extra_params=global_params) - - # - # BUGBUG: - # The boto FPS library throws an exception if an error is generated, we need to do a better - # job of reporting the error when this occurs - # - - self.response = self.raw_response[0] - logging.debug("Amazon EXECUTE response for transaction id: %d" % transaction.id) - logging.debug(str(self.response)) - - self.status = self.response.TransactionStatus - - # - # For amazon, the transactionID is per transaction, not per receiver. For now we will store it in the preapproval key field - # so we can use it to refund or get status later - # - transaction.preapproval_key = self.response.TransactionId - - logging.debug("Amazon EXECUTE API returning with variables:") - logging.debug(locals()) - - except FPSResponseError as (responseStatus, responseReason, body): - - logging.error("Amazon EXECUTE api failed with status: %s, reason: %s and data:" % (responseStatus, responseReason)) - logging.error(body) - self.errorMessage = body - - except: - logging.error("Amazon EXECUTE FAILED with exception:") - traceback.print_exc() - self.errorMessage = "Error: Server Error" - - def api(self): - return "Amazon API Pay" - - def key(self): - # IN paypal land, our key is updated from a preapproval to a pay key here, just return the existing key - return self.transaction.pay_key - - - -class Finish(AmazonRequest): - ''' - The Finish function handles the secondary receiver in a chained payment. Currently not implemented - for amazon - ''' - def __init__(self, transaction): - - try: - - print "Finish" - - except: - traceback.print_exc() - self.errorMessage = "Error: Server Error" - -class PaymentDetails(AmazonRequest): - ''' - Get details about executed PAY operation - - This api must set the following class variables to work with the code in manager.py - - status - one of the global transaction status codes - transactions -- Not supported for amazon, used by paypal - - ''' - def __init__(self, transaction=None): - - try: - logging.debug("Amazon PAYMENTDETAILS API for transaction id: %d" % transaction.id) - - # Use the boto class top open a connection - self.connection = FPSConnection(FPS_ACCESS_KEY, FPS_SECRET_KEY, host=settings.AMAZON_FPS_HOST) - self.transaction = transaction - - if not transaction.preapproval_key: - # This is where we store the transaction ID - self.errorMessage = "No Valid Transaction ID" - return - - # - # We need to reference the transaction ID here, this is stored in the preapproval_key as this - # field is not used for amazon - # - self.raw_response = self.connection.get_transaction_status(transaction.preapproval_key) - self.response = self.raw_response[0] - - logging.debug("Amazon PAYMENTDETAILS API for transaction id: %d returned response:") - logging.debug(self.response) - - # - # Now we need to build values to match the paypal response. - # The two we need are status and and array of transactions. - # - - # Check our status codes, note that these are different than the IPN status codes - self.local_status = self.response.StatusCode - self.message = self.response.StatusMessage - - - if self.local_status == 'Canceled': - self.status = TRANSACTION_STATUS_CANCELED - - elif self.local_status == 'Success': - # - # Note, there is a limitation here. If the current status is refunded, this API will return "Success". - # We must be careful to not overwrite refunded status codes. There is no way that I can find to poll - # to see if a transaction is refunded. I need to investigate all of the data fields and see if we can find - # that information - # - if transaction.status != TRANSACTION_STATUS_REFUNDED: - self.status = TRANSACTION_STATUS_COMPLETE - else: - self.status = TRANSACTION_STATUS_REFUNDED - - elif self.local_status == 'PendingNetworkResponse' or self.local_status == 'PendingVerification': - self.status = TRANSACTION_STATUS_PENDING - elif self.local_status == 'TransactionDenied': - self.status = TRANSACTION_STATUS_FAILED - else: - self.status = TRANSACTION_STATUS_ERROR - - # Amazon does not support receivers at this point - self.transactions = [] - - logging.debug("Amazon PAYMENTDETAILS API returning with variables:") - logging.debug(locals()) - - except FPSResponseError as (responseStatus, responseReason, body): - - logging.error("Amazon PAYMENTDETAILS api failed with status: %s, reason: %s and data:" % (responseStatus, responseReason)) - logging.error(body) - self.errorMessage = body - - except: - logging.error("Amazon PAYMENTDETAILS FAILED with exception:") - self.errorMessage = "Error: ServerError" - traceback.print_exc() - - - -class CancelPreapproval(AmazonRequest): - ''' - Cancels an exisiting token. The current boto FPS library does not directly support - the CancelToken API, just the Cancel API(for real money in-flight or reserved). - ''' - - def __init__(self, transaction): - - try: - logging.debug("Amazon CANCELPREAPPROVAL api called for transaction id: %d" % transaction.id) - - # Use the boto class top open a connection - self.connection = FPSConnection(FPS_ACCESS_KEY, FPS_SECRET_KEY, host=settings.AMAZON_FPS_HOST) - self.transaction = transaction - - global_params = {"OverrideIPNURL": get_ipn_url()} - params = global_params - params['TokenId'] = transaction.pay_key - params['ReasonText'] = "Cancel Reason" - - fps_response = self.connection.make_request("CancelToken", params) - - body = fps_response.read() - - if(fps_response.status == 200): - - rs = ResultSet() - h = handler.XmlHandler(rs, self) - xml.sax.parseString(body, h) - - if rs: - self.raw_response = rs - self.response = self.raw_response[0] - self.status = self.response.TransactionStatus - self.errorMessage = None - - else: - # - # Set an error message and failure status for - # our success() and error() functions - # - self.status = AMAZON_STATUS_FAILURE - self.errorMessage = "%s - %s" % (fps_response.reason, body) - - logging.debug("Amazon CANCELPREAPPROVAL API returning with variables:") - logging.debug(locals()) - - except FPSResponseError as (responseStatus, responseReason, body): - - logging.error("Amazon CANCELPREAPPROVAL api failed with status: %s, reason: %s and data:" % (responseStatus, responseReason)) - logging.error(body) - self.errorMessage = body - - except: - logging.error("Amazon CANCELPREAPPROVAL FAILED with exception:") - traceback.print_exc() - self.errorMessage = "Error: Server Error" - - -class RefundPayment(AmazonRequest): - - def __init__(self, transaction): - - try: - logging.debug("Amazon REFUNDPAYMENT API called for transaction id: %d", transaction.id) - - # Use the boto class top open a connection - self.connection = FPSConnection(FPS_ACCESS_KEY, FPS_SECRET_KEY, host=settings.AMAZON_FPS_HOST) - self.transaction = transaction - - if not transaction.preapproval_key: - # This is where we store the transaction ID - self.errorMessage = "No Valid Transaction ID" - return - - # - # We need to reference the transaction ID here, this is stored in the preapproval_key as this - # field is not used for amazon - # - global_params = {"OverrideIPNURL": get_ipn_url()} - self.raw_response = self.connection.refund(transaction.secret, transaction.preapproval_key, extra_params=global_params) - self.response = self.raw_response[0] - - logging.debug("Amazon REFUNDPAYMENT response was:") - logging.debug(str(self.response)) - - self.status = self.response.TransactionStatus - - logging.debug("Amazon REFUNDPAYMENT API returning with variables:") - logging.debug(locals()) - - except FPSResponseError as (responseStatus, responseReason, body): - - logging.error("Amazon REFUNDPAYMENT api failed with status: %s, reason: %s and data:" % (responseStatus, responseReason)) - logging.error(body) - self.errorMessage = body - - except: - logging.error("Amazon REFUNDPAYMENT FAILED with exception:") - traceback.print_exc() - self.errorMessage = "Error: Server Error" - - -class PreapprovalDetails(AmazonRequest): - ''' - Get details about an authorized token - - This api must set 4 different class variables to work with the code in manager.py - - status - one of the global transaction status codes - approved - boolean value - currency - not used in this API, but we can get some more info via other APIs - TODO - amount - not used in this API, but we can get some more info via other APIs - TODO - - ''' - def __init__(self, transaction=None): - - try: - logging.debug("Amazon PREAPPROVALDETAILS API called for transaction id: %d", transaction.id) - - # Use the boto class top open a connection - self.connection = FPSConnection(FPS_ACCESS_KEY, FPS_SECRET_KEY, host=settings.AMAZON_FPS_HOST) - self.transaction = transaction - - - # - # We need to reference the caller reference here, we may not have a token if the return URL failed - # - self.raw_response = self.connection.get_token_by_caller_reference(transaction.secret) - self.response = self.raw_response - - logging.debug("Amazon PREAPPROVALDETAILS response:") - logging.debug(str(self.response)) - - # - # Look for a token, we store this in the pay_key field - # - self.pay_key = self.response.TokenId - self.local_status = self.response.TokenStatus - - # Possible status for the Token object are Active and Inactive - if self.local_status == 'Active': - self.status = TRANSACTION_STATUS_ACTIVE - self.approved = True - else: - # It is not clear here if this should be failed or cancelled, but we have no way to know - # the token is only active or now, so we will assume it is canceled. - self.status = TRANSACTION_STATUS_CANCELED - self.approved = False - - # Set the other fields that are expected. We don't have values for these now, so just copy the transaction - self.currency = transaction.currency - self.amount = transaction.amount - - logging.debug("Amazon PREAPPROVALDETAILS API returning with variables:") - logging.debug(locals()) - - except FPSResponseError as (responseStatus, responseReason, body): - - logging.error("Amazon PREAPPROVALDETAILS api failed with status: %s, reason: %s and data:" % (responseStatus, responseReason)) - logging.error(body) - self.errorMessage = body - - except: - # If the boto API fails, it also throws an exception and we end up here - logging.error("Amazon PREAPPROVALDETAILS FAILED with exception:") - self.errorMessage = "Error: ServerError" - traceback.print_exc() - \ No newline at end of file diff --git a/payment/baseprocessor.py b/payment/baseprocessor.py new file mode 100644 index 00000000..138c3ec0 --- /dev/null +++ b/payment/baseprocessor.py @@ -0,0 +1,182 @@ +from regluit.payment.models import Transaction, PaymentResponse +from django.http import HttpResponseForbidden +from datetime import timedelta +from regluit.utils.localdatetime import now, zuluformat + +import datetime +import time + + + +def ProcessIPN(request): + return HttpResponseForbidden() + + +class BasePaymentRequest: + ''' + Handles common information incident to payment processing + + ''' + + # Global values for the class + response = None + raw_response = None + errorMessage = None + status = None + url = None + + def ack( self ): + return None + + def success(self): + + if self.errorMessage: + return False + else: + return True + + def error(self): + if self.errorMessage: + return True + else: + return False + + def error_data(self): + return None + + def error_id(self): + return None + + def error_string(self): + return self.errorMessage + + def envelope(self): + # The envelope is used to store info about this request + if self.response: + return str(self.response) + else: + return None + + def correlation_id(self): + return None + + def timestamp(self): + return str(datetime.datetime.now()) + + +class Pay( BasePaymentRequest ): + + ''' + The pay function generates a redirect URL to approve the transaction + ''' + + def __init__( self, transaction, return_url=None, amount=None, paymentReason=""): + self.transaction=transaction + + def api(self): + return "null api" + + def exec_status( self ): + return None + + def amount( self ): + return None + + def key( self ): + return None + + def next_url( self ): + return self.url + +class Preapproval(Pay): + + def __init__( self, transaction, amount, expiry=None, return_url=None, paymentReason=""): + + # set the expiration date for the preapproval if not passed in. This is what the paypal library does + now_val = now() + if expiry is None: + expiry = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD ) + transaction.date_authorized = now_val + transaction.date_expired = expiry + transaction.save() + + # Call into our parent class + Pay.__init__(self, transaction, return_url=return_url, amount=amount, paymentReason=paymentReason) + + +class Execute(BasePaymentRequest): + + ''' + The Execute function sends an existing token(generated via the URL from the pay operation), and collects + the money. + ''' + + def __init__(self, transaction=None): + self.transaction = transaction + + def api(self): + return "Base Pay" + + def key(self): + # IN paypal land, our key is updated from a preapproval to a pay key here, just return the existing key + return self.transaction.pay_key + + + +class Finish(BasePaymentRequest): + ''' + The Finish function handles the secondary receiver in a chained payment. + ''' + def __init__(self, transaction): + + print "Finish" + + +class PaymentDetails(BasePaymentRequest): + ''' + Get details about executed PAY operation + + This api must set the following class variables to work with the code in manager.py + + status - one of the global transaction status codes + transactions -- Not supported for amazon, used by paypal + + ''' + def __init__(self, transaction=None): + self.transaction = transaction + + + +class CancelPreapproval(BasePaymentRequest): + ''' + Cancels an exisiting token. + ''' + + def __init__(self, transaction): + self.transaction = transaction + + +class RefundPayment(BasePaymentRequest): + + def __init__(self, transaction): + self.transaction = transaction + + + + + +class PreapprovalDetails(BasePaymentRequest): + ''' + Get details about an authorized token + + This api must set 4 different class variables to work with the code in manager.py + + status - one of the global transaction status codes + approved - boolean value + currency - not used in this API, but we can get some more info via other APIs - TODO + amount - not used in this API, but we can get some more info via other APIs - TODO + + ''' + def __init__(self, transaction=None): + self.transaction = transaction + \ No newline at end of file diff --git a/payment/credit.py b/payment/credit.py new file mode 100644 index 00000000..4976089d --- /dev/null +++ b/payment/credit.py @@ -0,0 +1,55 @@ +from datetime import timedelta + +from django.contrib.auth.models import User +from django.conf import settings + +from regluit.payment.parameters import * +from regluit.payment.baseprocessor import BasePaymentRequest + + +def pledge_transaction(t,user,amount): + """commit from a 's credit to a specified transaction """ + + if t.amount and t.host == PAYMENT_HOST_CREDIT: + #changing the pledge_transaction + user.credit.add_to_pledged(amount-t.amount) + else: + user.credit.add_to_pledged(amount) + t.max_amount=amount + t.set_credit_approved(amount) + +def credit_transaction(t,user,amount): + '''user has new credit, use it to fund the transaction''' + # first, credit the user's account + user.credit.add_to_balance(amount) + + # now pledge to the transaction + pledge_amount = t.max_amount if t.max_amount <= user.credit.available else amount + user.credit.add_to_pledged(pledge_amount) + t.set_credit_approved(pledge_amount) + +class CancelPreapproval(BasePaymentRequest): + ''' + Cancels an exisiting token. + ''' + + def __init__(self, transaction): + self.transaction = transaction + if transaction.user.credit.add_to_pledged(-transaction.amount): + #success + transaction.status=TRANSACTION_STATUS_CANCELED + transaction.save() + else: + self.errorMessage="couldn't cancel the transaction" + self.status = 'Credit Cancel Failure' + +class PreapprovalDetails(BasePaymentRequest): + status = None + approved = None + currency = None + amount = None + def __init__(self, transaction): + self.status = transaction.status + self.approved = transaction.approved + self.currency = transaction.currency + self.amount = transaction.amount diff --git a/payment/forms.py b/payment/forms.py new file mode 100644 index 00000000..db65a947 --- /dev/null +++ b/payment/forms.py @@ -0,0 +1,7 @@ +from django import forms +import logging + +logger = logging.getLogger(__name__) + +class StripePledgeForm(forms.Form): + stripe_token = forms.CharField(required=False, widget=forms.HiddenInput()) diff --git a/payment/management/commands/add_credit_records.py b/payment/management/commands/add_credit_records.py new file mode 100644 index 00000000..1c08e6b6 --- /dev/null +++ b/payment/management/commands/add_credit_records.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User + +import regluit.payment + +class Command(BaseCommand): + help = "initialize credit table" + + def handle(self, *args, **kwargs): + users = User.objects.all() + for user in users: + regluit.payment.models.Credit(user=user).save() + \ No newline at end of file diff --git a/payment/management/commands/grant_user_credit.py b/payment/management/commands/grant_user_credit.py new file mode 100644 index 00000000..acd05f0c --- /dev/null +++ b/payment/management/commands/grant_user_credit.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User + +class Command(BaseCommand): + help = "grant credit to a user" + args = " " + + def handle(self, username, amount, action="credit", *args, **kwargs): + if action=="debit": + amount=-int(amount) + else: + amount= int(amount) + user = User.objects.get(username=username) + user.credit.add_to_balance(amount) + print "%s now has a balance of %s donation credits" % (username, user.credit.balance) + \ No newline at end of file diff --git a/payment/manager.py b/payment/manager.py index 4d77dd03..64389779 100644 --- a/payment/manager.py +++ b/payment/manager.py @@ -4,15 +4,11 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.conf import settings from regluit.payment.parameters import * -from regluit.payment.paypal import IPN_SENDER_STATUS_COMPLETED from regluit.payment.signals import transaction_charged, pledge_modified, pledge_created +from regluit.payment import credit + +from regluit.payment.baseprocessor import Pay, Finish, Preapproval, ProcessIPN, CancelPreapproval, PaymentDetails, PreapprovalDetails, RefundPayment -if settings.PAYMENT_PROCESSOR == 'paypal': - from regluit.payment.paypal import Pay, Finish, Preapproval, ProcessIPN, CancelPreapproval, PaymentDetails, PreapprovalDetails, RefundPayment - from regluit.payment.paypal import Pay as Execute - -elif settings.PAYMENT_PROCESSOR == 'amazon': - from regluit.payment.amazon import Pay, Execute, Finish, Preapproval, ProcessIPN, CancelPreapproval, PaymentDetails, PreapprovalDetails, RefundPayment import uuid import traceback from regluit.utils.localdatetime import now @@ -21,6 +17,8 @@ import logging from decimal import Decimal as D from xml.dom import minidom import urllib, urlparse +from datetime import timedelta + from django.conf import settings @@ -37,10 +35,7 @@ def append_element(doc, parent, name, text): # at this point, there is no internal context and therefore, the methods of PaymentManager can be recast into static methods class PaymentManager( object ): - - def __init__( self, embedded=False): - self.embedded = embedded - + def processIPN(self, request, module): # Forward to our payment processor @@ -541,53 +536,28 @@ class PaymentManager( object ): logger.info("Cancel Transaction " + str(transaction.id) + " Failed with error: " + p.error_string()) return False - def authorize(self, currency, target, amount, expiry=None, campaign=None, list=None, user=None, - return_url=None, nevermind_url=None, anonymous=False, premium=None, - paymentReason="unglue.it Pledge", modification=False): + def authorize(self, transaction, expiry= None, return_url=None, paymentReason="unglue.it Pledge", modification=False): ''' authorize authorizes a set amount of money to be collected at a later date - currency: a 3-letter paypal currency code, i.e. USD - target: a defined target type, i.e. TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST, TARGET_TYPE_NONE - amount: the amount to authorize - campaign: optional campaign object(to be set with TARGET_TYPE_CAMPAIGN) - list: optional list object(to be set with TARGET_TYPE_LIST) - user: optional user object return_url: url to redirect supporter to after a successful PayPal transaction - nevermind_url: url to send supporter to if support hits cancel while in middle of PayPal transaction - anonymous: whether this pledge is anonymous - premium: the premium selected by the supporter for this transaction paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account - modification: whether this authorize call is part of a modification of an existing pledge + modification: whether this authorize call is part of a modification of an existing pledge return value: a tuple of the new transaction object and a re-direct url. If the process fails, the redirect url will be None - ''' - - t = Transaction.objects.create(amount=amount, - max_amount=amount, - type=PAYMENT_TYPE_AUTHORIZATION, - execution = EXECUTE_TYPE_CHAINED_DELAYED, - target=target, - currency=currency, - status='NONE', - campaign=campaign, - list=list, - user=user, - anonymous=anonymous, - premium=premium - ) + ''' - # we might want to not allow for a return_url or nevermind_url to be passed in but calculated + if host==None: + #TODO send user to select a payment processor + pass + + # we might want to not allow for a return_url to be passed in but calculated # here because we have immediate access to the Transaction object. - if nevermind_url is None: - nevermind_path = "{0}?{1}".format(reverse('pledge_nevermind'), - urllib.urlencode({'tid':t.id})) - nevermind_url = urlparse.urljoin(settings.BASE_URL, nevermind_path) if return_url is None: return_path = "{0}?{1}".format(reverse('pledge_complete'), @@ -595,7 +565,7 @@ class PaymentManager( object ): return_url = urlparse.urljoin(settings.BASE_URL, return_path) method = getattr(t.get_payment_class(), "Preapproval") - p = method(t, amount, expiry, return_url=return_url, nevermind_url=nevermind_url, paymentReason=paymentReason) + p = method(transaction, transaction.max_amount, expiry, return_url=return_url, paymentReason=paymentReason) # Create a response for this envelope = p.envelope() @@ -605,11 +575,11 @@ class PaymentManager( object ): correlation_id = p.correlation_id(), timestamp = p.timestamp(), info = p.raw_response, - transaction=t) + transaction=transaction) if p.success() and not p.error(): - t.preapproval_key = p.key() - t.save() + transaction.preapproval_key = p.key() + transaction.save() url = p.next_url() @@ -629,16 +599,62 @@ class PaymentManager( object ): # send the notice here for now # this is actually premature since we're only about to send the user off to the payment system to # authorize a charge - pledge_created.send(sender=self, transaction=t) + pledge_created.send(sender=self, transaction=transaction) - return t, url + return transaction, url else: - t.error = p.error_string() - t.save() + transaction.error = p.error_string() + transaction.save() logger.info("Authorize Error: " + p.error_string()) - return t, None + return transaction, None + + def process_transaction(self, currency, amount, host=None, campaign=None, user=None, + return_url=None, paymentReason="unglue.it Pledge", pledge_extra=None, + modification=False): + ''' + process + + saves and processes a proposed transaction; decides if the transaction should be processed immediately. + + currency: a 3-letter currency code, i.e. USD + amount: the amount to authorize + host: the name of the processing module; if none, send user back to decide! + campaign: required campaign object + user: optional user object + return_url: url to redirect supporter to after a successful transaction + paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account + modification: whether this authorize call is part of a modification of an existing pledge + pledge_extra: extra pledge stuff + + return value: a tuple of the new transaction object and a re-direct url. If the process fails, + the redirect url will be None + ''' + # set the expiry date based on the campaign deadline + expiry = campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) + + t = Transaction.create(amount=0, + max_amount=amount, + currency=currency, + campaign=campaign, + user=user, + pledge_extra=pledge_extra + ) + t.save() + # does user have enough credit to pledge now? + if user.credit.available >= amount : + # YES! + credit.pledge_transaction(t,user,amount) + return_path = "{0}?{1}".format(reverse('pledge_complete'), + urllib.urlencode({'tid':t.id})) + return_url = urlparse.urljoin(settings.BASE_URL, return_path) + pledge_created.send(sender=self, transaction=t) + return t, return_url + else: + # send user to choose payment path + return t, reverse('fund_pledge', args=[t.id]) + def cancel_related_transaction(self, transaction, status=TRANSACTION_STATUS_ACTIVE, campaign=None): ''' @@ -682,26 +698,26 @@ class PaymentManager( object ): return canceled - def modify_transaction(self, transaction, amount, expiry=None, anonymous=None, premium=None, - return_url=None, nevermind_url=None, - paymentReason=None): + def modify_transaction(self, transaction, amount, expiry=None, pledge_extra=None, + return_url=None, nevermind_url=None, paymentReason=None): ''' modify - Modifies a transaction. The only type of modification allowed is to the amount and expiration date + Modifies a transaction. + 2 main situations: if the new amount is less than max_amount, no need to go out to PayPal again + if new amount is greater than max_amount...need to go out and get new approval. + to start with, we can use the standard pledge_complete, pledge_cancel machinery + might have to modify the pledge_complete, pledge_cancel because the messages are going to be + different because we're modifying a pledge rather than a new one. amount: the new amount expiry: the new expiration date, or if none the current expiration date will be used - anonymous: new anonymous value; if None, then keep old value - premium: new premium selected; if None, then keep old value return_url: the return URL after the preapproval(if needed) - nevermind_url: the cancel url after the preapproval(if needed) paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account return value: True if successful, False otherwise. An optional second parameter for the forward URL if a new authorhization is needed ''' - # Can only modify the amount of a preapproval for now if transaction.type != PAYMENT_TYPE_AUTHORIZATION: logger.info("Error, attempt to modify an invalid transaction type") return False, None @@ -712,31 +728,57 @@ class PaymentManager( object ): logger.info("Error, attempt to modify a transaction that is not active") return False, None - # if any of expiry, anonymous, or premium is None, use the existing value - if expiry is None: - expiry = transaction.date_expired - if anonymous is None: - anonymous = transaction.anonymous - if premium is None: - premium = transaction.premium - - if amount > transaction.max_amount or expiry != transaction.date_expired: + if transaction.host == PAYMENT_HOST_CREDIT: + # does user have enough credit to pledge now? + if transaction.user.credit.available >= amount-transaction.amount : + # YES! + transaction.set_pledge_extra(pledge_extra) + credit.pledge_transaction(transaction,transaction.user,amount) + return_path = "{0}?{1}".format(reverse('pledge_complete'), + urllib.urlencode({'tid':transaction.id})) + return_url = urlparse.urljoin(settings.BASE_URL, return_path) - # Start a new authorization for the new amount - - t, url = self.authorize(transaction.currency, - transaction.target, - amount, - expiry, - transaction.campaign, - transaction.list, - transaction.user, - return_url, - nevermind_url, - transaction.anonymous, - premium, - paymentReason, - True) + logger.info("Updated amount of transaction to %f" % amount) + pledge_modified.send(sender=self, transaction=transaction,up_or_down="decreased" if amount-transaction.amount<0 else "increased") + return transaction, return_url + else: + # cancel old transaction, send user to choose new payment path + # set the expiry date based on the campaign deadline + expiry = transaction.campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) + t = Transaction.create(amount=0, + max_amount=amount, + currency=transaction.currency, + status=TRANSACTION_STATUS_MODIFIED, + campaign=transaction.campaign, + user=transaction.user, + pledge_extra=pledge_extra + ) + t.save() + credit.CancelPreapproval(transaction) + return t, reverse('fund_pledge', args=[t.id]) + + elif amount > transaction.max_amount or expiry != transaction.date_expired: + + # set the expiry date based on the campaign deadline + expiry = transaction.campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) + + # Start a new transaction for the new amount + t = Transaction.create(amount=amount, + max_amount=amount, + host=transaction.host, + currency=transaction.currency, + status=TRANSACTION_STATUS_CREATED, + campaign=transaction.campaign, + user=transaction.user, + pledge_extra=pledge_extra + ) + t.save() + t, url = self.authorize(transaction, + expiry=expiry if expiry else transaction.date_expired, + return_url=return_url, + paymentReason=paymentReason, + modification=True + ) if t and url: # Need to re-direct to approve the transaction @@ -761,8 +803,7 @@ class PaymentManager( object ): elif amount <= transaction.max_amount: # Update transaction but leave the preapproval alone transaction.amount = amount - transaction.anonymous = anonymous - transaction.premium = premium + transaction.set_pledge_extra(pledge_extra) transaction.save() logger.info("Updated amount of transaction to %f" % amount) @@ -819,29 +860,19 @@ class PaymentManager( object ): logger.info("Refund Transaction " + str(transaction.id) + " Failed with error: " + p.error_string()) return False - def pledge(self, currency, target, receiver_list, campaign=None, list=None, user=None, return_url=None, nevermind_url=None, anonymous=False, premium=None): + def pledge(self, currency, campaign=None, user=None, + return_url=None, nevermind_url=None, pledge_extra=None): ''' pledge Performs an instant payment currency: a 3-letter paypal currency code, i.e. USD - target: a defined target type, i.e. TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST, TARGET_TYPE_NONE - receiver_list: a list of receivers for the transaction, in this format: - - [ - {'email':'email-1', 'amount':amount1}, - {'email':'email-2', 'amount':amount2} - ] - - campaign: optional campaign object(to be set with TARGET_TYPE_CAMPAIGN) - list: optional list object(to be set with TARGET_TYPE_LIST) + campaign: required campaign object user: optional user object return_url: url to redirect supporter to after a successful PayPal transaction nevermind_url: url to send supporter to if support hits cancel while in middle of PayPal transaction - anonymous: whether this pledge is anonymous - premium: the premium selected by the supporter for this transaction - + return value: a tuple of the new transaction object and a re-direct url. If the process fails, the redirect url will be None @@ -849,25 +880,18 @@ class PaymentManager( object ): amount = D('0.00') - # for chained payments, first amount is the total amount - amount = D(receiver_list[0]['amount']) - - t = Transaction.objects.create(amount=amount, - max_amount=amount, - type=PAYMENT_TYPE_INSTANT, - execution=EXECUTE_TYPE_CHAINED_INSTANT, - target=target, - currency=currency, - status='NONE', - campaign=campaign, - list=list, - user=user, - date_payment=now(), - anonymous=anonymous, - premium=premium - ) + t = Transaction.create(amount=amount, + max_amount=amount, + currency=currency, + status=TRANSACTION_STATUS_NONE, + campaign=campaign, + user=user, + pledge_extra=pledge_extra + ) - t.create_receivers(receiver_list) + t.date_payment=now() + t.execution=EXECUTE_TYPE_CHAINED_INSTANT + t.type=PAYMENT_TYPE_INSTANT method = getattr(t.get_payment_class(), "Pay") p = method(t,return_url=return_url, nevermind_url=nevermind_url) @@ -887,11 +911,7 @@ class PaymentManager( object ): t.status = TRANSACTION_STATUS_CREATED t.save() - if self.embedded: - url = p.embedded_url() - logger.info(url) - else: - url = p.next_url() + url = p.next_url() logger.info("Pledge Success: " + url) return t, url diff --git a/payment/migrations/0008_auto__add_credit__add_creditlog__del_field_transaction_target__del_fie.py b/payment/migrations/0008_auto__add_credit__add_creditlog__del_field_transaction_target__del_fie.py new file mode 100644 index 00000000..664f4ec6 --- /dev/null +++ b/payment/migrations/0008_auto__add_credit__add_creditlog__del_field_transaction_target__del_fie.py @@ -0,0 +1,224 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Credit' + db.create_table('payment_credit', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='credit', unique=True, to=orm['auth.User'])), + ('balance', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2)), + ('pledged', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2)), + ('last_activity', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal('payment', ['Credit']) + + # Adding model 'CreditLog' + db.create_table('payment_creditlog', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)), + ('amount', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2)), + ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('action', self.gf('django.db.models.fields.CharField')(max_length=16)), + )) + db.send_create_signal('payment', ['CreditLog']) + + # Deleting field 'Transaction.target' + db.delete_column('payment_transaction', 'target') + + # Deleting field 'Transaction.list' + db.delete_column('payment_transaction', 'list_id') + + # Adding field 'Transaction.ack_name' + db.add_column('payment_transaction', 'ack_name', self.gf('django.db.models.fields.CharField')(max_length=64, null=True), keep_default=False) + + # Adding field 'Transaction.ack_dedication' + db.add_column('payment_transaction', 'ack_dedication', self.gf('django.db.models.fields.CharField')(max_length=140, null=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting model 'Credit' + db.delete_table('payment_credit') + + # Deleting model 'CreditLog' + db.delete_table('payment_creditlog') + + # Adding field 'Transaction.target' + db.add_column('payment_transaction', 'target', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False) + + # Adding field 'Transaction.list' + db.add_column('payment_transaction', 'list', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['core.Wishlist'], null=True), keep_default=False) + + # Deleting field 'Transaction.ack_name' + db.delete_column('payment_transaction', 'ack_name') + + # Deleting field 'Transaction.ack_dedication' + db.delete_column('payment_transaction', 'ack_dedication') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 31, 2, 10, 24, 467332)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 31, 2, 10, 24, 467190)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'core.campaign': { + 'Meta': {'object_name': 'Campaign'}, + 'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deadline': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('ckeditor.fields.RichTextField', [], {'null': 'True'}), + 'details': ('ckeditor.fields.RichTextField', [], {'null': 'True', 'blank': 'True'}), + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'null': 'True', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'license': ('django.db.models.fields.CharField', [], {'default': "'CC BY-NC-ND'", 'max_length': '255'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}), + 'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"}) + }, + 'core.edition': { + 'Meta': {'object_name': 'Edition'}, + 'cover_image': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_domain': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"}) + }, + 'core.premium': { + 'Meta': {'object_name': 'Premium'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '0'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'premiums'", 'null': 'True', 'to': "orm['core.Campaign']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '2'}) + }, + 'core.work': { + 'Meta': {'ordering': "['title']", 'object_name': 'Work'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}), + 'num_wishes': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}) + }, + 'payment.credit': { + 'Meta': {'object_name': 'Credit'}, + 'balance': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'pledged': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'credit'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'payment.creditlog': { + 'Meta': {'object_name': 'CreditLog'}, + 'action': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'payment.paymentresponse': { + 'Meta': {'object_name': 'PaymentResponse'}, + 'api': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'correlation_id': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'timestamp': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}) + }, + 'payment.receiver': { + 'Meta': {'object_name': 'Receiver'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'currency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}), + 'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'payment.transaction': { + 'Meta': {'object_name': 'Transaction'}, + 'ack_dedication': ('django.db.models.fields.CharField', [], {'max_length': '140', 'null': 'True'}), + 'ack_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'approved': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Campaign']", 'null': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '10', 'null': 'True'}), + 'date_authorized': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_executed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_expired': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'date_payment': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'error': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'execution': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '32', 'null': 'True'}), + 'max_amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'pay_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'preapproval_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'premium': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Premium']", 'null': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'receipt': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'None'", 'max_length': '32'}), + 'type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['payment'] diff --git a/payment/migrations/0009_auto__add_sent__add_field_creditlog_sent.py b/payment/migrations/0009_auto__add_sent__add_field_creditlog_sent.py new file mode 100644 index 00000000..f6cc1c11 --- /dev/null +++ b/payment/migrations/0009_auto__add_sent__add_field_creditlog_sent.py @@ -0,0 +1,200 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Sent' + db.create_table('payment_sent', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.CharField')(max_length=32, null=True)), + ('amount', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2)), + ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal('payment', ['Sent']) + + # Adding field 'CreditLog.sent' + db.add_column('payment_creditlog', 'sent', self.gf('django.db.models.fields.IntegerField')(null=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting model 'Sent' + db.delete_table('payment_sent') + + # Deleting field 'CreditLog.sent' + db.delete_column('payment_creditlog', 'sent') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 5, 23, 7, 10, 823074)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 5, 23, 7, 10, 822938)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'core.campaign': { + 'Meta': {'object_name': 'Campaign'}, + 'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deadline': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('ckeditor.fields.RichTextField', [], {'null': 'True'}), + 'details': ('ckeditor.fields.RichTextField', [], {'null': 'True', 'blank': 'True'}), + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'null': 'True', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'license': ('django.db.models.fields.CharField', [], {'default': "'CC BY-NC-ND'", 'max_length': '255'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}), + 'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"}) + }, + 'core.edition': { + 'Meta': {'object_name': 'Edition'}, + 'cover_image': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_domain': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"}) + }, + 'core.premium': { + 'Meta': {'object_name': 'Premium'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '0'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'premiums'", 'null': 'True', 'to': "orm['core.Campaign']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '2'}) + }, + 'core.work': { + 'Meta': {'ordering': "['title']", 'object_name': 'Work'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}), + 'num_wishes': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}) + }, + 'payment.credit': { + 'Meta': {'object_name': 'Credit'}, + 'balance': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'pledged': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'credit'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'payment.creditlog': { + 'Meta': {'object_name': 'CreditLog'}, + 'action': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sent': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'payment.paymentresponse': { + 'Meta': {'object_name': 'PaymentResponse'}, + 'api': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'correlation_id': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'timestamp': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}) + }, + 'payment.receiver': { + 'Meta': {'object_name': 'Receiver'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'currency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}), + 'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'payment.sent': { + 'Meta': {'object_name': 'Sent'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}) + }, + 'payment.transaction': { + 'Meta': {'object_name': 'Transaction'}, + 'ack_dedication': ('django.db.models.fields.CharField', [], {'max_length': '140', 'null': 'True'}), + 'ack_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'approved': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Campaign']", 'null': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '10', 'null': 'True'}), + 'date_authorized': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_executed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_expired': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'date_payment': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'error': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'execution': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '32', 'null': 'True'}), + 'max_amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'pay_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'preapproval_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'premium': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Premium']", 'null': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'receipt': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'None'", 'max_length': '32'}), + 'type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['payment'] diff --git a/payment/migrations/0010_auto__add_account.py b/payment/migrations/0010_auto__add_account.py new file mode 100644 index 00000000..c6ce6e01 --- /dev/null +++ b/payment/migrations/0010_auto__add_account.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Account' + db.create_table('payment_account', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('host', self.gf('django.db.models.fields.CharField')(default='none', max_length=32)), + ('account_id', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)), + )) + db.send_create_signal('payment', ['Account']) + + + def backwards(self, orm): + # Deleting model 'Account' + db.delete_table('payment_account') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'core.campaign': { + 'Meta': {'object_name': 'Campaign'}, + 'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deadline': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('ckeditor.fields.RichTextField', [], {'null': 'True'}), + 'details': ('ckeditor.fields.RichTextField', [], {'null': 'True', 'blank': 'True'}), + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'null': 'True', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'license': ('django.db.models.fields.CharField', [], {'default': "'CC BY-NC-ND'", 'max_length': '255'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}), + 'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"}) + }, + 'core.edition': { + 'Meta': {'object_name': 'Edition'}, + 'cover_image': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_domain': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), + 'unglued': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"}) + }, + 'core.premium': { + 'Meta': {'object_name': 'Premium'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '0'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'premiums'", 'null': 'True', 'to': "orm['core.Campaign']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '2'}) + }, + 'core.work': { + 'Meta': {'ordering': "['title']", 'object_name': 'Work'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}), + 'num_wishes': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}) + }, + 'payment.account': { + 'Meta': {'object_name': 'Account'}, + 'account_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'payment.credit': { + 'Meta': {'object_name': 'Credit'}, + 'balance': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'pledged': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'credit'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'payment.creditlog': { + 'Meta': {'object_name': 'CreditLog'}, + 'action': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sent': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'payment.paymentresponse': { + 'Meta': {'object_name': 'PaymentResponse'}, + 'api': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'correlation_id': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'timestamp': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}) + }, + 'payment.receiver': { + 'Meta': {'object_name': 'Receiver'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'currency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}), + 'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'payment.sent': { + 'Meta': {'object_name': 'Sent'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}) + }, + 'payment.transaction': { + 'Meta': {'object_name': 'Transaction'}, + 'ack_dedication': ('django.db.models.fields.CharField', [], {'max_length': '140', 'null': 'True'}), + 'ack_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'approved': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Campaign']", 'null': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '10', 'null': 'True'}), + 'date_authorized': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_executed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_expired': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'date_payment': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'error': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'execution': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '32', 'null': 'True'}), + 'max_amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'pay_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'preapproval_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'premium': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Premium']", 'null': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'receipt': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'None'", 'max_length': '32'}), + 'type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['payment'] \ No newline at end of file diff --git a/payment/migrations/0011_auto__add_field_account_card_last4__add_field_account_card_type__add_f.py b/payment/migrations/0011_auto__add_field_account_card_last4__add_field_account_card_type__add_f.py new file mode 100644 index 00000000..c674066c --- /dev/null +++ b/payment/migrations/0011_auto__add_field_account_card_last4__add_field_account_card_type__add_f.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Account.card_last4' + db.add_column('payment_account', 'card_last4', + self.gf('django.db.models.fields.CharField')(max_length=4, null=True), + keep_default=False) + + # Adding field 'Account.card_type' + db.add_column('payment_account', 'card_type', + self.gf('django.db.models.fields.CharField')(max_length=32, null=True), + keep_default=False) + + # Adding field 'Account.card_exp_month' + db.add_column('payment_account', 'card_exp_month', + self.gf('django.db.models.fields.IntegerField')(null=True), + keep_default=False) + + # Adding field 'Account.card_exp_year' + db.add_column('payment_account', 'card_exp_year', + self.gf('django.db.models.fields.IntegerField')(null=True), + keep_default=False) + + # Adding field 'Account.card_fingerprint' + db.add_column('payment_account', 'card_fingerprint', + self.gf('django.db.models.fields.CharField')(max_length=32, null=True), + keep_default=False) + + # Adding field 'Account.card_country' + db.add_column('payment_account', 'card_country', + self.gf('django.db.models.fields.CharField')(max_length=2, null=True), + keep_default=False) + + # Adding field 'Account.date_created' + db.add_column('payment_account', 'date_created', + self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default=datetime.datetime(2012, 9, 17, 0, 0), blank=True), + keep_default=False) + + # Adding field 'Account.date_modified' + db.add_column('payment_account', 'date_modified', + self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=datetime.datetime(2012, 9, 17, 0, 0), blank=True), + keep_default=False) + + # Adding field 'Account.date_deactivated' + db.add_column('payment_account', 'date_deactivated', + self.gf('django.db.models.fields.DateTimeField')(null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Account.card_last4' + db.delete_column('payment_account', 'card_last4') + + # Deleting field 'Account.card_type' + db.delete_column('payment_account', 'card_type') + + # Deleting field 'Account.card_exp_month' + db.delete_column('payment_account', 'card_exp_month') + + # Deleting field 'Account.card_exp_year' + db.delete_column('payment_account', 'card_exp_year') + + # Deleting field 'Account.card_fingerprint' + db.delete_column('payment_account', 'card_fingerprint') + + # Deleting field 'Account.card_country' + db.delete_column('payment_account', 'card_country') + + # Deleting field 'Account.date_created' + db.delete_column('payment_account', 'date_created') + + # Deleting field 'Account.date_modified' + db.delete_column('payment_account', 'date_modified') + + # Deleting field 'Account.date_deactivated' + db.delete_column('payment_account', 'date_deactivated') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'core.campaign': { + 'Meta': {'object_name': 'Campaign'}, + 'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deadline': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('ckeditor.fields.RichTextField', [], {'null': 'True'}), + 'details': ('ckeditor.fields.RichTextField', [], {'null': 'True', 'blank': 'True'}), + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'null': 'True', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'license': ('django.db.models.fields.CharField', [], {'default': "'CC BY-NC-ND'", 'max_length': '255'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}), + 'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"}) + }, + 'core.edition': { + 'Meta': {'object_name': 'Edition'}, + 'cover_image': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_domain': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), + 'unglued': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"}) + }, + 'core.premium': { + 'Meta': {'object_name': 'Premium'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '0'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'premiums'", 'null': 'True', 'to': "orm['core.Campaign']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '2'}) + }, + 'core.work': { + 'Meta': {'ordering': "['title']", 'object_name': 'Work'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}), + 'num_wishes': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}) + }, + 'payment.account': { + 'Meta': {'object_name': 'Account'}, + 'account_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'card_country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'null': 'True'}), + 'card_exp_month': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'card_exp_year': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'card_fingerprint': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'card_last4': ('django.db.models.fields.CharField', [], {'max_length': '4', 'null': 'True'}), + 'card_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'date_deactivated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'payment.credit': { + 'Meta': {'object_name': 'Credit'}, + 'balance': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'pledged': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'credit'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'payment.creditlog': { + 'Meta': {'object_name': 'CreditLog'}, + 'action': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sent': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'payment.paymentresponse': { + 'Meta': {'object_name': 'PaymentResponse'}, + 'api': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'correlation_id': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'timestamp': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}) + }, + 'payment.receiver': { + 'Meta': {'object_name': 'Receiver'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'currency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}), + 'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'payment.sent': { + 'Meta': {'object_name': 'Sent'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}) + }, + 'payment.transaction': { + 'Meta': {'object_name': 'Transaction'}, + 'ack_dedication': ('django.db.models.fields.CharField', [], {'max_length': '140', 'null': 'True'}), + 'ack_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'approved': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Campaign']", 'null': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '10', 'null': 'True'}), + 'date_authorized': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_executed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_expired': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'date_payment': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'error': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'execution': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '32', 'null': 'True'}), + 'max_amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'pay_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'preapproval_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'premium': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Premium']", 'null': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'receipt': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'None'", 'max_length': '32'}), + 'type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['payment'] \ No newline at end of file diff --git a/payment/models.py b/payment/models.py index ac793ca6..1f5d5a6e 100644 --- a/payment/models.py +++ b/payment/models.py @@ -1,11 +1,23 @@ from django.db import models from django.contrib.auth.models import User from django.conf import settings -from regluit.core.models import Campaign, Wishlist, Premium +from regluit.core.models import Campaign, Wishlist, Premium, PledgeExtra from regluit.payment.parameters import * +from regluit.payment.signals import credit_balance_added, pledge_created +from regluit.utils.localdatetime import now from decimal import Decimal +from datetime import timedelta import uuid +import urllib +import logging +logger = logging.getLogger(__name__) +# in fitting stripe -- here are possible fields to fit in with Transaction +# c.id, c.amount, c.amount_refunded, c.currency, c.description, datetime.fromtimestamp(c.created, tz=utc), c.paid, +# c.fee, c.disputed, c.amount_refunded, c.failure_message, +# c.card.fingerprint, c.card.type, c.card.last4, c.card.exp_month, c.card.exp_year + +# promising fields class Transaction(models.Model): @@ -13,16 +25,13 @@ class Transaction(models.Model): type = models.IntegerField(default=PAYMENT_TYPE_NONE, null=False) # host: the payment processor. Named after the payment module that hosts the payment processing functions - host = models.CharField(default=settings.PAYMENT_PROCESSOR, max_length=32, null=False) - - # target: e.g, TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST -- defined in parameters.py - target = models.IntegerField(default=TARGET_TYPE_NONE, null=False) - + host = models.CharField(default=PAYMENT_HOST_NONE, max_length=32, null=False) + #execution: e.g. EXECUTE_TYPE_CHAINED_INSTANT, EXECUTE_TYPE_CHAINED_DELAYED, EXECUTE_TYPE_PARALLEL execution = models.IntegerField(default=EXECUTE_TYPE_NONE, null=False) # status: general status constants defined in parameters.py - status = models.CharField(max_length=32, default='None', null=False) + status = models.CharField(max_length=32, default=TRANSACTION_STATUS_NONE, null=False) # local_status: status code specific to the payment processor local_status = models.CharField(max_length=32, default='NONE', null=True) @@ -72,12 +81,17 @@ class Transaction(models.Model): campaign = models.ForeignKey(Campaign, null=True) premium = models.ForeignKey(Premium, null=True) - # list: makes allowance for pledging against a Wishlist: not currently in use - list = models.ForeignKey(Wishlist, null=True) + # how to acknowledge the user on the supporter page of the campaign ebook + ack_name = models.CharField(max_length=64, null=True) + ack_dedication = models.CharField(max_length=140, null=True) # whether the user wants to be not listed publicly anonymous = models.BooleanField(null=False) + @property + def ack_link(self): + return 'https://unglue.it/supporter/%s'%urllib.urlencode(self.user.username) if not self.anonymous else '' + def save(self, *args, **kwargs): if not self.secret: self.secret = str(uuid.uuid1()) @@ -102,8 +116,52 @@ class Transaction(models.Model): else: mod = __import__("regluit.payment." + self.host, fromlist=[str(self.host)]) return mod - - + + def set_credit_approved(self, amount): + self.amount=amount + self.host = PAYMENT_HOST_CREDIT + self.type = PAYMENT_TYPE_AUTHORIZATION + self.status=TRANSACTION_STATUS_ACTIVE + self.approved=True + now_val = now() + self.date_authorized = now_val + self.date_expired = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD ) + self.save() + pledge_created.send(sender=self, transaction=self) + + def set_pledge_extra(self, pledge_extra): + if pledge_extra: + self.anonymous = pledge_extra.anonymous + self.premium = pledge_extra.premium + self.ack_name = pledge_extra.ack_name + self.ack_dedication = pledge_extra.ack_dedication + + def get_pledge_extra(self, pledge_extra): + return PledgeExtra(anonymous=self.anonymous, + premium=self.premium, + ack_name=self.ack_name, + ack_dedication=self.ack_dedication) + + @classmethod + def create(cls,amount=0.00, host=PAYMENT_HOST_NONE, max_amount=0.00, currency='USD', + status=TRANSACTION_STATUS_NONE,campaign=None, user=None, pledge_extra=None): + if pledge_extra: + return cls.objects.create(amount=amount, + host=host, + max_amount=max_amount, + currency=currency, + status=status, + campaign=campaign, + user=user, + premium=pledge_extra.premium, + anonymous=pledge_extra.anonymous, + ack_name=pledge_extra.ack_name, + ack_dedication=pledge_extra.ack_dedication + ) + else: + return cls.objects.create(amount=amount, host=host, max_amount=max_amount, currency=currency,status=status, + campaign=campaign, user=user) + class PaymentResponse(models.Model): # The API used api = models.CharField(max_length=64, null=False) @@ -142,6 +200,120 @@ class Receiver(models.Model): def __unicode__(self): return u"Receiver -- email: {0} status: {1} transaction: {2}".format(self.email, self.status, unicode(self.transaction)) + +class CreditLog(models.Model): + # a write only record of Donation Credit Transactions + user = models.ForeignKey(User, null=True) + amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 + timestamp = models.DateTimeField(auto_now=True) + action = models.CharField(max_length=16) + # used to record the sent id when action = 'deposit' + sent=models.IntegerField(null=True) + +class Credit(models.Model): + user = models.OneToOneField(User, related_name='credit') + balance = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 + pledged = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 + last_activity = models.DateTimeField(auto_now=True) + + @property + def available(self): + return self.balance - self.pledged + + def add_to_balance(self, num_credits): + if self.pledged - self.balance > num_credits : # negative to withdraw + return False + else: + self.balance = self.balance + num_credits + self.save() + try: # bad things can happen here if you don't return True + CreditLog(user = self.user, amount = num_credits, action="add_to_balance").save() + except: + logger.exception("failed to log add_to_balance of %s", num_credits) + try: + credit_balance_added.send(sender=self, amount=num_credits) + except: + logger.exception("credit_balance_added failed of %s", num_credits) + return True + + def add_to_pledged(self, num_credits): + num_credits=Decimal(num_credits) + if num_credits is Decimal('NaN'): + return False + if self.balance - self.pledged < num_credits : + return False + else: + self.pledged=self.pledged + num_credits + self.save() + try: # bad things can happen here if you don't return True + CreditLog(user = self.user, amount = num_credits, action="add_to_pledged").save() + except: + logger.exception("failed to log add_to_pledged of %s", num_credits) + return True + + def use_pledge(self, num_credits): + if not isinstance( num_credits, int): + return False + if self.pledged < num_credits : + return False + else: + self.pledged=self.pledged - num_credits + self.balance = self.balance - num_credits + self.save() + try: + CreditLog(user = self.user, amount = - num_credits, action="use_pledge").save() + except: + logger.exception("failed to log use_pledge of %s", num_credits) + return True + + def transfer_to(self, receiver, num_credits): + if not isinstance( num_credits, int) or not isinstance( receiver, User): + return False + if self.add_to_balance(-num_credits): + if receiver.credit.add_to_balance(num_credits): + return True + else: + # unwind transfer + self.add_to_balance(num_credits) + return False + else: + return False + +class Sent(models.Model): + '''used by donation view to record donations it has sent''' + user = models.CharField(max_length=32, null=True) + amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 + timestamp = models.DateTimeField(auto_now=True) + +class Account(models.Model): + """holds references to accounts at third party payment gateways, especially for representing credit cards""" + + # the following fields from stripe Customer might be relevant to Account -- we need to pick good selection + # c.id, c.description, c.email, datetime.fromtimestamp(c.created, tz=utc), c.account_balance, c.delinquent, + # c.active_card.fingerprint, c.active_card.type, c.active_card.last4, c.active_card.exp_month, c.active_card.exp_year, + # c.active_card.country + + # host: the payment processor. Named after the payment module that hosts the payment processing functions + host = models.CharField(default=PAYMENT_HOST_NONE, max_length=32, null=False) + account_id = models.CharField(max_length=128, null=True) + + # card related info + card_last4 = models.CharField(max_length=4, null=True) + + # Visa, American Express, MasterCard, Discover, JCB, Diners Club, or Unknown + card_type = models.CharField(max_length=32, null=True) + card_exp_month = models.IntegerField(null=True) + card_exp_year = models.IntegerField(null=True) + card_fingerprint = models.CharField(max_length=32, null=True) + card_country = models.CharField(max_length=2, null=True) + + # creation and last modified timestamps + date_created = models.DateTimeField(auto_now_add=True) + date_modified = models.DateTimeField(auto_now=True) + date_deactivated = models.DateTimeField(null=True) + + # associated User + user = models.ForeignKey(User, null=True) from django.db.models.signals import post_save, post_delete import regluit.payment.manager diff --git a/payment/parameters.py b/payment/parameters.py index 872f07a6..0d408751 100644 --- a/payment/parameters.py +++ b/payment/parameters.py @@ -5,29 +5,30 @@ PAYMENT_TYPE_AUTHORIZATION = 2 PAYMENT_HOST_NONE = "none" PAYMENT_HOST_PAYPAL = "paypal" PAYMENT_HOST_AMAZON = "amazon" +PAYMENT_HOST_STRIPE = "stripe" + +PAYMENT_HOST_TEST = "test" +PAYMENT_HOST_CREDIT = "credit" EXECUTE_TYPE_NONE = 0 EXECUTE_TYPE_CHAINED_INSTANT = 1 EXECUTE_TYPE_CHAINED_DELAYED = 2 EXECUTE_TYPE_PARALLEL = 3 -TARGET_TYPE_NONE = 0 -TARGET_TYPE_CAMPAIGN = 1 -TARGET_TYPE_LIST = 2 -TARGET_TYPE_DONATION = 3 - # The default status for a transaction that is newly created TRANSACTION_STATUS_NONE = 'None' # Indicates a transaction has been sent to the co-branded API TRANSACTION_STATUS_CREATED = 'Created' -# A general complete code to indicate payment is comlete to all receivers +# A general complete code to indicate payment is complete to all receivers TRANSACTION_STATUS_COMPLETE = 'Complete' # A general pending code that means in process TRANSACTION_STATUS_PENDING = 'Pending' +# This means that the max amount has increased but the increase hasn't been executed +TRANSACTION_STATUS_MODIFIED = 'Modified' # Indicates a preapproval is active TRANSACTION_STATUS_ACTIVE = 'Active' @@ -47,6 +48,3 @@ TRANSACTION_STATUS_REFUNDED = 'Refunded' # The transaction was refused/denied TRANSACTION_STATUS_FAILED = 'Failed' -# these two following parameters are probably extraneous since I think we will compute dynamically where to return each time. -COMPLETE_URL = '/paymentcomplete' -NEVERMIND_URL = '/paymentnevermind' diff --git a/payment/paypal.py b/payment/paypal.py deleted file mode 100644 index 0d3dd26a..00000000 --- a/payment/paypal.py +++ /dev/null @@ -1,1023 +0,0 @@ -from regluit.payment.parameters import * -from django.core.urlresolvers import reverse -from django.conf import settings -from regluit.payment.models import Transaction, Receiver -from django.contrib.auth.models import User -from django.utils import simplejson as json -from django.utils.xmlutils import SimplerXMLGenerator -from django.db import IntegrityError -from django.db.models.query_utils import Q -from django.shortcuts import render_to_response -from django.template import RequestContext - -from datetime import timedelta -from regluit.utils.localdatetime import now, zuluformat -import dateutil - -import dateutil.parser -import hashlib -import httplib -import traceback -import uuid -import os -import urllib -import urllib2 -import logging -import random -import commands -import smtplib -import urlparse -import decimal -import sys - -logger = logging.getLogger(__name__) - -# transaction_type constants -IPN_TYPE_PAYMENT = 'Adaptive Payment PAY' -IPN_TYPE_ADJUSTMENT = 'Adjustment' -IPN_TYPE_PREAPPROVAL = 'Adaptive Payment PREAPPROVAL' - -#pay API status constants -# NONE' is not something the API produces but is particular to our implementation -IPN_PAY_STATUS_NONE = 'NONE' - - -#The following apply to INSTANT PAYMENTS -#https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_APPayAPI -#CREATED - The payment request was received; funds will be transferred once the payment is approved -#COMPLETED - The payment was successful -#INCOMPLETE - Some transfers succeeded and some failed for a parallel payment or, for a delayed chained payment, secondary receivers have not been paid -#ERROR - The payment failed and all attempted transfers failed or all completed transfers were successfully reversed -#REVERSALERROR - One or more transfers failed when attempting to reverse a payment -#PROCESSING - The payment is in progress -#PENDING - The payment is awaiting processing - -IPN_PAY_STATUS_CREATED = 'CREATED' -IPN_PAY_STATUS_COMPLETED = 'COMPLETED' -IPN_PAY_STATUS_INCOMPLETE = 'INCOMPLETE' -IPN_PAY_STATUS_ERROR = 'ERROR' -IPN_PAY_STATUS_REVERSALERROR = 'REVERSALERROR' -IPN_PAY_STATUS_PROCESSING = 'PROCESSING' -IPN_PAY_STATUS_PENDING = 'PENDING' - -# particular to preapprovals -# https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_APPreapprovalDetails -#ACTIVE - The preapproval is active -#CANCELED - The preapproval was explicitly canceled by the sender or by PayPal -#DEACTIVED - The preapproval is not active; you can be reactivate it by resetting the personal identification number (PIN) or by contacting PayPal - -IPN_PREAPPROVAL_STATUS_ACTIVE = "ACTIVE" -IPN_PREAPPROVAL_STATUS_CANCELED = "CANCELED" -IPN_PREAPPROVAL_STATUS_DEACTIVED = "DEACTIVED" - -# https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_APIPN -#COMPLETED - The sender's transaction has completed -#PENDING - The transaction is awaiting further processing -#CREATED - The payment request was received; funds will be transferred once approval is received -#PARTIALLY_REFUNDED - Transaction was partially refunded -#DENIED - The transaction was rejected by the receiver -#PROCESSING - The transaction is in progress -#REVERSED - The payment was returned to the sender -#REFUNDED - The payment was refunded -#FAILED - The payment failed - -IPN_SENDER_STATUS_COMPLETED = 'COMPLETED' -IPN_SENDER_STATUS_PENDING = 'PENDING' -IPN_SENDER_STATUS_CREATED = 'CREATED' -IPN_SENDER_STATUS_PARTIALLY_REFUNDED = 'PARTIALLY_REFUNDED' -IPN_SENDER_STATUS_DENIED = 'DENIED' -IPN_SENDER_STATUS_PROCESSING = 'PROCESSING' -IPN_SENDER_STATUS_REVERSED = 'REVERSED' -IPN_SENDER_STATUS_REFUNDED = 'REFUNDED' -IPN_SENDER_STATUS_FAILED = 'FAILED' - -# action_type constants -IPN_ACTION_TYPE_PAY = 'PAY' -IPN_ACTION_TYPE_CREATE = 'CREATE' - -# individual sender transaction constants (???) -IPN_TXN_STATUS_COMPLETED = 'Completed' -IPN_TXN_STATUS_PENDING = 'Pending' -IPN_TXN_STATUS_REFUNDED = 'Refunded' - -# adaptive payment adjusted IPN reason codes -IPN_REASON_CODE_CHARGEBACK_SETTLEMENT = 'Chargeback Settlement' -IPN_REASON_CODE_ADMIN_REVERSAL = 'Admin reversal' -IPN_REASON_CODE_REFUND = 'Refund' - -def PaypalStatusToGlobalStatus(status): - ''' - This represents the default mapping of paypal status codes to global status codes - There are cases where this mapping will not apply and the individual API calls must do - additional processing - ''' - - if status == IPN_PAY_STATUS_CREATED: - return TRANSACTION_STATUS_CREATED - - elif status == IPN_PAY_STATUS_COMPLETED: - return TRANSACTION_STATUS_COMPLETE - - elif status == IPN_PAY_STATUS_INCOMPLETE: - return TRANSACTION_STATUS_INCOMPLETE - - elif status == IPN_PAY_STATUS_ERROR: - return TRANSACTION_STATUS_ERROR - - elif status == IPN_PAY_STATUS_REVERSALERROR: - return TRANSACTION_STATUS_ERROR - - elif status == IPN_PAY_STATUS_PROCESSING: - return TRANSACTION_STATUS_PENDING - - elif status == IPN_PAY_STATUS_PENDING: - return TRANSACTION_STATUS_PENDING - - elif status == IPN_PREAPPROVAL_STATUS_ACTIVE: - return TRANSACTION_STATUS_ACTIVE - - elif status == IPN_PREAPPROVAL_STATUS_CANCELED: - return TRANSACTION_STATUS_CANCELED - - elif status == IPN_PREAPPROVAL_STATUS_DEACTIVED: - return TRANSACTION_STATUS_CANCELED - - elif status == IPN_SENDER_STATUS_COMPLETED: - return TRANSACTION_STATUS_COMPLETE - - elif status == IPN_SENDER_STATUS_PENDING: - return TRANSACTION_STATUS_CPENDING - - elif status == IPN_SENDER_STATUS_CREATED: - return TRANSACTION_STATUS_CREATED - - elif status == IPN_SENDER_STATUS_PARTIALLY_REFUNDED: - return TRANSACTION_STATUS_REFUNDED - - elif status == IPN_SENDER_STATUS_DENIED: - return TRANSACTION_STATUS_FAILED - - elif status == IPN_SENDER_STATUS_PROCESSING: - return TRANSACTION_STATUS_PENDING - - elif status == IPN_SENDER_STATUS_REVERSED: - return TRANSACTION_STATUS_REFUNDED - - elif status == IPN_SENDER_STATUS_REFUNDED: - return TRANSACTION_STATUS_REFUNDED - - elif status == IPN_SENDER_STATUS_FAILED: - return TRANSACTION_STATUS_FAILED - - else: - return TRANSACTION_STATUS_ERRROR - - -def ProcessIPN(request): - ''' - processIPN - - Turns a request from Paypal into an IPN, and extracts info. We support 2 types of IPNs: - - 1) Payment - Used for instant payments and to execute pre-approved payments - 2) Preapproval - Used for comfirmation of a preapproval - - ''' - try: - ipn = IPN(request) - - if ipn.success(): - logger.info("Valid IPN") - logger.info("IPN Transaction Type: %s" % ipn.transaction_type) - - if ipn.transaction_type == IPN_TYPE_PAYMENT: - # payment IPN. we use our unique reference for the transaction as the key - # is only valid for 3 hours - - uniqueID = ipn.uniqueID() - t = Transaction.objects.get(secret=uniqueID) - - # The local_status is always one of the IPN_PAY_STATUS codes defined in paypal.py - t.local_status = ipn.status - t.status = PaypalStatusToGlobalStatus(ipn.status) - - for item in ipn.transactions: - - try: - r = Receiver.objects.get(transaction=t, email=item['receiver']) - logger.info(item) - # one of the IPN_SENDER_STATUS codes defined in paypal.py, If we are doing delayed chained - # payments, then there is no status or id for non-primary receivers. Leave their status alone - r.status = item['status_for_sender_txn'] - r.txn_id = item['id_for_sender_txn'] - r.save() - except: - # Log an exception if we have a receiver that is not found. This will be hit - # for delayed chained payments as there is no status or id for the non-primary receivers yet - traceback.print_exc() - - t.save() - - logger.info("Final transaction status: %s" % t.status) - - elif ipn.transaction_type == IPN_TYPE_ADJUSTMENT: - # a chargeback, reversal or refund for an existng payment - - uniqueID = ipn.uniqueID() - if uniqueID: - t = Transaction.objects.get(secret=uniqueID) - else: - key = ipn.pay_key - t = Transaction.objects.get(pay_key=key) - - # The status is always one of the IPN_PAY_STATUS codes defined in paypal.py - t.local_status = ipn.status - t.status = PaypalStatusToGlobalStatus(ipn.status) - - # Reason code indicates more details of the adjustment type - t.reason = ipn.reason_code - - # Update the receiver status codes - for item in ipn.transactions: - - try: - r = Receiver.objects.get(transaction=t, email=item['receiver']) - logger.info(item) - # one of the IPN_SENDER_STATUS codes defined in paypal.py, If we are doing delayed chained - # payments, then there is no status or id for non-primary receivers. Leave their status alone - r.status = item['status_for_sender_txn'] - r.save() - except: - # Log an exception if we have a receiver that is not found. This will be hit - # for delayed chained payments as there is no status or id for the non-primary receivers yet - traceback.print_exc() - - t.save() - - - elif ipn.transaction_type == IPN_TYPE_PREAPPROVAL: - - # IPN for preapproval always uses the key to ref the transaction as this is always valid - key = ipn.preapproval_key - t = Transaction.objects.get(preapproval_key=key) - - # The status is always one of the IPN_PREAPPROVAL_STATUS codes defined in paypal.py - t.local_status = ipn.status - t.status = PaypalStatusToGlobalStatus(ipn.status) - - # capture whether the transaction has been approved - t.approved = ipn.approved - - t.save() - logger.info("IPN: Preapproval transaction: " + str(t.id) + " Status: " + ipn.status) - - else: - logger.info("IPN: Unknown Transaction Type: " + ipn.transaction_type) - - - else: - logger.info("ERROR: INVALID IPN") - logger.info(ipn.error) - - except: - traceback.print_exc() - - -class PaypalError(RuntimeError): - pass - -class url_request( object ): - - def __init__( self, request): - - conn = httplib.HTTPSConnection(settings.PAYPAL_ENDPOINT) - conn.request("POST", request.url, request.raw_request, request.headers) - - #Check the response - should be 200 OK. - self.response = conn.getresponse() - - def content( self ): - return self.response.read() - - def code( self ): - return self.response.status - -class PaypalEnvelopeRequest: - ''' - Handles common information that is processed from the response envelope of the paypal request. - - All of our requests have a response envelope of the following format: - - ack common:AckCode - Acknowledgement code. It is one of the following values: - Success - The operation completed successfully. - Failure - The operation failed. - Warning - Warning. - SuccessWithWarning - The operation completed successfully; however, there is a warning message. - FailureWithWarning - The operation failed with a warning message. - build Build number; it is used only by Developer Technical Support. - correlationId Correlation ID; it is used only by Developer Technical Support. - timestamp Date on which the response was sent. The time is currently not supported. - - Additionally, our subclasses may set the error_message field if an undetermined error occurs. Examples of undertmined errors are: - HTTP error codes(not 200) - Invalid parameters - Python exceptions during processing - - All clients should check both the success() and the error() functions to determine the result of the operation - - ''' - - # Global values for the class - response = None - raw_response = None - errorMessage = None - raw_request = None - url = None - - def ack( self ): - - if self.response and self.response.has_key( 'responseEnvelope' ) and self.response['responseEnvelope'].has_key( 'ack' ): - return self.response['responseEnvelope']['ack'] - else: - return None - - def success(self): - status = self.ack() - - # print status - if status == "Success" or status == "SuccessWithWarning": - return True - else: - return False - - def error(self): - message = self.errorMessage - # print message - if message: - return True - else: - return False - - def error_data(self): - - if self.response and self.response.has_key('error'): - return self.response['error'] - else: - return None - - def error_id(self): - - if self.response and self.response.has_key('error'): - return self.response['error'][0]['errorId'] - else: - return None - - def error_string(self): - - if self.response and self.response.has_key('error'): - return self.response['error'][0]['message'] - - elif self.errorMessage: - return self.errorMessage - - else: - return None - - def envelope(self): - - if self.response and self.response.has_key('responseEnvelope'): - return self.response['responseEnvelope'] - else: - return None - - def correlation_id(self): - if self.response and self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('correlationId'): - return self.response['responseEnvelope']['correlationId'] - else: - return None - - def timestamp(self): - if self.response and self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('timestamp'): - return self.response['responseEnvelope']['timestamp'] - else: - return None - -class Pay( PaypalEnvelopeRequest ): - def __init__( self, transaction, return_url=None, nevermind_url=None, paymentReason=""): - - #BUGBUG: though I'm passing in paymentReason (to make it signature compatible with Amazon, it's not being wired in properly yet) - try: - - headers = { - 'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME, - 'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD, - 'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE, - 'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID, - 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON', - 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON' - } - - if return_url is None: - return_url = settings.BASE_URL + COMPLETE_URL - if nevermind_url is None: - nevermind_url = settings.BASE_URL + nevermind_url - - logger.info("Return URL: " + return_url) - logger.info("Cancel URL: " + nevermind_url) - - receiver_list = [] - receivers = transaction.receiver_set.all() - - if len(receivers) == 0: - raise Exception - - # by setting primary_string of the first receiver to 'true', we are doing a Chained payment - for r in receivers: - if len(receivers) > 1: - if r.primary and (transaction.execution == EXECUTE_TYPE_CHAINED_INSTANT or transaction.execution == EXECUTE_TYPE_CHAINED_DELAYED): - # Only set a primary if we are using chained payments - primary_string = 'true' - else: - primary_string = 'false' - - receiver_list.append({'email':r.email,'amount':str(r.amount), 'primary':primary_string}) - else: - receiver_list.append({'email':r.email,'amount':str(r.amount)}) - - logger.info(receiver_list) - - # actionType can be 'PAY', 'CREATE', or 'PAY_PRIMARY' - # PAY_PRIMARY': "For chained payments only, specify this value to delay payments to the secondary receivers; only the payment to the primary receiver is processed" - - if transaction.execution == EXECUTE_TYPE_CHAINED_DELAYED: - self.actionType = 'PAY_PRIMARY' - else: - self.actionType = 'PAY' - - # feesPayer: SENDER, PRIMARYRECEIVER, EACHRECEIVER, SECONDARYONLY - # The PayPal documentation says that fees for delayed chain payments cannot be set to secondaryonly - # but the sandbox seems to show the secondary recipient paying all the fees. - # if only one receiver, set to EACHRECEIVER, otherwise set to SECONDARYONLY - - if len(receivers) == 1: - feesPayer = 'EACHRECEIVER' - else: - feesPayer = 'SECONDARYONLY' - - data = { - 'actionType': self.actionType, - 'receiverList': { 'receiver': receiver_list }, - 'currencyCode': transaction.currency, - 'returnUrl': return_url, - 'cancelUrl': nevermind_url, - 'requestEnvelope': { 'errorLanguage': 'en_US' }, - 'ipnNotificationUrl': settings.BASE_URL + reverse('HandleIPN', args=["paypal"]), - 'feesPayer': feesPayer, - 'trackingId': transaction.secret - } - - logging.info("paypal PAY data: %s" % data) - #print >> sys.stderr, "paypal PAY data:", data - # Is ipnNotificationUrl being computed properly - #print >> sys.stderr, 'ipnNotificationUrl', settings.BASE_URL + reverse('PayPalIPN') - - # a Pay operation can be for a payment that goes through immediately or for setting up a preapproval. - # transaction.reference is not null if it represents a preapproved payment, which has a preapprovalKey. - if transaction.preapproval_key: - data['preapprovalKey'] = transaction.preapproval_key - - self.raw_request = json.dumps(data) - self.url = "/AdaptivePayments/Pay" - self.headers = headers - self.connection = url_request(self) - self.code = self.connection.code() - - if self.code != 200: - self.errorMessage = 'PayPal response code was %i' % self.code - return - - self.raw_response = self.connection.content() - #print >> sys.stderr, "PAY request", settings.PAYPAL_ENDPOINT, "/AdaptivePayments/Pay", self.raw_request, headers - logger.info("paypal PAY response was: %s" % self.raw_response) - #print >> sys.stderr, "paypal PAY response was:", self.raw_response - self.response = json.loads( self.raw_response ) - logger.info(self.response) - - except: - traceback.print_exc() - self.errorMessage = "Error: Server Error" - - def api(self): - return self.actionType - - def exec_status( self ): - if self.response.has_key( 'paymentExecStatus' ): - return self.response['paymentExecStatus'] - else: - return None - - def amount( self ): - if self.response.has_key('payment_gross'): - return self.response['payment_gross'] - else: - return None - - def key( self ): - if self.response.has_key('payKey'): - return self.response['payKey'] - else: - return None - - def next_url( self ): - return '%s?cmd=_ap-payment&paykey=%s' % (settings.PAYPAL_PAYMENT_HOST, self.response['payKey'] ) - - def embedded_url(self): - return '%s/webapps/adaptivepayment/flow/pay?paykey=%s&expType=light' % ( settings.PAYPAL_PAYMENT_HOST, self.response['payKey'] ) - - -class Execute(Pay): - ''' - For payapl, execute is the same as pay. The pay funciton detects whether an execute or a co-branded operation - is called for. - ''' - def __init__(self, transaction, return_url=None, nevermind_url=None): - # Call our super class. In python 2.2+, we can't use super here, so just call init directly - Pay.__init__(self, transaction, return_url, nevermind_url) - -class Finish(PaypalEnvelopeRequest): - - def __init__(self, transaction=None): - - try: - - self.errorMessage = None - self.response = None - - headers = { - 'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME, - 'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD, - 'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE, - 'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID, - 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON', - 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON' - } - - - if transaction.execution != EXECUTE_TYPE_CHAINED_DELAYED: - self.errorMessage = "Invalid transaction type for execution" - return - - if not transaction.pay_key: - self.errorMessage = "No Paykey Found in transaction" - return - - data = { - 'payKey': transaction.pay_key, - 'requestEnvelope': { 'errorLanguage': 'en_US' } - } - - logging.info("paypal EXECUTE data: %s" % data) - self.raw_request = json.dumps(data) - self.url = "/AdaptivePayments/ExecutePayment" - self.headers = headers - self.connection = url_request(self) - self.code = self.connection.code() - - if self.code != 200: - self.errorMessage = 'PayPal response code was %i' % self.code - return - - self.raw_response = self.connection.content() - - logger.info("paypal EXECUTE response was: %s" % self.raw_response) - self.response = json.loads( self.raw_response ) - - except: - traceback.print_exc() - self.errorMessage = "Error: Server error occurred" - - -class PaymentDetails(PaypalEnvelopeRequest): - - ''' - Get details about executed PAY operation - - This api must set the following class variables to work with the code in manager.py - - status - one of the global transaction status codes - transactions -- A list of all receiver transactions associated with this payment - status - the status of the receiver transaction - txn_id - the id of the receiver transaction - - ''' - - def __init__(self, transaction=None): - - try: - self.transaction = transaction - - headers = { - 'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME, - 'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD, - 'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE, - 'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID, - 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON', - 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON' - } - - # we can feed any of payKey, transactionId, and trackingId to identify transaction in question - # I think we've been tracking payKey. We might want to use our own trackingId (what's Transaction.secret for?) - data = { - 'requestEnvelope': { 'errorLanguage': 'en_US' }, - 'trackingId':transaction.secret - } - - self.raw_request = json.dumps(data) - self.headers = headers - self.url = "/AdaptivePayments/PaymentDetails" - self.connection = url_request(self) - - self.code = self.connection.code() - - if self.code != 200: - self.errorMessage = 'PayPal response code was %i' % self.code - return - - self.raw_response = self.connection.content() - - logger.info("paypal PaymentDetails response was: %s" % self.raw_response) - self.response = json.loads( self.raw_response ) - logger.info(self.response) - - self.local_status = self.response.get("status", None) - self.status = PaypalStatusToGlobalStatus(self.local_status) - - self.trackingId = self.response.get("trackingId", None) - self.feesPayer = self.response.get("feesPayer", None) - payment_info_list = self.response.get("paymentInfoList", None) - payment_info = payment_info_list.get("paymentInfo", None) - - self.transactions = [] - for payment in payment_info: - receiver = {} - receiver['status'] = payment.get("transactionStatus", None) - receiver['txn_id'] = payment.get("transactionId") - - r = payment.get("receiver", None) - if r: - receiver['email'] = r.get('email') - - - self.transactions.append(receiver) - - except: - self.errorMessage = "Error: ServerError" - traceback.print_exc() - - - def compare(self): - """compare current status information from what's in the current transaction object""" - # I don't think we do anything with fundingtypeList, memo - # status can be: - # transaction.type should be PAYMENT_TYPE_INSTANT - # actionType can be: 'PAY', 'CREATE', 'PAY_PRIMARY' -- I think we are doing only 'PAY' right now - - comp = [(self.transaction.status, self.response.get('status')), - (self.transaction.type, self.response.get('actionType')), - (self.transaction.currency, self.response.get('currencyCode')), - ('EACHRECEIVER' if len(self.transaction.receiver_set.all()) == 1 else 'SECONDARYONLY',self.response.get('feesPayer')), - (self.transaction.reference, self.response.get('payKey')), # payKey supposedly expires after 3 hours - ('false', self.response.get('reverseAllParallelPaymentsOnError')), - (None, self.response.get('sender')) - ] - - # loop through recipients - - return comp - - # also getting sender / senderEmail info too here that we don't currently hold in transaction. Want to save? Does that info come in IPN? - # responseEnvelope - - # reverseAllParallelPaymentsOnError - # self.response.get('responseEnvelope')['ack'] should be 'Success' Can also be 'Failure', 'Warning', 'SuccessWithWarning', 'FailureWithWarning' - # can possibly use self.response.get('responseEnvelope')['timestamp'] to update self.transaction.date_modified - # preapprovalKey -- self.transaction doesn't hold that info right now - # paymentInfoList -- holds info for each recipient - - -class CancelPreapproval(PaypalEnvelopeRequest): - - def __init__(self, transaction): - - try: - - headers = { - 'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME, - 'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD, - 'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE, - 'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID, - 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON', - 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON', - } - - data = { - 'preapprovalKey':transaction.preapproval_key, - 'requestEnvelope': { 'errorLanguage': 'en_US' } - } - - self.raw_request = json.dumps(data) - self.headers = headers - self.url = "/AdaptivePayments/CancelPreapproval" - self.connection = url_request(self) - self.code = self.connection.code() - - if self.code != 200: - self.errorMessage = 'PayPal response code was %i' % self.code - return - - self.raw_response = self.connection.content() - logger.info("paypal CANCEL PREAPPROBAL response was: %s" % self.raw_response) - self.response = json.loads( self.raw_response ) - logger.info(self.response) - - except: - traceback.print_exc() - self.errorMessage = "Error: Server Error" - - -class RefundPayment(PaypalEnvelopeRequest): - - def __init__(self, transaction): - - try: - - headers = { - 'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME, - 'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD, - 'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE, - 'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID, - 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON', - 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON', - } - - data = { - 'payKey':transaction.pay_key, - 'requestEnvelope': { 'errorLanguage': 'en_US' } - } - - self.raw_request = json.dumps(data) - self.headers = headers - self.url = "/AdaptivePayments/Refund" - self.connection = url_request(self) - self.code = self.connection.code() - - if self.code != 200: - self.errorMessage = 'PayPal response code was %i' % self.code - return - - self.raw_response = self.connection.content() - logger.info("paypal Refund response was: %s" % self.raw_response) - self.response = json.loads( self.raw_response ) - logger.info(self.response) - - except: - traceback.print_exc() - self.errorMessage = "Error: Server Error" - - -class Preapproval( PaypalEnvelopeRequest ): - def __init__( self, transaction, amount, expiry=None, return_url=None, nevermind_url=None, paymentReason=""): - - # BUGBUG: though I'm passing in paymentReason (to make it signature compatible with Amazon, it's not being wired in properly yet) - - try: - - headers = { - 'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME, - 'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD, - 'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE, - 'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID, - 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON', - 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON', - } - - if return_url is None: - return_url = settings.BASE_URL + COMPLETE_URL - if nevermind_url is None: - nevermind_url = settings.BASE_URL + NEVERMIND_URL - - # set the expiration date for the preapproval if not passed in - now_val = now() - if expiry is None: - expiry = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD ) - transaction.date_authorized = now_val - transaction.date_expired = expiry - transaction.save() - - data = { - 'endingDate': zuluformat(expiry), - 'startingDate': zuluformat(now_val), - 'maxTotalAmountOfAllPayments': '%.2f' % transaction.amount, - 'maxNumberOfPayments':1, - 'maxAmountPerPayment': '%.2f' % transaction.amount, - 'currencyCode': transaction.currency, - 'returnUrl': return_url, - 'cancelUrl': nevermind_url, - 'requestEnvelope': { 'errorLanguage': 'en_US' }, - 'ipnNotificationUrl': settings.BASE_URL + reverse('HandleIPN', args=["paypal"]) - } - - # Is ipnNotificationUrl being computed properly - # print >> sys.stderr, 'ipnNotificationUrl', settings.BASE_URL + reverse('PayPalIPN') - - self.raw_request = json.dumps(data) - self.url = "/AdaptivePayments/Preapproval" - self.headers = headers - self.connection = url_request(self) - self.code = self.connection.code() - - if self.code != 200: - self.errorMessage = 'PayPal response code was %i' % self.code - return - - self.raw_response = self.connection.content() - logger.info("paypal PREAPPROVAL response was: %s" % self.raw_response) - # print >> sys.stderr, "paypal PREAPPROVAL response was:", self.raw_response - self.response = json.loads( self.raw_response ) - logger.info(self.response) - - except: - traceback.print_exc() - self.errorMessage = "Error: Server Error Occurred" - - def key( self ): - if self.response.has_key( 'preapprovalKey' ): - return self.response['preapprovalKey'] - else: - return None - - def next_url( self ): - return '%s?cmd=_ap-preapproval&preapprovalkey=%s' % ( settings.PAYPAL_PAYMENT_HOST, self.response['preapprovalKey'] ) - - -class PreapprovalDetails(PaypalEnvelopeRequest): - - ''' - Get details about an authorized token - - This api must set 4 different class variables to work with the code in manager.py - - status - one of the global transaction status codes - approved - boolean value - currency - - amount - - - ''' - - def __init__(self, transaction): - - try: - self.transaction = transaction - - headers = { - 'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME, - 'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD, - 'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE, - 'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID, - 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON', - 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON' - } - - # we can feed any of payKey, transactionId, and trackingId to identify transaction in question - # I think we've been tracking payKey. We might want to use our own trackingId (what's Transaction.secret for?) - data = { - 'requestEnvelope': { 'errorLanguage': 'en_US' }, - 'preapprovalKey':transaction.preapproval_key - } - - self.raw_request = json.dumps(data) - self.headers = headers - self.url = "/AdaptivePayments/PreapprovalDetails" - self.connection = url_request(self) - self.code = self.connection.code() - - if self.code != 200: - self.errorMessage = 'PayPal response code was %i' % self.code - return - - self.raw_response = self.connection.content() - logger.info("paypal PreapprovalDetails response was: %s" % self.raw_response) - self.response = json.loads( self.raw_response ) - logger.info(self.response) - - self.local_status = self.response.get("status", None) - self.status = PaypalStatusToGlobalStatus(self.local_status) - - self.amount = self.response.get("maxTotalAmountOfAllPayments", None) - self.currency = self.response.get("currencyCode", None) - - # a bit uncertain about how well PayPal sticks to a standard case - approved = self.response.get("approved", 'None') - if approved.lower() == 'true': - self.approved = True - elif approved.lower() == 'false': - self.approved = False - else: - self.approved = None - - try: - self.expiration = dateutil.parser.parse(self.response.get("endingDate")) - except: - self.expiration = None - - try: - self.date = dateutil.parser.parse(self.response.get("startingDate", None)) - except: - self.date = None - - except: - self.errorMessage = "Error: ServerError" - traceback.print_exc() - - -class IPN( object ): - - def __init__( self, request ): - - try: - # verify that the request is paypal's - self.error = None - - url = "%s?cmd=_notify-validate" % settings.PAYPAL_PAYMENT_HOST - data=urllib.urlencode(request.POST.copy()) - req = urllib2.Request(url, data) - response = urllib2.urlopen(req) - raw_response = response.read() - status = response.code - - # check code - if status != 200: - self.error = 'PayPal response code was %i' % verify_response.code() - return - - # check response - if raw_response != 'VERIFIED': - self.error = 'PayPal response was "%s"' % raw_response - return - - # Process the details - self.status = request.POST.get('status', None) - self.sender_email = request.POST.get('sender_email', None) - self.action_type = request.POST.get('action_type', None) - self.pay_key = request.POST.get('pay_key', None) - self.preapproval_key = request.POST.get('preapproval_key', None) - self.transaction_type = request.POST.get('transaction_type', None) - self.reason_code = request.POST.get('reason_code', None) - self.trackingId = request.POST.get('tracking_id', None) - - # a bit uncertain about how well PayPal sticks to a standard case - approved = request.POST.get("approved", 'None') - if approved.lower() == 'true': - self.approved = True - elif approved.lower() == 'false': - self.approved = False - else: - self.approved = None - - self.process_transactions(request) - - except: - self.error = "Error: ServerError" - traceback.print_exc() - - def uniqueID(self): - - if self.trackingId: - return self.trackingId - else: - return None - - - def success( self ): - return self.error == None - - - @classmethod - def slicedict(cls, d, s): - return dict((str(k.replace(s, '', 1)), v) for k,v in d.iteritems() if k.startswith(s)) - - def process_transactions(self, request): - - self.transactions = [] - - transaction_nums = range(6) - for transaction_num in transaction_nums: - transdict = IPN.slicedict(request.POST, 'transaction[%s].' % transaction_num) - if len(transdict) > 0: - self.transactions.append(transdict) - logger.info(transdict) - - - - diff --git a/payment/signals.py b/payment/signals.py index 6f13ce85..5117fd5c 100644 --- a/payment/signals.py +++ b/payment/signals.py @@ -1,5 +1,38 @@ +from notification import models as notification from django.dispatch import Signal transaction_charged = Signal(providing_args=["transaction"]) pledge_created = Signal(providing_args=["transaction"]) -pledge_modified = Signal(providing_args=["transaction", "up_or_down"]) \ No newline at end of file +pledge_modified = Signal(providing_args=["transaction", "up_or_down"]) +credit_balance_added = Signal(providing_args=["amount"]) + +from django.db.models.signals import post_save +from django.db.utils import DatabaseError +from django.db.models import get_model +from django.contrib.auth.models import User + +# create Credit to associate with User +def create_user_objects(sender, created, instance, **kwargs): + # use get_model to avoid circular import problem with models + try: + Credit = get_model('payment', 'Credit') + if created: + Credit.objects.create(user=instance) + except DatabaseError: + # this can happen when creating superuser during syncdb since the + # core_wishlist table doesn't exist yet + return + +post_save.connect(create_user_objects, sender=User) + +def handle_credit_balance(sender, amount=0, **kwargs): + notification.queue([sender.user], "pledge_donation_credit", { + 'user':sender.user, + 'amount':amount, + 'minus_amount':-amount + }, True) + from regluit.core.tasks import emit_notifications + emit_notifications.delay() + +# successful_campaign -> send notices +credit_balance_added.connect(handle_credit_balance) \ No newline at end of file diff --git a/payment/stripelib.py b/payment/stripelib.py new file mode 100644 index 00000000..8e6fa9f0 --- /dev/null +++ b/payment/stripelib.py @@ -0,0 +1,274 @@ +# https://github.com/stripe/stripe-python +# https://stripe.com/docs/api?lang=python#top + +from datetime import datetime +from pytz import utc + +import stripe + +try: + import unittest + from unittest import TestCase +except: + from django.test import TestCase + from django.utils import unittest + +# if customer.id doesn't exist, create one and then charge the customer +# we probably should ask our users whether they are ok with our creating a customer id account -- or ask for credit +# card info each time.... + +# should load the keys for Stripe from db -- but for now just hardcode here +# moving towards not having the stripe api key for the non profit partner in the unglue.it code -- but in a logically +# distinct application + +try: + from regluit.core.models import Key + STRIPE_PK = Key.objects.get(name="STRIPE_PK").value + STRIPE_SK = Key.objects.get(name="STRIPE_SK").value + STRIPE_PARTNER_PK = Key.objects.get(name="STRIPE_PARTNER_PK").value + STRIPE_PARTNER_SK = Key.objects.get(name="STRIPE_PARTNER_SK").value + logger.info('Successful loading of STRIPE_*_KEYs') +except Exception, e: + # currently test keys for Gluejar and for raymond.yee@gmail.com as standin for non-profit + STRIPE_PK = 'pk_0EajXPn195ZdF7Gt7pCxsqRhNN5BF' + STRIPE_SK = 'sk_0EajIO4Dnh646KPIgLWGcO10f9qnH' + STRIPE_PARTNER_PK ='pk_0AnIkNu4WRiJYzxMKgruiUwxzXP2T' + STRIPE_PARTNER_SK = 'sk_0AnIvBrnrJoFpfD3YmQBVZuTUAbjs' + +# set default stripe api_key to that of unglue.it + +stripe.api_key = STRIPE_SK + +# https://stripe.com/docs/testing + +TEST_CARDS = ( + ('4242424242424242', 'Visa'), + ('4012888888881881', 'Visa'), + ('5555555555554444', 'MasterCard'), + ('5105105105105100', 'MasterCard'), + ('378282246310005', 'American Express'), + ('371449635398431', 'American Express'), + ('6011111111111117', 'Discover'), + ('6011000990139424', 'Discover'), + ('30569309025904', "Diner's Club"), + ('38520000023237', "Diner's Club"), + ('3530111333300000', 'JCB'), + ('3566002020360505','JCB') +) + +ERROR_TESTING = dict(( + ('ADDRESS1_ZIP_FAIL', ('4000000000000010', 'address_line1_check and address_zip_check will both fail')), + ('ADDRESS1_FAIL', ('4000000000000028', 'address_line1_check will fail.')), + ('ADDRESS_ZIP_FAIL', ('4000000000000036', 'address_zip_check will fail.')), + ('CVC_CHECK_FAIL', ('4000000000000101', 'cvc_check will fail.')), + ('BAD_ATTACHED_CARD', ('4000000000000341', 'Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.')), + ('CHARGE_DECLINE', ('4000000000000002', 'Charges with this card will always be declined.')) +)) + +# types of errors / when they can be handled + +#card_declined: Use this special card number - 4000000000000002. +#incorrect_number: Use a number that fails the Luhn check, e.g. 4242424242424241. +#invalid_expiry_month: Use an invalid month e.g. 13. +#invalid_expiry_year: Use a year in the past e.g. 1970. +#invalid_cvc: Use a two digit number e.g. 99. + + +def filter_none(d): + return dict([(k,v) for (k,v) in d.items() if v is not None]) + +# if you create a Customer object, then you'll be able to charge multiple times. You can create a customer with a token. + +# https://stripe.com/docs/tutorials/charges + +def card (number=TEST_CARDS[0][0], exp_month='01', exp_year='2020', cvc=None, name=None, + address_line1=None, address_line2=None, address_zip=None, address_state=None, address_country=None): + + card = { + "number": number, + "exp_month": str(exp_month), + "exp_year": str(exp_year), + "cvc": str(cvc) if cvc is not None else None, + "name": name, + "address_line1": address_line1, + "address_line2": address_line2, + "address_zip": address_zip, + "address_state": address_state, + "address_country": address_country + } + + return filter_none(card) + + +class StripeClient(object): + def __init__(self, api_key=STRIPE_SK): + self.api_key = api_key + + # key entities: Charge, Customer, Token, Event + + @property + def charge(self): + return stripe.Charge(api_key=self.api_key) + + @property + def customer(self): + return stripe.Customer(api_key=self.api_key) + + @property + def token(self): + return stripe.Token(api_key=self.api_key) + + @property + def transfer(self): + return stripe.Transfer(api_key=self.api_key) + + @property + def event(self): + return stripe.Event(api_key=self.api_key) + + + def create_token(self, card): + return stripe.Token(api_key=self.api_key).create(card=card) + + def create_customer (self, card=None, description=None, email=None, account_balance=None, plan=None, trial_end=None): + """card is a dictionary or a token""" + # https://stripe.com/docs/api?lang=python#create_customer + + customer = stripe.Customer(api_key=self.api_key).create( + card=card, + description=description, + email=email, + account_balance=account_balance, + plan=plan, + trial_end=trial_end + ) + + # customer.id is useful to save in db + return customer + + + def create_charge(self, amount, currency="usd", customer=None, card=None, description=None ): + # https://stripe.com/docs/api?lang=python#create_charge + # customer or card required but not both + # charge the Customer instead of the card + # amount in cents + + charge = stripe.Charge(api_key=self.api_key).create( + amount=int(100*amount), # in cents + currency=currency, + customer=customer.id if customer is not None else None, + card=card, + description=description + ) + + return charge + + def refund_charge(self, charge_id): + # https://stripe.com/docs/api?lang=python#refund_charge + ch = stripe.Charge(api_key=self.api_key).retrieve(charge_id) + ch.refund() + return ch + + def list_all_charges(self, count=None, offset=None, customer=None): + # https://stripe.com/docs/api?lang=python#list_charges + return stripe.Charge(api_key=self.api_key).all(count=count, offset=offset, customer=customer) + +# what to work through? + +# can't test Transfer in test mode: "There are no transfers in test mode." + +#pledge scenario +# bad card -- what types of erros to handle? +# https://stripe.com/docs/api#errors + +# what errors are handled in the python library and how? +# + +# Account? + +# https://stripe.com/docs/api#event_types +# events of interest -- especially ones that do not directly arise immediately (synchronously) from something we do -- I think +# especially: charge.disputed +# I think following (charge.succeeded, charge.failed, charge.refunded) pretty much sychronous to our actions +# customer.created, customer.updated, customer.deleted + +# transfer +# I expect the ones related to transfers all happen asynchronously: transfer.created, transfer.updated, transfer.failed + +# When will the money I charge with Stripe end up in my bank account? +# Every day, we transfer the money that you charged seven days previously?that is, you receive the money for your March 1st charges on March 8th. + +# pending payments? +# how to tell whether money transferred to bank account yet +# best practices for calling Events -- not too often. + +class PledgeScenarioTest(TestCase): + @classmethod + def setUpClass(cls): + cls._sc = StripeClient(api_key=STRIPE_SK) + + # valid card + card0 = card() + cls._good_cust = cls._sc.create_customer(card=card0, description="test good customer", email="raymond.yee@gmail.com") + + # bad card + test_card_num_to_get_BAD_ATTACHED_CARD = ERROR_TESTING['BAD_ATTACHED_CARD'][0] + card1 = card(number=test_card_num_to_get_BAD_ATTACHED_CARD) + cls._cust_bad_card = cls._sc.create_customer(card=card1, description="test bad customer", email="rdhyee@gluejar.com") + + def test_charge_good_cust(self): + charge = self._sc.create_charge(10, customer=self._good_cust, description="$10 for good cust") + self.assertEqual(type(charge.id), str) + + # print out all the pieces of Customer and Charge objects + print dir(charge) + print dir(self._good_cust) + + def test_error_creating_customer_with_declined_card(self): + # should get a CardError upon attempt to create Customer with this card + _card = card(number=card(ERROR_TESTING['CHARGE_DECLINE'][0])) + self.assertRaises(stripe.CardError, self._sc.create_customer, card=_card) + + def test_charge_bad_cust(self): + # expect the card to be declined -- and for us to get CardError + self.assertRaises(stripe.CardError, self._sc.create_charge, 10, + customer = self._cust_bad_card, description="$10 for bad cust") + + + @classmethod + def tearDownClass(cls): + # clean up stuff we create in test -- right now list current objects + + #cls._good_cust.delete() + + print "list of customers" + print [(i, c.id, c.description, c.email, datetime.fromtimestamp(c.created, tz=utc), c.account_balance, c.delinquent, c.active_card.fingerprint, c.active_card.type, c.active_card.last4, c.active_card.exp_month, c.active_card.exp_year, c.active_card.country) for(i, c) in enumerate(cls._sc.customer.all()["data"])] + + print "list of charges" + print [(i, c.id, c.amount, c.amount_refunded, c.currency, c.description, datetime.fromtimestamp(c.created, tz=utc), c.paid, c.fee, c.disputed, c.amount_refunded, c.failure_message, c.card.fingerprint, c.card.type, c.card.last4, c.card.exp_month, c.card.exp_year) for (i, c) in enumerate(cls._sc.charge.all()['data'])] + + # can retrieve events since a certain time? + print "list of events", cls._sc.event.all() + print [(i, e.id, e.type, e.created, e.pending_webhooks, e.data) for (i,e) in enumerate(cls._sc.event.all()['data'])] + +def suite(): + + testcases = [PledgeScenarioTest] + #testcases = [] + suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases]) + #suites.addTest(LibraryThingTest('test_cache')) + #suites.addTest(SettingsTest('test_dev_me_alignment')) # give option to test this alignment + return suites + + +# IPNs/webhooks: https://stripe.com/docs/webhooks +# how to use pending_webhooks ? + +# all events +# https://stripe.com/docs/api?lang=python#list_events + +if __name__ == '__main__': + #unittest.main() + suites = suite() + #suites = unittest.defaultTestLoader.loadTestsFromModule(__import__('__main__')) + unittest.TextTestRunner().run(suites) diff --git a/payment/templates/stripe.html b/payment/templates/stripe.html new file mode 100644 index 00000000..83e7962f --- /dev/null +++ b/payment/templates/stripe.html @@ -0,0 +1,38 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}Stripe{% endblock %} + +{% block extra_extra_head %} + + + + + + + +{% endblock %} + +{% block doccontent %} +Stripe Test!: + + +
    + {% csrf_token %} + + +
    + + + +{% endblock %} + diff --git a/payment/tests.py b/payment/tests.py index 06ce262c..c0236d33 100644 --- a/payment/tests.py +++ b/payment/tests.py @@ -190,7 +190,7 @@ class PledgeTest(TestCase): # Note, set this to 1-5 different receivers with absolute amounts for each receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':20.00}] - t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None) + t, url = p.pledge('USD', receiver_list, campaign=None, list=None, user=None) self.validateRedirect(t, url, 1) @@ -220,7 +220,7 @@ class PledgeTest(TestCase): receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':20.00}, {'email':settings.PAYPAL_TEST_RH_EMAIL, 'amount':10.00}] - t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None) + t, url = p.pledge('USD', receiver_list, campaign=None, list=None, user=None) self.validateRedirect(t, url, 2) @@ -244,7 +244,7 @@ class PledgeTest(TestCase): # Note, set this to 1-5 different receivers with absolute amounts for each receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':50000.00}] - t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None) + t, url = p.pledge('USD', receiver_list, campaign=None, list=None, user=None) self.validateRedirect(t, url, 1) @@ -284,7 +284,7 @@ class AuthorizeTest(TestCase): # Note, set this to 1-5 different receivers with absolute amounts for each - t, url = p.authorize('USD', TARGET_TYPE_NONE, 100.0, campaign=None, list=None, user=None) + t, url = p.authorize(t) self.validateRedirect(t, url) @@ -300,6 +300,31 @@ class AuthorizeTest(TestCase): def tearDown(self): self.selenium.quit() + +class CreditTest(TestCase): + user1=None + user2=None + def setUp(self): + """ + """ + self.user1 = User.objects.create_user('credit_test1', 'support@gluejar.com', 'credit_test1') + self.user2 = User.objects.create_user('credit_test2', 'support+1@gluejar.com', 'credit_test2') + + def testSimple(self): + """ + """ + self.assertFalse(self.user1.credit.add_to_balance(-100)) + self.assertTrue(self.user1.credit.add_to_balance(100)) + self.assertTrue(self.user1.credit.add_to_pledged(50)) + self.assertFalse(self.user1.credit.add_to_pledged(60)) + self.assertFalse(self.user1.credit.use_pledge(60)) + self.assertTrue(self.user1.credit.use_pledge(50)) + self.assertFalse(self.user1.credit.transfer_to(self.user2,60)) + self.assertTrue(self.user1.credit.transfer_to(self.user2,50)) + self.assertEqual(self.user1.credit.balance, 0) + self.assertEqual(self.user2.credit.balance, 50) + + class TransactionTest(TestCase): def setUp(self): @@ -363,7 +388,7 @@ class BasicGuiTest(TestCase): def suite(): #testcases = [PledgeTest, AuthorizeTest, TransactionTest] - testcases = [TransactionTest] + testcases = [TransactionTest, CreditTest] suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases]) return suites diff --git a/payment/urls.py b/payment/urls.py index 47e302a7..7528cb47 100644 --- a/payment/urls.py +++ b/payment/urls.py @@ -1,16 +1,12 @@ from django.conf.urls.defaults import * from django.conf import settings +from regluit.payment.views import StripeView urlpatterns = patterns( "regluit.payment.views", url(r"^handleipn/(?P\w+)$", "handleIPN", name="HandleIPN"), ) -# Amazon payment URLs -urlpatterns += patterns( - "regluit.payment.amazon", - url(r"^amazonpaymentreturn", "amazonPaymentReturn", name="AmazonPaymentReturn"), -) # this should be on only if DEBUG is on @@ -22,12 +18,11 @@ if settings.DEBUG: url(r"^testexecute", "testExecute"), url(r"^testcancel", "testCancel"), url(r"^querycampaign", "queryCampaign"), - url(r"^runtests", "runTests"), - url(r"^paymentcomplete","paymentcomplete"), url(r"^checkstatus", "checkStatus"), url(r"^testfinish", "testFinish"), url(r"^testrefund", "testRefund"), url(r"^testmodify", "testModify"), + url(r"^stripe/test", StripeView.as_view()) ) \ No newline at end of file diff --git a/payment/views.py b/payment/views.py index 1b94f551..c0950859 100644 --- a/payment/views.py +++ b/payment/views.py @@ -1,7 +1,11 @@ from regluit.payment.manager import PaymentManager -from regluit.payment.paypal import IPN from regluit.payment.models import Transaction from regluit.core.models import Campaign, Wishlist + +from regluit.payment.stripelib import STRIPE_PK + +from regluit.payment.forms import StripePledgeForm + from django.conf import settings from django.core.urlresolvers import reverse from django.shortcuts import render_to_response @@ -13,8 +17,12 @@ from django.views.decorators.csrf import csrf_exempt from django.test.utils import setup_test_environment from django.template import RequestContext +from django.views.generic.edit import FormView +from django.views.generic.base import TemplateView + from unittest import TestResult -from regluit.payment.tests import PledgeTest, AuthorizeTest + + import uuid from decimal import Decimal as D @@ -113,12 +121,8 @@ def testAuthorize(request): receiver_list = [{'email': TEST_RECEIVERS[0], 'amount':20.00}, {'email': TEST_RECEIVERS[1], 'amount':10.00}] - if campaign_id: - campaign = Campaign.objects.get(id=int(campaign_id)) - t, url = p.authorize('USD', TARGET_TYPE_CAMPAIGN, amount, campaign=campaign, return_url=None, list=None, user=None) - - else: - t, url = p.authorize('USD', TARGET_TYPE_NONE, amount, campaign=None, return_url=None, list=None, user=None) + campaign = Campaign.objects.get(id=int(campaign_id)) + t, url = p.authorize(Transaction.objects.create(currency='USD', max_amount=amount, campaign=campaign, user=None), return_url=None) if url: logger.info("testAuthorize: " + url) @@ -249,12 +253,9 @@ def testPledge(request): else: receiver_list = [{'email':TEST_RECEIVERS[0], 'amount':78.90}, {'email':TEST_RECEIVERS[1], 'amount':34.56}] - if campaign_id: - campaign = Campaign.objects.get(id=int(campaign_id)) - t, url = p.pledge('USD', TARGET_TYPE_CAMPAIGN, receiver_list, campaign=campaign, list=None, user=user, return_url=None) + campaign = Campaign.objects.get(id=int(campaign_id)) + t, url = p.pledge('USD', receiver_list, campaign=campaign, list=None, user=user, return_url=None) - else: - t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=user, return_url=None) if url: logger.info("testPledge: " + url) @@ -265,33 +266,6 @@ def testPledge(request): logger.info("testPledge: Error " + str(t.error)) return HttpResponse(response) -def runTests(request): - - try: - # Setup the test environement. We need to run these tests on a live server - # so our code can receive IPN notifications from paypal - setup_test_environment() - result = TestResult() - - # Run the authorize test - test = AuthorizeTest('test_authorize') - test.run(result) - - # Run the pledge test - test = PledgeTest('test_pledge_single_receiver') - test.run(result) - - # Run the pledge failure test - test = PledgeTest('test_pledge_too_much') - test.run(result) - - output = "Tests Run: " + str(result.testsRun) + str(result.errors) + str(result.failures) - logger.info(output) - - return HttpResponse(output) - - except: - traceback.print_exc() @csrf_exempt def handleIPN(request, module): @@ -304,11 +278,6 @@ def handleIPN(request, module): return HttpResponse("ipn") -def paymentcomplete(request): - # pick up all get and post parameters and display - output = "payment complete" - output += request.method + "\n" + str(request.REQUEST.items()) - return HttpResponse(output) def checkStatus(request): # Check the status of all PAY transactions and flag any errors @@ -322,5 +291,25 @@ def checkStatus(request): def _render(request, template, template_vars={}): return render_to_response(template, template_vars, RequestContext(request)) +class StripeView(FormView): + template_name="stripe.html" + form_class = StripePledgeForm + + def get_context_data(self, **kwargs): + + context = super(StripeView, self).get_context_data(**kwargs) - \ No newline at end of file + context.update({ + 'STRIPE_PK':STRIPE_PK + }) + return context + + def form_valid(self, form): + stripe_token = form.cleaned_data["stripe_token"] + # e.g., tok_0C0k4jG5B2Oxox + # + return HttpResponse("stripe_token: {0}".format(stripe_token)) + + + + diff --git a/requirements.pip b/requirements.pip index 3d273ecf..7b450acd 100644 --- a/requirements.pip +++ b/requirements.pip @@ -1,4 +1,4 @@ -Django==1.3.1 +Django==1.4.1 MySQL-python==1.2.3 south django-extensions @@ -19,8 +19,8 @@ freebase django-endless-pagination django-selectable pytz -django-notification git+ssh://git@github.com/Gluejar/boto.git@2.3.0 +https://github.com/aladagemre/django-notification/tarball/master fabric paramiko pyasn1 @@ -28,3 +28,4 @@ pycrypto==2.5 django-maintenancemode django-smtp-ssl django-ckeditor +stripe diff --git a/requirements_versioned.pip b/requirements_versioned.pip index dfd0d088..3987f02c 100644 --- a/requirements_versioned.pip +++ b/requirements_versioned.pip @@ -1,56 +1,52 @@ -# requirements.pip does not specify versions of the libraries. Here's a modified version of the output from pip freeze -# on March 7, 2012 for which the tests run successfully for https://github.com/Gluejar/regluit/commit/000d78dbd0e377d2b1688a4a1a333df49a0dd11b -# If you were to run pip install -r requirements.pip anew, the tests no longer run perfectly because of the use of -# some later libraries +Django==1.4.1 +Fabric==1.4.3 +MySQL-python==1.2.3 +Pillow==1.7.7 +Pyzotero==0.9.51 +South==0.7.6 amqplib==1.0.2 -anyjson==0.3.1 -boto==2.3.0 -celery==2.4.6 -certifi==0.0.6 -django-celery==2.4.2 +anyjson==0.3.3 +billiard==2.7.3.12 +#boto==2.3.0 +git+ssh://git@github.com/Gluejar/boto.git@2.3.0 +celery==3.0.9 +distribute==0.6.28 +django-celery==3.0.9 django-ckeditor==3.6.2.1 -django-debug-toolbar==0.8.5 django-endless-pagination==1.1 -django-extensions==0.7.1 +django-extensions==0.9 django-kombu==0.9.4 django-maintenancemode==0.10 django-nose-selenium==0.7.3 -django-notification==0.2 -django-picklefield==0.1.9 -django-profiles==0.2 -https://bitbucket.org/ubernostrum/django-registration/get/tip.tar.gz -#django-registration==0.8-alpha-1 -django-selectable==0.2 +#django-notification==0.2 +git+git://github.com/aladagemre/django-notification.git@2927346f4c513a217ac8ad076e494dd1adbf70e1 +django-registration==0.8 +django-selectable==0.5.2 django-smtp-ssl==1.0 -django-social-auth==0.6.1 -#https://github.com/toastdriven/django-tastypie/tarball/master +django-social-auth==0.7.5 django-tastypie==0.9.11 -Django==1.3.1 -Fabric==1.4.1 -feedparser==5.1 +feedparser==5.1.2 freebase==1.0.8 -httplib2==0.7.2 -kombu==1.5.1 -# lxml requires special handling to install properly -#lxml==2.3.1 +httplib2==0.7.5 +kombu==2.4.5 +lxml==3.0alpha2 mechanize==0.2.5 mimeparse==0.1.3 -MySQL-python==1.2.3 nose==1.1.2 oauth2==1.5.211 -paramiko==1.7.7.1 -pyasn1==0.1.3 -pycrypto==2.5 -pyparsing==1.5.6 -python-dateutil==1.5 -python-digest==1.7 +paramiko==1.7.7.2 +pyasn1==0.1.4 +pycrypto==2.6 +python-dateutil==2.1 python-openid==2.2.5 -pytz==2012b -Pyzotero==0.9.4 -rdflib==3.1.0 -redis==2.4.11 -requests==0.9.1 -selenium==2.24.0 -South==0.7.3 -ssh==1.7.13 -wsgiref==0.1.2 +pytz==2012d +rdflib==2.4.0 +redis==2.6.2 +requests==0.14.0 +selenium==2.25.0 +six==1.2.0 +ssh==1.7.14 +stripe==1.7.4 +virtualenv==1.4.9 +virtualenvwrapper==2.2.2 +wsgiref==0.1.2 \ No newline at end of file diff --git a/settings/common.py b/settings/common.py index 23053eb9..b5f70796 100644 --- a/settings/common.py +++ b/settings/common.py @@ -49,7 +49,7 @@ STATIC_URL = '/static/' # URL prefix for admin static files -- CSS, JavaScript and images. # Make sure to use a trailing slash. # Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/static/admin/' +# ADMIN_MEDIA_PREFIX = '/static/admin/' # Additional locations of static files STATICFILES_DIRS = ( @@ -271,12 +271,8 @@ EBOOK_NOTIFICATIONS_JOB = { # by default, in common, we don't turn any of the celerybeat jobs on -- turn them on in the local settings file -# set -- sandbox or production Amazon FPS? -AMAZON_FPS_HOST = "fps.sandbox.amazonaws.com" -#AMAZON_FPS_HOST = "fps.amazonaws.com" - # amazon or paypal for now. -PAYMENT_PROCESSOR = 'amazon' +PAYMENT_PROCESSOR = 'test' # a SECRET_KEY to be used for encrypting values in core.models.Key -- you should store in settings/local.py SECRET_KEY = '' @@ -286,3 +282,8 @@ SECRET_KEY = '' MAINTENANCE_MODE = False # Sequence of URL path regexes to exclude from the maintenance mode. MAINTENANCE_IGNORE_URLS = {} + +class NONPROFIT: + is_on = True + name = 'Library Renewal' + link = 'http://127.0.0.1:8000/donate_to_campaign/' \ No newline at end of file diff --git a/static/css/book_panel.css b/static/css/book_panel.css index 8c4377d7..52c52aa6 100644 --- a/static/css/book_panel.css +++ b/static/css/book_panel.css @@ -102,8 +102,8 @@ border-bottom-width: 1px; border-top-style: solid; border-bottom-style: solid; - border-top-color: #FFFFFF; - border-bottom-color: #FFFFFF; + border-top-color: #FFF; + border-bottom-color: #FFF; } .panelhoverlink { text-decoration: none; @@ -113,7 +113,7 @@ width: 118px; height: 35px; padding: 0px 0px; - background: #FFFFFF; + background: #FFF; margin: 0px; -moz-border-radius: 4px; -webkit-border-radius: 4px; @@ -178,6 +178,9 @@ color: #3d4e53; overflow: hidden; } +.panelview.book-name div a { + color: #6994a3; +} .panelview.booklist-status { display: none; } @@ -276,7 +279,7 @@ div.panelview.side2 { width: 118px; height: 35px; padding: 0px 0px; - background: #FFFFFF; + background: #FFF; margin: 0px; -moz-border-radius: 4px; -webkit-border-radius: 4px; @@ -330,7 +333,7 @@ div.panelview.side2 { width: 118px; height: 35px; padding: 0px 0px; - background: #FFFFFF; + background: #FFF; margin: 0px; -moz-border-radius: 4px; -webkit-border-radius: 4px; @@ -356,7 +359,7 @@ div.panelview.side2 { width: 118px; height: 35px; padding: 0px 0px; - background: #FFFFFF; + background: #FFF; margin: 0px; -moz-border-radius: 4px; -webkit-border-radius: 4px; @@ -388,8 +391,8 @@ div.panelview.side2 { border-bottom-width: 1px; border-top-style: solid; border-bottom-style: solid; - border-top-color: #FFFFFF; - border-bottom-color: #FFFFFF; + border-top-color: #FFF; + border-bottom-color: #FFF; background: url("/static/images/book-panel/add_wish_icon.png") no-repeat left center; padding-right: 0; } @@ -423,8 +426,8 @@ div.panelview.side2 { border-bottom-width: 1px; border-top-style: solid; border-bottom-style: solid; - border-top-color: #FFFFFF; - border-bottom-color: #FFFFFF; + border-top-color: #FFF; + border-bottom-color: #FFF; background: url("/static/images/booklist/remove-wishlist-white.png") no-repeat left center; } .moreinfo.remove-wishlist a, @@ -453,8 +456,8 @@ div.panelview.side2 { border-bottom-width: 1px; border-top-style: solid; border-bottom-style: solid; - border-top-color: #FFFFFF; - border-bottom-color: #FFFFFF; + border-top-color: #FFF; + border-bottom-color: #FFF; background: url("/static/images/checkmark_small-white.png") no-repeat left center; } .moreinfo.on-wishlist a, @@ -482,7 +485,7 @@ div.panelview.side2 { margin: 0px; } .white_text a { - color: #FFFFFF; + color: #FFF; text-decoration: none; } .white_text a:hover { @@ -505,8 +508,8 @@ div.panelview.side2 { border-bottom-width: 1px; border-top-style: solid; border-bottom-style: solid; - border-top-color: #FFFFFF; - border-bottom-color: #FFFFFF; + border-top-color: #FFF; + border-bottom-color: #FFF; background: url("/static/images/book-panel/more_icon.png") no-repeat left center; cursor: pointer; } diff --git a/static/css/pledge.css b/static/css/pledge.css index e5804977..0a8fd96c 100644 --- a/static/css/pledge.css +++ b/static/css/pledge.css @@ -98,7 +98,7 @@ input[type="submit"] { float: right; font-size: 19px; - margin: 10px; + margin: 10px 0 10px; cursor: pointer; } .pledge_amount { @@ -149,7 +149,7 @@ p { } #fakepledgesubmit { background-color: #e35351; - cursor: pointer; + cursor: default; font-weight: bold; font-size: 19px; display: none; @@ -157,3 +157,59 @@ p { span.menu-item-price { float: none !important; } +#mandatory_premiums { + font-size: 15px; +} +#mandatory_premiums div { + float: left; +} +#mandatory_premiums div.ack_level { + width: 16%; + margin-right: 3%; + height: 100%; + padding: 1%; +} +#mandatory_premiums div.ack_header { + width: 73%; + padding: 1%; +} +#mandatory_premiums div.ack_active, +#mandatory_premiums div.ack_inactive { + width: 100%; + font-size: 13px; +} +#mandatory_premiums div.ack_active .ack_header, +#mandatory_premiums div.ack_active .ack_level { + border: solid #3d4e53; + border-width: 1%; + background: white; +} +#mandatory_premiums div.ack_inactive .ack_header, +#mandatory_premiums div.ack_inactive .ack_level { + border: solid #d6dde0; + border-width: 1%; + background: #d6dde0; +} +#mandatory_premiums div.ack_inactive input, +#mandatory_premiums div.ack_inactive textarea { + background: #d6dde0; + border: dashed 1px #3d4e53; +} +#mandatory_premiums > div { + margin: 7px 0; +} +#mandatory_premiums input[type=text], +#mandatory_premiums textarea { + width: 95%; + font-size: 15px; + color: #3d4e53; + margin: 5px 0; +} +#mandatory_premiums input[type=text] { + height: 19.5px; + line-height: 19.5px; +} +#id_ack_link { + border: none; + cursor: default; +} diff --git a/static/js/prefill_pledge.js b/static/js/prefill_pledge.js deleted file mode 100644 index b61b7ffd..00000000 --- a/static/js/prefill_pledge.js +++ /dev/null @@ -1,19 +0,0 @@ -var $j = jQuery.noConflict(); -//give pledge box focus -$j(function() { - $j('#id_preapproval_amount').focus(); -}); -// This autofills the pledge box when users select a premium tier. -$j().ready(function() { - var inputbox = $j('#id_preapproval_amount'); - - $j('#premiums_list input').on("click", function() { - amount = $j(this).siblings('span.menu-item-price').html(); - amount = amount.split('$')[1]; - amount = parseInt(amount); - current = inputbox.val(); - if (current= 25 && current < 50) { + deactivateLink(); + deactivate('ack_dedication'); + if (anon) { + anonymizeName(); + } else { + activate('ack_name'); + } + ackSection.html(' as a Supporter'); + } else if (current >= 50 && current < 100) { + deactivate('ack_dedication'); + if (anon) { + anonymizeName(); + deactivateLink(); + } else { + activate('ack_name'); + activateLink(); + } + ackSection.html(' as a Benefactor'); + } else if (current >= 100) { + activate('ack_dedication'); + if (anon) { + anonymizeName(); + deactivateLink(); + } else { + activate('ack_name'); + activateLink(); + } + ackSection.html(' as a Bibliophile'); + } + } + + // initialize the acknowledgements fields. if we've prefilled the pledge info, + // use that. + current = inputbox.val(); + if (current) { + rectifyAcknowledgements(current); + } else { + rectifyAcknowledgements(0); + } + if (ackAnon == 'True') { + anonymizeName(); + deactivateLink(); + anonbox.prop("checked", true); + } + + // when user clicks a premium, ensure it is compatible with the pledge box amount $j('#premiums_list input').on("click", function() { - // when user clicks a premium, ensure it is compatible with the pledge box amount amount = canonicalize($j(this)); current = inputbox.val(); if (current div { + margin: 7px 0; + } + + input[type=text], textarea { + width: 95%; + font-size: @font-size-larger; + color: @text-blue; + margin: 5px 0; + } + + input[type=text] { + .height(@font-size-larger*1.3); + } +} + +#id_ack_link { + border: none; + cursor: default; } \ No newline at end of file diff --git a/static/less/variables.css b/static/less/variables.css index 4d261f86..f5e3c848 100644 --- a/static/less/variables.css +++ b/static/less/variables.css @@ -13,3 +13,80 @@ border-style: solid none; border-color: #FFFFFF; } +.roundedspan { + border: 1px solid #d4d4d4; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + padding: 1px; + color: #fff; + margin: 0 8px 0 0; + display: inline-block; +} +.roundedspan > span { + padding: 7px 7px; + min-width: 15px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + text-align: center; + display: inline-block; +} +.roundedspan > span .hovertext { + display: none; +} +.roundedspan > span:hover .hovertext { + display: inline; +} +.mediaborder { + padding: 5px; + border: solid 5px #EDF3F4; +} +.google_signup_div { + padding: 14px 0; +} +.google_signup_div div { + height: 24px; + line-height: 24px; + float: left; + padding-left: 5px; +} +.google_signup_div img { + float: left; + height: 24px; + width: 24px; +} +.actionbuttons { + width: auto; + height: 36px; + line-height: 36px; + background: #8dc63f; + -moz-border-radius: 32px; + -webkit-border-radius: 32px; + border-radius: 32px; + color: white; + cursor: pointer; + font-size: 13px; + font-weight: bold; + padding: 0 15px; + border: none; + margin: 5px 0; +} +.errors { + -moz-border-radius: 16px 16px 0 0; + -webkit-border-radius: 16px 16px 0 0; + border-radius: 16px 16px 0 0; + border: solid #e35351 3px; + clear: both; + width: 90%; + height: auto; + line-height: 16px; + padding: 7px 0; + font-weight: bold; + font-size: 13px; + text-align: center; +} +.errors li { + list-style: none; + border: none; +} diff --git a/static/stripe/tag.css b/static/stripe/tag.css new file mode 100755 index 00000000..4476ec5a --- /dev/null +++ b/static/stripe/tag.css @@ -0,0 +1,58 @@ +payment, .payment { + position: relative; + display: block; + + padding: 15px 20px; + max-width: 300px; + overflow: hidden; + + box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-sizing: border-box; +} + +payment label, .payment label { + display: block; + padding: 5px 0; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +payment input, .payment input { + padding: 5px 5px; + + box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-sizing: border-box; +} + +payment .number input, .payment .number input { + padding: 7px 40px 7px 7px; + width: 100%; +} + +payment .expiry input, payment .cvc input { + width: 45px; +} + +payment .expiry em { + display: none; +} + +payment .cvc, +.payment .cvc { + float: right; + text-align: right; +} + +payment .expiry, +.payment .expiry { + float: left; +} + +payment .message, +.payment .message { + display: block; +} \ No newline at end of file diff --git a/static/stripe/tag.dev.js b/static/stripe/tag.dev.js new file mode 100755 index 00000000..1bbac091 --- /dev/null +++ b/static/stripe/tag.dev.js @@ -0,0 +1,405 @@ +(function() { + var $, global, script, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __slice = [].slice; + + $ = this.jQuery || this.Zepto; + + if (!$) { + throw 'jQuery/Zepto required'; + } + + this.PaymentTag = (function() { + + PaymentTag.replaceTags = function(element) { + var _this = this; + if (element == null) { + element = document.body; + } + return $('payment, .payment-tag', element).each(function(i, tag) { + return new _this({ + el: tag + }).render(); + }); + }; + + PaymentTag.prototype.defaults = { + tokenName: 'stripe_token', + token: true, + cvc: true + }; + + function PaymentTag(options) { + var _ref, _ref1; + if (options == null) { + options = {}; + } + this.changeCardType = __bind(this.changeCardType, this); + + this.restrictNumeric = __bind(this.restrictNumeric, this); + + this.formatNumber = __bind(this.formatNumber, this); + + this.handleToken = __bind(this.handleToken, this); + + this.submit = __bind(this.submit, this); + + this.$el = options.el || ''; + this.$el = $(this.$el); + options.key || (options.key = this.$el.attr('key') || this.$el.attr('data-key')); + if ((_ref = options.cvc) == null) { + options.cvc = !((this.$el.attr('nocvc') != null) || (this.$el.attr('data-nocvc') != null)); + } + if ((_ref1 = options.token) == null) { + options.token = !((this.$el.attr('notoken') != null) || (this.$el.attr('data-notoken') != null)); + } + options.form || (options.form = this.$el.parents('form')); + this.options = $.extend({}, this.defaults, options); + if (this.options.key) { + this.setKey(this.options.key); + } + this.setForm(this.options.form); + this.$el.delegate('.number input', 'keydown', this.formatNumber); + this.$el.delegate('.number input', 'keyup', this.changeCardType); + this.$el.delegate('input[type=tel]', 'keypress', this.restrictNumeric); + } + + PaymentTag.prototype.render = function() { + this.$el.html(this.constructor.view(this)); + this.$number = this.$('.number input'); + this.$cvc = this.$('.cvc input'); + this.$expiryMonth = this.$('.expiry input.expiryMonth'); + this.$expiryYear = this.$('.expiry input.expiryYear'); + this.$message = this.$('.message'); + return this; + }; + + PaymentTag.prototype.renderToken = function(token) { + this.$token = $(''); + this.$token.attr('name', this.options.tokenName); + this.$token.val(token); + return this.$el.html(this.$token); + }; + + PaymentTag.prototype.setForm = function($form) { + this.$form = $($form); + return this.$form.bind('submit.payment', this.submit); + }; + + PaymentTag.prototype.setKey = function(key) { + this.key = key; + return Stripe.setPublishableKey(this.key); + }; + + PaymentTag.prototype.validate = function() { + var expiry, valid; + valid = true; + this.$('div').removeClass('invalid'); + this.$message.empty(); + if (!Stripe.validateCardNumber(this.$number.val())) { + valid = false; + this.handleError({ + code: 'invalid_number' + }); + } + expiry = this.expiryVal(); + if (!Stripe.validateExpiry(expiry.month, expiry.year)) { + valid = false; + this.handleError({ + code: 'expired_card' + }); + } + if (this.options.cvc && !Stripe.validateCVC(this.$cvc.val())) { + valid = false; + this.handleError({ + code: 'invalid_cvc' + }); + } + if (!valid) { + this.$('.invalid input:first').select(); + } + return valid; + }; + + PaymentTag.prototype.createToken = function(callback) { + var complete, expiry, + _this = this; + complete = function(status, response) { + if (response.error) { + return callback(response.error); + } else { + return callback(null, response); + } + }; + expiry = this.expiryVal(); + return Stripe.createToken({ + number: this.$number.val(), + cvc: this.$cvc.val() || null, + exp_month: expiry.month, + exp_year: expiry.year + }, complete); + }; + + PaymentTag.prototype.submit = function(e) { + if (e != null) { + e.preventDefault(); + } + if (e != null) { + e.stopImmediatePropagation(); + } + if (!this.validate()) { + return; + } + if (this.pending) { + return; + } + this.pending = true; + this.disableInputs(); + this.trigger('pending'); + this.$el.addClass('pending'); + return this.createToken(this.handleToken); + }; + + PaymentTag.prototype.handleToken = function(err, response) { + this.enableInputs(); + this.trigger('complete'); + this.$el.removeClass('pending'); + this.pending = false; + if (err) { + return this.handleError(err); + } else { + this.trigger('success', response); + this.$el.addClass('success'); + if (this.options.token) { + this.renderToken(response.id); + } + this.$form.unbind('submit.payment', this.submit); + return this.$form.submit(); + } + }; + + PaymentTag.prototype.formatNumber = function(e) { + var digit, lastDigits, value; + digit = String.fromCharCode(e.which); + if (!/^\d+$/.test(digit)) { + return; + } + value = this.$number.val(); + if (Stripe.cardType(value) === 'American Express') { + lastDigits = value.match(/^(\d{4}|\d{4}\s\d{6})$/); + } else { + lastDigits = value.match(/(?:^|\s)(\d{4})$/); + } + if (lastDigits) { + return this.$number.val(value + ' '); + } + }; + + PaymentTag.prototype.restrictNumeric = function(e) { + var char; + if (e.shiftKey || e.metaKey) { + return true; + } + if (e.which === 0) { + return true; + } + char = String.fromCharCode(e.which); + return !/[A-Za-z]/.test(char); + }; + + PaymentTag.prototype.cardTypes = { + 'Visa': 'visa', + 'American Express': 'amex', + 'MasterCard': 'mastercard', + 'Discover': 'discover', + 'Unknown': 'unknown' + }; + + PaymentTag.prototype.changeCardType = function(e) { + var map, name, type, _ref; + type = Stripe.cardType(this.$number.val()); + if (!this.$number.hasClass(type)) { + _ref = this.cardTypes; + for (name in _ref) { + map = _ref[name]; + this.$number.removeClass(map); + } + return this.$number.addClass(this.cardTypes[type]); + } + }; + + PaymentTag.prototype.handleError = function(err) { + if (err.message) { + this.$message.text(err.message); + } + switch (err.code) { + case 'card_declined': + this.invalidInput(this.$number); + break; + case 'invalid_number': + case 'incorrect_number': + this.invalidInput(this.$number); + break; + case 'invalid_expiry_month': + this.invalidInput(this.$expiryMonth); + break; + case 'invalid_expiry_year': + case 'expired_card': + this.invalidInput(this.$expiryYear); + break; + case 'invalid_cvc': + this.invalidInput(this.$cvc); + } + this.$('label.invalid:first input').select(); + this.trigger('error', err); + return typeof console !== "undefined" && console !== null ? console.error('Stripe error:', err) : void 0; + }; + + PaymentTag.prototype.invalidInput = function(input) { + input.parent().addClass('invalid'); + return this.trigger('invalid', [input.attr('name'), input]); + }; + + PaymentTag.prototype.expiryVal = function() { + var month, prefix, trim, year; + trim = function(s) { + return s.replace(/^\s+|\s+$/g, ''); + }; + month = trim(this.$expiryMonth.val()); + year = trim(this.$expiryYear.val()); + if (year.length === 2) { + prefix = (new Date).getFullYear(); + prefix = prefix.toString().slice(0, 2); + year = prefix + year; + } + return { + month: month, + year: year + }; + }; + + PaymentTag.prototype.enableInputs = function() { + var $elements; + $elements = this.$el.add(this.$form).find(':input'); + return $elements.each(function() { + var $item, _ref; + $item = $(this); + return $elements.attr('disabled', (_ref = $item.data('olddisabled')) != null ? _ref : false); + }); + }; + + PaymentTag.prototype.disableInputs = function() { + var $elements; + $elements = this.$el.add(this.$form).find(':input'); + return $elements.each(function() { + var $item; + $item = $(this); + $item.data('olddisabled', $item.attr('disabled')); + return $item.attr('disabled', true); + }); + }; + + PaymentTag.prototype.trigger = function() { + var data, event, _ref; + event = arguments[0], data = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + return (_ref = this.$el).trigger.apply(_ref, ["" + event + ".payment"].concat(__slice.call(data))); + }; + + PaymentTag.prototype.$ = function(sel) { + return $(sel, this.$el); + }; + + return PaymentTag; + + })(); + + document.createElement('payment'); + + if (typeof module !== "undefined" && module !== null) { + module.exports = PaymentTag; + } + + global = this; + + if (global.Stripe) { + $(function() { + return typeof PaymentTag.replaceTags === "function" ? PaymentTag.replaceTags() : void 0; + }); + } else { + script = document.createElement('script'); + script.onload = script.onreadystatechange = function() { + if (!global.Stripe) { + return; + } + if (script.done) { + return; + } + script.done = true; + return typeof PaymentTag.replaceTags === "function" ? PaymentTag.replaceTags() : void 0; + }; + script.src = 'https://js.stripe.com/v1/'; + $(function() { + var sibling; + sibling = document.getElementsByTagName('script')[0]; + return sibling != null ? sibling.parentNode.insertBefore(script, sibling) : void 0; + }); + } + +}).call(this); +(function() { + this.PaymentTag || (this.PaymentTag = {}); + this.PaymentTag["view"] = function(__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + + __out.push('\n\n
    \n \n\n \n
    \n\n
    \n \n\n \n \n
    \n\n'); + + if (this.options.cvc) { + __out.push('\n
    \n \n \n
    \n'); + } + + __out.push('\n'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); + }; +}).call(this); diff --git a/static/stripe/tag.js b/static/stripe/tag.js new file mode 100755 index 00000000..dcd82026 --- /dev/null +++ b/static/stripe/tag.js @@ -0,0 +1 @@ +(function(){var e,t,n,r=function(e,t){return function(){return e.apply(t,arguments)}},i=[].slice;e=this.jQuery||this.Zepto;if(!e)throw"jQuery/Zepto required";this.PaymentTag=function(){function t(t){var n,i;t==null&&(t={}),this.changeCardType=r(this.changeCardType,this),this.restrictNumeric=r(this.restrictNumeric,this),this.formatNumber=r(this.formatNumber,this),this.handleToken=r(this.handleToken,this),this.submit=r(this.submit,this),this.$el=t.el||"",this.$el=e(this.$el),t.key||(t.key=this.$el.attr("key")||this.$el.attr("data-key")),(n=t.cvc)==null&&(t.cvc=this.$el.attr("nocvc")==null&&this.$el.attr("data-nocvc")==null),(i=t.token)==null&&(t.token=this.$el.attr("notoken")==null&&this.$el.attr("data-notoken")==null),t.form||(t.form=this.$el.parents("form")),this.options=e.extend({},this.defaults,t),this.options.key&&this.setKey(this.options.key),this.setForm(this.options.form),this.$el.delegate(".number input","keydown",this.formatNumber),this.$el.delegate(".number input","keyup",this.changeCardType),this.$el.delegate("input[type=tel]","keypress",this.restrictNumeric)}return t.replaceTags=function(t){var n=this;return t==null&&(t=document.body),e("payment, .payment-tag",t).each(function(e,t){return(new n({el:t})).render()})},t.prototype.defaults={tokenName:"stripe_token",token:!0,cvc:!0},t.prototype.render=function(){return this.$el.html(this.constructor.view(this)),this.$number=this.$(".number input"),this.$cvc=this.$(".cvc input"),this.$expiryMonth=this.$(".expiry input.expiryMonth"),this.$expiryYear=this.$(".expiry input.expiryYear"),this.$message=this.$(".message"),this},t.prototype.renderToken=function(t){return this.$token=e(''),this.$token.attr("name",this.options.tokenName),this.$token.val(t),this.$el.html(this.$token)},t.prototype.setForm=function(t){return this.$form=e(t),this.$form.bind("submit.payment",this.submit)},t.prototype.setKey=function(e){return this.key=e,Stripe.setPublishableKey(this.key)},t.prototype.validate=function(){var e,t;return t=!0,this.$("div").removeClass("invalid"),this.$message.empty(),Stripe.validateCardNumber(this.$number.val())||(t=!1,this.handleError({code:"invalid_number"})),e=this.expiryVal(),Stripe.validateExpiry(e.month,e.year)||(t=!1,this.handleError({code:"expired_card"})),this.options.cvc&&!Stripe.validateCVC(this.$cvc.val())&&(t=!1,this.handleError({code:"invalid_cvc"})),t||this.$(".invalid input:first").select(),t},t.prototype.createToken=function(e){var t,n,r=this;return t=function(t,n){return n.error?e(n.error):e(null,n)},n=this.expiryVal(),Stripe.createToken({number:this.$number.val(),cvc:this.$cvc.val()||null,exp_month:n.month,exp_year:n.year},t)},t.prototype.submit=function(e){e!=null&&e.preventDefault(),e!=null&&e.stopImmediatePropagation();if(!this.validate())return;if(this.pending)return;return this.pending=!0,this.disableInputs(),this.trigger("pending"),this.$el.addClass("pending"),this.createToken(this.handleToken)},t.prototype.handleToken=function(e,t){return this.enableInputs(),this.trigger("complete"),this.$el.removeClass("pending"),this.pending=!1,e?this.handleError(e):(this.trigger("success",t),this.$el.addClass("success"),this.options.token&&this.renderToken(t.id),this.$form.unbind("submit.payment",this.submit),this.$form.submit())},t.prototype.formatNumber=function(e){var t,n,r;t=String.fromCharCode(e.which);if(!/^\d+$/.test(t))return;r=this.$number.val(),Stripe.cardType(r)==="American Express"?n=r.match(/^(\d{4}|\d{4}\s\d{6})$/):n=r.match(/(?:^|\s)(\d{4})$/);if(n)return this.$number.val(r+" ")},t.prototype.restrictNumeric=function(e){var t;return e.shiftKey||e.metaKey?!0:e.which===0?!0:(t=String.fromCharCode(e.which),!/[A-Za-z]/.test(t))},t.prototype.cardTypes={Visa:"visa","American Express":"amex",MasterCard:"mastercard",Discover:"discover",Unknown:"unknown"},t.prototype.changeCardType=function(e){var t,n,r,i;r=Stripe.cardType(this.$number.val());if(!this.$number.hasClass(r)){i=this.cardTypes;for(n in i)t=i[n],this.$number.removeClass(t);return this.$number.addClass(this.cardTypes[r])}},t.prototype.handleError=function(e){e.message&&this.$message.text(e.message);switch(e.code){case"card_declined":this.invalidInput(this.$number);break;case"invalid_number":case"incorrect_number":this.invalidInput(this.$number);break;case"invalid_expiry_month":this.invalidInput(this.$expiryMonth);break;case"invalid_expiry_year":case"expired_card":this.invalidInput(this.$expiryYear);break;case"invalid_cvc":this.invalidInput(this.$cvc)}return this.$("label.invalid:first input").select(),this.trigger("error",e),typeof console!="undefined"&&console!==null?console.error("Stripe error:",e):void 0},t.prototype.invalidInput=function(e){return e.parent().addClass("invalid"),this.trigger("invalid",[e.attr("name"),e])},t.prototype.expiryVal=function(){var e,t,n,r;return n=function(e){return e.replace(/^\s+|\s+$/g,"")},e=n(this.$expiryMonth.val()),r=n(this.$expiryYear.val()),r.length===2&&(t=(new Date).getFullYear(),t=t.toString().slice(0,2),r=t+r),{month:e,year:r}},t.prototype.enableInputs=function(){var t;return t=this.$el.add(this.$form).find(":input"),t.each(function(){var n,r;return n=e(this),t.attr("disabled",(r=n.data("olddisabled"))!=null?r:!1)})},t.prototype.disableInputs=function(){var t;return t=this.$el.add(this.$form).find(":input"),t.each(function(){var t;return t=e(this),t.data("olddisabled",t.attr("disabled")),t.attr("disabled",!0)})},t.prototype.trigger=function(){var e,t,n;return t=arguments[0],e=2<=arguments.length?i.call(arguments,1):[],(n=this.$el).trigger.apply(n,[""+t+".payment"].concat(i.call(e)))},t.prototype.$=function(t){return e(t,this.$el)},t}(),document.createElement("payment"),typeof module!="undefined"&&module!==null&&(module.exports=PaymentTag),t=this,t.Stripe?e(function(){return typeof PaymentTag.replaceTags=="function"?PaymentTag.replaceTags():void 0}):(n=document.createElement("script"),n.onload=n.onreadystatechange=function(){if(!t.Stripe)return;if(n.done)return;return n.done=!0,typeof PaymentTag.replaceTags=="function"?PaymentTag.replaceTags():void 0},n.src="https://js.stripe.com/v1/",e(function(){var e;return e=document.getElementsByTagName("script")[0],e!=null?e.parentNode.insertBefore(n,e):void 0}))}).call(this),function(){this.PaymentTag||(this.PaymentTag={}),this.PaymentTag.view=function(e){e||(e={});var t=[],n=function(e){var n=t,r;return t=[],e.call(this),r=t.join(""),t=n,i(r)},r=function(e){return e&&e.ecoSafe?e:typeof e!="undefined"&&e!=null?o(e):""},i,s=e.safe,o=e.escape;return i=e.safe=function(e){if(e&&e.ecoSafe)return e;if(typeof e=="undefined"||e==null)e="";var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('\n\n
    \n \n\n \n
    \n\n
    \n \n\n \n \n
    \n\n'),this.options.cvc&&t.push('\n
    \n \n \n
    \n'),t.push("\n")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")}}.call(this); \ No newline at end of file diff --git a/static/stripe/themes/amex.png b/static/stripe/themes/amex.png new file mode 100755 index 00000000..959159f3 Binary files /dev/null and b/static/stripe/themes/amex.png differ diff --git a/static/stripe/themes/discover.png b/static/stripe/themes/discover.png new file mode 100755 index 00000000..6d8860bd Binary files /dev/null and b/static/stripe/themes/discover.png differ diff --git a/static/stripe/themes/generic.png b/static/stripe/themes/generic.png new file mode 100755 index 00000000..19e4d008 Binary files /dev/null and b/static/stripe/themes/generic.png differ diff --git a/static/stripe/themes/mastercard.png b/static/stripe/themes/mastercard.png new file mode 100755 index 00000000..b8558403 Binary files /dev/null and b/static/stripe/themes/mastercard.png differ diff --git a/static/stripe/themes/spinner.gif b/static/stripe/themes/spinner.gif new file mode 100755 index 00000000..ca0285b1 Binary files /dev/null and b/static/stripe/themes/spinner.gif differ diff --git a/static/stripe/themes/stripe.css b/static/stripe/themes/stripe.css new file mode 100755 index 00000000..916751f2 --- /dev/null +++ b/static/stripe/themes/stripe.css @@ -0,0 +1,152 @@ +payment, .payment { + position: relative; + display: block; + + border-radius: 5px; + padding: 15px 20px; + max-width: 300px; + overflow: hidden; + + box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-sizing: border-box; + + font-size: 12px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + font-family: 'Helvetica Neue', Helvetica, Arial Geneva, sans-serif; + + background: #FFF; + background-image: -o-linear-gradient(#FFF, #F9FAFA); + background-image: -ms-linear-gradient(#FFF, #F9FAFA); + background-image: -moz-linear-gradient(#FFF, #F9FAFA); + background-image: -webkit-linear-gradient(#FEFEFE, #F9FAFA); + background-image: linear-gradient(#FFF, #F9FAFA); + + -moz-box-shadow: 0 0 2px rgba(80,84,92,0.3), 0 1px 1px rgba(80,84,92,0.5); + -webkit-box-shadow: 0 0 2px rgba(80, 84, 92, 0.3), 0 1px 1px rgba(80, 84, 92, 0.5); + -ms-box-shadow: 0 0 2px rgba(80, 84, 92, 0.3), 0 1px 1px rgba(80, 84, 92, 0.5); + box-shadow: 0 0 2px rgba(80, 84, 92, 0.3), 0 1px 1px rgba(80, 84, 92, 0.5); +} + +payment ::-webkit-input-placeholder, +.payment ::-webkit-input-placeholder { + text-transform: uppercase; +} + +payment label, .payment label { + display: block; + color: #999; + font-size: 13px; + padding: 5px 0; + text-transform: uppercase; + text-shadow: 0 1px 0 #FFF; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +payment input, .payment input { + font-size: 13px; + padding: 5px 5px; + border: 1px solid #BBB; + border-top-color: #999; + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); + border-radius: 3px; + + -webkit-transition: -webkit-box-shadow 0.1s ease-in-out; + -moz-transition: -moz-box-shadow 0.1s ease-in-out; + transition: -moz-box-shadow 0.1s ease-in-out; + + box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-sizing: border-box; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + font-family: 'Helvetica Neue', Helvetica, Arial Geneva, sans-serif; + font-size: 14px; +} + +payment input:focus, .payment input:focus { + border: 1px solid #5695DB; + outline: none; + -webkit-box-shadow: inset 0 1px 2px #DDD, 0px 0 5px #5695DB; + -moz-box-shadow: 0 0 5px #5695db; + box-shadow: inset 0 1px 2px #DDD, 0px 0 5px #5695DB; +} + +payment .invalid input, .payment .invalid input { + outline: none; + border-color: rgba(255, 0, 0, 0.5); + -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20), 0 1px 5px 0 rgba(255, 0, 0, 0.4); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.20), 0 1px 5px 0 rgba(255, 0, 0, 0.4); + -ms-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.20), 0 1px 5px 0 rgba(255, 0, 0, 0.4); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.20), 0 1px 5px 0 rgba(255, 0, 0, 0.4); +} + +payment input:disabled, .payment input:disabled { + opacity: 0.5; +} + +payment .number, +.payment .number { + margin-bottom: 8px; +} + +payment .number input, .payment .number input { + padding: 7px 40px 7px 7px; + background: #FFF url(generic.png) 98.5% 20% no-repeat; + width: 100%; +} + +payment .number input.visa, .payment .number input.visa { + background-image: url(visa.png); +} + +payment .number input.mastercard, .payment .number input.mastercard { + background-image: url(mastercard.png); +} + +payment .number input.discover, .payment .number input.discover { + background-image: url(discover.png); +} + +payment .number input.amex, .payment .number input.amex { + background-image: url(amex.png); +} + +payment .expiry input, payment .cvc input { + width: 45px; +} + +payment .expiry em { + font-size: 10px; + font-style: normal; + display: none; +} + +payment .cvc, +.payment .cvc { + float: right; + text-align: right; +} + +payment .expiry, +.payment .expiry { + float: left; +} + +payment .message, +.payment .message { + display: block; +} + +payment.pending, +.payment.pending, +payment.success, +.payment.success { + background: #FFF url(spinner.gif) center center no-repeat; + min-height: 130px; +} \ No newline at end of file diff --git a/static/stripe/themes/visa.png b/static/stripe/themes/visa.png new file mode 100755 index 00000000..e39df455 Binary files /dev/null and b/static/stripe/themes/visa.png differ