Repository: internetarchive/openlibrary

Issue:
Yearly Reading Overview Page for given Year
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

Create way for patron to see and share their check-ins

e.g. `/account/books/already-ready/2023`

### Describe the problem that you'd like solved
<!-- A clear and concise description of what you want to happen. -->
It is very difficult to view one's check-ins.  Currently, check-ins can only be viewed on book pages and My Books reading log shelf views.  If you'd like to see all of your check-ins, your best bet is to view the reading log shelves.  Even then, check-ins will be spread across shelves and intermingled with books that have no check-ins.

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->
Create a view which displays all check-ins for a year.  The endpoint can be something like `/account/books/already-read/{year}`.  There should be an easy way to navigate to the new check-in view from the Already Read My Books view.  If the patron's reading log is set to private, only the patron should be able to access the view and edit their check-ins.  Otherwise, anybody can access the view (but only the owner can edit).

#### Additional Requirements
* If possible, use the @constantinazouni + @Vassilis-Boubis List social preview link for this view
* If it’s my page, then show me a carousel of up to 5 books marked as “Already Read” without check-in dates
    * Title: “Books Missing Read Dates (12)” → Already Read
        * Not a requirement to filter out books already with check-ins
    * Show msg: “Some books you’ve read don’t have completion dates. Do you want to add them?"

<!-- 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. -->

### Stakeholders
<!-- @ tag stakeholders of this bug -->





Discussion:

Resolving patch:
diff --git a/openlibrary/core/bookshelves.py b/openlibrary/core/bookshelves.py
--- a/openlibrary/core/bookshelves.py
+++ b/openlibrary/core/bookshelves.py
@@ -210,6 +210,7 @@ def get_users_logged_books(
         limit: int = 100,
         page: int = 1,  # Not zero-based counting!
         sort: Literal['created asc', 'created desc'] = 'created desc',
+        checkin_year: int | None = None,
         q: str = "",
     ) -> Any:  # Circular imports prevent type hinting LoggedBooksData
         """
@@ -229,6 +230,17 @@ def get_users_logged_books(
         from openlibrary.plugins.worksearch.code import run_solr_query
         from openlibrary.plugins.worksearch.schemes.works import WorkSearchScheme
 
+        shelf_totals = cls.count_total_books_logged_by_user_per_shelf(username)
+        oldb = db.get_db()
+        page = int(page or 1)
+        query_params: dict[str, str | int | None] = {
+            'username': username,
+            'limit': limit,
+            'offset': limit * (page - 1),
+            'bookshelf_id': bookshelf_id,
+            'checkin_year': checkin_year,
+        }
+
         @dataclass
         class ReadingLogItem:
             """Holds the datetime a book was logged and the edition ID."""
@@ -295,7 +307,7 @@ def add_reading_log_data(
             return solr_docs
 
         def get_filtered_reading_log_books(
-            q: str, query_params: dict[str, str | int], filter_book_limit: int
+            q: str, query_params: dict[str, str | int | None], filter_book_limit: int
         ) -> LoggedBooksData:
             """
             Filter reading log books based an a query and return LoggedBooksData.
@@ -352,8 +364,9 @@ def get_filtered_reading_log_books(
             )
 
         def get_sorted_reading_log_books(
-            query_params: dict[str, str | int],
+            query_params: dict[str, str | int | None],
             sort: Literal['created asc', 'created desc'],
+            checkin_year: int | None,
         ):
             """
             Get a page of sorted books from the reading log. This does not work with
@@ -364,26 +377,30 @@ def get_sorted_reading_log_books(
             Solr for more complete book information, and then put the logged info into
             the Solr response.
             """
-            if sort == 'created desc':
-                query = (
-                    "SELECT work_id, created, edition_id from bookshelves_books WHERE "
-                    "bookshelf_id=$bookshelf_id AND username=$username "
-                    "ORDER BY created DESC "
-                    "LIMIT $limit OFFSET $offset"
-                )
+            if checkin_year:
+                query = """
+                SELECT b.work_id, b.created, b.edition_id
+                FROM bookshelves_books b
+                INNER JOIN bookshelves_events e
+                ON b.work_id = e.work_id AND b.username = e.username
+                WHERE b.bookshelf_id = $bookshelf_id
+                AND b.username = $username
+                AND e.event_date LIKE $checkin_year || '%'
+                ORDER BY b.created DESC
+                """
             else:
                 query = (
                     "SELECT work_id, created, edition_id from bookshelves_books WHERE "
                     "bookshelf_id=$bookshelf_id AND username=$username "
-                    "ORDER BY created ASC "
+                    f"ORDER BY created {'DESC' if sort == 'created desc' else 'ASC'} "
                     "LIMIT $limit OFFSET $offset"
                 )
+
             if not bookshelf_id:
                 query = "SELECT * from bookshelves_books WHERE username=$username"
                 # XXX Removing limit, offset, etc from data looks like a bug
                 # unrelated / not fixing in this PR.
                 query_params = {'username': username}
