dynamicweb/digitalglarus/views.py

878 lines
32 KiB
Python

import logging
from django.conf import settings
from django.shortcuts import render
from django.http import HttpResponseRedirect, Http404
from django.core.urlresolvers import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.translation import get_language
from djangocms_blog.models import Post
from django.contrib import messages
from django.views.generic import DetailView, ListView
from utils.forms import ContactUsForm
from utils.mailer import BaseEmail
from django.views.generic.edit import FormView
from membership.models import StripeCustomer
from utils.views import (
LoginViewMixin, SignupViewMixin, PasswordResetViewMixin,
PasswordResetConfirmViewMixin
)
from utils.forms import (
PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm
)
from utils.stripe_utils import StripeUtils
from utils.models import UserBillingAddress
from utils.tasks import send_plain_email_task
from .forms import (
LoginForm, SignupForm, MembershipBillingForm, BookingDateForm,
BookingBillingForm, CancelBookingForm
)
from .models import (
MembershipType, Membership, MembershipOrder, Booking, BookingPrice,
BookingOrder, BookingCancellation, Supporter
)
from .mixins import (
MembershipRequiredMixin, IsNotMemberMixin, ChangeMembershipStatusMixin
)
logger = logging.getLogger(__name__)
class IndexView(TemplateView):
template_name = "digitalglarus/index.html"
class SupportusView(TemplateView):
template_name = "digitalglarus/supportus.html"
def get_context_data(self, *args, **kwargs):
context = super(SupportusView, self).get_context_data(**kwargs)
tags = ["dg-renovation"]
posts = Post.objects.filter(tags__name__in=tags, publish=True).translated(get_language())
context.update({
'post_list': posts
})
return context
class LoginView(LoginViewMixin):
template_name = "digitalglarus/login.html"
form_class = LoginForm
def get_success_url(self):
# redirect to membership orders list if user has at least one.
if self.request.user \
and MembershipOrder.objects.filter(customer__user=self.request.user):
return reverse_lazy('digitalglarus:membership_orders_list')
return reverse_lazy('digitalglarus:membership_pricing')
class SignupView(SignupViewMixin):
template_name = "digitalglarus/signup.html"
form_class = SignupForm
success_url = reverse_lazy('digitalglarus:login')
class PasswordResetView(PasswordResetViewMixin):
template_name = 'digitalglarus/reset_password.html'
success_url = reverse_lazy('digitalglarus:login')
form_class = PasswordResetRequestForm
template_email_path = 'digitalglarus/emails/'
class PasswordResetConfirmView(PasswordResetConfirmViewMixin):
template_name = 'digitalglarus/confirm_reset_password.html'
success_url = reverse_lazy('digitalglarus:login')
class HistoryView(TemplateView):
template_name = "digitalglarus/history.html"
def get_context_data(self, *args, **kwargs):
context = super(HistoryView, self).get_context_data(**kwargs)
supporters = Supporter.objects.all()
context.update({
'supporters': supporters
})
return context
class EditCreditCardView(FormView):
template_name = "digitalglarus/edit_credit_card.html"
form_class = EditCreditCardForm
success_url = reverse_lazy('digitalglarus:booking_payment')
def get_context_data(self, *args, **kwargs):
context = super(EditCreditCardView, self).get_context_data(*args, **kwargs)
context.update({
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
})
return context
def form_valid(self, form):
token = form.cleaned_data.get('token')
user = self.request.user
customer = user.stripecustomer
stripe_utls = StripeUtils()
card_response = stripe_utls.update_customer_card(customer.stripe_id, token)
new_card_data = card_response.get('response_object')
self.request.session.update({
'new_change_credit_card': new_card_data
})
return super(EditCreditCardView, self).form_valid(form)
class BookingSelectDatesView(LoginRequiredMixin, MembershipRequiredMixin, FormView):
template_name = "digitalglarus/booking.html"
form_class = BookingDateForm
membership_redirect_url = reverse_lazy('digitalglarus:membership_pricing')
login_url = reverse_lazy('digitalglarus:login')
success_url = reverse_lazy('digitalglarus:booking_payment')
def get_form_kwargs(self):
kwargs = super(BookingSelectDatesView, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
def form_valid(self, form):
user = self.request.user
start_date = form.cleaned_data.get('start_date')
end_date = form.cleaned_data.get('end_date')
booking_days = (end_date - start_date).days + 1
price_per_day = BookingPrice.objects.get().price_per_day
original_price, final_price, free_days = Booking.\
booking_price(user, start_date, end_date)
total_discount = price_per_day * free_days
self.request.session.update({
'original_price': original_price,
'final_price': final_price,
'total_discount': total_discount,
'booking_price_per_day': price_per_day,
'booking_days': booking_days,
'free_days': free_days,
'start_date': start_date.strftime('%m/%d/%Y'),
'end_date': end_date.strftime('%m/%d/%Y'),
'is_free': final_price == 0
})
return super(BookingSelectDatesView, self).form_valid(form)
class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView):
template_name = "digitalglarus/booking_payment.html"
form_class = BookingBillingForm
membership_redirect_url = reverse_lazy('digitalglarus:membership_pricing')
# success_url = reverse_lazy('digitalglarus:booking_payment')
booking_needed_fields = ['original_price', 'final_price', 'booking_days', 'free_days',
'start_date', 'end_date', 'booking_price_per_day',
'total_discount', 'is_free']
def dispatch(self, request, *args, **kwargs):
from_booking = all(field in request.session.keys()
for field in self.booking_needed_fields)
if not from_booking:
return HttpResponseRedirect(reverse('digitalglarus:booking'))
return super(BookingPaymentView, self).dispatch(request, *args, **kwargs)
def get_success_url(self, order_id):
return reverse('digitalglarus:booking_orders_detail', kwargs={'pk': order_id})
def get_form_kwargs(self):
current_billing_address = self.request.user.billing_addresses.first()
form_kwargs = super(BookingPaymentView, self).get_form_kwargs()
form_kwargs.update({
'initial': {
'start_date': self.request.session.get('start_date'),
'end_date': self.request.session.get('end_date'),
'price': self.request.session.get('final_price'),
'street_address': current_billing_address.street_address,
'city': current_billing_address.city,
'postal_code': current_billing_address.postal_code,
'country': current_billing_address.country,
}
})
return form_kwargs
def get_context_data(self, *args, **kwargs):
context = super(BookingPaymentView, self).get_context_data(*args, **kwargs)
booking_data = {key: self.request.session.get(key)
for key in self.booking_needed_fields}
user = self.request.user
last_booking_order = BookingOrder.objects.filter(customer__user=user).last()
last_membership_order = MembershipOrder.objects.filter(customer__user=user).last()
# check if user changes his credit card
credit_card_data = self.request.session.get('new_change_credit_card')
# import pdb
# pdb.set_trace()
if not credit_card_data:
credit_card_data = last_booking_order.get_booking_cc_data() if last_booking_order \
and last_booking_order.get_booking_cc_data() \
else last_membership_order.get_membership_order_cc_data()
booking_data.update({
'credit_card_data': credit_card_data if credit_card_data else None,
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
})
context.update(booking_data)
return context
def form_valid(self, form):
data = form.cleaned_data
context = self.get_context_data()
token = data.get('token')
start_date = data.get('start_date')
end_date = data.get('end_date')
is_free = context.get('is_free')
normal_price, final_price, free_days = Booking.\
booking_price(self.request.user, start_date, end_date)
charge = None
# if not credit_card_needed:
# Get or create stripe customer
customer = StripeCustomer.get_or_create(email=self.request.user.email,
token=token)
if not customer:
form.add_error("__all__", "Invalid credit card")
return self.render_to_response(self.get_context_data(form=form))
# If booking is not free, make the stripe charge
if not is_free:
# Make stripe charge to a customer
stripe_utils = StripeUtils()
charge_response = stripe_utils.make_charge(amount=final_price,
customer=customer.stripe_id)
charge = charge_response.get('response_object')
# Check if the payment was approved
if not charge:
context.update({
'paymentError': charge_response.get('error'),
'form': form
})
return render(self.request, self.template_name, context)
charge = charge_response.get('response_object')
# Create Billing Address for Membership Order
billing_address = form.save()
# Create Billing Address for User if he does not have one
if not customer.user.billing_addresses.count():
data.update({
'user': customer.user.id
})
billing_address_user_form = UserBillingAddressForm(data)
billing_address_user_form.is_valid()
billing_address_user_form.save()
# Create Booking
booking_data = {
'start_date': start_date,
'end_date': end_date,
'free_days': free_days,
'price': normal_price,
'final_price': final_price,
}
booking = Booking.create(booking_data)
# Create Booking order
order_data = {
'booking': booking,
'customer': customer,
'billing_address': billing_address,
'stripe_charge': charge,
'amount': final_price,
'original_price': normal_price,
'special_month_price': BookingPrice.objects.last().special_month_price,
}
order = BookingOrder.create(order_data)
context = {
'booking': booking,
'order': order,
'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host())
}
email_data = {
'subject': 'Your booking order has been placed',
'to': self.request.user.email,
'context': context,
'template_name': 'booking_order_email',
'template_path': 'digitalglarus/emails/'
}
email = BaseEmail(**email_data)
email.send()
return HttpResponseRedirect(self.get_success_url(order.id))
class MembershipPricingView(TemplateView):
template_name = "digitalglarus/membership_pricing.html"
def get_context_data(self, **kwargs):
context = super(MembershipPricingView, self).get_context_data(**kwargs)
membership_type = MembershipType.objects.last()
context.update({
'membership_type': membership_type
})
return context
class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
template_name = "digitalglarus/membership_payment.html"
login_url = reverse_lazy('digitalglarus:signup')
form_class = MembershipBillingForm
already_member_redirect_url = reverse_lazy('digitalglarus:membership_orders_list')
def get_form_kwargs(self):
self.membership_type = MembershipType.objects.get(name='standard')
form_kwargs = super(MembershipPaymentView, self).get_form_kwargs()
form_kwargs.update({
'initial': {
'membership_type': self.membership_type.id
}
})
return form_kwargs
def get_context_data(self, **kwargs):
context = super(MembershipPaymentView, self).get_context_data(**kwargs)
context.update({
'stripe_key': settings.STRIPE_API_PUBLIC_KEY,
'membership_type': self.membership_type
})
return context
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
data = form.cleaned_data
context = self.get_context_data()
token = data.get('token')
membership_type = data.get('membership_type')
# Get or create stripe customer
customer = StripeCustomer.get_or_create(
email=self.request.user.email, token=token
)
if not customer:
form.add_error("__all__", "Invalid credit card")
return self.render_to_response(
self.get_context_data(form=form)
)
# Make stripe charge to a customer
stripe_utils = StripeUtils()
charge_response = stripe_utils.make_charge(
amount=membership_type.first_month_price,
customer=customer.stripe_id
)
charge = charge_response.get('response_object')
# Check if the payment was approved
if not charge:
context.update({
'paymentError': charge_response.get('error'),
'form': form
})
email_to_admin_data = {
'subject': "Could not create charge for Digital Glarus "
"user: {user}".format(
user=self.request.user.email
),
'from_email': 'info@digitalglarus.ch',
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
charge_response.items()]),
}
send_plain_email_task.delay(email_to_admin_data)
return render(request, self.template_name, context)
# Subscribe the customer to dg plan from the next month onwards
stripe_plan = stripe_utils.get_or_create_stripe_plan(
amount=membership_type.price,
name='Digital Glarus {sub_type_name} Subscription'.format(
sub_type_name=membership_type.name
),
stripe_plan_id='dg-{sub_type_name}'.format(
sub_type_name=membership_type.name
)
)
subscription_result = stripe_utils.subscribe_customer_to_plan(
customer.stripe_id,
[{"plan": stripe_plan.get('response_object').stripe_plan_id}],
trial_end=membership_type.next_month_in_sec_since_epoch
)
stripe_subscription_obj = subscription_result.get(
'response_object'
)
# Check if call to create subscription was ok
if (stripe_subscription_obj is None or
(stripe_subscription_obj.status != 'active' and
stripe_subscription_obj.status != 'trialing')):
context.update({
'paymentError': subscription_result.get('error'),
'form': form
})
email_to_admin_data = {
'subject': "Could not create Stripe subscription for "
"Digital Glarus user: {user}".format(
user=self.request.user.email
),
'from_email': 'info@digitalglarus.ch',
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
subscription_result.items()]),
}
send_plain_email_task.delay(email_to_admin_data)
return render(request, self.template_name, context)
charge = charge_response.get('response_object')
if 'source' in charge:
cardholder_name = charge['source']['name']
else:
cardholder_name = customer.user.name
# Create Billing Address
billing_address = form.save()
# Create Billing Address for User if he does not have one
if not customer.user.billing_addresses.count():
data.update({
'user': customer.user.id,
'cardholder_name': cardholder_name
})
billing_address_user_form = UserBillingAddressForm(data)
billing_address_user_form.is_valid()
billing_address_user_form.save()
# Get membership dates
membership_start_date, membership_end_date = membership_type.first_month_range
# Create or update membership plan
membership_data = {
'type': membership_type,
'active': True,
'start_date': membership_start_date,
'end_date': membership_end_date
}
membership = Membership.activate_or_crete(membership_data, self.request.user)
# Create membership order
order_data = {
'membership': membership,
'customer': customer,
'billing_address': billing_address,
'stripe_charge': charge,
'stripe_subscription_id': stripe_subscription_obj.id,
'amount': membership_type.first_month_price,
'start_date': membership_start_date,
'end_date': membership_end_date
}
membership_order = MembershipOrder.create(order_data)
request.session.update({
'membership_price': membership.type.first_month_price,
'membership_dates': membership.type.first_month_formated_range
})
email_to_admin_data = {
'subject': "New Digital Glarus subscription: {user}".format(
user=self.request.user.email
),
'from_email': 'info@digitalglarus.ch',
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
order_data.items()]),
}
send_plain_email_task.delay(email_to_admin_data)
context = {
'membership': membership,
'order': membership_order,
'membership_start_date': membership_start_date,
'membership_end_date': membership_end_date,
'base_url': "{0}://{1}".format(request.scheme, request.get_host())
}
email_data = {
'subject': 'Your membership has been charged',
'to': request.user.email,
'context': context,
'template_name': 'membership_charge',
'template_path': 'digitalglarus/emails/'
}
email = BaseEmail(**email_data)
email.send()
return HttpResponseRedirect(reverse('digitalglarus:membership_activated'))
else:
return self.form_invalid(form)
class MembershipActivatedView(TemplateView):
template_name = "digitalglarus/membership_activated.html"
def get_context_data(self, **kwargs):
context = super(MembershipActivatedView, self).get_context_data(**kwargs)
membership_price = self.request.session.get('membership_price')
membership_dates = self.request.session.get('membership_dates')
context.update({
'membership_price': membership_price,
'membership_dates': membership_dates,
})
return context
class MembershipDeactivateView(LoginRequiredMixin, UpdateView):
template_name = "digitalglarus/membership_deactivated.html"
model = Membership
success_message = "Your membership has been deactivated :("
success_url = reverse_lazy('digitalglarus:membership_orders_list')
login_url = reverse_lazy('digitalglarus:login')
fields = '__all__'
def get_object(self):
membership_order = MembershipOrder.objects.\
filter(customer__user=self.request.user).last()
if not membership_order:
raise AttributeError("Membership does not exists")
membership = membership_order.membership
return membership
def post(self, *args, **kwargs):
membership = self.get_object()
membership.deactivate()
messages.add_message(
self.request, messages.SUCCESS, self.success_message
)
# cancel Stripe subscription
stripe_utils = StripeUtils()
membership_order = MembershipOrder.objects.filter(
customer__user=self.request.user
).last()
if membership_order:
if membership_order.stripe_subscription_id:
result = stripe_utils.unsubscribe_customer(
subscription_id=membership_order.stripe_subscription_id
)
stripe_subscription_obj = result.get('response_object')
# Check if the subscription was canceled
if (stripe_subscription_obj is None or
stripe_subscription_obj.status != 'canceled'):
error_msg = result.get('error')
logger.error(
"Could not cancel Digital Glarus subscription. "
"Reason: {reason}".format(
reason=error_msg
)
)
else:
logger.error(
"User {user} may have Stripe subscriptions created "
"manually. Please check.".format(
user=self.request.user.name
)
)
else:
logger.error(
"MembershipOrder for {user} not found".format(
user=self.request.user.name
)
)
return HttpResponseRedirect(self.success_url)
class MembershipReactivateView(ChangeMembershipStatusMixin):
success_message = "Your membership has been reactivate :)"
template_name = "digitalglarus/membership_orders_list.html"
def post(self, request, *args, **kwargs):
membership = self.get_object()
membership.activate()
return super(MembershipReactivateView, self).post(request, *args, **kwargs)
class UserBillingAddressView(LoginRequiredMixin, UpdateView):
model = UserBillingAddress
form_class = UserBillingAddressForm
template_name = "digitalglarus/user_billing_address.html"
success_url = reverse_lazy('digitalglarus:user_billing_address')
success_message = "Billing Address Updated"
def get_success_url(self):
next_url = self.request.POST.get('next') if self.request.POST.get('next')\
else self.success_url
return next_url
def get_context_data(self, **kwargs):
context = super(UserBillingAddressView, self).get_context_data(**kwargs)
current_billing_address = self.request.user.billing_addresses.all().first()
context.update({
'current_billing_address': current_billing_address
})
return context
def form_valid(self, form):
"""
If the form is valid, save the associated model.
"""
messages.add_message(self.request, messages.SUCCESS, self.success_message)
self.object = form.save()
return super(UserBillingAddressView, self).form_valid(form)
def get_form_kwargs(self):
current_billing_address = self.request.user.billing_addresses.first()
form_kwargs = super(UserBillingAddressView, self).get_form_kwargs()
if not current_billing_address:
return form_kwargs
form_kwargs.update({
'initial': {
'street_address': current_billing_address.street_address,
'city': current_billing_address.city,
'postal_code': current_billing_address.postal_code,
'country': current_billing_address.country,
}
})
return form_kwargs
def get_object(self):
current_billing_address = self.request.user.\
billing_addresses.filter(current=True).last()
# if not current_billing_address:
# raise AttributeError("Billing Address does not exists")
return current_billing_address
class MembershipDeactivateSuccessView(LoginRequiredMixin, TemplateView):
template_name = "digitalglarus/membership_deactivated_success.html"
class MembershipOrdersListView(LoginRequiredMixin, ListView):
template_name = "digitalglarus/membership_orders_list.html"
context_object_name = "orders"
login_url = reverse_lazy('digitalglarus:login')
model = MembershipOrder
paginate_by = 10
def get_context_data(self, **kwargs):
context = super(MembershipOrdersListView, self).get_context_data(**kwargs)
current_membership = Membership.get_current_membership(self.request.user)
start_date, end_date = (current_membership.start_date, current_membership.end_date)\
if current_membership else [None, None]
next_start_date, next_end_date = MembershipOrder.next_membership_dates(self.request.user)
current_billing_address = self.request.user.billing_addresses.filter(current=True).last()
context.update({
'membership_start_date': start_date,
'membership_end_date': end_date,
'current_membership': current_membership,
'next_membership_start_date': next_start_date,
'next_membership_end_date': next_end_date,
'billing_address': current_billing_address
})
return context
def get_queryset(self):
queryset = super(MembershipOrdersListView, self).get_queryset()
queryset = queryset.filter(customer__user=self.request.user)
return queryset
class OrdersMembershipDetailView(LoginRequiredMixin, DetailView):
template_name = "digitalglarus/membership_orders_detail.html"
context_object_name = "order"
login_url = reverse_lazy('digitalglarus:login')
# permission_required = ['view_hostingorder']
model = MembershipOrder
def get_context_data(self, **kwargs):
context = super(OrdersMembershipDetailView, self).get_context_data(**kwargs)
start_date, end_date = self.object.get_membership_range_date()
context.update({
'membership_start_date': start_date,
'membership_end_date': end_date,
})
return context
class OrdersBookingDetailView(LoginRequiredMixin, UpdateView):
template_name = "digitalglarus/booking_orders_detail.html"
context_object_name = "order"
login_url = reverse_lazy('digitalglarus:login')
form_class = CancelBookingForm
success_message = "You booking has been cancelled"
# permission_required = ['view_hostingorder']
model = BookingOrder
def get_success_url(self):
pk = self.kwargs.get(self.pk_url_kwarg)
return reverse_lazy('digitalglarus:booking_orders_detail', kwargs={'pk': pk})
def form_valid(self, form):
booking_order = self.get_object()
booking_order.cancel()
request = self.request
BookingCancellation.create(booking_order)
context = {
'order': booking_order,
'booking': booking_order.booking,
'base_url': "{0}://{1}".format(request.scheme, request.get_host()),
'user': request.user
}
email_data = {
'subject': 'A cancellation has been requested',
'to': 'info@ungleich.ch',
'context': context,
'template_name': 'booking_cancellation_notification',
'template_path': 'digitalglarus/emails/'
}
email = BaseEmail(**email_data)
email.send()
context = {
'order': booking_order,
'booking': booking_order.booking,
'base_url': "{0}://{1}".format(request.scheme, request.get_host())
}
email_data = {
'subject': 'Your booking has been cancelled',
'to': request.user.email,
'context': context,
'template_name': 'booking_cancellation',
'template_path': 'digitalglarus/emails/'
}
email = BaseEmail(**email_data)
email.send()
messages.add_message(self.request, messages.SUCCESS, self.success_message)
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs):
context = super(OrdersBookingDetailView, self).get_context_data(**kwargs)
bookig_order = self.object
booking = bookig_order.booking
start_date = booking.start_date
end_date = booking.end_date
free_days = booking.free_days
booking_days = (end_date - start_date).days + 1
original_price = booking.price
final_price = booking.final_price
context.update({
'original_price': original_price,
'total_discount': original_price - final_price,
'final_price': final_price,
'booking_days': booking_days,
'free_days': free_days,
'start_date': start_date.strftime('%m/%d/%Y'),
'end_date': end_date.strftime('%m/%d/%Y'),
'booking_required': bookig_order.refund_required(),
})
return context
class BookingOrdersListView(LoginRequiredMixin, ListView):
template_name = "digitalglarus/booking_orders_list.html"
context_object_name = "orders"
login_url = reverse_lazy('digitalglarus:login')
model = BookingOrder
paginate_by = 10
def get_context_data(self, **kwargs):
context = super(BookingOrdersListView, self).get_context_data(**kwargs)
current_billing_address = self.request.user.billing_addresses.filter(current=True).last()
context.update({
'billing_address': current_billing_address
})
return context
def get_queryset(self):
queryset = super(BookingOrdersListView, self).get_queryset()
queryset = queryset.filter(customer__user=self.request.user)
return queryset
class ContactView(FormView):
template_name = 'contact.html'
form_class = ContactUsForm
success_url = reverse_lazy('digitalglarus:contact')
success_message = _('Message Successfully Sent')
def form_valid(self, form):
print("digital glarus contactusform")
#form.save()
#form.send_email()
#messages.add_message(self.request, messages.SUCCESS, self.success_message)
return super(ContactView, self).form_valid(form)
# OLD VIEWS
def blog(request):
tags = ["digitalglarus"]
posts = (Post.objects
.filter(tags__name__in=tags, publish=True)
.translated(get_language()))
context = {
'post_list': posts,
}
return render(request, 'glarus_blog/post_list.html', context)
def blog_detail(request, slug):
post = Post.objects.translated(get_language(), slug=slug).first()
if post is None:
raise Http404()
context = {
'post': post,
}
return render(request, 'glarus_blog/post_detail.html', context)
def support(request):
return render(request, 'support.html')
def supporters(request):
context = {
'supporters': Supporter.objects.order_by('name')
}
return render(request, 'supporters.html', context)