We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
"year in the books" should not appear for newly registered users
**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
1. Register a new account during the time "{year} in the books" appears
2. Get a messsage encouraging you to see all the books you read in the year, which will be none.

**Expected behavior**
This should not be displayed for users who have no readthroughs.

**Screenshots**

![Screenshot 2024-01-03 at 11 08 20 am](https://github.com/bookwyrm-social/bookwyrm/assets/17669239/a33d5f2e-a666-4e57-93c3-59a28a62bddd)

**Instance**
dev site



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bookwyrm/views/feed.py`
Content:
```
1 """ non-interactive pages """
2 from django.contrib.auth.decorators import login_required
3 from django.core.paginator import Paginator
4 from django.db.models import Q
5 from django.http import HttpResponseNotFound, Http404
6 from django.shortcuts import get_object_or_404
7 from django.template.response import TemplateResponse
8 from django.utils import timezone
9 from django.utils.decorators import method_decorator
10 from django.views import View
11 
12 from bookwyrm import activitystreams, forms, models
13 from bookwyrm.models.user import FeedFilterChoices
14 from bookwyrm.activitypub import ActivitypubResponse
15 from bookwyrm.settings import PAGE_LENGTH, STREAMS
16 from bookwyrm.suggested_users import suggested_users
17 from .helpers import filter_stream_by_status_type, get_user_from_username
18 from .helpers import is_api_request, is_bookwyrm_request, maybe_redirect_local_path
19 from .annual_summary import get_annual_summary_year
20 
21 
22 # pylint: disable= no-self-use
23 @method_decorator(login_required, name="dispatch")
24 class Feed(View):
25     """activity stream"""
26 
27     def post(self, request, tab):
28         """save feed settings form, with a silent validation fail"""
29         filters_applied = False
30         form = forms.FeedStatusTypesForm(request.POST, instance=request.user)
31         if form.is_valid():
32             # workaround to avoid broadcasting this change
33             user = form.save(request, commit=False)
34             user.save(broadcast=False, update_fields=["feed_status_types"])
35             filters_applied = True
36 
37         return self.get(request, tab, filters_applied)
38 
39     def get(self, request, tab, filters_applied=False):
40         """user's homepage with activity feed"""
41         tab = [s for s in STREAMS if s["key"] == tab]
42         tab = tab[0] if tab else STREAMS[0]
43 
44         activities = activitystreams.streams[tab["key"]].get_activity_stream(
45             request.user
46         )
47         filtered_activities = filter_stream_by_status_type(
48             activities,
49             allowed_types=request.user.feed_status_types,
50         )
51         paginated = Paginator(filtered_activities, PAGE_LENGTH)
52 
53         suggestions = suggested_users.get_suggestions(request.user)
54 
55         data = {
56             **feed_page_data(request.user),
57             **{
58                 "user": request.user,
59                 "activities": paginated.get_page(request.GET.get("page")),
60                 "suggested_users": suggestions,
61                 "tab": tab,
62                 "streams": STREAMS,
63                 "goal_form": forms.GoalForm(),
64                 "feed_status_types_options": FeedFilterChoices,
65                 "filters_applied": filters_applied,
66                 "path": f"/{tab['key']}",
67                 "annual_summary_year": get_annual_summary_year(),
68                 "has_tour": True,
69             },
70         }
71         return TemplateResponse(request, "feed/feed.html", data)
72 
73 
74 @method_decorator(login_required, name="dispatch")
75 class DirectMessage(View):
76     """dm view"""
77 
78     def get(self, request, username=None):
79         """like a feed but for dms only"""
80         # remove fancy subclasses of status, keep just good ol' notes
81         activities = (
82             models.Status.privacy_filter(request.user, privacy_levels=["direct"])
83             .filter(
84                 review__isnull=True,
85                 comment__isnull=True,
86                 quotation__isnull=True,
87                 generatednote__isnull=True,
88             )
89             .order_by("-published_date")
90         )
91 
92         user = None
93         if username:
94             try:
95                 user = get_user_from_username(request.user, username)
96             except Http404:
97                 pass
98         if user:
99             activities = activities.filter(Q(user=user) | Q(mention_users=user))
100 
101         paginated = Paginator(activities, PAGE_LENGTH)
102         data = {
103             **feed_page_data(request.user),
104             **{
105                 "user": request.user,
106                 "partner": user,
107                 "activities": paginated.get_page(request.GET.get("page")),
108                 "path": "/direct-messages",
109             },
110         }
111         return TemplateResponse(request, "feed/direct_messages.html", data)
112 
113 
114 class Status(View):
115     """get posting"""
116 
117     # pylint: disable=unused-argument
118     def get(self, request, username, status_id, slug=None):
119         """display a particular status (and replies, etc)"""
120         user = get_user_from_username(request.user, username)
121         status = get_object_or_404(
122             models.Status.objects.select_subclasses(),
123             user=user,
124             id=status_id,
125             deleted=False,
126         )
127         # make sure the user is authorized to see the status
128         status.raise_visible_to_user(request.user)
129 
130         if is_api_request(request):
131             return ActivitypubResponse(
132                 status.to_activity(pure=not is_bookwyrm_request(request))
133             )
134 
135         if redirect_local_path := maybe_redirect_local_path(request, status):
136             return redirect_local_path
137 
138         visible_thread = (
139             models.Status.privacy_filter(request.user)
140             .filter(thread_id=status.thread_id)
141             .values_list("id", flat=True)
142         )
143         visible_thread = list(visible_thread)
144 
145         ancestors = models.Status.objects.select_subclasses().raw(
146             """
147             WITH RECURSIVE get_thread(depth, id, path) AS (
148 
149                 SELECT 1, st.id, ARRAY[st.id]
150                 FROM bookwyrm_status st
151                 WHERE id = '%s' AND id = ANY(%s)
152 
153                 UNION
154 
155                 SELECT (gt.depth + 1), st.reply_parent_id, path || st.id
156                 FROM get_thread gt, bookwyrm_status st
157 
158                 WHERE st.id = gt.id AND depth < 5 AND st.id = ANY(%s)
159 
160             )
161 
162             SELECT * FROM get_thread ORDER BY path DESC;
163         """,
164             params=[status.reply_parent_id or 0, visible_thread, visible_thread],
165         )
166         children = models.Status.objects.select_subclasses().raw(
167             """
168             WITH RECURSIVE get_thread(depth, id, path) AS (
169 
170                 SELECT 1, st.id, ARRAY[st.id]
171                 FROM bookwyrm_status st
172                 WHERE reply_parent_id = '%s' AND id = ANY(%s)
173 
174                 UNION
175 
176                 SELECT (gt.depth + 1), st.id, path || st.id
177                 FROM get_thread gt, bookwyrm_status st
178 
179                 WHERE st.reply_parent_id = gt.id AND depth < 5 AND st.id = ANY(%s)
180 
181             )
182 
183             SELECT * FROM get_thread ORDER BY path;
184         """,
185             params=[status.id, visible_thread, visible_thread],
186         )
187 
188         preview = None
189         if hasattr(status, "book"):
190             preview = status.book.preview_image
191         elif status.mention_books.exists():
192             preview = status.mention_books.first().preview_image
193 
194         data = {
195             **feed_page_data(request.user),
196             **{
197                 "status": status,
198                 "children": children,
199                 "ancestors": ancestors,
200                 "preview": preview,
201             },
202         }
203         return TemplateResponse(request, "feed/status.html", data)
204 
205 
206 class Replies(View):
207     """replies page (a json view of status)"""
208 
209     def get(self, request, username, status_id):
210         """ordered collection of replies to a status"""
211         # the html view is the same as Status
212         if not is_api_request(request):
213             status_view = Status.as_view()
214             return status_view(request, username, status_id)
215 
216         # the json view is different than Status
217         status = models.Status.objects.get(id=status_id)
218         if status.user.localname != username:
219             return HttpResponseNotFound()
220         status.raise_visible_to_user(request.user)
221 
222         return ActivitypubResponse(status.to_replies(**request.GET))
223 
224 
225 def feed_page_data(user):
226     """info we need for every feed page"""
227     if not user.is_authenticated:
228         return {}
229 
230     goal = models.AnnualGoal.objects.filter(user=user, year=timezone.now().year).first()
231     return {
232         "goal": goal,
233         "goal_form": forms.GoalForm(),
234     }
235 
236 
237 def get_suggested_books(user, max_books=5):
238     """helper to get a user's recent books"""
239     book_count = 0
240     preset_shelves = {"reading": max_books, "read": 2, "to-read": max_books}
241     suggested_books = []
242 
243     user_shelves = {
244         shelf.identifier: shelf
245         for shelf in user.shelf_set.filter(
246             identifier__in=preset_shelves.keys()
247         ).exclude(books__isnull=True)
248     }
249 
250     for preset, shelf_max in preset_shelves.items():
251         limit = (
252             shelf_max
253             if shelf_max < (max_books - book_count)
254             else max_books - book_count
255         )
256         shelf = user_shelves.get(preset, None)
257         if not shelf:
258             continue
259 
260         shelf_preview = {
261             "name": shelf.name,
262             "identifier": shelf.identifier,
263             "books": models.Edition.viewer_aware_objects(user)
264             .filter(
265                 shelfbook__shelf=shelf,
266             )
267             .order_by("-shelfbook__shelved_date")
268             .prefetch_related("authors")[:limit],
269         }
270         suggested_books.append(shelf_preview)
271         book_count += len(shelf_preview["books"])
272     return suggested_books
273
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py
--- a/bookwyrm/views/feed.py
+++ b/bookwyrm/views/feed.py
@@ -1,4 +1,5 @@
 """ non-interactive pages """
+from datetime import date
 from django.contrib.auth.decorators import login_required
 from django.core.paginator import Paginator
 from django.db.models import Q
@@ -52,6 +53,19 @@
 
         suggestions = suggested_users.get_suggestions(request.user)
 
+        cutoff = (
+            date(get_annual_summary_year(), 12, 31)
+            if get_annual_summary_year()
+            else None
+        )
+        readthroughs = (
+            models.ReadThrough.objects.filter(
+                user=request.user, finish_date__lte=cutoff
+            )
+            if get_annual_summary_year()
+            else []
+        )
+
         data = {
             **feed_page_data(request.user),
             **{
@@ -66,6 +80,7 @@
                 "path": f"/{tab['key']}",
                 "annual_summary_year": get_annual_summary_year(),
                 "has_tour": True,
+                "has_summary_read_throughs": len(readthroughs),
             },
         }
         return TemplateResponse(request, "feed/feed.html", data)

{"golden_diff": "diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py\n--- a/bookwyrm/views/feed.py\n+++ b/bookwyrm/views/feed.py\n@@ -1,4 +1,5 @@\n \"\"\" non-interactive pages \"\"\"\n+from datetime import date\n from django.contrib.auth.decorators import login_required\n from django.core.paginator import Paginator\n from django.db.models import Q\n@@ -52,6 +53,19 @@\n \n         suggestions = suggested_users.get_suggestions(request.user)\n \n+        cutoff = (\n+            date(get_annual_summary_year(), 12, 31)\n+            if get_annual_summary_year()\n+            else None\n+        )\n+        readthroughs = (\n+            models.ReadThrough.objects.filter(\n+                user=request.user, finish_date__lte=cutoff\n+            )\n+            if get_annual_summary_year()\n+            else []\n+        )\n+\n         data = {\n             **feed_page_data(request.user),\n             **{\n@@ -66,6 +80,7 @@\n                 \"path\": f\"/{tab['key']}\",\n                 \"annual_summary_year\": get_annual_summary_year(),\n                 \"has_tour\": True,\n+                \"has_summary_read_throughs\": len(readthroughs),\n             },\n         }\n         return TemplateResponse(request, \"feed/feed.html\", data)\n", "issue": "\"year in the books\" should not appear for newly registered users\n**Describe the bug**\r\nA clear and concise description of what the bug is.\r\n\r\n**To Reproduce**\r\n1. Register a new account during the time \"{year} in the books\" appears\r\n2. Get a messsage encouraging you to see all the books you read in the year, which will be none.\r\n\r\n**Expected behavior**\r\nThis should not be displayed for users who have no readthroughs.\r\n\r\n**Screenshots**\r\n\r\n![Screenshot 2024-01-03 at 11 08 20 am](https://github.com/bookwyrm-social/bookwyrm/assets/17669239/a33d5f2e-a666-4e57-93c3-59a28a62bddd)\r\n\r\n**Instance**\r\ndev site\r\n\r\n\n", "before_files": [{"content": "\"\"\" non-interactive pages \"\"\"\nfrom django.contrib.auth.decorators import login_required\nfrom django.core.paginator import Paginator\nfrom django.db.models import Q\nfrom django.http import HttpResponseNotFound, Http404\nfrom django.shortcuts import get_object_or_404\nfrom django.template.response import TemplateResponse\nfrom django.utils import timezone\nfrom django.utils.decorators import method_decorator\nfrom django.views import View\n\nfrom bookwyrm import activitystreams, forms, models\nfrom bookwyrm.models.user import FeedFilterChoices\nfrom bookwyrm.activitypub import ActivitypubResponse\nfrom bookwyrm.settings import PAGE_LENGTH, STREAMS\nfrom bookwyrm.suggested_users import suggested_users\nfrom .helpers import filter_stream_by_status_type, get_user_from_username\nfrom .helpers import is_api_request, is_bookwyrm_request, maybe_redirect_local_path\nfrom .annual_summary import get_annual_summary_year\n\n\n# pylint: disable= no-self-use\n@method_decorator(login_required, name=\"dispatch\")\nclass Feed(View):\n    \"\"\"activity stream\"\"\"\n\n    def post(self, request, tab):\n        \"\"\"save feed settings form, with a silent validation fail\"\"\"\n        filters_applied = False\n        form = forms.FeedStatusTypesForm(request.POST, instance=request.user)\n        if form.is_valid():\n            # workaround to avoid broadcasting this change\n            user = form.save(request, commit=False)\n            user.save(broadcast=False, update_fields=[\"feed_status_types\"])\n            filters_applied = True\n\n        return self.get(request, tab, filters_applied)\n\n    def get(self, request, tab, filters_applied=False):\n        \"\"\"user's homepage with activity feed\"\"\"\n        tab = [s for s in STREAMS if s[\"key\"] == tab]\n        tab = tab[0] if tab else STREAMS[0]\n\n        activities = activitystreams.streams[tab[\"key\"]].get_activity_stream(\n            request.user\n        )\n        filtered_activities = filter_stream_by_status_type(\n            activities,\n            allowed_types=request.user.feed_status_types,\n        )\n        paginated = Paginator(filtered_activities, PAGE_LENGTH)\n\n        suggestions = suggested_users.get_suggestions(request.user)\n\n        data = {\n            **feed_page_data(request.user),\n            **{\n                \"user\": request.user,\n                \"activities\": paginated.get_page(request.GET.get(\"page\")),\n                \"suggested_users\": suggestions,\n                \"tab\": tab,\n                \"streams\": STREAMS,\n                \"goal_form\": forms.GoalForm(),\n                \"feed_status_types_options\": FeedFilterChoices,\n                \"filters_applied\": filters_applied,\n                \"path\": f\"/{tab['key']}\",\n                \"annual_summary_year\": get_annual_summary_year(),\n                \"has_tour\": True,\n            },\n        }\n        return TemplateResponse(request, \"feed/feed.html\", data)\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass DirectMessage(View):\n    \"\"\"dm view\"\"\"\n\n    def get(self, request, username=None):\n        \"\"\"like a feed but for dms only\"\"\"\n        # remove fancy subclasses of status, keep just good ol' notes\n        activities = (\n            models.Status.privacy_filter(request.user, privacy_levels=[\"direct\"])\n            .filter(\n                review__isnull=True,\n                comment__isnull=True,\n                quotation__isnull=True,\n                generatednote__isnull=True,\n            )\n            .order_by(\"-published_date\")\n        )\n\n        user = None\n        if username:\n            try:\n                user = get_user_from_username(request.user, username)\n            except Http404:\n                pass\n        if user:\n            activities = activities.filter(Q(user=user) | Q(mention_users=user))\n\n        paginated = Paginator(activities, PAGE_LENGTH)\n        data = {\n            **feed_page_data(request.user),\n            **{\n                \"user\": request.user,\n                \"partner\": user,\n                \"activities\": paginated.get_page(request.GET.get(\"page\")),\n                \"path\": \"/direct-messages\",\n            },\n        }\n        return TemplateResponse(request, \"feed/direct_messages.html\", data)\n\n\nclass Status(View):\n    \"\"\"get posting\"\"\"\n\n    # pylint: disable=unused-argument\n    def get(self, request, username, status_id, slug=None):\n        \"\"\"display a particular status (and replies, etc)\"\"\"\n        user = get_user_from_username(request.user, username)\n        status = get_object_or_404(\n            models.Status.objects.select_subclasses(),\n            user=user,\n            id=status_id,\n            deleted=False,\n        )\n        # make sure the user is authorized to see the status\n        status.raise_visible_to_user(request.user)\n\n        if is_api_request(request):\n            return ActivitypubResponse(\n                status.to_activity(pure=not is_bookwyrm_request(request))\n            )\n\n        if redirect_local_path := maybe_redirect_local_path(request, status):\n            return redirect_local_path\n\n        visible_thread = (\n            models.Status.privacy_filter(request.user)\n            .filter(thread_id=status.thread_id)\n            .values_list(\"id\", flat=True)\n        )\n        visible_thread = list(visible_thread)\n\n        ancestors = models.Status.objects.select_subclasses().raw(\n            \"\"\"\n            WITH RECURSIVE get_thread(depth, id, path) AS (\n\n                SELECT 1, st.id, ARRAY[st.id]\n                FROM bookwyrm_status st\n                WHERE id = '%s' AND id = ANY(%s)\n\n                UNION\n\n                SELECT (gt.depth + 1), st.reply_parent_id, path || st.id\n                FROM get_thread gt, bookwyrm_status st\n\n                WHERE st.id = gt.id AND depth < 5 AND st.id = ANY(%s)\n\n            )\n\n            SELECT * FROM get_thread ORDER BY path DESC;\n        \"\"\",\n            params=[status.reply_parent_id or 0, visible_thread, visible_thread],\n        )\n        children = models.Status.objects.select_subclasses().raw(\n            \"\"\"\n            WITH RECURSIVE get_thread(depth, id, path) AS (\n\n                SELECT 1, st.id, ARRAY[st.id]\n                FROM bookwyrm_status st\n                WHERE reply_parent_id = '%s' AND id = ANY(%s)\n\n                UNION\n\n                SELECT (gt.depth + 1), st.id, path || st.id\n                FROM get_thread gt, bookwyrm_status st\n\n                WHERE st.reply_parent_id = gt.id AND depth < 5 AND st.id = ANY(%s)\n\n            )\n\n            SELECT * FROM get_thread ORDER BY path;\n        \"\"\",\n            params=[status.id, visible_thread, visible_thread],\n        )\n\n        preview = None\n        if hasattr(status, \"book\"):\n            preview = status.book.preview_image\n        elif status.mention_books.exists():\n            preview = status.mention_books.first().preview_image\n\n        data = {\n            **feed_page_data(request.user),\n            **{\n                \"status\": status,\n                \"children\": children,\n                \"ancestors\": ancestors,\n                \"preview\": preview,\n            },\n        }\n        return TemplateResponse(request, \"feed/status.html\", data)\n\n\nclass Replies(View):\n    \"\"\"replies page (a json view of status)\"\"\"\n\n    def get(self, request, username, status_id):\n        \"\"\"ordered collection of replies to a status\"\"\"\n        # the html view is the same as Status\n        if not is_api_request(request):\n            status_view = Status.as_view()\n            return status_view(request, username, status_id)\n\n        # the json view is different than Status\n        status = models.Status.objects.get(id=status_id)\n        if status.user.localname != username:\n            return HttpResponseNotFound()\n        status.raise_visible_to_user(request.user)\n\n        return ActivitypubResponse(status.to_replies(**request.GET))\n\n\ndef feed_page_data(user):\n    \"\"\"info we need for every feed page\"\"\"\n    if not user.is_authenticated:\n        return {}\n\n    goal = models.AnnualGoal.objects.filter(user=user, year=timezone.now().year).first()\n    return {\n        \"goal\": goal,\n        \"goal_form\": forms.GoalForm(),\n    }\n\n\ndef get_suggested_books(user, max_books=5):\n    \"\"\"helper to get a user's recent books\"\"\"\n    book_count = 0\n    preset_shelves = {\"reading\": max_books, \"read\": 2, \"to-read\": max_books}\n    suggested_books = []\n\n    user_shelves = {\n        shelf.identifier: shelf\n        for shelf in user.shelf_set.filter(\n            identifier__in=preset_shelves.keys()\n        ).exclude(books__isnull=True)\n    }\n\n    for preset, shelf_max in preset_shelves.items():\n        limit = (\n            shelf_max\n            if shelf_max < (max_books - book_count)\n            else max_books - book_count\n        )\n        shelf = user_shelves.get(preset, None)\n        if not shelf:\n            continue\n\n        shelf_preview = {\n            \"name\": shelf.name,\n            \"identifier\": shelf.identifier,\n            \"books\": models.Edition.viewer_aware_objects(user)\n            .filter(\n                shelfbook__shelf=shelf,\n            )\n            .order_by(\"-shelfbook__shelved_date\")\n            .prefetch_related(\"authors\")[:limit],\n        }\n        suggested_books.append(shelf_preview)\n        book_count += len(shelf_preview[\"books\"])\n    return suggested_books\n", "path": "bookwyrm/views/feed.py"}], "after_files": [{"content": "\"\"\" non-interactive pages \"\"\"\nfrom datetime import date\nfrom django.contrib.auth.decorators import login_required\nfrom django.core.paginator import Paginator\nfrom django.db.models import Q\nfrom django.http import HttpResponseNotFound, Http404\nfrom django.shortcuts import get_object_or_404\nfrom django.template.response import TemplateResponse\nfrom django.utils import timezone\nfrom django.utils.decorators import method_decorator\nfrom django.views import View\n\nfrom bookwyrm import activitystreams, forms, models\nfrom bookwyrm.models.user import FeedFilterChoices\nfrom bookwyrm.activitypub import ActivitypubResponse\nfrom bookwyrm.settings import PAGE_LENGTH, STREAMS\nfrom bookwyrm.suggested_users import suggested_users\nfrom .helpers import filter_stream_by_status_type, get_user_from_username\nfrom .helpers import is_api_request, is_bookwyrm_request, maybe_redirect_local_path\nfrom .annual_summary import get_annual_summary_year\n\n\n# pylint: disable= no-self-use\n@method_decorator(login_required, name=\"dispatch\")\nclass Feed(View):\n    \"\"\"activity stream\"\"\"\n\n    def post(self, request, tab):\n        \"\"\"save feed settings form, with a silent validation fail\"\"\"\n        filters_applied = False\n        form = forms.FeedStatusTypesForm(request.POST, instance=request.user)\n        if form.is_valid():\n            # workaround to avoid broadcasting this change\n            user = form.save(request, commit=False)\n            user.save(broadcast=False, update_fields=[\"feed_status_types\"])\n            filters_applied = True\n\n        return self.get(request, tab, filters_applied)\n\n    def get(self, request, tab, filters_applied=False):\n        \"\"\"user's homepage with activity feed\"\"\"\n        tab = [s for s in STREAMS if s[\"key\"] == tab]\n        tab = tab[0] if tab else STREAMS[0]\n\n        activities = activitystreams.streams[tab[\"key\"]].get_activity_stream(\n            request.user\n        )\n        filtered_activities = filter_stream_by_status_type(\n            activities,\n            allowed_types=request.user.feed_status_types,\n        )\n        paginated = Paginator(filtered_activities, PAGE_LENGTH)\n\n        suggestions = suggested_users.get_suggestions(request.user)\n\n        cutoff = (\n            date(get_annual_summary_year(), 12, 31)\n            if get_annual_summary_year()\n            else None\n        )\n        readthroughs = (\n            models.ReadThrough.objects.filter(\n                user=request.user, finish_date__lte=cutoff\n            )\n            if get_annual_summary_year()\n            else []\n        )\n\n        data = {\n            **feed_page_data(request.user),\n            **{\n                \"user\": request.user,\n                \"activities\": paginated.get_page(request.GET.get(\"page\")),\n                \"suggested_users\": suggestions,\n                \"tab\": tab,\n                \"streams\": STREAMS,\n                \"goal_form\": forms.GoalForm(),\n                \"feed_status_types_options\": FeedFilterChoices,\n                \"filters_applied\": filters_applied,\n                \"path\": f\"/{tab['key']}\",\n                \"annual_summary_year\": get_annual_summary_year(),\n                \"has_tour\": True,\n                \"has_summary_read_throughs\": len(readthroughs),\n            },\n        }\n        return TemplateResponse(request, \"feed/feed.html\", data)\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass DirectMessage(View):\n    \"\"\"dm view\"\"\"\n\n    def get(self, request, username=None):\n        \"\"\"like a feed but for dms only\"\"\"\n        # remove fancy subclasses of status, keep just good ol' notes\n        activities = (\n            models.Status.privacy_filter(request.user, privacy_levels=[\"direct\"])\n            .filter(\n                review__isnull=True,\n                comment__isnull=True,\n                quotation__isnull=True,\n                generatednote__isnull=True,\n            )\n            .order_by(\"-published_date\")\n        )\n\n        user = None\n        if username:\n            try:\n                user = get_user_from_username(request.user, username)\n            except Http404:\n                pass\n        if user:\n            activities = activities.filter(Q(user=user) | Q(mention_users=user))\n\n        paginated = Paginator(activities, PAGE_LENGTH)\n        data = {\n            **feed_page_data(request.user),\n            **{\n                \"user\": request.user,\n                \"partner\": user,\n                \"activities\": paginated.get_page(request.GET.get(\"page\")),\n                \"path\": \"/direct-messages\",\n            },\n        }\n        return TemplateResponse(request, \"feed/direct_messages.html\", data)\n\n\nclass Status(View):\n    \"\"\"get posting\"\"\"\n\n    # pylint: disable=unused-argument\n    def get(self, request, username, status_id, slug=None):\n        \"\"\"display a particular status (and replies, etc)\"\"\"\n        user = get_user_from_username(request.user, username)\n        status = get_object_or_404(\n            models.Status.objects.select_subclasses(),\n            user=user,\n            id=status_id,\n            deleted=False,\n        )\n        # make sure the user is authorized to see the status\n        status.raise_visible_to_user(request.user)\n\n        if is_api_request(request):\n            return ActivitypubResponse(\n                status.to_activity(pure=not is_bookwyrm_request(request))\n            )\n\n        if redirect_local_path := maybe_redirect_local_path(request, status):\n            return redirect_local_path\n\n        visible_thread = (\n            models.Status.privacy_filter(request.user)\n            .filter(thread_id=status.thread_id)\n            .values_list(\"id\", flat=True)\n        )\n        visible_thread = list(visible_thread)\n\n        ancestors = models.Status.objects.select_subclasses().raw(\n            \"\"\"\n            WITH RECURSIVE get_thread(depth, id, path) AS (\n\n                SELECT 1, st.id, ARRAY[st.id]\n                FROM bookwyrm_status st\n                WHERE id = '%s' AND id = ANY(%s)\n\n                UNION\n\n                SELECT (gt.depth + 1), st.reply_parent_id, path || st.id\n                FROM get_thread gt, bookwyrm_status st\n\n                WHERE st.id = gt.id AND depth < 5 AND st.id = ANY(%s)\n\n            )\n\n            SELECT * FROM get_thread ORDER BY path DESC;\n        \"\"\",\n            params=[status.reply_parent_id or 0, visible_thread, visible_thread],\n        )\n        children = models.Status.objects.select_subclasses().raw(\n            \"\"\"\n            WITH RECURSIVE get_thread(depth, id, path) AS (\n\n                SELECT 1, st.id, ARRAY[st.id]\n                FROM bookwyrm_status st\n                WHERE reply_parent_id = '%s' AND id = ANY(%s)\n\n                UNION\n\n                SELECT (gt.depth + 1), st.id, path || st.id\n                FROM get_thread gt, bookwyrm_status st\n\n                WHERE st.reply_parent_id = gt.id AND depth < 5 AND st.id = ANY(%s)\n\n            )\n\n            SELECT * FROM get_thread ORDER BY path;\n        \"\"\",\n            params=[status.id, visible_thread, visible_thread],\n        )\n\n        preview = None\n        if hasattr(status, \"book\"):\n            preview = status.book.preview_image\n        elif status.mention_books.exists():\n            preview = status.mention_books.first().preview_image\n\n        data = {\n            **feed_page_data(request.user),\n            **{\n                \"status\": status,\n                \"children\": children,\n                \"ancestors\": ancestors,\n                \"preview\": preview,\n            },\n        }\n        return TemplateResponse(request, \"feed/status.html\", data)\n\n\nclass Replies(View):\n    \"\"\"replies page (a json view of status)\"\"\"\n\n    def get(self, request, username, status_id):\n        \"\"\"ordered collection of replies to a status\"\"\"\n        # the html view is the same as Status\n        if not is_api_request(request):\n            status_view = Status.as_view()\n            return status_view(request, username, status_id)\n\n        # the json view is different than Status\n        status = models.Status.objects.get(id=status_id)\n        if status.user.localname != username:\n            return HttpResponseNotFound()\n        status.raise_visible_to_user(request.user)\n\n        return ActivitypubResponse(status.to_replies(**request.GET))\n\n\ndef feed_page_data(user):\n    \"\"\"info we need for every feed page\"\"\"\n    if not user.is_authenticated:\n        return {}\n\n    goal = models.AnnualGoal.objects.filter(user=user, year=timezone.now().year).first()\n    return {\n        \"goal\": goal,\n        \"goal_form\": forms.GoalForm(),\n    }\n\n\ndef get_suggested_books(user, max_books=5):\n    \"\"\"helper to get a user's recent books\"\"\"\n    book_count = 0\n    preset_shelves = {\"reading\": max_books, \"read\": 2, \"to-read\": max_books}\n    suggested_books = []\n\n    user_shelves = {\n        shelf.identifier: shelf\n        for shelf in user.shelf_set.filter(\n            identifier__in=preset_shelves.keys()\n        ).exclude(books__isnull=True)\n    }\n\n    for preset, shelf_max in preset_shelves.items():\n        limit = (\n            shelf_max\n            if shelf_max < (max_books - book_count)\n            else max_books - book_count\n        )\n        shelf = user_shelves.get(preset, None)\n        if not shelf:\n            continue\n\n        shelf_preview = {\n            \"name\": shelf.name,\n            \"identifier\": shelf.identifier,\n            \"books\": models.Edition.viewer_aware_objects(user)\n            .filter(\n                shelfbook__shelf=shelf,\n            )\n            .order_by(\"-shelfbook__shelved_date\")\n            .prefetch_related(\"authors\")[:limit],\n        }\n        suggested_books.append(shelf_preview)\n        book_count += len(shelf_preview[\"books\"])\n    return suggested_books\n", "path": "bookwyrm/views/feed.py"}]}