Repository: internetarchive/openlibrary

Issue:
Expand patron exports
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

### Describe the problem that you'd like solved
<!-- A clear and concise description of what you want to happen. -->
The account export page (https://openlibrary.org/account/import) only allows patrons to download their reading log history.  As a patron, I expect to be able to download other types of OL data on this page, like my book notes, lists, or reviews.

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->
Expand the account exports view to include lists, book notes and reviews.  Write some copy for each, concisely describing what the data is.

<!-- Which suggestions or requirements should be considered for how feature needs to appear or be implemented? -->

### Additional context
<!-- Add any other context or screenshots about the feature request here. -->
We should already have the code to get all of the required data at the patron level. Book review types and values are stored as integers in the DB, and will need to be transformed into something that our patrons understand without needing to reference our code.

### Stakeholders
<!-- @ tag stakeholders of this bug -->
@mekarpeles 




Discussion:

Resolving patch:
diff --git a/openlibrary/core/db.py b/openlibrary/core/db.py
--- a/openlibrary/core/db.py
+++ b/openlibrary/core/db.py
@@ -95,6 +95,15 @@ def update_work_ids_individually(cls, current_work_id, new_work_id, _test=False)
                     rows_deleted -= 1
         return rows_changed, rows_deleted, failed_deletes
 
+    @classmethod
+    def select_all_by_username(cls, username, _test=False):
+        oldb = get_db()
+        return list(oldb.select(
+            cls.TABLENAME,
+            where="username=$username",
+            vars={"username": username}
+        ))
+
 
 def _proxy(method_name):
     """Create a new function that call method with given name on the
diff --git a/openlibrary/core/observations.py b/openlibrary/core/observations.py
--- a/openlibrary/core/observations.py
+++ b/openlibrary/core/observations.py
@@ -1090,3 +1090,17 @@ def remove_observations(
             where_clause += ' AND observation_type=$observation_type AND observation_value=$observation_value'
 
         return oldb.delete('observations', where=(where_clause), vars=data)
+
+    @classmethod
+    def select_all_by_username(cls, username, _test=False):
+        rows = super().select_all_by_username(username, _test=_test)
+        types_and_values = _get_all_types_and_values()
+
+        for row in rows:
+            type_id = f"{row['observation_type']}"
+            value_id = f"{row['observation_value']}"
+            row['observation_type'] = types_and_values[type_id]['type']
+            row['observation_value'] = types_and_values[type_id]['values'][value_id]['name']
+
+        return rows
+
diff --git a/openlibrary/plugins/upstream/account.py b/openlibrary/plugins/upstream/account.py
--- a/openlibrary/plugins/upstream/account.py
+++ b/openlibrary/plugins/upstream/account.py
@@ -19,7 +19,10 @@
 from openlibrary import accounts
 from openlibrary.i18n import gettext as _
 from openlibrary.core import helpers as h, lending
+from openlibrary.core.booknotes import Booknotes
 from openlibrary.core.bookshelves import Bookshelves
+from openlibrary.core.observations import Observations
+from openlibrary.core.ratings import Ratings
 from openlibrary.plugins.recaptcha import recaptcha
 from openlibrary.plugins.upstream.mybooks import MyBooksTemplate
 from openlibrary.plugins import openlibrary as olib
@@ -806,10 +809,39 @@ def POST(self):
 class export_books(delegate.page):
     path = "/account/export"
 
+    date_format = '%Y-%m-%d %H:%M:%S'
+
     @require_login
     def GET(self):
+        i = web.input(type='')
+        filename = ''
+
         user = accounts.get_current_user()
         username = user.key.split('/')[-1]
+
+        if i.type == 'reading_log':
+            data = self.generate_reading_log(username)
+            filename = 'OpenLibrary_ReadingLog.csv'
+        elif i.type == 'book_notes':
+            data = self.generate_book_notes(username)
+            filename = 'OpenLibrary_BookNotes.csv'
+        elif i.type == 'reviews':
+            data = self.generate_reviews(username)
+            filename = 'OpenLibrary_Reviews.csv'
+        elif i.type == 'lists':
+            data = self.generate_list_overview(user.get_lists(limit=1000))
+            filename = 'Openlibrary_ListOverview.csv'
+        elif i.type == 'ratings':
+            data = self.generate_star_ratings(username)
+            filename = 'OpenLibrary_Ratings.csv'
+
+        web.header('Content-Type', 'text/csv')
+        web.header(
+            'Content-disposition', f'attachment; filename={filename}'
+        )
+        return delegate.RawText('' or data, content_type="text/csv")
+
+    def generate_reading_log(self, username):
         books = Bookshelves.get_users_logged_books(username, limit=10000)
         csv = []
         csv.append('Work Id,Edition Id,Bookshelf\n')
@@ -821,12 +853,84 @@ def GET(self):
                 '{}\n'.format(mapping[book['bookshelf_id']]),
             ]
             csv.append(','.join(row))
-        web.header('Content-Type', 'text/csv')
-        web.header(
-            'Content-disposition', 'attachment; filename=OpenLibrary_ReadingLog.csv'
-        )
-        csv = ''.join(csv)
-        return delegate.RawText(csv, content_type="text/csv")
+        return ''.join(csv)
+
+    def generate_book_notes(self, username):
+        csv = []
+        csv.append('Work ID,Edition ID,Note,Created On')
+        notes = Booknotes.select_all_by_username(username)
+
+        for note in notes:
+            escaped_note = note['notes'].replace('"', '""')
+            row = [
+                f"OL{note['work_id']}W",
+                f"OL{note['edition_id']}M",
+                f'"{escaped_note}"',
+                note['created'].strftime(self.date_format)
+            ]
+            csv.append(','.join(row))
+
+        return '\n'.join(csv)
+
+    def generate_reviews(self, username):
+        csv = []
+        csv.append('Work ID,Review Category,Review Value,Created On')
+        observations = Observations.select_all_by_username(username)
+
+        for o in observations:
+            row = [
+                f"OL{o['work_id']}W",
+                f'"{o["observation_type"]}"',
+                f'"{o["observation_value"]}"',
+                o['created'].strftime(self.date_format)
+            ]
+            csv.append(','.join(row))
+
+        return '\n'.join(csv)
+
+    def generate_list_overview(self, lists):
+        csv = []
+        csv.append('List ID,List Name,List Description,Entry,Created On,Last Updated')
+
+        for list in lists:
+            logger.info(list)
+            logger.info(vars(list))
+            logger.info(dir(list))
+            list_id = list.key.split('/')[-1]
+            created_on = list.created.strftime(self.date_format)
+            last_updated = list.last_modified.strftime(self.date_format) if list.last_modified else ''
+            for seed in list.seeds:
+                entry = seed
+                if not isinstance(seed, str):
+                    entry = seed.key
+                row = [
+                    list_id,
+                    f'"{list.name}"' if list.name else '', # Replace double quotes?
+                    f'"{list.description}"' if list.description else '',
+                    entry,
+                    created_on,
+                    last_updated
+                ]
+                csv.append(','.join(row))
+
+        return '\n'.join(csv)
+
+
+    def generate_star_ratings(self, username):
+        csv = []
+        csv.append('Work ID,Edition ID,Rating,Created On')
+        ratings = Ratings.select_all_by_username(username)
+
+        for rating in ratings:
+            row = [
+                f"OL{rating['work_id']}W",
+                f"OL{rating['edition_id']}M" if rating['edition_id'] else '',
+                f"{rating['rating']}",
+                rating['created'].strftime(self.date_format)
+            ]
+            csv.append(','.join(row))
+
+        return '\n'.join(csv)
 
 
 class account_loans(delegate.page):


Test patch: