Allow accountless membership cancellation via the token #96851
@ -4,16 +4,31 @@
|
||||
|
||||
{% block admin_tools %}{# don't show any admin links here #}{% endblock admin_tools %}
|
||||
|
||||
{% block content_settings %}
|
||||
{% block after_membership_info %}
|
||||
{% if user.is_anonymous %}
|
||||
<div class="ml-4 mt-2 mb-2">
|
||||
<h2>Link Membership</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
You need a Blender ID account to manage this membership.
|
||||
You can create a new Blender ID account if you don't have it yet.
|
||||
<a class="btn" href="{{ settings.LOGIN_URL }}">Sign in</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-12">
|
||||
To directly cancel your membership, please use the button below.
|
||||
<form class="form cancel-membership-form" method="post" action="{{ cancel_url }}">
|
||||
{% csrf_token %}
|
||||
<button class="btn-danger form-submit" id="submit-button" type="submit"><span>Cancel membership</span></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="ml-4 mt-2 mb-2">
|
||||
<h3>Link Membership</h3>
|
||||
<div class="row">
|
||||
<div class="{% if membership.level.badge %}col-md-9{% else %}col-md-12{% endif %}">
|
||||
<p class="mt-2">
|
||||
You are going to link this <strong>{{ membership.level.name }}</strong> membership
|
||||
(created on <abbr title="{{ membership.created_at }}">{{ membership.created_at | date:"Y-m-d" }}</abbr>,
|
||||
currently {{ membership.status }}) to your account.
|
||||
</p>
|
||||
{{ membership.level.badge }}
|
||||
</div>
|
||||
{% if membership.level.badge %}
|
||||
@ -55,4 +70,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -25,14 +25,21 @@ class LinkMembershipTest(AbstractLooperTestCase):
|
||||
self.subscription = self.create_active_accountless_subscription()
|
||||
self.url = reverse('link_membership', kwargs={'token': self.link_customer_token.token})
|
||||
|
||||
def test_get_redirects_if_not_logged_in(self):
|
||||
def test_shows_cancelviatoken_if_not_logged_in(self):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(
|
||||
response['Location'],
|
||||
f'/oauth/login?next=/link-membership/{self.link_customer_token.token}/',
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, '<form class="form cancel-membership-form" method="post"')
|
||||
|
||||
def test_cancelviatoken(self):
|
||||
url = reverse(
|
||||
'settings_membership_cancel_via_token',
|
||||
kwargs={'token': self.link_customer_token.token}
|
||||
)
|
||||
response = self.client.post(url, data={'confirm': 'True'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.subscription.refresh_from_db()
|
||||
self.assertEqual(self.subscription.status, 'pending-cancellation')
|
||||
|
||||
def test_get_displays_a_form(self):
|
||||
some_user = User.objects.create(email='joe@example.com')
|
||||
|
@ -40,6 +40,9 @@ urlpatterns = [
|
||||
name='settings_membership_edit'),
|
||||
path('settings/membership/<int:membership_id>/cancel', settings.CancelMembershipView.as_view(),
|
||||
name='settings_membership_cancel'),
|
||||
path('settings/membership/cancelviatoken/<token>',
|
||||
settings.CancelMembershipViaTokenView.as_view(),
|
||||
name='settings_membership_cancel_via_token'),
|
||||
|
||||
path(
|
||||
'settings/billing/payment-methods/change/<int:subscription_id>',
|
||||
|
@ -4,7 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db import transaction
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.views.generic import FormView
|
||||
|
||||
from looper.models import (
|
||||
@ -77,7 +77,7 @@ def merge_customer_and_grant_badges(token: str, old_customer, user):
|
||||
campaign_order.campaign.grant_badges({campaign_order.order_id})
|
||||
|
||||
|
||||
class LinkMembershipView(LoginRequiredMixin, FormView):
|
||||
class LinkMembershipView(FormView):
|
||||
template_name = 'blender_fund_main/link_membership.html'
|
||||
form_class = forms.LinkMembershipForm
|
||||
success_url = reverse_lazy('settings_home')
|
||||
@ -113,6 +113,9 @@ class LinkMembershipView(LoginRequiredMixin, FormView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data()
|
||||
context['cancel_url'] = reverse(
|
||||
'settings_membership_cancel_via_token', kwargs={'token': self.kwargs['token']}
|
||||
)
|
||||
context['membership'] = self._get_membership(self.kwargs['token'])
|
||||
return context
|
||||
|
||||
@ -120,6 +123,8 @@ class LinkMembershipView(LoginRequiredMixin, FormView):
|
||||
token = self.kwargs['token']
|
||||
membership = self._get_membership(token)
|
||||
user = self.request.user
|
||||
if not user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
old_customer = membership.customer
|
||||
assert old_customer.user is None
|
||||
|
||||
|
@ -209,6 +209,14 @@ class CancelMembershipView(SingleMembershipMixin, FormView):
|
||||
|
||||
_log = log.getChild('CancelMembershipView')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {
|
||||
**super().get_context_data(**kwargs),
|
||||
'back_url': reverse(
|
||||
'settings_membership_edit', kwargs={'membership_id': self.membership_id}
|
||||
),
|
||||
}
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('settings_membership_edit',
|
||||
kwargs={'membership_id': self.membership_id})
|
||||
@ -221,6 +229,51 @@ class CancelMembershipView(SingleMembershipMixin, FormView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class CancelMembershipViaTokenView(FormView):
|
||||
template_name = 'settings/membership_cancel.html'
|
||||
form_class = forms.CancelMembershipForm
|
||||
initial = {'confirm': False}
|
||||
|
||||
_log = log.getChild('CancelMembershipViaTokenView')
|
||||
|
||||
def get_membership(self, token):
|
||||
linktoken = get_object_or_404(looper.models.LinkCustomerToken, token=token)
|
||||
memberships = linktoken.customer.memberships
|
||||
memberships_count = memberships.count()
|
||||
if memberships_count != 1:
|
||||
self._log.error(
|
||||
'Expected exactly one membership, found %s, customer pk=%s',
|
||||
memberships_count,
|
||||
linktoken.customer_id,
|
||||
)
|
||||
return memberships.first()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
token = self.kwargs['token']
|
||||
membership = self.get_membership(token)
|
||||
|
||||
return {
|
||||
**super().get_context_data(**kwargs),
|
||||
'back_url': reverse('link_membership', kwargs={'token': token}),
|
||||
'membership': membership,
|
||||
'subscription': membership.subscription,
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
token = self.kwargs['token']
|
||||
membership = self.get_membership(token)
|
||||
self._log.info('Cancelling membership pk=%d using token=%s',
|
||||
membership.pk, token)
|
||||
membership.cancel()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
# land on the same page, the template will hide the form
|
||||
# and confirm that cancellation has happened
|
||||
return reverse('settings_membership_cancel_via_token',
|
||||
kwargs={'token': self.kwargs['token']})
|
||||
|
||||
|
||||
class ExtendMembershipView(SingleMembershipMixin, ExpectReadableIPAddressMixin, FormView):
|
||||
"""Allow users to extend their membership by paying any amount."""
|
||||
# TODO(Sybren): maybe move this into Looper, or at least some of the code.
|
||||
|
@ -4,6 +4,9 @@
|
||||
|
||||
{% block after_membership_info %}
|
||||
<h2>Cancellation</h2>
|
||||
{% if subscription.status == 'cancelled' or subscription.status == 'pending-cancellation' %}
|
||||
<p>Your membership has been cancelled.</p>
|
||||
{% else %}
|
||||
<p>Are you sure you want to cancel your Blender Development Fund membership?</p>
|
||||
{% if subscription.status == 'active' and subscription.next_payment_in_future %}
|
||||
<p>
|
||||
@ -15,7 +18,8 @@
|
||||
{% csrf_token %}
|
||||
{% include "blender_fund_main/components/form.html" %}
|
||||
<hr/>
|
||||
<a class="btn" href="{% url 'settings_membership_edit' membership_id=membership.id %}"><span>← Keep Membership and Go Back</span></a>
|
||||
<a class="btn" href="{{ back_url }}"><span>← Keep Membership and Go Back</span></a>
|
||||
<button class="btn-danger form-submit ml-3" id="submit-button" type="submit"><span>Confirm Cancellation of Membership</span></button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock after_membership_info %}
|
||||
|
Loading…
Reference in New Issue
Block a user