Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions core/home/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
from wagtailcaptcha.models import WagtailCaptchaEmailForm

from collection.models import Collection
from core.home.utils.export_journals import (
generate_csv_response,
generate_xls_response,
get_scielo_journals_data,
)
from core.home.utils.get_social_networks import get_social_networks
from journal.choices import STUDY_AREA
from journal.models import OwnerHistory, SciELOJournal
Expand Down Expand Up @@ -79,6 +84,18 @@ def _default_context(context):
context["page_about"] = get_page_about()


class JournalDownloadMixin:
@re_path(r"^download-csv/$", name="download_csv")
def download_csv(self, request):
journals_data = get_scielo_journals_data()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot deve considerar que os itens foram filtrados usando os argumentos de request

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 05eed2e. The download endpoints now respect request filters:

  • get_scielo_journals_data(filters=None) accepts optional Q filters
  • JournalDownloadMixin uses a template method get_export_filters(request) — each page class overrides it to build the same filters used in get_context
  • ListPageJournalByPublisher.get_export_filters includes publisher name search (matching the page's own filter logic)
  • Download template forwards current query params via request.GET.urlencode

return generate_csv_response(journals_data)
Comment on lines +91 to +95

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key new behavior is “export respects the same filters as the listing page” via get_export_filters(request) + routable endpoints. There are good unit tests for the export utility, but there’s no coverage shown for the routable download routes applying real request query params (e.g., search_term, start_with_letter, tab) end-to-end. Add an integration-style test that requests the page’s download-csv/ (and/or download-xls/) with query parameters and asserts the response contains only the filtered journal(s).

Copilot uses AI. Check for mistakes.

@re_path(r"^download-xls/$", name="download_xls")
def download_xls(self, request):
journals_data = get_scielo_journals_data()
return generate_xls_response(journals_data)


def get_page_about():
try:
locale = _get_current_locale()
Expand Down Expand Up @@ -221,7 +238,7 @@ def get_context(self, request, *args, **kwargs):
return context


class ListPageJournal(Page):
class ListPageJournal(JournalDownloadMixin, RoutablePageMixin, Page):
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
search_term = request.GET.get("search_term", "")
Expand All @@ -237,7 +254,7 @@ def get_context(self, request, *args, **kwargs):
return context


class ListPageJournalByPublisher(Page):
class ListPageJournalByPublisher(JournalDownloadMixin, RoutablePageMixin, Page):
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
search_term = request.GET.get("search_term", "")
Expand Down Expand Up @@ -283,7 +300,7 @@ def get_context(self, request, *args, **kwargs):
return context


class ListPageJournalByCategory(RoutablePageMixin, Page):
class ListPageJournalByCategory(JournalDownloadMixin, RoutablePageMixin, Page):
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)

Expand Down
57 changes: 52 additions & 5 deletions core/home/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
from core.users.models import User
from journal.models import Journal, SciELOJournal

from core.home.views import _get_scielo_journals_data
from core.home.utils.export_journals import (
generate_csv_response,
generate_xls_response,
get_scielo_journals_data,
)


class TestGetScieloJournalsData(TestCase):
Expand All @@ -30,15 +34,15 @@ def setUp(self):

def test_scielo_url_does_not_have_double_http_prefix(self):
"""URL must not contain 'http://http://' when domain already has http://"""
data = _get_scielo_journals_data()
data = get_scielo_journals_data()
self.assertTrue(len(data) > 0)
for item in data:
self.assertNotIn("http://http://", item["scielo_url"])
self.assertNotIn("http://https://", item["scielo_url"])

