From 75261118c2a7aaf7ed070d5b300695adaf84fbec Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 18 Dec 2018 19:22:27 +0100 Subject: [PATCH] adding some model changes around paid plans --- rowers/admin.py | 6 + rowers/braintreestuff.py | 53 +++++++ rowers/forms.py | 4 +- rowers/models.py | 33 ++-- rowers/templates/paidplans.html | 269 ++++++++++++++++++++++++++++++++ rowers/urls.py | 1 + rowers/utils.py | 7 + rowers/views.py | 15 +- static/css/rowsandall.css | 28 ++++ 9 files changed, 401 insertions(+), 15 deletions(-) create mode 100644 rowers/braintreestuff.py create mode 100644 rowers/templates/paidplans.html diff --git a/rowers/admin.py b/rowers/admin.py index e7a22b76..5e77e04f 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -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 @@ -120,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') admin.site.unregister(User) admin.site.register(User,UserAdmin) @@ -137,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) diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py new file mode 100644 index 00000000..863b512d --- /dev/null +++ b/rowers/braintreestuff.py @@ -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 diff --git a/rowers/forms.py b/rowers/forms.py index c86e6cb8..effeadf1 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -18,8 +18,8 @@ from django.forms import formset_factory from utils import landingpages from metrics import axes -# Braintree form -class BrainTreeForm(forms.Form): +# BillingForm form +class BillingForm(forms.Form): amount = forms.FloatField(required=True) payment_method_nonce = forms.CharField(max_length=255) diff --git a/rowers/models.py b/rowers/models.py index ad3d68b5..04de22f5 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -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 class Rower(models.Model): adaptivetypes = mytypes.adaptivetypes @@ -694,13 +715,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'), @@ -720,10 +734,7 @@ class Rower(models.Model): paymenttype = models.CharField( default='single',max_length=30, verbose_name='Payment Type', - choices=( - ('single','single'), - ('recurring','recurring') - ) + choices=paymenttypes, ) planexpires = models.DateField(default=timezone.now) diff --git a/rowers/templates/paidplans.html b/rowers/templates/paidplans.html new file mode 100644 index 00000000..0cd4a34f --- /dev/null +++ b/rowers/templates/paidplans.html @@ -0,0 +1,269 @@ +{% extends "newbase.html" %} +{% block title %}Rowsandall Paid Membership{% endblock title %} +{% load rowerfilters %} +{% block main %} + +

Paid Membership Plans

+ +
    +
  • +

    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. +

    + +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if rower %} + + + + + + + + {% endif %} + + + + + + + + + + + {% if user.is_anonymous %} + + {% elif rower and rower.rowerplan == 'basic' %} + + {% elif rower and rower.rowerplan == 'pro' %} + + + {% elif rower and rower.rowerplan == 'plan' %} + + + + {% else %} + + {% endif %} + + +
     BASICPROSELF-COACHCOACH
    Basic rowing metrics (spm, time, distance, heart rate, power)✔✔✔✔
    Manual Import, Export, Synchronization and download of all your data✔✔✔✔
    Automatic Synchronization with other fitness sites ✔✔✔
    Heart rate and power zones✔✔✔✔
    Ranking Pieces, Stroke Analysis✔✔✔✔
    Advanced Analysis (Critical Power, Stats, Box Chart, Trend Flex) ✔✔✔
    Compare Workouts ✔✔✔
    Empower Stroke Profile ✔✔✔
    Sensor Fusion, Split Workout, In-stroke metrics ✔✔✔
    Create Training plans, tests and challenges for yourself. Track your performance + against plan.  ✔✔
    Create Training plans, tests and challenges for your athletes. Track their performance + against plan.    ✔
    Create and manage teams.   ✔
    Manage your athlete's workouts   ✔
    PricingFREEFrom 15€/yearFrom 65€/yearFrom 90€/year
    Your current plan + {% if rower.rowerplan == 'basic' %} +

    BASIC

    + {% else %} +   + {% endif %} +
    + {% if rower.rowerplan == 'pro' %} + PRO + {% else %} +   + {% endif %} + + {% if rower.rowerplan == 'plan' %} + SELF-COACH + {% else %} +   + {% endif %} + + {% if rower.rowerplan == 'coach' %} + COACH + {% else %} +   + {% endif %} +
    + Available trials + +   + + {% if user.is_anonymous %} + + {% elif rower and rower.rowerplan == 'basic' and rower.protrialexpires|date_dif == 1 %} + + {% else %} +   + {% endif %} + + {% if user.is_anonymous %} + + {% elif rower and rower.rowerplan == 'basic' and rower.plantrialexpires|date_dif == 1 %} + + {% else %} +   + {% endif %} + +   +
    + Available upgrades + +   + + + + +   + +    + + +   +
    +

    + +

    Coach and Self-Coach Membership

    + +

    The Coach plan functionality listed is available to the coach only. Individual athletes + can purchase upgrades to "Pro" and "Self-Coach" plans. +

    + +

    Rowsandall.com's Training Planning functionality + is part of the paid "Self-Coach" and "Coach" plans.

    + +

    On the "Self-Coach" plan, you can plan your own sessions.

    + +

    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. +

    + +

    If you would like to find a coach who helps you plan your training + through rowsandall.com, contact me throught the contact form.

    + +
  • +
+ +{% endblock %} + +{% block sidebar %} +{% include 'menu_help.html' %} +{% endblock %} + diff --git a/rowers/urls.py b/rowers/urls.py index 87312e90..a74d085d 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -438,6 +438,7 @@ 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'^paidplans',views.paidplans_view,name='paidplans'), url(r'^checkouts',views.checkouts_view,name='checkouts'), url(r'^payments',views.payments_view,name='payments'), url(r'^planrequired',views.planrequired_view), diff --git a/rowers/utils.py b/rowers/utils.py index cb9fbaae..eb5627b3 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -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): diff --git a/rowers/views.py b/rowers/views.py index d0110059..069773d8 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -51,7 +51,7 @@ from rowers.forms import ( RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm, FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm, MetricsForm,DisqualificationForm,disqualificationreasons, - disqualifiers,SearchForm,BrainTreeForm + disqualifiers,SearchForm,BillingForm ) from django.core.urlresolvers import reverse, reverse_lazy @@ -1029,6 +1029,17 @@ 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}) + # Experimental - Payments @login_required() def payments_view(request): @@ -1091,7 +1102,7 @@ def checkouts_view(request): ) ) - form = BrainTreeForm(request.POST) + form = BillingForm(request.POST) if form.is_valid(): nonce_from_the_client = form.cleaned_data['payment_method_nonce'] amount = form.cleaned_data['amount'] diff --git a/static/css/rowsandall.css b/static/css/rowsandall.css index 95fd1d8b..86682c66 100644 --- a/static/css/rowsandall.css +++ b/static/css/rowsandall.css @@ -170,6 +170,15 @@ cox { text-align: center; } +.plantable { + border-collapse: collapse; +} + +.plantable > td { + text-align: center; +} + + th.rotate { /* Something you can count on */ height: 78px; @@ -220,6 +229,7 @@ th.rotate > div > span { background-color: #fee; } + .successmessage { border: 1px solid #000; background-color: #8f8; @@ -907,3 +917,21 @@ a.wh:hover { 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%;}