diff --git a/hc/accounts/tests/test_close_account.py b/hc/accounts/tests/test_close_account.py new file mode 100644 index 00000000..13dcc3db --- /dev/null +++ b/hc/accounts/tests/test_close_account.py @@ -0,0 +1,57 @@ +from django.contrib.auth.models import User +from mock import patch + +from hc.test import BaseTestCase +from hc.api.models import Check +from hc.payments.models import Subscription + + +class CloseAccountTestCase(BaseTestCase): + + @patch("hc.payments.models.Subscription.cancel") + def test_it_works(self, mock_cancel): + Check.objects.create(user=self.alice, tags="foo a-B_1 baz@") + Subscription.objects.create(user=self.alice, subscription_id="123") + + self.client.login(username="alice@example.org", password="password") + r = self.client.post("/accounts/close/") + self.assertEqual(r.status_code, 302) + + # Alice should be gone + alices = User.objects.filter(username="alice") + self.assertFalse(alices.exists()) + + # Alice should be gone + alices = User.objects.filter(username="alice") + self.assertFalse(alices.exists()) + + # Bob's current team should be updated to self + self.bobs_profile.refresh_from_db() + self.assertEqual(self.bobs_profile.current_team, self.bobs_profile) + + # Check should be gone + self.assertFalse(Check.objects.exists()) + + # Subscription should have been canceled + self.assertTrue(mock_cancel.called) + + # Subscription should be gone + self.assertFalse(Subscription.objects.exists()) + + def test_partner_removal_works(self): + self.client.login(username="bob@example.org", password="password") + r = self.client.post("/accounts/close/") + self.assertEqual(r.status_code, 302) + + # Alice should be still present + self.alice.refresh_from_db() + self.profile.refresh_from_db() + + # Bob should be gone + bobs = User.objects.filter(username="bob") + self.assertFalse(bobs.exists()) + + def test_it_rejects_get(self): + self.client.login(username="bob@example.org", password="password") + r = self.client.get("/accounts/close/") + self.assertEqual(r.status_code, 405) diff --git a/hc/accounts/urls.py b/hc/accounts/urls.py index 06fc8445..23e47e97 100644 --- a/hc/accounts/urls.py +++ b/hc/accounts/urls.py @@ -16,6 +16,7 @@ urlpatterns = [ url(r'^profile/$', views.profile, name="hc-profile"), url(r'^profile/notifications/$', views.notifications, name="hc-notifications"), url(r'^profile/badges/$', views.badges, name="hc-badges"), + url(r'^close/$', views.close, name="hc-close"), url(r'^unsubscribe_reports/([\w-]+)/$', views.unsubscribe_reports, name="hc-unsubscribe-reports"), diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 519c1f11..f0765e95 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -12,12 +12,14 @@ from django.contrib.auth.models import User from django.core import signing from django.http import HttpResponseForbidden, HttpResponseBadRequest from django.shortcuts import redirect, render +from django.views.decorators.http import require_POST from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm, RemoveTeamMemberForm, ReportSettingsForm, SetPasswordForm, TeamNameForm) from hc.accounts.models import Profile, Member from hc.api.models import Channel, Check from hc.lib.badges import get_badge_url +from hc.payments.models import Subscription def _make_user(email): @@ -338,3 +340,24 @@ def switch_team(request, target_username): request.user.profile.save() return redirect("hc-checks") + + +@require_POST +@login_required +def close(request): + user = request.user + + # Subscription needs to be canceled before it is deleted: + sub = Subscription.objects.filter(user=user).first() + if sub: + sub.cancel() + + # Any users currently using this team need to switch to their own team: + for partner in Profile.objects.filter(current_team=user.profile): + partner.current_team = partner + partner.save() + + user.delete() + + request.session.flush() + return redirect("hc-index") diff --git a/hc/payments/models.py b/hc/payments/models.py index b6f47f53..6c05ef4c 100644 --- a/hc/payments/models.py +++ b/hc/payments/models.py @@ -39,6 +39,14 @@ class Subscription(models.Model): self._pm = braintree.PaymentMethod.find(self.payment_method_token) return self._pm + def cancel(self): + if self.subscription_id: + braintree.Subscription.cancel(self.subscription_id) + + self.subscription_id = "" + self.plan_id = "" + self.save() + def pm_is_credit_card(self): return isinstance(self._get_braintree_payment_method(), braintree.credit_card.CreditCard) diff --git a/hc/payments/tests/test_cancel_plan.py b/hc/payments/tests/test_cancel_plan.py index c8c86833..38edb031 100644 --- a/hc/payments/tests/test_cancel_plan.py +++ b/hc/payments/tests/test_cancel_plan.py @@ -13,7 +13,7 @@ class CancelPlanTestCase(BaseTestCase): self.sub.plan_id = "P5" self.sub.save() - @patch("hc.payments.views.braintree") + @patch("hc.payments.models.braintree") def test_it_works(self, mock_braintree): self.client.login(username="alice@example.org", password="password") diff --git a/hc/payments/views.py b/hc/payments/views.py index f6fa1c1b..3f85d7b6 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -156,12 +156,7 @@ def update_payment_method(request): @require_POST def cancel_plan(request): sub = Subscription.objects.get(user=request.user) - - braintree.Subscription.cancel(sub.subscription_id) - sub.subscription_id = "" - sub.plan_id = "" - sub.save() - + sub.cancel() return redirect("hc-pricing") diff --git a/static/css/settings.css b/static/css/settings.css index ecd01d8a..11b1e4a9 100644 --- a/static/css/settings.css +++ b/static/css/settings.css @@ -17,4 +17,10 @@ .page-profile .icon-ok { color: #5cb85c; +} + +#close-account { + margin-left: 24px; + border-color: #d43f3a; + color: #d43f3a; } \ No newline at end of file diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index eccd32d7..89e83cd3 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -136,6 +136,22 @@ {% endif %} + +