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 }}

+ +
+ {% csrf_token %} + {{ form }} + +
+
+{% 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 }}

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+{{ 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 %} +
+

Profile for {{ user }}

+ +

TagLine: {{ profile.tagline }}

+

Edit Again

+

{{user}}'s Unglue.it Page

+
+{% 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 }}

+
+ {% csrf_token %} + {{ form.as_p }} + +
+

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: + + +{% 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')) )