Private
Public Access
1
0

Merge branch 'feature/braintree' into develop

This commit is contained in:
Sander Roosendaal
2018-12-21 10:46:01 +01:00
36 changed files with 2789 additions and 43 deletions

View File

@@ -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
View 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

View File

@@ -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())

View File

@@ -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
View 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

View File

@@ -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(

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

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

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

View 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&euro; plan to a 15&euro; plan
(50&euro; difference), in the 6th month of the 12 month
billing cycle, you will have a credit of 25&euro; 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 %}

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

View 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>&euro; {{ 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 &euro; {{ 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 %}

View File

@@ -27,8 +27,16 @@
<i class="fas fa-tachometer-alt-slow fa-fw"></i>&nbsp;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>&nbsp;Transactions
</a>
</li>
{% endif %}
</ul><!-- cd-accordion-menu -->
{% if user.is_authenticated and user|is_manager %}
<p>&nbsp;</p>
{% if user|team_members %}

View 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>&nbsp;</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>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Manual Import, Export, Synchronization and download of all your data</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Automatic Synchronization with other fitness sites</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Heart rate and power zones</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Ranking Pieces, Stroke Analysis</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Advanced Analysis (Critical Power, Stats, Box Chart, Trend Flex)</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Compare Workouts</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Empower Stroke Profile</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Sensor Fusion, Split Workout, In-stroke metrics</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Create Training plans, tests and challenges for yourself. Track your performance
against plan.</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Create Training plans, tests and challenges for your athletes. Track their performance
against plan. </td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Create and manage teams.</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Manage your athlete's workouts</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Pricing</td>
<td>FREE</td>
<td nowrap="nowrap">From 15&euro;/year</td>
<td nowrap="nowrap">From 65&euro;/year</td>
<td nowrap="nowrap">From 90&euro;/year</td>
</tr>
{% if rower %}
<tr>
<td>Your current plan</td>
<td>
{% if rower.rowerplan == 'basic' %}
<h3>BASIC</h3>
{% else %}
&nbsp;
{% endif %}
</td>
<td>
{% if rower.rowerplan == 'pro' %}
PRO
{% else %}
&nbsp;
{% endif %}
</td>
<td>
{% if rower.rowerplan == 'plan' %}
SELF-COACH
{% else %}
&nbsp;
{% endif %}
</td>
<td>
{% if rower.rowerplan == 'coach' %}
COACH
{% else %}
&nbsp;
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<td>
Available trials
</td>
<td>
&nbsp;
</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 %}
&nbsp;
{% 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 %}
&nbsp;
{% endif %}
</td>
<td>
&nbsp;
</td>
</tr>
<tr>
<td>
Available upgrades
</td>
<td>
&nbsp;
</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>&nbsp;</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>&nbsp;</td>
<td>&nbsp;</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>&nbsp;</td>
<td>&nbsp;</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>
&nbsp;
</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 %}

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

View 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>&euro; {{ 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 &euro; {{ plan.price|currency }}</span></button>
</form>
</li>
</ul>
{% include 'braintreedropin.html' %}
{% endblock %}
{% block sidebar %}
{% include 'menu_profile.html' %}
{% endblock %}

View 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 &euro; {{ 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 %}

View 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>&nbsp;</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>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Manual Import, Export, Synchronization and download of all your data</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Automatic Synchronization with other fitness sites</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Heart rate and power zones</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Ranking Pieces, Stroke Analysis</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Advanced Analysis (Critical Power, Stats, Box Chart, Trend Flex)</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Compare Workouts</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Empower Stroke Profile</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Sensor Fusion, Split Workout, In-stroke metrics</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Create Training plans, tests and challenges for yourself. Track your performance
against plan.</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Create Training plans, tests and challenges for your athletes. Track their performance
against plan. </td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Create and manage teams.</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&#10004;</td>
</tr>
<tr>
<td>Manage your athlete's workouts</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&#10004;</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 %}

View File

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

View File

@@ -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 %}
&nbsp;
{% endif %}
@@ -34,29 +34,49 @@
{{ userform.as_table }}
{{ accountform.as_table }}
<tr>
<th>Plan</th><td>{{ rower.rowerplan }}</td>
<th>&nbsp;</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 %}
&nbsp;
<p>
&nbsp;
</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 %}

View 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 &euro; {{ 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 %}

View 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 &euro;{{ price }} per year.
The next charge is due on {{ end_of_billing_period }}. on that date.
</p>
{% else %}
<p>
The subscription cost is &euro;{{ price }}. The subscription ends on {{ end_of_billing_period }}
</p>
{% endif %}
<p>
Amount charged: &euro;{{ amount }}
</p>
<p>
Best Regards, the Rowsandall Team
</p>
{% endblock %}

View 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 &euro;{{ 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 &euro;{{ 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 %}

View 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 &euro;{{ price }} per year.
The next charge is due on {{ end_of_billing_period }}. on that date.
</p>
{% else %}
<p>
The subscription cost is &euro;{{ price }}. The subscription ends on {{ end_of_billing_period }}
</p>
{% endif %}
<p>
Best Regards, the Rowsandall Team
</p>
{% endblock %}

View 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 &euro; {{ amount }} for
your updated Rowsandall subscription.
You are now on the Rowsandall paid plan "{{ planname }}".
</p>
{% if recurring %}
<p>
The subscription cost is &euro;{{ 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 &euro;{{ price }}. You have paid &euro;{{ 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 %}

View 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 &euro;{{ price }} per year.
The next charge is due on {{ end_of_billing_period }}. on that date.
</p>
{% else %}
<p>
The subscription cost is &euro;{{ price }}. The subscription ends on {{ end_of_billing_period }}
</p>
{% endif %}
<p>
Amount charged: &euro;{{ amount }}
</p>
<p>
Best Regards, the Rowsandall Team
</p>
{% endblock %}

View 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>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for subscription in subscriptions %}
<tr>
<td>
{{ subscription|lookup:"plan" }}
</td>
<td>
{{ subscription|lookup:"end_date" }}
</td>
<td>
{{ subscription|lookup:"price" }} &euro;
</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 %}

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

View 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&euro; plan to a 65&euro; plan (a difference of
50&euro;) in the 6th month of the 12 month billing cycle, you
will be charged 35&euro;.
</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 %}

View 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>&euro; {{ 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 &euro; {{ 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&euro; plan to a 65&euro; plan (a difference of
50&euro;) in the 6th month of the 12 month billing cycle, you
will be charged 35&euro;.
</p>
</li>
</ul>
{% include 'braintreedropin.html' %}
{% endblock %}
{% block sidebar %}
{% include 'menu_profile.html' %}
{% endblock %}

View File

@@ -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:

View File

@@ -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),

View File

@@ -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):

View File

@@ -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):