-
             reading_log_books: list[web.storage] = list(
                 oldb.query(query, vars=query_params)
             )
@@ -415,24 +432,15 @@ def get_sorted_reading_log_books(
                 docs=solr_docs,
             )
 
-        shelf_totals = cls.count_total_books_logged_by_user_per_shelf(username)
-        oldb = db.get_db()
-        page = int(page or 1)
-        query_params: dict[str, str | int] = {
-            'username': username,
-            'limit': limit,
-            'offset': limit * (page - 1),
-            'bookshelf_id': bookshelf_id,
-        }
-
-        # q won't have a value, and therefore filtering won't occur, unless len(q) >= 3,
-        # as limited in mybooks.my_books_view().
         if q:
+            # checkin_year ignored :(
             return get_filtered_reading_log_books(
                 q=q, query_params=query_params, filter_book_limit=FILTER_BOOK_LIMIT
             )
         else:
-            return get_sorted_reading_log_books(query_params=query_params, sort=sort)
+            return get_sorted_reading_log_books(
+                query_params=query_params, sort=sort, checkin_year=checkin_year
+            )
 
     @classmethod
     def iterate_users_logged_books(cls, username: str) -> Iterable[dict]:
diff --git a/openlibrary/plugins/upstream/mybooks.py b/openlibrary/plugins/upstream/mybooks.py
--- a/openlibrary/plugins/upstream/mybooks.py
+++ b/openlibrary/plugins/upstream/mybooks.py
@@ -8,6 +8,7 @@
 
 from openlibrary import accounts
 from openlibrary.utils import extract_numeric_id_from_olid
+from openlibrary.utils.dateutil import current_year
 from openlibrary.core.booknotes import Booknotes
 from openlibrary.core.bookshelves import Bookshelves
 from openlibrary.core.lending import add_availability, get_loans_of_user
@@ -104,6 +105,18 @@ def GET(self, username, key='want-to-read'):
             )
 
 
+class readinglog_yearly(delegate.page):
+    path = "/people/([^/]+)/books/already-read/year/([0-9]+)"
+
+    def GET(self, username, year=None):
+        year = int(year or current_year())
+        if year < 1000:
+            # The year is used in a LIKE statement when we query for the yearly summary, so
+            # ensuring that the year is at least four digits long avoids incorrect results.
+            raise web.badrequest(message="Year must be four digits")
+        return MyBooksTemplate(username, 'already-read').render(year=year)
+
+
 class readinglog_stats(delegate.page):
     path = "/people/([^/]+)/books/([a-zA-Z_-]+)/stats"
 
@@ -185,7 +198,14 @@ def __init__(self, username, key):
         self.counts = self.readlog.reading_log_counts
 
     def render(
-        self, page=1, sort='desc', list=None, q="", doc_count: int = 0, ratings=None
+        self,
+        page=1,
+        sort='desc',
+        list=None,
+        q="",
+        doc_count: int = 0,
+        ratings=None,
+        year=None,
     ):
         """
         Gather the data necessary to render the My Books template, and then
@@ -223,7 +243,12 @@ def render(
             # Reading log for logged in users.
             elif self.key in self.READING_LOG_KEYS:
                 logged_book_data: LoggedBooksData = self.readlog.get_works(
-                    key=self.key, page=page, sort='created', sort_order=sort, q=q
+                    key=self.key,
+                    page=page,
+                    sort='created',
+                    sort_order=sort,
+                    q=q,
+                    year=year,
                 )
                 docs = add_availability(logged_book_data.docs, mode="openlibrary_work")
                 doc_count = logged_book_data.total_results
@@ -243,7 +268,7 @@ def render(
         # Reading log for non-logged in users.
         elif self.key in self.READING_LOG_KEYS and is_public:
             logged_book_data: LoggedBooksData = self.readlog.get_works(  # type: ignore[no-redef]
-                key=self.key, page=page, sort='created', sort_order=sort, q=q
+                key=self.key, page=page, sort='created', sort_order=sort, q=q, year=year
             )
             docs = add_availability(logged_book_data.docs, mode="openlibrary_work")
             doc_count = logged_book_data.total_results
@@ -264,6 +289,7 @@ def render(
                 q=q,
                 results_per_page=RESULTS_PER_PAGE,
                 ratings=ratings,
+                checkin_year=year,
             )
 
         raise web.seeother(self.user.key)
@@ -390,6 +416,7 @@ def get_works(
         sort: str = 'created',
         sort_order: str = 'desc',
         q: str = "",
+        year: int | None = None,
     ) -> LoggedBooksData:
         """
         Get works for want-to-read, currently-reading, and already-read as
@@ -421,6 +448,7 @@ def get_works(
             page=page,
             limit=limit,
             sort=sort_literal,  # type: ignore[arg-type]
+            checkin_year=year,
             q=q,
         )
 


Test patch: