Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7c0dad1
Moderniza a administração de tarefas periódicas no Wagtail
pitangainnovare Sep 21, 2026
deb1d1b
Corrige a contabilização de acessos a livros e capítulos
pitangainnovare Sep 21, 2026
f9198d6
Permite reconstruir integralmente os metadados de uso
pitangainnovare Sep 21, 2026
6b692e0
Ajusta a criação do cliente do OpenSearch
pitangainnovare Sep 21, 2026
48eb750
Adiciona dependências da interface COUNTER
pitangainnovare Sep 22, 2026
4c64a0f
Prepara a aplicação COUNTER
pitangainnovare Sep 22, 2026
b0407af
Adiciona chaves de acesso à interface COUNTER
pitangainnovare Sep 22, 2026
f07916d
Valida períodos e exceções dos relatórios COUNTER
pitangainnovare Sep 22, 2026
3660fa4
Define os contratos dos relatórios COUNTER
pitangainnovare Sep 22, 2026
ccc1f1b
Normaliza identificadores dos relatórios COUNTER
pitangainnovare Sep 22, 2026
d91ac7e
Valida parâmetros dos relatórios COUNTER
pitangainnovare Sep 22, 2026
b063ba4
Registra a conclusão mensal no OpenSearch
pitangainnovare Sep 22, 2026
05db63f
Marca dias concluídos após a exportação
pitangainnovare Sep 22, 2026
a0587c8
Registra os meses da migração histórica
pitangainnovare Sep 22, 2026
bae0273
Reconstrói a disponibilidade mensal
pitangainnovare Sep 22, 2026
533d648
Controla a cobertura temporal dos relatórios COUNTER
pitangainnovare Sep 22, 2026
47f4071
Protege consultas ao OpenSearch
pitangainnovare Sep 22, 2026
35197ed
Carrega metadados dos relatórios em lotes
pitangainnovare Sep 22, 2026
86775b9
Consulta as métricas dos relatórios COUNTER
pitangainnovare Sep 22, 2026
848339e
Inclui o esquema oficial da COUNTER Release 5.1
pitangainnovare Sep 22, 2026
1d6e32d
Transforma agregações em entradas COUNTER
pitangainnovare Sep 22, 2026
dfe6f57
Organiza títulos e itens dos relatórios COUNTER
pitangainnovare Sep 22, 2026
b8d6c39
Monta cabeçalhos e resultados dos relatórios COUNTER
pitangainnovare Sep 22, 2026
e11ccbc
Protege a integridade das métricas de livros
pitangainnovare Sep 22, 2026
ea9ac8f
Orquestra a geração dos relatórios COUNTER
pitangainnovare Sep 22, 2026
ccb320e
Gera relatórios COUNTER em formatos tabulares
pitangainnovare Sep 22, 2026
a37fffe
Descreve os contratos HTTP da interface COUNTER
pitangainnovare Sep 22, 2026
143de0b
Isola o acesso HTTP da interface COUNTER
pitangainnovare Sep 22, 2026
d219e6c
Expõe os recursos HTTP da interface COUNTER
pitangainnovare Sep 22, 2026
b1b3929
Remove chaves COUNTER dos eventos do Sentry
pitangainnovare Sep 22, 2026
27aa361
Adiciona sincronização dos metadados COUNTER
pitangainnovare Sep 22, 2026
5b0eb6d
Adiciona preenchimento do ano de publicação
pitangainnovare Sep 22, 2026
b201aa1
Atualiza a identificação da lista de robôs COUNTER
pitangainnovare Sep 22, 2026
4cecd6c
Define contratos das extensões de uso
pitangainnovare Sep 22, 2026
b33e4a0
Adiciona rankings às extensões de uso
pitangainnovare Sep 22, 2026
44f9286
Adiciona distribuições às extensões de uso
pitangainnovare Sep 22, 2026
56f6a7c
Exporta análises em CSV e XLSX
pitangainnovare Sep 22, 2026
e5f2bb6
Descreve os contratos das extensões de uso
pitangainnovare Sep 22, 2026
7a8e57f
Publica as extensões de uso
pitangainnovare Sep 22, 2026
4520d3f
Publica as rotas e a documentação COUNTER
pitangainnovare Sep 22, 2026
4c66733
Preserva o tipo de conteúdo no relatório de plataforma
pitangainnovare Sep 22, 2026
c49a8cf
Elimina repetições nas extensões de relatórios
pitangainnovare Sep 22, 2026
88e6abf
Exclui arquivos de mídia das imagens Docker
pitangainnovare Sep 22, 2026
33ac214
Distingue a granularidade JSON da exportação tabular
pitangainnovare Sep 22, 2026
cace941
Exige cobertura mensal completa nas análises estendidas
pitangainnovare Sep 22, 2026
fb1d9a8
Impede duplicidade de dias vazios na migração histórica
pitangainnovare Sep 22, 2026
36a05e9
Atualiza a versão da aplicação para 2.6.0
pitangainnovare Sep 22, 2026
5a073c7
Integra as mudanças recentes da aplicação principal
pitangainnovare Sep 22, 2026
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
.travis.yml
.continue
.envs/.local/.django
core/media
temp
venv
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.5.2
2.6.0
28 changes: 28 additions & 0 deletions config/sentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from urllib.parse import parse_qsl, urlsplit, urlunsplit


