[GrandComicsDatabase/gcd-django] Bugfix/issue 619 json export series overview (PR #718)

20 views
Skip to first unread message

jhunterjActual

unread,
Jun 13, 2026, 5:30:37 PMJun 13
to GrandComicsDatabase/gcd-django, Subscribed

Adding the API pieces needed for a JSON download of the Cover and Main Story Overview page on a series, for #619 .

The button works the same as on the issue: you have to click on the (JSON) portion, even though that's the only format available here. And it's just in the upper left of the overview page, sitting there by itself. So perhaps there are better user experience choices?

Should this also be added for the issue overview for creators https://www.comics.org/creator/18/overview/ and/or the issue overview for features https://www.comics.org/feature/4340/overview/ ?


You can view, comment on, or merge this pull request online at:

  https://github.com/GrandComicsDatabase/gcd-django/pull/718

Commit Summary

  • 4b38446 Fix series issue count for cross-series variants (For #235 and #157)
  • 132fae1 Fix cross-series variant issue counting
  • 3d6fc63 Incorporate Gemini Code Assist feedback
  • 777503b update comment blocks
  • 13a6ffc Add api and button for JSON export of cover and main story series overview.

File Changes

(8 files)

Patch Links:


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718@github.com>

gemini-code-assist[bot]

unread,
Jun 13, 2026, 5:32:28 PMJun 13
to GrandComicsDatabase/gcd-django, Subscribed

@gemini-code-assist[bot] commented on this pull request.

Code Review

This pull request introduces a new API endpoint and serializer to provide a JSON overview of a series' covers and main stories, alongside a download button on the series overview page. It also updates the caching and routing logic for cross-series variant statistics. The review feedback highlights critical N+1 query bottlenecks in the serializer and model, dead fallback logic in the serializer, a hardcoded URL path that should use Django's reverse utility, and invalid HTML nesting in the template.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.


In apps/api/serializers.py:

> +    def get_cover_url(self, obj) -> str:
+        covers = obj.active_covers()
+        if covers:
+            cover = covers[0]
+            return cover.get_base_url() + ("/w400/%d.jpg" % cover.id)
+        return ""

high

N+1 Query Bottleneck

Calling obj.active_covers() inside the serializer field for every issue triggers a database query per row. For a series with $N$ issues, this results in $N$ extra queries. We can completely eliminate this by using Django's Prefetch in the view's queryset to pre-load the active covers in bulk.

⬇️ Suggested change
-    def get_cover_url(self, obj) -> str:
-        covers = obj.active_covers()
-        if covers:
-            cover = covers[0]
-            return cover.get_base_url() + ("/w400/%d.jpg" % cover.id)
-        return ""
+    def get_cover_url(self, obj) -> str:
+        covers = getattr(obj, 'active_covers_list', [])
+        if covers:
+            cover = covers[0]
+            return cover.get_base_url() + ("/w400/%d.jpg" % cover.id)
+        return ""

In apps/api/serializers.py:

> +    def get_longest_story(self, obj):
+        from apps.gcd.models.story import Story
+
+        story_id = getattr(obj, 'longest_story_id', None)
+        if story_id:
+            try:
+                story = Story.objects.prefetch_related(
+                    'credits__creator__creator').get(id=story_id)
+                return StorySerializer(story).data
+            except Story.DoesNotExist:
+                pass
+
+        # Fallback: variant issues — check parent issue's longest story
+        if obj.variant_of_id:
+            story_qs = obj.variant_of.active_stories()\
+                          .filter(type_id=19, deleted=False)\
+                          .prefetch_related('credits__creator__creator')\
+                          .order_by('-page_count')
+            if story_qs.exists():
+                return StorySerializer(story_qs[0]).data
+
+        return None

high

N+1 Query Bottleneck & Dead Fallback Logic

  1. N+1 Query Issue: Calling Story.objects.get(id=story_id) inside the serializer field for every issue triggers a database query per row. We can completely eliminate this by using Django's Prefetch in the view's queryset to pre-load the longest stories in bulk.

  2. Dead Fallback Logic: The fallback check if obj.variant_of_id: will never execute because the view's queryset explicitly filters out variants using .filter(variant_of=None). Thus, variant_of_id is always None here.

    def get_longest_story(self, obj):
        stories = getattr(obj, 'longest_story_list', [])
        if stories:
            return StorySerializer(stories[0]).data
        return None

In apps/api/views.py:

> +    def get_queryset(self):
+        series_id = self.kwargs['series_id']
+        series = get_object_or_404(Series, id=series_id, deleted=False)
+        return (
+            series.active_issues()
+            .filter(variant_of=None)
+            .annotate(
+                longest_story_id=Subquery(
+                    Story.objects.filter(
+                        issue_id=OuterRef('pk'),
+                        type_id=19,
+                        deleted=False,
+                    ).values('pk').order_by('-page_count', 'sequence_number')[:1]
+                )
+            )
+            .select_related('series__publisher')
+        )

high

Query Optimization

Optimize the queryset using Prefetch to pre-load the active covers and the longest stories (including their creator credits) in bulk. This reduces the database roundtrips from $2N + 1$ to exactly 3 queries.

    def get_queryset(self):
        from django.db.models import Prefetch
        from apps.gcd.models import Cover
        series_id = self.kwargs['series_id']
        series = get_object_or_404(Series, id=series_id, deleted=False)
        return (
            series.active_issues()
            .filter(variant_of=None)
            .prefetch_related(
                Prefetch(
                    'cover_set',
                    queryset=Cover.objects.filter(deleted=False),
                    to_attr='active_covers_list'
                ),
                Prefetch(
                    'story_set',
                    queryset=Story.objects.filter(type_id=19, deleted=False)
                    .order_by('-page_count', 'sequence_number')
                    .prefetch_related('credits__creator__creator'),
                    to_attr='longest_story_list'
                )
            )
            .select_related('series__publisher')
        )

In apps/oi/models.py:

> +            for variant in variants:
+                # 1. Variant left behind: Goes from Standard -> Cross-Series (+1)
+                if variant.series == old_series and variant.series != new_series:
+                    variant.series.issue_count += 1
+                    variant.series.save()
+                    
+                # 2. Base issue returns: Goes from Cross-Series -> Standard (-1)
+                elif variant.series != old_series and variant.series == new_series:
+                    if variant.series.issue_count > 0:
+                        variant.series.issue_count -= 1
+                    variant.series.save()

high

N+1 Query and Object Sync Issue

  1. N+1 Queries: Accessing variant.series inside the loop triggers a database query for each variant to fetch its series. Comparing variant.series_id with old_series_id and new_series_id avoids these queries entirely.
  2. In-memory Sync: Instead of calling variant.series.save(), update and save the already-fetched old_series and new_series instances directly to ensure in-memory state remains synchronized.
⬇️ Suggested change
-            for variant in variants:
-                # 1. Variant left behind: Goes from Standard -> Cross-Series (+1)
-                if variant.series == old_series and variant.series != new_series:
-                    variant.series.issue_count += 1
-                    variant.series.save()
-                    
-                # 2. Base issue returns: Goes from Cross-Series -> Standard (-1)
-                elif variant.series != old_series and variant.series == new_series:
-                    if variant.series.issue_count > 0:
-                        variant.series.issue_count -= 1
-                    variant.series.save()
+            old_series_id = old_series.id if old_series else None
+            new_series_id = self.issue.series_id
+            for variant in variants:
+                # 1. Variant left behind: Goes from Standard -> Cross-Series (+1)
+                if variant.series_id == old_series_id and variant.series_id != new_series_id:
+                    old_series.issue_count += 1
+                    old_series.save()
+                    
+                # 2. Base issue returns: Goes from Cross-Series -> Standard (-1)
+                elif variant.series_id != old_series_id and variant.series_id == new_series_id:
+                    if new_series.issue_count > 0:
+                        new_series.issue_count -= 1
+                    new_series.save()

In apps/gcd/views/details.py:

> @@ -2292,6 +2292,7 @@ def series_overview(request, series_id):
         'item_name': 'issue',
         'plural_suffix': 's',
         'heading': heading,
+        'json_download_url': '/api/series/%d/overview/?format=json' % series_id,

medium

Hardcoded URL Path

Avoid hardcoding the API URL path. Instead, name the route in apps/api/urls.py and use Django's reverse utility to dynamically generate the URL. This prevents broken links if the API routing structure changes in the future.

⬇️ Suggested change
-        'json_download_url': '/api/series/%d/overview/?format=json' % series_id,
+        'json_download_url': urlresolvers.reverse('series-overview-list', kwargs={'series_id': series_id}) + '?format=json',

In apps/api/urls.py:

> +    path('series/<int:series_id>/overview/',
+         views.SeriesOverviewList.as_view()),

medium

Missing Route Name

Add a name parameter to the path to allow reversing the URL dynamically using reverse().

    path('series/<int:series_id>/overview/',
         views.SeriesOverviewList.as_view(),
         name='series-overview-list'),

In templates/gcd/search/tw_list_sortable.html:

> +    <btn class="btn btn-blue-editing hover:bg-stone-200">
+      <div class="flex justify-center">
+        Download:
+        <a class="hover:bg-stone-400" href="{{ json_download_url }}"><span class="px-1">(JSON)</span></a>
+      </div>
+    </btn>

medium

Invalid HTML Nesting & Non-standard Tag

  1. Non-standard Tag: <btn> is not a standard HTML element. Use <a> or <button> instead.
  2. Invalid Nesting: Nesting an <a> tag inside a button-like element is invalid HTML because both are interactive elements. This can cause click events to fail or behave inconsistently across browsers.

Style the <a> tag directly as a button to keep the HTML valid and clean.

  <div class="me-1">
    <a class="btn btn-blue-editing hover:bg-stone-200 flex justify-center" href="{{ json_download_url }}">
      Download: <span class="px-1 font-semibold">(JSON)</span>
    </a>
  </div>


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/review/4492059760@github.com>

jhunterjActual

unread,
Jun 13, 2026, 6:39:22 PMJun 13
to GrandComicsDatabase/gcd-django, Push

@jhunterjActual pushed 1 commit.

  • 32bb4bb Incorporate Gemini Code Assist feedback


View it on GitHub or unsubscribe.


Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/before/13a6ffc90cf2a4540c067a26690b2f8e1442c26d/after/32bb4bb4f92d7c829cb658153508a5fab7bfd5aa@github.com>

jhunterjActual

unread,
Jun 13, 2026, 6:40:01 PMJun 13
to GrandComicsDatabase/gcd-django, Subscribed

@jhunterjActual commented on this pull request.


In templates/gcd/search/tw_list_sortable.html:

> +    <btn class="btn btn-blue-editing hover:bg-stone-200">
+      <div class="flex justify-center">
+        Download:
+        <a class="hover:bg-stone-400" href="{{ json_download_url }}"><span class="px-1">(JSON)</span></a>
+      </div>
+    </btn>

Incorporated


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/review/4492122305@github.com>

JochenGCD

unread,
Jun 20, 2026, 5:26:21 AM (8 days ago) Jun 20
to GrandComicsDatabase/gcd-django, Subscribed
jochengcd left a comment (GrandComicsDatabase/gcd-django#718)

There is now a conflict with beta that needs to be resolved.

Did you address the gemini comments ? Without replying to these I can't easily what was done.


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/c4757155118@github.com>

jhunterjActual

unread,
Jun 20, 2026, 9:28:44 AM (8 days ago) Jun 20
to GrandComicsDatabase/gcd-django, Push

@jhunterjActual pushed 1 commit.

  • 511764d Merge branch 'beta' into bugfix/issue-619-JSON-export-series-overview


View it on GitHub or unsubscribe.


Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/before/32bb4bb4f92d7c829cb658153508a5fab7bfd5aa/after/511764d8a193dc74b2227c199db40ac42d2b8998@github.com>

jhunterjActual

unread,
Jun 20, 2026, 9:30:42 AM (8 days ago) Jun 20
to GrandComicsDatabase/gcd-django, Subscribed
jhunterjActual left a comment (GrandComicsDatabase/gcd-django#718)

Sorry, forgot to mark the "resolves" when I committed the Gemini feedback changes earlier. I also fixed the line lengths when resolving the conflict just now.


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/c4758225155@github.com>

jhunterjActual

unread,
Jun 20, 2026, 9:31:50 AM (8 days ago) Jun 20
to GrandComicsDatabase/gcd-django, Subscribed
jhunterjActual left a comment (GrandComicsDatabase/gcd-django#718)

The changes for the Gemini comments: 32bb4bb


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/c4758235114@github.com>

JochenGCD

unread,
Jun 26, 2026, 1:43:25 PM (2 days ago) Jun 26
to GrandComicsDatabase/gcd-django, Subscribed
jochengcd left a comment (GrandComicsDatabase/gcd-django#718)

I didn't mean the resolve status, but also what was done. For my comments, I know the context. But the gemini comments I don't necessarily follow and don't see what was done afterwards. Which means I have to look at it from scratch, which takes time and effort.


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/c4811955079@github.com>

JochenGCD

unread,
Jun 26, 2026, 1:54:50 PM (2 days ago) Jun 26
to GrandComicsDatabase/gcd-django, Subscribed
jochengcd left a comment (GrandComicsDatabase/gcd-django#718)

I don't think that we should add issue overview for creators or features to the existing API at this time. There is current work on an v2 of the API, so any API additions should be considered with that in mind.


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/c4812046753@github.com>

JochenGCD

unread,
Jun 26, 2026, 1:55:01 PM (2 days ago) Jun 26
to GrandComicsDatabase/gcd-django, Subscribed

Merged #718 into beta.


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <GrandComicsDatabase/gcd-django/pull/718/issue_event/27257682618@github.com>

Reply all
Reply to author
Forward
0 new messages