Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Implement project (Team) closing #4287

Merged
merged 10 commits into from
Jan 12, 2017
3 changes: 2 additions & 1 deletion gratipay/models/team/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def slugize(name):
return slug


class Team(Model, mixins.Available, mixins.Membership, mixins.Takes, mixins.TipMigration):
class Team(Model, mixins.Available, mixins.Closing, mixins.Membership, mixins.Takes,
mixins.TipMigration):
"""Represent a Gratipay team.
"""

Expand Down
3 changes: 2 additions & 1 deletion gratipay/models/team/mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .available import AvailableMixin as Available
from .closing import ClosingMixin as Closing
from .membership import MembershipMixin as Membership
from .takes import TakesMixin as Takes
from .tip_migration import TipMigrationMixin as TipMigration

__all__ = ['Available', 'Membership', 'Takes', 'TipMigration']
__all__ = ['Available', 'Closing', 'Membership', 'Takes', 'TipMigration']
22 changes: 22 additions & 0 deletions gratipay/models/team/mixins/closing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

from gratipay.models import add_event


class ClosingMixin(object):
"""This mixin implements team closing.
"""

#: Whether the team is closed or not.

is_closed = False


def close(self):
"""Close the team account.
"""
with self.db.get_cursor() as cursor:
cursor.run("UPDATE teams SET is_closed=true WHERE id=%s", (self.id,))
add_event(cursor, 'team', dict(id=self.id, action='set', values=dict(is_closed=True)))
self.set_attributes(is_closed=True)
9 changes: 5 additions & 4 deletions js/gratipay/edit_team.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
Gratipay.edit_team = {}

Gratipay.edit_team.initForm = function() {
$form = $("#edit-team");
$buttons = $form.find("button"); // submit and cancel btns
$form.submit(Gratipay.edit_team.submitForm);
$('#edit-team').submit(Gratipay.edit_team.submitForm);
$('#close-team').submit(function() { return confirm('Really close project?') });
}

Gratipay.edit_team.submitForm = function(e) {
e.preventDefault();

var $form = $("#edit-team");
var $buttons = $form.find("button");
var data = new FormData($form[0]);

$buttons.prop("disabled", true);
Expand All @@ -27,4 +28,4 @@ Gratipay.edit_team.submitForm = function(e) {
},
error: [Gratipay.error, function () { $buttons.prop("disabled", false); }]
});
}
}
14 changes: 14 additions & 0 deletions scss/components/danger-zone.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.danger-zone {
margin-top: 64px;
border: 1px solid $red;
@include border-radius(5px);
padding: 20px;
h2 {
margin: 0 0 10px;
padding: 0;
color: $red;
}
button {
background: $red;
}
}
15 changes: 0 additions & 15 deletions scss/components/long-form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,4 @@
input.invalid:focus + .invalid-msg {
display: block;
}

.danger-zone {
margin-top: 64px;
border: 1px solid $red;
@include border-radius(5px);
padding: 20px;
h2 {
margin: 0 0 10px;
padding: 0;
color: $red;
}
button {
background: $red;
}
}
}
12 changes: 0 additions & 12 deletions scss/elements/buttons-knobs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,6 @@ button.selected:hover, button.selected.drag,
background: $darker-green;
}

button.close-account {
font-weight: normal;
background: none;
color: $red;
border: 1px solid $red;
&:hover {
background: $red;
color: white;
text-decoration: none;
}
}

.buttons button {
margin-top: 3px;
}
17 changes: 1 addition & 16 deletions tests/py/test_close.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,7 @@ def test_close_page_shows_a_message_to_team_owners(self):
alice = self.make_participant('alice', claimed_time='now')
self.make_team('A', alice)
body = self.client.GET('/~alice/settings/close', auth_as='alice').body
assert 'You are the owner of the A team.' in body

def test_close_page_shows_a_message_to_owners_of_two_teams(self):
alice = self.make_participant('alice', claimed_time='now')
self.make_team('A', alice)
self.make_team('B', alice)
body = self.client.GET('/~alice/settings/close', auth_as='alice').body
assert 'You are the owner of the A and B teams.' in body

def test_close_page_shows_a_message_to_owners_of_three_teams(self):
alice = self.make_participant('alice', claimed_time='now')
self.make_team('A', alice)
self.make_team('B', alice)
self.make_team('C', alice)
body = self.client.GET('/~alice/settings/close', auth_as='alice').body
assert 'You are the owner of the A, B and C teams.' in body
assert 'Please close the following projects first:' in body


# cpi - clear_payment_instructions
Expand Down
38 changes: 38 additions & 0 deletions tests/py/test_team_closing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

from gratipay.testing import Harness, T


class TestTeamClosing(Harness):

def test_teams_can_be_closed_via_python(self):
team = self.make_team()
team.close()
assert team.is_closed

def test_teams_can_be_closed_via_http(self):
self.make_team()
response = self.client.PxST('/TheEnterprise/edit/close', auth_as='picard')
assert response.headers['Location'] == '/~picard/'
assert response.code == 302
assert T('TheEnterprise').is_closed

