diff --git a/core/migrations/0012_auto__add_userprofile.py b/core/migrations/0012_auto__add_userprofile.py
new file mode 100644
index 00000000..daf5a3e7
--- /dev/null
+++ b/core/migrations/0012_auto__add_userprofile.py
@@ -0,0 +1,131 @@
+# 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 'UserProfile'
+ db.create_table('core_userprofile', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True)),
+ ('tagline', self.gf('django.db.models.fields.CharField')(max_length=140, blank=True)),
+ ))
+ db.send_create_signal('core', ['UserProfile'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'UserProfile'
+ db.delete_table('core_userprofile')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'core.author': {
+ 'Meta': {'object_name': 'Author'},
+ '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', [], {'max_length': '500'}),
+ 'openlibrary_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
+ 'works': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authors'", 'symmetrical': 'False', 'to': "orm['core.Work']"})
+ },
+ 'core.campaign': {
+ 'Meta': {'object_name': 'Campaign'},
+ 'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'deadline': ('django.db.models.fields.DateTimeField', [], {}),
+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '10000'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
+ 'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}),
+ 'target': ('django.db.models.fields.FloatField', [], {}),
+ 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"})
+ },
+ 'core.edition': {
+ 'Meta': {'object_name': 'Edition'},
+ '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'}),
+ 'isbn_10': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True'}),
+ 'isbn_13': ('django.db.models.fields.CharField', [], {'max_length': '13', 'null': 'True'}),
+ 'openlibrary_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
+ 'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ '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.editioncover': {
+ 'Meta': {'object_name': 'EditionCover'},
+ 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'covers'", 'to': "orm['core.Edition']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'openlibrary_id': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'core.subject': {
+ 'Meta': {'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', [], {'max_length': '500'}),
+ 'works': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'subjects'", 'symmetrical': 'False', 'to': "orm['core.Work']"})
+ },
+ 'core.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagline': ('django.db.models.fields.CharField', [], {'max_length': '140', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ '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', 'to': "orm['core.Work']"})
+ },
+ 'core.work': {
+ 'Meta': {'object_name': 'Work'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'openlibrary_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'})
+ }
+ }
+
+ complete_apps = ['core']
diff --git a/core/models.py b/core/models.py
index 61044456..9b8525c4 100755
--- a/core/models.py
+++ b/core/models.py
@@ -85,5 +85,8 @@ class Wishlist(models.Model):
user = models.OneToOneField(User, related_name='wishlist')
works = models.ManyToManyField('Work', related_name='wishlists')
+class UserProfile(models.Model):
+ user = models.ForeignKey(User, unique=True)
+ tagline = models.CharField(max_length=140, blank=True)
from regluit.core import signals
diff --git a/frontend/forms.py b/frontend/forms.py
new file mode 100644
index 00000000..ef2484e1
--- /dev/null
+++ b/frontend/forms.py
@@ -0,0 +1,38 @@
+from django import forms
+from django.db import models
+#from django.forms import Form, ModelForm, Textarea, CharField, ValidationError, RegexField
+from regluit.core.models import UserProfile
+from django.contrib.auth.models import User
+from django.utils.translation import ugettext_lazy as _
+
+class ProfileForm(forms.ModelForm):
+ class Meta:
+ model = UserProfile
+ exclude = 'user'
+ widgets = {
+ 'tagline': forms.Textarea(attrs={'cols': 70, 'rows': 2}),
+ }
+
+class UserData(forms.Form):
+ username = forms.RegexField(
+ label=_("New Username"),
+ max_length=30,
+ regex=r'^[\w.@+-]+$',
+ help_text = _("30 characters or less."),
+ error_messages = {
+ 'invalid': _("This value may contain only letters, numbers and @/./+/-/_ characters.")
+ }
+ )
+ # oldusername = forms.CharField(max_length=30)
+
+
+ def clean_username(self):
+ username = self.data["username"]
+ oldusername = self.data["oldusername"]
+ if username != oldusername:
+ try:
+ User.objects.get(username=username)
+ except User.DoesNotExist:
+ return username
+ raise forms.ValidationError(_("Another user with that username already exists."))
+ raise forms.ValidationError(_("Your username is already "+oldusername))
\ No newline at end of file
diff --git a/frontend/templates/base.html b/frontend/templates/base.html
index d1acd444..85563067 100644
--- a/frontend/templates/base.html
+++ b/frontend/templates/base.html
@@ -1,6 +1,9 @@
{# raw url references raise test errors in tests for django registration; this is a workaround #}
{% url privacy as privacyurl %}
+{% url regluit.frontend.views.edit_user as editurl %}
+{% url rightsholders as rhtoolsurl %}
+
unglue.it {% block title %}{% endblock %}
@@ -45,9 +48,13 @@
diff --git a/frontend/templates/profiles/create_profile.html b/frontend/templates/profiles/create_profile.html
new file mode 100644
index 00000000..d06891f3
--- /dev/null
+++ b/frontend/templates/profiles/create_profile.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block title %}Create Profile{% endblock %}
+
+{% block content %}
+
+
Create a new profile for {{ user }}
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/frontend/templates/profiles/edit_profile.html b/frontend/templates/profiles/edit_profile.html
new file mode 100644
index 00000000..50f6bc3d
--- /dev/null
+++ b/frontend/templates/profiles/edit_profile.html
@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% block title %}Edit Profile{% endblock %}
+
+{% block content %}
+
+
Edit profile for {{ user }}
+
+
+{{ debug }}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/frontend/templates/profiles/profile_detail.html b/frontend/templates/profiles/profile_detail.html
new file mode 100644
index 00000000..a1b4f57a
--- /dev/null
+++ b/frontend/templates/profiles/profile_detail.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block title %}Edit Profile{% endblock %}
+
+{% block content %}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/frontend/templates/profiles/profile_list.html b/frontend/templates/profiles/profile_list.html
new file mode 100644
index 00000000..175c4e9e
--- /dev/null
+++ b/frontend/templates/profiles/profile_list.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block title %}Profile List{% endblock %}
+
+{% block content %}
+
+Sorry, you can't list the profiles on this site.
+
+{% endblock content %}
\ No newline at end of file
diff --git a/frontend/templates/registration/user_change_form.html b/frontend/templates/registration/user_change_form.html
new file mode 100644
index 00000000..4523849f
--- /dev/null
+++ b/frontend/templates/registration/user_change_form.html
@@ -0,0 +1,19 @@
+{% extends "registration/registration_base.html" %}
+{% block title %}Change User Data{% endblock %}
+{% block content %}
+
+
Changing your Username.
+
+
If you change your Username, the web address for your profile page will change as well.
+
+
Your current Username: {{ user.username }}
+
+
Edit Your Profile
+
+{% endblock %}
+
+
diff --git a/frontend/templates/rhtools.html b/frontend/templates/rhtools.html
new file mode 100644
index 00000000..558b762f
--- /dev/null
+++ b/frontend/templates/rhtools.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+unglue.it tools for rightsholders
+
+Rightsholder social media tools
+
+Coming soon.
+
+Rewards
+
+Campaigns have rewards as a way to thank supporters. unglue.it includes a standard set of rewards in all campaigns. You are encouraged to add additional sweeteners to motivate people to donate.
+
+Here are the standard rewards:
+
+- Any level — The unglued ebook delivered to your inbox
+- $25 — Your name under "supporters" in the acknowledgements section
+- $50 — Your name & link of your choice under "benefactors"
+- $100 — Your name, link of your choice, & a brief message (140 characters max) under "bibliophiles"
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/frontend/urls.py b/frontend/urls.py
index 3ad0a064..73464479 100644
--- a/frontend/urls.py
+++ b/frontend/urls.py
@@ -9,5 +9,7 @@ urlpatterns = patterns(
url(r"^search/$", "search", name="search"),
url(r"^privacy/$", TemplateView.as_view(template_name="privacy.html"),
name="privacy"),
+ url(r"^rightsholders/$", TemplateView.as_view(template_name="rhtools.html"),
+ name="rightsholders"),
url(r"^wishlist/$", "wishlist", name="wishlist"),
)
diff --git a/frontend/views.py b/frontend/views.py
index 568bb286..0244f03b 100755
--- a/frontend/views.py
+++ b/frontend/views.py
@@ -1,5 +1,6 @@
from django.template import RequestContext
from django.contrib.auth.models import User
+# from django.contrib.auth.forms import UserChangeForm
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.views.decorators.csrf import csrf_exempt
@@ -10,6 +11,8 @@ from django.shortcuts import render, render_to_response, get_object_or_404
from regluit.core import models, bookloader
from regluit.core.search import gluejar_search
+from regluit.frontend.forms import UserData
+
def home(request):
if request.user.is_authenticated():
return HttpResponseRedirect(reverse('supporter',
@@ -25,6 +28,24 @@ def supporter(request, supporter_username):
}
return render(request, 'supporter.html', context)
+
+def edit_user(request):
+ form=UserData()
+ if not request.user.is_authenticated():
+ return HttpResponseRedirect(reverse('auth_login'))
+ oldusername=request.user.username
+ if request.method == 'POST':
+ # surely there's a better way to add data to the POST data?
+ postcopy=request.POST.copy()
+ postcopy['oldusername']=oldusername
+ form = UserData(postcopy)
+ if form.is_valid(): # All validation rules pass, go and change the username
+ request.user.username=form.cleaned_data['username']
+ request.user.save()
+ return HttpResponseRedirect(reverse('home')) # Redirect after POST
+ return render(request,'registration/user_change_form.html', {'form': form},)
+
+
def search(request):
q = request.GET.get('q', None)
results = gluejar_search(q)
diff --git a/payment/manager.py b/payment/manager.py
index 611b9fbb..0e40f076 100644
--- a/payment/manager.py
+++ b/payment/manager.py
@@ -2,7 +2,7 @@ from regluit.core.models import Campaign, Wishlist
from regluit.payment.models import Transaction, Receiver
from django.contrib.auth.models import User
from regluit.payment.parameters import *
-from regluit.payment.paypal import Pay, IPN, IPN_TYPE_PAYMENT, IPN_TYPE_PREAPPROVAL, Preapproval
+from regluit.payment.paypal import Pay, IPN, IPN_TYPE_PAYMENT, IPN_TYPE_PREAPPROVAL, IPN_TYPE_ADJUSTMENT, Preapproval, IPN_PAY_STATUS_COMPLETED, CancelPreapproval, IPN_SENDER_STATUS_COMPLETED
import uuid
import traceback
@@ -27,38 +27,119 @@ class PaymentManager( object ):
if ipn.transaction_type == IPN_TYPE_PAYMENT:
+ # payment IPN
- if ipn.preapproval_key:
- key = ipn.preapproval_key
- else:
- key = ipn.key
-
+ key = ipn.key()
t = Transaction.objects.get(reference=key)
+
+ # The status is always one of the IPN_PAY_STATUS codes defined in paypal.py
t.status = ipn.status
+
for item in ipn.transactions:
- r = Receiver.objects.get(transaction=t, email=item['receiver'])
- r.status = item['status_for_sender_txn']
- r.save()
+ try:
+ r = Receiver.objects.get(transaction=t, email=item['receiver'])
+ print item
+ # one of the IPN_SENDER_STATUS codes defined in paypal.py
+ r.status = item['status_for_sender_txn']
+ r.txn_id = item['id_for_sender_txn']
+ r.save()
+ except:
+ # Log an excecption if we have a receiver that is not found
+ traceback.print_exc()
t.save()
+
+ elif ipn.transaction_type == IPN_TYPE_ADJUSTMENT:
+ # a chargeback, reversal or refund for an existng payment
+
+ key = ipn.key()
+ t = Transaction.objects.get(reference=key)
+
+ # The status is always one of the IPN_PAY_STATUS codes defined in paypal.py
+ t.status = ipn.status
+
+ # Reason code indicates more details of the adjustment type
+ t.reason = ipn.reason_code
+
elif ipn.transaction_type == IPN_TYPE_PREAPPROVAL:
- t = Transaction.objects.get(reference=ipn.preapproval_key)
+
+ key = ipn.key()
+ t = Transaction.objects.get(reference=key)
+
+ # The status is always one of the IPN_PREAPPROVAL_STATUS codes defined in paypal.py
t.status = ipn.status
t.save()
+ print "IPN: Preapproval transaction: " + str(t.id) + " Status: " + ipn.status
else:
print "IPN: Unknown Transaction Type: " + ipn.transaction_type
else:
+ print "ERROR: INVALID IPN"
print ipn.error
except:
traceback.print_exc()
+
+ '''
+ Generic query handler for returning summary and transaction info, see query_user, query_list and query_campaign
+ '''
+ def run_query(self, transaction_list, summary, pledged, authorized ):
+
+ if pledged:
+ pledged_list = transaction_list.filter(type=PAYMENT_TYPE_INSTANT,
+ status="COMPLETED")
+ else:
+ pledged_list = []
+
+ if authorized:
+ authorized_list = Transaction.objects.filter(type=PAYMENT_TYPE_AUTHORIZATION,
+ status="ACTIVE")
+ else:
+ authorized_list = []
+
+ if summary:
+ pledged_amount = 0.0
+ authorized_amount = 0.0
+
+ for t in pledged_list:
+ for r in t.receiver_set.all():
+ if r.status == IPN_SENDER_STATUS_COMPLETED:
+ # individual senders may not have been paid due to errors, and disputes/chargebacks only appear here
+ pledged_amount += r.amount
+
+ for t in authorized_list:
+ authorized_amount += t.amount
+
+ amount = pledged_amount + authorized_amount
+ return amount
+
+ else:
+ return pledged_list | authorized_list
+
+
+ '''
+ query_user
+
+ Returns either an amount or list of transactions for a user
+
+ summary: if true, return a float of the total, if false, return a list of transactions
+ pledged: include amounts pledged
+ authorized: include amounts pre-authorized
+
+ return value: either a float summary or a list of transactions
+
+ '''
+ def query_user(self, user, summary=False, pledged=True, authorized=True):
+
+ transactions = Transaction.objects.filter(user=user)
+ return self.run_query(transactions, summary, pledged, authorized)
+
'''
query_campaign
@@ -74,35 +155,25 @@ class PaymentManager( object ):
'''
def query_campaign(self, campaign, summary=False, pledged=True, authorized=True):
- if pledged:
- pledged_list = Transaction.objects.filter(campaign=campaign,
- type=PAYMENT_TYPE_INSTANT,
- status="COMPLETED")
- else:
- pledged_list = []
+ transactions = Transaction.objects.filter(campaign=campaign)
+ return self.run_query(transactions, summary, pledged, authorized)
+
+ '''
+ query_list
+
+ Returns either an amount or list of transactions for a list
+
+ summary: if true, return a float of the total, if false, return a list of transactions
+ pledged: include amounts pledged
+ authorized: include amounts pre-authorized
+
+ return value: either a float summary or a list of transactions
+
+ '''
+ def query_campaign(self, list, summary=False, pledged=True, authorized=True):
- if authorized:
- authorized_list = Transaction.objects.filter(campaign=campaign,
- type=PAYMENT_TYPE_AUTHORIZATION,
- status="ACTIVE")
- else:
- authorized_list = []
-
- if summary:
- pledged_amount = 0.0
- authorized_amount = 0.0
-
- for t in pledged_list:
- pledged_amount += t.amount
-
- for t in authorized_list:
- authorized_amount += t.amount
-
- amount = pledged_amount + authorized_amount
- return amount
-
- else:
- return pledged_list | authorized_list
+ transactions = Transaction.objects.filter(list=list)
+ return self.run_query(transactions, summary, pledged, authorized)
'''
execute_campaign
@@ -114,12 +185,14 @@ class PaymentManager( object ):
'''
def execute_campaign(self, campaign):
+ # only allow active transactions to go through again, if there is an error, intervention is needed
transactions = Transaction.objects.filter(campaign=campaign, status="ACTIVE")
for t in transactions:
- receiver_list = [{'email':'jakace_1309677337_biz@gmail.com', 'amount':t.amount * 0.80},
- {'email':'boogus@gmail.com', 'amount':t.amount * 0.20}]
+ # BUGBUG: Fill this in with the correct info from the campaign object
+ receiver_list = [{'email':'jakace_1309677337_biz@gmail.com', 'amount':t.amount},
+ {'email':'seller_1317463643_biz@gmail.com', 'amount':t.amount * 0.20}]
self.execute_transaction(t, receiver_list)
@@ -144,13 +217,17 @@ class PaymentManager( object ):
'''
def execute_transaction(self, transaction, receiver_list):
- for r in receiver_list:
- receiver = Receiver.objects.create(email=r['email'], amount=r['amount'], currency=transaction.currency, status="ACTIVE", transaction=transaction)
+ if len(transaction.receiver_set.all()) > 0:
+ # we are re-submitting a transaction, wipe the old receiver list
+ transaction.receiver_set.all().delete()
+
+ transaction.create_receivers(receiver_list)
+
+ p = Pay(transaction)
- p = Pay(transaction, receiver_list)
- transaction.status = p.status()
+ # We will update our transaction status when we receive the IPN
- if p.status() == 'COMPLETED':
+ if p.status() == IPN_PAY_STATUS_COMPLETED:
print "Execute Success"
return True
@@ -159,6 +236,27 @@ class PaymentManager( object ):
print "Execute Error: " + p.error()
return False
+ '''
+ cancel
+
+ cancels a pre-approved transaction
+
+ return value: True if successful, false otherwise
+ '''
+ def cancel(self, transaction):
+
+ p = CancelPreapproval(transaction)
+
+ if p.success():
+ print "Cancel Transaction " + str(transaction.id) + " Completed"
+ return True
+
+ else:
+ print "Cancel Transaction " + str(transaction.id) + " Failed with error: " + p.error()
+ transaction.error = p.error()
+ return False
+
+
'''
authorize
@@ -191,14 +289,12 @@ class PaymentManager( object ):
p = Preapproval(t, amount)
if p.status() == 'Success':
- t.status = 'CREATED'
t.reference = p.paykey()
t.save()
print "Authorize Success: " + p.next_url()
return t, p.next_url()
else:
- t.status = 'ERROR'
t.error = p.error()
t.save()
print "Authorize Error: " + p.error()
@@ -230,8 +326,9 @@ class PaymentManager( object ):
def pledge(self, currency, target, receiver_list, campaign=None, list=None, user=None):
amount = 0.0
- for r in receiver_list:
- amount += r['amount']
+
+ # for chained payments, first amount is the total amount
+ amount = receiver_list[0]['amount']
t = Transaction.objects.create(amount=amount,
type=PAYMENT_TYPE_INSTANT,
@@ -244,11 +341,9 @@ class PaymentManager( object ):
user=user
)
- for r in receiver_list:
- receiver = Receiver.objects.create(email=r['email'], amount=r['amount'], currency=currency, status="None", transaction=t)
+ t.create_receivers(receiver_list)
- p = Pay(t, receiver_list)
- t.status = p.status()
+ p = Pay(t)
if p.status() == 'CREATED':
t.reference = p.paykey()
@@ -259,7 +354,7 @@ class PaymentManager( object ):
else:
t.error = p.error()
t.save()
- print "Pledge Status: " + p.status() + "Error: " + p.error()
+ print "Pledge Error: " + p.error()
return t, None
\ No newline at end of file
diff --git a/payment/models.py b/payment/models.py
index 64b348b1..ba47b72c 100644
--- a/payment/models.py
+++ b/payment/models.py
@@ -14,10 +14,12 @@ class Transaction(models.Model):
reference = models.CharField(max_length=128, null=True)
receipt = models.CharField(max_length=256, null=True)
error = models.CharField(max_length=256, null=True)
+ reason = models.CharField(max_length=64, null=True)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
date_payment = models.DateTimeField(null=True)
date_authorized = models.DateTimeField(null=True)
+ date_expired = models.DateTimeField(null=True)
user = models.ForeignKey(User, null=True)
campaign = models.ForeignKey(Campaign, null=True)
list = models.ForeignKey(Wishlist, null=True)
@@ -25,12 +27,23 @@ class Transaction(models.Model):
def __unicode__(self):
return u"-- Transaction:\n \tstatus: %s\n \t amount: %s\n \treference: %s\n \terror: %s\n" % (self.status, str(self.amount), self.reference, self.error)
+ def create_receivers(self, receiver_list):
+
+ primary = True
+ for r in receiver_list:
+ receiver = Receiver.objects.create(email=r['email'], amount=r['amount'], currency=self.currency, status="None", primary=primary, transaction=self)
+ primary = False
+
+
class Receiver(models.Model):
email = models.CharField(max_length=64)
amount = models.FloatField()
currency = models.CharField(max_length=10)
status = models.CharField(max_length=64)
+ reason = models.CharField(max_length=64)
+ primary = models.BooleanField()
+ txn_id = models.CharField(max_length=64)
transaction = models.ForeignKey(Transaction)
\ No newline at end of file
diff --git a/payment/parameters.py b/payment/parameters.py
index 5e645a06..07e290cc 100644
--- a/payment/parameters.py
+++ b/payment/parameters.py
@@ -7,7 +7,6 @@ TARGET_TYPE_NONE = 0
TARGET_TYPE_CAMPAIGN = 1
TARGET_TYPE_LIST = 2
-
PAYPAL_USERNAME = 'jakace_1309677337_biz_api1.gmail.com'
PAYPAL_PASSWORD = '1309677386'
PAYPAL_SIGNATURE = 'A543DNCPfye3PpgUquUAuyfN2wNQAt.h8FJqHIro2U3-Z886XQvIdWSy'
@@ -21,4 +20,9 @@ COMPLETE_URL = 'paymentcomplete'
CANCEL_URL = 'paymentcancel'
PREAPPROVAL_PERIOD = 365 # days to ask for in a preapproval
-PAYPAL_COMMISSION = 0.10
\ No newline at end of file
+PAYPAL_COMMISSION = 0.10
+
+try:
+ from local_parameters import *
+except ImportError:
+ pass
diff --git a/payment/paypal.py b/payment/paypal.py
index 7244c691..35217e85 100644
--- a/payment/paypal.py
+++ b/payment/paypal.py
@@ -30,14 +30,28 @@ IPN_TYPE_PAYMENT = 'Adaptive Payment PAY'
IPN_TYPE_ADJUSTMENT = 'Adjustment'
IPN_TYPE_PREAPPROVAL = 'Adaptive Payment PREAPPROVAL'
-#status constants
-IPN_STATUS_CREATED = 'CREATED'
-IPN_STATUS_COMPLETED = 'COMPLETED'
-IPN_STATUS_INCOMPLETE = 'INCOMPLETE'
-IPN_STATUS_ERROR = 'ERROR'
-IPN_STATUS_REVERSALERROR = 'REVERSALERROR'
-IPN_STATUS_PROCESSING = 'PROCESSING'
-IPN_STATUS_PENDING = 'PENDING'
+#pay API status constants
+IPN_PAY_STATUS_NONE = 'NONE'
+IPN_PAY_STATUS_CREATED = 'CREATED'
+IPN_PAY_STATUS_COMPLETED = 'COMPLETED'
+IPN_PAY_STATUS_INCOMPLETE = 'INCOMPLETE'
+IPN_PAY_STATUS_ERROR = 'ERROR'
+IPN_PAY_STATUS_REVERSALERROR = 'REVERSALERROR'
+IPN_PAY_STATUS_PROCESSING = 'PROCESSING'
+IPN_PAY_STATUS_PENDING = 'PENDING'
+IPN_PAY_STATUS_ACTIVE = "ACTIVE"
+IPN_PAY_STATUS_CANCELED = "CANCELED"
+
+
+IPN_SENDER_STATUS_COMPLETED = 'COMPLETED'
+IPN_SENDER_STATUS_PENDING = 'PENDING'
+IPN_SENDER_STATUS_CREATED = 'CREATED'
+IPN_SENDER_STATUS_PARTIALLY_REFUNDED = 'PARTIALLY_REFUNDED'
+IPN_SENDER_STATUS_DENIED = 'DENIED'
+IPN_SENDER_STATUS_PROCESSING = 'PROCESSING'
+IPN_SENDER_STATUS_REVERSED = 'REVERSED'
+IPN_SENDER_STATUS_REFUNDED = 'REFUNDED'
+IPN_SENDER_STATUS_FAILED = 'FAILED'
# action_type constants
IPN_ACTION_TYPE_PAY = 'PAY'
@@ -48,11 +62,13 @@ IPN_TXN_STATUS_COMPLETED = 'Completed'
IPN_TXN_STATUS_PENDING = 'Pending'
IPN_TXN_STATUS_REFUNDED = 'Refunded'
-IPN_REASON_CODE_CHARGEBACK = 'Chargeback'
-IPN_REASON_CODE_SETTLEMENT = 'Settlement'
+# addaptive payment adjusted IPN reason codes
+IPN_REASON_CODE_CHARGEBACK_SETTLEMENT = 'Chargeback Settlement'
IPN_REASON_CODE_ADMIN_REVERSAL = 'Admin reversal'
IPN_REASON_CODE_REFUND = 'Refund'
+class PaypalError(RuntimeError):
+ pass
class url_request( object ):
@@ -72,7 +88,7 @@ class url_request( object ):
class Pay( object ):
- def __init__( self, transaction, receiver_list):
+ def __init__( self, transaction):
headers = {
'X-PAYPAL-SECURITY-USERID':PAYPAL_USERNAME,
@@ -88,6 +104,25 @@ class Pay( object ):
print "Return URL: " + return_url
print "Cancel URL: " + cancel_url
+ receiver_list = []
+ receivers = transaction.receiver_set.all()
+
+ if len(receivers) == 0:
+ raise Exception
+
+ for r in receivers:
+ if len(receivers) > 1:
+ if r.primary:
+ primary_string = 'true'
+ else:
+ primary_string = 'false'
+
+ receiver_list.append({'email':r.email,'amount':str(r.amount), 'primary':primary_string})
+ else:
+ receiver_list.append({'email':r.email,'amount':str(r.amount)})
+
+ print receiver_list
+
data = {
'actionType': 'PAY',
'receiverList': { 'receiver': receiver_list },
@@ -120,7 +155,7 @@ class Pay( object ):
print error
return error[0]['message']
else:
- return None
+ return 'Paypal PAY: Unknown Error'
def amount( self ):
return decimal.Decimal(self.results[ 'payment_gross' ])
@@ -132,6 +167,51 @@ class Pay( object ):
return '%s?cmd=_ap-payment&paykey=%s' % ( PAYPAL_PAYMENT_HOST, self.response['payKey'] )
+class CancelPreapproval(object):
+
+ def __init__(self, transaction):
+
+ headers = {
+ 'X-PAYPAL-SECURITY-USERID':PAYPAL_USERNAME,
+ 'X-PAYPAL-SECURITY-PASSWORD':PAYPAL_PASSWORD,
+ 'X-PAYPAL-SECURITY-SIGNATURE':PAYPAL_SIGNATURE,
+ 'X-PAYPAL-APPLICATION-ID':PAYPAL_APPID,
+ 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON',
+ 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON',
+ }
+
+ data = {
+ 'preapprovalKey':transaction.reference,
+ 'requestEnvelope': { 'errorLanguage': 'en_US' }
+ }
+
+ self.raw_request = json.dumps(data)
+ self.raw_response = url_request(PAYPAL_ENDPOINT, "/AdaptivePayments/CancelPreapproval", data=self.raw_request, headers=headers ).content()
+ print "paypal CANCEL PREAPPROBAL response was: %s" % self.raw_response
+ self.response = json.loads( self.raw_response )
+ print self.response
+
+ def success(self):
+ if self.status() == 'Success' or self.status() == "SuccessWithWarning":
+ return True
+ else:
+ return False
+
+ def error(self):
+ if self.response.has_key('error'):
+ error = self.response['error']
+ print error
+ return error[0]['message']
+ else:
+ return 'Paypal Preapproval Cancel: Unknown Error'
+
+ def status(self):
+ if self.response.has_key( 'responseEnvelope' ) and self.response['responseEnvelope'].has_key( 'ack' ):
+ return self.response['responseEnvelope']['ack']
+ else:
+ return None
+
+
class Preapproval( object ):
def __init__( self, transaction, amount ):
@@ -147,14 +227,19 @@ class Preapproval( object ):
return_url = BASE_URL + COMPLETE_URL
cancel_url = BASE_URL + CANCEL_URL
+ # set the expiration date for the preapproval
now = datetime.datetime.utcnow()
- expiry = now + datetime.timedelta( days=PREAPPROVAL_PERIOD )
-
+ expiry = now + datetime.timedelta( days=PREAPPROVAL_PERIOD )
+ transaction.date_authorized = now
+ transaction.date_expired = expiry
+ transaction.save()
data = {
'endingDate': expiry.isoformat(),
'startingDate': now.isoformat(),
'maxTotalAmountOfAllPayments': '%.2f' % transaction.amount,
+ 'maxNumberOfPayments':1,
+ 'maxAmountPerPayment': '%.2f' % transaction.amount,
'currencyCode': transaction.currency,
'returnUrl': return_url,
'cancelUrl': cancel_url,
@@ -183,7 +268,7 @@ class Preapproval( object ):
print error
return error[0]['message']
else:
- return None
+ return 'Paypal Preapproval: Unknown Error'
def status( self ):
if self.response.has_key( 'responseEnvelope' ) and self.response['responseEnvelope'].has_key( 'ack' ):
@@ -216,19 +301,15 @@ class IPN( object ):
if raw_response != 'VERIFIED':
self.error = 'PayPal response was "%s"' % raw_response
return
-
- # check payment status
- if request.POST['status'] != 'COMPLETED' and request.POST['status'] != 'ACTIVE':
- self.error = 'PayPal status was "%s"' % request.POST['status']
- return
# Process the details
self.status = request.POST.get('status', None)
self.sender_email = request.POST.get('sender_email', None)
self.action_type = request.POST.get('action_type', None)
- self.key = request.POST.get('pay_key', None)
+ self.pay_key = request.POST.get('pay_key', None)
self.preapproval_key = request.POST.get('preapproval_key', None)
self.transaction_type = request.POST.get('transaction_type', None)
+ self.reason_code = request.POST.get('reason_code', None)
self.process_transactions(request)
@@ -236,9 +317,21 @@ class IPN( object ):
self.error = "Error: ServerError"
traceback.print_exc()
+ def key(self):
+ # We only keep one reference, either a prapproval key, or a pay key, for the transaction. This avoids the
+ # race condition that may result if the IPN for an executed pre-approval(with both a pay key and preapproval key) is received
+ # before we have time to store the pay key
+ if self.preapproval_key:
+ return self.preapproval_key
+ elif self.pay_key:
+ return self.pay_key
+ else:
+ return None
+
def success( self ):
return self.error == None
-
+
+
@classmethod
def slicedict(cls, d, s):
return dict((str(k.replace(s, '', 1)), v) for k,v in d.iteritems() if k.startswith(s))
diff --git a/payment/urls.py b/payment/urls.py
index b1230662..83021cf3 100644
--- a/payment/urls.py
+++ b/payment/urls.py
@@ -5,6 +5,7 @@ urlpatterns = patterns(
url(r"^testpledge", "testPledge"),
url(r"^testauthorize", "testAuthorize"),
url(r"^testexecute", "testExecute"),
+ url(r"^testcancel", "testCancel"),
url(r"^querycampaign", "queryCampaign"),
url(r"^paypalipn", "paypalIPN")
)
diff --git a/payment/views.py b/payment/views.py
index 9a6c31b0..6c1ca86c 100644
--- a/payment/views.py
+++ b/payment/views.py
@@ -57,7 +57,7 @@ def testExecute(request):
# Note, set this to 1-5 different receivers with absolute amounts for each
receiver_list = [{'email':'jakace_1309677337_biz@gmail.com', 'amount':t.amount * 0.80},
- {'email':'boogus@gmail.com', 'amount':t.amount * 0.20}]
+ {'email':'seller_1317463643_biz@gmail.com', 'amount':t.amount * 0.20}]
p.execute_transaction(t, receiver_list)
output += str(t)
@@ -88,7 +88,7 @@ def testAuthorize(request):
# Note, set this to 1-5 different receivers with absolute amounts for each
receiver_list = [{'email':'jakace_1309677337_biz@gmail.com', 'amount':20.00},
- {'email':'boogus@gmail.com', 'amount':10.00}]
+ {'email':'seller_1317463643_biz@gmail.com', 'amount':10.00}]
if campaign_id:
campaign = Campaign.objects.get(id=int(campaign_id))
@@ -106,6 +106,25 @@ def testAuthorize(request):
print "testAuthorize: Error " + str(t.reference)
return HttpResponse(response)
+'''
+http://BASE/testcancel?transaction=2
+
+Example that cancels a preapproved transaction
+'''
+def testCancel(request):
+
+ if "transaction" not in request.GET.keys():
+ return HttpResponse("No Transaction in Request")
+
+ t = Transaction.objects.get(id=int(request.GET['transaction']))
+ p = PaymentManager()
+ if p.cancel(t):
+ return HttpResponse("Success")
+ else:
+ message = "Error: " + t.error
+ return HttpResponse(message)
+
+
'''
http://BASE/testpledge?campaign=2
@@ -122,8 +141,7 @@ def testPledge(request):
# Note, set this to 1-5 different receivers with absolute amounts for each
- receiver_list = [{'email':'jakace_1309677337_biz@gmail.com', 'amount':20.00},
- {'email':'boogus@gmail.com', 'amount':10.00}]
+ receiver_list = [{'email':'rh1_1317336251_biz@gluejar.com', 'amount':20.00},]
if campaign_id:
campaign = Campaign.objects.get(id=int(campaign_id))
@@ -151,4 +169,4 @@ def paypalIPN(request):
print str(request.POST)
return HttpResponse("ipn")
-
\ No newline at end of file
+
diff --git a/requirements.pip b/requirements.pip
index ff3b4e52..21c3d23a 100644
--- a/requirements.pip
+++ b/requirements.pip
@@ -5,3 +5,4 @@ https://github.com/toastdriven/django-tastypie/tarball/master
requests
https://bitbucket.org/ubernostrum/django-registration/get/tip.tar.gz
django-social-auth
+django-profiles
diff --git a/settings/common.py b/settings/common.py
index abb49079..65395ed8 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -107,6 +107,7 @@ INSTALLED_APPS = (
'registration',
'social_auth',
'tastypie',
+ 'profiles',
)
# A sample logging configuration. The only tangible logging
@@ -169,6 +170,8 @@ AUTHENTICATION_BACKENDS = (
SOCIAL_AUTH_ENABLED_BACKENDS = ['google', 'facebook', 'twitter']
SOCIAL_AUTH_ASSOCIATE_BY_MAIL = True
+SOCIAL_AUTH_NEW_USER_REDIRECT_URL = '/accounts/edit/'
+
FACEBOOK_EXTENDED_PERMISSIONS = ['email']
LOGIN_URL = "/accounts/login/"
@@ -178,3 +181,5 @@ LOGOUT_URL = "/accounts/logout/"
USER_AGENT = "unglue.it.bot v0.0.1 "
SOUTH_TESTS_MIGRATE = False
+
+AUTH_PROFILE_MODULE = "core.userprofile"
\ No newline at end of file
diff --git a/urls.py b/urls.py
index fefe5034..42d61ca9 100755
--- a/urls.py
+++ b/urls.py
@@ -1,11 +1,15 @@
from django.conf.urls.defaults import *
+from frontend.forms import ProfileForm
urlpatterns = patterns('',
url(r'^accounts/activate/complete/$','django.contrib.auth.views.login',
{'template_name': 'registration/activation_complete.html'}),
+ (r'^accounts/edit/$', 'regluit.frontend.views.edit_user'),
(r'^accounts/', include('registration.backends.default.urls')),
(r'^socialauth/', include('social_auth.urls')),
(r'^api/', include('regluit.api.urls')),
+ ('^profiles/edit/$', 'profiles.views.edit_profile', {'form_class': ProfileForm,}),
+ (r'^profiles/', include('profiles.urls')),
(r'', include('regluit.frontend.urls')),
(r'', include('regluit.payment.urls'))
)