Merge branch 'feature/braintree' into develop
This commit is contained in:
@@ -7,10 +7,12 @@ from .models import (
|
||||
Team,TeamInvite,TeamRequest,
|
||||
WorkoutComment,C2WorldClassAgePerformance,PlannedSession,
|
||||
GeoCourse,GeoPolygon,GeoPoint,VirtualRace,VirtualRaceResult,
|
||||
PaidPlan
|
||||
)
|
||||
|
||||
# Register your models here so you can use them in the Admin module
|
||||
|
||||
|
||||
# Rower details directly under the User
|
||||
class RowerInline(admin.StackedInline):
|
||||
model = Rower
|
||||
@@ -19,8 +21,10 @@ class RowerInline(admin.StackedInline):
|
||||
filter_horizontal = ('team','friends')
|
||||
|
||||
fieldsets = (
|
||||
('Billing Details',
|
||||
{'fields':('street_address','city','postal_code','country','paymentprocessor','customer_id')}),
|
||||
('Rower Plan',
|
||||
{'fields':('rowerplan','paymenttype','planexpires','teamplanexpires','clubsize','protrialexpires','plantrialexpires',)}),
|
||||
{'fields':('paidplan','rowerplan','paymenttype','planexpires','teamplanexpires','clubsize','protrialexpires','plantrialexpires',)}),
|
||||
('Rower Settings',
|
||||
{'fields':
|
||||
('gdproptin','gdproptindate','weightcategory','sex','adaptiveclass','birthdate','getemailnotifications',
|
||||
@@ -118,6 +122,9 @@ class VirtualRaceAdmin(admin.ModelAdmin):
|
||||
class VirtualRaceResultAdmin(admin.ModelAdmin):
|
||||
list_display = ('race','userid','username','boattype','age','weightcategory')
|
||||
search_fields = ['race__name','username']
|
||||
|
||||
class PaidPlanAdmin(admin.ModelAdmin):
|
||||
list_display = ('name','shortname','price','paymenttype','paymentprocessor','clubsize','external_id')
|
||||
|
||||
admin.site.unregister(User)
|
||||
admin.site.register(User,UserAdmin)
|
||||
@@ -135,3 +142,4 @@ admin.site.register(PlannedSession,PlannedSessionAdmin)
|
||||
admin.site.register(GeoCourse, GeoCourseAdmin)
|
||||
admin.site.register(VirtualRace, VirtualRaceAdmin)
|
||||
admin.site.register(VirtualRaceResult, VirtualRaceResultAdmin)
|
||||
admin.site.register(PaidPlan,PaidPlanAdmin)
|
||||
|
||||
390
rowers/braintreestuff.py
Normal file
390
rowers/braintreestuff.py
Normal file
@@ -0,0 +1,390 @@
|
||||
import braintree
|
||||
from django.utils import timezone
|
||||
import datetime
|
||||
|
||||
import django_rq
|
||||
queue = django_rq.get_queue('default')
|
||||
queuelow = django_rq.get_queue('low')
|
||||
queuehigh = django_rq.get_queue('low')
|
||||
|
||||
from rowers.utils import myqueue
|
||||
from rowers.tasks import (
|
||||
handle_send_email_transaction,
|
||||
handle_send_email_subscription_update,
|
||||
handle_send_email_subscription_create,
|
||||
handle_send_email_failed_cancel,
|
||||
)
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from rowsandall_app.settings import (
|
||||
BRAINTREE_MERCHANT_ID,BRAINTREE_PUBLIC_KEY,BRAINTREE_PRIVATE_KEY
|
||||
)
|
||||
|
||||
gateway = braintree.BraintreeGateway(
|
||||
braintree.Configuration(
|
||||
braintree.Environment.Sandbox,
|
||||
merchant_id=BRAINTREE_MERCHANT_ID,
|
||||
public_key=BRAINTREE_PUBLIC_KEY,
|
||||
private_key=BRAINTREE_PRIVATE_KEY,
|
||||
)
|
||||
)
|
||||
|
||||
from rowers.models import Rower,PaidPlan
|
||||
from rowers.utils import ProcessorCustomerError
|
||||
|
||||
def create_customer(rower,force=False):
|
||||
if not rower.customer_id or force:
|
||||
result = gateway.customer.create(
|
||||
{
|
||||
'first_name':rower.user.first_name,
|
||||
'last_name':rower.user.last_name,
|
||||
'email':rower.user.email,
|
||||
})
|
||||
if not result.is_success:
|
||||
raise ProcessorCustomerError
|
||||
else:
|
||||
rower.customer_id = result.customer.id
|
||||
rower.paymentprocessor = 'braintree'
|
||||
rower.save()
|
||||
return rower.customer_id
|
||||
else:
|
||||
return rower.customer_id
|
||||
|
||||
|
||||
|
||||
def get_client_token(rower):
|
||||
try:
|
||||
client_token = gateway.client_token.generate({
|
||||
"customer_id":rower.customer_id,
|
||||
})
|
||||
except ValueError:
|
||||
customer_id = create_customer(rower,force=True)
|
||||
|
||||
client_token = gateway.client_token.generate({
|
||||
"customer_id": customer_id,
|
||||
})
|
||||
|
||||
return client_token
|
||||
|
||||
def get_plans_costs():
|
||||
plans = gateway.plan.all()
|
||||
|
||||
localplans = PaidPlan.object.filter(paymentprocessor='braintree')
|
||||
|
||||
for plan in localplans:
|
||||
for btplan in btplans:
|
||||
if int(btplan.id) == plan.external_id:
|
||||
plan.price = float(x)
|
||||
plan.save()
|
||||
|
||||
return plans
|
||||
|
||||
def make_payment(rower,data):
|
||||
nonce_from_the_client = data['payment_method_nonce']
|
||||
amount = data['amount']
|
||||
amount = '{amount:.f2}'.format(amount=amount)
|
||||
|
||||
result = gateway.transaction.sale({
|
||||
"amount": amount,
|
||||
"payment_method_nonce": nonce_from_the_client,
|
||||
"options": {
|
||||
"submit_for_settlement": True
|
||||
}
|
||||
})
|
||||
if result.is_success:
|
||||
transaction = result.transaction
|
||||
amount = transaction.amount
|
||||
name = '{f} {l}'.format(
|
||||
f = rower.user.first_name,
|
||||
l = rower.user.last_name,
|
||||
)
|
||||
|
||||
|
||||
job = myqueue(queuehigh,handle_send_email_transaction,
|
||||
name, rower.user.email, amount)
|
||||
|
||||
return amount
|
||||
else:
|
||||
return 0,''
|
||||
|
||||
def update_subscription(rower,data,method='up'):
|
||||
planid = data['plan']
|
||||
plan = PaidPlan.objects.get(id=planid)
|
||||
nonce_from_the_client = data['payment_method_nonce']
|
||||
amount = data['amount']
|
||||
amount = '{amount:.2f}'.format(amount=amount)
|
||||
|
||||
gatewaydata = {
|
||||
"price": amount,
|
||||
"plan_id": plan.external_id,
|
||||
"payment_method_nonce": nonce_from_the_client,
|
||||
"options": {
|
||||
"prorate_charges":True,
|
||||
},
|
||||
}
|
||||
|
||||
if plan.paymenttype == 'single':
|
||||
gatewaydata['number_of_billing_cycles'] = 1
|
||||
else:
|
||||
gatewaydata['never_expires'] = True
|
||||
|
||||
result = gateway.subscription.update(
|
||||
rower.subscription_id,
|
||||
gatewaydata
|
||||
)
|
||||
|
||||
|
||||
if result.is_success:
|
||||
rower.paidplan = plan
|
||||
rower.planexpires = result.subscription.billing_period_end_date
|
||||
rower.teamplanexpires = result.subscription.billing_period_end_date
|
||||
rower.clubsize = plan.clubsize
|
||||
rower.paymenttype = plan.paymenttype
|
||||
rower.rowerplan = plan.shortname
|
||||
rower.subscription_id = result.subscription.id
|
||||
rower.save()
|
||||
name = '{f} {l}'.format(
|
||||
f = rower.user.first_name,
|
||||
l = rower.user.last_name,
|
||||
)
|
||||
|
||||
if method == 'up':
|
||||
transactions = result.subscription.transactions
|
||||
|
||||
if transactions:
|
||||
amount = transactions[0].amount
|
||||
else:
|
||||
amount = 0
|
||||
else:
|
||||
amount = 0
|
||||
|
||||
|
||||
job = myqueue(queuehigh,
|
||||
handle_send_email_subscription_update,
|
||||
name, rower.user.email,
|
||||
plan.name,
|
||||
plan.paymenttype == 'recurring',
|
||||
plan.price,
|
||||
amount,
|
||||
result.subscription.billing_period_end_date.strftime('%Y-%m-%d'),
|
||||
method)
|
||||
|
||||
return True
|
||||
else:
|
||||
errors = result.errors.for_object("subscription")
|
||||
codes = [str(e.code) for e in errors]
|
||||
create_new = False
|
||||
proceed_codes = ['81901','81910']
|
||||
for c in codes:
|
||||
if c in proceed_codes:
|
||||
create_new = True
|
||||
|
||||
if create_new:
|
||||
return create_subscription(rower,data)
|
||||
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def create_subscription(rower,data):
|
||||
planid = data['plan']
|
||||
plan = PaidPlan.objects.get(id=planid)
|
||||
nonce_from_the_client = data['payment_method_nonce']
|
||||
amount = data['amount']
|
||||
|
||||
# create or find payment method
|
||||
result = gateway.payment_method.create({
|
||||
"customer_id": rower.customer_id,
|
||||
"payment_method_nonce": nonce_from_the_client
|
||||
})
|
||||
|
||||
if result.is_success:
|
||||
payment_method_token = result.payment_method.token
|
||||
else:
|
||||
return False
|
||||
|
||||
result = gateway.subscription.create({
|
||||
"payment_method_token": payment_method_token,
|
||||
"plan_id": plan.external_id
|
||||
})
|
||||
|
||||
if result.is_success:
|
||||
rower.paidplan = plan
|
||||
rower.planexpires = result.subscription.billing_period_end_date
|
||||
rower.teamplanexpires = result.subscription.billing_period_end_date
|
||||
rower.clubsize = plan.clubsize
|
||||
rower.paymenttype = plan.paymenttype
|
||||
rower.rowerplan = plan.shortname
|
||||
rower.subscription_id = result.subscription.id
|
||||
rower.save()
|
||||
name = '{f} {l}'.format(
|
||||
f = rower.user.first_name,
|
||||
l = rower.user.last_name,
|
||||
)
|
||||
|
||||
|
||||
recurring = plan.paymenttype == 'recurring',
|
||||
|
||||
job = myqueue(
|
||||
queuehigh,
|
||||
handle_send_email_subscription_create,
|
||||
name, rower.user.email,
|
||||
plan.name,
|
||||
recurring,
|
||||
plan.price,
|
||||
plan.price,
|
||||
result.subscription.billing_period_end_date.strftime('%Y-%m-%d')
|
||||
)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
return False
|
||||
|
||||
def cancel_subscription(rower,id):
|
||||
themessages = []
|
||||
errormessages = []
|
||||
try:
|
||||
result = gateway.subscription.cancel(id)
|
||||
themessages.append("Subscription canceled")
|
||||
except:
|
||||
errormessages.append("We could not find the subscription record in our customer database. We have notified the site owner, who will contact you.")
|
||||
|
||||
name = '{f} {l}'.format(f = rower.user.first_name, l = rower.user.last_name)
|
||||
|
||||
|
||||
job = myqueue(queuehigh,
|
||||
handle_send_email_failed_cancel,
|
||||
name, rower.user.email,rower.user.username,id)
|
||||
|
||||
return False, themessages, errormessages
|
||||
|
||||
basicplans = PaidPlan.objects.filter(price=0,paymentprocessor='braintree')
|
||||
rower.paidplan = basicplans[0]
|
||||
rower.teamplanexpires = timezone.now()
|
||||
rower.planexpires = timezone.now()
|
||||
rower.clubsize = 0
|
||||
rower.rowerplan = 'basic'
|
||||
rower.save()
|
||||
themessages.append("Your plan was reset to basic")
|
||||
|
||||
return True, themessages,errormessages
|
||||
|
||||
|
||||
def find_subscriptions(rower):
|
||||
try:
|
||||
result = gateway.customer.find(rower.customer_id)
|
||||
except:
|
||||
raise ProcessorCustomerError("We could not find the customer in the database")
|
||||
|
||||
active_subscriptions = []
|
||||
|
||||
cards = result.credit_cards
|
||||
for card in cards:
|
||||
for subscription in card.subscriptions:
|
||||
if subscription.status == 'Active':
|
||||
active_subscriptions.append(subscription)
|
||||
|
||||
try:
|
||||
paypal_accounts = result.paypal_accounts
|
||||
for account in accuonts:
|
||||
for subscription in account.subscriptions:
|
||||
if subscription.status == 'Active':
|
||||
active_subscriptions.append(subscription)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
result = []
|
||||
|
||||
for subscription in active_subscriptions:
|
||||
|
||||
plan = PaidPlan.objects.filter(paymentprocessor="braintree",
|
||||
external_id=subscription.plan_id)[0]
|
||||
|
||||
thedict = {
|
||||
'end_date': subscription.billing_period_end_date,
|
||||
'plan_id': subscription.plan_id,
|
||||
'price': subscription.price,
|
||||
'id': subscription.id,
|
||||
'plan': plan.name
|
||||
}
|
||||
|
||||
result.append(thedict)
|
||||
|
||||
return result
|
||||
|
||||
def get_transactions(start_date,end_date):
|
||||
results = gateway.transaction.search(
|
||||
braintree.TransactionSearch.created_at.between(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
)
|
||||
|
||||
amounts = []
|
||||
countries = []
|
||||
card_countries = []
|
||||
names = []
|
||||
emails = []
|
||||
dates = []
|
||||
currencies = []
|
||||
statuses = []
|
||||
ids = []
|
||||
usernames = []
|
||||
|
||||
for transaction in results:
|
||||
try:
|
||||
r = Rower.objects.filter(
|
||||
customer_id=transaction.customer['id'],
|
||||
paymentprocessor='braintree')[0]
|
||||
countries.append(r.country)
|
||||
names.append('{f} {l}'.format(
|
||||
f = r.user.first_name,
|
||||
l = r.user.last_name,
|
||||
)
|
||||
)
|
||||
emails.append(r.user.email)
|
||||
ids.append(r.id)
|
||||
usernames.append(r.user.username)
|
||||
|
||||
except KeyError:
|
||||
countries.append(
|
||||
transaction.credit_card_details.country_of_issuance)
|
||||
names.append('{f} {l}'.format(
|
||||
f = transaction.customer['first_name'],
|
||||
l = transaction.customer['last_name']
|
||||
)
|
||||
)
|
||||
emails.append(transaction.customer.email)
|
||||
ids.append(transaction.customer['id'])
|
||||
usernames.append('unknown')
|
||||
|
||||
|
||||
amounts.append(transaction.amount)
|
||||
dates.append(transaction.created_at)
|
||||
currencies.append(transaction.currency_iso_code)
|
||||
card_countries.append(
|
||||
transaction.credit_card_details.country_of_issuance)
|
||||
statuses.append(transaction.status)
|
||||
|
||||
|
||||
df = pd.DataFrame({
|
||||
'name':names,
|
||||
'email':emails,
|
||||
'date':dates,
|
||||
'amount':amounts,
|
||||
'currency':currencies,
|
||||
'country':countries,
|
||||
'card_country':card_countries,
|
||||
'status':statuses,
|
||||
'username':usernames,
|
||||
'user_id':ids,
|
||||
}
|
||||
)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from rowers.models import (
|
||||
Workout,Rower,Team,PlannedSession,GeoCourse,
|
||||
VirtualRace,VirtualRaceResult,IndoorVirtualRaceResult
|
||||
VirtualRace,VirtualRaceResult,IndoorVirtualRaceResult,
|
||||
PaidPlan
|
||||
)
|
||||
from rowers.rows import validate_file_extension,must_be_csv,validate_image_extension,validate_kml
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
@@ -18,7 +19,12 @@ from django.forms import formset_factory
|
||||
from utils import landingpages
|
||||
from metrics import axes
|
||||
|
||||
|
||||
# BillingForm form
|
||||
class BillingForm(forms.Form):
|
||||
amount = forms.FloatField(required=True)
|
||||
plan = forms.IntegerField(widget=forms.HiddenInput())
|
||||
payment_method_nonce = forms.CharField(max_length=255,required=True)
|
||||
|
||||
|
||||
# login form
|
||||
class LoginForm(forms.Form):
|
||||
@@ -708,7 +714,38 @@ class StatsOptionsForm(forms.Form):
|
||||
for type in mytypes.checktypes:
|
||||
self.fields[type] = forms.BooleanField(initial=True,required=False)
|
||||
|
||||
class PlanSelectForm(forms.Form):
|
||||
plan = forms.ModelChoiceField(queryset=PaidPlan.objects.all(),
|
||||
widget=forms.RadioSelect,required=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
paymentprocessor = kwargs.pop('paymentprocessor',None)
|
||||
rower = kwargs.pop('rower',None)
|
||||
includeall = kwargs.pop('includeall',False)
|
||||
super(PlanSelectForm, self).__init__(*args, **kwargs)
|
||||
self.fields['plan'].empty_label = None
|
||||
if paymentprocessor:
|
||||
self.fields['plan'].queryset = PaidPlan.objects.filter(
|
||||
paymentprocessor=paymentprocessor
|
||||
).exclude(
|
||||
shortname="basic"
|
||||
).order_by(
|
||||
"price","clubsize","shortname"
|
||||
)
|
||||
if rower and not includeall:
|
||||
try:
|
||||
amount = rower.paidplan.price
|
||||
except AttributeError:
|
||||
amount = 0
|
||||
self.fields['plan'].queryset = PaidPlan.objects.filter(
|
||||
paymentprocessor=rower.paymentprocessor
|
||||
).exclude(
|
||||
price__lte=amount
|
||||
).order_by(
|
||||
"price","clubsize","shortname"
|
||||
)
|
||||
|
||||
|
||||
class CourseSelectForm(forms.Form):
|
||||
course = forms.ModelChoiceField(queryset=GeoCourse.objects.all())
|
||||
|
||||
|
||||
116
rowers/models.py
116
rowers/models.py
@@ -17,6 +17,7 @@ import os
|
||||
import twitter
|
||||
import re
|
||||
import pytz
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
from scipy.interpolate import splprep, splev, CubicSpline
|
||||
import numpy as np
|
||||
@@ -536,6 +537,49 @@ weightcategories = (
|
||||
)
|
||||
|
||||
|
||||
# Plan
|
||||
plans = (
|
||||
('basic','basic'),
|
||||
('pro','pro'),
|
||||
('plan','plan'),
|
||||
('coach','coach')
|
||||
)
|
||||
|
||||
paymenttypes = (
|
||||
('single','single'),
|
||||
('recurring','recurring')
|
||||
)
|
||||
|
||||
paymentprocessors = (
|
||||
('paypal','PayPal'),
|
||||
('braintree','BrainTree')
|
||||
)
|
||||
|
||||
class PaidPlan(models.Model):
|
||||
shortname = models.CharField(max_length=50,choices=plans)
|
||||
name = models.CharField(max_length=200)
|
||||
external_id = models.CharField(blank=True,null=True,default=None,max_length=200)
|
||||
price = models.FloatField(blank=True,null=True,default=None)
|
||||
paymentprocessor = models.CharField(
|
||||
max_length=50,choices=paymentprocessors,default='braintree')
|
||||
paymenttype = models.CharField(
|
||||
default='single',max_length=30,
|
||||
verbose_name='Payment Type',
|
||||
choices=paymenttypes,
|
||||
)
|
||||
|
||||
clubsize = models.IntegerField(default=0)
|
||||
|
||||
def __unicode__(self):
|
||||
return '{name} - {shortname} at {price:.2f} EURO ({paymenttype} payment)'.format(
|
||||
name = self.name,
|
||||
shortname = self.shortname,
|
||||
price = self.price,
|
||||
paymenttype = self.paymenttype,
|
||||
paymentprocessor = self.paymentprocessor,
|
||||
)
|
||||
|
||||
|
||||
# Extension of User with rowing specific data
|
||||
class Rower(models.Model):
|
||||
adaptivetypes = mytypes.adaptivetypes
|
||||
@@ -571,9 +615,43 @@ class Rower(models.Model):
|
||||
('Yoga','Yoga'),
|
||||
)
|
||||
user = models.OneToOneField(User)
|
||||
|
||||
#billing details
|
||||
country = CountryField(default=None, null=True, blank=True)
|
||||
street_address = models.CharField(default='',blank=True,null=True,max_length=200)
|
||||
city = models.CharField(default='',blank=True,null=True,max_length=200)
|
||||
postal_code = models.CharField(default='',blank=True,null=True,max_length=200)
|
||||
|
||||
customer_id = models.CharField(default=None,null=True,blank=True,max_length=200)
|
||||
subscription_id = models.CharField(default=None,null=True,
|
||||
blank=True,max_length=200)
|
||||
|
||||
rowerplan = models.CharField(default='basic',max_length=30,
|
||||
choices=plans)
|
||||
paymenttype = models.CharField(
|
||||
default='single',max_length=30,
|
||||
verbose_name='Payment Type',
|
||||
choices=paymenttypes,
|
||||
)
|
||||
paymentprocessor = models.CharField(max_length=50,
|
||||
choices=paymentprocessors,
|
||||
null=True,blank=True,
|
||||
default='braintree')
|
||||
|
||||
paidplan = models.ForeignKey(PaidPlan,null=True,default=None)
|
||||
|
||||
planexpires = models.DateField(default=timezone.now)
|
||||
teamplanexpires = models.DateField(default=timezone.now)
|
||||
clubsize = models.IntegerField(default=0)
|
||||
protrialexpires = models.DateField(blank=True,null=True)
|
||||
plantrialexpires = models.DateField(blank=True,null=True)
|
||||
|
||||
|
||||
# Privacy Data
|
||||
gdproptin = models.BooleanField(default=False)
|
||||
gdproptindate = models.DateTimeField(blank=True,null=True)
|
||||
|
||||
|
||||
# Heart Rate Zone data
|
||||
max = models.IntegerField(default=192,verbose_name="Max Heart Rate")
|
||||
rest = models.IntegerField(default=48,verbose_name="Resting Heart Rate")
|
||||
@@ -683,13 +761,6 @@ class Rower(models.Model):
|
||||
blank=True,null=True)
|
||||
runkeeper_auto_export = models.BooleanField(default=False)
|
||||
|
||||
# Plan
|
||||
plans = (
|
||||
('basic','basic'),
|
||||
('pro','pro'),
|
||||
('plan','plan'),
|
||||
('coach','coach')
|
||||
)
|
||||
|
||||
privacychoices = (
|
||||
('visible','Visible'),
|
||||
@@ -704,23 +775,6 @@ class Rower(models.Model):
|
||||
getimportantemails = models.BooleanField(default=True,
|
||||
verbose_name='Get Important Emails')
|
||||
|
||||
rowerplan = models.CharField(default='basic',max_length=30,
|
||||
choices=plans)
|
||||
paymenttype = models.CharField(
|
||||
default='single',max_length=30,
|
||||
verbose_name='Payment Type',
|
||||
choices=(
|
||||
('single','single'),
|
||||
('recurring','recurring')
|
||||
)
|
||||
)
|
||||
|
||||
planexpires = models.DateField(default=timezone.now)
|
||||
teamplanexpires = models.DateField(default=timezone.now)
|
||||
clubsize = models.IntegerField(default=0)
|
||||
protrialexpires = models.DateField(blank=True,null=True)
|
||||
plantrialexpires = models.DateField(blank=True,null=True)
|
||||
|
||||
|
||||
# Friends/Team
|
||||
friends = models.ManyToManyField("self",blank=True)
|
||||
@@ -2951,6 +3005,20 @@ class RowerImportExportForm(ModelForm):
|
||||
'trainingpeaks_auto_export',
|
||||
]
|
||||
|
||||
# Form to collect rower's Billing Info
|
||||
class RowerBillingAddressForm(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'street_address',
|
||||
'city',
|
||||
'postal_code',
|
||||
'country'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RowerBillingAddressForm, self).__init__(*args, **kwargs)
|
||||
self.fields['country'].required = True
|
||||
|
||||
|
||||
# Form to set rower's Email and Weight category
|
||||
|
||||
40
rowers/payments.py
Normal file
40
rowers/payments.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from rowers.models import Rower,PaidPlan
|
||||
|
||||
# run once - copies plans to paypal
|
||||
def planstopaypal():
|
||||
plans = PaidPlan.objects.all()
|
||||
|
||||
for plan in plans:
|
||||
plan.pk = None
|
||||
plan.paymentprocessor = 'paypal'
|
||||
plan.external_id = None
|
||||
plan.save()
|
||||
|
||||
def initiaterowerplans():
|
||||
rowers = Rower.objects.filter(paymenttype = 'recurring',paidplan = None)
|
||||
for r in rowers:
|
||||
r.paymentprocessor = 'paypal'
|
||||
r.save()
|
||||
|
||||
def setrowerplans():
|
||||
rowers = Rower.objects.all()
|
||||
|
||||
for r in rowers:
|
||||
paidplans = PaidPlan.objects.filter(
|
||||
shortname = r.rowerplan,
|
||||
paymenttype = r.paymenttype,
|
||||
clubsize = r.clubsize,
|
||||
paymentprocessor=r.paymentprocessor)
|
||||
|
||||
if paidplans:
|
||||
r.paidplan = paidplans[0]
|
||||
r.save()
|
||||
else:
|
||||
print 'Could not set plan for ',r
|
||||
|
||||
def is_existing_customer(rower):
|
||||
if rower.country is not None and rower.customer_id is not None and rower.country != '':
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
134
rowers/tasks.py
134
rowers/tasks.py
@@ -739,7 +739,139 @@ def handle_updatedps(useremail, workoutids, debug=False,**kwargs):
|
||||
|
||||
return 1
|
||||
|
||||
# send email when a breakthrough workout is uploaded
|
||||
@app.task
|
||||
def handle_send_email_transaction(
|
||||
username, useremail, amount, **kwargs):
|
||||
|
||||
if 'debug' in kwargs:
|
||||
debug = kwargs['debug']
|
||||
else:
|
||||
debug = True
|
||||
|
||||
subject = "Rowsandall Payment Confirmation"
|
||||
|
||||
from_email = 'Rowsandall <admin@rowsandall.com>'
|
||||
|
||||
d = {
|
||||
'name': username,
|
||||
'siteurl': siteurl,
|
||||
'amount': amount
|
||||
}
|
||||
|
||||
res = send_template_email(from_email,[useremail],
|
||||
subject,
|
||||
'paymentconfirmationemail.html',
|
||||
d, **kwargs)
|
||||
|
||||
return 1
|
||||
|
||||
@app.task
|
||||
def handle_send_email_failed_cancel(
|
||||
name, email, username, id, **kwargs):
|
||||
|
||||
if 'debug' in kwargs:
|
||||
debug = kwargs['debug']
|
||||
else:
|
||||
debug = True
|
||||
|
||||
subject = "Rowsandall Subscription Cancellation Error"
|
||||
|
||||
from_email = 'Rowsandall <admin@rowsandall.com>'
|
||||
|
||||
d = {
|
||||
'name': name,
|
||||
'siteurl': siteurl,
|
||||
'email': email,
|
||||
'username': username,
|
||||
'id': id,
|
||||
}
|
||||
|
||||
res = send_template_email(from_email,["support@rowsandall.com"],
|
||||
subject,
|
||||
'cancel_subscription_fail_email.html',
|
||||
d, **kwargs)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_send_email_subscription_update(
|
||||
username, useremail, planname, recurring, price, amount,
|
||||
end_of_billing_period, method, **kwargs):
|
||||
|
||||
if 'debug' in kwargs:
|
||||
debug = kwargs['debug']
|
||||
else:
|
||||
debug = True
|
||||
|
||||
|
||||
from_email = 'Rowsandall <admin@rowsandall.com>'
|
||||
|
||||
d = {
|
||||
'name': username,
|
||||
'siteurl': siteurl,
|
||||
'amount': amount,
|
||||
'price':price,
|
||||
'planname': planname,
|
||||
'recurring': recurring,
|
||||
'end_of_billing_period': end_of_billing_period,
|
||||
}
|
||||
|
||||
if method == 'down':
|
||||
template_name = 'subscription_downgrade_email.html'
|
||||
notification_template_name = 'subscription_downgrade_notification.html'
|
||||
subject = "Rowsandall Change Confirmation"
|
||||
else:
|
||||
template_name = 'subscription_update_email.html'
|
||||
notification_template_name = 'subscription_update_notification.html'
|
||||
subject = "Rowsandall Payment Confirmation"
|
||||
|
||||
res = send_template_email(from_email,[useremail],
|
||||
subject,
|
||||
template_name,
|
||||
d, **kwargs)
|
||||
|
||||
res = send_template_email(from_email,['info@rowsandall.com'],
|
||||
'Subscription Update Notification',
|
||||
template_name,
|
||||
d, **kwargs)
|
||||
|
||||
return 1
|
||||
|
||||
@app.task
|
||||
def handle_send_email_subscription_create(
|
||||
username, useremail, planname, recurring, price, amount,
|
||||
end_of_billing_period, **kwargs):
|
||||
|
||||
if 'debug' in kwargs:
|
||||
debug = kwargs['debug']
|
||||
else:
|
||||
debug = True
|
||||
|
||||
subject = "Rowsandall Payment Confirmation"
|
||||
|
||||
from_email = 'Rowsandall <admin@rowsandall.com>'
|
||||
|
||||
d = {
|
||||
'name': username,
|
||||
'siteurl': siteurl,
|
||||
'amount': amount,
|
||||
'price':price,
|
||||
'planname': planname,
|
||||
'end_of_billing_period': end_of_billing_period,
|
||||
'recurring': recurring,
|
||||
}
|
||||
|
||||
res = send_template_email(from_email,[useremail],
|
||||
subject,
|
||||
'subscription_create_email.html',
|
||||
d, **kwargs)
|
||||
|
||||
res = send_template_email(from_email,['info@rowsandall.com'],
|
||||
'Subscription Update Notification',
|
||||
'subscription_create_notification.html',
|
||||
d, **kwargs)
|
||||
return 1
|
||||
|
||||
@app.task
|
||||
def handle_sendemail_raceregistration(
|
||||
|
||||
36
rowers/templates/billing.html
Normal file
36
rowers/templates/billing.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Upgrade</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
<ul class="main-content">
|
||||
<li class="grid_4">
|
||||
<h2>Fill in Billing Details</h2>
|
||||
<p>For tax reasons, we need your country of residence</p>
|
||||
<table>
|
||||
{{ billingaddressform.as_table }}
|
||||
</table>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
<h2>Choose your Plan</h2>
|
||||
<table width="100%">
|
||||
{{ planselectform.as_table }}
|
||||
</table>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="Proceed">
|
||||
You will be able to review your order before purchase.
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
25
rowers/templates/braintreedropin.html
Normal file
25
rowers/templates/braintreedropin.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<script src="https://js.braintreegateway.com/web/dropin/1.14.1/js/dropin.min.js"></script>
|
||||
<script>
|
||||
var form = document.querySelector('#payment-form');
|
||||
var client_token = '{{ client_token }}';
|
||||
braintree.dropin.create({
|
||||
authorization: client_token,
|
||||
container: '#bt-dropin',
|
||||
paypal: {
|
||||
flow: 'vault'
|
||||
}
|
||||
}, function (createErr, instance) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
instance.requestPaymentMethod(function (err, payload) {
|
||||
if (err) {
|
||||
console.log('Error', err);
|
||||
return;
|
||||
}
|
||||
// Add the nonce to the form and submit
|
||||
document.querySelector('#nonce').value = payload.nonce;
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
21
rowers/templates/cancel_subscription_fail_email.html
Normal file
21
rowers/templates/cancel_subscription_fail_email.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>
|
||||
User {{ name }} tried to cancel his subscription with id "{{ id }}" on {{ siteurl }} but failed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
User name: {{ username }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
User email: {{ email }}
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
52
rowers/templates/downgrade.html
Normal file
52
rowers/templates/downgrade.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Downgrade</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
<ul class="main-content">
|
||||
<li class="grid_3">
|
||||
<h2>Billing Details</h2>
|
||||
<p>For tax reasons, we need your country of residence. You should
|
||||
update this when it is incorrect.</p>
|
||||
<table>
|
||||
{{ billingaddressform.as_table }}
|
||||
</table>
|
||||
</li>
|
||||
<li class="grid_3">
|
||||
<h2>Choose your Plan</h2>
|
||||
<table width="100%">
|
||||
{{ planselectform.as_table }}
|
||||
</table>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Your downgrade will be effective immediately.
|
||||
The price difference for the current billing cycle will
|
||||
be credited to your next charge (prorated). For example,
|
||||
when you downgrade from a 65€ plan to a 15€ plan
|
||||
(50€ difference), in the 6th month of the 12 month
|
||||
billing cycle, you will have a credit of 25€ which
|
||||
will be used for the next billing cycles.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Looking for the <a href="/rowers/downgrade">downgrade</a> option?
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_3">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="Proceed">
|
||||
You will be able to review your order before purchase.
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
26
rowers/templates/downgrade_completed.html
Normal file
26
rowers/templates/downgrade_completed.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Your Change was completed</h1>
|
||||
|
||||
<p>
|
||||
Thank you for changing to {{ user.rower.paidplan.name }}. You're all settled.
|
||||
membership.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% if user.rower.paymenttype == 'recurring' %}
|
||||
Your next payment will be automatically processed on {{ user.rower.planexpires }}
|
||||
{% else %}
|
||||
Your plan will end automatically on {{ user.rower.planexpires }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
94
rowers/templates/downgradeconfirm.html
Normal file
94
rowers/templates/downgradeconfirm.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Confirm Your Changes</h1>
|
||||
|
||||
<h2>Order Overview</h2>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<table class="plantable shortpadded" width="80%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Plan</th><td>{{ plan.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payment Type</th><td>{{ plan.paymenttype }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Billing Cycle</th><td>1 year</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total</th><td>€ {{ plan.price|currency }}
|
||||
{% if plan.paymenttype == 'recurring' %}
|
||||
/year
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
<p>
|
||||
<table class="plantable shortpadded" width="80%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Street Address</th><td>{{ user.rower.street_address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>City</th><td>{{ user.rower.city }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Postal Code</th><td>{{ user.rower.postal_code }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Country</th><td>{{ user.rower.country }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<a href="/rowers/downgrade">Change Downgrade</a>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
<form id="payment-form" method="post" action="/rowers/downgradecheckouts"
|
||||
autocomplete="off">
|
||||
<section>
|
||||
<label for="amount">
|
||||
<div class="input-wrapper amount-wrapper">
|
||||
<input id="amount" name="amount" type="hidden" min="1" placeholder="Amount"
|
||||
value="{{ plan.price }}" readonly>
|
||||
</div>
|
||||
</label>
|
||||
<div class="bt-drop-in-wrapper">
|
||||
<div id="bt-dropin"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<input type="hidden" id="nonce" name="payment_method_nonce" />
|
||||
<input type="hidden" id="plan" name="plan" value="{{ plan.id }}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" id="submit-button"><span>Downgrade to the € {{ plan.price|currency }} plan</span></button>
|
||||
</form>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
<p>
|
||||
Your downgrade will be effective immediately. You will not be charged.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% include 'braintreedropin.html' %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -27,8 +27,16 @@
|
||||
<i class="fas fa-tachometer-alt-slow fa-fw"></i> Manage Workflow
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_authenticated and user.is_staff %}
|
||||
<li id="manage-transactions">
|
||||
<a href="/rowers/me/transactions/">
|
||||
<i class="fas fa-credit-card fa-fw"></i> Transactions
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul><!-- cd-accordion-menu -->
|
||||
|
||||
|
||||
{% if user.is_authenticated and user|is_manager %}
|
||||
<p> </p>
|
||||
{% if user|team_members %}
|
||||
|
||||
293
rowers/templates/paidplans.html
Normal file
293
rowers/templates/paidplans.html
Normal file
@@ -0,0 +1,293 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Paid Membership Plans</h1>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_4">
|
||||
<p>Rowsandall.com offers free data and analysis for rowers, by rowers.
|
||||
Of course, offering this service is not free. To help cover the
|
||||
hosting costs, we have created paid plans offering extended
|
||||
functionality.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<table class="plantable shortpadded" width="80%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>BASIC</th>
|
||||
<th>PRO</th>
|
||||
<th>SELF-COACH</th>
|
||||
<th>COACH</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Basic rowing metrics (spm, time, distance, heart rate, power)</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manual Import, Export, Synchronization and download of all your data</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Automatic Synchronization with other fitness sites</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Heart rate and power zones</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ranking Pieces, Stroke Analysis</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Advanced Analysis (Critical Power, Stats, Box Chart, Trend Flex)</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Compare Workouts</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Empower Stroke Profile</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sensor Fusion, Split Workout, In-stroke metrics</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create Training plans, tests and challenges for yourself. Track your performance
|
||||
against plan.</td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create Training plans, tests and challenges for your athletes. Track their performance
|
||||
against plan. </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create and manage teams.</td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manage your athlete's workouts</td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pricing</td>
|
||||
<td>FREE</td>
|
||||
<td nowrap="nowrap">From 15€/year</td>
|
||||
<td nowrap="nowrap">From 65€/year</td>
|
||||
<td nowrap="nowrap">From 90€/year</td>
|
||||
</tr>
|
||||
{% if rower %}
|
||||
<tr>
|
||||
<td>Your current plan</td>
|
||||
<td>
|
||||
{% if rower.rowerplan == 'basic' %}
|
||||
<h3>BASIC</h3>
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if rower.rowerplan == 'pro' %}
|
||||
PRO
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if rower.rowerplan == 'plan' %}
|
||||
SELF-COACH
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if rower.rowerplan == 'coach' %}
|
||||
COACH
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>
|
||||
Available trials
|
||||
</td>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_anonymous %}
|
||||
<button style="width:100%">
|
||||
<a href="/rowers/starttrial">Free PRO trial</a>
|
||||
</button>
|
||||
{% elif rower and rower.rowerplan == 'basic' and rower.protrialexpires|date_dif == 1 %}
|
||||
<button style="width:100%">
|
||||
<a href="/rowers/starttrial">Free PRO trial</a>
|
||||
</button>
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_anonymous %}
|
||||
<button style="width:100%">
|
||||
<a href="/rowers/startplantrial">Free SELF-COACH trial</a>
|
||||
</button>
|
||||
{% elif rower and rower.rowerplan == 'basic' and rower.plantrialexpires|date_dif == 1 %}
|
||||
<button style="width:100%">
|
||||
<a href="/rowers/startplantrial">Free SELF-COACH trial</a>
|
||||
</button>
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Available upgrades
|
||||
</td>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
{% if user.is_anonymous %}
|
||||
<td colspan="3">
|
||||
<button style="width:100%">
|
||||
<a href="/rowers/upgrade">UPGRADE NOW</a>
|
||||
</button>
|
||||
</td>
|
||||
{% elif rower and rower.rowerplan == 'basic' %}
|
||||
<td colspan="3">
|
||||
<button style="width:100%">
|
||||
{% if user|existing_customer %}
|
||||
<a href="/rowers/upgrade">UPGRADE NOW</a>
|
||||
{% else %}
|
||||
<a href="/rowers/billing">BUY NOW</a>
|
||||
{% endif %}
|
||||
</button>
|
||||
</td>
|
||||
{% elif rower and rower.rowerplan == 'pro' %}
|
||||
<td> </td>
|
||||
<td colspan="2">
|
||||
<button style="width:100%">
|
||||
{% if user|existing_customer %}
|
||||
<a href="/rowers/upgrade">UPGRADE NOW</a>
|
||||
{% else %}
|
||||
<a href="/rowers/billing">BUY NOW</a>
|
||||
{% endif %}
|
||||
</button>
|
||||
</td>
|
||||
{% elif rower and rower.rowerplan == 'plan' %}
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button style="width:100%">
|
||||
{% if user|existing_customer %}
|
||||
<a href="/rowers/upgrade">UPGRADE NOW</a>
|
||||
{% else %}
|
||||
<a href="/rowers/billing">BUY NOW</a>
|
||||
{% endif %}
|
||||
</button>
|
||||
</td>
|
||||
{% elif rower and rower.rowerplan == 'coach' and rower.clubsize < 100 %}
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button style="width:100%">
|
||||
{% if user|existing_customer %}
|
||||
<a href="/rowers/upgrade">UPGRADE NOW</a>
|
||||
{% else %}
|
||||
<a href="/rowers/billing">BUY NOW</a>
|
||||
{% endif %}
|
||||
</button>
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan=3>
|
||||
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
|
||||
<h2>Coach and Self-Coach Membership</h2>
|
||||
|
||||
<p>The Coach plan functionality listed is available to the coach only. Individual athletes
|
||||
can purchase upgrades to "Pro" and "Self-Coach" plans.
|
||||
</p>
|
||||
|
||||
<p>Rowsandall.com's Training Planning functionality
|
||||
is part of the paid "Self-Coach" and "Coach" plans.</p>
|
||||
|
||||
<p>On the "Self-Coach" plan, you can plan your own sessions.</p>
|
||||
|
||||
<p>On the "Coach" plan, you can establish teams, see workouts done by
|
||||
athletes on your team, and plan individual and group sessions for your
|
||||
athletes.
|
||||
</p>
|
||||
|
||||
<p>If you would like to find a coach who helps you plan your training
|
||||
through rowsandall.com, contact me throught the contact form.</p>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_help.html' %}
|
||||
{% endblock %}
|
||||
|
||||
26
rowers/templates/payment_completed.html
Normal file
26
rowers/templates/payment_completed.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Your Payment was completed</h1>
|
||||
|
||||
<p>
|
||||
Thank you for registering to {{ user.rower.paidplan.name }}. You have paid for 12 months
|
||||
membership.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% if user.rower.paymenttype == 'recurring' %}
|
||||
Your next payment will be automatically processed on {{ user.rower.planexpires }}
|
||||
{% else %}
|
||||
Your plan will end automatically on {{ user.rower.planexpires }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
89
rowers/templates/paymentconfirm.html
Normal file
89
rowers/templates/paymentconfirm.html
Normal file
@@ -0,0 +1,89 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Confirm Your Payment</h1>
|
||||
|
||||
<h2>Order Overview</h2>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<table class="plantable shortpadded" width="80%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Plan</th><td>{{ plan.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payment Type</th><td>{{ plan.paymenttype }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Plan Duration</th><td>1 year starting today</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total</th><td>€ {{ plan.price|currency }}
|
||||
{% if plan.paymenttype == 'recurring' %}
|
||||
/year
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
<p>
|
||||
<table class="plantable shortpadded" width="80%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Street Address</th><td>{{ user.rower.street_address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>City</th><td>{{ user.rower.city }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Postal Code</th><td>{{ user.rower.postal_code }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Country</th><td>{{ user.rower.country }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<a href="/rowers/billing">Change Order</a>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
<form id="payment-form" method="post" action="/rowers/checkouts"
|
||||
autocomplete="off">
|
||||
<section>
|
||||
<label for="amount">
|
||||
<div class="input-wrapper amount-wrapper">
|
||||
<input id="amount" name="amount" type="hidden" min="1" placeholder="Amount"
|
||||
value="{{ plan.price }}" readonly>
|
||||
</div>
|
||||
</label>
|
||||
<div class="bt-drop-in-wrapper">
|
||||
<div id="bt-dropin"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<input type="hidden" id="nonce" name="payment_method_nonce" />
|
||||
<input type="hidden" id="plan" name="plan" value="{{ plan.id }}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" id="submit-button"><span>Pay € {{ plan.price|currency }}</span></button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% include 'braintreedropin.html' %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
19
rowers/templates/paymentconfirmationemail.html
Normal file
19
rowers/templates/paymentconfirmationemail.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>Dear <strong>{{ name }}</strong>,</p>
|
||||
|
||||
<p>
|
||||
Thank you. We have received the payment of € {{ amount }} for Rowsandall related services.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please contact our customer service by replying to this email if you have any further
|
||||
questions regarding the payment.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
296
rowers/templates/payments.html
Normal file
296
rowers/templates/payments.html
Normal file
@@ -0,0 +1,296 @@
|
||||
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Pro Membership{% endblock title %}
|
||||
{% block main %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
<h1>Payments</h1>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<p>Donations are welcome to keep this web site going. To help cover the hosting
|
||||
costs, I have created several paid plans offering advanced functionality.
|
||||
Once I process your
|
||||
donation, I will give you access to some <q>special</q> features on this
|
||||
website. </p>
|
||||
|
||||
<p>The following table gives an overview of the different plans. As we are
|
||||
constantly developing new functionality, the table might be slightly outdated. Don't
|
||||
hesitate to contact us. </p>
|
||||
|
||||
<p>The Pro membership is open for a free 14 day trial</p>
|
||||
<p>
|
||||
<table class="listtable paddedtable" width="80%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>BASIC</th>
|
||||
<th>PRO</th>
|
||||
<th>SELF-COACH</th>
|
||||
<th>COACH</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Basic rowing metrics (spm, time, distance, heart rate, power)</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manual Import, Export, Synchronization and download of all your data</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Automatic Synchronization with other fitness sites</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Heart rate and power zones</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ranking Pieces, Stroke Analysis</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Advanced Analysis (Critical Power, Stats, Box Chart, Trend Flex)</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Compare Workouts</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Empower Stroke Profile</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sensor Fusion, Split Workout, In-stroke metrics</td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create Training plans, tests and challenges for yourself. Track your performance
|
||||
against plan.</td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create Training plans, tests and challenges for your athletes. Track their performance
|
||||
against plan. </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create and manage teams.</td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manage your athlete's workouts</td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td>✔</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
<h2>Coach and Self-Coach Membership</h2>
|
||||
|
||||
<p>The Coach plan functionality listed is available to the coach only. Individual athletes
|
||||
can purchase upgrades to "Pro" and "Self-Coach" plans.
|
||||
</p>
|
||||
|
||||
<p>Rowsandall.com's Training Planning functionality
|
||||
is part of the paid "Self-Coach" and "Coach" plans.</p>
|
||||
|
||||
<p>On the "Self-Coach" plan, you can plan your own sessions.</p>
|
||||
|
||||
<p>On the "Coach" plan, you can establish teams, see workouts done by
|
||||
athletes on your team, and plan individual and group sessions for your
|
||||
athletes.
|
||||
</p>
|
||||
|
||||
<p>If you would like to find a coach who helps you plan your training
|
||||
through rowsandall.com, contact me throught the contact form.</p>
|
||||
|
||||
{% if user.rower.rowerplan == 'basic' and user.rower.protrialexpires|date_dif == 1 %}
|
||||
<h2>Free Trial</h2>
|
||||
<p>
|
||||
You qualify for a 14 day free trial. No credit card needed.
|
||||
Try out Pro or Self-Coach membership for two weeks. Click the button below to
|
||||
sign up for the trial. After your trial period expires, you will be
|
||||
automatically reset to the Basic plan, unless you upgrade to Pro.
|
||||
</p>
|
||||
<p><a class="button green small" href="/rowers/starttrial">Yes, I want to try Pro membership for 14 days for free. No strings attached.</a></p>
|
||||
<p><a class="button green small" href="/rowers/startplantrial">Yes, I want to try Self-Coach membership for 14 days for free. No strings attached.</a></p>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<p>Click on the PayPal button to pay for your Pro membership. Before you pay, please <a href="/rowers/register">register</a> for the free Basic membership and add your user name to the form.
|
||||
Your payment will be valid for one year.
|
||||
You will be taken to the secure PayPal payment site.
|
||||
</p>
|
||||
<h2>Recurring Payment</h2>
|
||||
<p>You need a Paypal account for this. This is plan will automatically renew each year.</p>
|
||||
<p>
|
||||
<form class="paypal" action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_s-xclick">
|
||||
<input type="hidden" name="hosted_button_id" value="964GLEXX3THAW">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="on0" value="Plans">Plans
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<select name="os0">
|
||||
<option value="Pro Membership">Pro Membership : €15.00 EUR - yearly</option>
|
||||
<option value="Self-Coach Membership">Self-Coach Membership : €65.00 EUR - yearly</option>
|
||||
<option value="Coach 4 athletes or less">Coach 4 athletes or less : €90.00 EUR - yearly</option>
|
||||
<option value="Coach 4-10 athletes">Coach 4-10 athletes : €200.00 EUR - yearly</option>
|
||||
<option value="Coach more than 10 athletes">Coach more than 10 athletes : €450.00 EUR - yearly</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="on1" value="Your User Name">Your User Name
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" name="os1" maxlength="200">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="hidden" name="currency_code" value="EUR">
|
||||
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_subscribeCC_LG_global.gif" border="0" name="submit" alt="PayPal – The safer, easier way to pay online!">
|
||||
<img class="paypalpix" alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
||||
</form>
|
||||
</p>
|
||||
<h2>One Year Subscription</h2>
|
||||
<p>Only a credit card needed. Will not automatically renew</p>
|
||||
|
||||
<p>
|
||||
<form class="paypal" action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top"
|
||||
>
|
||||
<input type="hidden" name="cmd" value="_s-xclick">
|
||||
<input type="hidden" name="hosted_button_id" value="2YB32HQTF96QW">
|
||||
<table>
|
||||
<tr><td><input type="hidden" name="on0" value="Plans">Plans</td></tr><tr><td><select name="os0">
|
||||
<option value="Pro Membership">Pro Membership €20.00 EUR</option>
|
||||
<option value="Self-Coach Membership">Self-Coach Membership €75.00 EUR</option>
|
||||
<option value="Coach - 4 athletes or less">Coach - 4 athletes or less €120.00 EUR</option>
|
||||
<option value="Coach - 4-10 athletes">Coach - 4-10 athletes €250.00 EUR</option>
|
||||
<option value="Coach - more than 10 athletes">Coach - more than 10 athletes €500.00 EUR</option>
|
||||
</select> </td></tr>
|
||||
<tr><td><input type="hidden" name="on1" value="Your User Name">Your User Name</td></tr><tr><td><input type="text" name="os1" maxlength="200"></td></tr>
|
||||
</table>
|
||||
<input type="hidden" name="currency_code" value="EUR">
|
||||
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_buynowCC_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
|
||||
<img class="paypalpix" alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
||||
</form>
|
||||
</p>
|
||||
<h2>Payment Processing</h2>
|
||||
<p>After you do the payment, we will manually change your membership to
|
||||
"Pro". Depending on our availability, this may take some time
|
||||
(typically one working day). Don't hesitate to contact us
|
||||
if you have any questions at this stage.</p>
|
||||
|
||||
<p>If, for any reason, you are not happy with your Pro membership, please let me know through the contact form. I will contact you as soon as possible to discuss how we can make things better.</p>
|
||||
|
||||
<h2>BrainTree Experimental Corner</h2>
|
||||
|
||||
<form id="payment-form" method="post" action="/rowers/checkouts"
|
||||
autocomplete="off">
|
||||
<section>
|
||||
<label for="amount">
|
||||
<span class="input-label">Amount</span>
|
||||
<div class="input-wrapper amount-wrapper">
|
||||
<input id="amount" name="amount" type="tel" min="1" placeholder="Amount" value="10">
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="bt-drop-in-wrapper">
|
||||
<div id="bt-dropin"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<input type="hidden" id="nonce" name="payment_method_nonce" />
|
||||
{% csrf_token %}
|
||||
<button type="submit" id="submit-button"><span>Test Transaction</span></button>
|
||||
</form>
|
||||
|
||||
<script src="https://js.braintreegateway.com/web/dropin/1.14.1/js/dropin.min.js"></script>
|
||||
<script>
|
||||
var form = document.querySelector('#payment-form');
|
||||
var client_token = '{{ client_token }}';
|
||||
braintree.dropin.create({
|
||||
authorization: client_token,
|
||||
container: '#bt-dropin',
|
||||
paypal: {
|
||||
flow: 'checkout'
|
||||
}
|
||||
}, function (createErr, instance) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
instance.requestPaymentMethod(function (err, payload) {
|
||||
if (err) {
|
||||
console.log('Error', err);
|
||||
return;
|
||||
}
|
||||
// Add the nonce to the form and submit
|
||||
document.querySelector('#nonce').value = payload.nonce;
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_help.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
if you have any questions at this stage.</p>
|
||||
|
||||
<p>If, for any reason, you are not happy with your Pro membership, please let me know through the contact form. I will contact you as soon as possible to discuss how we can make things better.</p>
|
||||
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<h2>Account Information</h2>
|
||||
<p>
|
||||
{% if rower.user == user %}
|
||||
<a class="button blue small" href="/password_change/">Password Change</a>
|
||||
<a href="/password_change/">Password Change</a>
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
@@ -34,29 +34,49 @@
|
||||
{{ userform.as_table }}
|
||||
{{ accountform.as_table }}
|
||||
<tr>
|
||||
<th>Plan</th><td>{{ rower.rowerplan }}</td>
|
||||
<th> </th><td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Plan Expiry</th><td>{{ rower.planexpires }}</td>
|
||||
<th>Plan</th><td>{{ rower.paidplan.name }}</td>
|
||||
</tr>
|
||||
{% if rower.rowerplan != 'basic' %}
|
||||
<tr>
|
||||
<th>
|
||||
{% if rower.paymenttype != 'recurring' %}
|
||||
Plan Expiry
|
||||
{% else %}
|
||||
Next Payment Due
|
||||
{% endif %}
|
||||
</th><td>{{ rower.planexpires }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% csrf_token %}
|
||||
{% if rower.rowerplan == 'basic' and rower.user == user %}
|
||||
<a class="button blue" href="/rowers/promembership">Upgrade</a>
|
||||
{% if rower.clubsize < 100 and rower.user == user %}
|
||||
<p>
|
||||
<a href="/rowers/paidplans">Upgrade</a>
|
||||
</p>
|
||||
{% else %}
|
||||
|
||||
<p>
|
||||
|
||||
</p>
|
||||
{% endif %}
|
||||
<input class="button green" type="submit" value="Save">
|
||||
{% if rower.rowerplan != 'basic' and rower.user == user %}
|
||||
<p>
|
||||
<a href="/rowers/me/cancelsubscriptions">Cancel Subscription</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
</li>
|
||||
{% if rower.user == user %}
|
||||
<li class="grid_2">
|
||||
<h2>GDPR - Data Protection</h2>
|
||||
<p>
|
||||
<a class="button blue small" href="/rowers/exportallworkouts">Download your data</a>
|
||||
<a href="/rowers/exportallworkouts">Download your data</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="button blue small" href="/rowers/me/deactivate">Deactivate Account</a>
|
||||
<a href="/rowers/me/deactivate">Deactivate Account</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="button red small" href="/rowers/me/delete">Delete Account</a>
|
||||
@@ -79,7 +99,7 @@
|
||||
<td>{{ grant.application }}</td>
|
||||
<td>{{ grant.scope }}</td>
|
||||
<td>
|
||||
<a class="button red small" href="/rowers/me/revokeapp/{{ grant.application.id }}">Revoke</a>
|
||||
<a href="/rowers/me/revokeapp/{{ grant.application.id }}">Revoke</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
60
rowers/templates/subscription_create_email.html
Normal file
60
rowers/templates/subscription_create_email.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>Dear <strong>{{ name }}</strong>,</p>
|
||||
|
||||
<p>
|
||||
Thank you. We have received the payment of € {{ amount }} for your new
|
||||
subscription to the Rowsandall paid plan "{{ planname }}".
|
||||
</p>
|
||||
|
||||
{% if recurring %}
|
||||
<p>
|
||||
Your next charge is due on {{ end_of_billing_period }}. We will charge your {{ paymentmethod }}
|
||||
on that date.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The subscription will keep running until you change or stop it. At any point in time you
|
||||
can change the automatically renewing subscription to a "one year only" subscription through
|
||||
<a href="{{ siteurl}}/rowers/upgrade/">the upgrade page</a>. On this page, you can also
|
||||
upgrade your subscription.
|
||||
</p>
|
||||
|
||||
{% else %}
|
||||
<p>
|
||||
This one year subscription will automatically end on {{ end_of_billing_period }}. You can
|
||||
renew your subscription after that.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
At any point in time, you can change your subscription to an automatically renewing subscription.
|
||||
You can do this on <a href="{{ siteurl}}/rowers/upgrade/">the upgrade page</a>.
|
||||
Here, you can also upgrade your subscription.
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
Upgrades in the middle of a billing cycle will be charged pro-rated. For the current billing
|
||||
cycle, you will only be charged for the price difference for the remaining fraction of the
|
||||
billing cycle. If you downgrade to a lower cost subscription, the pro-rated difference will be
|
||||
used as a credit, lowering the amount charged on the next billing cycle.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can stop the subscription through
|
||||
<a href="{{ siteurl }}/rowers/me/cancelsubscriptions">the subscription management page</a>. The
|
||||
subscription will be stopped immediately without a refund.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please contact our customer service by replying to this email if you have any further
|
||||
questions regarding your subscription.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
30
rowers/templates/subscription_create_notification.html
Normal file
30
rowers/templates/subscription_create_notification.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>User <strong>{{ name }}</strong> has created a subscription.</p>
|
||||
|
||||
<p>
|
||||
New plan: "{{ planname }}".
|
||||
</p>
|
||||
|
||||
{% if recurring %}
|
||||
<p>
|
||||
The subscription cost is €{{ price }} per year.
|
||||
The next charge is due on {{ end_of_billing_period }}. on that date.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
The subscription cost is €{{ price }}. The subscription ends on {{ end_of_billing_period }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
Amount charged: €{{ amount }}
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
62
rowers/templates/subscription_downgrade_email.html
Normal file
62
rowers/templates/subscription_downgrade_email.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>Dear <strong>{{ name }}</strong>,</p>
|
||||
|
||||
<p>
|
||||
Thank you. You have successfully changed your plan to "{{ planname }}".
|
||||
</p>
|
||||
|
||||
{% if recurring %}
|
||||
<p>
|
||||
The subscription cost is €{{ price }} per year.
|
||||
Your next charge is due on {{ end_of_billing_period }}. We will charge you automatically
|
||||
on that date. Because you downgraded, you have a credit on your account which will be
|
||||
used before charging your payment method.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The subscription will keep running until you change or stop it. At any point in time you
|
||||
can change the automatically renewing subscription to a "one year only" subscription through
|
||||
<a href="{{ siteurl}}/rowers/upgrade/">the upgrade page</a>. On this page, you can also
|
||||
upgrade your subscription.
|
||||
</p>
|
||||
|
||||
{% else %}
|
||||
<p>
|
||||
The price of the subscription is €{{ price }}.
|
||||
This one year subscription will automatically end on {{ end_of_billing_period }}. You can
|
||||
renew your subscription after that. Because you downgraded, you have a credit on your
|
||||
account which will be used for future subscriptions or upgrades.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
At any point in time, you can change your subscription to an automatically renewing subscription.
|
||||
You can do this on <a href="{{ siteurl}}/rowers/upgrade/">the upgrade page</a>.
|
||||
Here, you can also upgrade your subscription.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
Upgrades in the middle of a billing cycle are charged pro-rated. For the current billing
|
||||
cycle, you have only been charged for the price difference for the remaining fraction of the
|
||||
billing cycle. If you downgraded to a lower cost subscription, the pro-rated difference will be
|
||||
used as a credit, lowering the amount charged on the next billing cycle.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can stop the subscription through
|
||||
<a href="{{ siteurl }}/rowers/me/cancelsubscriptions">the subscription management page</a>. The
|
||||
subscription will be stopped immediately without a refund.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please contact our customer service by replying to this email if you have any further
|
||||
questions regarding your subscription.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
26
rowers/templates/subscription_downgrade_notification.html
Normal file
26
rowers/templates/subscription_downgrade_notification.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>User <strong>{{ name }}</strong> has downgraded his subscription.</p>
|
||||
|
||||
<p>
|
||||
New plan: "{{ planname }}".
|
||||
</p>
|
||||
|
||||
{% if recurring %}
|
||||
<p>
|
||||
The subscription cost is €{{ price }} per year.
|
||||
The next charge is due on {{ end_of_billing_period }}. on that date.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
The subscription cost is €{{ price }}. The subscription ends on {{ end_of_billing_period }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
63
rowers/templates/subscription_update_email.html
Normal file
63
rowers/templates/subscription_update_email.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>Dear <strong>{{ name }}</strong>,</p>
|
||||
|
||||
<p>
|
||||
Thank you. We have received the payment of € {{ amount }} for
|
||||
your updated Rowsandall subscription.
|
||||
You are now on the Rowsandall paid plan "{{ planname }}".
|
||||
</p>
|
||||
|
||||
{% if recurring %}
|
||||
<p>
|
||||
The subscription cost is €{{ price }} per year.
|
||||
Your next charge is due on {{ end_of_billing_period }}. We will charge you automatically
|
||||
on that date.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The subscription will keep running until you change or stop it. At any point in time you
|
||||
can change the automatically renewing subscription to a "one year only" subscription through
|
||||
<a href="{{ siteurl}}/rowers/upgrade/">the upgrade page</a>. On this page, you can also
|
||||
upgrade your subscription.
|
||||
</p>
|
||||
|
||||
{% else %}
|
||||
<p>
|
||||
The price of the subscription is €{{ price }}. You have paid €{{ amount }} as a
|
||||
prorated cost of your upgrade.
|
||||
This one year subscription will automatically end on {{ end_of_billing_period }}. You can
|
||||
renew your subscription after that.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
At any point in time, you can change your subscription to an automatically renewing subscription.
|
||||
You can do this on <a href="{{ siteurl}}/rowers/upgrade/">the upgrade page</a>.
|
||||
Here, you can also upgrade your subscription.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
Upgrades in the middle of a billing cycle are charged pro-rated. For the current billing
|
||||
cycle, you have only been charged for the price difference for the remaining fraction of the
|
||||
billing cycle. If you downgraded to a lower cost subscription, the pro-rated difference will be
|
||||
used as a credit, lowering the amount charged on the next billing cycle.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can stop the subscription through
|
||||
<a href="{{ siteurl }}/rowers/me/cancelsubscriptions">the subscription management page</a>. The
|
||||
subscription will be stopped immediately without a refund.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please contact our customer service by replying to this email if you have any further
|
||||
questions regarding your subscription.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
30
rowers/templates/subscription_update_notification.html
Normal file
30
rowers/templates/subscription_update_notification.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>User <strong>{{ name }}</strong> has updated his subscription.</p>
|
||||
|
||||
<p>
|
||||
New plan: "{{ planname }}".
|
||||
</p>
|
||||
|
||||
{% if recurring %}
|
||||
<p>
|
||||
The subscription cost is €{{ price }} per year.
|
||||
The next charge is due on {{ end_of_billing_period }}. on that date.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
The subscription cost is €{{ price }}. The subscription ends on {{ end_of_billing_period }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
Amount charged: €{{ amount }}
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
64
rowers/templates/subscriptions_cancel.html
Normal file
64
rowers/templates/subscriptions_cancel.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Cancel Subscriptions</h1>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_4">
|
||||
{% if subscriptions %}
|
||||
<p>
|
||||
<table class="plantable shortpadded" width="80%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subscription</th><th>Next Billing Date</th><th>Price</th><th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for subscription in subscriptions %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ subscription|lookup:"plan" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ subscription|lookup:"end_date" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ subscription|lookup:"price" }} €
|
||||
</td>
|
||||
<td>
|
||||
<a href="/rowers/me/cancelsubscription/{{ subscription|lookup:'id' }}">Stop this plan</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<p>
|
||||
By clicking on the link to stop the plan, you will downgrade to the Basic plan.
|
||||
Future payments will be stopped.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
You don't have any subscriptions or your subscriptions cannot be automatically stopped
|
||||
from the site.
|
||||
</p>
|
||||
<p>
|
||||
If you have paid through PayPal, log in to your PayPal account and cancel the recurring payment
|
||||
there. We will manually downgrade your subscription.
|
||||
</p>
|
||||
<p>
|
||||
If you have questions, don't hesitate to contact us.
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
24
rowers/templates/transactions.html
Normal file
24
rowers/templates/transactions.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
{% block main %}
|
||||
<h1>Download Transactions</h1>
|
||||
|
||||
<form actions="" method="post">
|
||||
<table>
|
||||
{{ dateform.as_table }}
|
||||
</table>
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sideheader %}
|
||||
<h1>Profile</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
49
rowers/templates/upgrade.html
Normal file
49
rowers/templates/upgrade.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Upgrade</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
<ul class="main-content">
|
||||
<li class="grid_3">
|
||||
<h2>Billing Details</h2>
|
||||
<p>For tax reasons, we need your country of residence. You should
|
||||
update this when it is incorrect.</p>
|
||||
<table>
|
||||
{{ billingaddressform.as_table }}
|
||||
</table>
|
||||
</li>
|
||||
<li class="grid_3">
|
||||
<h2>Choose your Plan</h2>
|
||||
<table width="100%">
|
||||
{{ planselectform.as_table }}
|
||||
</table>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Your upgrade will be effective immediately. For the current billing
|
||||
cycle, you will be charged for a prorated amount. For example, when
|
||||
you upgrade from a 15€ plan to a 65€ plan (a difference of
|
||||
50€) in the 6th month of the 12 month billing cycle, you
|
||||
will be charged 35€.
|
||||
</p>
|
||||
<p>
|
||||
Looking for the <a href="/rowers/downgrade">downgrade</a> option?
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_3">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="Proceed">
|
||||
You will be able to review your order before purchase.
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
99
rowers/templates/upgradeconfirm.html
Normal file
99
rowers/templates/upgradeconfirm.html
Normal file
@@ -0,0 +1,99 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Paid Membership{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Confirm Your Payment</h1>
|
||||
|
||||
<h2>Order Overview</h2>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<table class="plantable shortpadded" width="80%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Plan</th><td>{{ plan.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payment Type</th><td>{{ plan.paymenttype }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Billing Cycle</th><td>1 year</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total</th><td>€ {{ plan.price|currency }}
|
||||
{% if plan.paymenttype == 'recurring' %}
|
||||
/year
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
<p>
|
||||
<table class="plantable shortpadded" width="80%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Street Address</th><td>{{ user.rower.street_address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>City</th><td>{{ user.rower.city }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Postal Code</th><td>{{ user.rower.postal_code }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Country</th><td>{{ user.rower.country }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<a href="/rowers/upgrade">Change Upgrade</a>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
<form id="payment-form" method="post" action="/rowers/upgradecheckouts"
|
||||
autocomplete="off">
|
||||
<section>
|
||||
<label for="amount">
|
||||
<div class="input-wrapper amount-wrapper">
|
||||
<input id="amount" name="amount" type="hidden" min="1" placeholder="Amount"
|
||||
value="{{ plan.price }}" readonly>
|
||||
</div>
|
||||
</label>
|
||||
<div class="bt-drop-in-wrapper">
|
||||
<div id="bt-dropin"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<input type="hidden" id="nonce" name="payment_method_nonce" />
|
||||
<input type="hidden" id="plan" name="plan" value="{{ plan.id }}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" id="submit-button"><span>Upgrade to the € {{ plan.price|currency }} plan</span></button>
|
||||
</form>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
<p>
|
||||
Your upgrade will be effective immediately. For the current billing
|
||||
cycle, you will be charged for a prorated amount. For example, when
|
||||
you upgrade from a 15€ plan to a 65€ plan (a difference of
|
||||
50€) in the 6th month of the 12 month billing cycle, you
|
||||
will be charged 35€.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% include 'braintreedropin.html' %}
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_profile.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,6 +24,8 @@ from rowers.models import checkaccessuser
|
||||
from rowers.mytypes import otwtypes
|
||||
from rowers.utils import NoTokenError
|
||||
|
||||
import rowers.payments as payments
|
||||
|
||||
def strfdelta(tdelta):
|
||||
minutes,seconds = divmod(tdelta.seconds,60)
|
||||
tenths = int(tdelta.microseconds/1e5)
|
||||
@@ -59,6 +61,13 @@ def secondstotimestring(tdelta):
|
||||
|
||||
return res
|
||||
|
||||
@register.filter
|
||||
def existing_customer(user):
|
||||
if user.is_anonymous():
|
||||
return False
|
||||
else:
|
||||
return payments.is_existing_customer(user.rower)
|
||||
|
||||
@register.filter
|
||||
def aantalcomments(workout):
|
||||
try:
|
||||
@@ -116,7 +125,16 @@ def c2userid(user):
|
||||
c2userid = c2stuff.get_userid(thetoken)
|
||||
|
||||
return c2userid
|
||||
|
||||
|
||||
@register.filter
|
||||
def currency(word):
|
||||
try:
|
||||
amount = float(word)
|
||||
except ValueError:
|
||||
return word
|
||||
|
||||
return '{amount:.2f}'.format(amount=amount)
|
||||
|
||||
@register.filter
|
||||
def rkuserid(user):
|
||||
try:
|
||||
|
||||
@@ -404,6 +404,7 @@ urlpatterns = [
|
||||
url(r'^me/edit/$',views.rower_edit_view),
|
||||
url(r'^me/edit/user/(?P<userid>\d+)$',views.rower_edit_view),
|
||||
url(r'^me/preferences/$',views.rower_prefs_view),
|
||||
url(r'^me/transactions/$',views.transactions_view),
|
||||
url(r'^me/preferences/user/(?P<userid>\d+)$',views.rower_prefs_view),
|
||||
url(r'^me/edit/(.+.*)/$',views.rower_edit_view),
|
||||
url(r'^me/c2authorize/$',views.rower_c2_authorize),
|
||||
@@ -438,6 +439,20 @@ urlpatterns = [
|
||||
url(r'^analysis/$', views.analysis_view,name='analysis'),
|
||||
url(r'^laboratory/$', views.laboratory_view,name='laboratory'),
|
||||
url(r'^promembership', TemplateView.as_view(template_name='promembership.html'),name='promembership'),
|
||||
url(r'^checkout/(?P<planid>\d+)$',views.payment_confirm_view),
|
||||
url(r'^upgradecheckout/(?P<planid>\d+)$',views.upgrade_confirm_view),
|
||||
url(r'^downgradecheckout/(?P<planid>\d+)$',views.downgrade_confirm_view),
|
||||
url(r'^billing$',views.billing_view,name='billing'),
|
||||
url(r'^upgrade$',views.upgrade_view,name='upgrade'),
|
||||
url(r'^downgrade$',views.downgrade_view,name='downgrade'),
|
||||
url(r'^paymentcompleted$',views.payment_completed_view),
|
||||
url(r'^downgradecompleted$',views.downgrade_completed_view),
|
||||
url(r'^paidplans$',views.paidplans_view,name='paidplans'),
|
||||
url(r'^me/cancelsubscriptions$',views.plan_stop_view),
|
||||
url(r'^me/cancelsubscription/(?P<id>[\w\ ]+.*)$',views.plan_tobasic_view),
|
||||
url(r'^checkouts$',views.checkouts_view,name='checkouts'),
|
||||
url(r'^upgradecheckouts$',views.upgrade_checkouts_view,name='upgrade_checkouts'),
|
||||
url(r'^downgradecheckouts$',views.downgrade_checkouts_view,name='downgrade_checkouts'),
|
||||
url(r'^planrequired',views.planrequired_view),
|
||||
url(r'^starttrial$',views.start_trial_view),
|
||||
url(r'^startplantrial$',views.start_plantrial_view),
|
||||
|
||||
@@ -426,6 +426,13 @@ class NoTokenError(Exception):
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
class ProcessorCustomerError(Exception):
|
||||
def __init__(self, value):
|
||||
self.value=value
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
# Custom exception handler, returns a 401 HTTP message
|
||||
# with exception details in the json data
|
||||
def custom_exception_handler(exc,message):
|
||||
|
||||
475
rowers/views.py
475
rowers/views.py
@@ -28,6 +28,8 @@ import isodate
|
||||
import re
|
||||
import cgi
|
||||
from icalendar import Calendar, Event
|
||||
import rowers.braintreestuff as braintreestuff
|
||||
import rowers.payments as payments
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.template.loader import render_to_string
|
||||
@@ -50,7 +52,7 @@ from rowers.forms import (
|
||||
RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm,
|
||||
FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm,
|
||||
MetricsForm,DisqualificationForm,disqualificationreasons,
|
||||
disqualifiers,SearchForm,
|
||||
disqualifiers,SearchForm,BillingForm,PlanSelectForm
|
||||
)
|
||||
from django.core.urlresolvers import reverse, reverse_lazy
|
||||
|
||||
@@ -83,7 +85,7 @@ from rowers.models import (
|
||||
createmicrofillers, createmesofillers,
|
||||
microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates,
|
||||
TrainingMesoCycleForm, TrainingMicroCycleForm,
|
||||
RaceLogo,
|
||||
RaceLogo,RowerBillingAddressForm,PaidPlan,
|
||||
)
|
||||
from rowers.models import (
|
||||
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
|
||||
@@ -139,6 +141,8 @@ from rowsandall_app.settings import (
|
||||
UNDERARMOUR_CLIENT_SECRET,UNDERARMOUR_CLIENT_KEY,
|
||||
RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET,
|
||||
TP_CLIENT_ID,TP_REDIRECT_URI,TP_CLIENT_KEY,TP_CLIENT_SECRET,
|
||||
BRAINTREE_MERCHANT_ID,BRAINTREE_PUBLIC_KEY,BRAINTREE_PRIVATE_KEY,
|
||||
PAYMENT_PROCESSING_ON
|
||||
)
|
||||
|
||||
from rowers.tasks_standalone import addcomment2
|
||||
@@ -736,8 +740,27 @@ def deactivate_user(request):
|
||||
if request.method == "POST":
|
||||
user_form = DeactivateUserForm(request.POST, instance=user)
|
||||
if user_form.is_valid():
|
||||
r = Rower.objects.get(user=user)
|
||||
if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree':
|
||||
try:
|
||||
subscriptions = braintreestuff.find_subscriptions(r)
|
||||
for subscription in subscriptions:
|
||||
success, themessages,errormessages = braintreestuff.cancel_subscription(r,id)
|
||||
for message in themessages:
|
||||
messages.info(request,message)
|
||||
except ProcessorCustomerError:
|
||||
pass
|
||||
|
||||
r.paidplan = None
|
||||
r.teamplanexpires = timezone.now()
|
||||
r.planexpires = timezone.now()
|
||||
r.clubsize = 0
|
||||
r.rowerplan = 'basic'
|
||||
r.save()
|
||||
|
||||
deactivate_user = user_form.save(commit=False)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
deactivate_user.save()
|
||||
# url = reverse(auth_views.logout_then_login)
|
||||
url = '/logout/?next=/login'
|
||||
@@ -788,7 +811,17 @@ def remove_user(request):
|
||||
name = user.first_name+' '+user.last_name
|
||||
email = user.email
|
||||
|
||||
|
||||
r = Rower.objects.get(user=user)
|
||||
if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree':
|
||||
try:
|
||||
subscriptions = braintreestuff.find_subscriptions(r)
|
||||
for subscription in subscriptions:
|
||||
success, themessages,errormessages = braintreestuff.cancel_subscription(r,id)
|
||||
for message in themessages:
|
||||
messages.info(request,message)
|
||||
except ProcessorCustomerError:
|
||||
pass
|
||||
|
||||
if cd['delete_user']:
|
||||
user.delete()
|
||||
res = myqueue(queuehigh,
|
||||
@@ -996,7 +1029,7 @@ def hasplannedsessions(user):
|
||||
|
||||
return result
|
||||
|
||||
from rowers.utils import isprorower
|
||||
from rowers.utils import isprorower,ProcessorCustomerError
|
||||
|
||||
# Check if a user is a Pro member
|
||||
def ispromember(user):
|
||||
@@ -1027,6 +1060,413 @@ def add_defaultfavorites(r):
|
||||
f.save()
|
||||
return 1
|
||||
|
||||
def paidplans_view(request):
|
||||
if not request.user.is_anonymous():
|
||||
r = getrequestrower(request)
|
||||
else:
|
||||
r = None
|
||||
|
||||
|
||||
|
||||
return render(request,
|
||||
'paidplans.html',
|
||||
{'rower':r})
|
||||
|
||||
@login_required()
|
||||
def billing_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
if payments.is_existing_customer(r):
|
||||
url = reverse(upgrade_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if request.method == 'POST':
|
||||
billingaddressform = RowerBillingAddressForm(request.POST)
|
||||
planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree')
|
||||
if billingaddressform.is_valid():
|
||||
cd = billingaddressform.cleaned_data
|
||||
for attr, value in cd.items():
|
||||
setattr(r, attr, value)
|
||||
r.save()
|
||||
|
||||
if planselectform.is_valid():
|
||||
plan = planselectform.cleaned_data['plan']
|
||||
if billingaddressform.is_valid():
|
||||
try:
|
||||
customer_id = braintreestuff.create_customer(r)
|
||||
except ProcessorCustomerError:
|
||||
messages.error(request,"Something went wrong registering you as a customer.")
|
||||
url = reverse(billing_view)
|
||||
return HttpResponseRedirect(url)
|
||||
url = reverse(payment_confirm_view,
|
||||
kwargs={
|
||||
'planid':plan.id
|
||||
})
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
else:
|
||||
billingaddressform = RowerBillingAddressForm(instance=r)
|
||||
planselectform = PlanSelectForm(paymentprocessor='braintree')
|
||||
|
||||
return render(request,
|
||||
'billing.html',
|
||||
{'rower':r,
|
||||
'billingaddressform':billingaddressform,
|
||||
'planselectform':planselectform,
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def upgrade_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
if r.subscription_id is None or r.subscription_id == '':
|
||||
url = reverse(billing_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if request.method == 'POST':
|
||||
billingaddressform = RowerBillingAddressForm(request.POST)
|
||||
planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree')
|
||||
if billingaddressform.is_valid():
|
||||
cd = billingaddressform.cleaned_data
|
||||
for attr, value in cd.items():
|
||||
setattr(r, attr, value)
|
||||
r.save()
|
||||
|
||||
if planselectform.is_valid():
|
||||
plan = planselectform.cleaned_data['plan']
|
||||
if billingaddressform.is_valid():
|
||||
url = reverse(upgrade_confirm_view,
|
||||
kwargs={
|
||||
'planid':plan.id
|
||||
})
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
else:
|
||||
billingaddressform = RowerBillingAddressForm(instance=r)
|
||||
planselectform = PlanSelectForm(paymentprocessor='braintree',
|
||||
rower=r)
|
||||
|
||||
return render(request,
|
||||
'upgrade.html',
|
||||
{'rower':r,
|
||||
'billingaddressform':billingaddressform,
|
||||
'planselectform':planselectform,
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def downgrade_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
if r.subscription_id is None or r.subscription_id == '':
|
||||
url = reverse(billing_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if request.method == 'POST':
|
||||
billingaddressform = RowerBillingAddressForm(request.POST)
|
||||
planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree')
|
||||
if billingaddressform.is_valid():
|
||||
cd = billingaddressform.cleaned_data
|
||||
for attr, value in cd.items():
|
||||
setattr(r, attr, value)
|
||||
r.save()
|
||||
|
||||
if planselectform.is_valid():
|
||||
plan = planselectform.cleaned_data['plan']
|
||||
|
||||
if plan.price > r.paidplan.price:
|
||||
nextview = upgrade_confirm_view
|
||||
elif plan.price == r.paidplan.price:
|
||||
messages.info(request,'You did not select a new plan')
|
||||
url = reverse(downgrade_view)
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
nextview = downgrade_confirm_view
|
||||
|
||||
if billingaddressform.is_valid():
|
||||
url = reverse(nextview,
|
||||
kwargs={
|
||||
'planid':plan.id
|
||||
})
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
else:
|
||||
billingaddressform = RowerBillingAddressForm(instance=r)
|
||||
planselectform = PlanSelectForm(paymentprocessor='braintree',
|
||||
rower=r,includeall=True, initial={'plan':r.paidplan})
|
||||
|
||||
return render(request,
|
||||
'downgrade.html',
|
||||
{'rower':r,
|
||||
'billingaddressform':billingaddressform,
|
||||
'planselectform':planselectform,
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def plan_stop_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
subscriptions = []
|
||||
|
||||
if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree':
|
||||
try:
|
||||
subscriptions = braintreestuff.find_subscriptions(r)
|
||||
except ProcessorCustomerError:
|
||||
r.paymentprocessor = None
|
||||
r.save()
|
||||
|
||||
|
||||
|
||||
return render(request,
|
||||
'subscriptions_cancel.html',
|
||||
{'rower':r,
|
||||
'subscriptions':subscriptions
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def plan_tobasic_view(request,id=0):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
if r.paidplan.paymentprocessor == 'braintree':
|
||||
success, themessages,errormessages = braintreestuff.cancel_subscription(r,id)
|
||||
for message in themessages:
|
||||
messages.info(request,message)
|
||||
|
||||
for message in errormessages:
|
||||
messages.error(request,message)
|
||||
|
||||
url = reverse(plan_stop_view)
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
|
||||
@login_required()
|
||||
def upgrade_confirm_view(request,planid = 0):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
plan = PaidPlan.objects.get(id=planid)
|
||||
except PaidPlan.DoesNotExist:
|
||||
messages.error(request,"Something went wrong. Please try again.")
|
||||
url = reverse(billing_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
client_token = braintreestuff.get_client_token(r)
|
||||
|
||||
return render(request,
|
||||
"upgradeconfirm.html",
|
||||
{
|
||||
'plan':plan,
|
||||
'client_token':client_token,
|
||||
'rower':r,
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def downgrade_confirm_view(request,planid = 0):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
plan = PaidPlan.objects.get(id=planid)
|
||||
except PaidPlan.DoesNotExist:
|
||||
messages.error(request,"Something went wrong. Please try again.")
|
||||
url = reverse(billing_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
client_token = braintreestuff.get_client_token(r)
|
||||
|
||||
return render(request,
|
||||
"downgradeconfirm.html",
|
||||
{
|
||||
'plan':plan,
|
||||
'client_token':client_token,
|
||||
'rower':r,
|
||||
})
|
||||
|
||||
|
||||
@login_required()
|
||||
def payment_confirm_view(request,planid = 0):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
plan = PaidPlan.objects.get(id=planid)
|
||||
except PaidPlan.DoesNotExist:
|
||||
messages.error(request,"Something went wrong. Please try again.")
|
||||
url = reverse(billing_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
client_token = braintreestuff.get_client_token(r)
|
||||
|
||||
return render(request,
|
||||
"paymentconfirm.html",
|
||||
{
|
||||
'plan':plan,
|
||||
'client_token':client_token,
|
||||
'rower':r,
|
||||
})
|
||||
|
||||
|
||||
@login_required()
|
||||
def checkouts_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
if request.method != 'POST':
|
||||
url = reverse(paidplans_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
form = BillingForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
success = braintreestuff.create_subscription(r,data)
|
||||
if success:
|
||||
messages.info(request,"Your payment has succeeded and your plan has been updated")
|
||||
url = reverse(payment_completed_view)
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
messages.error(request,"There was a problem with your payment")
|
||||
url = reverse(billing_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
else:
|
||||
messages.error(request,"There was an error in the payment form")
|
||||
url = reverse(billing_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
url = reverse(paidplans_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@login_required()
|
||||
def upgrade_checkouts_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
if request.method != 'POST':
|
||||
url = reverse(paidplans_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
form = BillingForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
success = braintreestuff.update_subscription(r,data)
|
||||
if success:
|
||||
messages.info(request,"Your payment has succeeded and your plan has been updated")
|
||||
url = reverse(payment_completed_view)
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
messages.error(request,"There was a problem with your payment")
|
||||
url = reverse(upgrade_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
else:
|
||||
messages.error(request,"There was an error in the payment form")
|
||||
url = reverse(upgrade_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
url = reverse(paidplans_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@login_required()
|
||||
def downgrade_checkouts_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
if request.method != 'POST':
|
||||
url = reverse(paidplans_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
form = BillingForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
success = braintreestuff.update_subscription(r,data,method='down')
|
||||
if success:
|
||||
messages.info(request,"Your plan has been updated")
|
||||
url = reverse(downgrade_completed_view)
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
messages.error(request,"There was a problem with your transaction")
|
||||
url = reverse(upgrade_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
else:
|
||||
messages.error(request,"There was an error in the payment form")
|
||||
url = reverse(upgrade_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
url = reverse(paidplans_view)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
@login_required()
|
||||
def payment_completed_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
return render(request,
|
||||
"payment_completed.html",
|
||||
{
|
||||
'rower':r
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def downgrade_completed_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = getrequestrower(request)
|
||||
|
||||
return render(request,
|
||||
"downgrade_completed.html",
|
||||
{
|
||||
'rower':r
|
||||
})
|
||||
|
||||
# User registration
|
||||
def rower_register_view(request):
|
||||
@@ -1477,6 +1917,33 @@ def plannedsessions_icsemail_view(request,userid=0):
|
||||
|
||||
return response
|
||||
|
||||
@login_required()
|
||||
def transactions_view(request):
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied("Not Allowed")
|
||||
|
||||
if request.method == 'POST':
|
||||
dateform = DateRangeForm(request.POST)
|
||||
if dateform.is_valid():
|
||||
startdate = dateform.cleaned_data['startdate']
|
||||
enddate = dateform.cleaned_data['enddate']
|
||||
|
||||
df = braintreestuff.get_transactions(startdate,enddate)
|
||||
filename="transactions_{s}_{e}.csv".format(s = startdate, e = enddate)
|
||||
response = HttpResponse(df.to_csv())
|
||||
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
|
||||
response['Content-Type'] = 'application/octet-stream'
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
dateform = DateRangeForm()
|
||||
|
||||
return render(request,
|
||||
'transactions.html',
|
||||
{
|
||||
'dateform':dateform
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def course_kmlemail_view(request,id=0):
|
||||
|
||||
Reference in New Issue
Block a user