Repository: liberapay/liberapay.com

Issue:
Invitation notification doesn't dismiss
Since I received my first team invitation, I have a pending notification that stay even if I already accept the invitation. Click again on it display the invitation notification, and clicking on accept again display the following badly encoded message:

```
Vous avez dÃ©jÃ  acceptÃ© cette invitation.
```



Discussion:

Resolving patch:
diff --git a/liberapay/models/_mixin_team.py b/liberapay/models/_mixin_team.py
--- a/liberapay/models/_mixin_team.py
+++ b/liberapay/models/_mixin_team.py
@@ -21,14 +21,16 @@ class InactiveParticipantAdded(Exception): pass
 class MixinTeam(object):
 
     def invite(self, invitee, inviter):
+        assert self.kind == 'group'
         with self.db.get_cursor() as c:
-            self.add_event(c, 'invite', dict(invitee=invitee.id), inviter.id)
-            invitee.notify(
+            n_id = invitee.notify(
                 'team_invite',
                 team=self.username,
                 team_url=self.url(),
                 inviter=inviter.username,
             )
+            payload = dict(invitee=invitee.id, notification_id=n_id)
+            self.add_event(c, 'invite', payload, inviter.id)
 
     def add_member(self, member, cursor=None):
         """Add a member to this team.
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -727,10 +727,10 @@ def set_email_lang(self, accept_lang):
     # =============
 
     def notify(self, event, force_email=False, web=True, **context):
-        if web:
-            self.add_notification(event, **context)
         if force_email or self.email_notif_bits & EVENTS.get(event).bit:
             self.queue_email(event, **context)
+        if web:
+            return self.add_notification(event, **context)
 
     def add_notification(self, event, **context):
         p_id = self.id
@@ -750,24 +750,60 @@ def add_notification(self, event, **context):
         self.set_attributes(pending_notifs=pending_notifs)
         return n_id
 
+    def mark_notification_as_read(self, n_id):
+        p_id = self.id
+        r = self.db.one("""
+            WITH updated AS (
+                UPDATE notification_queue
+                   SET is_new = false
+                 WHERE participant = %(p_id)s
+                   AND id = %(n_id)s
+                   AND is_new
+             RETURNING id
+            )
+            UPDATE participants
+               SET pending_notifs = pending_notifs - (SELECT count(*) FROM updated)
+             WHERE id = %(p_id)s
+         RETURNING pending_notifs;
+        """, locals())
+        self.set_attributes(pending_notifs=r)
+
+    def mark_notifications_as_read(self, event=None):
+        if not self.pending_notifs:
+            return
+        p_id = self.id
+        sql_filter = 'AND event = %(event)s' if event else ''
+        r = self.db.one("""
+            WITH updated AS (
+                UPDATE notification_queue
+                   SET is_new = false
+                 WHERE participant = %(p_id)s
+                   AND is_new
+                   {0}
+             RETURNING id
+            )
+            UPDATE participants
+               SET pending_notifs = pending_notifs - (SELECT count(*) FROM updated)
+             WHERE id = %(p_id)s
+         RETURNING pending_notifs;
+        """.format(sql_filter), locals())
+        self.set_attributes(pending_notifs=r)
+
     def remove_notification(self, n_id):
         p_id = self.id
         r = self.db.one("""
-            DO $$
-            BEGIN
+            WITH deleted AS (
                 DELETE FROM notification_queue
                  WHERE id = %(n_id)s
-                   AND participant = %(p_id)s;
-                IF (NOT FOUND) THEN RETURN; END IF;
-                UPDATE participants
-                   SET pending_notifs = pending_notifs - 1
-                 WHERE id = %(p_id)s;
-            END;
-            $$ LANGUAGE plpgsql;
-
-            SELECT pending_notifs
-              FROM participants
-             WHERE id = %(p_id)s;
+                   AND participant = %(p_id)s
+             RETURNING is_new
+            )
+            UPDATE participants
+               SET pending_notifs = pending_notifs - (
+                       SELECT count(*) FROM deleted WHERE is_new
+                   )
+             WHERE id = %(p_id)s
+         RETURNING pending_notifs;
         """, locals())
         self.set_attributes(pending_notifs=r)
 
@@ -783,14 +819,13 @@ def fill_notification_context(self, context):
 
     def render_notifications(self, state):
         r = []
-        if not self.pending_notifs:
-            return r
         notifs = self.db.all("""
-            SELECT id, event, context
+            SELECT id, event, context, is_new
               FROM notification_queue
              WHERE participant = %s
+          ORDER BY is_new DESC, id DESC
         """, (self.id,))
-        for id, event, notif_context in notifs:
+        for id, event, notif_context, is_new in notifs:
             try:
                 notif_context = pickle.loads(notif_context)
                 context = dict(state)
@@ -799,7 +834,7 @@ def render_notifications(self, state):
                 spt = website.emails[event]
                 html = spt['text/html'].render(context).strip()
                 typ = notif_context.get('type', 'info')
-                r.append(dict(id=id, html=html, type=typ))
+                r.append(dict(id=id, html=html, type=typ, is_new=is_new))
             except Exception as e:
                 website.tell_sentry(e, state, allow_reraise=True)
         return r


Test patch: