diff --git a/core/migrations/0039_payment.py b/core/migrations/0039_payment.py new file mode 100644 index 00000000..1e030aae --- /dev/null +++ b/core/migrations/0039_payment.py @@ -0,0 +1,250 @@ +# 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' + # first drop tables created by syncdb + 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, 8, 31, 2, 9, 51, 31703)'}), + '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, 9, 51, 31561)'}), + '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, 8, 31, 2, 9, 50, 804173)', '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'}), + 'unglued': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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'}), + '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/frontend/forms.py b/frontend/forms.py index 2cd4577d..3f76ab1f 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -296,16 +296,15 @@ class CampaignPledgeForm(forms.Form): ) 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_link = forms.URLField(required=False, label=_("Your web site:")) ack_dedication = forms.CharField(required=False, max_length=140, label=_("Your dedication:")) premium_id = forms.IntegerField(required=False) 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? @@ -317,11 +316,6 @@ class CampaignPledgeForm(forms.Form): 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: - 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: @@ -329,6 +323,11 @@ class CampaignPledgeForm(forms.Form): raise forms.ValidationError(_("Sorry, that premium is fully subscribed.")) except Premium.DoesNotExist: raise forms.ValidationError(_("Sorry, that premium is not valid.")) + premium_amount = premium.amount + logger.info("preapproval_amount: {0}, premium_id: {1}, premium_amount:{2}".format(preapproval_amount, premium_id, premium_amount)) + if preapproval_amount < premium_amount: + logger.info("raising form validating error") + raise forms.ValidationError(_("Sorry, you must pledge at least $%s to select that premium." % (premium_amount))) except Exception, e: if isinstance(e, forms.ValidationError): diff --git a/frontend/templates/pledge.html b/frontend/templates/pledge.html index 352913f1..ac34377f 100644 --- a/frontend/templates/pledge.html +++ b/frontend/templates/pledge.html @@ -108,6 +108,8 @@ +
+{% if faqmenu == 'modify' %}We hope you won't, but of course you're also free to cancel your pledge.{% endif %}
{% if transaction.ack_name %} diff --git a/frontend/urls.py b/frontend/urls.py index 8436a444..1b2c4ce8 100644 --- a/frontend/urls.py +++ b/frontend/urls.py @@ -7,7 +7,7 @@ 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 GoodreadsDisplayView, LibraryThingView, PledgeView, PledgeCompleteView, PledgeCancelView, PledgeRechargeView, FAQView from regluit.frontend.views import CampaignListView, DonateView, WorkListView, UngluedListView, InfoPageView, InfoLangView, DonationView urlpatterns = patterns( @@ -55,8 +55,7 @@ urlpatterns = patterns( 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/modify/(?P\d+)$", login_required(PledgeView.as_view()), name="pledge_modify"), url(r"^pledge/recharge/(?P\d+)$", login_required(PledgeRechargeView.as_view()), name="pledge_recharge"), url(r"^subjects/$", "subjects", name="subjects"), url(r"^librarything/$", LibraryThingView.as_view(), name="librarything"), diff --git a/frontend/views.py b/frontend/views.py index b51d934a..0944a2f3 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -50,8 +50,8 @@ from regluit.frontend.forms import EbookForm, CustomPremiumForm, EditManagersFor from regluit.frontend.forms import getTransferCreditForm 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.parameters import PAYMENT_TYPE_AUTHORIZATION from regluit.core import goodreads from tastypie.models import ApiKey from regluit.payment.models import Transaction @@ -583,280 +583,142 @@ class DonationView(TemplateView): class PledgeView(FormView): template_name="pledge.html" form_class = CampaignPledgeForm + 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 + def get_form_kwargs(self): 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() + 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 - - if campaign is None or campaign.status != 'ACTIVE': - raise Http404 - - custom_premiums = campaign.custom_premiums() - # need to also include the no-premiums default in the queryset we send the page - premiums = custom_premiums | models.Premium.objects.filter(id=150) - premium_id = self.request.REQUEST.get('premium_id', None) - preapproval_amount = self.request.REQUEST.get('preapproval_amount', None) + try: + 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 self.campaign.status == 'ACTIVE' + except Exception, e: + raise e + + transactions = self.campaign.transactions().filter(user=self.request.user, status=TRANSACTION_STATUS_ACTIVE, type=PAYMENT_TYPE_AUTHORIZATION) + if transactions.count() == 0: + premium_id = self.request.REQUEST.get('premium_id', None) + preapproval_amount = self.request.REQUEST.get('preapproval_amount', None) + premium_description = None + ack_name='' + ack_dedication='' + anonymous='' + + else: + self.transaction = transactions[0] + # what stuff do we need to pull out to populate form? + # preapproval_amount, premium_id (which we don't have stored yet) + if self.transaction.premium is not None: + premium_id = self.transaction.premium.id + premium_description = self.transaction.premium.description + else: + premium_id = None + premium_description = None + preapproval_amount = self.transaction.amount + ack_name=self.transaction.ack_name + ack_dedication=self.transaction.ack_dedication + anonymous=self.transaction.anonymous if premium_id is not None and preapproval_amount is 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) + self.data = {'preapproval_amount':preapproval_amount, + 'premium_id':premium_id, 'premium_description':premium_description, + 'ack_name':ack_name, 'ack_dedication':ack_dedication, 'anonymous':anonymous} + if self.request.method == 'POST': + self.data.update(self.request.POST.dict()) + if self.request.method == 'POST' or premium_id: + return {'data':self.data} else: - form = form_class() - + return {} + + def get_context_data(self, **kwargs): + """set up the pledge page""" + + context = super(PledgeView, self).get_context_data(**kwargs) + try: - pubdate = work.publication_date[:4] + pubdate = self.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', + 'work':self.work, + 'campaign':self.campaign, + 'premiums':self.premiums, + 'premium_id':self.data['premium_id'], + 'faqmenu': 'modify' if self.transaction else 'pledge', 'pubdate':pubdate, - 'payment_processor':settings.PAYMENT_PROCESSOR, - }) + 'transaction': self.transaction, + 'tid': self.transaction.id if self.transaction else None, + 'premium_description': self.data['premium_description'], + 'preapproval_amount':self.data['preapproval_amount'], + 'payment_processor':self.transaction.host if self.transaction else None, + }) - # 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}) - 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"] - ack_name = form.cleaned_data["ack_name"] - ack_link = form.cleaned_data["ack_link"] - ack_dedication = form.cleaned_data["ack_dedication"] + def get_premium(self,form): + 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 == self.campaign): + premium = None + except models.Premium.DoesNotExist, e: + premium = None + return premium + def form_valid(self, form): # 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 + 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"], + premium=self.get_premium(form), + paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), + ack_name=form.cleaned_data["ack_name"], + ack_dedication=form.cleaned_data["ack_dedication"], + anonymous=form.cleaned_data["anonymous"], + ) + 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_complete'), self.transaction.id)) + else: + return HttpResponse("No modification made") 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, ack_name=ack_name, ack_link=ack_link, ack_dedication=ack_dedication) - 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, - ack_name=ack_name, ack_link=ack_link, ack_dedication=ack_dedication) - - 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 - assert self.request.user.is_authenticated() - user = self.request.user - - work = get_object_or_404(models.Work, id=self.kwargs["work_id"]) - - try: - campaign = work.last_campaign() - premiums = campaign.custom_premiums() | models.Premium.objects.filter(id=150) - - # which combination of campaign and transaction status required? - # 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 - - 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 - else: - premium_id = None - premium_description = None - - # is there a Transaction for an ACTIVE campaign for this - # should make sure Transaction is modifiable. - - 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.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, - 'transaction': transaction, - }) - 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): - - # 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"] - ack_name = form.cleaned_data["ack_name"] - ack_link = form.cleaned_data["ack_link"] - ack_dedication = form.cleaned_data["ack_dedication"] - 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, ack_name=ack_name, ack_link=ack_link, - ack_dedication=ack_dedication, anonymous=anonymous) - - 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)) - 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, + premium=premium, + paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), + ack_name=form.cleaned_data["ack_name"], + ack_dedication=form.cleaned_data["ack_dedication"], + anonymous=form.cleaned_data["anonymous"], + ) + 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 PledgeRechargeView(TemplateView): @@ -869,7 +731,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 @@ -888,18 +750,15 @@ class PledgeRechargeView(TemplateView): 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 ) ack_name = transaction.ack_name - ack_link = transaction.ack_link ack_dedication = transaction.ack_dedication 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, ack_name=ack_name, ack_link=ack_link, ack_dedication=ack_dedication) + p = PaymentManager() + t, url = p.authorize('USD', transaction.amount, campaign=campaign, list=None, user=user, + return_url=return_url, anonymous=transaction.anonymous, premium=transaction.premium, + paymentReason=paymentReason, ack_name=ack_name, ack_dedication=ack_dedication) logger.info("Recharge url: {0}".format(url)) else: url = None @@ -1090,73 +949,11 @@ class PledgeCancelView(FormView): 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) @@ -1173,7 +970,7 @@ class DonateView(FormView): # 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) + p = PaymentManager() # 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(): @@ -1187,8 +984,8 @@ class DonateView(FormView): #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, ack_name=ack_name, ack_link=ack_link, + t, url = p.pledge('USD', receiver_list, campaign=campaign, list=None, user=user, + return_url=return_url, anonymous=anonymous, ack_name=ack_name, ack_dedication=ack_dedication) if url: diff --git a/payment/credit.py b/payment/credit.py new file mode 100644 index 00000000..c5987d5b --- /dev/null +++ b/payment/credit.py @@ -0,0 +1,56 @@ +from datetime import timedelta + +from django.contrib.auth.models import User +from django.conf import settings + +from regluit.payment.parameters import * +from regluit.utils.localdatetime import now +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.amount=amount + t.max_amount=amount + t.host = PAYMENT_HOST_CREDIT + t.type = PAYMENT_TYPE_AUTHORIZATION + t.status=TRANSACTION_STATUS_ACTIVE + t.approved=True + now_val = now() + t.date_authorized = now_val + t.date_expired = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD ) + + t.save() + + +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/manager.py b/payment/manager.py index 27e742a5..017bfe2a 100644 --- a/payment/manager.py +++ b/payment/manager.py @@ -5,6 +5,9 @@ from django.core.urlresolvers import reverse from django.conf import settings from regluit.payment.parameters import * 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 import uuid import traceback @@ -14,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 @@ -531,58 +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", ack_name=None, ack_link=None, ack_dedication=None, - 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 - ack_name, ack_link, ack_dedication: how the user will be credited in the unglued ebook, if applicable 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, - ack_name=ack_name, - ack_link=ack_link, - ack_dedication=ack_dedication - ) + ''' - # 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'), @@ -590,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() @@ -600,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() @@ -624,16 +599,69 @@ 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 transaction, None + + def process_transaction(self, currency, amount, host=None, campaign=None, user=None, + return_url=None, anonymous=False, premium=None, + paymentReason="unglue.it Pledge", ack_name=None, ack_dedication=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 + 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 + ack_name, ack_dedication: how the user will be credited in the unglued ebook, if applicable + + 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.objects.create(amount=0, + max_amount=amount, + currency=currency, + status='NONE', + campaign=campaign, + user=user, + anonymous=anonymous, + premium=premium, + ack_name=ack_name, + ack_dedication=ack_dedication + ) + + # 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: + #TODO send user to choose payment path return t, None + def cancel_related_transaction(self, transaction, status=TRANSACTION_STATUS_ACTIVE, campaign=None): ''' @@ -677,26 +705,29 @@ class PaymentManager( object ): return canceled - def modify_transaction(self, transaction, amount, expiry=None, anonymous=None, premium=None, + def modify_transaction(self, transaction, amount, expiry=None, premium=None, return_url=None, nevermind_url=None, paymentReason=None, - ack_name=None, ack_link=None, ack_dedication=None): + ack_name=None, ack_dedication=None, anonymous=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 @@ -707,34 +738,50 @@ 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.anonymous = anonymous + transaction.premium = premium + transaction.ack_name = ack_name + transaction.ack_dedication = ack_dedication + 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 + 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: + #TODO send user to choose payment path + return t, None + 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.objects.create(amount=amount, + max_amount=amount, + host=transaction.host, + currency=currency, + status=TRANSACTION_STATUS_CREATED, + campaign=transaction.campaign, + user=transaction.user, + anonymous=anonymous if anonymous!=None else transaction.anonymous, + premium=premium if premium != None else transaction.premium, + ack_name=ack_name, + ack_dedication=ack_dedication + ) - t, url = self.authorize(transaction.currency, - transaction.target, - amount, - expiry, - transaction.campaign, - transaction.list, - transaction.user, - return_url, - nevermind_url, - anonymous, - premium, - paymentReason, - ack_name, - ack_link, - ack_dedication, - True) + 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 @@ -762,7 +809,6 @@ class PaymentManager( object ): transaction.anonymous = anonymous transaction.premium = premium transaction.ack_name = ack_name - transaction.ack_link = ack_link transaction.ack_dedication = ack_dedication transaction.save() @@ -820,16 +866,15 @@ 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, + def pledge(self, currency, receiver_list, campaign=None, user=None, return_url=None, nevermind_url=None, anonymous=False, premium=None, ack_name=None, - ack_link=None, ack_dedication=None): + ack_dedication=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: [ @@ -837,8 +882,7 @@ class PaymentManager( object ): {'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 @@ -863,13 +907,11 @@ class PaymentManager( object ): currency=currency, status='NONE', campaign=campaign, - list=list, user=user, date_payment=now(), anonymous=anonymous, premium=premium, ack_name=ack_name, - ack_link=ack_link, ack_dedication=ack_dedication ) 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/models.py b/payment/models.py index e339d567..1016f5e7 100644 --- a/payment/models.py +++ b/payment/models.py @@ -4,8 +4,11 @@ from django.conf import settings from regluit.core.models import Campaign, Wishlist, Premium from regluit.payment.parameters import * from regluit.payment.signals import credit_balance_added -from decimal import Decimal +from decimal import Decimal, NaN import uuid +import urllib +import logging +logger = logging.getLogger(__name__) class Transaction(models.Model): @@ -14,16 +17,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) @@ -75,14 +75,14 @@ class Transaction(models.Model): # how to acknowledge the user on the supporter page of the campaign ebook ack_name = models.CharField(max_length=64, null=True) - ack_link = models.URLField(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) - - # list: makes allowance for pledging against a Wishlist: not currently in use - list = models.ForeignKey(Wishlist, null=True) + + @property + def ack_link(self): + return 'https://unglue.it/supporter/%s'%urllib.urlencode(self.user.username) def save(self, *args, **kwargs): if not self.secret: @@ -149,10 +149,17 @@ 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) + class Credit(models.Model): user = models.OneToOneField(User, related_name='credit') - balance = models.IntegerField(default=0) - pledged = models.IntegerField(default=0) + 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 @@ -165,17 +172,29 @@ class Credit(models.Model): else: self.balance = self.balance + num_credits self.save() - credit_balance_added.send(sender=self, amount=num_credits) + 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): - if not isinstance( num_credits, int): + num_credits=Decimal(num_credits) + if num_credits is 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): @@ -187,6 +206,10 @@ class Credit(models.Model): 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): diff --git a/payment/parameters.py b/payment/parameters.py index 872f07a6..829afa3a 100644 --- a/payment/parameters.py +++ b/payment/parameters.py @@ -5,17 +5,15 @@ PAYMENT_TYPE_AUTHORIZATION = 2 PAYMENT_HOST_NONE = "none" PAYMENT_HOST_PAYPAL = "paypal" PAYMENT_HOST_AMAZON = "amazon" +PAYMENT_HOST_TEST = "test" +PAYMENT_HOST_CREDIT = "credit" +PAYMENT_HOST_STRIPE = "stripe" 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' @@ -47,6 +45,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/tests.py b/payment/tests.py index b86e3eed..0edd5d1f 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('USD', 100.0, campaign=None, list=None, user=None) self.validateRedirect(t, url) @@ -388,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 d50a346e..c91c6dd4 100644 --- a/payment/urls.py +++ b/payment/urls.py @@ -17,8 +17,6 @@ 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"), diff --git a/payment/views.py b/payment/views.py index 33e5b384..b3bdb12d 100644 --- a/payment/views.py +++ b/payment/views.py @@ -13,7 +13,8 @@ from django.test.utils import setup_test_environment from django.template import RequestContext from unittest import TestResult -from regluit.payment.tests import PledgeTest, AuthorizeTest + + import uuid from decimal import Decimal as D @@ -112,12 +113,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('USD', amount, campaign=campaign, return_url=None, list=None, user=None) if url: logger.info("testAuthorize: " + url) @@ -248,12 +245,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) @@ -264,33 +258,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): @@ -303,11 +270,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