def test_but_not_by_anon(self):
self.make_team()
response = self.client.PxST('/TheEnterprise/edit/close')
assert response.code == 401

def test_nor_by_turkey(self):
self.make_participant('turkey')
self.make_team()
response = self.client.PxST('/TheEnterprise/edit/close', auth_as='turkey')
assert response.code == 403

def test_admin_is_cool_though(self):
self.make_participant('Q', is_admin=True)
self.make_team()
response = self.client.PxST('/TheEnterprise/edit/close', auth_as='Q')
assert response.headers['Location'] == '/~Q/'
assert response.code == 302
assert T('TheEnterprise').is_closed
22 changes: 22 additions & 0 deletions www/%team/edit/close.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from aspen import Response
from gratipay.utils import get_team

[----------------------------------------------------------------------------]

if user.ANON:
raise Response(401, _("You need to log in to access this page."))

request.allow('POST')

team = get_team(state)

if not user.ADMIN and user.participant.username != team.owner:
raise Response(403, _("You are not authorized to access this page."))

if team.is_closed:
raise Response(403, _("Already closed."))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about disable "close" button instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was quicker to put together. We can enhance (add project reopening?) once the basic functionality is there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. BTW after your latest commit (hide closed team), in theory user should not reach here, as they won't see the already closed teams?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you try to close again by URL for some reason, you would get this error. All my latest commit did was hide the link to the project from the project owner's profile.


team.close()

website.redirect('/~{}/'.format(user.participant.username))
[---]
7 changes: 6 additions & 1 deletion www/%team/edit/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ suppress_sidebar = True
}
</style>

<form action="edit.json" method="POST" id = "edit-team">
<form action="edit.json" method="POST" id="edit-team">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">

<label><h2>{{ _("Project Name") }}</h2></label>
Expand All @@ -63,4 +63,9 @@ suppress_sidebar = True
<button onclick="window.location='../';return false;">{{ _("Cancel") }}</button>

</form>
<form action="close" method="POST" class="danger-zone" id="close-team">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<h2>{{ _("Danger Zone") }}</h2>
<button type="submit">{{ _("Close Project") }}</button>
</form>
{% endblock %}
1 change: 1 addition & 0 deletions www/assets/gratipay.css.spt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
@import "scss/components/chosen";
@import "scss/components/communities";
@import "scss/components/cta";
@import "scss/components/danger-zone";
@import "scss/components/dropdown";
@import "scss/components/emails";
@import "scss/components/js-edit";
Expand Down
1 change: 1 addition & 0 deletions www/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ teams = website.db.all("""

SELECT teams.*::teams
FROM teams
WHERE not is_closed
ORDER BY ctime DESC

""")
Expand Down
29 changes: 14 additions & 15 deletions www/~/%username/settings/close.spt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,9 @@ if request.method == 'POST':
else:
participant.close()
website.redirect('/~%s/' % participant.username)
teams = participant.get_teams()
nteams = len(teams)
if nteams > 1:
teams = ', '.join([t.name for t in teams[:-1]]) + ' and ' + teams[-1].name + ' teams'
elif nteams == 1:
teams = teams[0].name + ' team'

teams = website.db.all( "SELECT teams.*::teams FROM teams WHERE owner=%s AND NOT is_closed"
, (participant.username,)
)
[---] text/html
{% extends "templates/base.html" %}

Expand All @@ -32,9 +28,12 @@ elif nteams == 1:

{% if teams %}

<p>You are the owner of the {{ teams }}. We don't yet <a
href="https://github.com/gratipay/gratipay.com/issues/3602">support
team owners closing their accounts</a>.</p>
<p>Please close the following projects first:</p>
<ul>
{% for team in teams %}
<li><a href="/{{ team.slug }}/edit/">{{ team.name }}</a></li>
{% endfor %}
</ul>

{% endif %}

Expand Down Expand Up @@ -79,12 +78,12 @@ elif nteams == 1:
href="https://github.com/gratipay/gratipay.com/issues/397">data
retention policy</a>).</p>

<p>Things we clear immediately include your profile statement,
the payroll you're taking, and the payments you're giving.</p>
<p>Things we clear immediately include your profile statement,
the payments you're taking, and the payments you're giving.</p>

<p>We specifically <i>don't</i> delete your past giving and
taking history on the site, because that information also belongs
equally to other users (the owners of the Teams you gave to and
<p>We specifically <i>don't</i> delete your past giving and
taking history on the site, because that information also belongs
equally to other users (the owners of the Teams you gave to and
took from).</p>

<p>After you close your account, your profile page will say,
Expand Down
10 changes: 5 additions & 5 deletions www/~/%username/settings/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ emails = participant.get_emails()
{% endif %}
</p>

<div class="close">
<h2>{{ _("Close") }}</h2>
<div class="buttons">
<button class="close-account">{{ _("Close Account") }}</button>
</div>
<div class="danger-zone">
<h2>{{ _("Danger Zone") }}</h2>
<div class="buttons">
<button class="close-account">{{ _("Close Account") }}</button>
</div>
</div>
</div>
{% endblock %}