def scrub_api_key(event, _hint):
request = event.get("request") or {}
query_string = request.get("query_string") or ""
url = request.get("url") or ""
url_query = urlsplit(url).query if url else ""

if not _contains_api_key(query_string) and not _contains_api_key(url_query):
return event

request["query_string"] = ""
if url:
parts = urlsplit(url)
request["url"] = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))

return event


def _contains_api_key(query_string):
if not isinstance(query_string, str):
return False

return any(
name.lower() == "api_key"
for name, _value in parse_qsl(query_string, keep_blank_values=True)
)
48 changes: 44 additions & 4 deletions config/settings/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Base settings to build other settings files upon.
"""

from datetime import timedelta
from pathlib import Path

Expand Down Expand Up @@ -93,10 +94,8 @@
"core.search_site",
"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail_modeladmin",
"wagtail.contrib.simple_translation",
"wagtail_localize",
"wagtail_localize.modeladmin",
"wagtail.embeds",
"wagtail.sites",
"wagtail.users",
Expand Down Expand Up @@ -139,6 +138,7 @@
"wagtailcaptcha",
"wagtailmenus",
"rest_framework",
"drf_spectacular",
]

LOCAL_APPS = [
Expand All @@ -151,6 +151,7 @@
"log_manager",
"log_manager_config",
"metrics",
"counter_api",
"reports",
"resources",
"source",
Expand Down Expand Up @@ -457,11 +458,38 @@
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": env.int("DRF_PAGE_SIZE", default=10),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}

SPECTACULAR_SETTINGS = {
"TITLE": "SciELO COUNTER API",
"DESCRIPTION": (
"COUNTER Project Release 5.1 API with SciELO extensions. "
"Report availability follows completed months in OpenSearch."
),
"VERSION": "5.1",
"SERVE_INCLUDE_SCHEMA": False,
"SERVE_AUTHENTICATION": [],
"SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
"SWAGGER_UI_SETTINGS": {
"deepLinking": True,
"displayOperationId": True,
"persistAuthorization": False,
},
"APPEND_COMPONENTS": {
"securitySchemes": {
"CounterAPIKey": {
"type": "apiKey",
"in": "query",
"name": "api_key",
}
}
},
}

# JWT
# ------------------------------------------------------------------------------
SIMPLE_JWT = {
Expand Down Expand Up @@ -503,10 +531,22 @@
COUNTER_ROBOTS_URL = env(
"COUNTER_ROBOTS_URL",
default=(
"https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/"
"COUNTER_Robots_list.json"
"https://raw.githubusercontent.com/Project-Counter/counter-bots/"
"counter-bots/counter_bots.json"
),
)
COUNTER_REGISTRY_URL = env("COUNTER_REGISTRY_URL", default="")
COUNTER_CUSTOMER_ID = env("COUNTER_CUSTOMER_ID", default="0000000000000000")
COUNTER_INSTITUTION_NAME = env("COUNTER_INSTITUTION_NAME", default="The World")
COUNTER_CREATED_BY = env("COUNTER_CREATED_BY", default="SciELO")
COUNTER_QUERY_BUDGET_SECONDS = env.int(
"COUNTER_QUERY_BUDGET_SECONDS",
default=25,
)
COUNTER_MAX_REPORT_ENTRIES = env.int(
"COUNTER_MAX_REPORT_ENTRIES",
default=100000,
)
MMDB_URL_TEMPLATE = env(
"MMDB_URL_TEMPLATE",
default=(
Expand Down
8 changes: 7 additions & 1 deletion config/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.redis import RedisIntegration

from config.sentry import scrub_api_key

from .base import * # noqa
from .base import env

Expand Down Expand Up @@ -86,7 +88,9 @@
EMAIL_PORT = env.int("DJANGO_EMAIL_PORT", default=25)

# https://docs.djangoproject.com/en/dev/ref/settings/#email-host-user
EMAIL_HOST_USER = env.str("DJANGO_EMAIL_HOST_USER", default="suporte.aplicacao@scielo.org")
EMAIL_HOST_USER = env.str(
"DJANGO_EMAIL_HOST_USER", default="suporte.aplicacao@scielo.org"
)

# https://docs.djangoproject.com/en/dev/ref/settings/#std-setting-EMAIL_HOST_PASSWORD
EMAIL_HOST_PASSWORD = env.str("DJANGO_EMAIL_HOST_PASSWORD", default="")
Expand Down Expand Up @@ -197,6 +201,8 @@
integrations=integrations,
environment=env("SENTRY_ENVIRONMENT", default="production"),
traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.0),
before_send=scrub_api_key,
before_send_transaction=scrub_api_key,
)

# Redis
Expand Down
8 changes: 3 additions & 5 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path, re_path
from django.views import defaults as default_views
from wagtail import urls as wagtail_urls
Expand All @@ -10,6 +11,7 @@
from wagtailautocomplete.urls.admin import urlpatterns as autocomplete_admin_urls

urlpatterns = [
path("r51/", include("counter_api.urls")),
path("admin/autocomplete/", include(autocomplete_admin_urls)),
path(settings.DJANGO_ADMIN_URL, admin.site.urls),
# Wagtail Admin
Expand All @@ -30,8 +32,6 @@

if settings.DEBUG:
# Wagtail settings: Serve static and media files from development server
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# This allows the error pages to be debugged during development, just visit
Expand All @@ -55,6 +55,4 @@
path("500/", default_views.server_error),
]
if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar

urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns
urlpatterns = [path("__debug__/", include("debug_toolbar.urls"))] + urlpatterns
Empty file added counter_api/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions counter_api/access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView

from counter_api.exceptions import CounterAPIError
from counter_api.models import APIKey
from counter_api.negotiation import CounterContentNegotiation


class CounterAPIView(APIView):
authentication_classes = []
permission_classes = []
renderer_classes = [JSONRenderer]
content_negotiation_class = CounterContentNegotiation
http_method_names = ["get"]


def get_authentication_error(request):
key = APIKey.authenticate(request.query_params.get("api_key"))
if key:
return None

error = CounterAPIError(2020, "APIKey Invalid", 401)

return Response(error.as_dict(), status=error.status_code)
21 changes: 21 additions & 0 deletions counter_api/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.contrib import admin

from counter_api.models import APIKey


@admin.register(APIKey)
class APIKeyAdmin(admin.ModelAdmin):
list_display = (
"name",
"active",
"expires_at",
"last_used_at",
"created_at",
)
list_filter = ("active",)
search_fields = ("name",)
fields = ("name", "active", "expires_at", "last_used_at", "created_at")
readonly_fields = ("name", "last_used_at", "created_at")

def has_add_permission(self, request):
return False
11 changes: 11 additions & 0 deletions counter_api/api_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import path

from counter_api import views

urlpatterns = [
path("status", views.StatusView.as_view(), name="status"),
path("members", views.MembersView.as_view(), name="members"),
path("platforms", views.PlatformsView.as_view(), name="platforms"),
path("reports", views.ReportsView.as_view(), name="reports"),
path("reports/<str:report_id>", views.ReportView.as_view(), name="report"),
]
6 changes: 6 additions & 0 deletions counter_api/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class CounterAPIConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "counter_api"
79 changes: 79 additions & 0 deletions counter_api/availability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from datetime import date
from time import monotonic

from django.conf import settings
from opensearchpy import NotFoundError

from counter_api.dates import iter_months
from counter_api.search import search
from metrics.opensearch.client import OpenSearchUsageClient
from metrics.opensearch.names import generate_month_status_index_name

PAGE_SIZE = 1000


def available_months(platform):
index_name = generate_month_status_index_name(settings.OPENSEARCH_INDEX_NAME)
client = OpenSearchUsageClient().client
deadline = monotonic() + settings.COUNTER_QUERY_BUDGET_SECONDS
current_month = date.today().strftime("%Y-%m")
months = []
after = None

while True:
body = {
"size": 0,
"track_total_hits": False,
"query": {
"bool": {
"filter": [
{"term": {"collection": platform.acron3}},
{"term": {"complete": True}},
{"range": {"month": {"lt": current_month}}},
]
}
},
"aggs": {
"months": {
"composite": {
"size": PAGE_SIZE,
"sources": [{"month": {"terms": {"field": "month"}}}],
}
}
},
}
if after:
body["aggs"]["months"]["composite"]["after"] = after

try:
response = search(client, index_name, body, deadline)
except NotFoundError:
return []

page = response["aggregations"]["months"]
months.extend(
date.fromisoformat(f"{bucket['key']['month']}-01")
for bucket in page["buckets"]
)
after = page.get("after_key")
if not after:
return months


def split_period(platform, begin, end):
requested = list(iter_months(begin, end))
complete = set(available_months(platform))

if not complete:
return [], requested, []

first_available = min(complete)
available = [month for month in requested if month in complete]
unavailable = [month for month in requested if month < first_available]
pending = [
month
for month in requested
if month not in complete and month not in unavailable
]

return available, pending, unavailable
Loading
Loading