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 %}
{{ request.user.username }}
{% if transaction.ack_name %}
{{ 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