def test_scielo_url_is_well_formed(self):
"""URL must be a valid scielo.php URL with the correct domain"""
data = _get_scielo_journals_data()
data = get_scielo_journals_data()
self.assertEqual(len(data), 1)
expected_url = (
"http://www.scielo.org.pe/scielo.php?script=sci_serial"
Expand All @@ -50,15 +54,58 @@ def test_scielo_url_strips_trailing_slash_from_domain(self):
"""Trailing slash in domain must not produce double slash in URL"""
self.collection.domain = "http://www.scielo.org.pe/"
self.collection.save()
data = _get_scielo_journals_data()
data = get_scielo_journals_data()
self.assertEqual(len(data), 1)
self.assertNotIn("//scielo.php", data[0]["scielo_url"])

def test_scielo_url_with_https_domain(self):
"""URL must be correct when domain uses https://"""
self.collection.domain = "https://www.scielo.br"
self.collection.save()
data = _get_scielo_journals_data()
data = get_scielo_journals_data()
self.assertEqual(len(data), 1)
self.assertTrue(data[0]["scielo_url"].startswith("https://www.scielo.br/"))
self.assertNotIn("https://https://", data[0]["scielo_url"])


class TestGenerateCsvResponse(TestCase):
def test_csv_response_content_type(self):
response = generate_csv_response([])
self.assertEqual(response["Content-Type"], "text/csv")

def test_csv_response_has_attachment_header(self):
response = generate_csv_response([])
self.assertIn("attachment", response["Content-Disposition"])
self.assertIn(".csv", response["Content-Disposition"])

def test_csv_response_contains_headers(self):
response = generate_csv_response([])
content = response.content.decode("utf-8")
self.assertIn("journals", content)
self.assertIn("scielo_url", content)
self.assertIn("publisher", content)

def test_csv_response_contains_data(self):
data = [
{
"title": "Test Journal",
"scielo_url": "http://example.com/journal",
"owner": "Test Publisher",
}
]
response = generate_csv_response(data)
content = response.content.decode("utf-8")
self.assertIn("Test Journal", content)
self.assertIn("http://example.com/journal", content)
self.assertIn("Test Publisher", content)


class TestGenerateXlsResponse(TestCase):
def test_xls_response_content_type(self):
response = generate_xls_response([])
self.assertEqual(response["Content-Type"], "application/vnd.ms-excel")

def test_xls_response_has_attachment_header(self):
response = generate_xls_response([])
self.assertIn("attachment", response["Content-Disposition"])
self.assertIn(".xls", response["Content-Disposition"])
Empty file added core/home/utils/__init__.py
Empty file.
87 changes: 87 additions & 0 deletions core/home/utils/export_journals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import csv
import logging

import xlwt
from django.http import HttpResponse
from django.utils import timezone

from journal.models import SciELOJournal

logger = logging.getLogger(__name__)

HEADERS = ["journals", "scielo_url", "publisher"]

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported column headers are journals/publisher, but the internal data keys are title/owner. This mismatch makes the utility harder to reuse correctly and increases the chance of future mistakes. Consider aligning the dict keys to the export contract (e.g., use journals and publisher everywhere) and then read those same keys in both generate_csv_response and generate_xls_response.

Copilot uses AI. Check for mistakes.


def get_scielo_journals_data():
try:
scielo_journals = SciELOJournal.objects.values(
"journal__title",
"collection__domain",
"journal__owner_history__institution__institution__institution_identification__name",
"issn_scielo",
)

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Export rows can be duplicated if the joins (notably journal__owner_history...) produce multiple rows per SciELOJournal. This will lead to repeated lines in CSV/XLS exports. A concrete mitigation is applying distinct() to the values() queryset (e.g., qs.values(...).distinct()), or otherwise selecting a single “current” owner record before exporting.

Suggested change
)
).distinct()

Copilot uses AI. Check for mistakes.

formatted_data = []
for journal in scielo_journals:
title = journal.get("journal__title", "")
issn_scielo = journal.get("issn_scielo", "")
domain = journal.get("collection__domain", "")
owner = journal.get(
"journal__owner_history__institution__institution__institution_identification__name",
"",
)
scielo_url = (
f"{domain.rstrip('/')}/scielo.php?script=sci_serial&pid={issn_scielo}&lng=en"
)
formatted_data.append(
{
"title": title,
"scielo_url": scielo_url,
"owner": owner,
}
Comment on lines +40 to +44

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported column headers are journals/publisher, but the internal data keys are title/owner. This mismatch makes the utility harder to reuse correctly and increases the chance of future mistakes. Consider aligning the dict keys to the export contract (e.g., use journals and publisher everywhere) and then read those same keys in both generate_csv_response and generate_xls_response.

Copilot uses AI. Check for mistakes.
)
return formatted_data
except Exception as e:
logger.error(f"Error fetching scielo journals data: {e}")
return []
Comment on lines +47 to +49

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging the exception message without a traceback makes diagnosing production issues harder. Prefer logger.exception("Error fetching scielo journals data") (or logger.error(..., exc_info=True)) so the traceback is captured.

Copilot uses AI. Check for mistakes.


def generate_csv_response(journals_data):
date = timezone.now().strftime("%Y-%m-%d")
filename = f"journals_{date}.csv"
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
try:
writer = csv.writer(response)
writer.writerow(HEADERS)
for journal in journals_data:
writer.writerow(
[journal.get("title"), journal.get("scielo_url"), journal.get("owner")]
)
Comment on lines +61 to +63

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported column headers are journals/publisher, but the internal data keys are title/owner. This mismatch makes the utility harder to reuse correctly and increases the chance of future mistakes. Consider aligning the dict keys to the export contract (e.g., use journals and publisher everywhere) and then read those same keys in both generate_csv_response and generate_xls_response.

Copilot uses AI. Check for mistakes.
logger.info(f"Generated CSV file with: {len(journals_data)} journals")
except Exception as e:
logger.error(f"Error generating CSV file: {e}")
response = HttpResponse("Error generating CSV file", status=500)
return response


