Repository: liberapay/liberapay.com

Issue:
Reason for closing account
We might get some useful feedback from people who close their Liberapay accounts if we add an optional text input to ask them why.

Implementation requirements:

- [x] Giving feedback doesn't prevent the deletion of the account, i.e. it's stored in a new table which doesn't have a foreign key constraint on the `participants` table.
- [x] The feedback is limited in length.
- [x] The feedback is displayed in the `/admin/users` page.
- [x] The feedback is displayed in the `/~/admin` page.
- [x] The feedback is automatically deleted after some time (a year?) if the account has been deleted or reopened.


Discussion:
Should the text of the feedback be stored in the database?
Yes. I've added an implementation checklist to the issue description.
@Changaco 
I'm trying to figure out how to add a new table. According to the readme, any changes should be put in `sql/branch.sql`. Becuase there is no `branch.sql` file I'm unsure how to go about this. Should I copy the entire contents of `sql/schema.sql` into a new `branch.sql` file, and then edit the `branch.sql` file? 

Also, to see the changes in the database, is running `make schema` sufficient?
@biskwikman The content of the `sql/branch.sql` file automatically ends up in [the `sql/migrations.sql` file](https://github.com/liberapay/liberapay.com/blob/master/sql/migrations.sql), so that file is a convenient way to see the history, but you can also [look at the commit history](https://github.com/liberapay/liberapay.com/commits/master/sql/branch.sql) and [search for the file's name in the code](https://github.com/liberapay/liberapay.com/search?q=branch.sql) to fully understand how it works.

`make schema` recreates the schema from scratch, deleting all data in the process. To check that your `branch.sql` is correct, you can use `make test-schema`. To apply the changes to the database used by your local instance, you can use `psql liberapay -f sql/branch.sql`.
@Changaco I'm making some progress on this. One thing I keep running into that I don't understand is that the `Participant` class seems to have a lot of properties that I can't seem to find the definitions of. Things like `id`, and particularly `db`. Do you know where I can see where these are defined? 
@biskwikman Most of the attributes, including `id`, correspond to the columns of the `participants` database table, which you can see in the `sql/schema.sql` file. The `db` attribute is a convenience reference to the database object (see [the postgres.py docs](https://postgres-py.readthedocs.io/en/latest/)).

Resolving patch:
diff --git a/liberapay/constants.py b/liberapay/constants.py
--- a/liberapay/constants.py
+++ b/liberapay/constants.py
@@ -428,6 +428,7 @@ def __missing__(self, currency):
 })
 
 SUMMARY_MAX_SIZE = 100
+FEEDBACK_MAX_SIZE = 1000
 
 TAKE_THROTTLING_THRESHOLD = MoneyAutoConvertDict(
     {k: Money('1.00', k) for k in ('EUR', 'USD')}
diff --git a/liberapay/main.py b/liberapay/main.py
--- a/liberapay/main.py
+++ b/liberapay/main.py
@@ -181,6 +181,7 @@ def default_body_parser(body_bytes, headers):
     cron(Daily(hour=16), fetch_currency_exchange_rates, True)
     cron(Daily(hour=17), paypal.sync_all_pending_payments, True)
     cron(Daily(hour=18), Payday.update_cached_amounts, True)
+    cron(Daily(hour=19), Participant.delete_old_feedback, True)
     cron(intervals.get('notify_patrons', 1200), Participant.notify_patrons, True)
     if conf.ses_feedback_queue_url:
         cron(intervals.get('fetch_email_bounces', 60), handle_email_bounces, True)
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -850,6 +850,39 @@ def invalidate_exchange_routes(self):
         for route in routes:
             route.invalidate()
 
+    def store_feedback(self, feedback):
+        """Store feedback in database if provided by user
+        """
+        feedback = '' if feedback is None else feedback.strip()
+        if feedback:
+            self.db.run("""
+                INSERT INTO feedback
+                            (participant, feedback)
+                     VALUES (%s, %s)
+                ON CONFLICT (participant) DO UPDATE
+                        SET feedback = excluded.feedback
+                          , ctime = excluded.ctime
+            """, (self.id, feedback))
+
+    @classmethod
+    def delete_old_feedback(cls):
+        """Delete old user feedback.
+        """
+        n = cls.db.one("""
+            WITH deleted AS (
+                DELETE FROM feedback
+                 WHERE ctime < current_date - interval '1 year'
+                   AND coalesce((
+                           SELECT status = 'active'
+                             FROM participants
+                            WHERE id = feedback.participant
+                       ), true)
+             RETURNING 1
+            ) SELECT count(*) FROM deleted
+        """)
+        if n:
+            website.logger.info(f"Deleted {n} old feedbacks.")
+
     @cached_property
     def closed_time(self):
         return self.db.one("""


Test patch: