Repository: liberapay/liberapay.com

Issue:
Can't rename team account
It's currently impossible to change the username of a team account ~~unless you're an admin~~.


Discussion:
We should notify team members when the team's name is changed. Ideally there would be a vote, but we don't support votes.

This is blocked on #22 and #699.

Resolving patch:
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -123,7 +123,7 @@ def make_active(cls, kind, username=None, password=None, cursor=None):
                   RETURNING participants.*::participants
             """.format(cols, placeholders), vals)
             if username:
-                p.change_username(username, c)
+                p.change_username(username, cursor=c)
         return p
 
     def make_team(self, name, email=None, email_lang=None, throttle_takes=True):
@@ -143,7 +143,7 @@ def make_team(self, name, email=None, email_lang=None, throttle_takes=True):
                      VALUES ('group', 'active', now(), %s)
                   RETURNING participants.*::participants
             """, (throttle_takes,))
-            t.change_username(name, c)
+            t.change_username(name, cursor=c)
             t.add_member(self, c)
             if email:
                 t.set_email_lang(email_lang, cursor=c)
@@ -1280,7 +1280,7 @@ def pay_invoice(self, invoice):
     # More Random Stuff
     # =================
 
-    def change_username(self, suggested, cursor=None):
+    def change_username(self, suggested, cursor=None, recorder=None):
         suggested = suggested and suggested.strip()
 
         if not suggested:
@@ -1316,11 +1316,11 @@ def change_username(self, suggested, cursor=None):
                 if actual is None:
                     return suggested
                 assert (suggested, lowercased) == actual  # sanity check
-                c.hit_rate_limit('change_username', self.id, TooManyUsernameChanges)
 
                 # Deal with redirections
                 last_rename = self.get_last_event_of_type('set_username')
                 if last_rename:
+                    c.hit_rate_limit('change_username', self.id, TooManyUsernameChanges)
                     old_username = last_rename.payload
                     prefixes = {
                         'old': '/%s/' % old_username.lower(),
@@ -1334,21 +1334,38 @@ def change_username(self, suggested, cursor=None):
                              , mtime = now()
                          WHERE to_prefix = %(old)s;
                     """, prefixes)
-                    # Add a redirection if the old name was in use long enough (1 hour)
-                    active_period = utcnow() - last_rename.ts
-                    if active_period.total_seconds() > 3600:
-                        c.run("""
-                            INSERT INTO redirections
-                                        (from_prefix, to_prefix)
-                                 VALUES (%(old)s || '%%', %(new)s)
-                            ON CONFLICT (from_prefix) DO UPDATE
-                                    SET to_prefix = excluded.to_prefix
-                                      , mtime = now()
-                        """, prefixes)
+                    if prefixes['old'] != prefixes['new']:
+                        # Add a redirection if the old name was in use long enough (1 hour)
+                        active_period = utcnow() - last_rename.ts
+                        if active_period.total_seconds() > 3600:
+                            c.run("""
+                                INSERT INTO redirections
+                                            (from_prefix, to_prefix)
+                                     VALUES (%(old)s || '%%', %(new)s)
+                                ON CONFLICT (from_prefix) DO UPDATE
+                                        SET to_prefix = excluded.to_prefix
+                                          , mtime = now()
+                            """, prefixes)
 
                 self.add_event(c, 'set_username', suggested)
                 self.set_attributes(username=suggested)
 
+            if last_rename and self.kind == 'group':
+                assert isinstance(recorder, Participant)
+                members = self.db.all("""
+                    SELECT p
+                      FROM current_takes t
+                      JOIN participants p ON p.id = t.member
+                     WHERE t.team = %s
+                """, (self.id,))
+                for m in members:
+                    if m != recorder:
+                        m.notify(
+                            'team_rename', email=False, web=True,
+                            old_name=old_username, new_name=suggested,
+                            renamed_by=recorder.username,
+                        )
+
         return suggested
 
     def update_avatar(self, src=None, cursor=None):


Test patch:
diff --git a/tests/py/test_settings.py b/tests/py/test_settings.py
--- a/tests/py/test_settings.py
+++ b/tests/py/test_settings.py
@@ -97,3 +97,21 @@ def test_too_long(self):
         r = self.change_username(username)
         assert r.code == 400
         assert "The username &#39;%s&#39; is too long." % username in r.text, r.text
+
+    def test_change_team_name(self):
+        team = self.make_participant(None, kind='group')
+        team.change_username('team')
+        alice = self.make_participant('alice')
+        team.add_member(alice)
+        bob = self.make_participant('bob')
+        team.add_member(bob)
+        r = self.client.POST('/team/settings/edit', {'username': 'Team'},
+                             auth_as=alice, raise_immediately=False)
+        assert r.code == 302
+        assert r.headers[b'Location'] == b'/Team/edit'
+        team = team.refetch()
+        assert team.username == 'Team'
+        alice = alice.refetch()
+        assert alice.pending_notifs == 0
+        bob = bob.refetch()
+        assert bob.pending_notifs == 1