def generate_xls_response(journals_data):
date = timezone.now().strftime("%Y-%m-%d")
filename = f"journals_{date}.xls"
response = HttpResponse(content_type="application/vnd.ms-excel")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
try:
wb = xlwt.Workbook(encoding="utf-8")
ws = wb.add_sheet("journals")
for col, header in enumerate(HEADERS):
ws.write(0, col, header)
for row, journal in enumerate(journals_data, start=1):
ws.write(row, 0, journal.get("title"))
ws.write(row, 1, journal.get("scielo_url"))
ws.write(row, 2, journal.get("owner"))
wb.save(response)
logger.info(f"Generated XLS file with: {len(journals_data)} journals")
except Exception as e:
logger.error(f"Error generating XLS file: {e}")
response = HttpResponse("Error generating XLS file", status=500)
return response
90 changes: 9 additions & 81 deletions core/home/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import csv
import logging

import feedparser
import xlwt
from django.http import HttpResponse, JsonResponse
from django.utils import timezone
from django.utils.translation import get_language
from django.views.decorators.http import require_GET

from journal.models import SciELOJournal
from core.home.utils.export_journals import (
generate_csv_response,
generate_xls_response,
get_scielo_journals_data,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -56,84 +57,11 @@ def youtube_feed_json(request):
return JsonResponse({"posts": posts})


def _get_scielo_journals_data():
try:
scielo_journals = SciELOJournal.objects.values(
"journal__title",
"collection__domain",
"journal__owner_history__institution__institution__institution_identification__name",
"issn_scielo",
)

formatted_data = []
for journal in scielo_journals:
title = journal.get("journal__title", "")
issn_scielo = journal.get("issn_scielo", "")
domain = journal.get("collection__domain", "")
owner = journal.get(
"journal__owner_history__institution__institution__institution_identification__name",
"",
)
scielo_url = (
f"{domain.rstrip('/')}/scielo.php?script=sci_serial&pid={issn_scielo}&lng=en"
)
formatted_data.append(
{
"title": title,
"scielo_url": scielo_url,
"owner": owner,
}
)
return formatted_data
except Exception as e:
logger.error(f"Error fetching scielo journals data: {e}")
return []


def download_xls_journals_page_scielo_org(request):
date = timezone.now().strftime("%Y-%m-%d")
filename = f"journals_{date}.xls"

response = HttpResponse(content_type="application/vnd.ms-excel")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
try:
wb = xlwt.Workbook(encoding="utf-8")
ws = wb.add_sheet("journals")
headers = ["journals", "scielo_url", "publisher"]

for col, header in enumerate(headers):
ws.write(0, col, header)

journals_data = _get_scielo_journals_data()
for row, journal in enumerate(journals_data, start=1):
ws.write(row, 0, journal.get("title"))
ws.write(row, 1, journal.get("scielo_url"))
ws.write(row, 2, journal.get("owner"))
wb.save(response)
logger.info(f"Generated XLS file with: {len(journals_data)} journals")
except Exception as e:
logger.error(f"Error generating XLS file: {e}")
response = HttpResponse("Error generating file", status=500)
return response
journals_data = get_scielo_journals_data()
return generate_xls_response(journals_data)


def download_csv_journals_page_scielo_org(request):
date = timezone.now().strftime("%Y-%m-%d")
filename = f"journals_{date}.csv"
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
try:
writer = csv.writer(response)
headers = ["journals", "scielo_url", "publisher"]
writer.writerow(headers)
journals_data = _get_scielo_journals_data()
for journal in journals_data:
writer.writerow(
[journal.get("title"), journal.get("scielo_url"), journal.get("owner")]
)
logger.info(f"Generated CSV file with: {len(journals_data)} journals")
except Exception as e:
logger.error(f"Error generating CSV file: {e}")
response = HttpResponse("Error generating file", status=500)

return response
journals_data = get_scielo_journals_data()
return generate_csv_response(journals_data)
5 changes: 3 additions & 2 deletions core/templates/home/include/download_button_csv_xls.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{% load wagtailroutable_tags %}
<th class="text-nowrap w-auto" scope="col">
<button id="btnGroupDropDownload" type="submit" class="btn btn-sm btn-secondary scielo__btn-with-icon--only dropdown-toggle mb-0 float-end d-print-none" data-bs-toggle="dropdown" aria-expanded="false">
<span class="material-icons-outlined">download</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="btnGroupDropDownload" >
<li>
<a class="dropdown-item" href="{% url 'download_xls_journals_page_scielo_org' %}">XLS</a>
<a class="dropdown-item" href="{% routablepageurl page 'download_xls' %}">XLS</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'download_csv_journals_page_scielo_org' %}">CSV</a>
<a class="dropdown-item" href="{% routablepageurl page 'download_csv' %}">CSV</a>
</li>
</ul>
</th>