works but cannot change address
This commit is contained in:
@@ -183,7 +183,7 @@ def make_payment(rower,data):
|
||||
return False,0
|
||||
|
||||
amount = data['amount']
|
||||
amount = '{amount:.f2}'.format(amount=amount)
|
||||
amount = '{amount}'.format(amount=amount)
|
||||
|
||||
result = gateway.transaction.sale({
|
||||
"amount": amount,
|
||||
@@ -200,13 +200,18 @@ def make_payment(rower,data):
|
||||
l = rower.user.last_name,
|
||||
)
|
||||
|
||||
fakturoid_contact_id = fakturoid.get_contacts(rower)
|
||||
if not fakturoid_contact_id:
|
||||
fakturoid_contact_id = fakturoid.create_contact(rower)
|
||||
id = fakturoid.create_invoice(rower,amount,transaction.id,dosend=True,contact_id=fakturoid_contact_id,
|
||||
name='Rowsandall Purchase')
|
||||
|
||||
job = myqueue(queuehigh,handle_send_email_transaction,
|
||||
name, rower.user.email, amount)
|
||||
|
||||
return amount,''
|
||||
return amount,True
|
||||
else:
|
||||
return 0,''
|
||||
return 0,False
|
||||
|
||||
def update_subscription(rower,data,method='up'):
|
||||
planid = data['plan']
|
||||
|
||||
@@ -83,11 +83,14 @@ def create_contact(rower):
|
||||
|
||||
# this should be triggered by a Braintree webhook
|
||||
def create_invoice(rower,amount,braintreeid,dosend=True,
|
||||
contact_id=None):
|
||||
contact_id=None,name=None):
|
||||
|
||||
if not contact_id:
|
||||
contact_id = get_contacts(rower)
|
||||
|
||||
if not name:
|
||||
name = 'Rowsandall Subscription'
|
||||
|
||||
|
||||
with open('braintreewebhooks.log','a') as f:
|
||||
f.write('Creating invoice for contact iD '+str(contact_id)+'\n')
|
||||
@@ -104,7 +107,7 @@ def create_invoice(rower,amount,braintreeid,dosend=True,
|
||||
'paid_amount': str(amount),
|
||||
'status': 'paid',
|
||||
'lines': [{
|
||||
'name': 'Rowsandall Subscription',
|
||||
'name': name,
|
||||
'quantity': '1',
|
||||
'unit_price': str(amount),
|
||||
'vat_rate': 0,
|
||||
|
||||
@@ -101,6 +101,19 @@ class BillingForm(forms.Form):
|
||||
paymenttype = forms.CharField(max_length=255,required=True)
|
||||
tac= forms.BooleanField(required=True,initial=False)
|
||||
|
||||
# TrainingPlanBillingForm form
|
||||
class TrainingPlanBillingForm(forms.Form):
|
||||
amount = FlexibleDecimalField(required=True,decimal_places=2,
|
||||
max_digits=8)
|
||||
plan = forms.IntegerField(widget=forms.HiddenInput())
|
||||
payment_method_nonce = forms.CharField(max_length=255,required=True)
|
||||
paymenttype = forms.CharField(max_length=255,required=True)
|
||||
enddate = forms.DateField(widget=forms.HiddenInput)
|
||||
name = forms.CharField(max_length=255,required=False)
|
||||
notes = forms.CharField(max_length=255,required=True)
|
||||
status = forms.CharField(max_length=255,required=True)
|
||||
tac= forms.BooleanField(required=True,initial=False)
|
||||
|
||||
|
||||
# login form
|
||||
class LoginForm(forms.Form):
|
||||
|
||||
@@ -1612,7 +1612,7 @@ class TrainingPlan(models.Model):
|
||||
rowers = models.ManyToManyField(Rower,related_name='planathletes',
|
||||
verbose_name='Athletes')
|
||||
manager = models.ForeignKey(Rower,related_name='planmanager',null=True,on_delete=models.SET_NULL)
|
||||
name = models.CharField(max_length=150,blank=True)
|
||||
name = models.CharField(max_length=150,blank=True, verbose_name="Plan Name")
|
||||
status = models.BooleanField(default=True,verbose_name='Active')
|
||||
target = models.ForeignKey(TrainingTarget,blank=True,null=True,on_delete=models.SET_NULL)
|
||||
startdate = models.DateField(default=current_day)
|
||||
|
||||
43
rowers/templates/buy_trainingplan.html
Normal file
43
rowers/templates/buy_trainingplan.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Purchase Training Plan{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<h1>Purchase Training Plan</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>
|
||||
{{ form.as_table }}
|
||||
{{ billingaddressform.as_table }}
|
||||
</table>
|
||||
</li>
|
||||
<li class="grid_3">
|
||||
<h2>Your Purchase</h2>
|
||||
|
||||
<p>Plan: {{ plan.name }}</p>
|
||||
<p>Price: {{ plan.price }}€</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Your purchase will be effective immediately. You will be charged
|
||||
for the price of the plan.
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_3">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="Proceed">
|
||||
You will be able to review your order before finalizing your purchase.
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_payments.html' %}
|
||||
{% endblock %}
|
||||
114
rowers/templates/confirm_trainingplan.html
Normal file
114
rowers/templates/confirm_trainingplan.html
Normal file
@@ -0,0 +1,114 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% block title %}Rowsandall Purchase Training Plan{% endblock title %}
|
||||
{% load rowerfilters %}
|
||||
{% block main %}
|
||||
|
||||
<div id="paymenterror">
|
||||
</div>
|
||||
|
||||
<h1>Confirm Your Payment</h1>
|
||||
|
||||
<h2>Order Overview</h2>
|
||||
|
||||
<p>
|
||||
Please refer to our <a href="/rowers/legal/">terms and conditions</a> for our
|
||||
payments and refunds policy. Accepted payment methods are the payment methods offered
|
||||
by
|
||||
<a href="https://www.braintreegateway.com/merchants/jytq7yxsm66qqdzb/verified">Braintree</a>
|
||||
through us. If you have any questions about our payments and refunds policy, please contact
|
||||
us by email at support@rowsandall.com.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Payments will be processed by Braintree (A PayPal service):
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://www.braintreegateway.com/merchants/{{ BRAINTREE_MERCHANT_ID }}/verified" target="_blank">
|
||||
<img src="https://s3.amazonaws.com/braintree-badges/braintree-badge-light.png" width="164px" height ="44px" border="0"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<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>Total</th><td>€ {{ plan.price|currency }}
|
||||
</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_4">
|
||||
<form id="payment-form" method="post" action="/rowers/purchasecheckouts/"
|
||||
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>
|
||||
<div id="paymenterror2"> </div>
|
||||
<input type="hidden" id="nonce" name="payment_method_nonce" />
|
||||
<input type="hidden" id="paymenttype" name="paymenttype" />
|
||||
<input type="hidden" id="plan" name="plan" value="{{ plan.id }}">
|
||||
<input type="hidden" id="status" name="status" value="{{ status }}">
|
||||
<input type="hidden" id="notes" name="notes" value="{{ notes }}">
|
||||
<input type="hidden" id="name" name="name" value="{{ name }}">
|
||||
<input type="hidden" id="enddate" name="enddate" value="{{ enddate }}">
|
||||
<p>
|
||||
<input id="tac" type="checkbox" name="tac" value="tac">I have taken note of the
|
||||
<a href="/rowers/legal/#refunds" target="_blank">Refund and Cancellation</a>
|
||||
Policy and agree with the <a href="/rowers/legal/" target="_blank">Terms of Service</a>.
|
||||
</p>
|
||||
{% csrf_token %}
|
||||
<button type="submit" id="submit-button"><span>Purchase for € {{ plan.price|currency }}</span></button>
|
||||
</form>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
<p>
|
||||
After you hit the Purchase button, the transaction will be launched.
|
||||
Please wait until the transaction completes. Do not click the
|
||||
button twice. Do not close your browser window.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% include 'braintreedropin.html' %}
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_payments.html' %}
|
||||
{% endblock %}
|
||||
@@ -27,6 +27,11 @@
|
||||
<p>What the plan will achieve: {{ plan.target }}</p>
|
||||
{% endif %}
|
||||
<p>Weekly volume: {{ plan.hoursperweek }} hours per week over {{ plan.sessionsperweek }} sessions.</p>
|
||||
{% if plan.price == 0 %}
|
||||
<p>Price: Free</p>
|
||||
{% else %}
|
||||
<p>Price: {{ plan.price }}€</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
@@ -37,12 +42,20 @@
|
||||
You can select the end date manually or use the training target (if you have any), and the plan will start at
|
||||
the date it needs to complete in time.
|
||||
</p>
|
||||
{% if plan.price == 0 %}
|
||||
<form enctype="multipart/form-data" action="" method="post">
|
||||
{% else %}
|
||||
<form enctype="multipart/form-data" action="/rowers/buyplan/{{ plan.id }}/" method="post">
|
||||
{% endif %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
{% csrf_token %}
|
||||
{% if plan.price == 0 %}
|
||||
<p><input class="button" type="submit" value="Create Plan and Add Sessions"></p>
|
||||
{% else %}
|
||||
<p><input class="button" type="submit" action="/rowers/buyplan/{{ plan.id }}/" value="BUY NOW and Add Sessions"></p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
<p>Goal: {{ plan.goal }}</p>
|
||||
{% endif %}
|
||||
<p>{{ plan.hoursperweek }} hours per week over {{ plan.sessionsperweek }} sessions</p>
|
||||
{% if plan.price == 0 %}
|
||||
<p>Price: Free</p>
|
||||
{% else %}
|
||||
<p>Price: {{ plan.price }}€</p>
|
||||
{% endif %}
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -719,7 +719,8 @@ urlpatterns = [
|
||||
re_path(r'^me/cancelsubscription/(?P<id>[\w\ ]+.*)/$',views.plan_tobasic_view,name='plan_tobasic_view'),
|
||||
re_path(r'^checkouts/$',views.checkouts_view,name='checkouts'),
|
||||
re_path(r'^upgradecheckouts/$',views.upgrade_checkouts_view,name='upgrade_checkouts'),
|
||||
re_path(r'^downgradecheckouts/$',views.downgrade_checkouts_view,name='downgrade_checkouts'),
|
||||
re_path(r'^upgradecheckouts/$',views.upgrade_checkouts_view,name='upgrade_checkouts'),
|
||||
re_path(r'^purchasecheckouts/$',views.purchase_checkouts_view,name='purchase_checkouts_view'),
|
||||
re_path(r'^planrequired/',views.planrequired_view,name='planrequired_view'),
|
||||
re_path(r'^starttrial/$',views.start_trial_view,name='start_trial_view'),
|
||||
re_path(r'^startplantrial/$',views.start_plantrial_view,name='start_plantrial_view'),
|
||||
@@ -744,6 +745,8 @@ urlpatterns = [
|
||||
re_path(r'^plans/$', views.rower_select_instantplan, name='rower_select_instantplan'),
|
||||
re_path(r'^plans/(?P<id>[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})/$',
|
||||
views.rower_view_instantplan, name='rower_view_instantplan'),
|
||||
re_path(r'^buyplan/(?P<id>\d+)/$',views.buy_trainingplan_view,name='buy_trainingplan_view'),
|
||||
re_path(r'^confirmpurchaseplan/(?P<id>\d+)/$',views.confirm_trainingplan_purchase_view,name='confirm_trainingplan_purchase_view'),
|
||||
re_path(r'^addinstantplan/$', views.add_instantplan_view, name='add_instantplan_view'),
|
||||
re_path(r'^deleteplan/(?P<pk>\d+)/$',login_required(
|
||||
views.TrainingPlanDelete.as_view()),name='trainingplan_delete_view'),
|
||||
|
||||
@@ -84,6 +84,212 @@ def billing_view(request):
|
||||
'planselectform':planselectform,
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def buy_trainingplan_view(request,id=0):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = request.user.rower
|
||||
|
||||
plan = get_object_or_404(InstantPlan,pk=id)
|
||||
|
||||
if r.paymentprocessor != 'braintree':
|
||||
messages.error(request,"This purchase is currently only available through BrainTree (by PayPal)")
|
||||
|
||||
if id == 0 or id is None:
|
||||
messages.error(request,"There was an error accessing this plan")
|
||||
url = reverse('rower_view_instantplan',kwargs={
|
||||
'id':plan.uuid,
|
||||
})
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if request.method == 'POST':
|
||||
billingaddressform = RowerBillingAddressForm(instance=r)
|
||||
form = TrainingPlanForm(request.POST,user=request.user)
|
||||
if billingaddressform.is_valid():
|
||||
cd = billingaddressform.cleaned_data
|
||||
for attr, value in cd.items():
|
||||
setattr(r, attr, value)
|
||||
r.save()
|
||||
|
||||
# redirect to payment confirmation view
|
||||
if form.is_valid():
|
||||
cd = form.cleaned_data
|
||||
|
||||
enddate = cd['enddate']
|
||||
rowers = cd['rowers']
|
||||
notes = cd['notes']
|
||||
status = cd['status']
|
||||
|
||||
# get target and set enddate
|
||||
try:
|
||||
target = cd['target']
|
||||
except KeyError:
|
||||
try:
|
||||
targetid = request.POST['target']
|
||||
|
||||
if targetid != '':
|
||||
target = TrainingTarget.objects.get(id=int(targetid))
|
||||
else:
|
||||
target = None
|
||||
except KeyError:
|
||||
target = None
|
||||
|
||||
if target:
|
||||
enddate = target.date
|
||||
|
||||
pars = {
|
||||
'name':cd['name'],
|
||||
'enddate':enddate,
|
||||
'notes':notes,
|
||||
'status':status,
|
||||
'rower':rowers[0].id,
|
||||
}
|
||||
params = urllib.parse.urlencode(pars)
|
||||
url = reverse('confirm_trainingplan_purchase_view',kwargs={'id':plan.id})
|
||||
url = url + "?%s" % params
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
else:
|
||||
form = TrainingPlanForm(user=request.user)
|
||||
billingaddressform = RowerBillingAddressForm(instance=r)
|
||||
|
||||
return render(request,
|
||||
'buy_trainingplan.html',
|
||||
{
|
||||
'rower':r,
|
||||
'plan':plan,
|
||||
'billingaddressform':billingaddressform,
|
||||
'form':form,
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def purchase_checkouts_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = request.user.rower
|
||||
|
||||
if request.method != 'POST':
|
||||
url = reverse('rower_view_instantplan',kwargs={
|
||||
'id':plan.uuid,
|
||||
})
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
form = TrainingPlanBillingForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
data = form.cleaned_data
|
||||
|
||||
plan = InstantPlan.objects.get(id=data['plan'])
|
||||
authorizationstring = 'Bearer '+settings.WORKOUTS_FIT_TOKEN
|
||||
url = settings.WORKOUTS_FIT_URL+"/trainingplan/"+str(plan.uuid)
|
||||
headers = {'Authorization':authorizationstring}
|
||||
response = requests.get(url=url,headers=headers)
|
||||
if response.status_code != 200:
|
||||
messages.error(request,"Could not connect to the training plan server")
|
||||
return HttpResponseRedirect(reverse('rower_select_instantplan'))
|
||||
|
||||
amount, success = braintreestuff.make_payment(r,data)
|
||||
|
||||
if success:
|
||||
messages.info(request,"Your payment was completed and the sessions are copied to your calendar")
|
||||
plansteps = response.json()
|
||||
name = data['name']
|
||||
enddate = data['enddate']
|
||||
notes = data['notes']
|
||||
status = data['status']
|
||||
startdate = enddate-datetime.timedelta(days=plan.duration)
|
||||
p = TrainingPlan(
|
||||
name=name,
|
||||
#target=target,
|
||||
manager=r,
|
||||
startdate=startdate,
|
||||
enddate=enddate,status=status,
|
||||
notes=notes,
|
||||
)
|
||||
|
||||
p.save()
|
||||
|
||||
p.rowers.add(r)
|
||||
|
||||
create_sessions_from_json(plansteps,[r],startdate,r.user)
|
||||
|
||||
url = reverse('plannedsessions_view')
|
||||
timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d')
|
||||
url = url+'?when='+timeperiod
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
messages.error(request,"There was a problem with your payment")
|
||||
url = reverse('rower_view_instantplan',kwargs={
|
||||
'id':plan.uuid,
|
||||
})
|
||||
return HttpResponseRedirect(url)
|
||||
elif 'tac' not in request.POST:
|
||||
try:
|
||||
planid=int(request.POST['plan'])
|
||||
enddate = request.POST['enddate']
|
||||
rower = r.id
|
||||
# incomplete
|
||||
except IndexError:
|
||||
messages.error(request,"There was an error in the payment form")
|
||||
url = reverse("purchase_checkouts_view")
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
url = reverse('rower_view_instantplan',kwargs={
|
||||
'id':plan.uuid,
|
||||
})
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@login_required()
|
||||
def confirm_trainingplan_purchase_view(request,id = 0):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
url = reverse('promembership')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
r = request.user.rower
|
||||
|
||||
plan = get_object_or_404(InstantPlan,pk=id)
|
||||
|
||||
if r.paymentprocessor != 'braintree':
|
||||
messages.error(request,"This purchase is currently only available through BrainTree (by PayPal)")
|
||||
|
||||
if id == 0 or id is None:
|
||||
messages.error(request,"There was an error accessing this plan")
|
||||
url = reverse('rower_view_instantplan',kwargs={
|
||||
'id':plan.uuid,
|
||||
})
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
client_token = braintreestuff.get_client_token(r)
|
||||
|
||||
enddate = request.GET.get('enddate',None)
|
||||
name = request.GET.get('name','')
|
||||
status = request.GET.get('status',True)
|
||||
notes = request.GET.get('notes','')
|
||||
if enddate is None:
|
||||
messages.error(request,"There was an error accessing this plan")
|
||||
url = reverse('rower_view_instantplan',kwargs={
|
||||
'id':plan.uuid,
|
||||
})
|
||||
|
||||
return render(request,
|
||||
'confirm_trainingplan.html',
|
||||
{
|
||||
'plan':plan,
|
||||
'client_token':client_token,
|
||||
'rower':r,
|
||||
'enddate':enddate,
|
||||
'status':status,
|
||||
'name':name,
|
||||
'notes':notes,
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def upgrade_view(request):
|
||||
if not PAYMENT_PROCESSING_ON:
|
||||
|
||||
@@ -2473,9 +2473,6 @@ class PlannedSessionDelete(DeleteView):
|
||||
|
||||
return obj
|
||||
|
||||
@user_passes_test(can_plan,login_url="/rowers/paidplans",
|
||||
message="This functionality requires a Coach or Self-Coach plan",
|
||||
redirect_field_name=None)
|
||||
def rower_view_instantplan(request,id='',userid=0):
|
||||
r = getrequestrower(request,userid=userid)
|
||||
if not id:
|
||||
@@ -2521,6 +2518,12 @@ def rower_view_instantplan(request,id='',userid=0):
|
||||
).order_by("-date")
|
||||
|
||||
if request.method == 'POST':
|
||||
if not can_plan(request.user):
|
||||
messages.error(request,'You must be on a <a href="/rowers/paidplans">paid plan</a> to use this functionality')
|
||||
url = reverse('rower_view_instantplan',kwargs={
|
||||
'id':id,
|
||||
})
|
||||
return HttpResponseRedirect(url)
|
||||
form = TrainingPlanForm(request.POST,user=request.user)
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
@@ -77,6 +77,7 @@ from rowers.forms import (
|
||||
VideoAnalysisCreateForm,WorkoutSingleSelectForm,
|
||||
VideoAnalysisMetricsForm,SurveyForm,HistorySelectForm,
|
||||
StravaChartForm,FitnessFitForm,PerformanceManagerForm,
|
||||
TrainingPlanBillingForm,
|
||||
)
|
||||
|
||||
from django.urls import reverse, reverse_lazy
|
||||
|
||||
Reference in New Issue
Block a user