Private
Public Access
1
0

adding some model changes around paid plans

This commit is contained in:
Sander Roosendaal
2018-12-18 19:22:27 +01:00
parent 6d7d9f5792
commit 75261118c2
9 changed files with 401 additions and 15 deletions

View File

@@ -7,10 +7,12 @@ from .models import (
Team,TeamInvite,TeamRequest, Team,TeamInvite,TeamRequest,
WorkoutComment,C2WorldClassAgePerformance,PlannedSession, WorkoutComment,C2WorldClassAgePerformance,PlannedSession,
GeoCourse,GeoPolygon,GeoPoint,VirtualRace,VirtualRaceResult, GeoCourse,GeoPolygon,GeoPoint,VirtualRace,VirtualRaceResult,
PaidPlan
) )
# Register your models here so you can use them in the Admin module # Register your models here so you can use them in the Admin module
# Rower details directly under the User # Rower details directly under the User
class RowerInline(admin.StackedInline): class RowerInline(admin.StackedInline):
model = Rower model = Rower
@@ -121,6 +123,9 @@ class VirtualRaceResultAdmin(admin.ModelAdmin):
list_display = ('race','userid','username','boattype','age','weightcategory') list_display = ('race','userid','username','boattype','age','weightcategory')
search_fields = ['race__name','username'] search_fields = ['race__name','username']
class PaidPlanAdmin(admin.ModelAdmin):
list_display = ('name','shortname','price','paymenttype')
admin.site.unregister(User) admin.site.unregister(User)
admin.site.register(User,UserAdmin) admin.site.register(User,UserAdmin)
admin.site.register(Workout,WorkoutAdmin) admin.site.register(Workout,WorkoutAdmin)
@@ -137,3 +142,4 @@ admin.site.register(PlannedSession,PlannedSessionAdmin)
admin.site.register(GeoCourse, GeoCourseAdmin) admin.site.register(GeoCourse, GeoCourseAdmin)
admin.site.register(VirtualRace, VirtualRaceAdmin) admin.site.register(VirtualRace, VirtualRaceAdmin)
admin.site.register(VirtualRaceResult, VirtualRaceResultAdmin) admin.site.register(VirtualRaceResult, VirtualRaceResultAdmin)
admin.site.register(PaidPlan,PaidPlanAdmin)

53
rowers/braintreestuff.py Normal file
View File

@@ -0,0 +1,53 @@
import braintree
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):
if not rower.customer_id:
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.save()
else:
return rower.customer_id
def get_client_token(rower):
client_token = gateway.client_token.generate({
"customer_id":rower.customer_id,
})
return client_token
def get_plans_costs():
plans = gateway.plan.all()
localplans = PaidPlan.object.all()
for plan in localplans:
for btplan in btplans:
if int(btplan.id) == plan.braintree_id:
plan.price = float(x)
plan.save()
return plans

View File

@@ -18,8 +18,8 @@ from django.forms import formset_factory
from utils import landingpages from utils import landingpages
from metrics import axes from metrics import axes
# Braintree form # BillingForm form
class BrainTreeForm(forms.Form): class BillingForm(forms.Form):
amount = forms.FloatField(required=True) amount = forms.FloatField(required=True)
payment_method_nonce = forms.CharField(max_length=255) payment_method_nonce = forms.CharField(max_length=255)

View File

@@ -537,6 +537,27 @@ weightcategories = (
) )
# Plan
plans = (
('basic','basic'),
('pro','pro'),
('plan','plan'),
('coach','coach')
)
paymenttypes = (
('single','single'),
('recurring','recurring')
)
class PaidPlan(models.Model):
shortname = models.CharField(max_length=50,choices=plans)
name = models.CharField(max_length=200)
braintree_id = models.IntegerField(blank=True,null=True,default=None)
price = models.FloatField(blank=True,null=True,default=None)
paymenttype = models.CharField(max_length=50,choices=paymenttypes)
clubsize = models.IntegerField(default=0)
# Extension of User with rowing specific data # Extension of User with rowing specific data
class Rower(models.Model): class Rower(models.Model):
adaptivetypes = mytypes.adaptivetypes adaptivetypes = mytypes.adaptivetypes
@@ -694,13 +715,6 @@ class Rower(models.Model):
blank=True,null=True) blank=True,null=True)
runkeeper_auto_export = models.BooleanField(default=False) runkeeper_auto_export = models.BooleanField(default=False)
# Plan
plans = (
('basic','basic'),
('pro','pro'),
('plan','plan'),
('coach','coach')
)
privacychoices = ( privacychoices = (
('visible','Visible'), ('visible','Visible'),
@@ -720,10 +734,7 @@ class Rower(models.Model):
paymenttype = models.CharField( paymenttype = models.CharField(
default='single',max_length=30, default='single',max_length=30,
verbose_name='Payment Type', verbose_name='Payment Type',
choices=( choices=paymenttypes,
('single','single'),
('recurring','recurring')
)
) )
planexpires = models.DateField(default=timezone.now) planexpires = models.DateField(default=timezone.now)

View File

@@ -0,0 +1,269 @@
{% 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%">
<a href="/rowers/upgrade">UPGRADE NOW</a>
</button>
</td>
{% elif rower and rower.rowerplan == 'pro' %}
<td>&nbsp;</td>
<td colspan="2">
<button style="width:100%">
<a href="/rowers/upgrade">UPGRADE NOW</a>
</button>
</td>
{% elif rower and rower.rowerplan == 'plan' %}
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>
<button style="width:100%">
<a href="/rowers/upgrade">UPGRADE NOW</a>
</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

@@ -438,6 +438,7 @@ urlpatterns = [
url(r'^analysis/$', views.analysis_view,name='analysis'), url(r'^analysis/$', views.analysis_view,name='analysis'),
url(r'^laboratory/$', views.laboratory_view,name='laboratory'), url(r'^laboratory/$', views.laboratory_view,name='laboratory'),
url(r'^promembership', TemplateView.as_view(template_name='promembership.html'),name='promembership'), url(r'^promembership', TemplateView.as_view(template_name='promembership.html'),name='promembership'),
url(r'^paidplans',views.paidplans_view,name='paidplans'),
url(r'^checkouts',views.checkouts_view,name='checkouts'), url(r'^checkouts',views.checkouts_view,name='checkouts'),
url(r'^payments',views.payments_view,name='payments'), url(r'^payments',views.payments_view,name='payments'),
url(r'^planrequired',views.planrequired_view), url(r'^planrequired',views.planrequired_view),

View File

@@ -426,6 +426,13 @@ class NoTokenError(Exception):
def __str__(self): def __str__(self):
return repr(self.value) 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 # Custom exception handler, returns a 401 HTTP message
# with exception details in the json data # with exception details in the json data
def custom_exception_handler(exc,message): def custom_exception_handler(exc,message):

View File

@@ -51,7 +51,7 @@ from rowers.forms import (
RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm, RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm,
FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm, FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm,
MetricsForm,DisqualificationForm,disqualificationreasons, MetricsForm,DisqualificationForm,disqualificationreasons,
disqualifiers,SearchForm,BrainTreeForm disqualifiers,SearchForm,BillingForm
) )
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
@@ -1029,6 +1029,17 @@ def add_defaultfavorites(r):
f.save() f.save()
return 1 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})
# Experimental - Payments # Experimental - Payments
@login_required() @login_required()
def payments_view(request): def payments_view(request):
@@ -1091,7 +1102,7 @@ def checkouts_view(request):
) )
) )
form = BrainTreeForm(request.POST) form = BillingForm(request.POST)
if form.is_valid(): if form.is_valid():
nonce_from_the_client = form.cleaned_data['payment_method_nonce'] nonce_from_the_client = form.cleaned_data['payment_method_nonce']
amount = form.cleaned_data['amount'] amount = form.cleaned_data['amount']

View File

@@ -170,6 +170,15 @@ cox {
text-align: center; text-align: center;
} }
.plantable {
border-collapse: collapse;
}
.plantable > td {
text-align: center;
}
th.rotate { th.rotate {
/* Something you can count on */ /* Something you can count on */
height: 78px; height: 78px;
@@ -220,6 +229,7 @@ th.rotate > div > span {
background-color: #fee; background-color: #fee;
} }
.successmessage { .successmessage {
border: 1px solid #000; border: 1px solid #000;
background-color: #8f8; background-color: #8f8;
@@ -907,3 +917,21 @@ a.wh:hover {
hyphens: auto; hyphens: auto;
} }
.upgradebutton {
background-color: #4CAF50;
border: none;
color: white;
padding: 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
}
.buttonr2 {border-radius: 2px;}
.buttonr4 {border-radius: 4px;}
.buttonr8 {border-radius: 8px;}
.buttonr12 {border-radius: 12px;}
.buttonround {border-radius: 50%;}